@wildorder/nightshift 0.15.0 → 0.17.0

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 (50) hide show
  1. package/README.md +22 -2
  2. package/dist/agent-runner.d.ts +56 -4
  3. package/dist/agent-runner.d.ts.map +1 -1
  4. package/dist/agent-runner.js +253 -28
  5. package/dist/agent-runner.js.map +1 -1
  6. package/dist/author.d.ts +12 -0
  7. package/dist/author.d.ts.map +1 -1
  8. package/dist/author.js +183 -88
  9. package/dist/author.js.map +1 -1
  10. package/dist/causal-analysis.d.ts +212 -0
  11. package/dist/causal-analysis.d.ts.map +1 -0
  12. package/dist/causal-analysis.js +733 -0
  13. package/dist/causal-analysis.js.map +1 -0
  14. package/dist/decider-review.d.ts +6 -1
  15. package/dist/decider-review.d.ts.map +1 -1
  16. package/dist/decider-review.js +19 -7
  17. package/dist/decider-review.js.map +1 -1
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +5 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/preflight.d.ts +3 -0
  23. package/dist/preflight.d.ts.map +1 -1
  24. package/dist/preflight.js +73 -57
  25. package/dist/preflight.js.map +1 -1
  26. package/dist/prompt-telemetry.d.ts +64 -0
  27. package/dist/prompt-telemetry.d.ts.map +1 -0
  28. package/dist/prompt-telemetry.js +112 -0
  29. package/dist/prompt-telemetry.js.map +1 -0
  30. package/dist/provider-telemetry.d.ts +106 -0
  31. package/dist/provider-telemetry.d.ts.map +1 -0
  32. package/dist/provider-telemetry.js +423 -0
  33. package/dist/provider-telemetry.js.map +1 -0
  34. package/dist/run-analytics-report.d.ts +173 -0
  35. package/dist/run-analytics-report.d.ts.map +1 -0
  36. package/dist/run-analytics-report.js +650 -0
  37. package/dist/run-analytics-report.js.map +1 -0
  38. package/dist/run-analytics.d.ts +738 -0
  39. package/dist/run-analytics.d.ts.map +1 -0
  40. package/dist/run-analytics.js +545 -0
  41. package/dist/run-analytics.js.map +1 -0
  42. package/dist/run-program.d.ts +69 -1
  43. package/dist/run-program.d.ts.map +1 -1
  44. package/dist/run-program.js +1263 -716
  45. package/dist/run-program.js.map +1 -1
  46. package/dist/whole-program-review.d.ts +3 -0
  47. package/dist/whole-program-review.d.ts.map +1 -1
  48. package/dist/whole-program-review.js +8 -1
  49. package/dist/whole-program-review.js.map +1 -1
  50. package/package.json +3 -3
package/dist/author.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { NightshiftConfig } from "./config.js";
3
3
  import { type PermitsContext } from "./permits.js";
4
4
  import { type ReviewPassOutcome } from "./review-pass.js";
5
5
  import { type GitOps } from "./run-program.js";
6
+ import { type RunRecorder } from "./run-analytics.js";
6
7
  export interface AuthorOptions {
7
8
  cwd: string;
8
9
  programId: string;
@@ -33,6 +34,13 @@ export interface AuthorOptions {
33
34
  * Threaded the same way as `reviewed`.
34
35
  */
35
36
  triaged?: Set<string>;
37
+ /**
38
+ * The run's analytics recorder (WS-01); `runProgram` threads its own
39
+ * recorder through so authoring's agent/critique spawns can be observed
40
+ * under the same run. A caller with no run-scoped recorder (a standalone
41
+ * `author` CLI invocation, tests) gets a shared no-op default.
42
+ */
43
+ recorder?: RunRecorder;
36
44
  }
37
45
  export type AuthorOutcome = {
38
46
  status: "authored";
@@ -68,6 +76,10 @@ export interface AuthorResult {
68
76
  * spawned; surfaced here, never fatal — mirrors run-program.ts's
69
77
  * WorkstreamResult.stageErrors (WS-04 SC-12). */
70
78
  stageErrors?: string[];
79
+ /** What the runner had to do about the author's reply — a summary lifted
80
+ * out of the spec file, or none emitted at all. Rendered in the report so
81
+ * a degraded reply is stated, never silently substituted. */
82
+ notes?: string[];
71
83
  }
72
84
  export interface AuthorStageResult {
73
85
  programId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"author.d.ts","sourceRoot":"","sources":["../src/author.ts"],"names":[],"mappings":"AAEA,OAAO,EAOL,KAAK,WAAW,EAEjB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAkBzE,OAAO,EASL,KAAK,iBAAiB,EAEvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAiC,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAe9E,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,gBAAgB,CAAC;IACzB;iFAC6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;sCAEkC;IAClC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,gEAAgE;IAChE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,+EAA+E;IAC/E,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,qEAAqE;IACrE,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,uEAAuE;IACvE,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,8FAA8F;IAC9F,UAAU,EAAE,OAAO,CAAC;IACpB,gFAAgF;IAChF,YAAY,CAAC,EAAE,iBAAiB,CAAC;IACjC;;sDAEkD;IAClD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,8EAA8E;IAC9E,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAooBD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,iBAAiB,CAAC,CAqjB5B"}
1
+ {"version":3,"file":"author.d.ts","sourceRoot":"","sources":["../src/author.ts"],"names":[],"mappings":"AAEA,OAAO,EAOL,KAAK,WAAW,EAEjB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAkBzE,OAAO,EASL,KAAK,iBAAiB,EAEvB,MAAM,kBAAkB,CAAC;AAO1B,OAAO,EAAwD,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACrG,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAezE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,gBAAgB,CAAC;IACzB;iFAC6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;sCAEkC;IAClC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB;;;;OAIG;IACH,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,gEAAgE;IAChE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,+EAA+E;IAC/E,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,qEAAqE;IACrE,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,uEAAuE;IACvE,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,8FAA8F;IAC9F,UAAU,EAAE,OAAO,CAAC;IACpB,gFAAgF;IAChF,YAAY,CAAC,EAAE,iBAAiB,CAAC;IACjC;;sDAEkD;IAClD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;kEAE8D;IAC9D,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,8EAA8E;IAC9E,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAyvBD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,iBAAiB,CAAC,CA2nB5B"}
package/dist/author.js CHANGED
@@ -9,7 +9,9 @@ import { reviewDecisions, triageFindings } from "./decider-review.js";
9
9
  import { findCycles, stableTopologicalOrder, topologicalLevels } from "./graph.js";
10
10
  import { loadManifest, saveManifest, specInputsHash, } from "./manifest.js";
11
11
  import { extractFindings, findingsContract, findingsToLedgerEvents, hasRoutableEvidence, locateInRepo, reviewerAbsentOutcome, runReviewPass, verifyEvidence, } from "./review-pass.js";
12
- import { defaultGitOps, downstreamCone } from "./run-program.js";
12
+ import { createBriefBuilder, demotedDependenciesPoint, promptComponentSizePoints, } from "./prompt-telemetry.js";
13
+ import { commitEvidence, defaultGitOps, downstreamCone, timed } from "./run-program.js";
14
+ import { NOOP_RUN_RECORDER } from "./run-analytics.js";
13
15
  /**
14
16
  * The authoring stage: for every workstream whose spec does not exist, a
15
17
  * clean agent writes it — human-first, grounded in the repository,
@@ -50,23 +52,83 @@ function classifySpec(existing, workstream) {
50
52
  }
51
53
  /**
52
54
  * True when a *returned* (not thrown) author invocation reported failure —
53
- * a nonzero exit code, an undelivered prompt, or an absent required summary
54
- * block. Each means the process either reported failure or never produced
55
- * the reply contract, so any spec it may have written cannot be trusted as
56
- * complete (WS-01 SC-07): the safe direction is to re-author, not to
57
- * silently substitute raw output and proceed toward completion.
55
+ * a nonzero exit code or an undelivered prompt. Each means the process
56
+ * itself reported failure, so any spec it may have written cannot be
57
+ * trusted as complete (WS-01 SC-07): the safe direction is to re-author.
58
+ *
59
+ * A missing summary block is deliberately *not* a protocol failure. The
60
+ * summary contract (`agent-summary.ts`) says a missing block is never fatal,
61
+ * and the implementer path honors that; the author path once did not, and
62
+ * the first real run to hit it discarded a complete 36 KB spec because the
63
+ * agent ended the *file* with its summary instead of its *reply*. The spec
64
+ * is the artifact; its presence is checked deterministically above. The
65
+ * reply's shape is a proxy, and grading the proxy is the courtroom.
58
66
  */
59
- function isAuthorProtocolFailure(invocation, summary) {
60
- return invocation.exitCode !== 0 || invocation.inputError !== undefined || !summary.available;
67
+ function isAuthorProtocolFailure(invocation) {
68
+ return invocation.exitCode !== 0 || invocation.inputError !== undefined;
61
69
  }
62
- function describeProtocolFailure(invocation, summary) {
70
+ function describeProtocolFailure(invocation) {
63
71
  const parts = [`exit code ${invocation.exitCode}`];
64
72
  if (invocation.inputError !== undefined)
65
73
  parts.push(`input error: ${invocation.inputError}`);
66
- if (!summary.available)
67
- parts.push("no summary block in the reply");
68
74
  return parts.join(", ");
69
75
  }
76
+ /**
77
+ * A fenced summary block that ends the spec file, with nothing after it.
78
+ * Only a *trailing* block is treated as a misplaced reply: a summary block
79
+ * quoted mid-document is spec content and stays untouched.
80
+ */
81
+ const TRAILING_SUMMARY_BLOCK = /(?:\r?\n)*```summary[^\S\r\n]*\r?\n([\s\S]*?)```\s*$/u;
82
+ /**
83
+ * When an author emitted no summary in its reply, the block usually landed
84
+ * at the end of the spec it wrote — the agent treated the file as its
85
+ * output. Lift it out: the summary is recorded as the agent's own words and
86
+ * the spec no longer carries a reply artifact the implementer would read.
87
+ * Returns the salvaged summary and the spec content *before* stripping (so
88
+ * decision blocks the agent also wrote into the file can be journaled), or
89
+ * undefined when the file has no trailing summary block. Never throws.
90
+ */
91
+ async function salvageSummaryFromSpec(root, workstream) {
92
+ const path = join(root, workstream.taskFile);
93
+ let content;
94
+ try {
95
+ content = await readFile(path, "utf8");
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ const match = TRAILING_SUMMARY_BLOCK.exec(content);
101
+ const text = match?.[1]?.trim();
102
+ if (match === null || text === undefined || text === "")
103
+ return undefined;
104
+ const stripped = `${content.slice(0, match.index).replace(/\s+$/u, "")}\n`;
105
+ await writeFile(path, stripped, "utf8");
106
+ return { summary: text, spec: content };
107
+ }
108
+ /**
109
+ * The summary and decision source for an author reply, degrading in order:
110
+ * the reply's own block; a block salvaged from the end of the spec file; the
111
+ * reply's output tail. Each degradation is stated in `notes` so the run
112
+ * report says what happened instead of silently substituting.
113
+ */
114
+ async function resolveAuthorReply(root, workstream, invocation, notes) {
115
+ const fromReply = resolveSummary(invocation.output);
116
+ if (fromReply.available)
117
+ return { summary: fromReply, decisionSource: invocation.output };
118
+ const salvaged = await salvageSummaryFromSpec(root, workstream);
119
+ if (salvaged !== undefined) {
120
+ notes.push(`${workstream.id}'s author wrote its summary block into ${workstream.taskFile} ` +
121
+ `instead of ending its reply with it; the runner lifted the block out of the ` +
122
+ `spec and recorded it as the summary.`);
123
+ return {
124
+ summary: { text: salvaged.summary, available: true, needsPermission: [] },
125
+ decisionSource: `${invocation.output}\n${salvaged.spec}`,
126
+ };
127
+ }
128
+ notes.push(`${workstream.id}'s author ended its reply without a summary block; the tail of ` +
129
+ `its reply was recorded instead. The spec it wrote is kept.`);
130
+ return { summary: fromReply, decisionSource: invocation.output };
131
+ }
70
132
  async function programDocumentSection(root, manifest) {
71
133
  const relPath = join("docs", "programs", `${manifest.program.id}-program.md`);
72
134
  let content;
@@ -166,6 +228,7 @@ async function dependencySpecsSection(root, workstream, rosterById) {
166
228
  return {
167
229
  section: ["## Dependency specs", "", "No dependencies declared.", ""],
168
230
  demoted: [],
231
+ demotedBytes: 0,
169
232
  };
170
233
  }
171
234
  const entries = await Promise.all(workstream.dependencies
@@ -177,6 +240,7 @@ async function dependencySpecsSection(root, workstream, rosterById) {
177
240
  })));
178
241
  let total = entries.reduce((sum, e) => sum + (e.content?.length ?? 0), 0);
179
242
  const demoted = new Set();
243
+ let demotedBytes = 0;
180
244
  const bySizeDesc = entries
181
245
  .filter((e) => e.content !== undefined)
182
246
  .sort((a, b) => (b.content?.length ?? 0) - (a.content?.length ?? 0));
@@ -185,6 +249,7 @@ async function dependencySpecsSection(root, workstream, rosterById) {
185
249
  break;
186
250
  demoted.add(e.entry.id);
187
251
  total -= e.content?.length ?? 0;
252
+ demotedBytes += e.content === undefined ? 0 : Buffer.byteLength(e.content, "utf8");
188
253
  }
189
254
  const lines = ["## Dependency specs", ""];
190
255
  for (const { entry, content } of entries) {
@@ -202,7 +267,7 @@ async function dependencySpecsSection(root, workstream, rosterById) {
202
267
  lines.push(`### ${entry.id} ${entry.name}`, "", content.trim(), "");
203
268
  }
204
269
  lines.push("");
205
- return { section: lines, demoted: [...demoted] };
270
+ return { section: lines, demoted: [...demoted], demotedBytes };
206
271
  }
207
272
  const SPEC_CONTRACT = [
208
273
  "## Write a spec for a human",
@@ -247,39 +312,36 @@ block entirely when you have nothing to add.
247
312
  function dependenciesContract() {
248
313
  return DEPENDENCIES_CONTRACT;
249
314
  }
315
+ /**
316
+ * Builds the author's brief, byte-for-byte identical to the plain
317
+ * `array.join("\n")` this produced before WS-03 — but through a
318
+ * `BriefBuilder` so every line is also classified into an SC-07 prompt
319
+ * component, with `components` summing exactly to the joined brief's byte
320
+ * length.
321
+ */
250
322
  async function authorBrief(root, manifest, workstream, rosterById, reauthorNote) {
251
323
  const programDoc = await programDocumentSection(root, manifest);
252
324
  const deps = await dependencySpecsSection(root, workstream, rosterById);
253
- const brief = [
254
- `# Workstream ${workstream.id}: ${workstream.name}`,
255
- "",
256
- `Program: ${manifest.program.id}${manifest.program.name}`,
257
- "",
258
- "Write this workstream's specification document — the narrative file a",
259
- "human and a later implementing agent will both read to build it.",
260
- "",
261
- ...programDoc,
262
- ...successCriteriaSection(manifest),
263
- ...scopeSection(workstream),
264
- ...rosterSection(manifest),
265
- ...prerequisitesSection(manifest, workstream),
266
- ...deps.section,
267
- ...(reauthorNote ? [reauthorNote, ""] : []),
268
- SPEC_CONTRACT,
269
- GROUNDING_CONTRACT,
270
- dependenciesContract(),
271
- "",
272
- "## Rules",
273
- "",
274
- "- Write exactly one file: this workstream's taskFile (create parent",
275
- " directories as needed). Do not write or edit any other file.",
276
- "- Never commit; the runner owns commits.",
277
- "",
278
- decisionContract(),
279
- "",
280
- summaryContract(),
281
- ].join("\n");
282
- return { brief, demoted: deps.demoted };
325
+ const builder = createBriefBuilder();
326
+ builder.push("framing", `# Workstream ${workstream.id}: ${workstream.name}`, "");
327
+ builder.push("program-narrative", `Program: ${manifest.program.id} — ${manifest.program.name}`);
328
+ builder.push("framing", "", "Write this workstream's specification document the narrative file a", "human and a later implementing agent will both read to build it.", "");
329
+ builder.push("program-narrative", ...programDoc);
330
+ builder.push("success-criteria", ...successCriteriaSection(manifest));
331
+ builder.push("scope", ...scopeSection(workstream));
332
+ builder.push("roster", ...rosterSection(manifest));
333
+ builder.push("prerequisites", ...prerequisitesSection(manifest, workstream));
334
+ builder.push("dependency-specs", ...deps.section);
335
+ builder.push("re-author-note", ...(reauthorNote ? [reauthorNote, ""] : []));
336
+ builder.push("runner-instructions", SPEC_CONTRACT, GROUNDING_CONTRACT, dependenciesContract());
337
+ builder.push("framing", "", "## Rules", "", "- Write exactly one file: this workstream's taskFile (create parent", " directories as needed). Do not write or edit any other file.", "- Your reply is a separate channel from that file. The decision and", " summary blocks described below go in your reply, never inside the", " taskFile — the runner reads them from what you say, not what you wrote.", "- Never commit; the runner owns commits.", "");
338
+ builder.push("runner-instructions", decisionContract(), "", summaryContract());
339
+ return {
340
+ brief: builder.join(),
341
+ demoted: deps.demoted,
342
+ demotedBytes: deps.demotedBytes,
343
+ components: builder.components(),
344
+ };
283
345
  }
284
346
  const SPEC_CRITIQUE_FRAMING = [
285
347
  "## Critique this spec",
@@ -405,8 +467,10 @@ async function runSpecCritique(options) {
405
467
  const reviewer = resolveReviewerAgent(options.config);
406
468
  if (!reviewer)
407
469
  return reviewerAbsentOutcome();
470
+ const recorder = options.recorder ?? NOOP_RUN_RECORDER;
408
471
  const locate = (file) => locateInRepo(options.root, file);
409
- const review = async (_round, priorOpen) => {
472
+ const span = recorder.span({ stage: "spec-critique", workstream: options.workstream.id });
473
+ const review = async (round, priorOpen) => {
410
474
  const currentSpec = (await readFinishedSpec(options.root, options.workstream)) ?? options.spec;
411
475
  const brief = await specCritiqueReviewerBrief(options.root, options.manifest, options.workstream, options.rosterById, currentSpec, priorOpen);
412
476
  let invocation;
@@ -416,7 +480,7 @@ async function runSpecCritique(options) {
416
480
  programId: options.programId,
417
481
  label: `${options.workstream.id}-spec-critique-reviewer`,
418
482
  log: options.log,
419
- });
483
+ }, recorder, { workstream: options.workstream.id, attemptIndex: round });
420
484
  }
421
485
  catch (error) {
422
486
  // Fail open, matching the equivalent build-side guard in
@@ -433,7 +497,7 @@ async function runSpecCritique(options) {
433
497
  const ran = invocation.exitCode === 0 && hasFindingsBlock(invocation.output);
434
498
  return { findings, errors: parsed.errors, ran };
435
499
  };
436
- const respond = async (_round, findings) => {
500
+ const respond = async (round, findings) => {
437
501
  const currentSpec = (await readFinishedSpec(options.root, options.workstream)) ?? options.spec;
438
502
  const brief = specCritiqueWriterBrief(options.workstream, currentSpec, findings);
439
503
  let invocation;
@@ -443,7 +507,7 @@ async function runSpecCritique(options) {
443
507
  programId: options.programId,
444
508
  label: `${options.workstream.id}-spec-critique-writer`,
445
509
  log: options.log,
446
- });
510
+ }, recorder, { workstream: options.workstream.id, attemptIndex: round });
447
511
  }
448
512
  catch (error) {
449
513
  // Fail open: the spec is left as the reviewer last saw it and the
@@ -455,7 +519,15 @@ async function runSpecCritique(options) {
455
519
  }
456
520
  return { note: resolveSummary(invocation.output).text };
457
521
  };
458
- return runReviewPass({ review, respond });
522
+ try {
523
+ const outcome = await runReviewPass({ review, respond });
524
+ span.close({ outcome: "success" });
525
+ return outcome;
526
+ }
527
+ catch (error) {
528
+ span.close({ outcome: "failed" });
529
+ throw error;
530
+ }
459
531
  }
460
532
  function mergeDependencies(workstream, rosterIds, declaredIds) {
461
533
  const merged = [];
@@ -516,6 +588,7 @@ export async function authorProgram(options) {
516
588
  const force = options.force === true;
517
589
  const reviewed = options.reviewed ?? new Set();
518
590
  const triaged = options.triaged ?? new Set();
591
+ const recorder = options.recorder ?? NOOP_RUN_RECORDER;
519
592
  const manifest = await loadManifest(root, options.programId);
520
593
  const resolvedAuthor = resolveAuthorAgent(config);
521
594
  if (!resolvedAuthor) {
@@ -595,7 +668,7 @@ export async function authorProgram(options) {
595
668
  };
596
669
  if (blockedCone.has(workstream.id)) {
597
670
  workstream.status = "parked";
598
- await saveManifest(root, options.programId, manifest, { log });
671
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
599
672
  base.outcome = {
600
673
  status: "parked",
601
674
  reason: "an upstream dependency's authoring failed; parked, not attempted",
@@ -622,7 +695,7 @@ export async function authorProgram(options) {
622
695
  workstream.specHash !== undefined &&
623
696
  specInputsHash(manifest, workstream) === workstream.specHash) {
624
697
  workstream.specStatus = "complete";
625
- await saveManifest(root, options.programId, manifest, { log });
698
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
626
699
  log(`${workstream.id}: recognized as completed legacy authoring — ` +
627
700
  'normalized to specStatus "complete"');
628
701
  return base;
@@ -639,7 +712,7 @@ export async function authorProgram(options) {
639
712
  const current = specInputsHash(manifest, workstream);
640
713
  if (current !== workstream.specHash) {
641
714
  workstream.status = "parked";
642
- await saveManifest(root, options.programId, manifest, { log });
715
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
643
716
  for (const id of downstreamCone(manifest.workstreams, [workstream.id])) {
644
717
  blockedCone.add(id);
645
718
  }
@@ -669,12 +742,18 @@ export async function authorProgram(options) {
669
742
  const allDecisionsById = new Map();
670
743
  const first = await authorBrief(root, manifest, workstream, rosterById);
671
744
  base.demotedDependencies = first.demoted;
745
+ for (const point of promptComponentSizePoints(first.components, { role: "authorAgent" })) {
746
+ recorder.point(point);
747
+ }
748
+ const firstDemotedPoint = demotedDependenciesPoint(first.demoted, first.demotedBytes, { role: "authorAgent" });
749
+ if (firstDemotedPoint)
750
+ recorder.point(firstDemotedPoint);
672
751
  // Stamped before the agent can write anything (SC-01): the agent has
673
752
  // whole-file write access, so a crash at any point after this line
674
753
  // leaves a durable `in_progress` marker the next attempt classifies as
675
754
  // interrupted, regardless of whether a partial file exists.
676
755
  workstream.specStatus = "in_progress";
677
- await saveManifest(root, options.programId, manifest, { log });
756
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
678
757
  let invocation;
679
758
  try {
680
759
  invocation = await invokeAgent(agentRunner, author, first.brief, root, permits, "authorAgent", {
@@ -682,7 +761,7 @@ export async function authorProgram(options) {
682
761
  programId: options.programId,
683
762
  label: `${workstream.id}-author`,
684
763
  log,
685
- });
764
+ }, recorder, { stage: "author", workstream: workstream.id });
686
765
  }
687
766
  catch (error) {
688
767
  // A spawn error here means the author agent never started — the same
@@ -694,7 +773,7 @@ export async function authorProgram(options) {
694
773
  // stays `in_progress` — never cleared on a failure path — so a later
695
774
  // retry resumes without needing --force (SC-07).
696
775
  workstream.status = "parked";
697
- await saveManifest(root, options.programId, manifest, { log });
776
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
698
777
  for (const id of downstreamCone(manifest.workstreams, [workstream.id])) {
699
778
  blockedCone.add(id);
700
779
  }
@@ -705,28 +784,30 @@ export async function authorProgram(options) {
705
784
  };
706
785
  return base;
707
786
  }
708
- const firstSummary = resolveSummary(invocation.output);
709
- if (isAuthorProtocolFailure(invocation, firstSummary)) {
710
- // The process reported failure, its prompt was not fully delivered, or
711
- // it never produced the required summary contract any spec it may
712
- // have written cannot be trusted as complete (SC-07). Park exactly
713
- // like a spawn failure and leave `specStatus` at `in_progress` so a
714
- // force-free retry re-authors.
787
+ if (isAuthorProtocolFailure(invocation)) {
788
+ // The process reported failure or its prompt was not fully delivered —
789
+ // any spec it may have written cannot be trusted as complete (SC-07).
790
+ // Park exactly like a spawn failure and leave `specStatus` at
791
+ // `in_progress` so a force-free retry re-authors.
715
792
  workstream.status = "parked";
716
- await saveManifest(root, options.programId, manifest, { log });
793
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
717
794
  for (const id of downstreamCone(manifest.workstreams, [workstream.id])) {
718
795
  blockedCone.add(id);
719
796
  }
720
- base.summary = firstSummary.text;
797
+ base.summary = resolveSummary(invocation.output).text;
721
798
  base.outcome = {
722
799
  status: "failed",
723
- reason: `the author agent reported a protocol failure (${describeProtocolFailure(invocation, firstSummary)}); ` +
800
+ reason: `the author agent reported a protocol failure (${describeProtocolFailure(invocation)}); ` +
724
801
  `not trusted as a finished spec.`,
725
802
  };
726
803
  return base;
727
804
  }
728
- base.summary = firstSummary.text;
729
- let parsed = extractDecisions(invocation.output);
805
+ const notes = [];
806
+ const firstReply = await resolveAuthorReply(root, workstream, invocation, notes);
807
+ base.summary = firstReply.summary.text;
808
+ if (notes.length > 0)
809
+ (base.notes ??= []).push(...notes);
810
+ let parsed = extractDecisions(firstReply.decisionSource);
730
811
  base.decisionErrors.push(...parsed.errors);
731
812
  for (const decision of parsed.decisions) {
732
813
  allDecisionsById.set(decisionFingerprint(workstream.id, decision), decision);
@@ -744,12 +825,12 @@ export async function authorProgram(options) {
744
825
  if (merge.merged.length > 0) {
745
826
  const originalDependencies = workstream.dependencies.filter((id) => !merge.merged.includes(id));
746
827
  base.mergedDependencies = merge.merged;
747
- await saveManifest(root, options.programId, manifest, { log });
828
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
748
829
  log(`${workstream.id}: merged new dependency edge(s): ${merge.merged.join(", ")}`);
749
830
  const cyclesNow = findCycles(manifest.workstreams);
750
831
  if (cyclesNow.length > 0) {
751
832
  workstream.dependencies = originalDependencies;
752
- await saveManifest(root, options.programId, manifest, { log });
833
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
753
834
  throw new Error(`Dependency cycle(s) created by a discovered edge from ${workstream.id}: ` +
754
835
  `${cyclesNow.map((cycle) => cycle.join(" -> ")).join("; ")}. Re-plan with /plan-program.`);
755
836
  }
@@ -777,9 +858,17 @@ export async function authorProgram(options) {
777
858
  ].join("\n");
778
859
  const second = await authorBrief(root, manifest, workstream, rosterById, note);
779
860
  base.demotedDependencies = second.demoted;
861
+ for (const point of promptComponentSizePoints(second.components, { role: "authorAgent" })) {
862
+ recorder.point(point);
863
+ }
864
+ const secondDemotedPoint = demotedDependenciesPoint(second.demoted, second.demotedBytes, {
865
+ role: "authorAgent",
866
+ });
867
+ if (secondDemotedPoint)
868
+ recorder.point(secondDemotedPoint);
780
869
  let reauthorInvocation;
781
870
  try {
782
- reauthorInvocation = await invokeAgent(agentRunner, author, second.brief, root, permits, "authorAgent", { root, programId: options.programId, label: `${workstream.id}-author-reauthor`, log });
871
+ reauthorInvocation = await invokeAgent(agentRunner, author, second.brief, root, permits, "authorAgent", { root, programId: options.programId, label: `${workstream.id}-author-reauthor`, log }, recorder, { stage: "re-author", workstream: workstream.id });
783
872
  }
784
873
  catch (error) {
785
874
  // Fail open: the first pass already wrote a spec and journaled
@@ -790,35 +879,38 @@ export async function authorProgram(options) {
790
879
  `pass: ${error.message}. The pre-discovery spec was kept.`);
791
880
  }
792
881
  if (reauthorInvocation !== undefined) {
793
- const reauthorSummary = resolveSummary(reauthorInvocation.output);
794
- if (isAuthorProtocolFailure(reauthorInvocation, reauthorSummary)) {
882
+ if (isAuthorProtocolFailure(reauthorInvocation)) {
795
883
  // A *returned* protocol failure on the re-author pass is treated
796
884
  // exactly like one on the first invocation (SC-07): the process
797
- // reported failure, its prompt was not fully delivered, or it
798
- // never produced the summary contract, so nothing this pass
799
- // produced can be trusted park, isolate the downstream cone,
800
- // and leave specStatus at in_progress for a force-free retry.
801
- // This differs from a *thrown* spawn error above, which fails
802
- // open because the first pass's spec is otherwise untouched; a
803
- // returned failure carries no such guarantee.
885
+ // reported failure or its prompt was not fully delivered, so
886
+ // nothing this pass produced can be trusted park, isolate the
887
+ // downstream cone, and leave specStatus at in_progress for a
888
+ // force-free retry. This differs from a *thrown* spawn error
889
+ // above, which fails open because the first pass's spec is
890
+ // otherwise untouched; a returned failure carries no such
891
+ // guarantee.
804
892
  workstream.status = "parked";
805
- await saveManifest(root, options.programId, manifest, { log });
893
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
806
894
  for (const id of downstreamCone(manifest.workstreams, [workstream.id])) {
807
895
  blockedCone.add(id);
808
896
  }
809
- base.summary = reauthorSummary.text;
897
+ base.summary = resolveSummary(reauthorInvocation.output).text;
810
898
  base.outcome = {
811
899
  status: "failed",
812
900
  reason: `the author agent reported a protocol failure during ${workstream.id}'s ` +
813
- `re-author pass (${describeProtocolFailure(reauthorInvocation, reauthorSummary)}); ` +
901
+ `re-author pass (${describeProtocolFailure(reauthorInvocation)}); ` +
814
902
  `not trusted as a finished spec.`,
815
903
  };
816
904
  return base;
817
905
  }
818
906
  else {
819
907
  invocation = reauthorInvocation;
820
- base.summary = reauthorSummary.text;
821
- parsed = extractDecisions(invocation.output);
908
+ const reauthorNotes = [];
909
+ const reauthorReply = await resolveAuthorReply(root, workstream, reauthorInvocation, reauthorNotes);
910
+ base.summary = reauthorReply.summary.text;
911
+ if (reauthorNotes.length > 0)
912
+ (base.notes ??= []).push(...reauthorNotes);
913
+ parsed = extractDecisions(reauthorReply.decisionSource);
822
914
  base.decisionErrors.push(...parsed.errors);
823
915
  for (const decision of parsed.decisions) {
824
916
  allDecisionsById.set(decisionFingerprint(workstream.id, decision), decision);
@@ -837,7 +929,7 @@ export async function authorProgram(options) {
837
929
  const final = await readFinishedSpec(root, workstream);
838
930
  if (final === undefined) {
839
931
  workstream.status = "parked";
840
- await saveManifest(root, options.programId, manifest, { log });
932
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
841
933
  for (const id of downstreamCone(manifest.workstreams, [workstream.id])) {
842
934
  blockedCone.add(id);
843
935
  }
@@ -863,6 +955,7 @@ export async function authorProgram(options) {
863
955
  author,
864
956
  log,
865
957
  pushStageError: (message) => (base.stageErrors ??= []).push(message),
958
+ recorder,
866
959
  });
867
960
  base.specCritique = critique;
868
961
  const postCritique = await readFinishedSpec(root, workstream);
@@ -877,7 +970,7 @@ export async function authorProgram(options) {
877
970
  ...(baseCommit === undefined ? {} : { baseCommit }),
878
971
  now,
879
972
  });
880
- await appendLedgerEvents(root, options.programId, critiqueEvents);
973
+ await timed(recorder, { stage: "ledger-persist", workstream: workstream.id }, () => appendLedgerEvents(root, options.programId, critiqueEvents));
881
974
  // Stamped after every dependency merge above, so the hash records the
882
975
  // manifest exactly as this spec's author last saw it — a later manifest
883
976
  // edit is drift; authoring's own merges are not. `specStatus` is
@@ -887,10 +980,10 @@ export async function authorProgram(options) {
887
980
  // must still classify as interrupted on retry, not falsely complete with
888
981
  // an unmade commit.
889
982
  workstream.specHash = specInputsHash(manifest, workstream);
890
- await saveManifest(root, options.programId, manifest, { log });
983
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
891
984
  let commit;
892
985
  if (isRepository) {
893
- commit = await git.commitPaths(root, `nightshift(${options.programId}): author ${workstream.id} ${workstream.name}`, [workstream.taskFile, "docs/programs"]);
986
+ commit = await timed(recorder, { stage: "git-commit", workstream: workstream.id }, () => git.commitPaths(root, `nightshift(${options.programId}): author ${workstream.id} ${workstream.name}`, [workstream.taskFile, "docs/programs"]), commitEvidence);
894
987
  }
895
988
  // Outside a git repository there is no commit to wait for — the finished
896
989
  // spec, its specHash, and this manifest save are already the durable
@@ -898,7 +991,7 @@ export async function authorProgram(options) {
898
991
  // only runs after `commitPaths` above resolves, so the transition is
899
992
  // recoverable on both sides of the commit (SC-01, SC-02).
900
993
  workstream.specStatus = "complete";
901
- await saveManifest(root, options.programId, manifest, { log });
994
+ await timed(recorder, { stage: "manifest-persist", workstream: workstream.id }, () => saveManifest(root, options.programId, manifest, { log }));
902
995
  {
903
996
  const spawnErrors = await reviewWorkstreamDecisions(workstream.id, [...allDecisionsById.values()], baseCommit);
904
997
  if (spawnErrors.length > 0)
@@ -925,7 +1018,7 @@ export async function authorProgram(options) {
925
1018
  ...(baseCommit === undefined ? {} : { baseCommit }),
926
1019
  decidedBy: "implementer",
927
1020
  }));
928
- await appendLedgerEvents(root, options.programId, events);
1021
+ await timed(recorder, { stage: "ledger-persist", workstream: workstream.id }, () => appendLedgerEvents(root, options.programId, events));
929
1022
  for (const decision of decisions) {
930
1023
  log(`${workstream.id} decision: ${decision.title} -> ${decision.chosen}`);
931
1024
  }
@@ -948,6 +1041,7 @@ export async function authorProgram(options) {
948
1041
  now,
949
1042
  log,
950
1043
  observe: { root, programId: options.programId, label: `${workstreamId}-decider-decision`, log },
1044
+ recorder,
951
1045
  });
952
1046
  }
953
1047
  function reviewWorkstreamFindings(workstreamId, findings, baseCommit) {
@@ -968,6 +1062,7 @@ export async function authorProgram(options) {
968
1062
  now,
969
1063
  log,
970
1064
  observe: { root, programId: options.programId, label: `${workstreamId}-decider-finding`, log },
1065
+ recorder,
971
1066
  });
972
1067
  }
973
1068
  }