@bill10/agent-007 0.9.0 → 0.9.1000
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/package.json +1 -1
- package/public/modules/terminal.js +3 -0
- package/server/config.js +6 -0
- package/server/jobs.js +13 -3
- package/server/pty.js +4 -1
- package/server/ws.js +18 -6
- package/server.js +2 -0
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ up a stale local base or whatever unrelated branch you happen to have checked
|
|
|
83
83
|
out. Override it per agent with **Advanced -> Start from** when you want to
|
|
84
84
|
branch off work in progress.
|
|
85
85
|
|
|
86
|
-
The job board reuses that same machinery: a dispatched job is an ordinary agent, with a real terminal you can type into and take over at any point. Each job gets its own worktree and branch, so a job maps one-to-one onto a branch and a pull request. When the agent finishes it calls the board's `finish_job` tool (a card that requires a pull request hands over the PR it opened; one that does not hands over a summary), and the card moves to Review with the agent still running, so you can click straight into it to ask about the work. When the card reaches Done the board closes the agent and releases its worktree and local branch; the PR itself is untouched, and work that was never pushed is kept as an orphan rather than deleted. **Re-spawn** on an orphan picks that conversation back up with the CLI it ran, `codex resume <session-id>` (the newest Codex session recorded in that exact worktree) or `claude --continue`, under the permission mode its job card was dispatched with (a board agent whose card is already finished or deleted follows the board's current setting), or, for an agent you spawned by hand, under the permission flags you started it with. When the PR merges the job is filed away as finished -- the record is kept, the card is not.
|
|
86
|
+
The job board reuses that same machinery: a dispatched job is an ordinary agent, with a real terminal you can type into and take over at any point. Each job gets its own worktree and branch, so a job maps one-to-one onto a branch and a pull request. When the agent finishes it calls the board's `finish_job` tool (a card that requires a pull request hands over the PR it opened; one that does not hands over a summary), and the card moves to Review with the agent still running, so you can click straight into it to ask about the work. When the card reaches Done the board closes the agent and releases its worktree and local branch; the PR itself is untouched, and work that was never pushed is kept as an orphan rather than deleted. **Re-spawn** on an orphan picks that conversation back up with the CLI it ran, `codex resume <session-id>` (the newest Codex session recorded in that exact worktree) or `claude --continue`, under the permission mode its job card was dispatched with (a board agent whose card is already finished or deleted follows the board's current setting), or, for an agent you spawned by hand, under the permission flags you started it with. A board worker re-spawned after a restart is its card's worker again: it counts toward the cap, sends its approvals where the card says, is told once to carry on if its card is still In progress, and leaves like any other board worker when the card is filed. When the PR merges the job is filed away as finished -- the record is kept, the card is not.
|
|
87
87
|
|
|
88
88
|
```
|
|
89
89
|
┌─────────────┬──────────────┬────────────────────┐
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.1.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bill10/agent-007",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1000",
|
|
4
4
|
"description": "From web terminals for your coding agents to a self-running agent company: Claude Code and Codex in parallel git worktrees, a job board they pick work from, and one agent that runs the board from a goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -260,6 +260,9 @@ export function handleSessionEnded(msg) {
|
|
|
260
260
|
// would transcribe speech into a dead pty forever.
|
|
261
261
|
if (msg.sessionId === activeSessionId) stopVoice({ notice: 'Voice input stopped — agent ended' });
|
|
262
262
|
agent.state = 'DISCONNECTED';
|
|
263
|
+
// The server's word at exit wins: a re-spawned agent relinked to its card,
|
|
264
|
+
// or one the board retired, is a board worker even if it opened as a user's.
|
|
265
|
+
if ('spawnedBy' in msg) { agent.spawnedBy = msg.spawnedBy; agent.jobId = msg.jobId || null; }
|
|
263
266
|
|
|
264
267
|
// A board agent retired after opening its PR takes its tab with it. Under
|
|
265
268
|
// unattended dispatch these arrive steadily, and a row of dead tabs is pure
|
package/server/config.js
CHANGED
|
@@ -121,6 +121,10 @@ export function saveActiveSession(session, broadcast) {
|
|
|
121
121
|
agent: sessionAgent(session),
|
|
122
122
|
permissionFlags: sessionPermissionFlags(session),
|
|
123
123
|
origin: sessionOrigin(session),
|
|
124
|
+
// The card it works on, so a re-spawn after a restart is that card's
|
|
125
|
+
// worker again rather than a stranger on its branch.
|
|
126
|
+
jobId: session.jobId || null,
|
|
127
|
+
approvalsToBillion: !!session.approvalsToBillion,
|
|
124
128
|
savedAt: new Date().toISOString(),
|
|
125
129
|
});
|
|
126
130
|
saveConfig(broadcast);
|
|
@@ -171,6 +175,8 @@ export function recoverCrashedSessions(broadcast) {
|
|
|
171
175
|
agent: isValidJobAgent(s.agent) ? s.agent : null,
|
|
172
176
|
permissionFlags: recordedPermissionFlags(s),
|
|
173
177
|
origin: s.origin === 'board' ? 'board' : 'user',
|
|
178
|
+
jobId: typeof s.jobId === 'string' ? s.jobId : null,
|
|
179
|
+
approvalsToBillion: s.approvalsToBillion === true,
|
|
174
180
|
reason: 'server-restart',
|
|
175
181
|
createdAt: new Date().toISOString(),
|
|
176
182
|
};
|
package/server/jobs.js
CHANGED
|
@@ -1242,7 +1242,9 @@ export async function dispatchOnce(createSession, broadcast, { onSessionCreated,
|
|
|
1242
1242
|
// belongs to, or null. relinkSessionToJob below ties the session to it, and
|
|
1243
1243
|
// resumeCommandForOrphan reads the card's agent through the same lookup before
|
|
1244
1244
|
// the spawn, so the two cannot disagree about which card the orphan came from.
|
|
1245
|
-
|
|
1245
|
+
// A record that saved its card's id (jobId) picks that card when two share the
|
|
1246
|
+
// branch; the branch alone still decides for older records.
|
|
1247
|
+
export function findJobForBranch({ repoPath, branchName, jobId }) {
|
|
1246
1248
|
if (!branchName) return null;
|
|
1247
1249
|
// in-progress OR review: a job can reach review while its link is null (the
|
|
1248
1250
|
// PR was found after a restart, so there was no session to retire), and the
|
|
@@ -1258,7 +1260,8 @@ export function findJobForBranch({ repoPath, branchName }) {
|
|
|
1258
1260
|
);
|
|
1259
1261
|
// Prefer work still in flight: if an old review job and a new in-progress job
|
|
1260
1262
|
// share a branch, the agent belongs to the one that is not finished.
|
|
1261
|
-
return matches.find(j => j.
|
|
1263
|
+
return (jobId && matches.find(j => j.id === jobId))
|
|
1264
|
+
|| matches.find(j => j.state === 'in-progress') || matches[0] || null;
|
|
1262
1265
|
}
|
|
1263
1266
|
|
|
1264
1267
|
// What re-adopting an orphan should run. The orphan record says which CLI the
|
|
@@ -1326,7 +1329,10 @@ export function relinkSessionToJob(session, broadcast) {
|
|
|
1326
1329
|
job.lastErrorAt = null;
|
|
1327
1330
|
job.prCheckError = null;
|
|
1328
1331
|
job.prCheckErrorAt = null;
|
|
1329
|
-
|
|
1332
|
+
// A board worker again, so the PR path retires it and its tab and desk go
|
|
1333
|
+
// like any board agent's (the client keys that on spawnedBy + jobId).
|
|
1334
|
+
session.spawnedBy = 'board';
|
|
1335
|
+
session.jobId = job.id;
|
|
1330
1336
|
persist(broadcast);
|
|
1331
1337
|
return job;
|
|
1332
1338
|
}
|
|
@@ -1632,6 +1638,10 @@ async function retireAgentForJob(job, askedBranch, askedSessionId, killSession,
|
|
|
1632
1638
|
if (!session || session.exited) return false;
|
|
1633
1639
|
try {
|
|
1634
1640
|
if (!job.agentName) job.agentName = session.name;
|
|
1641
|
+
// The board retired it, so it leaves as a board worker does: session-ended
|
|
1642
|
+
// carries these, and the client closes the tab and walks it out on them.
|
|
1643
|
+
session.spawnedBy = 'board';
|
|
1644
|
+
session.jobId = job.id;
|
|
1635
1645
|
await killSession(session.id);
|
|
1636
1646
|
job.agentSessionId = null; // only after the kill actually succeeded
|
|
1637
1647
|
return true;
|
package/server/pty.js
CHANGED
|
@@ -158,7 +158,10 @@ export function setupPtyHandlers(session, sessionId, broadcast) {
|
|
|
158
158
|
dropMessages(sessionId);
|
|
159
159
|
if (session.isBillion) dropApprovals();
|
|
160
160
|
updateState(session, broadcast);
|
|
161
|
-
|
|
161
|
+
// What it is as it ends, which a relink or a board retirement may have
|
|
162
|
+
// changed since session-created: the client's finished-worker path reads it.
|
|
163
|
+
broadcast({ type: 'session-ended', sessionId, reason: `Process exited with code ${exitCode}`,
|
|
164
|
+
spawnedBy: session.spawnedBy, jobId: session.jobId });
|
|
162
165
|
});
|
|
163
166
|
|
|
164
167
|
session.stateCheckInterval = setInterval(() => {
|
package/server/ws.js
CHANGED
|
@@ -11,17 +11,20 @@ import { authEnabled, resolveToken, tokenFromRequest, publicUser, userById, load
|
|
|
11
11
|
import { saveActiveSession, syncOrphansToConfig, saveConfig } from './config.js';
|
|
12
12
|
import { addRepo, removeRepo, scanFileTree, startTreeScanLoop, getDiff, broadcastReposList, gitExec, deleteBranch } from './git.js';
|
|
13
13
|
import { createSessionFromConfig } from './pty.js';
|
|
14
|
-
import { isTyping } from './messages.js';
|
|
14
|
+
import { isTyping, sendText } from './messages.js';
|
|
15
|
+
import { autoTrusts, trustClaudeFolder } from './claude-trust.js';
|
|
15
16
|
import { waitingPayload, dismissWaiting } from './owner.js';
|
|
16
17
|
import { parseGitStatus, buildFileTree, safeFilename } from '../lib/helpers.js';
|
|
17
|
-
import { isValidJobAgent } from '../lib/jobs.js';
|
|
18
|
+
import { isValidJobAgent, sessionAgentFromCommand } from '../lib/jobs.js';
|
|
18
19
|
import { billionRuns } from './billion.js';
|
|
19
20
|
import {
|
|
20
21
|
addJob, updateJob, deleteJob, moveJob, updateSettings, setJobPaused,
|
|
21
22
|
jobsPayload, broadcastJobs, runScan, relinkSessionToJob, allJobs,
|
|
22
|
-
orphanResumePlan,
|
|
23
|
+
orphanResumePlan, findJobForBranch,
|
|
23
24
|
} from './jobs.js';
|
|
24
25
|
|
|
26
|
+
export const RESPAWN_NUDGE = 'Agent 007 restarted and you were re-spawned. Continue your card where you left off.';
|
|
27
|
+
|
|
25
28
|
// --- Client tracking ---
|
|
26
29
|
const clients = new Set();
|
|
27
30
|
|
|
@@ -400,8 +403,13 @@ export function setupWebSocket(wss, { createSession, killSession, startBillion }
|
|
|
400
403
|
// by a card, a transcript or the default is a guess, and writing it
|
|
401
404
|
// down would make a wrong one permanent.
|
|
402
405
|
const { command, mode, flags } = orphanResumePlan(orphan);
|
|
403
|
-
//
|
|
404
|
-
|
|
406
|
+
// The card it worked on: its saved jobId, or (an older record) the
|
|
407
|
+
// one on its branch in its repo — the same lookup relinkSessionToJob
|
|
408
|
+
// makes below. With one, it comes back as that card's board worker.
|
|
409
|
+
const card = findJobForBranch(orphan);
|
|
410
|
+
const spawnedBy = card ? 'board' : 'user';
|
|
411
|
+
const autoTrust = autoTrusts({ spawnedBy, worktreePath: orphan.worktreePath, command });
|
|
412
|
+
if (autoTrust && sessionAgentFromCommand(command) === 'claude') trustClaudeFolder(orphan.worktreePath);
|
|
405
413
|
const result = createSessionFromConfig({
|
|
406
414
|
sessionId: nextSessionId(),
|
|
407
415
|
name: orphan.name,
|
|
@@ -420,7 +428,8 @@ export function setupWebSocket(wss, { createSession, killSession, startBillion }
|
|
|
420
428
|
// recorded flags, it keeps them.
|
|
421
429
|
permissionFlags: mode ? [] : flags,
|
|
422
430
|
origin: orphan.origin === 'board' ? 'board' : 'user',
|
|
423
|
-
|
|
431
|
+
spawnedBy, jobId: card?.id || null, autoTrust,
|
|
432
|
+
approvalsToBillion: card ? card.postedByBillion === true : orphan.approvalsToBillion === true,
|
|
424
433
|
}, broadcast);
|
|
425
434
|
if (result.error) {
|
|
426
435
|
adoptingOrphans.delete(msg.orphanId);
|
|
@@ -439,6 +448,9 @@ export function setupWebSocket(wss, { createSession, killSession, startBillion }
|
|
|
439
448
|
type: 'notification', level: 'info',
|
|
440
449
|
message: `${session.name} reconnected to job "${relinked.title}"`,
|
|
441
450
|
});
|
|
451
|
+
// `claude --continue` resumes at the prompt and waits there. Typed
|
|
452
|
+
// once it rests at the prompt; a card in Review has nothing to do.
|
|
453
|
+
if (relinked.state === 'in-progress') sendText(session, RESPAWN_NUDGE);
|
|
442
454
|
}
|
|
443
455
|
adoptingOrphans.delete(msg.orphanId);
|
|
444
456
|
orphans.delete(msg.orphanId);
|
package/server.js
CHANGED
|
@@ -159,6 +159,8 @@ async function killSession(sessionId, { discardChanges = false } = {}) {
|
|
|
159
159
|
agent: sessionAgent(session),
|
|
160
160
|
permissionFlags: sessionPermissionFlags(session),
|
|
161
161
|
origin: sessionOrigin(session),
|
|
162
|
+
jobId: session.jobId || null,
|
|
163
|
+
approvalsToBillion: !!session.approvalsToBillion,
|
|
162
164
|
reason, createdAt: new Date().toISOString(),
|
|
163
165
|
};
|
|
164
166
|
orphans.set(orphanId, orphan);
|