ahead-pi 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +42 -22
  2. package/generated/product-change/ai-audit.md +6 -3
  3. package/generated/product-change/ai-review.md +6 -3
  4. package/generated/product-change/decision.md +6 -3
  5. package/generated/product-change/define.md +6 -3
  6. package/generated/product-change/deploy.md +6 -3
  7. package/generated/product-change/human-review.md +6 -3
  8. package/generated/product-change/implement.md +11 -4
  9. package/generated/product-change/manifest.json +2 -2
  10. package/generated/product-change/options.md +6 -3
  11. package/generated/product-change/outcome.md +6 -3
  12. package/generated/product-change/plan.md +6 -3
  13. package/generated/product-change/questions.md +6 -3
  14. package/generated/product-change/research.md +6 -3
  15. package/generated/product-change/verify.md +6 -3
  16. package/generated/reference/CONSTITUTION.md +43 -0
  17. package/generated/reference/docs/acceptable-ai-use.md +217 -0
  18. package/generated/reference/docs/design/debugging-and-operations.md +119 -0
  19. package/generated/reference/docs/design/executable-workflows.md +110 -0
  20. package/generated/reference/docs/design/process-taxonomy.md +144 -0
  21. package/generated/reference/docs/engineering-practice.md +163 -0
  22. package/generated/reference/docs/evidence/evidence-standard.md +123 -0
  23. package/generated/reference/docs/evidence/research-map.md +98 -0
  24. package/generated/reference/docs/rationale.md +210 -0
  25. package/generated/reference/docs/references/pragmatic-programmer-page-index.md +113 -0
  26. package/generated/reference/docs/references/submitted-engineering-notes.md +306 -0
  27. package/generated/reference/docs/releasing-pi.md +89 -0
  28. package/generated/reference/docs/workflows/README.md +142 -0
  29. package/generated/reference/docs/workflows/corrective-debugging.md +203 -0
  30. package/generated/reference/docs/workflows/decision.md +149 -0
  31. package/generated/reference/docs/workflows/internal-improvement.md +207 -0
  32. package/generated/reference/docs/workflows/investigation.md +159 -0
  33. package/generated/reference/docs/workflows/operational-stabilization.md +185 -0
  34. package/generated/reference/docs/workflows/product-change.md +205 -0
  35. package/generated/reference/index.json +180 -0
  36. package/package.json +4 -2
  37. package/src/guidance.ts +360 -0
  38. package/src/index.ts +538 -136
  39. package/src/reference-viewer.ts +71 -0
  40. package/src/reference.ts +56 -0
package/src/index.ts CHANGED
@@ -7,6 +7,20 @@ import type {
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import { Type } from "typebox";
9
9
  import { AheadEngine, AheadEngineError } from "./engine.js";
10
+ import {
11
+ buildArtifactTemplate,
12
+ buildWidgetLines,
13
+ nextAction,
14
+ phaseGuide,
15
+ phasePosition,
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";
10
24
  import { humanActor, projectRoot, RunStore } from "./storage.js";
11
25
  import type { Actor, Capability, EventAction, Run, RunState } from "./types.js";
12
26
 
@@ -31,38 +45,34 @@ const RecordArtifactParams = Type.Object({
31
45
  kind: Type.String({ description: "Artifact kind permitted for AI in the active phase" }),
32
46
  content: Type.String({ description: "Complete Markdown artifact content", maxLength: 100_000 }),
33
47
  });
48
+ const ReferenceParams = Type.Object({
49
+ topic: Type.Optional(Type.String({ description: "Reference id, path, or title; omit to list phase-relevant references" })),
50
+ });
34
51
 
35
52
  export default function aheadExtension(pi: ExtensionAPI): void {
53
+ pi.registerCommand("ahead", {
54
+ description: "Enter or continue the guided AHEAD mode",
55
+ handler: async (args, ctx) => command(ctx, async () => {
56
+ await openAheadMode(pi, args, ctx);
57
+ }),
58
+ });
59
+
60
+ pi.registerCommand("ahead-guide", {
61
+ 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
+ }),
65
+ });
66
+
36
67
  pi.registerCommand("ahead-start", {
37
- description: "Start a Product Change workflow owned by the current human",
68
+ description: "Advanced: start a Product Change run directly",
38
69
  handler: async (args, ctx) => command(ctx, async () => {
39
- const engine = await enginePromise;
40
- const store = storeFor(ctx);
41
- const current = await store.loadCurrent();
42
- if (current && !engine.deriveState(current).closed) {
43
- throw new AheadEngineError(
44
- "active_run_exists",
45
- `run ${current.id} is still active; close it before starting another`,
46
- );
47
- }
48
- const title = args.trim() || (ctx.hasUI ? await ctx.ui.input("AHEAD Product Change", "Run title") : undefined);
49
- if (!title?.trim()) return;
50
- const owner = humanActor(store.projectRoot);
51
- const run = engine.createRun({
52
- id: store.newRunId(),
53
- title: title.trim(),
54
- owner,
55
- timestamp: new Date().toISOString(),
56
- workflow_id: "product-change",
57
- });
58
- await store.save(run);
59
- await refreshUi(ctx, run);
60
- ctx.ui.notify(`Started AHEAD run ${run.id}. Record the human-owned problem with /ahead-record problem.`, "info");
70
+ await startRun(ctx, args);
61
71
  }),
62
72
  });
63
73
 
64
74
  pi.registerCommand("ahead-status", {
65
- description: "Show the active AHEAD phase, evidence, gate, and blockers",
75
+ description: "Advanced: show the raw active AHEAD phase contract",
66
76
  handler: async (_args, ctx) => command(ctx, async () => {
67
77
  const run = await requireRun(ctx);
68
78
  const state = (await enginePromise).deriveState(run);
@@ -72,50 +82,14 @@ export default function aheadExtension(pi: ExtensionAPI): void {
72
82
  });
73
83
 
74
84
  pi.registerCommand("ahead-record", {
75
- description: "Write and record a human-owned artifact for the active phase",
85
+ description: "Advanced: record a human-owned artifact directly",
76
86
  handler: async (args, ctx) => command(ctx, async () => {
77
- if (!ctx.hasUI) throw new Error("/ahead-record 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 allowed = state.artifacts.filter((artifact) => artifact.actor !== "ai");
83
- let kind = args.trim();
84
- if (!kind) {
85
- kind = (await ctx.ui.select(
86
- `Record human artifact · ${state.phase.title}`,
87
- allowed.map((artifact) => artifact.kind),
88
- )) ?? "";
89
- }
90
- const artifact = allowed.find((candidate) => candidate.kind === kind);
91
- if (!artifact) {
92
- throw new AheadEngineError(
93
- "artifact_not_human_owned",
94
- `human cannot record ${kind || "that artifact"} in phase ${state.phase.id}; choose: ${allowed.map((item) => item.kind).join(", ")}`,
95
- );
96
- }
97
- const content = await ctx.ui.editor(
98
- `AHEAD · ${artifact.title}`,
99
- artifactTemplate(run, state, artifact.kind, artifact.title),
100
- );
101
- if (!content?.trim()) return;
102
- const path = store.artifactPath(run, state.phase.id, artifact.kind);
103
- const action: EventAction = {
104
- type: "artifact_recorded",
105
- phase: state.phase.id,
106
- kind: artifact.kind,
107
- path: path.relative,
108
- };
109
- const updated = engine.applyEvent(run, humanActor(store.projectRoot), action);
110
- await store.writeArtifact(path.absolute, content);
111
- await store.save(updated);
112
- await refreshUi(ctx, updated);
113
- ctx.ui.notify(`Recorded ${artifact.kind} as ${path.relative}`, "info");
87
+ await recordHumanArtifact(ctx, args.trim());
114
88
  }),
115
89
  });
116
90
 
117
91
  pi.registerCommand("ahead-accept", {
118
- description: "Human acceptance of the active phase gate",
92
+ description: "Advanced: accept the active gate without advancing",
119
93
  handler: async (_args, ctx) => command(ctx, async () => {
120
94
  if (!ctx.hasUI) throw new Error("/ahead-accept requires interactive or RPC UI support");
121
95
  const engine = await enginePromise;
@@ -139,7 +113,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
139
113
  });
140
114
 
141
115
  pi.registerCommand("ahead-advance", {
142
- description: "Human transition to the next phase, or close the final phase",
116
+ description: "Advanced: advance an already accepted gate",
143
117
  handler: async (_args, ctx) => command(ctx, async () => {
144
118
  if (!ctx.hasUI) throw new Error("/ahead-advance requires interactive or RPC UI support");
145
119
  const engine = await enginePromise;
@@ -168,57 +142,26 @@ export default function aheadExtension(pi: ExtensionAPI): void {
168
142
  });
169
143
 
170
144
  pi.registerCommand("ahead-return", {
171
- description: "Human return to an allowed earlier phase with a recorded reason",
145
+ description: "Advanced: return to an earlier phase with a reason",
172
146
  handler: async (args, ctx) => command(ctx, async () => {
173
- if (!ctx.hasUI) throw new Error("/ahead-return requires interactive or RPC UI support");
174
- const engine = await enginePromise;
175
- const store = storeFor(ctx);
176
- const run = await requireRun(ctx);
177
- const state = engine.deriveState(run);
178
- if (!state.return_targets.length) {
179
- throw new AheadEngineError("no_return_target", `phase ${state.phase.id} has no return transition`);
180
- }
181
- let target = args.trim();
182
- if (!target) target = (await ctx.ui.select("Return to which phase?", state.return_targets)) ?? "";
183
- if (!state.return_targets.includes(target)) {
184
- throw new AheadEngineError(
185
- "invalid_return",
186
- `phase ${state.phase.id} can return only to: ${state.return_targets.join(", ")}`,
187
- );
188
- }
189
- const reason = await ctx.ui.editor(`Why return to ${target}?`);
190
- if (!reason?.trim()) return;
191
- const confirmed = await ctx.ui.confirm(
192
- `Return to ${target}?`,
193
- "This creates a new phase visit. Earlier artifacts remain in history but will not satisfy the reopened gate.",
194
- );
195
- if (!confirmed) return;
196
- const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
197
- type: "phase_transitioned",
198
- from: state.phase.id,
199
- to: target,
200
- direction: "return",
201
- reason: reason.trim(),
202
- });
203
- await store.save(updated);
204
- await refreshUi(ctx, updated);
205
- ctx.ui.notify(`Returned to ${target}. New evidence and human gate acceptance are required.`, "warning");
147
+ await returnToEarlierPhase(ctx, args);
206
148
  }),
207
149
  });
208
150
 
209
151
  pi.registerCommand("ahead-help", {
210
- description: "Show AHEAD Pi commands and the human/AI boundary",
152
+ description: "Explain guided AHEAD mode and its authority boundary",
211
153
  handler: async (_args, ctx) => {
212
154
  ctx.ui.notify(
213
155
  [
214
- "/ahead-start [title] — start a human-owned Product Change run",
215
- "/ahead-status — show phase, evidence, gate, and blockers",
216
- "/ahead-record [kind] — human writes an artifact",
217
- "/ahead-accept — human accepts the current gate",
218
- "/ahead-advance — human advances or closes",
219
- "/ahead-return [phase] — human reopens an allowed earlier phase",
156
+ "/ahead [title] — enter, resume, or act in guided AHEAD mode",
157
+ "/ahead-guide [topic] — read the applicable AHEAD framework Markdown",
158
+ "",
159
+ "Once started, the repository run remains in AHEAD mode until an accountable human closes the outcome.",
160
+ "Use normal conversation to think and work with AI. Run /ahead whenever you want the next valid action.",
161
+ "The persistent guide explains what you own, what AI may do, required evidence, and what happens next.",
220
162
  "",
221
- "AI can inspect context and record only AI-permitted artifacts. It cannot accept gates or transition the run.",
163
+ "Advanced fallback commands: /ahead-status, /ahead-record, /ahead-accept, /ahead-advance, /ahead-return.",
164
+ "AI can record only AI/shared artifacts allowed in the active phase. It cannot accept gates, transition, approve, deploy, or close the run.",
222
165
  ].join("\n"),
223
166
  "info",
224
167
  );
@@ -240,6 +183,34 @@ export default function aheadExtension(pi: ExtensionAPI): void {
240
183
  },
241
184
  });
242
185
 
186
+ pi.registerTool({
187
+ name: "ahead_get_reference",
188
+ 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.",
191
+ parameters: ReferenceParams,
192
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
193
+ return toolResult(async () => {
194
+ const run = await storeFor(ctx).loadCurrent();
195
+ const phase = run && !(await enginePromise).deriveState(run).closed
196
+ ? (await enginePromise).deriveState(run).phase.id
197
+ : undefined;
198
+ if (!params.topic?.trim()) {
199
+ const index = await loadReferenceIndex();
200
+ return {
201
+ phase: phase ?? null,
202
+ recommended: await relevantReferences(phase),
203
+ available: index.references.map(({ id, title, path }) => ({ id, title, path })),
204
+ instruction: "Request one reference by id, path, or title. Load only what is relevant.",
205
+ };
206
+ }
207
+ const entry = await findReference(params.topic);
208
+ if (!entry) throw new Error(`No packaged AHEAD reference matches ${params.topic}`);
209
+ return { reference: entry, content: await readReference(entry) };
210
+ });
211
+ },
212
+ });
213
+
243
214
  pi.registerTool({
244
215
  name: "ahead_record_artifact",
245
216
  label: "Record AHEAD artifact",
@@ -290,7 +261,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
290
261
  requested: true,
291
262
  transitioned: false,
292
263
  message: state.can_advance
293
- ? `The gate is accepted. Ask the human to use /ahead-advance to ${state.phase.next ?? "close the run"}.`
264
+ ? `The gate is accepted. Ask the human to use /ahead to ${state.phase.next ?? "close the run"}.`
294
265
  : "The phase cannot advance. The human must resolve the blockers and accept the gate.",
295
266
  blockers: state.blockers,
296
267
  };
@@ -311,6 +282,14 @@ export default function aheadExtension(pi: ExtensionAPI): void {
311
282
  pi.on("session_start", async (_event, ctx) => {
312
283
  try {
313
284
  await refreshUi(ctx);
285
+ const run = await storeFor(ctx).loadCurrent();
286
+ if (run && !(await enginePromise).deriveState(run).closed && ctx.hasUI) {
287
+ const state = (await enginePromise).deriveState(run);
288
+ ctx.ui.notify(
289
+ `AHEAD mode resumed · ${state.phase.title}. Human leads, AI assists. Run /ahead for the next guided action.`,
290
+ "info",
291
+ );
292
+ }
314
293
  } catch (error) {
315
294
  ctx.ui.setStatus("ahead", "AHEAD · invalid state");
316
295
  ctx.ui.notify(errorMessage(error), "error");
@@ -320,7 +299,12 @@ export default function aheadExtension(pi: ExtensionAPI): void {
320
299
  pi.on("before_agent_start", async (event, ctx) => {
321
300
  const run = await storeFor(ctx).loadCurrent();
322
301
  if (!run) return;
323
- const state = (await enginePromise).deriveState(run);
302
+ const engine = await enginePromise;
303
+ const state = engine.deriveState(run);
304
+ if (state.closed) return;
305
+ const workflow = engine.getWorkflow(run.workflow_id);
306
+ const guidance = phaseGuide(state.phase.id);
307
+ const action = nextAction(state, workflow);
324
308
  const phaseInstructions = await loadInstructions(state.phase.id);
325
309
  const liveContext = [
326
310
  "# Live AHEAD run",
@@ -329,6 +313,18 @@ export default function aheadExtension(pi: ExtensionAPI): void {
329
313
  `- Gate accepted: ${state.gate.accepted}`,
330
314
  `- Current blockers: ${state.blockers.length ? state.blockers.join("; ") : "none"}`,
331
315
  `- Allowed AI capabilities: ${state.allowed_ai_capabilities.length ? state.allowed_ai_capabilities.join(", ") : "none"}`,
316
+ `- Human responsibility: ${guidance.human}`,
317
+ `- AI role: ${guidance.ai}`,
318
+ `- Next guided action (${action.actor}): ${action.label}`,
319
+ "",
320
+ "## AHEAD interaction behavior",
321
+ "- AHEAD is an active working mode, not a command checklist. Help the human understand and complete the active phase through normal conversation.",
322
+ "- When asked what to do, explain the current expectation in plain language; do not merely repeat artifact identifiers or slash commands.",
323
+ "- Never author a human-owned artifact, make a human decision, accept a gate, transition the run, approve a change, or claim accountability.",
324
+ "- When required AI-owned work is ready, record it with ahead_record_artifact and explain what the human must validate or decide.",
325
+ "- Treat AI review findings as hypotheses. Independent human review remains required for lasting engineering changes.",
326
+ "- 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
+ "- When AHEAD policy or rationale is unclear, use ahead_get_reference to retrieve only the applicable packaged Markdown.",
332
328
  ].join("\n");
333
329
  return { systemPrompt: `${event.systemPrompt}\n\n${phaseInstructions}\n\n${liveContext}\n` };
334
330
  });
@@ -337,6 +333,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
337
333
  if (event.toolName.startsWith("ahead_")) return;
338
334
  const run = await storeFor(ctx).loadCurrent();
339
335
  if (!run) return;
336
+ if ((await enginePromise).deriveState(run).closed) return;
340
337
  const capability = toolCapabilities[event.toolName];
341
338
  if (!capability) {
342
339
  return {
@@ -353,13 +350,437 @@ export default function aheadExtension(pi: ExtensionAPI): void {
353
350
  });
354
351
  }
355
352
 
353
+ interface GuidedAction {
354
+ label: string;
355
+ run: () => Promise<void>;
356
+ }
357
+
358
+ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionCommandContext): Promise<void> {
359
+ const engine = await enginePromise;
360
+ const store = storeFor(ctx);
361
+ let run = await store.loadCurrent();
362
+
363
+ if (run && engine.deriveState(run).closed) {
364
+ if (!ctx.hasUI) {
365
+ ctx.ui.notify("The current AHEAD run is complete. Start new work in an interactive Pi session.", "info");
366
+ return;
367
+ }
368
+ const choice = await ctx.ui.select("AHEAD work is complete", [
369
+ "Start a new Product Change",
370
+ "View the completed run",
371
+ ]);
372
+ if (choice === "View the completed run") {
373
+ ctx.ui.notify(formatState(engine.deriveState(run)), "info");
374
+ return;
375
+ }
376
+ if (choice !== "Start a new Product Change") return;
377
+ run = undefined;
378
+ }
379
+
380
+ if (!run) {
381
+ run = await startRun(ctx, args);
382
+ if (!run) return;
383
+ }
384
+
385
+ await refreshUi(ctx, run);
386
+ if (!ctx.hasUI) {
387
+ ctx.ui.notify(formatState(engine.deriveState(run)), "info");
388
+ return;
389
+ }
390
+
391
+ const state = engine.deriveState(run);
392
+ const workflow = engine.getWorkflow(run.workflow_id);
393
+ const guidance = phaseGuide(state.phase.id);
394
+ const action = nextAction(state, workflow);
395
+ const actions: GuidedAction[] = [];
396
+ const missingRequired = state.artifacts.filter((artifact) => artifact.required && !artifact.present);
397
+ if (action.artifactKind) {
398
+ actions.push({
399
+ label: action.label,
400
+ run: action.actor === "ai"
401
+ ? async () => requestAiAssistance(pi, state, action.artifactKind)
402
+ : async () => recordHumanArtifact(ctx, action.artifactKind ?? ""),
403
+ });
404
+ }
405
+
406
+ if (action.optional) {
407
+ const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor === "human");
408
+ if (nextHumanArtifact) {
409
+ actions.push({
410
+ label: `Continue without optional AI challenge · Write ${nextHumanArtifact.title}`,
411
+ run: async () => recordHumanArtifact(ctx, nextHumanArtifact.kind),
412
+ });
413
+ }
414
+ }
415
+
416
+ if (missingRequired.length === 0 && !action.artifactKind) {
417
+ actions.push({
418
+ label: state.gate.accepted
419
+ ? action.label
420
+ : `Accept and continue · ${state.gate.title}`,
421
+ run: async () => acceptAndContinue(ctx),
422
+ });
423
+ }
424
+
425
+ if (
426
+ state.allowed_ai_capabilities.length > 0
427
+ && action.actor !== "ai"
428
+ && !missingRequired.some((artifact) => artifact.actor === "ai")
429
+ && state.phase.id !== "implement"
430
+ ) {
431
+ actions.push({
432
+ label: `Ask AI to assist · ${state.phase.title}`,
433
+ run: async () => requestAiAssistance(pi, state),
434
+ });
435
+ }
436
+
437
+ if (state.phase.id === "implement") {
438
+ actions.push({
439
+ label: "Ask AI for help understanding or solving a problem",
440
+ run: async () => askImplementationQuestion(pi, ctx, state),
441
+ });
442
+ }
443
+
444
+ if (state.return_targets.length > 0) {
445
+ actions.push({
446
+ label: "Return to an earlier phase",
447
+ run: async () => returnToEarlierPhase(ctx, ""),
448
+ });
449
+ }
450
+
451
+ actions.push({
452
+ label: "Read AHEAD framework guidance for this phase",
453
+ run: async () => showAheadGuide(ctx, ""),
454
+ });
455
+
456
+ actions.push({
457
+ label: "Explain this phase and its expectations",
458
+ run: async () => {
459
+ ctx.ui.notify(
460
+ [
461
+ `${state.phase.title} · Human leads, AI assists`,
462
+ `Goal: ${guidance.objective}`,
463
+ `You: ${guidance.human}`,
464
+ `AI: ${guidance.ai}`,
465
+ `Gate: ${state.gate.title}`,
466
+ ].join("\n"),
467
+ "info",
468
+ );
469
+ },
470
+ });
471
+
472
+ const selected = await ctx.ui.select(
473
+ `AHEAD mode · ${state.phase.title}\nNext (${action.actor === "human" ? "you" : "AI"}): ${action.label}`,
474
+ actions.map((candidate) => candidate.label),
475
+ );
476
+ const chosen = actions.find((candidate) => candidate.label === selected);
477
+ if (chosen) await chosen.run();
478
+ }
479
+
480
+ async function askImplementationQuestion(
481
+ pi: ExtensionAPI,
482
+ ctx: ExtensionCommandContext,
483
+ state: RunState,
484
+ ): Promise<void> {
485
+ if (!ctx.hasUI) throw new Error("Implementation coaching requires interactive or RPC UI support");
486
+ const question = await ctx.ui.editor(
487
+ "AHEAD implementation help · human first",
488
+ [
489
+ "## What are you trying to understand or solve?",
490
+ "",
491
+ "",
492
+ "## What do you currently think is happening or should happen?",
493
+ "",
494
+ "",
495
+ "## What have you tried or inspected so far?",
496
+ "",
497
+ "",
498
+ "## What kind of help would be useful?",
499
+ "",
500
+ "<!-- Ask for explanation, a hint, competing approaches, debugging help, or a bounded suggestion. -->",
501
+ "",
502
+ ].join("\n"),
503
+ );
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"));
512
+ }
513
+
514
+ 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");
516
+ const run = await storeFor(ctx).loadCurrent();
517
+ const phase = run && !(await enginePromise).deriveState(run).closed
518
+ ? (await enginePromise).deriveState(run).phase.id
519
+ : undefined;
520
+ const index = await loadReferenceIndex();
521
+ let entry = requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all"
522
+ ? await findReference(requestedTopic)
523
+ : undefined;
524
+
525
+ if (requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all" && !entry) {
526
+ throw new Error(`No packaged AHEAD reference matches ${requestedTopic.trim()}`);
527
+ }
528
+
529
+ if (!entry) {
530
+ const recommended = requestedTopic.trim().toLowerCase() === "all"
531
+ ? index.references
532
+ : await relevantReferences(phase);
533
+ const browseAll = "Browse all packaged AHEAD Markdown";
534
+ const selected = await ctx.ui.select(
535
+ phase ? `AHEAD guidance · ${phase}` : "AHEAD framework guidance",
536
+ [...recommended.map((candidate) => candidate.title), ...(recommended.length < index.references.length ? [browseAll] : [])],
537
+ );
538
+ if (!selected) return;
539
+ if (selected === browseAll) return showAheadGuide(ctx, "all");
540
+ entry = recommended.find((candidate) => candidate.title === selected);
541
+ }
542
+ if (!entry) return;
543
+
544
+ await showReferenceViewer(ctx, `AHEAD reference · ${entry.title}`, await readReference(entry));
545
+ }
546
+
547
+ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): Promise<Run | undefined> {
548
+ const engine = await enginePromise;
549
+ const store = storeFor(ctx);
550
+ const current = await store.loadCurrent();
551
+ if (current && !engine.deriveState(current).closed) {
552
+ throw new AheadEngineError(
553
+ "active_run_exists",
554
+ `run ${current.id} is still active; use /ahead to continue it`,
555
+ );
556
+ }
557
+
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;
561
+
562
+ const owner = humanActor(store.projectRoot);
563
+ const run = engine.createRun({
564
+ id: store.newRunId(),
565
+ title: title.trim(),
566
+ owner,
567
+ timestamp: new Date().toISOString(),
568
+ workflow_id: "product-change",
569
+ });
570
+ await store.save(run);
571
+ await refreshUi(ctx, run);
572
+ ctx.ui.notify(
573
+ [
574
+ `AHEAD mode started · ${run.title}`,
575
+ "Human leads · AI assists",
576
+ "This run remains active in the repository until an accountable human closes the outcome.",
577
+ "Use /ahead for the next guided action; use normal conversation to think and work with AI.",
578
+ ].join("\n"),
579
+ "info",
580
+ );
581
+ return run;
582
+ }
583
+
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");
586
+ const engine = await enginePromise;
587
+ const store = storeFor(ctx);
588
+ const run = await requireRun(ctx);
589
+ const state = engine.deriveState(run);
590
+ const allowed = state.artifacts.filter((artifact) => artifact.actor !== "ai" && !artifact.present);
591
+ let kind = requestedKind.trim();
592
+ if (!kind) {
593
+ kind = (await ctx.ui.select(
594
+ `AHEAD mode · Write for ${state.phase.title}`,
595
+ allowed.map((artifact) => artifact.title),
596
+ )) ?? "";
597
+ kind = allowed.find((artifact) => artifact.title === kind)?.kind ?? kind;
598
+ }
599
+ const artifact = allowed.find((candidate) => candidate.kind === kind);
600
+ if (!artifact) {
601
+ throw new AheadEngineError(
602
+ "artifact_not_human_owned",
603
+ `there is no unrecorded human artifact named ${kind || "that"} in ${state.phase.title}`,
604
+ );
605
+ }
606
+
607
+ const content = await ctx.ui.editor(
608
+ `AHEAD mode · ${artifact.title} · write in your own words`,
609
+ buildArtifactTemplate(run, state, artifact.kind, artifact.title),
610
+ );
611
+ if (!content?.trim()) return;
612
+ const path = store.artifactPath(run, state.phase.id, artifact.kind);
613
+ const action: EventAction = {
614
+ type: "artifact_recorded",
615
+ phase: state.phase.id,
616
+ kind: artifact.kind,
617
+ path: path.relative,
618
+ };
619
+ const updated = engine.applyEvent(run, humanActor(store.projectRoot), action);
620
+ await store.writeArtifact(path.absolute, content);
621
+ await store.save(updated);
622
+ await refreshUi(ctx, updated);
623
+ ctx.ui.notify(
624
+ `Saved ${artifact.title}. AHEAD mode remains active; continue the conversation or run /ahead for the next guided action.`,
625
+ "info",
626
+ );
627
+ }
628
+
629
+ function requestAiAssistance(pi: ExtensionAPI, state: RunState, requiredKind?: string): void {
630
+ const guidance = phaseGuide(state.phase.id);
631
+ const artifact = requiredKind
632
+ ? state.artifacts.find((candidate) => candidate.kind === requiredKind)
633
+ : undefined;
634
+ const request = artifact
635
+ ? [
636
+ `AHEAD mode: perform the ${artifact.required ? "required" : "recommended"} ${state.phase.title} work for the exact current evidence and changeset.`,
637
+ `Produce ${artifact.title}.`,
638
+ `Follow the active human/AI boundary: ${guidance.ai}`,
639
+ `Use ahead_get_context first, then record the completed artifact as ${artifact.kind} with ahead_record_artifact.`,
640
+ "Treat findings as hypotheses for human disposition. Do not accept the gate or transition the run.",
641
+ ].join("\n")
642
+ : [
643
+ `AHEAD mode: assist with the active ${state.phase.title} phase.`,
644
+ guidance.ai,
645
+ "Use ahead_get_context before acting. Stay within the allowed capabilities and never author a human-owned artifact or decision.",
646
+ "Explain what you found and what the human must understand or decide next.",
647
+ ].join("\n");
648
+ pi.sendUserMessage(request);
649
+ }
650
+
651
+ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
652
+ if (!ctx.hasUI) throw new Error("Accepting an AHEAD gate requires interactive or RPC UI support");
653
+ const engine = await enginePromise;
654
+ const store = storeFor(ctx);
655
+ const run = await requireRun(ctx);
656
+ let state = engine.deriveState(run);
657
+ const missing = state.artifacts.filter((artifact) => artifact.required && !artifact.present);
658
+ if (missing.length > 0) {
659
+ throw new AheadEngineError(
660
+ "required_artifact_missing",
661
+ `complete first: ${missing.map((artifact) => artifact.title).join(", ")}`,
662
+ );
663
+ }
664
+
665
+ const destination = state.phase.next
666
+ ? engine.getWorkflow(run.workflow_id).phases.find((phase) => phase.id === state.phase.next)?.title ?? state.phase.next
667
+ : "close this AHEAD run";
668
+ const confirmed = await ctx.ui.confirm(
669
+ `Accept and continue from ${state.phase.title}?`,
670
+ [
671
+ state.gate.title,
672
+ "",
673
+ `Next: ${destination}`,
674
+ `Accountable human: ${humanActor(store.projectRoot).identity}`,
675
+ "",
676
+ "This records human acceptance. AI cannot perform this action.",
677
+ ].join("\n"),
678
+ );
679
+ if (!confirmed) return;
680
+
681
+ const actor = humanActor(store.projectRoot);
682
+ let updated = run;
683
+ if (!state.gate.accepted) {
684
+ updated = engine.applyEvent(updated, actor, {
685
+ type: "gate_accepted",
686
+ phase: state.phase.id,
687
+ gate: state.gate.id,
688
+ });
689
+ state = engine.deriveState(updated);
690
+ }
691
+ if (!state.can_advance) {
692
+ throw new AheadEngineError("cannot_advance", state.blockers.join("; ") || "the phase cannot advance");
693
+ }
694
+
695
+ const action: EventAction = state.phase.next
696
+ ? {
697
+ type: "phase_transitioned",
698
+ from: state.phase.id,
699
+ to: state.phase.next,
700
+ direction: "advance",
701
+ }
702
+ : { type: "run_closed", phase: state.phase.id };
703
+ updated = engine.applyEvent(updated, actor, action);
704
+ await store.save(updated);
705
+ await refreshUi(ctx, updated);
706
+
707
+ const nextState = engine.deriveState(updated);
708
+ if (nextState.closed) {
709
+ ctx.ui.notify("AHEAD work complete. The accountable human accepted the outcome and closed the run.", "info");
710
+ } else if (nextState.phase.id === "human-review") {
711
+ ctx.ui.notify(
712
+ [
713
+ "READY FOR INDEPENDENT HUMAN REVIEW",
714
+ "The AI review is recorded and its material findings were disposed by a human.",
715
+ "A draft branch may already exist, but a human must now request review or mark the PR ready.",
716
+ "The independent reviewer opens this repository, runs /ahead, and records the review.",
717
+ ].join("\n"),
718
+ "info",
719
+ );
720
+ } else {
721
+ ctx.ui.notify(
722
+ `Continued to ${nextState.phase.title}. AHEAD mode remains active; run /ahead for the next guided action.`,
723
+ "info",
724
+ );
725
+ }
726
+ }
727
+
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");
730
+ const engine = await enginePromise;
731
+ const store = storeFor(ctx);
732
+ const run = await requireRun(ctx);
733
+ const state = engine.deriveState(run);
734
+ if (!state.return_targets.length) {
735
+ throw new AheadEngineError("no_return_target", `phase ${state.phase.id} has no return transition`);
736
+ }
737
+ const workflow = engine.getWorkflow(run.workflow_id);
738
+ const targetOptions = state.return_targets.map((target) => ({
739
+ id: target,
740
+ title: workflow.phases.find((phase) => phase.id === target)?.title ?? target,
741
+ }));
742
+ let target = requestedTarget.trim();
743
+ if (!target) {
744
+ const selected = await ctx.ui.select(
745
+ "Return to which phase?",
746
+ targetOptions.map((candidate) => candidate.title),
747
+ );
748
+ target = targetOptions.find((candidate) => candidate.title === selected)?.id ?? "";
749
+ }
750
+ if (!state.return_targets.includes(target)) {
751
+ throw new AheadEngineError(
752
+ "invalid_return",
753
+ `phase ${state.phase.id} can return only to: ${state.return_targets.join(", ")}`,
754
+ );
755
+ }
756
+ const reason = await ctx.ui.editor(
757
+ `Why return to ${workflow.phases.find((phase) => phase.id === target)?.title ?? target}?`,
758
+ );
759
+ if (!reason?.trim()) return;
760
+ const confirmed = await ctx.ui.confirm(
761
+ `Return to ${target}?`,
762
+ "This opens a new phase visit. Earlier artifacts remain as history but cannot satisfy the reopened gate.",
763
+ );
764
+ if (!confirmed) return;
765
+ const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
766
+ type: "phase_transitioned",
767
+ from: state.phase.id,
768
+ to: target,
769
+ direction: "return",
770
+ reason: reason.trim(),
771
+ });
772
+ await store.save(updated);
773
+ await refreshUi(ctx, updated);
774
+ ctx.ui.notify(`Returned to ${target}. AHEAD mode remains active with fresh evidence and gate requirements.`, "warning");
775
+ }
776
+
356
777
  function storeFor(ctx: ExtensionContext): RunStore {
357
778
  return new RunStore(projectRoot(ctx.cwd));
358
779
  }
359
780
 
360
781
  async function requireRun(ctx: ExtensionContext): Promise<Run> {
361
782
  const run = await storeFor(ctx).loadCurrent();
362
- if (!run) throw new AheadEngineError("no_active_run", "no active AHEAD run; use /ahead-start [title]");
783
+ if (!run) throw new AheadEngineError("no_active_run", "no active AHEAD run; use /ahead [title]");
363
784
  return run;
364
785
  }
365
786
 
@@ -378,26 +799,20 @@ async function refreshUi(ctx: ExtensionContext, supplied?: Run): Promise<void> {
378
799
  ctx.ui.setWidget("ahead", undefined);
379
800
  return;
380
801
  }
381
- const state = (await enginePromise).deriveState(run);
802
+ const engine = await enginePromise;
803
+ const state = engine.deriveState(run);
804
+ const workflow = engine.getWorkflow(run.workflow_id);
805
+ const position = phasePosition(state, workflow);
806
+ const action = nextAction(state, workflow);
382
807
  ctx.ui.setStatus(
383
808
  "ahead",
384
809
  state.closed
385
- ? `AHEAD · ${state.workflow_id} · closed`
386
- : `AHEAD · ${state.phase.id}#${state.phase.visit} · ${state.blockers.length} blocker${state.blockers.length === 1 ? "" : "s"}`,
810
+ ? `AHEAD · complete · ${state.workflow_id}`
811
+ : `AHEAD · ${position.current}/${position.total} · ${state.phase.id} · ${action.actor} action`,
387
812
  );
388
813
  ctx.ui.setWidget(
389
814
  "ahead",
390
- [
391
- `AHEAD · ${run.title}`,
392
- state.closed
393
- ? "Closed"
394
- : `${state.phase.title} · visit ${state.phase.visit} · gate ${state.gate.accepted ? "accepted" : "open"}`,
395
- state.blockers.length
396
- ? `Next: ${state.blockers[0]}`
397
- : state.phase.next
398
- ? "Next: /ahead-advance"
399
- : "Next: /ahead-advance (closes run)",
400
- ],
815
+ buildWidgetLines(run, state, workflow),
401
816
  { placement: "aboveEditor" },
402
817
  );
403
818
  }
@@ -426,19 +841,6 @@ function formatState(state: RunState): string {
426
841
  ].join("\n");
427
842
  }
428
843
 
429
- function artifactTemplate(run: Run, state: RunState, kind: string, title: string): string {
430
- return [
431
- `# ${title}`,
432
- "",
433
- `AHEAD run: ${run.id}`,
434
- `Phase: ${state.phase.id} (visit ${state.phase.visit})`,
435
- `Artifact: ${kind}`,
436
- "",
437
- "<!-- Replace this comment with the human-authored record. Preserve evidence, uncertainty, and rationale. -->",
438
- "",
439
- ].join("\n");
440
- }
441
-
442
844
  async function command(ctx: ExtensionCommandContext, action: () => Promise<void>): Promise<void> {
443
845
  try {
444
846
  await action();