flowviant 0.125.5 → 0.125.7

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.
Files changed (101) hide show
  1. package/README.md +4 -5
  2. package/bin/lib/agentInternalWait.mjs +1 -5
  3. package/bin/lib/agentPlan.mjs +13 -163
  4. package/bin/lib/agentRepositories.mjs +3 -1
  5. package/bin/lib/claudeInput.mjs +89 -10
  6. package/bin/lib/claudeMachineMcp.mjs +85 -0
  7. package/bin/lib/claudePersonal.mjs +6 -57
  8. package/bin/lib/claudePosture.mjs +6 -15
  9. package/bin/lib/claudeSettingsArg.mjs +67 -0
  10. package/bin/lib/claudeStream.mjs +30 -1
  11. package/bin/lib/claudeSubagentProgress.mjs +12 -28
  12. package/bin/lib/claudeSubagents.mjs +4 -2
  13. package/bin/lib/codexContract.mjs +52 -0
  14. package/bin/lib/codexFencedMount.mjs +275 -0
  15. package/bin/lib/codexPersonal.mjs +1 -265
  16. package/bin/lib/codexProjectRead.mjs +5 -4
  17. package/bin/lib/codexResearch.mjs +9 -3
  18. package/bin/lib/codexSubagentEvents.mjs +11 -7
  19. package/bin/lib/codexSubagents.mjs +55 -52
  20. package/bin/lib/codexToml.mjs +328 -0
  21. package/bin/lib/config.mjs +22 -1
  22. package/bin/lib/conversationIds.mjs +10 -0
  23. package/bin/lib/fleet.mjs +2 -2
  24. package/bin/lib/git.mjs +32 -0
  25. package/bin/lib/heldTool.mjs +49 -0
  26. package/bin/lib/hooks/mcpAllow.mjs +11 -5
  27. package/bin/lib/hooks/orchestrator.mjs +20 -10
  28. package/bin/lib/intakeDraft.mjs +1 -1
  29. package/bin/lib/jsonlTail.mjs +52 -0
  30. package/bin/lib/mcpInventory.mjs +11 -6
  31. package/bin/lib/mcpReasons.mjs +26 -0
  32. package/bin/lib/mcpReportBounds.mjs +10 -0
  33. package/bin/lib/mcpStdio.mjs +35 -0
  34. package/bin/lib/mergeBatch.mjs +6 -24
  35. package/bin/lib/mergeOrder.mjs +18 -0
  36. package/bin/lib/modelJson.mjs +173 -0
  37. package/bin/lib/nativeAgentTools.mjs +72 -0
  38. package/bin/lib/orchestrator.mjs +221 -133
  39. package/bin/lib/orchestratorClient.mjs +17 -21
  40. package/bin/lib/orchestratorHooks.mjs +21 -4
  41. package/bin/lib/orchestratorNative.mjs +58 -0
  42. package/bin/lib/orchestratorPrompt.mjs +7 -4
  43. package/bin/lib/orchestratorProtocol.mjs +25 -3
  44. package/bin/lib/postureTools.mjs +58 -13
  45. package/bin/lib/prWorkflow.mjs +13 -2
  46. package/bin/lib/processGroup.mjs +41 -0
  47. package/bin/lib/projectRepositories.mjs +2 -2
  48. package/bin/lib/projectToolRuntimes.mjs +92 -245
  49. package/bin/lib/projectTools.mjs +60 -19
  50. package/bin/lib/prompts.mjs +47 -18
  51. package/bin/lib/reportJournal.mjs +61 -0
  52. package/bin/lib/repositoryDelivery.mjs +6 -4
  53. package/bin/lib/repositoryMerge.mjs +8 -12
  54. package/bin/lib/repositoryPlaces.mjs +2 -2
  55. package/bin/lib/repositoryPrompts.mjs +42 -0
  56. package/bin/lib/repositoryTool.mjs +9 -32
  57. package/bin/lib/researchUse.mjs +39 -0
  58. package/bin/lib/resumeLost.mjs +21 -1
  59. package/bin/lib/resumeMarker.mjs +43 -0
  60. package/bin/lib/runTurn.mjs +55 -52
  61. package/bin/lib/runtimeAntigravity.mjs +1 -1
  62. package/bin/lib/runtimeCapabilities.mjs +13 -18
  63. package/bin/lib/runtimeClaude.mjs +68 -24
  64. package/bin/lib/runtimeCodex.mjs +12 -6
  65. package/bin/lib/runtimeEvents.mjs +13 -2
  66. package/bin/lib/runtimeOfferings.mjs +12 -5
  67. package/bin/lib/runtimes.mjs +3 -1
  68. package/bin/lib/startProject.mjs +10 -0
  69. package/bin/lib/subagentBounds.mjs +20 -0
  70. package/bin/lib/trace.mjs +30 -5
  71. package/bin/lib/turnDeclaration.mjs +1 -1
  72. package/bin/lib/turnDiagnostics.mjs +9 -12
  73. package/bin/lib/turnReceipts.mjs +64 -0
  74. package/bin/lib/turnSubagents.mjs +91 -0
  75. package/bin/lib/turnUsage.mjs +9 -6
  76. package/bin/lib/update.mjs +18 -4
  77. package/bin/lib/versionOrder.mjs +11 -1
  78. package/bin/lib/work.mjs +9 -9
  79. package/bin/lib/workAgentMerges.mjs +5 -7
  80. package/bin/lib/workAgentPrecheck.mjs +5 -5
  81. package/bin/lib/workAgentRecovery.mjs +2 -1
  82. package/bin/lib/workAgentResume.mjs +121 -0
  83. package/bin/lib/workAgentTools.mjs +111 -0
  84. package/bin/lib/workAgentTurnExecution.mjs +82 -246
  85. package/bin/lib/workAgentTurnOutcome.mjs +30 -3
  86. package/bin/lib/workAgentTurnReports.mjs +16 -0
  87. package/bin/lib/workAgentTurnShape.mjs +90 -0
  88. package/bin/lib/workAgentTurns.mjs +28 -12
  89. package/bin/lib/workArtifactReconcile.mjs +3 -2
  90. package/bin/lib/workAttachments.mjs +27 -5
  91. package/bin/lib/workMergeBatch.mjs +7 -5
  92. package/bin/lib/workNarration.mjs +32 -0
  93. package/bin/lib/workPlaceReports.mjs +3 -3
  94. package/bin/lib/workPlaces.mjs +14 -0
  95. package/bin/lib/workReportQueue.mjs +18 -1
  96. package/bin/lib/workSessionRuntime.mjs +2 -5
  97. package/bin/lib/workSessionTurns.mjs +68 -80
  98. package/bin/lib/workShip.mjs +8 -18
  99. package/bin/lib/workTurnShape.mjs +6 -4
  100. package/bin/lib/worktreeDiff.mjs +29 -5
  101. package/package.json +1 -1
@@ -0,0 +1,90 @@
1
+ /**
2
+ * AN AGENT TURN'S PROMPT PAIR — its kickoff, a live message's follow-up and
3
+ * its system prompt (pure, 2026-10-18, the SOLID pass). The twin of
4
+ * workTurnShape.mjs, which is the same thing for a tab.
5
+ *
6
+ * Split out of workAgentTurnExecution.mjs, which composed all three inline in
7
+ * the turn: two AGENT_HUMAN_KICKOFF argument bags spelling the same ten
8
+ * fields, the task kickoff beside them, and a system prompt of eight sections
9
+ * on one line. The copies had already drifted: investigationContext reached
10
+ * the task and human kickoffs on 2026-10-12, the follow-up written two days
11
+ * later went without it, and live messages carried less context than the
12
+ * first prompt until 2026-10-15 added it beside researchContext at all three
13
+ * sites. So the fields every kickoff shares are spelled once (`shared`), and
14
+ * the next job context key is one edit here.
15
+ *
16
+ * ONE SPEC BUILDER (`AGENT_TASK_SPEC`) is shared by the stash the run writes
17
+ * before the CLI and the kickoff here, so what the reviewer reads is
18
+ * byte-identical to what the agent read — the card's discussion included
19
+ * (0.106.0): both are handed the same `job.cardThread`, and the builder
20
+ * prints it inside the spec — and the card's files after it (0.112.0).
21
+ *
22
+ * Pure: the run measures (the files, the roster's answers, the tools note)
23
+ * and hands the values in; nothing here reads a disk or a clock.
24
+ */
25
+ import { AGENT_TASK_KICKOFF, AGENT_HUMAN_KICKOFF, SYSTEM_AGENT_FOR, withProjectContext } from './prompts.mjs';
26
+ import { INTERNAL_WAIT_CONTRACT } from './agentInternalWait.mjs';
27
+ import { ORCHESTRATOR_CONTRACT } from './orchestratorPrompt.mjs';
28
+
29
+ /** What every kickoff of this job is handed: the agent, the card and its
30
+ * discussion, the job's context handoffs, the card's files, the place in
31
+ * the run. */
32
+ const shared = (job, cardFiles) => ({
33
+ agentName: job.agentName,
34
+ task: job.task,
35
+ cardThread: job.cardThread,
36
+ investigationContext: job.investigationContext,
37
+ researchContext: job.researchContext,
38
+ cardFiles,
39
+ position: job.position ?? 1,
40
+ total: job.total ?? 1,
41
+ });
42
+
43
+ /** The turn's first prompt: the card, on a task turn; the person's words
44
+ * otherwise. `cardFiles` and `messageFiles` are the files already on disk. */
45
+ export const agentKickoff = (job, { cardFiles, messageFiles }) =>
46
+ job.kind === 'task' && job.task
47
+ ? AGENT_TASK_KICKOFF({
48
+ // The card's discussion (0.106.0), printed inside THE CARD by
49
+ // the same builder the run's stash wrote down — and the files
50
+ // on it (0.112.0), after it, the same way.
51
+ ...shared(job, cardFiles),
52
+ resumeBody: job.body,
53
+ })
54
+ : AGENT_HUMAN_KICKOFF({
55
+ // The discussion on the one card this message is about, in a
56
+ // fence of its own after the person's words (0.106.0).
57
+ ...shared(job, cardFiles),
58
+ // INTERRUPT NOW: the previous run was stopped to send this.
59
+ interrupted: job.interrupted === true,
60
+ message: job.body ?? '',
61
+ askedByName: job.askedByName,
62
+ // The files (0.112.0): the person's inside their words, the
63
+ // card's at the foot of its discussion.
64
+ messageFiles,
65
+ });
66
+
67
+ /** A live message sent while the turn runs, as the CLI is handed it — on an
68
+ * orchestrated card with its id, so the route it takes is reported. */
69
+ export const agentFollowupKickoff = (job, followup, { cardFiles, messageFiles, orchestrated }) =>
70
+ AGENT_HUMAN_KICKOFF({
71
+ ...shared(job, cardFiles),
72
+ message: followup.body + (followup.libraryPrompt ?? '') + (orchestrated ? `\nMessage id: ${followup.id}. Choose and report its route with card_orchestrator.route_message.` : ''),
73
+ askedByName: followup.askedByName,
74
+ messageFiles,
75
+ });
76
+
77
+ /** The turn's system prompt, in its fixed section order. */
78
+ export function agentTurnSystem({ taskKind, runtimeLabel, prerequisitesAccepted, knowledgeDir, artifactsAccepted, toolsNote, artifactInstructions, toolInstructions, repositoryRun, orchestrated }) {
79
+ // The knowledge paragraph, when this box holds a library — the same
80
+ // composer the tabs use, so an agent and a tab can never be told two
81
+ // different things about the same directory.
82
+ // Spoken to the CLI that runs it (0.115.0): a mockup on Codex is
83
+ // told it is Codex, never Claude.
84
+ return withProjectContext(SYSTEM_AGENT_FOR(taskKind, runtimeLabel, { prerequisitesAccepted }), {
85
+ knowledgeDir,
86
+ // ARTIFACTS (0.94.0), while the server can show one — an agent's
87
+ // land on its page, under the facts row.
88
+ artifacts: artifactsAccepted,
89
+ }) + (toolsNote ? `\n\n${toolsNote}` : '') + (artifactInstructions ? `\n\n${artifactInstructions}` : '') + `\n\n${INTERNAL_WAIT_CONTRACT}` + (toolInstructions ? `\n\n${toolInstructions}` : '') + (repositoryRun ? `\n\n${repositoryRun.note}\nRepository command argv: ${JSON.stringify([repositoryRun.tool?.config.command, ...(repositoryRun.tool?.config.args.slice(0, -1) ?? []), "__repository", "<name>"])}` : '') + (orchestrated ? `\n\n${ORCHESTRATOR_CONTRACT}` : '');
90
+ }
@@ -21,11 +21,13 @@
21
21
  */
22
22
  import { existsSync } from 'node:fs';
23
23
  import { isSafePathSegment } from './git.mjs';
24
+ import { AGENT_PLACE_PREFIX } from './workPlaces.mjs';
24
25
  import { c, note } from './ui.mjs';
25
26
  import { createAgentTurnReports } from './workAgentTurnReports.mjs';
26
27
  import { createAgentTurnExecution } from './workAgentTurnExecution.mjs';
27
- import { failedTurnSettlement } from './workAgentTurnOutcome.mjs';
28
- import { killGroup } from './processGroup.mjs';
28
+ import { failedTurnSettlement, interruptedTurnSettlement } from './workAgentTurnOutcome.mjs';
29
+ import { turnCommits } from './turnReceipts.mjs';
30
+ import { interruptThenKill } from './processGroup.mjs';
29
31
  import { createDeliveryReporter } from './workDelivery.mjs';
30
32
  import { createAgentBriefRunner } from './workAgentBriefRunner.mjs';
31
33
 
@@ -54,12 +56,15 @@ export function createWorkAgentTurns({
54
56
  /** The turn's files, fetched into its worktree (workAttachments.mjs, 0.112.0). */
55
57
  fetchAgentFiles,
56
58
  makeBrief = createAgentBriefRunner({ admit, workChildren }),
59
+ /** The run's live-message relay (workLiveMessages.mjs); a test's stand-in. */
60
+ makeLiveMessages,
57
61
  }) {
58
62
  const reportDelivery = createDeliveryReporter();
59
63
  const {
60
64
  postAgentTurn,
61
65
  postAgentTrace,
62
66
  postAgentActivity,
67
+ postAgentContext,
63
68
  postAgentParked,
64
69
  pruneHeldReports,
65
70
  agentReported,
@@ -111,7 +116,7 @@ export function createWorkAgentTurns({
111
116
  * read before the stop's settle landed. Taken once, for a little while. */
112
117
  const stoppedFor = new Map(); // agentId -> when
113
118
 
114
- const { runAgentTurnInPlace, commitsBetween } = createAgentTurnExecution({
119
+ const { runAgentTurnInPlace } = createAgentTurnExecution({
115
120
  admit,
116
121
  agentRepositories,
117
122
  makeBrief,
@@ -131,8 +136,10 @@ export function createWorkAgentTurns({
131
136
  agentChildren,
132
137
  postAgentTrace,
133
138
  postAgentActivity,
139
+ postAgentContext,
134
140
  postAgentParked,
135
141
  fetchAgentFiles,
142
+ makeLiveMessages,
136
143
  });
137
144
 
138
145
  /** `releaseSlot` hands back the admission reservation the caller took on this
@@ -183,33 +190,42 @@ export function createWorkAgentTurns({
183
190
  * branch, the spend, the branch and worktree. Through the same held-body
184
191
  * wire, so a failed POST is re-sent on the next offer and the CLI is not
185
192
  * run again.
193
+ *
194
+ * The catch carries what the run's own settles carry, the live receipts
195
+ * included (2026-10-18): a note the CLI already took is folded into this
196
+ * run only when a settle names it, and the relay's own flush is bounded,
197
+ * so a catch that posted without them left the note pending — offered
198
+ * later as its own turn, a second paid run.
186
199
  */
187
200
  const measured = { wt: null, before: null, branch: null, usage: null, model: null };
188
201
  let settled = false;
202
+ const withLiveReceipts = (body) => {
203
+ const liveMessages = agentRuns.get(turnId)?.live?.receipts();
204
+ return { ...body, ...(liveMessages?.ids.length ? { liveMessages } : {}) };
205
+ };
189
206
  const settleOnce = (body) => {
190
207
  settled = true;
191
- const liveMessages = agentRuns.get(turnId)?.live?.receipts();
192
- return postAgentTurn({ ...body, ...(liveMessages?.ids.length ? { liveMessages } : {}) });
208
+ return postAgentTurn(withLiveReceipts(body));
193
209
  };
194
210
 
195
211
  // A WRITER on the agent's own place — see the lane header. Only `a-<id>`
196
212
  // places: those are agents' by construction, and anything else here would
197
213
  // be a tab's directory, where a turn is a reader by the product's own law.
198
214
  const run = agentRuns.get(turnId) ?? { place, interrupted: false };
199
- await inPlace(place, place.startsWith('a-'), async () => {
215
+ await inPlace(place, place.startsWith(AGENT_PLACE_PREFIX), async () => {
200
216
  run.waitingReason = null;
201
217
  try {
202
218
  await runAgentTurnInPlace(job, { turnId, agentId, place, releaseSlot, postAgentTurn: settleOnce, measured, run });
203
219
  if (run.interrupted && measured.ranCli) stoppedFor.set(agentId, Date.now());
204
220
  } catch (error) {
205
221
  if (settled) return;
206
- const commits = measured.repositoryReceipts ? [...new Set(measured.repositoryReceipts().map((r) => r.sha))].slice(0, 50) : measured.wt ? commitsBetween(measured.wt, measured.before) : [];
222
+ const commits = turnCommits({ repositoryReceipts: measured.repositoryReceipts, wt: measured.wt, before: measured.before, baseRef });
207
223
  if (run.interrupted) {
208
- await postAgentTurn({ turnId, outcome: 'nothing', interrupted: true, ran: measured.ranCli === true, commits, ...(measured.usage ? { usage: measured.usage } : {}) });
224
+ await postAgentTurn(withLiveReceipts(interruptedTurnSettlement({ turnId, ran: measured.ranCli === true, commits, usage: measured.usage, model: measured.model, branch: measured.branch, wt: measured.wt })));
209
225
  return;
210
226
  }
211
227
  await postAgentTurn(
212
- failedTurnSettlement({
228
+ withLiveReceipts(failedTurnSettlement({
213
229
  turnId,
214
230
  error,
215
231
  commits,
@@ -217,7 +233,7 @@ export function createWorkAgentTurns({
217
233
  model: measured.model,
218
234
  branch: measured.branch,
219
235
  wt: measured.wt,
220
- })
236
+ }))
221
237
  );
222
238
  }
223
239
  }, () => {
@@ -299,7 +315,7 @@ export function createWorkAgentTurns({
299
315
  const place = String(job.placeId || '');
300
316
  const wt = typeof held.body.worktree === 'string' ? held.body.worktree : null;
301
317
  if (reply?.review === true && isSafePathSegment(place) && wt && existsSync(wt)) {
302
- await inPlace(place, place.startsWith('a-'), () =>
318
+ await inPlace(place, place.startsWith(AGENT_PLACE_PREFIX), () =>
303
319
  runReviewEntry(String(job.agentId || ''), wt, job.agentName)
304
320
  );
305
321
  }
@@ -420,7 +436,7 @@ export function createWorkAgentTurns({
420
436
  run.interrupted = true;
421
437
  run.abortSetup?.();
422
438
  const child = agentChildren.get(run.place);
423
- if (child) killGroup(child);
439
+ if (child) interruptThenKill(child);
424
440
  continue;
425
441
  }
426
442
  if (agentReported.has(id) || notRunningSaid.has(id)) continue;
@@ -15,6 +15,7 @@
15
15
  * number of times; the stored files stay listed, which is the truth.
16
16
  */
17
17
  import { existsSync } from 'node:fs';
18
+ import { agentIdFromPlace } from './workPlaces.mjs';
18
19
 
19
20
  /** Attempts per agent per process, and the wait between two of them. */
20
21
  const MAX_TRIES = 4;
@@ -37,7 +38,7 @@ export function createWorkArtifactReconcile({
37
38
  const reconcileOne = async (place) => {
38
39
  const wt = placeDir(place);
39
40
  if (!wt || !existsSync(wt)) return;
40
- const agentId = place.slice(2);
41
+ const agentId = agentIdFromPlace(place);
41
42
  try {
42
43
  await inPlace(place, true, () => artifacts.reconcile({ placeDir: wt, before: beforeArtifacts(wt), agentId }));
43
44
  done.add(place);
@@ -58,7 +59,7 @@ export function createWorkArtifactReconcile({
58
59
  for (const id of done) if (!live.has(id)) done.delete(id);
59
60
  for (const id of tries.keys()) if (!live.has(id)) tries.delete(id);
60
61
  const due = activeIds.filter((place) => {
61
- if (typeof place !== 'string' || !place.startsWith('a-') || place.length <= 2) return false;
62
+ if (agentIdFromPlace(place) === null) return false;
62
63
  if (done.has(place) || placeLocks.has(place)) return false; // a turn or a merge is there
63
64
  const t = tries.get(place);
64
65
  return !t || (t.n < MAX_TRIES && now() - t.at >= RETRY_MS);
@@ -54,6 +54,8 @@ export function createWorkAttachments() {
54
54
  * doing that check is one deploy away from being zero places.
55
55
  */
56
56
  const UPLOAD_DIR = '.flowviant/uploads';
57
+ /** The pause before a file fetch's one retry. */
58
+ const RETRY_FETCH_MS = 1_000;
57
59
  // `safeFileName` (bin/lib/safeFileName.mjs — the server's rule, one daemon
58
60
  // home): THE EXTENSION SURVIVES A CUT, and it is idempotent over its own
59
61
  // output, so re-applying it to what the server already sanitized is a no-op
@@ -151,10 +153,17 @@ export function createWorkAttachments() {
151
153
  if (!a?.id || typeof a.id !== 'string' || !/^[0-9a-f-]{8,64}$/i.test(a.id)) continue;
152
154
  if (Number(a.size) > ATTACHMENT_MAX_BYTES) continue;
153
155
  try {
154
- const res = await fetch(`${FILE_URL[endpoint]}/${a.id}`, {
156
+ // ONE MORE TRY for a blip (2026-10-19): a dropped connection or a 5xx
157
+ // cost the file outright. A 4xx is the server's answer and stands.
158
+ const get = () => fetch(`${FILE_URL[endpoint]}/${a.id}`, {
155
159
  headers: { Authorization: `Bearer ${FLEET_TOKEN}`, 'User-Agent': USER_AGENT },
156
160
  signal: AbortSignal.timeout(60_000),
157
161
  });
162
+ let res = await get().catch(() => null);
163
+ if (!res || res.status >= 500) {
164
+ await new Promise((r) => setTimeout(r, RETRY_FETCH_MS));
165
+ res = await get();
166
+ }
158
167
  if (!res.ok) continue;
159
168
  const buf = Buffer.from(await res.arrayBuffer());
160
169
  if (buf.byteLength === 0 || buf.byteLength > ATTACHMENT_MAX_BYTES) continue;
@@ -187,12 +196,25 @@ export function createWorkAttachments() {
187
196
  return landed;
188
197
  };
189
198
 
190
- /** @returns relative paths written, in the order the human attached them. */
191
- const fetchAttachments = async (wt, attachments) => {
199
+ /**
200
+ * A TAB MESSAGE'S FILES, each `{ path }` landed or `{ missed: name }`
201
+ * (2026-10-19). The Terminal listed only what arrived, on the argument that
202
+ * its person is watching and re-attaches — but the person sees the file's
203
+ * chip on their message whether it arrived or not, and a CLI told nothing
204
+ * answered as if it had read it. Named, a miss is said in the reply, as the
205
+ * agent lane's always were (`fetchAgentFiles`).
206
+ */
207
+ const fetchTabFiles = async (wt, attachments) => {
192
208
  if (!Array.isArray(attachments) || attachments.length === 0) return [];
193
- return (await landFiles(wt, attachments.slice(0, 8), 'attachment', false)).filter(Boolean);
209
+ const list = attachments.slice(0, 8);
210
+ const landed = await landFiles(wt, list, 'attachment', false);
211
+ return list.map((a, i) => (landed[i] ? { path: landed[i] } : { missed: safeUploadName(a?.name) }));
194
212
  };
195
213
 
214
+ /** @returns relative paths written, in the order the human attached them. */
215
+ const fetchAttachments = async (wt, attachments) =>
216
+ (await fetchTabFiles(wt, attachments)).flatMap((f) => (f.path ? [f.path] : []));
217
+
196
218
  /**
197
219
  * AN AGENT TURN'S FILES (0.112.0), read off the wire (agentFiles.mjs: each
198
220
  * origin at the server's cap, message files first) and fetched from
@@ -218,5 +240,5 @@ export function createWorkAttachments() {
218
240
  return out;
219
241
  };
220
242
 
221
- return { fetchAttachments, fetchAgentFiles };
243
+ return { fetchAttachments, fetchTabFiles, fetchAgentFiles };
222
244
  }
@@ -1,17 +1,19 @@
1
+ /** The daemon holds every selected place while one personal CLI integrates it. */
1
2
  import { trackedRepositorySetup } from './repositorySetup.mjs';
2
3
  import { git } from './git.mjs';
3
4
  import { knowledgeDirFor } from './knowledgeFiles.mjs';
4
- /** The daemon holds every selected place while one personal CLI integrates it. */
5
- import { integrateMergeBatch, mergeBatchPrompt } from './mergeBatch.mjs';
5
+ import { integrateMergeBatch } from './mergeBatch.mjs';
6
+ import { mergeBatchPrompt, MERGE_BATCH_SYSTEM } from './repositoryPrompts.mjs';
6
7
  import { runTurn } from './runTurn.mjs';
7
8
  import { runCappedTurn } from './workCappedTurn.mjs';
8
- import { candidateObjects } from './agentPlan.mjs';
9
+ import { candidateObjects } from './modelJson.mjs';
9
10
  import { pickRuntimeFor } from './runtimeDetection.mjs';
10
11
  import { fleetRequest } from './fleetPost.mjs';
11
12
  import { fleetEndpoint } from './fleetWire.mjs';
12
13
  import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
13
14
  import { worktreePath } from './workPlaces.mjs';
14
15
  import { scrub } from './uplinkScrub.mjs';
16
+ import { relayedText } from './prWorkflow.mjs';
15
17
  import { isSafePathSegment, stripUrlUserinfo } from './git.mjs';
16
18
  import { killGroup } from './processGroup.mjs';
17
19
 
@@ -63,7 +65,7 @@ export function createWorkMergeBatch({ repositoryPlaces, repoRoot, baseDir, inPl
63
65
  const result = await runCappedTurn({ runTurn: runCli, workChildren, releaseSlot: release,
64
66
  transcriptCwd: context.repositories[0].integrationWorktree, capMs: 20 * 60_000, killChild: killGroup,
65
67
  onSpawn: (ch) => { child = ch; groupKillChildren?.add(ch); },
66
- turnArgs: { prompt: mergeBatchPrompt(context), system: 'You are the person’s local merge coordinator. Integrate the whole authorized selection and preserve every card’s intent.',
68
+ turnArgs: { prompt: mergeBatchPrompt(context), system: MERGE_BATCH_SYSTEM,
67
69
  runtime, profile: 'build', knowledgeDir: knowledgeDirFor(repoRoot), model: leader.model || undefined, effort: leader.effort || undefined,
68
70
  cwd: context.repositories[0].integrationWorktree, writableDirs: context.repositories.filter((r) => !r.alreadyLanded).map((r) => r.integrationWorktree),
69
71
  answerFromResult: true, label: '[batch merge]' } });
@@ -88,7 +90,7 @@ export function createWorkMergeBatch({ repositoryPlaces, repoRoot, baseDir, inPl
88
90
  // After a successful push/report attempt, leave the lease for replay: a transport
89
91
  // failure says nothing about whether the server committed the final receipt.
90
92
  if (claimed && !reporting) {
91
- const detail = scrub(stripUrlUserinfo([e?.stdout, e?.stderr, e?.message].filter(Boolean).join('\n'))).slice(0, 2000);
93
+ const detail = relayedText([e?.stdout, e?.stderr, e?.message].filter(Boolean).join('\n')).slice(0, 2000);
92
94
  await request('agent-merge-batch-done', { batchId: spec.id, ok: false, detail }).catch(() => {});
93
95
  }
94
96
  } finally {
@@ -82,7 +82,39 @@ export function createWorkNarration() {
82
82
  }, wait);
83
83
  timer.unref?.(); // never hold the process open for a spinner
84
84
  };
85
+ // THE CONTEXT READOUT rides the same door with its turn (2026-10-19):
86
+ // the latest measurement only, one POST at a time, never a turn's failure.
87
+ // The server writes no narration line for a context post.
88
+ let ctxPending = null;
89
+ let ctxSending = false;
90
+ const sendContext = async () => {
91
+ ctxSending = true;
92
+ while (ctxPending && !stopped) {
93
+ const context = ctxPending;
94
+ ctxPending = null;
95
+ try {
96
+ await fetch(ACTIVITY_URL, {
97
+ method: 'POST',
98
+ headers: {
99
+ Authorization: `Bearer ${FLEET_TOKEN}`,
100
+ 'User-Agent': USER_AGENT,
101
+ 'Content-Type': 'application/json',
102
+ },
103
+ signal: AbortSignal.timeout(10_000),
104
+ body: JSON.stringify({ sessionId, turnId, lines: [], context }),
105
+ });
106
+ } catch {
107
+ /* a readout is decoration, like the line */
108
+ }
109
+ }
110
+ ctxSending = false;
111
+ };
85
112
  return {
113
+ context(measured) {
114
+ if (!turnId || stopped || !measured) return;
115
+ ctxPending = measured;
116
+ if (!ctxSending) void sendContext();
117
+ },
86
118
  line(label) {
87
119
  // Scrub, like every string that leaves this machine: a narration line
88
120
  // is the CLI's own stdout — a command echoing an env var, a read of a
@@ -20,7 +20,7 @@ import { RESUME_MARKERS } from './conversationIds.mjs';
20
20
  import { worktreeDiff } from './worktreeDiff.mjs';
21
21
  import { myPubB64 } from './boxIdentity.mjs';
22
22
  import { fleetEndpoint } from './fleetWire.mjs';
23
- import { isValidPlace } from './workPlaces.mjs';
23
+ import { isValidPlace, AGENT_PLACE_PREFIX, agentIdFromPlace } from './workPlaces.mjs';
24
24
 
25
25
  export function createWorkPlaceReports({
26
26
  repositoryPlaces,
@@ -176,7 +176,7 @@ export function createWorkPlaceReports({
176
176
  * TABS GET NOTHING. A tab's place is shared by design and its work is a
177
177
  * human's own directory; attributing one would be a fact with no reader.
178
178
  */
179
- const pub = sessionId.startsWith('a-') && sessionId.length > 2 ? myPubB64() : null;
179
+ const pub = agentIdFromPlace(sessionId) !== null ? myPubB64() : null;
180
180
  /**
181
181
  * …AND WHAT THIS MACHINE PUSHED OF IT (0.86.0).
182
182
  *
@@ -193,7 +193,7 @@ export function createWorkPlaceReports({
193
193
  * Mutually exclusive without a rule of its own, because the state it reads
194
194
  * holds a sha or an error and never both.
195
195
  */
196
- const pushed = sessionId.startsWith('a-') ? agentPublished.get(sessionId) : null;
196
+ const pushed = sessionId.startsWith(AGENT_PLACE_PREFIX) ? agentPublished.get(sessionId) : null;
197
197
  return {
198
198
  sessionId,
199
199
  ...d,
@@ -29,6 +29,20 @@ export const REPO_PLACE = 'repo';
29
29
  * audit F163). */
30
30
  export const PERSON_PLACE_PREFIX = 'u-';
31
31
 
32
+ /** An AGENT's place is `a-<agentId>` — the shared AGENT_PLACE_PREFIX /
33
+ * agentPlaceId; a wire value held by the release parity gate (2026-10-18,
34
+ * the SOLID pass, as F163 holds the other two). Its writer lock, its envpub
35
+ * and the artifact reconcile all key on it. */
36
+ export const AGENT_PLACE_PREFIX = 'a-';
37
+
38
+ /** The agent a place names, or null for every other place shape — the shared
39
+ * `agentIdFromPlace`, mirrored: the prefix alone names nothing. */
40
+ export function agentIdFromPlace(place) {
41
+ return typeof place === 'string' && place.startsWith(AGENT_PLACE_PREFIX) && place.length > AGENT_PLACE_PREFIX.length
42
+ ? place.slice(AGENT_PLACE_PREFIX.length)
43
+ : null;
44
+ }
45
+
32
46
  /**
33
47
  * A PLACE THIS MACHINE WILL RESOLVE: the checkout, or a safe path segment.
34
48
  *
@@ -19,6 +19,7 @@ import { sweepMergedBranch } from './shipSweep.mjs';
19
19
  import { note } from './ui.mjs';
20
20
  import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
21
21
  import { fleetRequest } from './fleetPost.mjs';
22
+ import { createReportJournal } from './reportJournal.mjs';
22
23
 
23
24
  export function createWorkReportQueue({
24
25
  repoRoot,
@@ -29,6 +30,9 @@ export function createWorkReportQueue({
29
30
  onRepoChanged,
30
31
  reportPlaceWorktrees,
31
32
  burstListeners,
33
+ /** Where the queue survives a restart (`reportJournal.mjs`); absent, the
34
+ * queue lives in memory only, as it did before 2026-10-19. */
35
+ journalDir = null,
32
36
  }) {
33
37
  const WORK_DONE_URL = fleetEndpoint('work-turn-done', FLEET_URL);
34
38
  const SHIP_DONE_URL = fleetEndpoint('ship-done', FLEET_URL);
@@ -43,6 +47,16 @@ export function createWorkReportQueue({
43
47
  */
44
48
  const pendingWorkReports = new Map(); // turnId -> work-turn-done body
45
49
  const pendingShipReports = new Map(); // sessionId -> ship-done body
50
+ // …AND ON DISK, so a restart between the answer and the 200 delivers the
51
+ // answer rather than re-running the turn (reportJournal.mjs). Seeded once;
52
+ // the first poll's flush delivers what a previous process left.
53
+ const journal = journalDir ? createReportJournal(journalDir) : null;
54
+ if (journal) {
55
+ const kept = journal.load();
56
+ for (const [id, body] of kept.work) pendingWorkReports.set(id, body);
57
+ for (const [id, body] of kept.ship) pendingShipReports.set(id, body);
58
+ }
59
+ const persist = () => journal?.save(pendingWorkReports, pendingShipReports);
46
60
  /** POST a settle body. Four outcomes, and the split between the last two is
47
61
  * load-bearing:
48
62
  * - 'ok' — delivered.
@@ -142,10 +156,13 @@ export function createWorkReportQueue({
142
156
  const deliver = async (q, id, body) => {
143
157
  const r = await postSettle(q.url, body, q.terminal);
144
158
  if (r === 'retry' || r === 'reject') {
159
+ const fresh = !q.pending.has(id);
145
160
  q.pending.set(id, body);
161
+ if (fresh) persist();
146
162
  if (r === 'reject') reportBackoff.set(backoffKey(q, id), Date.now() + REJECT_RETRY_MS);
147
163
  } else {
148
- q.pending.delete(id);
164
+ const held = q.pending.delete(id);
165
+ if (held) persist();
149
166
  reportBackoff.delete(backoffKey(q, id));
150
167
  q.delivered(id);
151
168
  }
@@ -8,17 +8,14 @@
8
8
  * THE ID SHAPES MOVED to a leaf (audit 2026-10-10, F157): conversationIds.mjs
9
9
  * owns every CLI's conversation-id guard and marker name, because the
10
10
  * machine readers below the lanes (runtimeLimits, the session scanners) need
11
- * them too and a lane module is not theirs to import. The two guards the
12
- * agent lane imports from here are re-exported unchanged.
11
+ * them too and a lane module is not theirs to import.
13
12
  */
14
13
  import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
15
14
  import { join } from 'node:path';
16
15
  import { homedir } from 'node:os';
17
16
  import { canRun, RUNTIMES, runtimeById, runtimeLabel } from './runtimes.mjs';
18
17
  import { detectRuntimes } from './runtimeDetection.mjs';
19
- import { AGY_CONV_RE, CODEX_THREAD_RE } from './conversationIds.mjs';
20
-
21
- export { AGY_CONV_RE, CODEX_THREAD_RE };
18
+ import { AGY_CONV_RE } from './conversationIds.mjs';
22
19
 
23
20
  export function createWorkSessionRuntime({ sessionMetaPath }) {
24
21
  /**