omp-conductor 0.19.7 → 0.20.1

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 (71) hide show
  1. package/REFERENCE.md +10 -1
  2. package/agents/to-spec.md +76 -9
  3. package/package.json +1 -1
  4. package/schema/config.schema.json +4 -0
  5. package/src/admission.ts +58 -14
  6. package/src/arm-challenge.ts +255 -85
  7. package/src/ask.ts +130 -615
  8. package/src/board.ts +7 -1
  9. package/src/brief-upgrade.ts +24 -0
  10. package/src/briefs/console.md +258 -0
  11. package/src/briefs/correction.md +203 -0
  12. package/src/briefs/orchestrator.md +167 -97
  13. package/src/briefs/policy.md +19 -16
  14. package/src/briefs/to-spec.md +76 -9
  15. package/src/briefs/worker.md +50 -16
  16. package/src/cli.ts +4 -0
  17. package/src/command-manifest.ts +54 -8
  18. package/src/commands/arm.ts +115 -49
  19. package/src/commands/console.ts +70 -0
  20. package/src/commands/context.ts +2 -0
  21. package/src/commands/epic.ts +132 -0
  22. package/src/commands/extend.ts +9 -1
  23. package/src/commands/intake.ts +44 -14
  24. package/src/commands/stats.ts +19 -4
  25. package/src/commands/worker.ts +9 -1
  26. package/src/config-schema.ts +13 -0
  27. package/src/config.ts +27 -0
  28. package/src/daemon/ack.ts +159 -0
  29. package/src/daemon/admission-pass.ts +135 -0
  30. package/src/daemon/brief.ts +461 -0
  31. package/src/daemon/deps.ts +539 -0
  32. package/src/daemon/dispatch.ts +1779 -0
  33. package/src/daemon/drain.ts +185 -0
  34. package/src/daemon/groom-pass.ts +422 -0
  35. package/src/daemon/http.ts +417 -0
  36. package/src/daemon/integrity.ts +108 -0
  37. package/src/daemon/panes.ts +180 -0
  38. package/src/daemon/review.ts +1888 -0
  39. package/src/daemon/runtime.ts +788 -0
  40. package/src/daemon/settle-pass.ts +606 -0
  41. package/src/daemon/supervision.ts +438 -0
  42. package/src/daemon/tick.ts +968 -0
  43. package/src/daemon/views.ts +751 -0
  44. package/src/daemon.ts +105 -7923
  45. package/src/dashboard/app.js +58 -0
  46. package/src/dashboard/controls.ts +22 -3
  47. package/src/dashboard/server.ts +4 -0
  48. package/src/diff-flags.ts +135 -9
  49. package/src/doctor.ts +2 -2
  50. package/src/failure-class.ts +257 -2
  51. package/src/fleet.ts +295 -176
  52. package/src/groom.ts +461 -0
  53. package/src/http-token.ts +142 -0
  54. package/src/knowledge.ts +229 -0
  55. package/src/mining.ts +316 -0
  56. package/src/orchestrator-tick.ts +689 -1670
  57. package/src/ready-gate.ts +267 -0
  58. package/src/settlement.ts +107 -11
  59. package/src/setup-host.ts +32 -9
  60. package/src/setup-wizard.ts +55 -7
  61. package/src/setup.ts +229 -3
  62. package/src/stats.ts +257 -2
  63. package/src/status-render.ts +169 -14
  64. package/src/store.ts +618 -28
  65. package/src/to-spec.ts +426 -44
  66. package/src/tracker/github.ts +50 -0
  67. package/src/types.ts +434 -18
  68. package/src/verbs/protocol.ts +28 -0
  69. package/src/verbs/server.ts +330 -39
  70. package/src/wake.ts +19 -2
  71. package/src/worker.ts +570 -1
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Brief building: everything that turns a routed issue plus the host's facts
3
+ * into the text a worker session actually reads.
4
+ *
5
+ * The cut is "what the dispatcher renders" versus "what the dispatcher does".
6
+ * Nothing in here claims an issue, opens a worktree or launches anything, and
7
+ * nothing in here takes `Deps` — the caller passes the pieces, which is what
8
+ * makes the gate grouping, the discussion budget and the whole assembled brief
9
+ * assertable against a fixture instead of against a dispatched run.
10
+ *
11
+ * `BRIEF_TEMPLATE_PATH` resolves through `PACKAGE_SRC_DIR` rather than this
12
+ * file's own `import.meta.dir`: the templates stayed in `src/briefs/` while this
13
+ * module moved a directory deeper, and a path that is silently one level off
14
+ * fails at runtime, not at the type check.
15
+ */
16
+ import { join } from "node:path";
17
+ import { effectiveLane, effectiveModel, laneEcho } from "../admission.ts";
18
+ import { graphHint } from "../graph.ts";
19
+ import { hostConstraintsNotice } from "../host.ts";
20
+ import { knowledgeSection } from "../knowledge.ts";
21
+ import { releaseShapeFromCommand, sharedHostBriefNotice, type GateShape } from "../release-policy.ts";
22
+ import type { Routed } from "../routing.ts";
23
+ import type { EffectiveModel, FileLane, HostConstraints, IssueComment, ProjectConfig, ReadyIssue, RepoTarget } from "../types.ts";
24
+ import { renderBrief } from "../worker.ts";
25
+ import { PACKAGE_SRC_DIR, knowledgeRepoKey, repoSlug } from "./deps.ts";
26
+
27
+ export const BRIEF_TEMPLATE_PATH = join(PACKAGE_SRC_DIR, "briefs", "worker.md");
28
+
29
+ /** One configured pre-push gate: the exact command CI runs, and its cwd. */
30
+ export type Gate = RepoTarget["gates"][number];
31
+
32
+ /** A configured gate the guard will refuse a worker, with the shape that decided it. */
33
+ export interface CiOwnedGate {
34
+ gate: Gate;
35
+ shape: GateShape;
36
+ }
37
+
38
+ /**
39
+ * The repo's pre-push gates split into the two groups a worker brief must keep
40
+ * apart: the ones the worker runs locally, and the ones the guard refuses it
41
+ * (#1042).
42
+ *
43
+ * One typed decision drives both — `releaseShapeFromCommand`, the same
44
+ * classifier the run-time interlock consults. Every shape it names is refused
45
+ * for a worker session: `SHARED_HOST_SHAPE` unconditionally, because a
46
+ * whole-package `bun test` is the exact load that stopped this shared host,
47
+ * and every release shape because a worker holds no grant whatever the config
48
+ * says (#126). So a gate it names must never reach the "run every one of
49
+ * them" list: commanding it buys a burnt turn, a refusal, and a
50
+ * `claimed-proof-blocked` settlement, which is the #1042 incident. Anything
51
+ * the classifier does not name is the worker's to run, which keeps a focused
52
+ * `bun test src/x.test.ts` local.
53
+ */
54
+ export function workerGateGroups(repo: RepoTarget): { local: Gate[]; ciOwned: CiOwnedGate[] } {
55
+ const local: Gate[] = [];
56
+ const ciOwned: CiOwnedGate[] = [];
57
+ for (const gate of repo.gates) {
58
+ const shape = releaseShapeFromCommand(gate.cmd);
59
+ if (shape === undefined) local.push(gate);
60
+ else ciOwned.push({ gate, shape });
61
+ }
62
+ return { local, ciOwned };
63
+ }
64
+
65
+ /**
66
+ * The locally runnable gates, with the "run every one" instruction scoped to
67
+ * exactly that group (#1042). An empty gate list keeps the honesty line it
68
+ * always had; a repo whose every gate is CI-owned renders nothing here rather
69
+ * than a heading over an empty list — {@link ciOwnedGatesBlock} says so in its
70
+ * own words instead.
71
+ */
72
+ export function gatesBlock(slug: string, groups: { local: Gate[]; ciOwned: CiOwnedGate[] }): string {
73
+ if (groups.local.length === 0) {
74
+ return groups.ciOwned.length === 0
75
+ ? "_No pre-push gates are configured for this repo. Say so in your report rather than inventing one._"
76
+ : "";
77
+ }
78
+ return [
79
+ `These are the gates for \`${slug}\` that are yours to run before pushing:`,
80
+ "",
81
+ groups.local.map((g) => `- \`${g.cmd}\` — run from \`${g.cwd}\``).join("\n"),
82
+ "",
83
+ "Run every one of these, from the directory listed, over the **whole tree** — not",
84
+ "just the directory you edited. Linting only the source dir is how an error in a",
85
+ "migration, a config file or a script reaches the runners.",
86
+ ].join("\n");
87
+ }
88
+
89
+ /**
90
+ * The CI-owned gates, each naming the shape that classified it so the worker
91
+ * reads *why* it is not theirs rather than guessing (#1042). Renders nothing
92
+ * when there are none, so an unguarded repo's brief is byte-for-byte the
93
+ * single-group brief it always was.
94
+ *
95
+ * The leading blank lines belong to the value, not the template: the
96
+ * placeholder sits flush against `{{GATES}}` so an empty group leaves no
97
+ * orphaned whitespace, the same convention `hostConstraintsNotice` uses.
98
+ */
99
+ export function ciOwnedGatesBlock(slug: string, groups: { local: Gate[]; ciOwned: CiOwnedGate[] }): string {
100
+ if (groups.ciOwned.length === 0) return "";
101
+ return [
102
+ "",
103
+ "",
104
+ groups.local.length === 0
105
+ ? `Every configured gate for \`${slug}\` is CI-owned on this host:`
106
+ : `CI owns these gates for \`${slug}\` on this host:`,
107
+ "",
108
+ groups.ciOwned
109
+ .map(
110
+ ({ gate, shape }) =>
111
+ `- \`${gate.cmd}\` (from \`${gate.cwd}\`) — classified \`${shape}\`; the guard refuses this one for a worker session`,
112
+ )
113
+ .join("\n"),
114
+ "",
115
+ "**Do not run these locally, and do not claim them as proof.** They run in CI on",
116
+ "your PR, which is where their verdict comes from. Attempting one here returns a",
117
+ "refusal, not evidence, and costs you a turn you needed for the work.",
118
+ ].join("\n");
119
+ }
120
+
121
+ export function acceptanceCriteria(issue: ReadyIssue): string {
122
+ const body = issue.body.trim();
123
+ // ponytail: the whole issue body stands in for a criteria section. Upgrade
124
+ // path is parsing the "## Acceptance criteria" heading once issue templates
125
+ // are consistent enough to trust; a fuzzy extraction today would silently
126
+ // drop context the worker needs, and the brief already tells it to read the
127
+ // issue itself.
128
+ return body.length > 0
129
+ ? body
130
+ : "_The issue body is empty. Read the issue and its comments, and escalate if it is genuinely underspecified._";
131
+ }
132
+
133
+ /**
134
+ * How much of a comment thread one brief may hold. Grooming notes are short,
135
+ * and a thread that outgrows this is truncated with an explicit marker rather
136
+ * than a silently dropped tail — the disappearance this fix exists to prevent.
137
+ */
138
+ export const DISCUSSION_CHARS_BUDGET = 8_000;
139
+
140
+ /**
141
+ * The issue's comments as a brief section: attributed, numbered, oldest
142
+ * first, so a later correction visibly supersedes an earlier note. Rendered
143
+ * beside the body at dispatch, because the orchestrator's grooming is posted
144
+ * as comments and a worker must never depend on a runtime read to see it.
145
+ *
146
+ * `"unread"` is not "empty": when the tracker refused at dispatch the section
147
+ * says so, instead of silently reading as "no discussion" — which is exactly
148
+ * the #517 failure. An empty comment list renders nothing, so a commentless
149
+ * issue's brief stays byte-identical to what this package has always shipped.
150
+ */
151
+ export function renderDiscussion(comments: IssueComment[] | "unread", lane?: FileLane): string {
152
+ if (comments === "unread") {
153
+ const lines = [
154
+ "## Discussion",
155
+ "",
156
+ "_The issue's comments could not be read at dispatch time. The live read below is_",
157
+ "_the only path to them; if it prints nothing, that is a failed read, not an_",
158
+ "_absence of discussion._",
159
+ "",
160
+ ];
161
+ // The lane admission enforced is carried through dispatch, so even an
162
+ // unreadable thread cannot hide it from the worker (#608): the gate's
163
+ // effective declaration renders from the admission snapshot, not from the
164
+ // read that just failed. A body declaration needs no note — the body
165
+ // always renders.
166
+ if (lane !== undefined && lane.at !== "body") {
167
+ lines.push(
168
+ "_The effective file lane below was enforced at admission — it supersedes any earlier declaration._",
169
+ lane.source,
170
+ "",
171
+ );
172
+ }
173
+ return lines.join("\n");
174
+ }
175
+ if (comments.length === 0) return "";
176
+ const total = comments.length;
177
+ const lines = [
178
+ "## Discussion",
179
+ "",
180
+ `${total} comment${total === 1 ? "" : "s"} on the issue at dispatch, oldest first — a later`,
181
+ "comment supersedes an earlier one.",
182
+ "",
183
+ ];
184
+ let chars = lines.join("\n").length;
185
+ let shown = 0;
186
+ for (const comment of comments) {
187
+ const block = `**@${comment.author} — comment ${shown + 1}:**\n\n${comment.body}\n\n`;
188
+ if (shown > 0 && chars + block.length > DISCUSSION_CHARS_BUDGET) break;
189
+ lines.push(`**@${comment.author} — comment ${shown + 1}:**`, "", comment.body, "");
190
+ chars += block.length;
191
+ shown += 1;
192
+ }
193
+ if (shown < total) {
194
+ const omitted = total - shown;
195
+ lines.push(
196
+ `… ${omitted} comment${omitted === 1 ? "" : "s"} omitted — the discussion exceeded ${DISCUSSION_CHARS_BUDGET} characters.`,
197
+ "Read the issue for the tail.",
198
+ "",
199
+ );
200
+ }
201
+ // The file-lane gate resolves its effective lane across the whole thread,
202
+ // not just what this budget renders, so a winning declaration beyond the
203
+ // budget would otherwise control admission while the worker never sees it
204
+ // (#608). Reproduce the winning declaration verbatim here — `at` records
205
+ // the comment it came from — so the rendered brief and the gate agree on
206
+ // the same lane, and the note itself re-parses through the same
207
+ // `File lane:` grammar. The note is suppressed only when the winning
208
+ // declaration is provably visible in the rendered thread (the body always
209
+ // renders, so a body winner needs none); a carried admission lane whose
210
+ // comment the dispatch re-read shifted or dropped still renders here,
211
+ // whether or not the thread was truncated.
212
+ if (
213
+ lane !== undefined &&
214
+ lane.at !== "body" &&
215
+ !comments.slice(0, shown).some((c) => c.body.includes(lane.source))
216
+ ) {
217
+ lines.push(
218
+ `_The effective file lane was declared in comment ${lane.at + 1} — it supersedes any earlier declaration._`,
219
+ lane.source,
220
+ "",
221
+ );
222
+ }
223
+ return lines.join("\n");
224
+ }
225
+
226
+ /**
227
+ * The parsed file lane as a brief section (#724): the file list the gate will
228
+ * enforce, or the explicit fail-open note — the same `laneEcho` the promotion
229
+ * verb prints, so the author and the worker read one parse. Deliberately only
230
+ * the parse, never the source line: the prose already renders in the body or
231
+ * the discussion, and in #720's case the prose is exactly what looked
232
+ * reasonable to a human while parsed greedily.
233
+ */
234
+ export function laneBlock(lane: FileLane | undefined): string {
235
+ const echo = laneEcho(lane);
236
+ const body = lane === undefined ? `_${echo}_` : `\`${echo}\``;
237
+ return ["## File lane (as parsed)", "", body, "", ""].join("\n");
238
+ }
239
+
240
+ /**
241
+ * The declared model as a brief section (#535): the selector the dispatch
242
+ * will launch on — the same parse admission carried, never a second read —
243
+ * or the explicit fail-open note, so the worker reads the run's model on the
244
+ * brief itself rather than inferring it. Deliberately only the selector,
245
+ * never the source line: the declaration's prose already renders in the body
246
+ * or Discussion.
247
+ */
248
+ export function modelBlock(model: EffectiveModel | undefined): string {
249
+ const body =
250
+ model === undefined
251
+ ? "_no model declared — the project's workerModel (or harness default) is in effect_"
252
+ : `\`${model.model}\``;
253
+ return ["## Model (as parsed)", "", body, "", ""].join("\n");
254
+ }
255
+
256
+ /**
257
+ * What an orphan-resumed worker is told about the file lane on top of the
258
+ * continuation notice (#608). The original brief already in the transcript
259
+ * may show an earlier declaration, while admission enforces the current one —
260
+ * replaying the whole brief would re-do the work, but continuing under a stale
261
+ * lane is the collision the gate exists to stop. The declaration is rendered
262
+ * verbatim, so it re-parses through the same `File lane:` grammar every
263
+ * surfaced declaration uses.
264
+ */
265
+ export function resumeLaneBlock(lane: FileLane): string {
266
+ return [
267
+ "",
268
+ "The file lane admission enforced for this continuation is below. It supersedes",
269
+ "any lane declaration in the brief already in your transcript:",
270
+ "",
271
+ lane.source,
272
+ "",
273
+ `Parsed files: ${laneEcho(lane)}.`,
274
+ "",
275
+ ].join("\n");
276
+ }
277
+
278
+ /**
279
+ * What a worker whose block has been answered is told, when its own session can
280
+ * be resumed (Phase 3 inner loop).
281
+ *
282
+ * The shape of `REVIEW_REVISION_PROMPT`, and for the same reason: the run's
283
+ * whole context is already in the transcript being continued, so replaying the
284
+ * cold brief is how a resumed worker re-reads a repo it just finished reading
285
+ * and re-does work it has already done. What it does NOT already have is the
286
+ * answer — that arrived after it parked — so the discussion travels with the
287
+ * prompt.
288
+ *
289
+ * The answer is not singled out, deliberately. Tracker comments carry no
290
+ * timestamp through this port, so the daemon cannot prove which comment is the
291
+ * answer; naming one would be a guess dressed as a fact. The thread newest-last
292
+ * plus "the answer is in it" is exactly what the daemon knows.
293
+ */
294
+ export const ANSWERED_BLOCK_PROMPT =
295
+ "You blocked this run for a decision, and the operator has now answered. Continue this same " +
296
+ "session: same issue, same branch, same worktree, and the same pull request if you had opened " +
297
+ "one — do not start over, and do not re-read what your transcript already holds. The issue's " +
298
+ "discussion as it stands now is below, oldest first: the answer to your blocking question is in " +
299
+ "it, and a later comment supersedes an earlier one. Act on it, finish the work against the same " +
300
+ "acceptance criteria, and settle exactly as you would have.";
301
+
302
+ export function renderAnsweredBlockPrompt(issue: number, discussion: string): string {
303
+ return [
304
+ ANSWERED_BLOCK_PROMPT,
305
+ "",
306
+ `Issue #${String(issue)} — the decision you were waiting on:`,
307
+ "",
308
+ discussion.trim().length === 0
309
+ ? "_No comment was posted on the issue. The block was cleared by an operator without a written " +
310
+ "answer: proceed on your own best reading of the acceptance criteria, and block again only if " +
311
+ "the question is genuinely still unanswerable._"
312
+ : discussion,
313
+ ].join("\n");
314
+ }
315
+
316
+ /**
317
+ * The worker's opening prompt.
318
+ *
319
+ * Exported for the same reason `salvageLines` is: this text is the entire
320
+ * context a session with the host's credentials gets, so the two things a test
321
+ * can hold it to are worth holding — that a configured graph reaches the worker,
322
+ * and that a project without one gets the brief this package has always shipped,
323
+ * to the byte.
324
+ */
325
+ export async function buildBrief(
326
+ project: ProjectConfig,
327
+ r: Routed,
328
+ branch: string,
329
+ worktree: string,
330
+ opts: {
331
+ continuation?: boolean;
332
+ defaultBranch?: string;
333
+ salvagedSha?: string;
334
+ /** The issue's comments, rendered into the Discussion section. `"unread"`
335
+ * when the tracker refused at dispatch — rare, but it must never read as
336
+ * "no comments" (the #517 failure mode). Absent means an empty list. */
337
+ comments?: IssueComment[] | "unread";
338
+ /**
339
+ * The effective file lane admission resolved for this candidate (#608).
340
+ * When carried, the brief renders exactly this value — the gate's own
341
+ * snapshot — instead of recomputing from the dispatch-time comment read,
342
+ * so a changed or failed second read can neither hide nor reword the
343
+ * lane admission enforced. Absent (unit-level callers), the lane is
344
+ * resolved from the rendered thread itself.
345
+ */
346
+ lane?: FileLane;
347
+ /**
348
+ * The effective `Model:` declaration admission resolved for this
349
+ * candidate (#535). When carried, the brief echoes exactly this
350
+ * selector — the same value dispatch launches on — instead of
351
+ * recomputing from the dispatch-time comment read, so the run's model is
352
+ * one value on every surface. Absent (unit-level callers), the model is
353
+ * resolved from the rendered thread itself.
354
+ */
355
+ model?: EffectiveModel;
356
+ /**
357
+ * The typed host-constraints block the brief renders (#721), re-read with
358
+ * the config at the tick boundary so an operator edit applies on the next
359
+ * dispatch. Absent renders no host-constraints section at all — the
360
+ * brief is byte-for-byte what it always was.
361
+ */
362
+ host?: HostConstraints;
363
+ } = {},
364
+ ): Promise<string> {
365
+ // Read per dispatch rather than caching: editing the brief then takes effect
366
+ // on the next issue instead of needing a daemon restart.
367
+ const template = await Bun.file(BRIEF_TEMPLATE_PATH).text();
368
+ const defaultBranch = opts.defaultBranch ?? r.repo.defaultBranch;
369
+ const continuation =
370
+ opts.continuation === true
371
+ ? [
372
+ "",
373
+ "## Continuation — do not start from zero",
374
+ "",
375
+ `You are **resuming** issue #${r.issue.number}. Branch \`${branch}\` already exists`,
376
+ "and was reattached with prior commits (and possibly a salvaged WIP tip).",
377
+ ...(opts.salvagedSha === undefined
378
+ ? []
379
+ : [
380
+ "",
381
+ `The previous attempt's uncommitted work was preserved for you as commit`,
382
+ `\`${opts.salvagedSha}\` on this branch. It is the tip you are continuing from,`,
383
+ "and it is the only copy of that work — do not reset past it or force-push over it.",
384
+ ]),
385
+ "Before writing anything:",
386
+ "",
387
+ "```bash",
388
+ "git log --oneline origin/" + defaultBranch + "..HEAD",
389
+ "git diff --stat origin/" + defaultBranch + "...HEAD",
390
+ "git status --porcelain",
391
+ "```",
392
+ "",
393
+ "Read that history. **Do not recreate work that already exists.** Finish",
394
+ "what remains against the same acceptance criteria. If a prior attempt",
395
+ "left a `wip(#…): … auto-salvaged` commit, treat it as your starting",
396
+ "point, not as trash to rewrite from scratch.",
397
+ "",
398
+ ].join("\n")
399
+ : "";
400
+ // The comments as rendered, and the effective file lane the brief must show
401
+ // — the admission-carried value when dispatch has one, else the lane the
402
+ // thread itself resolves to — passed to the renderer so a winning
403
+ // declaration can be reproduced verbatim even when it sits beyond the
404
+ // discussion budget, keeping the gate and the worker-visible brief on one
405
+ // lane (#608).
406
+ const comments = opts.comments ?? [];
407
+ // The effective lane admission resolved (or resolves) for this candidate:
408
+ // the carried admission snapshot when dispatch has one, else the thread
409
+ // itself. Both the discussion renderer and the parsed-lane section draw on
410
+ // the same value, so the brief shows one lane on every surface (#608, #724).
411
+ const lane = opts.lane ?? (comments === "unread" ? undefined : effectiveLane(r.issue.body, comments));
412
+ // The effective model declaration admission resolved (or resolves) for this
413
+ // candidate: the carried admission snapshot when dispatch has one, else the
414
+ // thread itself. Dispatch launches on the same value, so the brief shows
415
+ // one model on every surface (#535).
416
+ const model = opts.model ?? (comments === "unread" ? undefined : effectiveModel(r.issue.body, comments));
417
+ // One typed gate decision, rendered as two groups (#1042). Computed once so
418
+ // the two placeholders cannot disagree about which gate belongs where.
419
+ const gates = workerGateGroups(r.repo);
420
+ const slug = repoSlug(r.repo);
421
+ return renderBrief(template, {
422
+ ISSUE_NUMBER: String(r.issue.number),
423
+ ISSUE_TITLE: r.issue.title,
424
+ TRACKER_REPO: project.tracker.repo,
425
+ REPO: slug,
426
+ BRANCH: branch,
427
+ WORKTREE: worktree,
428
+ ACCEPTANCE_CRITERIA: acceptanceCriteria(r.issue),
429
+ ISSUE_COMMENTS: renderDiscussion(comments, lane),
430
+ FILE_LANE: laneBlock(lane),
431
+ MODEL: modelBlock(model),
432
+ // The fleet's accumulated knowledge about this repo, as its own capped
433
+ // section (Phase 3 knowledge overlay). Read per dispatch from
434
+ // `<stateDir>/knowledge/<repo>.md`, so what one worker discovered and
435
+ // reported at settlement reaches the next one instead of dying with its
436
+ // run row. Empty string when the file does not exist or holds nothing:
437
+ // the placeholder sits flush against `{{MODEL}}`, so a fleet with no
438
+ // knowledge yet renders the brief it always did, to the byte.
439
+ KNOWLEDGE: knowledgeSection(knowledgeRepoKey(r.repo)),
440
+ // The gates the worker runs, and — flush against them so an empty group
441
+ // leaves no orphaned whitespace — the ones the guard refuses it (#1042).
442
+ GATES: gatesBlock(slug, gates),
443
+ CI_OWNED_GATES: ciOwnedGatesBlock(slug, gates),
444
+ // The guarded shell suites and their sanctioned parse-only check, derived
445
+ // from SHARED_HOST_SCRIPTS so the brief and the refusal share one list
446
+ // (#687). Non-empty whenever the guard has paths, so the placeholder line
447
+ // always renders to a line.
448
+ SHARED_HOST_NOTICE: sharedHostBriefNotice(),
449
+ // The typed host-constraints paragraph (#721): derived cores/RAM folded
450
+ // into the operator's description, the non-interactive PATH, and the
451
+ // routed repo's convention. Empty when the config names none — no
452
+ // section, no placeholder text.
453
+ HOST_CONSTRAINTS: hostConstraintsNotice(opts.host, slug),
454
+ // The brief's code-graph paragraph: the exact `project` key for a
455
+ // configured repo, or an explicit "no graph" statement for an
456
+ // unconfigured one — never silence, because a worker that knows there is
457
+ // no graph stops looking for it.
458
+ GRAPH_HINT: graphHint(r.repo),
459
+ CONTINUATION: continuation,
460
+ });
461
+ }