ahead-pi 0.2.1 → 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 (91) hide show
  1. package/README.md +17 -7
  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 +7 -4
  54. package/generated/product-change/ai-review.md +15 -5
  55. package/generated/product-change/decision.md +11 -2
  56. package/generated/product-change/define.md +4 -2
  57. package/generated/product-change/deploy.md +4 -2
  58. package/generated/product-change/human-review.md +11 -2
  59. package/generated/product-change/implement.md +4 -2
  60. package/generated/product-change/manifest.json +8 -3
  61. package/generated/product-change/options.md +11 -2
  62. package/generated/product-change/outcome.md +4 -2
  63. package/generated/product-change/plan.md +17 -2
  64. package/generated/product-change/questions.md +17 -2
  65. package/generated/product-change/research.md +11 -2
  66. package/generated/product-change/verify.md +4 -2
  67. package/generated/recommended-skills.json +24 -0
  68. package/generated/reference/docs/design/adapted-skill-guidance.md +27 -0
  69. package/generated/reference/docs/design/executable-workflows.md +20 -8
  70. package/generated/reference/docs/design/instruction-authoring.md +28 -0
  71. package/generated/reference/docs/design/review-workbench.md +37 -0
  72. package/generated/reference/docs/recommended-skills.md +19 -0
  73. package/generated/reference/docs/workflows/README.md +3 -3
  74. package/generated/reference/docs/workflows/corrective-debugging.md +37 -19
  75. package/generated/reference/docs/workflows/decision.md +2 -2
  76. package/generated/reference/docs/workflows/internal-improvement.md +35 -23
  77. package/generated/reference/docs/workflows/investigation.md +3 -1
  78. package/generated/reference/docs/workflows/operational-stabilization.md +14 -12
  79. package/generated/reference/docs/workflows/product-change.md +14 -3
  80. package/generated/reference/index.json +131 -13
  81. package/package.json +34 -25
  82. package/src/engine.ts +26 -7
  83. package/src/flow-guides.ts +168 -0
  84. package/src/guidance.ts +218 -72
  85. package/src/index.ts +603 -175
  86. package/src/reference-viewer.ts +20 -18
  87. package/src/reference.ts +65 -14
  88. package/src/review.ts +360 -0
  89. package/src/skills.ts +133 -0
  90. package/src/storage.ts +77 -13
  91. package/src/types.ts +1 -0
package/src/index.ts CHANGED
@@ -21,12 +21,28 @@ import {
21
21
  relevantReferences,
22
22
  } from "./reference.js";
23
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";
24
40
  import { humanActor, projectRoot, RunStore } from "./storage.js";
25
41
  import type { Actor, Capability, EventAction, Run, RunState } from "./types.js";
26
42
 
27
43
  const wasmPath =
28
44
  process.env.AHEAD_WASM_PATH || fileURLToPath(new URL("../dist/ahead_wasm.wasm", import.meta.url));
29
- const instructionDirectory = fileURLToPath(new URL("../generated/product-change", import.meta.url));
45
+ const instructionDirectory = fileURLToPath(new URL("../generated", import.meta.url));
30
46
  const enginePromise = AheadEngine.load(wasmPath);
31
47
  const instructions = new Map<string, string>();
32
48
 
@@ -46,106 +62,145 @@ const RecordArtifactParams = Type.Object({
46
62
  content: Type.String({ description: "Complete Markdown artifact content", maxLength: 100_000 }),
47
63
  });
48
64
  const ReferenceParams = Type.Object({
49
- topic: Type.Optional(Type.String({ description: "Reference id, path, or title; omit to list phase-relevant references" })),
65
+ topic: Type.Optional(
66
+ Type.String({
67
+ description: "Reference id, path, or title; omit to list phase-relevant references",
68
+ }),
69
+ ),
50
70
  });
51
71
 
52
72
  export default function aheadExtension(pi: ExtensionAPI): void {
53
73
  pi.registerCommand("ahead", {
54
74
  description: "Enter or continue the guided AHEAD mode",
55
- handler: async (args, ctx) => command(ctx, async () => {
56
- await openAheadMode(pi, args, ctx);
57
- }),
75
+ handler: async (args, ctx) =>
76
+ command(ctx, async () => {
77
+ await openAheadMode(pi, args, ctx);
78
+ }),
58
79
  });
59
80
 
60
81
  pi.registerCommand("ahead-guide", {
61
82
  description: "Read the AHEAD framework guidance relevant to the active phase",
62
- handler: async (args, ctx) => command(ctx, async () => {
63
- await showAheadGuide(ctx, args);
64
- }),
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
+ }),
65
103
  });
66
104
 
67
105
  pi.registerCommand("ahead-start", {
68
- description: "Advanced: start a Product Change run directly",
69
- handler: async (args, ctx) => command(ctx, async () => {
70
- await startRun(ctx, args);
71
- }),
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
+ }),
72
111
  });
73
112
 
74
113
  pi.registerCommand("ahead-status", {
75
114
  description: "Advanced: show the raw active AHEAD phase contract",
76
- handler: async (_args, ctx) => command(ctx, async () => {
77
- const run = await requireRun(ctx);
78
- const state = (await enginePromise).deriveState(run);
79
- await refreshUi(ctx, run);
80
- ctx.ui.notify(formatState(state), state.blockers.length ? "warning" : "info");
81
- }),
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
+ }),
82
122
  });
83
123
 
84
124
  pi.registerCommand("ahead-record", {
85
125
  description: "Advanced: record a human-owned artifact directly",
86
- handler: async (args, ctx) => command(ctx, async () => {
87
- await recordHumanArtifact(ctx, args.trim());
88
- }),
126
+ handler: async (args, ctx) =>
127
+ command(ctx, async () => {
128
+ await recordHumanArtifact(ctx, args.trim());
129
+ }),
89
130
  });
90
131
 
91
132
  pi.registerCommand("ahead-accept", {
92
133
  description: "Advanced: accept the active gate without advancing",
93
- handler: async (_args, ctx) => command(ctx, async () => {
94
- if (!ctx.hasUI) throw new Error("/ahead-accept requires interactive or RPC UI support");
95
- const engine = await enginePromise;
96
- const store = storeFor(ctx);
97
- const run = await requireRun(ctx);
98
- const state = engine.deriveState(run);
99
- const confirmed = await ctx.ui.confirm(
100
- `Accept ${state.gate.id}?`,
101
- `${state.gate.title}\n\nThis records human acceptance as ${humanActor(store.projectRoot).identity}.`,
102
- );
103
- if (!confirmed) return;
104
- const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
105
- type: "gate_accepted",
106
- phase: state.phase.id,
107
- gate: state.gate.id,
108
- });
109
- await store.save(updated);
110
- await refreshUi(ctx, updated);
111
- ctx.ui.notify(`Accepted gate ${state.gate.id}. Use /ahead-advance when ready.`, "info");
112
- }),
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
+ }),
113
159
  });
114
160
 
115
161
  pi.registerCommand("ahead-advance", {
116
162
  description: "Advanced: advance an already accepted gate",
117
- handler: async (_args, ctx) => command(ctx, async () => {
118
- if (!ctx.hasUI) throw new Error("/ahead-advance requires interactive or RPC UI support");
119
- const engine = await enginePromise;
120
- const store = storeFor(ctx);
121
- const run = await requireRun(ctx);
122
- const state = engine.deriveState(run);
123
- const destination = state.phase.next ?? "closed";
124
- const confirmed = await ctx.ui.confirm(
125
- state.phase.next ? `Advance to ${state.phase.next}?` : "Close this AHEAD run?",
126
- `Current phase: ${state.phase.title}\nDestination: ${destination}\nActor: ${humanActor(store.projectRoot).identity}`,
127
- );
128
- if (!confirmed) return;
129
- const action: EventAction = state.phase.next
130
- ? {
131
- type: "phase_transitioned",
132
- from: state.phase.id,
133
- to: state.phase.next,
134
- direction: "advance",
135
- }
136
- : { type: "run_closed", phase: state.phase.id };
137
- const updated = engine.applyEvent(run, humanActor(store.projectRoot), action);
138
- await store.save(updated);
139
- await refreshUi(ctx, updated);
140
- ctx.ui.notify(state.phase.next ? `Advanced to ${state.phase.next}.` : "AHEAD run closed.", "info");
141
- }),
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
+ }),
142
196
  });
143
197
 
144
198
  pi.registerCommand("ahead-return", {
145
199
  description: "Advanced: return to an earlier phase with a reason",
146
- handler: async (args, ctx) => command(ctx, async () => {
147
- await returnToEarlierPhase(ctx, args);
148
- }),
200
+ handler: async (args, ctx) =>
201
+ command(ctx, async () => {
202
+ await returnToEarlierPhase(ctx, args);
203
+ }),
149
204
  });
150
205
 
151
206
  pi.registerCommand("ahead-help", {
@@ -153,8 +208,10 @@ export default function aheadExtension(pi: ExtensionAPI): void {
153
208
  handler: async (_args, ctx) => {
154
209
  ctx.ui.notify(
155
210
  [
156
- "/ahead [title] — enter, resume, or act in guided AHEAD mode",
211
+ "/ahead [title] — choose a workflow for new work, or resume guided AHEAD mode",
157
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",
158
215
  "",
159
216
  "Once started, the repository run remains in AHEAD mode until an accountable human closes the outcome.",
160
217
  "Use normal conversation to think and work with AI. Run /ahead whenever you want the next valid action.",
@@ -171,7 +228,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
171
228
  pi.registerTool({
172
229
  name: "ahead_get_context",
173
230
  label: "AHEAD context",
174
- 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.",
175
233
  promptSnippet: "Read the active AHEAD workflow state and human/AI boundaries.",
176
234
  parameters: EmptyParams,
177
235
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
@@ -183,29 +241,74 @@ export default function aheadExtension(pi: ExtensionAPI): void {
183
241
  },
184
242
  });
185
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
+
186
284
  pi.registerTool({
187
285
  name: "ahead_get_reference",
188
286
  label: "AHEAD framework reference",
189
- description: "List or read packaged AHEAD Constitution, philosophy, acceptable-use, engineering-practice, workflow, and evidence Markdown.",
190
- promptSnippet: "Retrieve relevant AHEAD framework guidance when the phase or policy is unclear.",
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.",
191
291
  parameters: ReferenceParams,
192
292
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
193
293
  return toolResult(async () => {
194
294
  const run = await storeFor(ctx).loadCurrent();
195
- const phase = run && !(await enginePromise).deriveState(run).closed
196
- ? (await enginePromise).deriveState(run).phase.id
197
- : undefined;
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;
198
298
  if (!params.topic?.trim()) {
199
299
  const index = await loadReferenceIndex();
200
300
  return {
201
301
  phase: phase ?? null,
202
- recommended: await relevantReferences(phase),
302
+ workflow: workflowId ?? null,
303
+ recommended: await relevantReferences(workflowId, phase),
203
304
  available: index.references.map(({ id, title, path }) => ({ id, title, path })),
204
305
  instruction: "Request one reference by id, path, or title. Load only what is relevant.",
205
306
  };
206
307
  }
207
308
  const entry = await findReference(params.topic);
208
- if (!entry) throw new Error(`No packaged AHEAD reference matches ${params.topic}`);
309
+ if (!entry) {
310
+ throw new Error(`No packaged AHEAD reference matches ${params.topic}`);
311
+ }
209
312
  return { reference: entry, content: await readReference(entry) };
210
313
  });
211
314
  },
@@ -214,7 +317,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
214
317
  pi.registerTool({
215
318
  name: "ahead_record_artifact",
216
319
  label: "Record AHEAD artifact",
217
- 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.",
218
322
  promptSnippet: "Record an AI-permitted artifact in the active AHEAD run.",
219
323
  parameters: RecordArtifactParams,
220
324
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -230,6 +334,13 @@ export default function aheadExtension(pi: ExtensionAPI): void {
230
334
  `AI cannot record ${params.kind} in phase ${state.phase.id}`,
231
335
  );
232
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
+ }
233
344
  const path = store.artifactPath(run, state.phase.id, artifact.kind);
234
345
  const updated = engine.applyEvent(run, aiActor(ctx), {
235
346
  type: "artifact_recorded",
@@ -252,7 +363,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
252
363
  pi.registerTool({
253
364
  name: "ahead_request_transition",
254
365
  label: "Request AHEAD transition",
255
- 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.",
256
368
  parameters: EmptyParams,
257
369
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
258
370
  return toolResult(async () => {
@@ -272,10 +384,14 @@ export default function aheadExtension(pi: ExtensionAPI): void {
272
384
  pi.registerTool({
273
385
  name: "ahead_validate",
274
386
  label: "Validate AHEAD run",
275
- 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.",
276
389
  parameters: EmptyParams,
277
390
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
278
- 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
+ }));
279
395
  },
280
396
  });
281
397
 
@@ -298,17 +414,22 @@ export default function aheadExtension(pi: ExtensionAPI): void {
298
414
 
299
415
  pi.on("before_agent_start", async (event, ctx) => {
300
416
  const run = await storeFor(ctx).loadCurrent();
301
- if (!run) return;
417
+ if (!run) {
418
+ return undefined;
419
+ }
302
420
  const engine = await enginePromise;
303
421
  const state = engine.deriveState(run);
304
- if (state.closed) return;
422
+ if (state.closed) {
423
+ return undefined;
424
+ }
305
425
  const workflow = engine.getWorkflow(run.workflow_id);
306
- const guidance = phaseGuide(state.phase.id);
426
+ const guidance = phaseGuide(run.workflow_id, state.phase.id);
307
427
  const action = nextAction(state, workflow);
308
- const phaseInstructions = await loadInstructions(state.phase.id);
428
+ const phaseInstructions = await loadInstructions(run.workflow_id, state.phase.id);
309
429
  const liveContext = [
310
430
  "# Live AHEAD run",
311
431
  `- Run: ${run.id} — ${run.title}`,
432
+ `- Workflow: ${workflow.title} (${workflow.id})`,
312
433
  `- Phase: ${state.phase.id} visit ${state.phase.visit}`,
313
434
  `- Gate accepted: ${state.gate.accepted}`,
314
435
  `- Current blockers: ${state.blockers.length ? state.blockers.join("; ") : "none"}`,
@@ -325,15 +446,22 @@ export default function aheadExtension(pi: ExtensionAPI): void {
325
446
  "- Treat AI review findings as hypotheses. Independent human review remains required for lasting engineering changes.",
326
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.",
327
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.",
328
450
  ].join("\n");
329
451
  return { systemPrompt: `${event.systemPrompt}\n\n${phaseInstructions}\n\n${liveContext}\n` };
330
452
  });
331
453
 
332
454
  pi.on("tool_call", async (event, ctx) => {
333
- if (event.toolName.startsWith("ahead_")) return;
455
+ if (event.toolName.startsWith("ahead_")) {
456
+ return undefined;
457
+ }
334
458
  const run = await storeFor(ctx).loadCurrent();
335
- if (!run) return;
336
- 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
+ }
337
465
  const capability = toolCapabilities[event.toolName];
338
466
  if (!capability) {
339
467
  return {
@@ -343,10 +471,13 @@ export default function aheadExtension(pi: ExtensionAPI): void {
343
471
  }
344
472
  try {
345
473
  const decision = (await enginePromise).toolAllowed(run, capability);
346
- if (!decision.allowed) return { block: true, reason: `AHEAD: ${decision.reason}` };
474
+ if (!decision.allowed) {
475
+ return { block: true, reason: `AHEAD: ${decision.reason}` };
476
+ }
347
477
  } catch (error) {
348
478
  return { block: true, reason: `AHEAD state validation failed: ${errorMessage(error)}` };
349
479
  }
480
+ return undefined;
350
481
  });
351
482
  }
352
483
 
@@ -355,31 +486,42 @@ interface GuidedAction {
355
486
  run: () => Promise<void>;
356
487
  }
357
488
 
358
- 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> {
359
494
  const engine = await enginePromise;
360
495
  const store = storeFor(ctx);
361
496
  let run = await store.loadCurrent();
362
497
 
363
498
  if (run && engine.deriveState(run).closed) {
364
499
  if (!ctx.hasUI) {
365
- 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
+ );
366
504
  return;
367
505
  }
368
506
  const choice = await ctx.ui.select("AHEAD work is complete", [
369
- "Start a new Product Change",
507
+ "Start new AHEAD work",
370
508
  "View the completed run",
371
509
  ]);
372
510
  if (choice === "View the completed run") {
373
511
  ctx.ui.notify(formatState(engine.deriveState(run)), "info");
374
512
  return;
375
513
  }
376
- if (choice !== "Start a new Product Change") return;
514
+ if (choice !== "Start new AHEAD work") {
515
+ return;
516
+ }
377
517
  run = undefined;
378
518
  }
379
519
 
380
520
  if (!run) {
381
521
  run = await startRun(ctx, args);
382
- if (!run) return;
522
+ if (!run) {
523
+ return;
524
+ }
383
525
  }
384
526
 
385
527
  await refreshUi(ctx, run);
@@ -390,43 +532,60 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
390
532
 
391
533
  const state = engine.deriveState(run);
392
534
  const workflow = engine.getWorkflow(run.workflow_id);
393
- const guidance = phaseGuide(state.phase.id);
535
+ const guidance = phaseGuide(run.workflow_id, state.phase.id);
394
536
  const action = nextAction(state, workflow);
395
537
  const actions: GuidedAction[] = [];
396
- const missingRequired = state.artifacts.filter((artifact) => artifact.required && !artifact.present);
538
+ const missingRequired = state.artifacts.filter(
539
+ (artifact) => artifact.required && !artifact.present,
540
+ );
397
541
  if (action.artifactKind) {
542
+ const actionArtifact = state.artifacts.find(
543
+ (artifact) => artifact.kind === action.artifactKind,
544
+ );
398
545
  actions.push({
399
546
  label: action.label,
400
- run: action.actor === "ai"
401
- ? async () => requestAiAssistance(pi, state, action.artifactKind)
402
- : 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 ?? ""),
403
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
+ }
404
560
  }
405
561
 
406
562
  if (action.optional) {
407
- const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor === "human");
563
+ const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor !== "ai");
408
564
  if (nextHumanArtifact) {
409
565
  actions.push({
410
566
  label: `Continue without optional AI challenge · Write ${nextHumanArtifact.title}`,
411
567
  run: async () => recordHumanArtifact(ctx, nextHumanArtifact.kind),
412
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
+ });
413
574
  }
414
575
  }
415
576
 
416
577
  if (missingRequired.length === 0 && !action.artifactKind) {
417
578
  actions.push({
418
- label: state.gate.accepted
419
- ? action.label
420
- : `Accept and continue · ${state.gate.title}`,
579
+ label: state.gate.accepted ? action.label : `Accept and continue · ${state.gate.title}`,
421
580
  run: async () => acceptAndContinue(ctx),
422
581
  });
423
582
  }
424
583
 
425
584
  if (
426
- state.allowed_ai_capabilities.length > 0
427
- && action.actor !== "ai"
428
- && !missingRequired.some((artifact) => artifact.actor === "ai")
429
- && state.phase.id !== "implement"
585
+ state.allowed_ai_capabilities.length > 0 &&
586
+ action.actor !== "ai" &&
587
+ !missingRequired.some((artifact) => artifact.actor === "ai") &&
588
+ state.phase.id !== "implement"
430
589
  ) {
431
590
  actions.push({
432
591
  label: `Ask AI to assist · ${state.phase.title}`,
@@ -441,6 +600,13 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
441
600
  });
442
601
  }
443
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
+
444
610
  if (state.return_targets.length > 0) {
445
611
  actions.push({
446
612
  label: "Return to an earlier phase",
@@ -453,6 +619,11 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
453
619
  run: async () => showAheadGuide(ctx, ""),
454
620
  });
455
621
 
622
+ actions.push({
623
+ label: "Inspect optional skills reviewed for this phase",
624
+ run: async () => showRecommendedSkills(ctx),
625
+ });
626
+
456
627
  actions.push({
457
628
  label: "Explain this phase and its expectations",
458
629
  run: async () => {
@@ -474,7 +645,78 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
474
645
  actions.map((candidate) => candidate.label),
475
646
  );
476
647
  const chosen = actions.find((candidate) => candidate.label === selected);
477
- 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
+ );
668
+ }
669
+
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
+ }
478
720
  }
479
721
 
480
722
  async function askImplementationQuestion(
@@ -482,7 +724,9 @@ async function askImplementationQuestion(
482
724
  ctx: ExtensionCommandContext,
483
725
  state: RunState,
484
726
  ): Promise<void> {
485
- if (!ctx.hasUI) throw new Error("Implementation coaching requires interactive or RPC UI support");
727
+ if (!ctx.hasUI) {
728
+ throw new Error("Implementation coaching requires interactive or RPC UI support");
729
+ }
486
730
  const question = await ctx.ui.editor(
487
731
  "AHEAD implementation help · human first",
488
732
  [
@@ -501,50 +745,67 @@ async function askImplementationQuestion(
501
745
  "",
502
746
  ].join("\n"),
503
747
  );
504
- if (!question?.trim()) return;
505
- pi.sendUserMessage([
506
- `AHEAD mode: help me with this ${state.phase.title} question while I remain the implementer.`,
507
- "Use my current model and first attempt below. Help me understand or solve the problem with questions, explanation, evidence, hints, and bounded next steps.",
508
- "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.",
509
- "",
510
- question.trim(),
511
- ].join("\n"));
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
+ );
512
760
  }
513
761
 
514
762
  async function showAheadGuide(ctx: ExtensionCommandContext, requestedTopic: string): Promise<void> {
515
- if (!ctx.hasUI) throw new Error("Reading AHEAD framework guidance requires interactive or RPC UI support");
763
+ if (!ctx.hasUI) {
764
+ throw new Error("Reading AHEAD framework guidance requires interactive or RPC UI support");
765
+ }
516
766
  const run = await storeFor(ctx).loadCurrent();
517
- const phase = run && !(await enginePromise).deriveState(run).closed
518
- ? (await enginePromise).deriveState(run).phase.id
519
- : undefined;
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;
520
770
  const index = await loadReferenceIndex();
521
- let entry = requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all"
522
- ? await findReference(requestedTopic)
523
- : undefined;
771
+ let entry =
772
+ requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all"
773
+ ? await findReference(requestedTopic)
774
+ : undefined;
524
775
 
525
776
  if (requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all" && !entry) {
526
777
  throw new Error(`No packaged AHEAD reference matches ${requestedTopic.trim()}`);
527
778
  }
528
779
 
529
780
  if (!entry) {
530
- const recommended = requestedTopic.trim().toLowerCase() === "all"
531
- ? index.references
532
- : await relevantReferences(phase);
781
+ const recommended =
782
+ requestedTopic.trim().toLowerCase() === "all"
783
+ ? index.references
784
+ : await relevantReferences(workflowId, phase);
533
785
  const browseAll = "Browse all packaged AHEAD Markdown";
534
786
  const selected = await ctx.ui.select(
535
787
  phase ? `AHEAD guidance · ${phase}` : "AHEAD framework guidance",
536
- [...recommended.map((candidate) => candidate.title), ...(recommended.length < index.references.length ? [browseAll] : [])],
788
+ [
789
+ ...recommended.map((candidate) => candidate.title),
790
+ ...(recommended.length < index.references.length ? [browseAll] : []),
791
+ ],
537
792
  );
538
- if (!selected) return;
539
- if (selected === browseAll) return showAheadGuide(ctx, "all");
793
+ if (!selected) {
794
+ return;
795
+ }
796
+ if (selected === browseAll) {
797
+ return showAheadGuide(ctx, "all");
798
+ }
540
799
  entry = recommended.find((candidate) => candidate.title === selected);
541
800
  }
542
- if (!entry) return;
801
+ if (!entry) {
802
+ return;
803
+ }
543
804
 
544
805
  await showReferenceViewer(ctx, `AHEAD reference · ${entry.title}`, await readReference(entry));
545
806
  }
546
807
 
547
- async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): Promise<Run | undefined> {
808
+ async function startRun(ctx: ExtensionCommandContext, request: string): Promise<Run | undefined> {
548
809
  const engine = await enginePromise;
549
810
  const store = storeFor(ctx);
550
811
  const current = await store.loadCurrent();
@@ -555,9 +816,40 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
555
816
  );
556
817
  }
557
818
 
558
- const title = requestedTitle.trim()
559
- || (ctx.hasUI ? await ctx.ui.input("Enter AHEAD mode · Product Change", "What work are you doing?") : undefined);
560
- 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
+ }
561
853
 
562
854
  const owner = humanActor(store.projectRoot);
563
855
  const run = engine.createRun({
@@ -565,13 +857,13 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
565
857
  title: title.trim(),
566
858
  owner,
567
859
  timestamp: new Date().toISOString(),
568
- workflow_id: "product-change",
860
+ workflow_id: workflow.id,
569
861
  });
570
862
  await store.save(run);
571
863
  await refreshUi(ctx, run);
572
864
  ctx.ui.notify(
573
865
  [
574
- `AHEAD mode started · ${run.title}`,
866
+ `AHEAD mode started · ${workflow.title} · ${run.title}`,
575
867
  "Human leads · AI assists",
576
868
  "This run remains active in the repository until an accountable human closes the outcome.",
577
869
  "Use /ahead for the next guided action; use normal conversation to think and work with AI.",
@@ -581,19 +873,27 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
581
873
  return run;
582
874
  }
583
875
 
584
- async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind: string): Promise<void> {
585
- 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
+ }
586
883
  const engine = await enginePromise;
587
884
  const store = storeFor(ctx);
588
885
  const run = await requireRun(ctx);
589
886
  const state = engine.deriveState(run);
590
- 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
+ );
591
890
  let kind = requestedKind.trim();
592
891
  if (!kind) {
593
- kind = (await ctx.ui.select(
594
- `AHEAD mode · Write for ${state.phase.title}`,
595
- allowed.map((artifact) => artifact.title),
596
- )) ?? "";
892
+ kind =
893
+ (await ctx.ui.select(
894
+ `AHEAD mode · Write for ${state.phase.title}`,
895
+ allowed.map((artifact) => artifact.title),
896
+ )) ?? "";
597
897
  kind = allowed.find((artifact) => artifact.title === kind)?.kind ?? kind;
598
898
  }
599
899
  const artifact = allowed.find((candidate) => candidate.kind === kind);
@@ -604,11 +904,15 @@ async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind:
604
904
  );
605
905
  }
606
906
 
907
+ const template = await humanArtifactTemplate(store, state, run, artifact.kind, artifact.title);
607
908
  const content = await ctx.ui.editor(
608
909
  `AHEAD mode · ${artifact.title} · write in your own words`,
609
- buildArtifactTemplate(run, state, artifact.kind, artifact.title),
910
+ template,
610
911
  );
611
- if (!content?.trim()) return;
912
+ if (!content?.trim()) {
913
+ return;
914
+ }
915
+ await validateHumanReviewArtifact(store, state, artifact.kind, content);
612
916
  const path = store.artifactPath(run, state.phase.id, artifact.kind);
613
917
  const action: EventAction = {
614
918
  type: "artifact_recorded",
@@ -626,14 +930,81 @@ async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind:
626
930
  );
627
931
  }
628
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
+
629
1000
  function requestAiAssistance(pi: ExtensionAPI, state: RunState, requiredKind?: string): void {
630
- const guidance = phaseGuide(state.phase.id);
1001
+ const guidance = phaseGuide(state.workflow_id, state.phase.id);
631
1002
  const artifact = requiredKind
632
1003
  ? state.artifacts.find((candidate) => candidate.kind === requiredKind)
633
1004
  : undefined;
634
1005
  const request = artifact
635
1006
  ? [
636
- `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.`,
637
1008
  `Produce ${artifact.title}.`,
638
1009
  `Follow the active human/AI boundary: ${guidance.ai}`,
639
1010
  `Use ahead_get_context first, then record the completed artifact as ${artifact.kind} with ahead_record_artifact.`,
@@ -649,7 +1020,9 @@ function requestAiAssistance(pi: ExtensionAPI, state: RunState, requiredKind?: s
649
1020
  }
650
1021
 
651
1022
  async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
652
- 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
+ }
653
1026
  const engine = await enginePromise;
654
1027
  const store = storeFor(ctx);
655
1028
  const run = await requireRun(ctx);
@@ -663,7 +1036,8 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
663
1036
  }
664
1037
 
665
1038
  const destination = state.phase.next
666
- ? 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)
667
1041
  : "close this AHEAD run";
668
1042
  const confirmed = await ctx.ui.confirm(
669
1043
  `Accept and continue from ${state.phase.title}?`,
@@ -676,7 +1050,9 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
676
1050
  "This records human acceptance. AI cannot perform this action.",
677
1051
  ].join("\n"),
678
1052
  );
679
- if (!confirmed) return;
1053
+ if (!confirmed) {
1054
+ return;
1055
+ }
680
1056
 
681
1057
  const actor = humanActor(store.projectRoot);
682
1058
  let updated = run;
@@ -689,7 +1065,10 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
689
1065
  state = engine.deriveState(updated);
690
1066
  }
691
1067
  if (!state.can_advance) {
692
- 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
+ );
693
1072
  }
694
1073
 
695
1074
  const action: EventAction = state.phase.next
@@ -706,7 +1085,10 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
706
1085
 
707
1086
  const nextState = engine.deriveState(updated);
708
1087
  if (nextState.closed) {
709
- 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
+ );
710
1092
  } else if (nextState.phase.id === "human-review") {
711
1093
  ctx.ui.notify(
712
1094
  [
@@ -725,14 +1107,22 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
725
1107
  }
726
1108
  }
727
1109
 
728
- async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarget: string): Promise<void> {
729
- 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
+ }
730
1117
  const engine = await enginePromise;
731
1118
  const store = storeFor(ctx);
732
1119
  const run = await requireRun(ctx);
733
1120
  const state = engine.deriveState(run);
734
1121
  if (!state.return_targets.length) {
735
- 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
+ );
736
1126
  }
737
1127
  const workflow = engine.getWorkflow(run.workflow_id);
738
1128
  const targetOptions = state.return_targets.map((target) => ({
@@ -756,12 +1146,16 @@ async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarge
756
1146
  const reason = await ctx.ui.editor(
757
1147
  `Why return to ${workflow.phases.find((phase) => phase.id === target)?.title ?? target}?`,
758
1148
  );
759
- if (!reason?.trim()) return;
1149
+ if (!reason?.trim()) {
1150
+ return;
1151
+ }
760
1152
  const confirmed = await ctx.ui.confirm(
761
1153
  `Return to ${target}?`,
762
1154
  "This opens a new phase visit. Earlier artifacts remain as history but cannot satisfy the reopened gate.",
763
1155
  );
764
- if (!confirmed) return;
1156
+ if (!confirmed) {
1157
+ return;
1158
+ }
765
1159
  const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
766
1160
  type: "phase_transitioned",
767
1161
  from: state.phase.id,
@@ -771,7 +1165,10 @@ async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarge
771
1165
  });
772
1166
  await store.save(updated);
773
1167
  await refreshUi(ctx, updated);
774
- 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
+ );
775
1172
  }
776
1173
 
777
1174
  function storeFor(ctx: ExtensionContext): RunStore {
@@ -780,7 +1177,9 @@ function storeFor(ctx: ExtensionContext): RunStore {
780
1177
 
781
1178
  async function requireRun(ctx: ExtensionContext): Promise<Run> {
782
1179
  const run = await storeFor(ctx).loadCurrent();
783
- 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
+ }
784
1183
  return run;
785
1184
  }
786
1185
 
@@ -810,24 +1209,44 @@ async function refreshUi(ctx: ExtensionContext, supplied?: Run): Promise<void> {
810
1209
  ? `AHEAD · complete · ${state.workflow_id}`
811
1210
  : `AHEAD · ${position.current}/${position.total} · ${state.phase.id} · ${action.actor} action`,
812
1211
  );
813
- ctx.ui.setWidget(
814
- "ahead",
815
- buildWidgetLines(run, state, workflow),
816
- { placement: "aboveEditor" },
817
- );
1212
+ ctx.ui.setWidget("ahead", buildWidgetLines(run, state, workflow), { placement: "aboveEditor" });
818
1213
  }
819
1214
 
820
- async function loadInstructions(phase: string): Promise<string> {
821
- const cached = instructions.get(phase);
822
- if (cached) return cached;
823
- const content = await readFile(`${instructionDirectory}/${phase}.md`, "utf8");
824
- 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);
825
1223
  return content;
826
1224
  }
827
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
+
828
1244
  function formatState(state: RunState): string {
829
1245
  const artifacts = state.artifacts
830
- .map((artifact) => `${artifact.present ? "✓" : artifact.required ? "○" : "·"} ${artifact.kind} (${artifact.actor})`)
1246
+ .map(
1247
+ (artifact) =>
1248
+ `${artifact.present ? "✓" : artifact.required ? "○" : "·"} ${artifact.kind} (${artifact.actor})`,
1249
+ )
831
1250
  .join("\n");
832
1251
  return [
833
1252
  `${state.title} · ${state.workflow_id}@${state.workflow_version}`,
@@ -852,14 +1271,23 @@ async function command(ctx: ExtensionCommandContext, action: () => Promise<void>
852
1271
  async function toolResult(action: () => Promise<unknown>) {
853
1272
  try {
854
1273
  const result = await action();
855
- 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
+ };
856
1278
  } catch (error) {
857
1279
  const message = errorMessage(error);
858
- 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
+ };
859
1285
  }
860
1286
  }
861
1287
 
862
1288
  function errorMessage(error: unknown): string {
863
- if (error instanceof AheadEngineError) return `${error.code}: ${error.message}`;
1289
+ if (error instanceof AheadEngineError) {
1290
+ return `${error.code}: ${error.message}`;
1291
+ }
864
1292
  return error instanceof Error ? error.message : String(error);
865
1293
  }