ahead-pi 0.2.0 → 0.3.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 (102) hide show
  1. package/README.md +29 -9
  2. package/dist/ahead_wasm.wasm +0 -0
  3. package/generated/corrective-debugging/ai-audit.md +39 -0
  4. package/generated/corrective-debugging/ai-review.md +46 -0
  5. package/generated/corrective-debugging/characterize.md +53 -0
  6. package/generated/corrective-debugging/conclude.md +59 -0
  7. package/generated/corrective-debugging/correction.md +53 -0
  8. package/generated/corrective-debugging/deploy.md +38 -0
  9. package/generated/corrective-debugging/human-review.md +45 -0
  10. package/generated/corrective-debugging/implement.md +42 -0
  11. package/generated/corrective-debugging/investigate.md +59 -0
  12. package/generated/corrective-debugging/manifest.json +30 -0
  13. package/generated/corrective-debugging/model.md +53 -0
  14. package/generated/corrective-debugging/outcome.md +38 -0
  15. package/generated/corrective-debugging/plan.md +53 -0
  16. package/generated/corrective-debugging/verify.md +47 -0
  17. package/generated/decision/compare.md +45 -0
  18. package/generated/decision/criteria.md +45 -0
  19. package/generated/decision/decide.md +45 -0
  20. package/generated/decision/frame.md +45 -0
  21. package/generated/decision/manifest.json +21 -0
  22. package/generated/decision/options.md +47 -0
  23. package/generated/decision/publish.md +38 -0
  24. package/generated/decision/research.md +45 -0
  25. package/generated/internal-improvement/ai-audit.md +39 -0
  26. package/generated/internal-improvement/ai-review.md +46 -0
  27. package/generated/internal-improvement/baseline.md +46 -0
  28. package/generated/internal-improvement/decision.md +45 -0
  29. package/generated/internal-improvement/deploy.md +38 -0
  30. package/generated/internal-improvement/human-review.md +45 -0
  31. package/generated/internal-improvement/implement.md +42 -0
  32. package/generated/internal-improvement/invariants.md +38 -0
  33. package/generated/internal-improvement/manifest.json +29 -0
  34. package/generated/internal-improvement/options.md +47 -0
  35. package/generated/internal-improvement/outcome.md +38 -0
  36. package/generated/internal-improvement/plan.md +53 -0
  37. package/generated/internal-improvement/target.md +45 -0
  38. package/generated/internal-improvement/verify.md +45 -0
  39. package/generated/investigation/bound.md +45 -0
  40. package/generated/investigation/conclude.md +45 -0
  41. package/generated/investigation/explore.md +60 -0
  42. package/generated/investigation/frame.md +45 -0
  43. package/generated/investigation/gather.md +45 -0
  44. package/generated/investigation/manifest.json +21 -0
  45. package/generated/investigation/synthesize.md +51 -0
  46. package/generated/operational-stabilization/assess.md +46 -0
  47. package/generated/operational-stabilization/execute-observe.md +45 -0
  48. package/generated/operational-stabilization/manifest.json +19 -0
  49. package/generated/operational-stabilization/monitor.md +45 -0
  50. package/generated/operational-stabilization/outcome.md +38 -0
  51. package/generated/operational-stabilization/respond.md +40 -0
  52. package/generated/operational-stabilization/verify-recovery.md +45 -0
  53. package/generated/product-change/ai-audit.md +12 -6
  54. package/generated/product-change/ai-review.md +20 -7
  55. package/generated/product-change/decision.md +16 -4
  56. package/generated/product-change/define.md +9 -4
  57. package/generated/product-change/deploy.md +9 -4
  58. package/generated/product-change/human-review.md +16 -4
  59. package/generated/product-change/implement.md +14 -5
  60. package/generated/product-change/manifest.json +9 -4
  61. package/generated/product-change/options.md +16 -4
  62. package/generated/product-change/outcome.md +9 -4
  63. package/generated/product-change/plan.md +22 -4
  64. package/generated/product-change/questions.md +22 -4
  65. package/generated/product-change/research.md +16 -4
  66. package/generated/product-change/verify.md +9 -4
  67. package/generated/recommended-skills.json +24 -0
  68. package/generated/reference/CONSTITUTION.md +43 -0
  69. package/generated/reference/docs/acceptable-ai-use.md +217 -0
  70. package/generated/reference/docs/design/adapted-skill-guidance.md +27 -0
  71. package/generated/reference/docs/design/debugging-and-operations.md +119 -0
  72. package/generated/reference/docs/design/executable-workflows.md +122 -0
  73. package/generated/reference/docs/design/instruction-authoring.md +28 -0
  74. package/generated/reference/docs/design/process-taxonomy.md +144 -0
  75. package/generated/reference/docs/design/review-workbench.md +37 -0
  76. package/generated/reference/docs/engineering-practice.md +163 -0
  77. package/generated/reference/docs/evidence/evidence-standard.md +123 -0
  78. package/generated/reference/docs/evidence/research-map.md +98 -0
  79. package/generated/reference/docs/rationale.md +210 -0
  80. package/generated/reference/docs/recommended-skills.md +19 -0
  81. package/generated/reference/docs/references/pragmatic-programmer-page-index.md +113 -0
  82. package/generated/reference/docs/references/submitted-engineering-notes.md +306 -0
  83. package/generated/reference/docs/releasing-pi.md +89 -0
  84. package/generated/reference/docs/workflows/README.md +142 -0
  85. package/generated/reference/docs/workflows/corrective-debugging.md +221 -0
  86. package/generated/reference/docs/workflows/decision.md +149 -0
  87. package/generated/reference/docs/workflows/internal-improvement.md +219 -0
  88. package/generated/reference/docs/workflows/investigation.md +161 -0
  89. package/generated/reference/docs/workflows/operational-stabilization.md +187 -0
  90. package/generated/reference/docs/workflows/product-change.md +216 -0
  91. package/generated/reference/index.json +298 -0
  92. package/package.json +36 -25
  93. package/src/engine.ts +26 -7
  94. package/src/flow-guides.ts +168 -0
  95. package/src/guidance.ts +220 -72
  96. package/src/index.ts +696 -140
  97. package/src/reference-viewer.ts +73 -0
  98. package/src/reference.ts +107 -0
  99. package/src/review.ts +360 -0
  100. package/src/skills.ts +133 -0
  101. package/src/storage.ts +77 -13
  102. package/src/types.ts +1 -0
package/src/index.ts CHANGED
@@ -14,12 +14,35 @@ import {
14
14
  phaseGuide,
15
15
  phasePosition,
16
16
  } from "./guidance.js";
17
+ import {
18
+ findReference,
19
+ loadReferenceIndex,
20
+ readReference,
21
+ relevantReferences,
22
+ } from "./reference.js";
23
+ import { showReferenceViewer } from "./reference-viewer.js";
24
+ import {
25
+ collectReviewSnapshot,
26
+ extractFindingIds,
27
+ extractReviewFingerprint,
28
+ openInConfiguredEditor,
29
+ reviewDispositionTemplate,
30
+ reviewRequest,
31
+ reviewSnapshotMarkdown,
32
+ validateAiReviewArtifact,
33
+ validateReviewDisposition,
34
+ } from "./review.js";
35
+ import {
36
+ loadRecommendedSkills,
37
+ recommendedSkillsMarkdown,
38
+ relevantRecommendedSkills,
39
+ } from "./skills.js";
17
40
  import { humanActor, projectRoot, RunStore } from "./storage.js";
18
41
  import type { Actor, Capability, EventAction, Run, RunState } from "./types.js";
19
42
 
20
43
  const wasmPath =
21
44
  process.env.AHEAD_WASM_PATH || fileURLToPath(new URL("../dist/ahead_wasm.wasm", import.meta.url));
22
- const instructionDirectory = fileURLToPath(new URL("../generated/product-change", import.meta.url));
45
+ const instructionDirectory = fileURLToPath(new URL("../generated", import.meta.url));
23
46
  const enginePromise = AheadEngine.load(wasmPath);
24
47
  const instructions = new Map<string, string>();
25
48
 
@@ -38,97 +61,146 @@ const RecordArtifactParams = Type.Object({
38
61
  kind: Type.String({ description: "Artifact kind permitted for AI in the active phase" }),
39
62
  content: Type.String({ description: "Complete Markdown artifact content", maxLength: 100_000 }),
40
63
  });
64
+ const ReferenceParams = Type.Object({
65
+ topic: Type.Optional(
66
+ Type.String({
67
+ description: "Reference id, path, or title; omit to list phase-relevant references",
68
+ }),
69
+ ),
70
+ });
41
71
 
42
72
  export default function aheadExtension(pi: ExtensionAPI): void {
43
73
  pi.registerCommand("ahead", {
44
74
  description: "Enter or continue the guided AHEAD mode",
45
- handler: async (args, ctx) => command(ctx, async () => {
46
- await openAheadMode(pi, args, ctx);
47
- }),
75
+ handler: async (args, ctx) =>
76
+ command(ctx, async () => {
77
+ await openAheadMode(pi, args, ctx);
78
+ }),
79
+ });
80
+
81
+ pi.registerCommand("ahead-guide", {
82
+ description: "Read the AHEAD framework guidance relevant to the active phase",
83
+ handler: async (args, ctx) =>
84
+ command(ctx, async () => {
85
+ await showAheadGuide(ctx, args);
86
+ }),
87
+ });
88
+
89
+ pi.registerCommand("ahead-skills", {
90
+ description: "Inspect optional skills reviewed for the active AHEAD phase",
91
+ handler: async (_args, ctx) =>
92
+ command(ctx, async () => {
93
+ await showRecommendedSkills(ctx);
94
+ }),
95
+ });
96
+
97
+ pi.registerCommand("ahead-review", {
98
+ description: "Inspect and perform the current changeset review handoff",
99
+ handler: async (_args, ctx) =>
100
+ command(ctx, async () => {
101
+ await openReviewWorkbench(pi, ctx);
102
+ }),
48
103
  });
49
104
 
50
105
  pi.registerCommand("ahead-start", {
51
- description: "Advanced: start a Product Change run directly",
52
- handler: async (args, ctx) => command(ctx, async () => {
53
- await startRun(ctx, args);
54
- }),
106
+ description: "Advanced: start a workflow directly with [workflow-id ::] title",
107
+ handler: async (args, ctx) =>
108
+ command(ctx, async () => {
109
+ await startRun(ctx, args);
110
+ }),
55
111
  });
56
112
 
57
113
  pi.registerCommand("ahead-status", {
58
114
  description: "Advanced: show the raw active AHEAD phase contract",
59
- handler: async (_args, ctx) => command(ctx, async () => {
60
- const run = await requireRun(ctx);
61
- const state = (await enginePromise).deriveState(run);
62
- await refreshUi(ctx, run);
63
- ctx.ui.notify(formatState(state), state.blockers.length ? "warning" : "info");
64
- }),
115
+ handler: async (_args, ctx) =>
116
+ command(ctx, async () => {
117
+ const run = await requireRun(ctx);
118
+ const state = (await enginePromise).deriveState(run);
119
+ await refreshUi(ctx, run);
120
+ ctx.ui.notify(formatState(state), state.blockers.length ? "warning" : "info");
121
+ }),
65
122
  });
66
123
 
67
124
  pi.registerCommand("ahead-record", {
68
125
  description: "Advanced: record a human-owned artifact directly",
69
- handler: async (args, ctx) => command(ctx, async () => {
70
- await recordHumanArtifact(ctx, args.trim());
71
- }),
126
+ handler: async (args, ctx) =>
127
+ command(ctx, async () => {
128
+ await recordHumanArtifact(ctx, args.trim());
129
+ }),
72
130
  });
73
131
 
74
132
  pi.registerCommand("ahead-accept", {
75
133
  description: "Advanced: accept the active gate without advancing",
76
- handler: async (_args, ctx) => command(ctx, async () => {
77
- if (!ctx.hasUI) throw new Error("/ahead-accept requires interactive or RPC UI support");
78
- const engine = await enginePromise;
79
- const store = storeFor(ctx);
80
- const run = await requireRun(ctx);
81
- const state = engine.deriveState(run);
82
- const confirmed = await ctx.ui.confirm(
83
- `Accept ${state.gate.id}?`,
84
- `${state.gate.title}\n\nThis records human acceptance as ${humanActor(store.projectRoot).identity}.`,
85
- );
86
- if (!confirmed) return;
87
- const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
88
- type: "gate_accepted",
89
- phase: state.phase.id,
90
- gate: state.gate.id,
91
- });
92
- await store.save(updated);
93
- await refreshUi(ctx, updated);
94
- ctx.ui.notify(`Accepted gate ${state.gate.id}. Use /ahead-advance when ready.`, "info");
95
- }),
134
+ handler: async (_args, ctx) =>
135
+ command(ctx, async () => {
136
+ if (!ctx.hasUI) {
137
+ throw new Error("/ahead-accept requires interactive or RPC UI support");
138
+ }
139
+ const engine = await enginePromise;
140
+ const store = storeFor(ctx);
141
+ const run = await requireRun(ctx);
142
+ const state = engine.deriveState(run);
143
+ const confirmed = await ctx.ui.confirm(
144
+ `Accept ${state.gate.id}?`,
145
+ `${state.gate.title}\n\nThis records human acceptance as ${humanActor(store.projectRoot).identity}.`,
146
+ );
147
+ if (!confirmed) {
148
+ return;
149
+ }
150
+ const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
151
+ type: "gate_accepted",
152
+ phase: state.phase.id,
153
+ gate: state.gate.id,
154
+ });
155
+ await store.save(updated);
156
+ await refreshUi(ctx, updated);
157
+ ctx.ui.notify(`Accepted gate ${state.gate.id}. Use /ahead-advance when ready.`, "info");
158
+ }),
96
159
  });
97
160
 
98
161
  pi.registerCommand("ahead-advance", {
99
162
  description: "Advanced: advance an already accepted gate",
100
- handler: async (_args, ctx) => command(ctx, async () => {
101
- if (!ctx.hasUI) throw new Error("/ahead-advance requires interactive or RPC UI support");
102
- const engine = await enginePromise;
103
- const store = storeFor(ctx);
104
- const run = await requireRun(ctx);
105
- const state = engine.deriveState(run);
106
- const destination = state.phase.next ?? "closed";
107
- const confirmed = await ctx.ui.confirm(
108
- state.phase.next ? `Advance to ${state.phase.next}?` : "Close this AHEAD run?",
109
- `Current phase: ${state.phase.title}\nDestination: ${destination}\nActor: ${humanActor(store.projectRoot).identity}`,
110
- );
111
- if (!confirmed) return;
112
- const action: EventAction = state.phase.next
113
- ? {
114
- type: "phase_transitioned",
115
- from: state.phase.id,
116
- to: state.phase.next,
117
- direction: "advance",
118
- }
119
- : { type: "run_closed", phase: state.phase.id };
120
- const updated = engine.applyEvent(run, humanActor(store.projectRoot), action);
121
- await store.save(updated);
122
- await refreshUi(ctx, updated);
123
- ctx.ui.notify(state.phase.next ? `Advanced to ${state.phase.next}.` : "AHEAD run closed.", "info");
124
- }),
163
+ handler: async (_args, ctx) =>
164
+ command(ctx, async () => {
165
+ if (!ctx.hasUI) {
166
+ throw new Error("/ahead-advance requires interactive or RPC UI support");
167
+ }
168
+ const engine = await enginePromise;
169
+ const store = storeFor(ctx);
170
+ const run = await requireRun(ctx);
171
+ const state = engine.deriveState(run);
172
+ const destination = state.phase.next ?? "closed";
173
+ const confirmed = await ctx.ui.confirm(
174
+ state.phase.next ? `Advance to ${state.phase.next}?` : "Close this AHEAD run?",
175
+ `Current phase: ${state.phase.title}\nDestination: ${destination}\nActor: ${humanActor(store.projectRoot).identity}`,
176
+ );
177
+ if (!confirmed) {
178
+ return;
179
+ }
180
+ const action: EventAction = state.phase.next
181
+ ? {
182
+ type: "phase_transitioned",
183
+ from: state.phase.id,
184
+ to: state.phase.next,
185
+ direction: "advance",
186
+ }
187
+ : { type: "run_closed", phase: state.phase.id };
188
+ const updated = engine.applyEvent(run, humanActor(store.projectRoot), action);
189
+ await store.save(updated);
190
+ await refreshUi(ctx, updated);
191
+ ctx.ui.notify(
192
+ state.phase.next ? `Advanced to ${state.phase.next}.` : "AHEAD run closed.",
193
+ "info",
194
+ );
195
+ }),
125
196
  });
126
197
 
127
198
  pi.registerCommand("ahead-return", {
128
199
  description: "Advanced: return to an earlier phase with a reason",
129
- handler: async (args, ctx) => command(ctx, async () => {
130
- await returnToEarlierPhase(ctx, args);
131
- }),
200
+ handler: async (args, ctx) =>
201
+ command(ctx, async () => {
202
+ await returnToEarlierPhase(ctx, args);
203
+ }),
132
204
  });
133
205
 
134
206
  pi.registerCommand("ahead-help", {
@@ -136,7 +208,10 @@ export default function aheadExtension(pi: ExtensionAPI): void {
136
208
  handler: async (_args, ctx) => {
137
209
  ctx.ui.notify(
138
210
  [
139
- "/ahead [title] — enter, resume, or act in guided AHEAD mode",
211
+ "/ahead [title] — choose a workflow for new work, or resume guided AHEAD mode",
212
+ "/ahead-guide [topic] — read the applicable AHEAD framework Markdown",
213
+ "/ahead-skills — inspect optional reviewed skills relevant to this phase",
214
+ "/ahead-review — inspect the exact changeset and review handoff",
140
215
  "",
141
216
  "Once started, the repository run remains in AHEAD mode until an accountable human closes the outcome.",
142
217
  "Use normal conversation to think and work with AI. Run /ahead whenever you want the next valid action.",
@@ -153,7 +228,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
153
228
  pi.registerTool({
154
229
  name: "ahead_get_context",
155
230
  label: "AHEAD context",
156
- description: "Read the authoritative active AHEAD workflow state, phase contract, artifacts, gate, and blockers.",
231
+ description:
232
+ "Read the authoritative active AHEAD workflow state, phase contract, artifacts, gate, and blockers.",
157
233
  promptSnippet: "Read the active AHEAD workflow state and human/AI boundaries.",
158
234
  parameters: EmptyParams,
159
235
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
@@ -165,10 +241,84 @@ export default function aheadExtension(pi: ExtensionAPI): void {
165
241
  },
166
242
  });
167
243
 
244
+ pi.registerTool({
245
+ name: "ahead_get_recommended_skills",
246
+ label: "AHEAD recommended skills",
247
+ description:
248
+ "List optional third-party skills reviewed for the active phase. Never installs a skill.",
249
+ promptSnippet:
250
+ "Discover reviewed optional skills only when they may help the active AHEAD work.",
251
+ parameters: EmptyParams,
252
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
253
+ return toolResult(async () => {
254
+ const catalog = await loadRecommendedSkills();
255
+ const run = await storeFor(ctx).loadCurrent();
256
+ const state = run ? (await enginePromise).deriveState(run) : undefined;
257
+ const phaseId = state && !state.closed ? state.phase.id : undefined;
258
+ const workflowId = state && !state.closed ? state.workflow_id : undefined;
259
+ return {
260
+ reviewed_at: catalog.reviewed_at,
261
+ phase: phaseId ?? null,
262
+ workflow: workflowId ?? null,
263
+ recommended: relevantRecommendedSkills(catalog, workflowId, phaseId),
264
+ available: catalog.skills,
265
+ instruction:
266
+ "Do not install automatically. Explain why a skill applies, show the pinned source and command, and let the human opt in. AHEAD remains authoritative.",
267
+ };
268
+ });
269
+ },
270
+ });
271
+
272
+ pi.registerTool({
273
+ name: "ahead_get_review_snapshot",
274
+ label: "AHEAD review snapshot",
275
+ description:
276
+ "Capture the exact current Git changeset, merge base, status, paths, diff, and stable fingerprint without modifying it.",
277
+ promptSnippet: "Bind review findings to the exact current AHEAD changeset fingerprint.",
278
+ parameters: EmptyParams,
279
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
280
+ return toolResult(async () => collectReviewSnapshot(storeFor(ctx).projectRoot));
281
+ },
282
+ });
283
+
284
+ pi.registerTool({
285
+ name: "ahead_get_reference",
286
+ label: "AHEAD framework reference",
287
+ description:
288
+ "List or read packaged AHEAD Constitution, philosophy, acceptable-use, engineering-practice, workflow, and evidence Markdown.",
289
+ promptSnippet:
290
+ "Retrieve relevant AHEAD framework guidance when the phase or policy is unclear.",
291
+ parameters: ReferenceParams,
292
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
293
+ return toolResult(async () => {
294
+ const run = await storeFor(ctx).loadCurrent();
295
+ const state = run ? (await enginePromise).deriveState(run) : undefined;
296
+ const phase = state && !state.closed ? state.phase.id : undefined;
297
+ const workflowId = state && !state.closed ? state.workflow_id : undefined;
298
+ if (!params.topic?.trim()) {
299
+ const index = await loadReferenceIndex();
300
+ return {
301
+ phase: phase ?? null,
302
+ workflow: workflowId ?? null,
303
+ recommended: await relevantReferences(workflowId, phase),
304
+ available: index.references.map(({ id, title, path }) => ({ id, title, path })),
305
+ instruction: "Request one reference by id, path, or title. Load only what is relevant.",
306
+ };
307
+ }
308
+ const entry = await findReference(params.topic);
309
+ if (!entry) {
310
+ throw new Error(`No packaged AHEAD reference matches ${params.topic}`);
311
+ }
312
+ return { reference: entry, content: await readReference(entry) };
313
+ });
314
+ },
315
+ });
316
+
168
317
  pi.registerTool({
169
318
  name: "ahead_record_artifact",
170
319
  label: "Record AHEAD artifact",
171
- description: "Persist an AI-owned or shared artifact permitted by the active phase. Cannot record human-owned artifacts.",
320
+ description:
321
+ "Persist an AI-owned or shared artifact permitted by the active phase. Cannot record human-owned artifacts.",
172
322
  promptSnippet: "Record an AI-permitted artifact in the active AHEAD run.",
173
323
  parameters: RecordArtifactParams,
174
324
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -184,6 +334,13 @@ export default function aheadExtension(pi: ExtensionAPI): void {
184
334
  `AI cannot record ${params.kind} in phase ${state.phase.id}`,
185
335
  );
186
336
  }
337
+ if (artifact.kind === "ai-review") {
338
+ const snapshot = await collectReviewSnapshot(store.projectRoot);
339
+ const errors = validateAiReviewArtifact(params.content, snapshot.fingerprint);
340
+ if (errors.length) {
341
+ throw new AheadEngineError("invalid_ai_review", errors.join("; "));
342
+ }
343
+ }
187
344
  const path = store.artifactPath(run, state.phase.id, artifact.kind);
188
345
  const updated = engine.applyEvent(run, aiActor(ctx), {
189
346
  type: "artifact_recorded",
@@ -206,7 +363,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
206
363
  pi.registerTool({
207
364
  name: "ahead_request_transition",
208
365
  label: "Request AHEAD transition",
209
- description: "Report whether a human can advance the active AHEAD phase. This tool never accepts a gate or transitions state.",
366
+ description:
367
+ "Report whether a human can advance the active AHEAD phase. This tool never accepts a gate or transitions state.",
210
368
  parameters: EmptyParams,
211
369
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
212
370
  return toolResult(async () => {
@@ -226,10 +384,14 @@ export default function aheadExtension(pi: ExtensionAPI): void {
226
384
  pi.registerTool({
227
385
  name: "ahead_validate",
228
386
  label: "Validate AHEAD run",
229
- description: "Replay and validate the active AHEAD event log against the embedded workflow contract.",
387
+ description:
388
+ "Replay and validate the active AHEAD event log against the embedded workflow contract.",
230
389
  parameters: EmptyParams,
231
390
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
232
- return toolResult(async () => ({ valid: true, state: (await enginePromise).validateRun(await requireRun(ctx)) }));
391
+ return toolResult(async () => ({
392
+ valid: true,
393
+ state: (await enginePromise).validateRun(await requireRun(ctx)),
394
+ }));
233
395
  },
234
396
  });
235
397
 
@@ -252,17 +414,22 @@ export default function aheadExtension(pi: ExtensionAPI): void {
252
414
 
253
415
  pi.on("before_agent_start", async (event, ctx) => {
254
416
  const run = await storeFor(ctx).loadCurrent();
255
- if (!run) return;
417
+ if (!run) {
418
+ return undefined;
419
+ }
256
420
  const engine = await enginePromise;
257
421
  const state = engine.deriveState(run);
258
- if (state.closed) return;
422
+ if (state.closed) {
423
+ return undefined;
424
+ }
259
425
  const workflow = engine.getWorkflow(run.workflow_id);
260
- const guidance = phaseGuide(state.phase.id);
426
+ const guidance = phaseGuide(run.workflow_id, state.phase.id);
261
427
  const action = nextAction(state, workflow);
262
- const phaseInstructions = await loadInstructions(state.phase.id);
428
+ const phaseInstructions = await loadInstructions(run.workflow_id, state.phase.id);
263
429
  const liveContext = [
264
430
  "# Live AHEAD run",
265
431
  `- Run: ${run.id} — ${run.title}`,
432
+ `- Workflow: ${workflow.title} (${workflow.id})`,
266
433
  `- Phase: ${state.phase.id} visit ${state.phase.visit}`,
267
434
  `- Gate accepted: ${state.gate.accepted}`,
268
435
  `- Current blockers: ${state.blockers.length ? state.blockers.join("; ") : "none"}`,
@@ -277,15 +444,24 @@ export default function aheadExtension(pi: ExtensionAPI): void {
277
444
  "- Never author a human-owned artifact, make a human decision, accept a gate, transition the run, approve a change, or claim accountability.",
278
445
  "- When required AI-owned work is ready, record it with ahead_record_artifact and explain what the human must validate or decide.",
279
446
  "- Treat AI review findings as hypotheses. Independent human review remains required for lasting engineering changes.",
447
+ "- Humans may ask questions at any phase. During implementation, help them understand or solve the problem without taking over; if their first attempt or current model is missing, ask for it.",
448
+ "- When AHEAD policy or rationale is unclear, use ahead_get_reference to retrieve only the applicable packaged Markdown.",
449
+ "- Use ahead_get_recommended_skills only when an optional reviewed skill could materially help. Never install one without the human's explicit choice; AHEAD remains authoritative.",
280
450
  ].join("\n");
281
451
  return { systemPrompt: `${event.systemPrompt}\n\n${phaseInstructions}\n\n${liveContext}\n` };
282
452
  });
283
453
 
284
454
  pi.on("tool_call", async (event, ctx) => {
285
- if (event.toolName.startsWith("ahead_")) return;
455
+ if (event.toolName.startsWith("ahead_")) {
456
+ return undefined;
457
+ }
286
458
  const run = await storeFor(ctx).loadCurrent();
287
- if (!run) return;
288
- if ((await enginePromise).deriveState(run).closed) return;
459
+ if (!run) {
460
+ return undefined;
461
+ }
462
+ if ((await enginePromise).deriveState(run).closed) {
463
+ return undefined;
464
+ }
289
465
  const capability = toolCapabilities[event.toolName];
290
466
  if (!capability) {
291
467
  return {
@@ -295,10 +471,13 @@ export default function aheadExtension(pi: ExtensionAPI): void {
295
471
  }
296
472
  try {
297
473
  const decision = (await enginePromise).toolAllowed(run, capability);
298
- if (!decision.allowed) return { block: true, reason: `AHEAD: ${decision.reason}` };
474
+ if (!decision.allowed) {
475
+ return { block: true, reason: `AHEAD: ${decision.reason}` };
476
+ }
299
477
  } catch (error) {
300
478
  return { block: true, reason: `AHEAD state validation failed: ${errorMessage(error)}` };
301
479
  }
480
+ return undefined;
302
481
  });
303
482
  }
304
483
 
@@ -307,31 +486,42 @@ interface GuidedAction {
307
486
  run: () => Promise<void>;
308
487
  }
309
488
 
310
- async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionCommandContext): Promise<void> {
489
+ async function openAheadMode(
490
+ pi: ExtensionAPI,
491
+ args: string,
492
+ ctx: ExtensionCommandContext,
493
+ ): Promise<void> {
311
494
  const engine = await enginePromise;
312
495
  const store = storeFor(ctx);
313
496
  let run = await store.loadCurrent();
314
497
 
315
498
  if (run && engine.deriveState(run).closed) {
316
499
  if (!ctx.hasUI) {
317
- ctx.ui.notify("The current AHEAD run is complete. Start new work in an interactive Pi session.", "info");
500
+ ctx.ui.notify(
501
+ "The current AHEAD run is complete. Start new work in an interactive Pi session.",
502
+ "info",
503
+ );
318
504
  return;
319
505
  }
320
506
  const choice = await ctx.ui.select("AHEAD work is complete", [
321
- "Start a new Product Change",
507
+ "Start new AHEAD work",
322
508
  "View the completed run",
323
509
  ]);
324
510
  if (choice === "View the completed run") {
325
511
  ctx.ui.notify(formatState(engine.deriveState(run)), "info");
326
512
  return;
327
513
  }
328
- if (choice !== "Start a new Product Change") return;
514
+ if (choice !== "Start new AHEAD work") {
515
+ return;
516
+ }
329
517
  run = undefined;
330
518
  }
331
519
 
332
520
  if (!run) {
333
521
  run = await startRun(ctx, args);
334
- if (!run) return;
522
+ if (!run) {
523
+ return;
524
+ }
335
525
  }
336
526
 
337
527
  await refreshUi(ctx, run);
@@ -342,42 +532,60 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
342
532
 
343
533
  const state = engine.deriveState(run);
344
534
  const workflow = engine.getWorkflow(run.workflow_id);
345
- const guidance = phaseGuide(state.phase.id);
535
+ const guidance = phaseGuide(run.workflow_id, state.phase.id);
346
536
  const action = nextAction(state, workflow);
347
537
  const actions: GuidedAction[] = [];
348
- const missingRequired = state.artifacts.filter((artifact) => artifact.required && !artifact.present);
538
+ const missingRequired = state.artifacts.filter(
539
+ (artifact) => artifact.required && !artifact.present,
540
+ );
349
541
  if (action.artifactKind) {
542
+ const actionArtifact = state.artifacts.find(
543
+ (artifact) => artifact.kind === action.artifactKind,
544
+ );
350
545
  actions.push({
351
546
  label: action.label,
352
- run: action.actor === "ai"
353
- ? async () => requestAiAssistance(pi, state, action.artifactKind)
354
- : async () => recordHumanArtifact(ctx, action.artifactKind ?? ""),
547
+ run:
548
+ action.actor === "ai"
549
+ ? state.phase.id === "ai-review"
550
+ ? async () => openReviewWorkbench(pi, ctx)
551
+ : async () => requestAiAssistance(pi, state, action.artifactKind)
552
+ : async () => recordHumanArtifact(ctx, action.artifactKind ?? ""),
355
553
  });
554
+ if (action.actor === "ai" && actionArtifact?.actor === "any") {
555
+ actions.push({
556
+ label: `Write ${actionArtifact.title} yourself`,
557
+ run: async () => recordHumanArtifact(ctx, actionArtifact.kind),
558
+ });
559
+ }
356
560
  }
357
561
 
358
562
  if (action.optional) {
359
- const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor === "human");
563
+ const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor !== "ai");
360
564
  if (nextHumanArtifact) {
361
565
  actions.push({
362
566
  label: `Continue without optional AI challenge · Write ${nextHumanArtifact.title}`,
363
567
  run: async () => recordHumanArtifact(ctx, nextHumanArtifact.kind),
364
568
  });
569
+ } else if (missingRequired.length === 0) {
570
+ actions.push({
571
+ label: `Continue without optional AI contribution · Accept ${state.gate.title}`,
572
+ run: async () => acceptAndContinue(ctx),
573
+ });
365
574
  }
366
575
  }
367
576
 
368
577
  if (missingRequired.length === 0 && !action.artifactKind) {
369
578
  actions.push({
370
- label: state.gate.accepted
371
- ? action.label
372
- : `Accept and continue · ${state.gate.title}`,
579
+ label: state.gate.accepted ? action.label : `Accept and continue · ${state.gate.title}`,
373
580
  run: async () => acceptAndContinue(ctx),
374
581
  });
375
582
  }
376
583
 
377
584
  if (
378
- state.allowed_ai_capabilities.length > 0
379
- && action.actor !== "ai"
380
- && !missingRequired.some((artifact) => artifact.actor === "ai")
585
+ state.allowed_ai_capabilities.length > 0 &&
586
+ action.actor !== "ai" &&
587
+ !missingRequired.some((artifact) => artifact.actor === "ai") &&
588
+ state.phase.id !== "implement"
381
589
  ) {
382
590
  actions.push({
383
591
  label: `Ask AI to assist · ${state.phase.title}`,
@@ -385,6 +593,20 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
385
593
  });
386
594
  }
387
595
 
596
+ if (state.phase.id === "implement") {
597
+ actions.push({
598
+ label: "Ask AI for help understanding or solving a problem",
599
+ run: async () => askImplementationQuestion(pi, ctx, state),
600
+ });
601
+ }
602
+
603
+ if (state.phase.id === "ai-review" || state.phase.id === "human-review") {
604
+ actions.push({
605
+ label: "Open the changeset review workbench",
606
+ run: async () => openReviewWorkbench(pi, ctx),
607
+ });
608
+ }
609
+
388
610
  if (state.return_targets.length > 0) {
389
611
  actions.push({
390
612
  label: "Return to an earlier phase",
@@ -392,6 +614,16 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
392
614
  });
393
615
  }
394
616
 
617
+ actions.push({
618
+ label: "Read AHEAD framework guidance for this phase",
619
+ run: async () => showAheadGuide(ctx, ""),
620
+ });
621
+
622
+ actions.push({
623
+ label: "Inspect optional skills reviewed for this phase",
624
+ run: async () => showRecommendedSkills(ctx),
625
+ });
626
+
395
627
  actions.push({
396
628
  label: "Explain this phase and its expectations",
397
629
  run: async () => {
@@ -413,10 +645,167 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
413
645
  actions.map((candidate) => candidate.label),
414
646
  );
415
647
  const chosen = actions.find((candidate) => candidate.label === selected);
416
- if (chosen) await chosen.run();
648
+ if (chosen) {
649
+ await chosen.run();
650
+ }
651
+ }
652
+
653
+ async function showRecommendedSkills(ctx: ExtensionCommandContext): Promise<void> {
654
+ if (!ctx.hasUI) {
655
+ throw new Error("Inspecting recommended skills requires interactive or RPC UI support");
656
+ }
657
+ const catalog = await loadRecommendedSkills();
658
+ const run = await storeFor(ctx).loadCurrent();
659
+ const state = run ? (await enginePromise).deriveState(run) : undefined;
660
+ const phaseId = state && !state.closed ? state.phase.id : undefined;
661
+ const workflowId = state && !state.closed ? state.workflow_id : undefined;
662
+ const relevant = relevantRecommendedSkills(catalog, workflowId, phaseId);
663
+ await showReferenceViewer(
664
+ ctx,
665
+ relevant.length ? `AHEAD skills · ${state?.phase.title}` : "AHEAD recommended skills",
666
+ recommendedSkillsMarkdown(catalog, relevant.length ? relevant : catalog.skills),
667
+ );
417
668
  }
418
669
 
419
- async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): Promise<Run | undefined> {
670
+ async function openReviewWorkbench(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
671
+ if (!ctx.hasUI) {
672
+ throw new Error("The review workbench requires interactive or RPC UI support");
673
+ }
674
+ const store = storeFor(ctx);
675
+ const run = await requireRun(ctx);
676
+ const state = (await enginePromise).deriveState(run);
677
+ const snapshot = await collectReviewSnapshot(store.projectRoot);
678
+ const aiReview = state.artifacts.find((artifact) => artifact.kind === "ai-review");
679
+ const disposition = state.artifacts.find((artifact) => artifact.kind === "review-disposition");
680
+ const options = ["View snapshot and changed files", "View exact terminal diff"];
681
+ if (snapshot.changed_files.length) {
682
+ options.push("Open a changed file in the configured editor");
683
+ }
684
+ if (state.phase.id === "ai-review" && !aiReview?.present) {
685
+ options.push("Request AI review of this exact snapshot");
686
+ }
687
+ if (state.phase.id === "ai-review" && aiReview?.present && !disposition?.present) {
688
+ options.push("Disposition the AI findings as the implementing human");
689
+ }
690
+ if (state.phase.id === "human-review") {
691
+ options.push("Record the independent human review of this snapshot");
692
+ }
693
+ const selected = await ctx.ui.select(
694
+ `AHEAD review · ${state.phase.title} · ${snapshot.changed_files.length} changed files`,
695
+ options,
696
+ );
697
+ if (selected === "View snapshot and changed files") {
698
+ await showReferenceViewer(ctx, "AHEAD exact review snapshot", reviewSnapshotMarkdown(snapshot));
699
+ } else if (selected === "View exact terminal diff") {
700
+ await showReferenceViewer(
701
+ ctx,
702
+ `AHEAD diff · ${snapshot.fingerprint.slice(0, 12)}`,
703
+ `${reviewSnapshotMarkdown(snapshot)}\n\n## Diff\n\n\`\`\`diff\n${snapshot.diff || "No tracked diff."}\n\`\`\``,
704
+ );
705
+ } else if (selected === "Open a changed file in the configured editor") {
706
+ const path = await ctx.ui.select("Open changed file", snapshot.changed_files);
707
+ if (path && !openInConfiguredEditor(store.projectRoot, { path })) {
708
+ ctx.ui.notify(
709
+ `No supported editor was detected. Open ${path} from the repository, or set AHEAD_EDITOR=vscode.`,
710
+ "info",
711
+ );
712
+ }
713
+ } else if (selected === "Request AI review of this exact snapshot") {
714
+ pi.sendUserMessage(reviewRequest(snapshot));
715
+ } else if (selected === "Disposition the AI findings as the implementing human") {
716
+ await recordHumanArtifact(ctx, "review-disposition");
717
+ } else if (selected === "Record the independent human review of this snapshot") {
718
+ await recordHumanArtifact(ctx, "human-review");
719
+ }
720
+ }
721
+
722
+ async function askImplementationQuestion(
723
+ pi: ExtensionAPI,
724
+ ctx: ExtensionCommandContext,
725
+ state: RunState,
726
+ ): Promise<void> {
727
+ if (!ctx.hasUI) {
728
+ throw new Error("Implementation coaching requires interactive or RPC UI support");
729
+ }
730
+ const question = await ctx.ui.editor(
731
+ "AHEAD implementation help · human first",
732
+ [
733
+ "## What are you trying to understand or solve?",
734
+ "",
735
+ "",
736
+ "## What do you currently think is happening or should happen?",
737
+ "",
738
+ "",
739
+ "## What have you tried or inspected so far?",
740
+ "",
741
+ "",
742
+ "## What kind of help would be useful?",
743
+ "",
744
+ "<!-- Ask for explanation, a hint, competing approaches, debugging help, or a bounded suggestion. -->",
745
+ "",
746
+ ].join("\n"),
747
+ );
748
+ if (!question?.trim()) {
749
+ return;
750
+ }
751
+ pi.sendUserMessage(
752
+ [
753
+ `AHEAD mode: help me with this ${state.phase.title} question while I remain the implementer.`,
754
+ "Use my current model and first attempt below. Help me understand or solve the problem with questions, explanation, evidence, hints, and bounded next steps.",
755
+ "Do not convert this question into autonomous implementation or author my human-owned records. If I later request a bounded mechanical edit, explain it so I can inspect and own it.",
756
+ "",
757
+ question.trim(),
758
+ ].join("\n"),
759
+ );
760
+ }
761
+
762
+ async function showAheadGuide(ctx: ExtensionCommandContext, requestedTopic: string): Promise<void> {
763
+ if (!ctx.hasUI) {
764
+ throw new Error("Reading AHEAD framework guidance requires interactive or RPC UI support");
765
+ }
766
+ const run = await storeFor(ctx).loadCurrent();
767
+ const state = run ? (await enginePromise).deriveState(run) : undefined;
768
+ const phase = state && !state.closed ? state.phase.id : undefined;
769
+ const workflowId = state && !state.closed ? state.workflow_id : undefined;
770
+ const index = await loadReferenceIndex();
771
+ let entry =
772
+ requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all"
773
+ ? await findReference(requestedTopic)
774
+ : undefined;
775
+
776
+ if (requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all" && !entry) {
777
+ throw new Error(`No packaged AHEAD reference matches ${requestedTopic.trim()}`);
778
+ }
779
+
780
+ if (!entry) {
781
+ const recommended =
782
+ requestedTopic.trim().toLowerCase() === "all"
783
+ ? index.references
784
+ : await relevantReferences(workflowId, phase);
785
+ const browseAll = "Browse all packaged AHEAD Markdown";
786
+ const selected = await ctx.ui.select(
787
+ phase ? `AHEAD guidance · ${phase}` : "AHEAD framework guidance",
788
+ [
789
+ ...recommended.map((candidate) => candidate.title),
790
+ ...(recommended.length < index.references.length ? [browseAll] : []),
791
+ ],
792
+ );
793
+ if (!selected) {
794
+ return;
795
+ }
796
+ if (selected === browseAll) {
797
+ return showAheadGuide(ctx, "all");
798
+ }
799
+ entry = recommended.find((candidate) => candidate.title === selected);
800
+ }
801
+ if (!entry) {
802
+ return;
803
+ }
804
+
805
+ await showReferenceViewer(ctx, `AHEAD reference · ${entry.title}`, await readReference(entry));
806
+ }
807
+
808
+ async function startRun(ctx: ExtensionCommandContext, request: string): Promise<Run | undefined> {
420
809
  const engine = await enginePromise;
421
810
  const store = storeFor(ctx);
422
811
  const current = await store.loadCurrent();
@@ -427,9 +816,40 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
427
816
  );
428
817
  }
429
818
 
430
- const title = requestedTitle.trim()
431
- || (ctx.hasUI ? await ctx.ui.input("Enter AHEAD mode · Product Change", "What work are you doing?") : undefined);
432
- if (!title?.trim()) return undefined;
819
+ const workflows = engine.listWorkflows();
820
+ const parsed = parseStartRequest(
821
+ request,
822
+ workflows.map((workflow) => workflow.id),
823
+ );
824
+ let workflow = parsed.workflowId
825
+ ? workflows.find((candidate) => candidate.id === parsed.workflowId)
826
+ : undefined;
827
+ if (parsed.workflowId && !workflow) {
828
+ throw new AheadEngineError(
829
+ "unknown_workflow",
830
+ `unknown workflow ${parsed.workflowId}; choose one of: ${workflows.map((candidate) => candidate.id).join(", ")}`,
831
+ );
832
+ }
833
+ if (!workflow && ctx.hasUI) {
834
+ const selected = await ctx.ui.select(
835
+ "Choose the AHEAD workflow that fits this work",
836
+ workflows.map((candidate) => candidate.title),
837
+ );
838
+ workflow = workflows.find((candidate) => candidate.title === selected);
839
+ }
840
+ workflow ??= workflows.find((candidate) => candidate.id === "product-change");
841
+ if (!workflow) {
842
+ throw new AheadEngineError("missing_workflow", "the engine did not provide Product Change");
843
+ }
844
+
845
+ const title =
846
+ parsed.title ||
847
+ (ctx.hasUI
848
+ ? await ctx.ui.input(`Enter AHEAD mode · ${workflow.title}`, "What work are you doing?")
849
+ : undefined);
850
+ if (!title?.trim()) {
851
+ return undefined;
852
+ }
433
853
 
434
854
  const owner = humanActor(store.projectRoot);
435
855
  const run = engine.createRun({
@@ -437,13 +857,13 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
437
857
  title: title.trim(),
438
858
  owner,
439
859
  timestamp: new Date().toISOString(),
440
- workflow_id: "product-change",
860
+ workflow_id: workflow.id,
441
861
  });
442
862
  await store.save(run);
443
863
  await refreshUi(ctx, run);
444
864
  ctx.ui.notify(
445
865
  [
446
- `AHEAD mode started · ${run.title}`,
866
+ `AHEAD mode started · ${workflow.title} · ${run.title}`,
447
867
  "Human leads · AI assists",
448
868
  "This run remains active in the repository until an accountable human closes the outcome.",
449
869
  "Use /ahead for the next guided action; use normal conversation to think and work with AI.",
@@ -453,19 +873,27 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
453
873
  return run;
454
874
  }
455
875
 
456
- async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind: string): Promise<void> {
457
- if (!ctx.hasUI) throw new Error("Recording a human artifact requires interactive or RPC UI support");
876
+ async function recordHumanArtifact(
877
+ ctx: ExtensionCommandContext,
878
+ requestedKind: string,
879
+ ): Promise<void> {
880
+ if (!ctx.hasUI) {
881
+ throw new Error("Recording a human artifact requires interactive or RPC UI support");
882
+ }
458
883
  const engine = await enginePromise;
459
884
  const store = storeFor(ctx);
460
885
  const run = await requireRun(ctx);
461
886
  const state = engine.deriveState(run);
462
- const allowed = state.artifacts.filter((artifact) => artifact.actor !== "ai" && !artifact.present);
887
+ const allowed = state.artifacts.filter(
888
+ (artifact) => artifact.actor !== "ai" && !artifact.present,
889
+ );
463
890
  let kind = requestedKind.trim();
464
891
  if (!kind) {
465
- kind = (await ctx.ui.select(
466
- `AHEAD mode · Write for ${state.phase.title}`,
467
- allowed.map((artifact) => artifact.title),
468
- )) ?? "";
892
+ kind =
893
+ (await ctx.ui.select(
894
+ `AHEAD mode · Write for ${state.phase.title}`,
895
+ allowed.map((artifact) => artifact.title),
896
+ )) ?? "";
469
897
  kind = allowed.find((artifact) => artifact.title === kind)?.kind ?? kind;
470
898
  }
471
899
  const artifact = allowed.find((candidate) => candidate.kind === kind);
@@ -476,11 +904,15 @@ async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind:
476
904
  );
477
905
  }
478
906
 
907
+ const template = await humanArtifactTemplate(store, state, run, artifact.kind, artifact.title);
479
908
  const content = await ctx.ui.editor(
480
909
  `AHEAD mode · ${artifact.title} · write in your own words`,
481
- buildArtifactTemplate(run, state, artifact.kind, artifact.title),
910
+ template,
482
911
  );
483
- if (!content?.trim()) return;
912
+ if (!content?.trim()) {
913
+ return;
914
+ }
915
+ await validateHumanReviewArtifact(store, state, artifact.kind, content);
484
916
  const path = store.artifactPath(run, state.phase.id, artifact.kind);
485
917
  const action: EventAction = {
486
918
  type: "artifact_recorded",
@@ -498,14 +930,81 @@ async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind:
498
930
  );
499
931
  }
500
932
 
933
+ async function humanArtifactTemplate(
934
+ store: RunStore,
935
+ state: RunState,
936
+ run: Run,
937
+ kind: string,
938
+ title: string,
939
+ ): Promise<string> {
940
+ if (kind === "review-disposition") {
941
+ const reviewArtifact = state.artifacts.find((artifact) => artifact.kind === "ai-review");
942
+ if (!reviewArtifact?.path) {
943
+ throw new AheadEngineError(
944
+ "ai_review_missing",
945
+ "the AI review must be recorded before human disposition",
946
+ );
947
+ }
948
+ const aiReview = await store.readArtifact(reviewArtifact.path);
949
+ const snapshot = await collectReviewSnapshot(store.projectRoot);
950
+ return reviewDispositionTemplate(snapshot, extractFindingIds(aiReview));
951
+ }
952
+ if (kind === "human-review") {
953
+ const snapshot = await collectReviewSnapshot(store.projectRoot);
954
+ return `${buildArtifactTemplate(run, state, kind, title)}\n\nAHEAD-Review-Snapshot: ${snapshot.fingerprint}\n`;
955
+ }
956
+ return buildArtifactTemplate(run, state, kind, title);
957
+ }
958
+
959
+ async function validateHumanReviewArtifact(
960
+ store: RunStore,
961
+ state: RunState,
962
+ kind: string,
963
+ content: string,
964
+ ): Promise<void> {
965
+ if (kind !== "review-disposition" && kind !== "human-review") {
966
+ return;
967
+ }
968
+ const snapshot = await collectReviewSnapshot(store.projectRoot);
969
+ if (kind === "human-review") {
970
+ if (extractReviewFingerprint(content) !== snapshot.fingerprint) {
971
+ throw new AheadEngineError(
972
+ "review_snapshot_stale",
973
+ "the human review must identify the exact current AHEAD review snapshot",
974
+ );
975
+ }
976
+ return;
977
+ }
978
+ const reviewArtifact = state.artifacts.find((artifact) => artifact.kind === "ai-review");
979
+ if (!reviewArtifact?.path) {
980
+ throw new AheadEngineError("ai_review_missing", "the AI review artifact is missing");
981
+ }
982
+ const aiReview = await store.readArtifact(reviewArtifact.path);
983
+ const aiFingerprint = extractReviewFingerprint(aiReview);
984
+ if (!aiFingerprint || aiFingerprint !== snapshot.fingerprint) {
985
+ throw new AheadEngineError(
986
+ "review_snapshot_stale",
987
+ "the changeset changed after AI review; return to implementation and review the new snapshot",
988
+ );
989
+ }
990
+ const errors = validateReviewDisposition(
991
+ content,
992
+ snapshot.fingerprint,
993
+ extractFindingIds(aiReview),
994
+ );
995
+ if (errors.length) {
996
+ throw new AheadEngineError("review_disposition_incomplete", errors.join("; "));
997
+ }
998
+ }
999
+
501
1000
  function requestAiAssistance(pi: ExtensionAPI, state: RunState, requiredKind?: string): void {
502
- const guidance = phaseGuide(state.phase.id);
1001
+ const guidance = phaseGuide(state.workflow_id, state.phase.id);
503
1002
  const artifact = requiredKind
504
1003
  ? state.artifacts.find((candidate) => candidate.kind === requiredKind)
505
1004
  : undefined;
506
1005
  const request = artifact
507
1006
  ? [
508
- `AHEAD mode: perform the ${artifact.required ? "required" : "recommended"} ${state.phase.title} work for the exact current evidence and changeset.`,
1007
+ `AHEAD mode: perform the ${artifact.required ? "required" : "recommended"} ${state.phase.title} work for the exact current run and evidence.`,
509
1008
  `Produce ${artifact.title}.`,
510
1009
  `Follow the active human/AI boundary: ${guidance.ai}`,
511
1010
  `Use ahead_get_context first, then record the completed artifact as ${artifact.kind} with ahead_record_artifact.`,
@@ -521,7 +1020,9 @@ function requestAiAssistance(pi: ExtensionAPI, state: RunState, requiredKind?: s
521
1020
  }
522
1021
 
523
1022
  async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
524
- if (!ctx.hasUI) throw new Error("Accepting an AHEAD gate requires interactive or RPC UI support");
1023
+ if (!ctx.hasUI) {
1024
+ throw new Error("Accepting an AHEAD gate requires interactive or RPC UI support");
1025
+ }
525
1026
  const engine = await enginePromise;
526
1027
  const store = storeFor(ctx);
527
1028
  const run = await requireRun(ctx);
@@ -535,7 +1036,8 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
535
1036
  }
536
1037
 
537
1038
  const destination = state.phase.next
538
- ? engine.getWorkflow(run.workflow_id).phases.find((phase) => phase.id === state.phase.next)?.title ?? state.phase.next
1039
+ ? (engine.getWorkflow(run.workflow_id).phases.find((phase) => phase.id === state.phase.next)
1040
+ ?.title ?? state.phase.next)
539
1041
  : "close this AHEAD run";
540
1042
  const confirmed = await ctx.ui.confirm(
541
1043
  `Accept and continue from ${state.phase.title}?`,
@@ -548,7 +1050,9 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
548
1050
  "This records human acceptance. AI cannot perform this action.",
549
1051
  ].join("\n"),
550
1052
  );
551
- if (!confirmed) return;
1053
+ if (!confirmed) {
1054
+ return;
1055
+ }
552
1056
 
553
1057
  const actor = humanActor(store.projectRoot);
554
1058
  let updated = run;
@@ -561,7 +1065,10 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
561
1065
  state = engine.deriveState(updated);
562
1066
  }
563
1067
  if (!state.can_advance) {
564
- throw new AheadEngineError("cannot_advance", state.blockers.join("; ") || "the phase cannot advance");
1068
+ throw new AheadEngineError(
1069
+ "cannot_advance",
1070
+ state.blockers.join("; ") || "the phase cannot advance",
1071
+ );
565
1072
  }
566
1073
 
567
1074
  const action: EventAction = state.phase.next
@@ -578,7 +1085,10 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
578
1085
 
579
1086
  const nextState = engine.deriveState(updated);
580
1087
  if (nextState.closed) {
581
- ctx.ui.notify("AHEAD work complete. The accountable human accepted the outcome and closed the run.", "info");
1088
+ ctx.ui.notify(
1089
+ "AHEAD work complete. The accountable human accepted the outcome and closed the run.",
1090
+ "info",
1091
+ );
582
1092
  } else if (nextState.phase.id === "human-review") {
583
1093
  ctx.ui.notify(
584
1094
  [
@@ -597,14 +1107,22 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
597
1107
  }
598
1108
  }
599
1109
 
600
- async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarget: string): Promise<void> {
601
- if (!ctx.hasUI) throw new Error("Returning an AHEAD phase requires interactive or RPC UI support");
1110
+ async function returnToEarlierPhase(
1111
+ ctx: ExtensionCommandContext,
1112
+ requestedTarget: string,
1113
+ ): Promise<void> {
1114
+ if (!ctx.hasUI) {
1115
+ throw new Error("Returning an AHEAD phase requires interactive or RPC UI support");
1116
+ }
602
1117
  const engine = await enginePromise;
603
1118
  const store = storeFor(ctx);
604
1119
  const run = await requireRun(ctx);
605
1120
  const state = engine.deriveState(run);
606
1121
  if (!state.return_targets.length) {
607
- throw new AheadEngineError("no_return_target", `phase ${state.phase.id} has no return transition`);
1122
+ throw new AheadEngineError(
1123
+ "no_return_target",
1124
+ `phase ${state.phase.id} has no return transition`,
1125
+ );
608
1126
  }
609
1127
  const workflow = engine.getWorkflow(run.workflow_id);
610
1128
  const targetOptions = state.return_targets.map((target) => ({
@@ -628,12 +1146,16 @@ async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarge
628
1146
  const reason = await ctx.ui.editor(
629
1147
  `Why return to ${workflow.phases.find((phase) => phase.id === target)?.title ?? target}?`,
630
1148
  );
631
- if (!reason?.trim()) return;
1149
+ if (!reason?.trim()) {
1150
+ return;
1151
+ }
632
1152
  const confirmed = await ctx.ui.confirm(
633
1153
  `Return to ${target}?`,
634
1154
  "This opens a new phase visit. Earlier artifacts remain as history but cannot satisfy the reopened gate.",
635
1155
  );
636
- if (!confirmed) return;
1156
+ if (!confirmed) {
1157
+ return;
1158
+ }
637
1159
  const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
638
1160
  type: "phase_transitioned",
639
1161
  from: state.phase.id,
@@ -643,7 +1165,10 @@ async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarge
643
1165
  });
644
1166
  await store.save(updated);
645
1167
  await refreshUi(ctx, updated);
646
- ctx.ui.notify(`Returned to ${target}. AHEAD mode remains active with fresh evidence and gate requirements.`, "warning");
1168
+ ctx.ui.notify(
1169
+ `Returned to ${target}. AHEAD mode remains active with fresh evidence and gate requirements.`,
1170
+ "warning",
1171
+ );
647
1172
  }
648
1173
 
649
1174
  function storeFor(ctx: ExtensionContext): RunStore {
@@ -652,7 +1177,9 @@ function storeFor(ctx: ExtensionContext): RunStore {
652
1177
 
653
1178
  async function requireRun(ctx: ExtensionContext): Promise<Run> {
654
1179
  const run = await storeFor(ctx).loadCurrent();
655
- if (!run) throw new AheadEngineError("no_active_run", "no active AHEAD run; use /ahead [title]");
1180
+ if (!run) {
1181
+ throw new AheadEngineError("no_active_run", "no active AHEAD run; use /ahead [title]");
1182
+ }
656
1183
  return run;
657
1184
  }
658
1185
 
@@ -682,24 +1209,44 @@ async function refreshUi(ctx: ExtensionContext, supplied?: Run): Promise<void> {
682
1209
  ? `AHEAD · complete · ${state.workflow_id}`
683
1210
  : `AHEAD · ${position.current}/${position.total} · ${state.phase.id} · ${action.actor} action`,
684
1211
  );
685
- ctx.ui.setWidget(
686
- "ahead",
687
- buildWidgetLines(run, state, workflow),
688
- { placement: "aboveEditor" },
689
- );
1212
+ ctx.ui.setWidget("ahead", buildWidgetLines(run, state, workflow), { placement: "aboveEditor" });
690
1213
  }
691
1214
 
692
- async function loadInstructions(phase: string): Promise<string> {
693
- const cached = instructions.get(phase);
694
- if (cached) return cached;
695
- const content = await readFile(`${instructionDirectory}/${phase}.md`, "utf8");
696
- instructions.set(phase, content);
1215
+ async function loadInstructions(workflowId: string, phase: string): Promise<string> {
1216
+ const cacheKey = `${workflowId}/${phase}`;
1217
+ const cached = instructions.get(cacheKey);
1218
+ if (cached) {
1219
+ return cached;
1220
+ }
1221
+ const content = await readFile(`${instructionDirectory}/${cacheKey}.md`, "utf8");
1222
+ instructions.set(cacheKey, content);
697
1223
  return content;
698
1224
  }
699
1225
 
1226
+ function parseStartRequest(
1227
+ request: string,
1228
+ workflowIds: string[],
1229
+ ): { workflowId?: string; title: string } {
1230
+ const trimmed = request.trim();
1231
+ const separator = trimmed.indexOf("::");
1232
+ if (separator >= 0) {
1233
+ return {
1234
+ workflowId: trimmed.slice(0, separator).trim(),
1235
+ title: trimmed.slice(separator + 2).trim(),
1236
+ };
1237
+ }
1238
+ if (workflowIds.includes(trimmed)) {
1239
+ return { workflowId: trimmed, title: "" };
1240
+ }
1241
+ return { title: trimmed };
1242
+ }
1243
+
700
1244
  function formatState(state: RunState): string {
701
1245
  const artifacts = state.artifacts
702
- .map((artifact) => `${artifact.present ? "✓" : artifact.required ? "○" : "·"} ${artifact.kind} (${artifact.actor})`)
1246
+ .map(
1247
+ (artifact) =>
1248
+ `${artifact.present ? "✓" : artifact.required ? "○" : "·"} ${artifact.kind} (${artifact.actor})`,
1249
+ )
703
1250
  .join("\n");
704
1251
  return [
705
1252
  `${state.title} · ${state.workflow_id}@${state.workflow_version}`,
@@ -724,14 +1271,23 @@ async function command(ctx: ExtensionCommandContext, action: () => Promise<void>
724
1271
  async function toolResult(action: () => Promise<unknown>) {
725
1272
  try {
726
1273
  const result = await action();
727
- return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }], details: result };
1274
+ return {
1275
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
1276
+ details: result,
1277
+ };
728
1278
  } catch (error) {
729
1279
  const message = errorMessage(error);
730
- return { content: [{ type: "text" as const, text: `AHEAD error: ${message}` }], details: { error: message }, isError: true };
1280
+ return {
1281
+ content: [{ type: "text" as const, text: `AHEAD error: ${message}` }],
1282
+ details: { error: message },
1283
+ isError: true,
1284
+ };
731
1285
  }
732
1286
  }
733
1287
 
734
1288
  function errorMessage(error: unknown): string {
735
- if (error instanceof AheadEngineError) return `${error.code}: ${error.message}`;
1289
+ if (error instanceof AheadEngineError) {
1290
+ return `${error.code}: ${error.message}`;
1291
+ }
736
1292
  return error instanceof Error ? error.message : String(error);
737
1293
  }