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.
- package/REFERENCE.md +10 -1
- package/agents/to-spec.md +76 -9
- package/package.json +1 -1
- package/schema/config.schema.json +4 -0
- package/src/admission.ts +58 -14
- package/src/arm-challenge.ts +255 -85
- package/src/ask.ts +130 -615
- package/src/board.ts +7 -1
- package/src/brief-upgrade.ts +24 -0
- package/src/briefs/console.md +258 -0
- package/src/briefs/correction.md +203 -0
- package/src/briefs/orchestrator.md +167 -97
- package/src/briefs/policy.md +19 -16
- package/src/briefs/to-spec.md +76 -9
- package/src/briefs/worker.md +50 -16
- package/src/cli.ts +4 -0
- package/src/command-manifest.ts +54 -8
- package/src/commands/arm.ts +115 -49
- package/src/commands/console.ts +70 -0
- package/src/commands/context.ts +2 -0
- package/src/commands/epic.ts +132 -0
- package/src/commands/extend.ts +9 -1
- package/src/commands/intake.ts +44 -14
- package/src/commands/stats.ts +19 -4
- package/src/commands/worker.ts +9 -1
- package/src/config-schema.ts +13 -0
- package/src/config.ts +27 -0
- package/src/daemon/ack.ts +159 -0
- package/src/daemon/admission-pass.ts +135 -0
- package/src/daemon/brief.ts +461 -0
- package/src/daemon/deps.ts +539 -0
- package/src/daemon/dispatch.ts +1779 -0
- package/src/daemon/drain.ts +185 -0
- package/src/daemon/groom-pass.ts +422 -0
- package/src/daemon/http.ts +417 -0
- package/src/daemon/integrity.ts +108 -0
- package/src/daemon/panes.ts +180 -0
- package/src/daemon/review.ts +1888 -0
- package/src/daemon/runtime.ts +788 -0
- package/src/daemon/settle-pass.ts +606 -0
- package/src/daemon/supervision.ts +438 -0
- package/src/daemon/tick.ts +968 -0
- package/src/daemon/views.ts +751 -0
- package/src/daemon.ts +105 -7923
- package/src/dashboard/app.js +58 -0
- package/src/dashboard/controls.ts +22 -3
- package/src/dashboard/server.ts +4 -0
- package/src/diff-flags.ts +135 -9
- package/src/doctor.ts +2 -2
- package/src/failure-class.ts +257 -2
- package/src/fleet.ts +295 -176
- package/src/groom.ts +461 -0
- package/src/http-token.ts +142 -0
- package/src/knowledge.ts +229 -0
- package/src/mining.ts +316 -0
- package/src/orchestrator-tick.ts +689 -1670
- package/src/ready-gate.ts +267 -0
- package/src/settlement.ts +107 -11
- package/src/setup-host.ts +32 -9
- package/src/setup-wizard.ts +55 -7
- package/src/setup.ts +229 -3
- package/src/stats.ts +257 -2
- package/src/status-render.ts +169 -14
- package/src/store.ts +618 -28
- package/src/to-spec.ts +426 -44
- package/src/tracker/github.ts +50 -0
- package/src/types.ts +434 -18
- package/src/verbs/protocol.ts +28 -0
- package/src/verbs/server.ts +330 -39
- package/src/wake.ts +19 -2
- 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
|
+
}
|