mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 17:02:28 +00:00
fleet update
This commit is contained in:
1 parent
6495e0618c
commit
b8e87e655b
11 files changed
+222
-19
No files matched your search
@@ -0,0 +1,8 @@
|
||||
# TODO — what each area did NOT finish
|
||||
|
||||
One section per area. Each session updates its own section before it stops: what is
|
||||
half-done, where it is, and what the next step is. Newest first inside a section.
|
||||
|
||||
## <area id>
|
||||
|
||||
- YYYY-MM-DD: <what is unfinished> — <where (branch, file, SHA)> — next: <step>
|
||||
@@ -0,0 +1,32 @@
|
||||
# Program — the owner's priorities and rulings
|
||||
|
||||
Kept by the session manager. Short on purpose: the backlog holds the tasks, RESUME-BRIEF.md
|
||||
what is mid-flight. Every ruling is DATED and QUOTES the owner; session-manager calls are
|
||||
marked vetoable.
|
||||
|
||||
Last updated: YYYY-MM-DD
|
||||
|
||||
## Current milestone
|
||||
|
||||
TODO: one paragraph.
|
||||
|
||||
## Priorities (owner's order)
|
||||
|
||||
1. TODO
|
||||
2. TODO
|
||||
|
||||
## Rulings log (newest first)
|
||||
|
||||
<!-- Format:
|
||||
### YYYY-MM-DD HH:MM — <short title>
|
||||
> "<the owner's exact words>" — owner
|
||||
Applies to: <areas>. Recorded by: <session>.
|
||||
|
||||
### YYYY-MM-DD HH:MM — <short title> (session manager call, VETOABLE)
|
||||
What was decided and why, in two lines. The owner overrides by saying so.
|
||||
-->
|
||||
|
||||
## Standing rules
|
||||
|
||||
- Approvals for spend, publishing and deploys are typed by the owner in the session that acts.
|
||||
- Agreed work is never silently dropped: a priority change defers it; only the owner cancels, with a reason.
|
||||
@@ -0,0 +1,23 @@
|
||||
# RESUME BRIEF — written YYYY-MM-DD HH:MM (<why the fleet stopped>)
|
||||
|
||||
Written by the dispatcher at every wind-down; read first by every session at restart.
|
||||
|
||||
## First reads on restart
|
||||
|
||||
1. `git log --oneline <main> -20` — what landed while the fleet was dark.
|
||||
2. Remove `<stateDir>/PAUSED` (the restart job does this).
|
||||
3. TODO: anything scheduled to run while dark, and where its output is.
|
||||
|
||||
## Mid-flight (per area)
|
||||
|
||||
| area | item | state | SHA / branch | next step |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| TODO | | | | |
|
||||
|
||||
## Waiting on the owner
|
||||
|
||||
- TODO
|
||||
|
||||
## Held (do not land until)
|
||||
|
||||
- TODO
|
||||
@@ -28,7 +28,7 @@ Bring these to the user, and do not approve them yourself: money, a push, a depl
|
||||
|
||||
## Status
|
||||
|
||||
When the user asks how things are going, answer in plain language: how many agents, which areas, what each is working on, what landed, and what needs the user. `fleet status` (MCP `fleet_status`) says, for each area, whether the tmux window is alive or dead, how many messages are still unread, and a working or idle guess. Idle with unread mail means that agent has not read it. Looking at status nudges that pane once if the mail has been waiting. A second look does not nudge again. Read `hear`, `fleet/LOG.md`, the landing queue, and `git log`. Offer a dev-server preview when that would help them see the work.
|
||||
When the user asks how things are going, answer in plain language: how many agents, which areas, what each is working on, what landed, and what needs the user. `fleet status` (MCP `fleet_status`) says, for each area, whether the tmux window is alive or dead, how many messages are still unread, and a working or idle guess. Idle with unread mail means that agent has not read it. Looking at status nudges that pane once if the mail has been waiting. A second look does not nudge again. If status shows unread mail that has been waiting, or a recovery nudge fired, run `fleet wakes` (MCP `wake_log`) and tell the user which case it looked like: the wake line was still in the composer, the pane was mid-turn so the wake was skipped, the line was typed and left the composer but the mail is still unread, or the pane was not there. Do not ask the user to open the log. Read `hear`, `fleet/LOG.md`, the landing queue, and `git log`. Offer a dev-server preview when that would help them see the work.
|
||||
|
||||
## Update the kit
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ The human says what they want in plain words. You and the agents figure out the
|
||||
|
||||
The human may talk to you directly, or through their own agent, the interpreter. Messages from `interpreter` are the user. A line in this window that says you have new fleet mail is only a wake. Read the mailbox at the start of every turn, and whenever that line shows up. Reading takes the messages out of unread. The mail is the message. Reply by mailbox (`mailbox send interpreter --from {{area.id}}`, or MCP `mailbox_send`) with your next action. Do that once.
|
||||
|
||||
At the start of every turn, run `fleet status` (MCP `fleet_status`). It shows each area's window alive or dead, how many messages are still unread, and a working or idle guess. Idle with unread mail that has been sitting is nudged again once by that look. A second look does not nudge again. Reading your mailbox runs that same check. There is no ack protocol and no background checker.
|
||||
At the start of every turn, run `fleet status` (MCP `fleet_status`). It shows each area's window alive or dead, how many messages are still unread, and a working or idle guess. Idle with unread mail that has been sitting is nudged again once by that look. A second look does not nudge again. Reading your mailbox runs that same check. There is no ack protocol and no background checker. If that look shows unread mail that has been waiting, or it nudges a pane again, run `fleet wakes` (MCP `wake_log`) and tell the user which case it looked like: the wake line was still in the composer, the pane was mid-turn so the wake was skipped, the line was typed and left the composer but the mail is still unread, or the pane was not there. Do not ask them to open the log.
|
||||
|
||||
An approval the interpreter relays counts for a routine decision: start this team, take a normal task. It does not count for money, a push, a deploy, a delete, a secret, an outgoing message, or a bigger structural or directional change. Those go to the real user. If you are unsure which it is, ask the interpreter to take it to the user. Do not ask the user to edit a file, attach to tmux, or run a command.
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Rule changes
|
||||
|
||||
Newest first. One `## YYYY-MM-DD` heading per day, then a line per change.
|
||||
Read this file newest first now and before every backlog item. These lines
|
||||
override a stale system prompt. Nothing filters them down to a window of days.
|
||||
|
||||
The session manager adds a line here whenever a rule, a prompt, a tool's usage, or
|
||||
a landing rule changes.
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
"docs/agents/schedules/winddown.md": "36429467ce62e1f26a7eb6c2e6111ecd490c18bf691ee3fdb73c2fda71838d74",
|
||||
"docs/agents/WIND-DOWN.md": "b27ae51b0b8e739e0fcca736e4cb44dfdd38b71fff1dd4261ff4df18d575df3a",
|
||||
"docs/agents/STRIKES.md": "dd3047e02287207a1de96c01d5e9a1889c2c37cb75a5b3655de6e813df7f5bd9",
|
||||
"docs/agents/prompts/interpreter.md": "6e47ffe144c8699ab32452efc9da3af1c898e0341276e41c4ba6e296fb1483d0",
|
||||
"docs/agents/prompts/session-manager.md": "0e2bd1d22b27e3ed39b12a5a0f68366acca85c52c97eb63aefaaa97d33eb6b44",
|
||||
"docs/agents/prompts/interpreter.md": "1178348875ece2992ab12d5bdca6b50e728d35f47d84d9d204eaa163be36e453",
|
||||
"docs/agents/prompts/session-manager.md": "569835c908b11b9f5cdd96520f5bd93aed4327d109c8000b9977a4a1d9aa54bd",
|
||||
".agents/skills/agent-fleet/SKILL.md": "25d5aeef4361b3eecff525f752d990acd2ce8988b59f790ba8363eece8e646c7"
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { area as getArea, die, pidAlive, tryGit, git, mainRef, writeAtomic, today, landerIsUp, matchesAny, startLander, runTimedSync } from '../lib.mjs';
|
||||
import { send as mailboxSendFile, read as mailboxReadDir, archive as mailboxArchiveDir } from '../../adapters/mailbox.mjs';
|
||||
import { appendWakeLog, priorWakes } from './wake-log.mjs';
|
||||
|
||||
const STATUSES = ['open', 'in-progress', 'blocked', 'done', 'deferred'];
|
||||
const SPECIAL = ['owner', 'scheduler', 'interpreter'];
|
||||
@@ -119,13 +120,19 @@ function knownParty(c, id) {
|
||||
getArea(c, id);
|
||||
}
|
||||
|
||||
export function sendMailbox(c, { to, from, body, subject = '' } = {}) {
|
||||
export function sendMailbox(c, { to, from, body, subject = '', trigger = 'send' } = {}) {
|
||||
if (!from) die('--from <your id> required');
|
||||
knownParty(c, from);
|
||||
if (!body || !String(body).trim()) die('empty message');
|
||||
const targets = to === 'all' ? c.areas.map((a) => a.id).filter((id) => id !== from) : [to];
|
||||
const files = targets.map((t) => { knownParty(c, t); return mailboxSendFile(c.mailbox.dir, t, from, body, subject ?? ''); });
|
||||
const wakes = targets.filter((t) => t !== from).map((t) => ({ to: t, ...nudgeMail(c, t, from) }));
|
||||
const files = [];
|
||||
const wakes = [];
|
||||
for (const t of targets) {
|
||||
knownParty(c, t);
|
||||
const file = mailboxSendFile(c.mailbox.dir, t, from, body, subject ?? '');
|
||||
files.push(file);
|
||||
if (t !== from) wakes.push({ to: t, ...nudgeMail(c, t, from, { trigger, file: path.basename(file) }) });
|
||||
}
|
||||
return Object.assign(files, { wakes });
|
||||
}
|
||||
|
||||
@@ -193,7 +200,7 @@ export function setupCommit(repo, { message = 'fleet init' } = {}) {
|
||||
export function tellDispatch(c, { body, from = 'interpreter', subject = '' } = {}) {
|
||||
const hub = c.areas.find((a) => a.role === 'dispatcher');
|
||||
if (!hub) die('no session manager (role dispatcher)');
|
||||
const files = sendMailbox(c, { to: hub.id, from, body, subject });
|
||||
const files = sendMailbox(c, { to: hub.id, from, body, subject, trigger: 'tell' });
|
||||
const wake = (files.wakes || []).find((w) => w.to === hub.id) || { submitted: false, reason: 'no-window' };
|
||||
const status = fleetStatus(c);
|
||||
return { files: [...files], to: hub.id, from, nudged: Boolean(wake.submitted), wake, recovered: status.recovered };
|
||||
@@ -243,6 +250,25 @@ function stillInInput(target, line) {
|
||||
return cursorRow(target, capturePane(target)).includes(line);
|
||||
}
|
||||
|
||||
function inspectPane(target) {
|
||||
if (tmuxText(['list-panes', '-t', target]).status !== 0) return { found: false, dead: false, interrupt: false, idleSec: null, pane: '' };
|
||||
const info = tmuxText(['display-message', '-p', '-t', target, '#{pane_dead} #{window_activity}']);
|
||||
const match = String(info.stdout || '').trim().match(/^([01])\s+(\d+)$/);
|
||||
const pane = capturePane(target);
|
||||
let idleSec = null;
|
||||
let dead = false;
|
||||
if (match) {
|
||||
dead = match[1] === '1';
|
||||
idleSec = Math.max(0, Math.round(Date.now() / 1000 - Number(match[2])));
|
||||
}
|
||||
return { found: true, dead, interrupt: BUSY_RE.test(pane), idleSec, pane };
|
||||
}
|
||||
|
||||
function paneTail(pane) {
|
||||
const rows = String(pane).split('\n').map((s) => s.trim()).filter(Boolean).slice(-4);
|
||||
return rows.map((s) => s.slice(0, 120)).join(' | ').slice(0, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wake an open tmux window with a short line, not the mail body.
|
||||
* Codex and Claude treat a fast burst as a paste and swallow Enter, so the
|
||||
@@ -250,21 +276,38 @@ function stillInInput(target, line) {
|
||||
* If the cursor row still holds the line, Enter is sent once more.
|
||||
* A pane that shows an interrupt hint is left alone; a later status check can nudge it.
|
||||
*/
|
||||
export function nudgeMail(c, areaId, from) {
|
||||
export function nudgeMail(c, areaId, from, meta = {}) {
|
||||
const target = tmuxTarget(c, areaId);
|
||||
if (tmuxText(['list-panes', '-t', target]).status !== 0) return { submitted: false, reason: 'no-window' };
|
||||
const before = inspectPane(target);
|
||||
const record = {
|
||||
t: new Date().toISOString(),
|
||||
area: areaId,
|
||||
window: target,
|
||||
file: meta.file || null,
|
||||
trigger: meta.trigger || 'send',
|
||||
before: { found: before.found, dead: before.dead, interrupt: before.interrupt, idleSec: before.idleSec },
|
||||
};
|
||||
const finish = (fields) => {
|
||||
const { line, reason, ...logged } = fields;
|
||||
const pane = logged.action === 'no-window' ? '' : capturePane(target);
|
||||
appendWakeLog(c, { ...record, ...logged, tail: paneTail(pane) });
|
||||
return { submitted: logged.outcome === 'submitted', reason, ...(line ? { line } : {}) };
|
||||
};
|
||||
if (!before.found) return finish({ action: 'no-window', enterAttempts: 0, outcome: 'no-window', reason: 'no-window' });
|
||||
const line = nudgeLine(from);
|
||||
if (BUSY_RE.test(capturePane(target))) return { submitted: false, reason: 'working', line };
|
||||
if (tmuxText(['send-keys', '-t', target, '-l', line]).status !== 0) return { submitted: false, reason: 'send-failed', line };
|
||||
if (before.interrupt) return finish({ action: 'skipped-busy', enterAttempts: 0, outcome: 'skipped-busy', reason: 'working', line });
|
||||
if (tmuxText(['send-keys', '-t', target, '-l', line]).status !== 0) return finish({ action: 'send-failed', enterAttempts: 0, outcome: 'send-failed', reason: 'send-failed', line });
|
||||
let enterAttempts = 0;
|
||||
const hitEnter = () => { tmuxText(['send-keys', '-t', target, 'Enter']); enterAttempts += 1; };
|
||||
sleepMs(NUDGE_PAUSE_MS);
|
||||
tmuxText(['send-keys', '-t', target, 'Enter']);
|
||||
hitEnter();
|
||||
sleepMs(NUDGE_SETTLE_MS);
|
||||
if (!stillInInput(target, line)) return { submitted: true, reason: 'submitted', line };
|
||||
if (!stillInInput(target, line)) return finish({ action: 'typed', enterAttempts, outcome: 'submitted', reason: 'submitted', line });
|
||||
sleepMs(NUDGE_RETRY_MS);
|
||||
tmuxText(['send-keys', '-t', target, 'Enter']);
|
||||
hitEnter();
|
||||
sleepMs(NUDGE_SETTLE_MS);
|
||||
const submitted = !stillInInput(target, line);
|
||||
return { submitted, reason: submitted ? 'submitted' : 'not-submitted', line };
|
||||
return finish({ action: 'typed', enterAttempts, outcome: submitted ? 'submitted' : 'swallowed', reason: submitted ? 'submitted' : 'not-submitted', line });
|
||||
}
|
||||
|
||||
function unreadFiles(c, id) {
|
||||
@@ -325,7 +368,12 @@ export function fleetStatus(c, { recoverAfterMs = RECOVER_AFTER_MS, idleAfterMs
|
||||
const age = oldest ? Date.now() - oldest.mtimeMs : 0;
|
||||
const already = (state[a.id] || []).includes(oldest?.file);
|
||||
if (view.window === 'alive' && view.activity === 'idle' && oldest && age >= recoverAfterMs && !already) {
|
||||
const wake = nudgeMail(c, a.id, senderOf(oldest.path));
|
||||
const prior = priorWakes(c, a.id, oldest.file).slice(-5).map((p) => ({ t: p.t, trigger: p.trigger, action: p.action, outcome: p.outcome }));
|
||||
appendWakeLog(c, {
|
||||
t: new Date().toISOString(), kind: 'stale', area: a.id, window: tmuxTarget(c, a.id),
|
||||
file: oldest.file, ageSec: Math.round(age / 1000), prior,
|
||||
});
|
||||
const wake = nudgeMail(c, a.id, senderOf(oldest.path), { trigger: 'recovery', file: oldest.file });
|
||||
state[a.id] = [...(state[a.id] || []), oldest.file];
|
||||
stateDirty = true;
|
||||
row.recovered = true;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// One JSON line per wake attempt, in the fleet state dir (not the checkout).
|
||||
// The line records what the pane looked like and what the nudge did. It does
|
||||
// not record mail bodies. The file is capped so it cannot grow without bound.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { writeAtomic } from '../lib.mjs';
|
||||
|
||||
export const WAKE_LOG_MAX = 400;
|
||||
|
||||
export function wakeLogPath(c) {
|
||||
return path.join(c.stateDir, 'wake-log.jsonl');
|
||||
}
|
||||
|
||||
export function appendWakeLog(c, entry) {
|
||||
const file = wakeLogPath(c);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
let lines = [];
|
||||
try { lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean); } catch { /* new log */ }
|
||||
lines.push(JSON.stringify(entry));
|
||||
if (lines.length > WAKE_LOG_MAX) lines = lines.slice(lines.length - WAKE_LOG_MAX);
|
||||
writeAtomic(file, `${lines.join('\n')}\n`);
|
||||
}
|
||||
|
||||
export function priorWakes(c, area, file) {
|
||||
return readRaw(c).filter((e) => e && e.kind !== 'stale' && e.area === area && e.file === file);
|
||||
}
|
||||
|
||||
function readRaw(c) {
|
||||
try {
|
||||
return fs.readFileSync(wakeLogPath(c), 'utf8').split('\n').filter(Boolean).map((line) => {
|
||||
try { return JSON.parse(line); } catch { return null; }
|
||||
}).filter(Boolean);
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
/** Plain words for the failure a later reader should tell the user. */
|
||||
export function wakeFlag(entry) {
|
||||
if (!entry) return 'unknown';
|
||||
if (entry.kind === 'stale') {
|
||||
const last = Array.isArray(entry.prior) ? entry.prior[entry.prior.length - 1] : null;
|
||||
const age = entry.ageSec ?? '?';
|
||||
if (!last) return `still unread after ${age}s; no earlier wake`;
|
||||
if (last.outcome === 'swallowed') return `woke at ${last.t} (${last.trigger}), wake line still in the composer, still unread after ${age}s`;
|
||||
if (last.action === 'skipped-busy') return `woke at ${last.t} (${last.trigger}), skipped because an interrupt hint was showing, still unread after ${age}s`;
|
||||
if (last.action === 'no-window' || last.outcome === 'no-window') return `woke at ${last.t} (${last.trigger}), pane not found, still unread after ${age}s`;
|
||||
if (last.outcome === 'submitted') return `woke at ${last.t} (${last.trigger}), line left the composer, still unread after ${age}s`;
|
||||
return `woke at ${last.t} (${last.trigger}), outcome ${last.outcome}, still unread after ${age}s`;
|
||||
}
|
||||
if (entry.action === 'no-window' || entry.outcome === 'no-window') return 'pane not found';
|
||||
if (entry.action === 'skipped-busy') return 'skipped: interrupt hint was showing, so the pane looked mid-turn';
|
||||
if (entry.action === 'send-failed') return 'send-keys failed before Enter';
|
||||
if (entry.outcome === 'swallowed') return `wake line still on the cursor row after ${entry.enterAttempts} Enter`;
|
||||
if (entry.outcome === 'submitted') {
|
||||
if (entry.before && entry.before.idleSec != null && entry.before.idleSec < 3 && !entry.before.interrupt) {
|
||||
return 'typed and the line left the composer while the pane was recently active';
|
||||
}
|
||||
return 'typed and the line left the composer';
|
||||
}
|
||||
return entry.outcome || entry.action || 'unknown';
|
||||
}
|
||||
|
||||
export function readWakeLog(c, { limit = 40 } = {}) {
|
||||
const all = readRaw(c);
|
||||
const entries = all.slice(-Math.max(1, Number(limit) || 40)).map((entry) => ({ ...entry, flag: wakeFlag(entry) }));
|
||||
return { path: wakeLogPath(c), entries };
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
// fleet doctor | landq | backlog | mailbox
|
||||
// the same tools as tools/fleet/bin/<name>.mjs
|
||||
// fleet status areas, run state (running / exited / failed / timeout / stopped), lander up or down
|
||||
// fleet wakes recent wake attempts, and plain words for a likely failure
|
||||
// fleet prompt <id> print the composed system prompt for an area
|
||||
// fleet crontab PRINT the crontab lines for scheduled jobs (never installs)
|
||||
// fleet run-job <name> one headless run of docs/agents/schedules/<name>.md (sched.sh calls this)
|
||||
@@ -51,6 +52,7 @@ import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { KIT, loadConfig, loadAdapter, area as getArea, dispatcher, sessionName, flag, die, writeAtomic, tryGit, mainRef, today, runProcessGroup, runForeground, finishHeadless, withHandoff, applyHeadlessSession, assignLaunchSession, areaBudgetMin, readRun, writeRun, outcomeOfRun, pidAlive, killProcessGroup, landerIsUp, landerPid, startLander, writableRoots } from './lib.mjs';
|
||||
import { commitWork, setupCommit, tellDispatch, hearInterpreter, fleetStatus } from './core/ops.mjs';
|
||||
import { readWakeLog } from './core/wake-log.mjs';
|
||||
import { fleetUpdate, recordInstalled, isTemplatePath } from './core/update.mjs';
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
@@ -308,6 +310,19 @@ async function status() {
|
||||
for (const id of report.recovered) console.log(`nudged ${id} again`);
|
||||
}
|
||||
|
||||
function wakesCmd() {
|
||||
const report = readWakeLog(c);
|
||||
if (!report.entries.length) {
|
||||
console.log(`no wake attempts in ${report.path}`);
|
||||
return;
|
||||
}
|
||||
console.log(report.path);
|
||||
for (const e of report.entries) {
|
||||
if (e.kind === 'stale') console.log(`${e.t} stale ${e.area} ${e.file} ${e.flag}`);
|
||||
else console.log(`${e.t} ${e.trigger} ${e.area} ${e.window} ${e.file || '-'} ${e.action} enter=${e.enterAttempts} ${e.outcome} ${e.flag}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureWorktree(a) {
|
||||
if (fs.existsSync(a.worktree)) return true;
|
||||
const dirty = uncommittedFleetFiles();
|
||||
@@ -618,7 +633,7 @@ function tool(name) {
|
||||
const run = { up, down, status, supervise: () => supervise(argv[1]), worktrees, crontab,
|
||||
prompt: async () => console.log(composePrompt(getArea(c, argv[1]), await adapterFor(getArea(c, argv[1]).harness))),
|
||||
'run-job': () => runJob(argv[1]), 'sched-env': () => schedEnv(argv[1]), lander: () => lander(argv[1] ?? 'status'),
|
||||
commit: commitCmd, 'setup-commit': setupCommitCmd, update: updateCmd, tell: tellCmd, hear: hearCmd,
|
||||
commit: commitCmd, 'setup-commit': setupCommitCmd, update: updateCmd, tell: tellCmd, hear: hearCmd, wakes: wakesCmd,
|
||||
doctor: () => tool('doctor'), landq: () => tool('landq'), backlog: () => tool('backlog'),
|
||||
mailbox: () => tool('mailbox') }[cmd];
|
||||
if (!run) die(`unknown command "${cmd}" (fleet help)`);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
setupCommit, tellDispatch, hearInterpreter, fleetStatus,
|
||||
} from './core/ops.mjs';
|
||||
import { fleetUpdate } from './core/update.mjs';
|
||||
import { readWakeLog } from './core/wake-log.mjs';
|
||||
|
||||
export const SERVER = { name: 'agent-fleet', version: '0.2.0' };
|
||||
const INSTRUCTIONS = [
|
||||
@@ -18,7 +19,7 @@ const INSTRUCTIONS = [
|
||||
'mailbox_read at the start of a turn. Mail is always delivered; the prompt says when to act on it. Sandboxed agents commit with the commit tool (message plus paths, or all), not git commit. submit on that tool also queues the tip. Then go back to work. At your next natural checkpoint, land_status can report that submission. Waiting is optional. Never infer the lander or the land result from silence. landq_submit queues the committed tip (a dirty worktree warns and still queues HEAD) and starts a drain. Do not push main.',
|
||||
'A scarce machine is a prompt convention: say so in .task and wait if a peer already has it. There is no slot lock.',
|
||||
'Sessions, worktrees, and cron stay on the fleet CLI. This server does not start them.',
|
||||
'setup_commit commits only the fleet setup files on the main checkout. A sandboxed agent uses it instead of git commit. update refreshes vendored runtime and the registered skill to this kit, leaves an edited prompt in place (the new template is a .kit file beside it), and commits only the paths it changed. It does not change fleet.config.json, product files, mail, or the landing queue. If that commit cannot write the index, it falls back to setup_commit. Mail to an open tmux window wakes it with a short line, not the body. mailbox_read and hear take those messages out of unread. fleet_status reports each window alive or dead, the unread count, and a working or idle guess. An idle area with mail that has been waiting is nudged once by that check. mailbox_read, hear, and tell run that same check. A second look does not nudge again. Do not scrape the tmux pane.',
|
||||
'setup_commit commits only the fleet setup files on the main checkout. A sandboxed agent uses it instead of git commit. update refreshes vendored runtime and the registered skill to this kit, leaves an edited prompt in place (the new template is a .kit file beside it), and commits only the paths it changed. It does not change fleet.config.json, product files, mail, or the landing queue. If that commit cannot write the index, it falls back to setup_commit. Mail to an open tmux window wakes it with a short line, not the body. mailbox_read and hear take those messages out of unread. fleet_status reports each window alive or dead, the unread count, and a working or idle guess. An idle area with mail that has been waiting is nudged once by that check. mailbox_read, hear, and tell run that same check. A second look does not nudge again. wake_log says whether a wake was swallowed, skipped, missing, or submitted while the mail is still unread. Tell the user that case. Do not ask them to open the log. Do not scrape the tmux pane.',
|
||||
].join(' ');
|
||||
|
||||
const cwdProp = { type: 'string', description: 'Directory to resolve the fleet from. Defaults to this server\'s cwd.' };
|
||||
@@ -39,6 +40,7 @@ export const TOOLS = [
|
||||
{ name: 'tell', description: 'Mail the session manager from the interpreter. Wakes its tmux window with a short line when that window is open. The body stays in the mail. Replies are read with hear.', inputSchema: { type: 'object', properties: { body: { type: 'string' }, subject: { type: 'string' }, cwd: cwdProp }, required: ['body'] } },
|
||||
{ name: 'hear', description: 'Read unread mailbox messages for the interpreter and move them out of unread.', inputSchema: { type: 'object', properties: { cwd: cwdProp } } },
|
||||
{ name: 'fleet_status', description: 'Each area: tmux window alive or dead, unread mail count, and a working or idle guess. Idle with unread mail that has been waiting is nudged once.', inputSchema: { type: 'object', properties: { cwd: cwdProp } } },
|
||||
{ name: 'wake_log', description: 'Recent wake attempts from the fleet state dir. Each line says the pane state, what was typed or skipped, and a plain-words flag when a wake likely failed. No mail bodies.', inputSchema: { type: 'object', properties: { limit: { type: 'number' }, cwd: cwdProp } } },
|
||||
];
|
||||
|
||||
function configFor(args) {
|
||||
@@ -66,6 +68,7 @@ export function dispatchTool(name, args = {}) {
|
||||
case 'tell': return tellDispatch(c, { body: args.body, subject: args.subject ?? '' });
|
||||
case 'hear': return hearInterpreter(c);
|
||||
case 'fleet_status': return fleetStatus(c);
|
||||
case 'wake_log': return readWakeLog(c, { limit: args.limit });
|
||||
default: throw new FleetError(`unknown tool ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user