@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 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.0.0
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.0",
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
- export function findJobForBranch({ repoPath, branchName }) {
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.state === 'in-progress') || matches[0] || null;
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
- session.jobId = job.id; // so the PR path can retire it like any board agent
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
- broadcast({ type: 'session-ended', sessionId, reason: `Process exited with code ${exitCode}` });
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
- // Matched on the branch, as relinkSessionToJob does below.
404
- const card = allJobs().find(j => j.branchName && j.branchName === orphan.branchName && j.repoPath === orphan.repoPath);
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
- approvalsToBillion: card?.postedByBillion === true,
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);