@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.
- package/README.md +22 -2
- package/dist/agent-runner.d.ts +56 -4
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +253 -28
- package/dist/agent-runner.js.map +1 -1
- package/dist/author.d.ts +12 -0
- package/dist/author.d.ts.map +1 -1
- package/dist/author.js +183 -88
- package/dist/author.js.map +1 -1
- package/dist/causal-analysis.d.ts +212 -0
- package/dist/causal-analysis.d.ts.map +1 -0
- package/dist/causal-analysis.js +733 -0
- package/dist/causal-analysis.js.map +1 -0
- package/dist/decider-review.d.ts +6 -1
- package/dist/decider-review.d.ts.map +1 -1
- package/dist/decider-review.js +19 -7
- package/dist/decider-review.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/preflight.d.ts +3 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +73 -57
- package/dist/preflight.js.map +1 -1
- package/dist/prompt-telemetry.d.ts +64 -0
- package/dist/prompt-telemetry.d.ts.map +1 -0
- package/dist/prompt-telemetry.js +112 -0
- package/dist/prompt-telemetry.js.map +1 -0
- package/dist/provider-telemetry.d.ts +106 -0
- package/dist/provider-telemetry.d.ts.map +1 -0
- package/dist/provider-telemetry.js +423 -0
- package/dist/provider-telemetry.js.map +1 -0
- package/dist/run-analytics-report.d.ts +173 -0
- package/dist/run-analytics-report.d.ts.map +1 -0
- package/dist/run-analytics-report.js +650 -0
- package/dist/run-analytics-report.js.map +1 -0
- package/dist/run-analytics.d.ts +738 -0
- package/dist/run-analytics.d.ts.map +1 -0
- package/dist/run-analytics.js +545 -0
- package/dist/run-analytics.js.map +1 -0
- package/dist/run-program.d.ts +69 -1
- package/dist/run-program.d.ts.map +1 -1
- package/dist/run-program.js +1263 -716
- package/dist/run-program.js.map +1 -1
- package/dist/whole-program-review.d.ts +3 -0
- package/dist/whole-program-review.d.ts.map +1 -1
- package/dist/whole-program-review.js +8 -1
- package/dist/whole-program-review.js.map +1 -1
- 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;
|
package/dist/author.d.ts.map
CHANGED
|
@@ -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;
|
|
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 {
|
|
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
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
|
60
|
-
return invocation.exitCode !== 0 || invocation.inputError !== undefined
|
|
67
|
+
function isAuthorProtocolFailure(invocation) {
|
|
68
|
+
return invocation.exitCode !== 0 || invocation.inputError !== undefined;
|
|
61
69
|
}
|
|
62
|
-
function describeProtocolFailure(invocation
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
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 =
|
|
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
|
|
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
|
-
|
|
729
|
-
|
|
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
|
-
|
|
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
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
//
|
|
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 =
|
|
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
|
|
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
|
-
|
|
821
|
-
|
|
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
|
}
|