ahead-pi 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # AHEAD for Pi
2
2
 
3
- Status: dogfood v0.1
3
+ Status: guided-mode dogfood v0.2
4
4
 
5
- The Pi integration runs the Rust AHEAD state machine as WebAssembly, injects generated phase instructions into any Pi model, persists the event/evidence chain, and exposes human gates through explicit slash commands.
5
+ The Pi integration runs the Rust AHEAD state machine as WebAssembly, injects generated phase instructions into any Pi model, persists the event/evidence chain, and presents AHEAD as a guided mode with human-owned gates.
6
6
 
7
7
  It does not own model authentication. Use whichever provider Pi already supports and your organization permits, including an existing GitHub Copilot configuration. AHEAD never receives the model credential.
8
8
 
9
9
  ## Install from npm
10
10
 
11
- After the first public release, install the current version globally in Pi:
11
+ Install the current version globally in Pi:
12
12
 
13
13
  ```sh
14
14
  pi install npm:ahead-pi
@@ -17,7 +17,7 @@ pi install npm:ahead-pi
17
17
  Pin an exact version for a team or project:
18
18
 
19
19
  ```sh
20
- pi install -l npm:ahead-pi@0.1.0
20
+ pi install -l npm:ahead-pi@0.2.0
21
21
  ```
22
22
 
23
23
  Or try it for one session without changing settings:
@@ -51,31 +51,41 @@ pi
51
51
 
52
52
  Pi may ask you to trust the project-local extension. Review it before accepting; project trust is not a sandbox.
53
53
 
54
- ## Dogfood loop
54
+ ## Guided mode
55
55
 
56
56
  1. Start Pi in the repository where the engineering work will occur.
57
- 2. Run `/ahead-start <short title>`.
58
- 3. Run `/ahead-status` to see the active phase contract.
59
- 4. Use `/ahead-record [kind]` for human-owned artifacts.
60
- 5. Ask the model for only the assistance allowed in the active phase. It can call `ahead_get_context` and, where allowed, `ahead_record_artifact`.
61
- 6. Run `/ahead-accept` only after examining the evidence and owning the gate decision.
62
- 7. Run `/ahead-advance`, or `/ahead-return [phase]` with a reason when the work must reopen.
63
- 8. Commit appropriate `.ahead` records with the work so review and later adapters can validate them.
57
+ 2. Run `/ahead <short title>` once to enter AHEAD mode and start a Product Change run.
58
+ 3. Work through normal conversation. The persistent widget shows the current goal, what the human owns, what AI may do, required evidence, and the next valid action.
59
+ 4. Run `/ahead` again whenever you want the contextual action menu. It opens the right guided editor, requests the right AI contribution, accepts and advances a human gate, or returns to an earlier phase.
60
+ 5. Keep the `.ahead` records with the work so another session or independent reviewer resumes the same authoritative run.
61
+
62
+ AHEAD remains active across Pi sessions until an accountable human completes the outcome phase and closes the run. Restarting Pi does not leave the mode or reset the workflow.
63
+
64
+ The normal implementation handoff is:
65
+
66
+ ```text
67
+ HUMAN IMPLEMENTS AND SELF-CHECKS
68
+
69
+ AI REVIEWS THE EXACT CURRENT CHANGESET
70
+
71
+ HUMAN DISPOSES MATERIAL AI FINDINGS
72
+
73
+ READY FOR INDEPENDENT HUMAN REVIEW
74
+
75
+ INDEPENDENT HUMAN REVIEWS AND ACCEPTS
76
+ ```
64
77
 
65
- The footer and editor widget show the current phase, visit, gate, and first blocker.
78
+ A draft branch or draft PR may exist earlier. The handoff gate is requesting human review or marking the PR ready, not ordinary draft pushes.
66
79
 
67
80
  ## Human commands
68
81
 
69
82
  | Command | Effect |
70
83
  |---|---|
71
- | `/ahead-start [title]` | Human starts and owns a Product Change run |
72
- | `/ahead-status` | Show phase, artifacts, capabilities, gate, and blockers |
73
- | `/ahead-record [kind]` | Human writes and records a permitted artifact |
74
- | `/ahead-accept` | Human accepts the current gate after required evidence exists |
75
- | `/ahead-advance` | Human advances, or closes the accepted final phase |
76
- | `/ahead-return [phase]` | Human reopens an allowed earlier phase with a reason |
84
+ | `/ahead [title]` | Enter, resume, or perform the next guided AHEAD action |
77
85
  | `/ahead-help` | Show commands and authority boundaries |
78
86
 
87
+ `/ahead-start`, `/ahead-status`, `/ahead-record`, `/ahead-accept`, `/ahead-advance`, and `/ahead-return` remain available as advanced recovery and inspection commands. Normal use should not require memorizing them.
88
+
79
89
  ## AI tools
80
90
 
81
91
  - `ahead_get_context` reads authoritative state.
@@ -101,11 +111,11 @@ Human identity is resolved from `AHEAD_HUMAN_IDENTITY`, Git `user.email`, Git `u
101
111
  AHEAD_HUMAN_IDENTITY=reviewer@example.com pi -e ./integrations/pi/src/index.ts
102
112
  ```
103
113
 
104
- This is local self-attestation, not cryptographic identity. The initial version is single-writer, implements only Product Change, and has no GitHub/CI workflow enforcement yet. See [Executable AHEAD workflows](https://github.com/Kade-Powell/ahead/blob/main/docs/design/executable-workflows.md) for the architecture and trust boundaries.
114
+ This is local self-attestation, not cryptographic identity. The initial version is single-writer, implements only Product Change, and has no GitHub/CI workflow enforcement yet. Review records ask for the exact commit or diff, but v0.2 does not yet cryptographically bind that changeset to the review; a changed changeset must be returned and reviewed again by the humans involved. See [Executable AHEAD workflows](https://github.com/Kade-Powell/ahead/blob/main/docs/design/executable-workflows.md) for the architecture and trust boundaries.
105
115
 
106
116
  ## Package and release verification
107
117
 
108
- `npm test` builds the Rust core for `wasm32-unknown-unknown`, regenerates instructions, runs Rust/WASM-facing tests, creates the exact npm tarball, verifies its allowlisted contents, loads the extracted package through the real Pi binary, and confirms that `/ahead-start` persists a valid run.
118
+ `npm test` builds the Rust core for `wasm32-unknown-unknown`, regenerates instructions, runs Rust/WASM-facing and guided-mode tests, creates the exact npm tarball, verifies its allowlisted contents, loads the extracted package through the real Pi binary, and confirms that the packaged extension persists a valid run.
109
119
 
110
120
  The npm package contains only its README, package metadata, TypeScript runtime, generated phase instructions, and compiled WASM engine. Build scripts, tests, source specs, development dependencies, and repository files are excluded.
111
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ahead-pi",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "AHEAD workflow enforcement and context for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,358 @@
1
+ import type { ArtifactState, Run, RunState, WorkflowDefinition } from "./types.js";
2
+
3
+ export interface PhaseGuide {
4
+ objective: string;
5
+ human: string;
6
+ ai: string;
7
+ artifactPrompts: Record<string, string[]>;
8
+ handoff?: string;
9
+ }
10
+
11
+ const guides: Record<string, PhaseGuide> = {
12
+ define: {
13
+ objective: "Agree on the problem and the observable outcome before solution work begins.",
14
+ human: "Describe the users, current problem, desired outcome, scope, constraints, and success signals.",
15
+ ai: "Explain the prompts. After your first statement, clarify ambiguity and expose assumptions without redefining the problem.",
16
+ artifactPrompts: {
17
+ problem: [
18
+ "Who experiences the problem, and what happens today?",
19
+ "What outcome should change, for whom, and why now?",
20
+ "What is in scope, out of scope, or constrained?",
21
+ "What observable signals would demonstrate success or failure?",
22
+ "Which assumptions or uncertainties should remain visible?",
23
+ ],
24
+ },
25
+ },
26
+ research: {
27
+ objective: "Build enough evidence to understand the problem before choosing a solution.",
28
+ human: "Set the research boundary, judge source relevance, and identify what evidence is material.",
29
+ ai: "Inspect authorized sources, organize evidence, surface contradictions, and state confidence and gaps.",
30
+ artifactPrompts: {
31
+ research: [
32
+ "What sources, code, behavior, history, or measurements were examined?",
33
+ "What findings are supported, contradicted, or still uncertain?",
34
+ "How directly does each finding apply to this work?",
35
+ "What important evidence is still missing?",
36
+ ],
37
+ },
38
+ },
39
+ questions: {
40
+ objective: "Dispose the unknowns that could materially change the decision or plan.",
41
+ human: "Decide which unknowns must be answered and which uncertainty can be accepted explicitly.",
42
+ ai: "Challenge gaps, perform authorized follow-up research, and distinguish evidence from inference.",
43
+ artifactPrompts: {
44
+ unknowns: [
45
+ "Which questions could materially change the work?",
46
+ "What answer or evidence resolved each question?",
47
+ "Which unknowns remain, and why is proceeding still acceptable?",
48
+ ],
49
+ "question-research": [
50
+ "What follow-up question was investigated?",
51
+ "What evidence was found, and what remains uncertain?",
52
+ ],
53
+ },
54
+ },
55
+ options: {
56
+ objective: "Understand viable approaches and tradeoffs before committing to one.",
57
+ human: "Produce the first option, then evaluate alternatives and tradeoffs in the system's real context.",
58
+ ai: "After the human first pass, challenge assumptions and add materially different alternatives.",
59
+ artifactPrompts: {
60
+ "human-option": [
61
+ "What is your first workable approach?",
62
+ "Why might it fit the problem and constraints?",
63
+ "What risks, costs, and uncertainties do you already see?",
64
+ ],
65
+ "ai-challenge": [
66
+ "Which assumptions or failure modes does the first option overlook?",
67
+ "What materially different alternatives deserve consideration?",
68
+ ],
69
+ options: [
70
+ "Which options were considered?",
71
+ "What are their tradeoffs, failure modes, reversibility, and operational consequences?",
72
+ "Which options were rejected, and why?",
73
+ ],
74
+ },
75
+ },
76
+ decision: {
77
+ objective: "Make an accountable and explainable choice.",
78
+ human: "Choose the approach and own its rationale, tradeoffs, remaining uncertainty, and reversibility.",
79
+ ai: "Test the recorded decision for contradictions, weak evidence, and hidden consequences.",
80
+ artifactPrompts: {
81
+ decision: [
82
+ "What was decided?",
83
+ "Why does it best fit the evidence and constraints?",
84
+ "What tradeoffs and risks are accepted?",
85
+ "What remains unknown, and how reversible is the decision?",
86
+ ],
87
+ },
88
+ },
89
+ plan: {
90
+ objective: "Create an implementable plan with verification, rollout, and recovery.",
91
+ human: "Write the first-pass sequence and approve the final plan after challenges are resolved.",
92
+ ai: "After the human first pass, identify missing dependencies, tests, edge cases, rollout evidence, recovery, and decision points.",
93
+ artifactPrompts: {
94
+ "first-pass-plan": [
95
+ "What sequence of changes do you currently expect?",
96
+ "Which systems, boundaries, and dependencies are involved?",
97
+ "How will you test, release, observe, and recover?",
98
+ ],
99
+ "ai-plan-review": [
100
+ "What is missing, risky, ambiguous, or ordered incorrectly?",
101
+ "Which tests, edge cases, rollout checks, and recovery steps should be added?",
102
+ ],
103
+ plan: [
104
+ "What is the final ordered implementation sequence?",
105
+ "What tests and observable evidence are required?",
106
+ "How will rollout and recovery work?",
107
+ "Which deviations require returning to decision or plan?",
108
+ ],
109
+ },
110
+ },
111
+ implement: {
112
+ objective: "Produce a change the engineer understands and can defend.",
113
+ human: "Own the implementation, understand every lasting change, run the planned checks, and record deviations.",
114
+ ai: "Assist within the approved plan, explain changes, run authorized checks, and surface reasons to revisit earlier phases.",
115
+ artifactPrompts: {
116
+ changeset: [
117
+ "What exact commit, branch, pull request, or diff identifies the current change?",
118
+ "Can you explain the important behavior and design choices?",
119
+ ],
120
+ tests: [
121
+ "Which planned and additional checks ran?",
122
+ "What passed, failed, or was not run?",
123
+ "What evidence covers boundaries, failures, and regressions?",
124
+ ],
125
+ "plan-deviations": [
126
+ "Where did implementation differ from the approved plan?",
127
+ "Why was each deviation acceptable, or state explicitly that there were none?",
128
+ ],
129
+ },
130
+ },
131
+ "ai-review": {
132
+ objective: "Add an AI review of the exact current changeset before independent human review.",
133
+ human: "Validate and dispose every material finding. Return to implementation when a change is required.",
134
+ ai: "Review without modifying: correctness, security, tests, architecture, plan compliance, operations, and maintainability.",
135
+ artifactPrompts: {
136
+ "ai-review": [
137
+ "What exact commit or diff was reviewed?",
138
+ "What findings were identified, with evidence and severity?",
139
+ "What disposition is proposed for each finding?",
140
+ "What could not be assessed?",
141
+ ],
142
+ },
143
+ },
144
+ "human-review": {
145
+ objective: "Obtain independent final engineering judgment on the current change.",
146
+ human: "A reviewer other than the implementer examines the exact change and material evidence, then accepts or returns it.",
147
+ ai: "Retrieve evidence and answer targeted questions. It cannot approve the change or replace reviewer judgment.",
148
+ handoff: "READY FOR INDEPENDENT HUMAN REVIEW",
149
+ artifactPrompts: {
150
+ "human-review": [
151
+ "Who reviewed the change, and what exact commit or diff was reviewed?",
152
+ "What code, tests, evidence, risks, and operational consequences were examined?",
153
+ "Which findings must be addressed or explicitly accepted?",
154
+ "Does the reviewer understand and accept the current change?",
155
+ ],
156
+ },
157
+ },
158
+ deploy: {
159
+ objective: "Authorize and record deployment or explicitly establish that it is not applicable.",
160
+ human: "Own the release decision and production risk. Record the exact version, target, actor, time, authorization, and result.",
161
+ ai: "Analyze readiness evidence. It cannot authorize deployment or claim a version is live.",
162
+ artifactPrompts: {
163
+ deployment: [
164
+ "What exact version was released to which target?",
165
+ "Who authorized and performed it, and when?",
166
+ "What did the deployment system report?",
167
+ "If deployment is not applicable, why?",
168
+ ],
169
+ },
170
+ },
171
+ verify: {
172
+ objective: "Demonstrate the intended outcome using observed evidence, not just test or deployment status.",
173
+ human: "Select adequate checks and decide whether the original success signals are demonstrated.",
174
+ ai: "Suggest checks and analyze authorized observations while separating code, deployment, and observed behavior.",
175
+ artifactPrompts: {
176
+ verification: [
177
+ "What pre-change and post-change behavior was compared?",
178
+ "Which tests, deployment facts, runtime observations, and user-visible signals were checked?",
179
+ "Did the original success and failure signals occur?",
180
+ "What remains uncertain or needs continued observation?",
181
+ ],
182
+ },
183
+ },
184
+ "ai-audit": {
185
+ objective: "Compare the result with the original intent and expose weak evidence or divergence.",
186
+ human: "Review and dispose material audit findings; reopen work when the evidence demands it.",
187
+ ai: "Audit the full chain from problem through observed outcome without changing or approving the work.",
188
+ artifactPrompts: {
189
+ "ai-audit": [
190
+ "Where did the result diverge from the problem, decision, plan, or reviews?",
191
+ "Which claims have weak or missing evidence?",
192
+ "Which findings require follow-up or reopening?",
193
+ ],
194
+ },
195
+ },
196
+ outcome: {
197
+ objective: "Make the accountable outcome decision and preserve learning.",
198
+ human: "Accept, roll back, follow up, abandon, or reopen the work, including remaining uncertainty.",
199
+ ai: "Organize evidence and summarize learning. It cannot choose or accept the outcome.",
200
+ artifactPrompts: {
201
+ outcome: [
202
+ "What outcome decision is being made?",
203
+ "Which evidence supports it?",
204
+ "What uncertainty, debt, monitoring, or follow-up remains?",
205
+ "What should the team or AHEAD process learn from this run?",
206
+ ],
207
+ },
208
+ },
209
+ };
210
+
211
+ const fallbackGuide: PhaseGuide = {
212
+ objective: "Complete the active phase using the recorded workflow contract.",
213
+ human: "Own the phase decision, required evidence, and gate.",
214
+ ai: "Assist only within the capabilities and artifact ownership allowed by the active phase.",
215
+ artifactPrompts: {},
216
+ };
217
+
218
+ export function phaseGuide(phaseId: string): PhaseGuide {
219
+ return guides[phaseId] ?? fallbackGuide;
220
+ }
221
+
222
+ export function promptsForArtifact(phaseId: string, kind: string): string[] {
223
+ return phaseGuide(phaseId).artifactPrompts[kind] ?? [
224
+ "What must another engineer understand from this record?",
225
+ "What evidence, uncertainty, and rationale should remain durable?",
226
+ ];
227
+ }
228
+
229
+ export function phasePosition(state: RunState, workflow: WorkflowDefinition): { current: number; total: number } {
230
+ const index = workflow.phases.findIndex((phase) => phase.id === state.phase.id);
231
+ return { current: index < 0 ? 0 : index + 1, total: workflow.phases.length };
232
+ }
233
+
234
+ export interface GuidedNextAction {
235
+ actor: "human" | "ai";
236
+ label: string;
237
+ artifactKind?: string;
238
+ optional?: boolean;
239
+ }
240
+
241
+ export function nextAction(state: RunState, workflow: WorkflowDefinition): GuidedNextAction {
242
+ if (state.closed) return { actor: "human", label: "Work complete; start another run only for new work" };
243
+
244
+ const orderedAssist = optionalAssistBeforeFinalHumanRecord(state);
245
+ if (orderedAssist) return orderedAssist;
246
+
247
+ const artifact = state.artifacts.find((candidate) => candidate.required && !candidate.present);
248
+ if (artifact) {
249
+ const actor = artifact.actor === "human"
250
+ ? "human"
251
+ : artifact.actor === "ai" || state.phase.id === "research"
252
+ ? "ai"
253
+ : "human";
254
+ if (actor === "human") {
255
+ const label = state.phase.id === "human-review"
256
+ ? "Independent reviewer records the current human review"
257
+ : `Write ${artifact.title}`;
258
+ return { actor, label, artifactKind: artifact.kind };
259
+ }
260
+
261
+ const label = state.phase.id === "ai-review"
262
+ ? "Run AI review of the exact current changeset"
263
+ : state.phase.id === "ai-audit"
264
+ ? "Run AI audit across intent, evidence, and outcome"
265
+ : `Ask AI to produce ${artifact.title}`;
266
+ return { actor, label, artifactKind: artifact.kind };
267
+ }
268
+
269
+ if (!state.gate.accepted) return { actor: "human", label: `Review evidence and accept: ${state.gate.title}` };
270
+
271
+ const nextPhase = workflow.phases.find((phase) => phase.id === state.phase.next);
272
+ return {
273
+ actor: "human",
274
+ label: nextPhase ? `Continue to ${nextPhase.title}` : "Accept the outcome and close this AHEAD run",
275
+ };
276
+ }
277
+
278
+ function optionalAssistBeforeFinalHumanRecord(state: RunState): GuidedNextAction | undefined {
279
+ const orderedAssist = state.phase.id === "options"
280
+ ? { prerequisite: "human-option", assist: "ai-challenge", final: "options" }
281
+ : state.phase.id === "plan"
282
+ ? { prerequisite: "first-pass-plan", assist: "ai-plan-review", final: "plan" }
283
+ : undefined;
284
+ if (!orderedAssist) return undefined;
285
+
286
+ const prerequisite = state.artifacts.find((artifact) => artifact.kind === orderedAssist.prerequisite);
287
+ const assist = state.artifacts.find((artifact) => artifact.kind === orderedAssist.assist);
288
+ const final = state.artifacts.find((artifact) => artifact.kind === orderedAssist.final);
289
+ if (!prerequisite?.present || assist?.present || final?.present) return undefined;
290
+
291
+ return {
292
+ actor: "ai",
293
+ artifactKind: assist?.kind,
294
+ label: state.phase.id === "options"
295
+ ? "Ask AI to challenge the human option and expand alternatives"
296
+ : "Ask AI to challenge the human first-pass plan",
297
+ optional: true,
298
+ };
299
+ }
300
+
301
+ export function buildWidgetLines(run: Run, state: RunState, workflow: WorkflowDefinition): string[] {
302
+ if (state.closed) {
303
+ return [
304
+ `AHEAD COMPLETE · ${run.title}`,
305
+ "The accountable human accepted the outcome and closed this run.",
306
+ "Run /ahead only when new work needs a new workflow.",
307
+ ];
308
+ }
309
+
310
+ const guide = phaseGuide(state.phase.id);
311
+ const position = phasePosition(state, workflow);
312
+ const required = state.artifacts.filter((artifact) => artifact.required);
313
+ const checklist = required.length
314
+ ? required.map(formatArtifactStatus).join(" · ")
315
+ : "No required artifact";
316
+ const action = nextAction(state, workflow);
317
+
318
+ return [
319
+ `AHEAD MODE · ${workflow.title.toUpperCase()} · ${position.current}/${position.total}`,
320
+ `${guide.handoff ?? state.phase.title.toUpperCase()} · HUMAN LEADS · AI ASSISTS`,
321
+ `Goal: ${guide.objective}`,
322
+ `You: ${guide.human}`,
323
+ `AI: ${guide.ai}`,
324
+ `Required: ${checklist}`,
325
+ `Next (${action.actor === "human" ? "you" : "AI"}): ${action.label}`,
326
+ "Run /ahead for the guided action.",
327
+ ];
328
+ }
329
+
330
+ export function buildArtifactTemplate(
331
+ run: Run,
332
+ state: RunState,
333
+ kind: string,
334
+ title: string,
335
+ ): string {
336
+ const prompts = promptsForArtifact(state.phase.id, kind);
337
+ return [
338
+ `# ${title}`,
339
+ "",
340
+ `AHEAD run: ${run.id}`,
341
+ `Phase: ${state.phase.id} (visit ${state.phase.visit})`,
342
+ `Artifact: ${kind}`,
343
+ "",
344
+ "## What to cover",
345
+ "",
346
+ ...prompts.map((prompt) => `- ${prompt}`),
347
+ "",
348
+ "## Record",
349
+ "",
350
+ "<!-- Write this human-owned record in your own words. Preserve evidence, uncertainty, and rationale. -->",
351
+ "",
352
+ ].join("\n");
353
+ }
354
+
355
+ function formatArtifactStatus(artifact: ArtifactState): string {
356
+ const owner = artifact.actor === "ai" ? "AI" : artifact.actor === "human" ? "you" : "you/AI";
357
+ return `${artifact.present ? "✓" : "○"} ${artifact.title} [${owner}]`;
358
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,13 @@ 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";
10
17
  import { humanActor, projectRoot, RunStore } from "./storage.js";
11
18
  import type { Actor, Capability, EventAction, Run, RunState } from "./types.js";
12
19
 
@@ -33,36 +40,22 @@ const RecordArtifactParams = Type.Object({
33
40
  });
34
41
 
35
42
  export default function aheadExtension(pi: ExtensionAPI): void {
43
+ pi.registerCommand("ahead", {
44
+ description: "Enter or continue the guided AHEAD mode",
45
+ handler: async (args, ctx) => command(ctx, async () => {
46
+ await openAheadMode(pi, args, ctx);
47
+ }),
48
+ });
49
+
36
50
  pi.registerCommand("ahead-start", {
37
- description: "Start a Product Change workflow owned by the current human",
51
+ description: "Advanced: start a Product Change run directly",
38
52
  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");
53
+ await startRun(ctx, args);
61
54
  }),
62
55
  });
63
56
 
64
57
  pi.registerCommand("ahead-status", {
65
- description: "Show the active AHEAD phase, evidence, gate, and blockers",
58
+ description: "Advanced: show the raw active AHEAD phase contract",
66
59
  handler: async (_args, ctx) => command(ctx, async () => {
67
60
  const run = await requireRun(ctx);
68
61
  const state = (await enginePromise).deriveState(run);
@@ -72,50 +65,14 @@ export default function aheadExtension(pi: ExtensionAPI): void {
72
65
  });
73
66
 
74
67
  pi.registerCommand("ahead-record", {
75
- description: "Write and record a human-owned artifact for the active phase",
68
+ description: "Advanced: record a human-owned artifact directly",
76
69
  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");
70
+ await recordHumanArtifact(ctx, args.trim());
114
71
  }),
115
72
  });
116
73
 
117
74
  pi.registerCommand("ahead-accept", {
118
- description: "Human acceptance of the active phase gate",
75
+ description: "Advanced: accept the active gate without advancing",
119
76
  handler: async (_args, ctx) => command(ctx, async () => {
120
77
  if (!ctx.hasUI) throw new Error("/ahead-accept requires interactive or RPC UI support");
121
78
  const engine = await enginePromise;
@@ -139,7 +96,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
139
96
  });
140
97
 
141
98
  pi.registerCommand("ahead-advance", {
142
- description: "Human transition to the next phase, or close the final phase",
99
+ description: "Advanced: advance an already accepted gate",
143
100
  handler: async (_args, ctx) => command(ctx, async () => {
144
101
  if (!ctx.hasUI) throw new Error("/ahead-advance requires interactive or RPC UI support");
145
102
  const engine = await enginePromise;
@@ -168,57 +125,25 @@ export default function aheadExtension(pi: ExtensionAPI): void {
168
125
  });
169
126
 
170
127
  pi.registerCommand("ahead-return", {
171
- description: "Human return to an allowed earlier phase with a recorded reason",
128
+ description: "Advanced: return to an earlier phase with a reason",
172
129
  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");
130
+ await returnToEarlierPhase(ctx, args);
206
131
  }),
207
132
  });
208
133
 
209
134
  pi.registerCommand("ahead-help", {
210
- description: "Show AHEAD Pi commands and the human/AI boundary",
135
+ description: "Explain guided AHEAD mode and its authority boundary",
211
136
  handler: async (_args, ctx) => {
212
137
  ctx.ui.notify(
213
138
  [
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",
139
+ "/ahead [title] — enter, resume, or act in guided AHEAD mode",
140
+ "",
141
+ "Once started, the repository run remains in AHEAD mode until an accountable human closes the outcome.",
142
+ "Use normal conversation to think and work with AI. Run /ahead whenever you want the next valid action.",
143
+ "The persistent guide explains what you own, what AI may do, required evidence, and what happens next.",
220
144
  "",
221
- "AI can inspect context and record only AI-permitted artifacts. It cannot accept gates or transition the run.",
145
+ "Advanced fallback commands: /ahead-status, /ahead-record, /ahead-accept, /ahead-advance, /ahead-return.",
146
+ "AI can record only AI/shared artifacts allowed in the active phase. It cannot accept gates, transition, approve, deploy, or close the run.",
222
147
  ].join("\n"),
223
148
  "info",
224
149
  );
@@ -290,7 +215,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
290
215
  requested: true,
291
216
  transitioned: false,
292
217
  message: state.can_advance
293
- ? `The gate is accepted. Ask the human to use /ahead-advance to ${state.phase.next ?? "close the run"}.`
218
+ ? `The gate is accepted. Ask the human to use /ahead to ${state.phase.next ?? "close the run"}.`
294
219
  : "The phase cannot advance. The human must resolve the blockers and accept the gate.",
295
220
  blockers: state.blockers,
296
221
  };
@@ -311,6 +236,14 @@ export default function aheadExtension(pi: ExtensionAPI): void {
311
236
  pi.on("session_start", async (_event, ctx) => {
312
237
  try {
313
238
  await refreshUi(ctx);
239
+ const run = await storeFor(ctx).loadCurrent();
240
+ if (run && !(await enginePromise).deriveState(run).closed && ctx.hasUI) {
241
+ const state = (await enginePromise).deriveState(run);
242
+ ctx.ui.notify(
243
+ `AHEAD mode resumed · ${state.phase.title}. Human leads, AI assists. Run /ahead for the next guided action.`,
244
+ "info",
245
+ );
246
+ }
314
247
  } catch (error) {
315
248
  ctx.ui.setStatus("ahead", "AHEAD · invalid state");
316
249
  ctx.ui.notify(errorMessage(error), "error");
@@ -320,7 +253,12 @@ export default function aheadExtension(pi: ExtensionAPI): void {
320
253
  pi.on("before_agent_start", async (event, ctx) => {
321
254
  const run = await storeFor(ctx).loadCurrent();
322
255
  if (!run) return;
323
- const state = (await enginePromise).deriveState(run);
256
+ const engine = await enginePromise;
257
+ const state = engine.deriveState(run);
258
+ if (state.closed) return;
259
+ const workflow = engine.getWorkflow(run.workflow_id);
260
+ const guidance = phaseGuide(state.phase.id);
261
+ const action = nextAction(state, workflow);
324
262
  const phaseInstructions = await loadInstructions(state.phase.id);
325
263
  const liveContext = [
326
264
  "# Live AHEAD run",
@@ -329,6 +267,16 @@ export default function aheadExtension(pi: ExtensionAPI): void {
329
267
  `- Gate accepted: ${state.gate.accepted}`,
330
268
  `- Current blockers: ${state.blockers.length ? state.blockers.join("; ") : "none"}`,
331
269
  `- Allowed AI capabilities: ${state.allowed_ai_capabilities.length ? state.allowed_ai_capabilities.join(", ") : "none"}`,
270
+ `- Human responsibility: ${guidance.human}`,
271
+ `- AI role: ${guidance.ai}`,
272
+ `- Next guided action (${action.actor}): ${action.label}`,
273
+ "",
274
+ "## AHEAD interaction behavior",
275
+ "- AHEAD is an active working mode, not a command checklist. Help the human understand and complete the active phase through normal conversation.",
276
+ "- When asked what to do, explain the current expectation in plain language; do not merely repeat artifact identifiers or slash commands.",
277
+ "- Never author a human-owned artifact, make a human decision, accept a gate, transition the run, approve a change, or claim accountability.",
278
+ "- When required AI-owned work is ready, record it with ahead_record_artifact and explain what the human must validate or decide.",
279
+ "- Treat AI review findings as hypotheses. Independent human review remains required for lasting engineering changes.",
332
280
  ].join("\n");
333
281
  return { systemPrompt: `${event.systemPrompt}\n\n${phaseInstructions}\n\n${liveContext}\n` };
334
282
  });
@@ -337,6 +285,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
337
285
  if (event.toolName.startsWith("ahead_")) return;
338
286
  const run = await storeFor(ctx).loadCurrent();
339
287
  if (!run) return;
288
+ if ((await enginePromise).deriveState(run).closed) return;
340
289
  const capability = toolCapabilities[event.toolName];
341
290
  if (!capability) {
342
291
  return {
@@ -353,13 +302,357 @@ export default function aheadExtension(pi: ExtensionAPI): void {
353
302
  });
354
303
  }
355
304
 
305
+ interface GuidedAction {
306
+ label: string;
307
+ run: () => Promise<void>;
308
+ }
309
+
310
+ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionCommandContext): Promise<void> {
311
+ const engine = await enginePromise;
312
+ const store = storeFor(ctx);
313
+ let run = await store.loadCurrent();
314
+
315
+ if (run && engine.deriveState(run).closed) {
316
+ if (!ctx.hasUI) {
317
+ ctx.ui.notify("The current AHEAD run is complete. Start new work in an interactive Pi session.", "info");
318
+ return;
319
+ }
320
+ const choice = await ctx.ui.select("AHEAD work is complete", [
321
+ "Start a new Product Change",
322
+ "View the completed run",
323
+ ]);
324
+ if (choice === "View the completed run") {
325
+ ctx.ui.notify(formatState(engine.deriveState(run)), "info");
326
+ return;
327
+ }
328
+ if (choice !== "Start a new Product Change") return;
329
+ run = undefined;
330
+ }
331
+
332
+ if (!run) {
333
+ run = await startRun(ctx, args);
334
+ if (!run) return;
335
+ }
336
+
337
+ await refreshUi(ctx, run);
338
+ if (!ctx.hasUI) {
339
+ ctx.ui.notify(formatState(engine.deriveState(run)), "info");
340
+ return;
341
+ }
342
+
343
+ const state = engine.deriveState(run);
344
+ const workflow = engine.getWorkflow(run.workflow_id);
345
+ const guidance = phaseGuide(state.phase.id);
346
+ const action = nextAction(state, workflow);
347
+ const actions: GuidedAction[] = [];
348
+ const missingRequired = state.artifacts.filter((artifact) => artifact.required && !artifact.present);
349
+ if (action.artifactKind) {
350
+ actions.push({
351
+ label: action.label,
352
+ run: action.actor === "ai"
353
+ ? async () => requestAiAssistance(pi, state, action.artifactKind)
354
+ : async () => recordHumanArtifact(ctx, action.artifactKind ?? ""),
355
+ });
356
+ }
357
+
358
+ if (action.optional) {
359
+ const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor === "human");
360
+ if (nextHumanArtifact) {
361
+ actions.push({
362
+ label: `Continue without optional AI challenge · Write ${nextHumanArtifact.title}`,
363
+ run: async () => recordHumanArtifact(ctx, nextHumanArtifact.kind),
364
+ });
365
+ }
366
+ }
367
+
368
+ if (missingRequired.length === 0 && !action.artifactKind) {
369
+ actions.push({
370
+ label: state.gate.accepted
371
+ ? action.label
372
+ : `Accept and continue · ${state.gate.title}`,
373
+ run: async () => acceptAndContinue(ctx),
374
+ });
375
+ }
376
+
377
+ if (
378
+ state.allowed_ai_capabilities.length > 0
379
+ && action.actor !== "ai"
380
+ && !missingRequired.some((artifact) => artifact.actor === "ai")
381
+ ) {
382
+ actions.push({
383
+ label: `Ask AI to assist · ${state.phase.title}`,
384
+ run: async () => requestAiAssistance(pi, state),
385
+ });
386
+ }
387
+
388
+ if (state.return_targets.length > 0) {
389
+ actions.push({
390
+ label: "Return to an earlier phase",
391
+ run: async () => returnToEarlierPhase(ctx, ""),
392
+ });
393
+ }
394
+
395
+ actions.push({
396
+ label: "Explain this phase and its expectations",
397
+ run: async () => {
398
+ ctx.ui.notify(
399
+ [
400
+ `${state.phase.title} · Human leads, AI assists`,
401
+ `Goal: ${guidance.objective}`,
402
+ `You: ${guidance.human}`,
403
+ `AI: ${guidance.ai}`,
404
+ `Gate: ${state.gate.title}`,
405
+ ].join("\n"),
406
+ "info",
407
+ );
408
+ },
409
+ });
410
+
411
+ const selected = await ctx.ui.select(
412
+ `AHEAD mode · ${state.phase.title}\nNext (${action.actor === "human" ? "you" : "AI"}): ${action.label}`,
413
+ actions.map((candidate) => candidate.label),
414
+ );
415
+ const chosen = actions.find((candidate) => candidate.label === selected);
416
+ if (chosen) await chosen.run();
417
+ }
418
+
419
+ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): Promise<Run | undefined> {
420
+ const engine = await enginePromise;
421
+ const store = storeFor(ctx);
422
+ const current = await store.loadCurrent();
423
+ if (current && !engine.deriveState(current).closed) {
424
+ throw new AheadEngineError(
425
+ "active_run_exists",
426
+ `run ${current.id} is still active; use /ahead to continue it`,
427
+ );
428
+ }
429
+
430
+ const title = requestedTitle.trim()
431
+ || (ctx.hasUI ? await ctx.ui.input("Enter AHEAD mode · Product Change", "What work are you doing?") : undefined);
432
+ if (!title?.trim()) return undefined;
433
+
434
+ const owner = humanActor(store.projectRoot);
435
+ const run = engine.createRun({
436
+ id: store.newRunId(),
437
+ title: title.trim(),
438
+ owner,
439
+ timestamp: new Date().toISOString(),
440
+ workflow_id: "product-change",
441
+ });
442
+ await store.save(run);
443
+ await refreshUi(ctx, run);
444
+ ctx.ui.notify(
445
+ [
446
+ `AHEAD mode started · ${run.title}`,
447
+ "Human leads · AI assists",
448
+ "This run remains active in the repository until an accountable human closes the outcome.",
449
+ "Use /ahead for the next guided action; use normal conversation to think and work with AI.",
450
+ ].join("\n"),
451
+ "info",
452
+ );
453
+ return run;
454
+ }
455
+
456
+ async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind: string): Promise<void> {
457
+ if (!ctx.hasUI) throw new Error("Recording a human artifact requires interactive or RPC UI support");
458
+ const engine = await enginePromise;
459
+ const store = storeFor(ctx);
460
+ const run = await requireRun(ctx);
461
+ const state = engine.deriveState(run);
462
+ const allowed = state.artifacts.filter((artifact) => artifact.actor !== "ai" && !artifact.present);
463
+ let kind = requestedKind.trim();
464
+ if (!kind) {
465
+ kind = (await ctx.ui.select(
466
+ `AHEAD mode · Write for ${state.phase.title}`,
467
+ allowed.map((artifact) => artifact.title),
468
+ )) ?? "";
469
+ kind = allowed.find((artifact) => artifact.title === kind)?.kind ?? kind;
470
+ }
471
+ const artifact = allowed.find((candidate) => candidate.kind === kind);
472
+ if (!artifact) {
473
+ throw new AheadEngineError(
474
+ "artifact_not_human_owned",
475
+ `there is no unrecorded human artifact named ${kind || "that"} in ${state.phase.title}`,
476
+ );
477
+ }
478
+
479
+ const content = await ctx.ui.editor(
480
+ `AHEAD mode · ${artifact.title} · write in your own words`,
481
+ buildArtifactTemplate(run, state, artifact.kind, artifact.title),
482
+ );
483
+ if (!content?.trim()) return;
484
+ const path = store.artifactPath(run, state.phase.id, artifact.kind);
485
+ const action: EventAction = {
486
+ type: "artifact_recorded",
487
+ phase: state.phase.id,
488
+ kind: artifact.kind,
489
+ path: path.relative,
490
+ };
491
+ const updated = engine.applyEvent(run, humanActor(store.projectRoot), action);
492
+ await store.writeArtifact(path.absolute, content);
493
+ await store.save(updated);
494
+ await refreshUi(ctx, updated);
495
+ ctx.ui.notify(
496
+ `Saved ${artifact.title}. AHEAD mode remains active; continue the conversation or run /ahead for the next guided action.`,
497
+ "info",
498
+ );
499
+ }
500
+
501
+ function requestAiAssistance(pi: ExtensionAPI, state: RunState, requiredKind?: string): void {
502
+ const guidance = phaseGuide(state.phase.id);
503
+ const artifact = requiredKind
504
+ ? state.artifacts.find((candidate) => candidate.kind === requiredKind)
505
+ : undefined;
506
+ const request = artifact
507
+ ? [
508
+ `AHEAD mode: perform the ${artifact.required ? "required" : "recommended"} ${state.phase.title} work for the exact current evidence and changeset.`,
509
+ `Produce ${artifact.title}.`,
510
+ `Follow the active human/AI boundary: ${guidance.ai}`,
511
+ `Use ahead_get_context first, then record the completed artifact as ${artifact.kind} with ahead_record_artifact.`,
512
+ "Treat findings as hypotheses for human disposition. Do not accept the gate or transition the run.",
513
+ ].join("\n")
514
+ : [
515
+ `AHEAD mode: assist with the active ${state.phase.title} phase.`,
516
+ guidance.ai,
517
+ "Use ahead_get_context before acting. Stay within the allowed capabilities and never author a human-owned artifact or decision.",
518
+ "Explain what you found and what the human must understand or decide next.",
519
+ ].join("\n");
520
+ pi.sendUserMessage(request);
521
+ }
522
+
523
+ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
524
+ if (!ctx.hasUI) throw new Error("Accepting an AHEAD gate requires interactive or RPC UI support");
525
+ const engine = await enginePromise;
526
+ const store = storeFor(ctx);
527
+ const run = await requireRun(ctx);
528
+ let state = engine.deriveState(run);
529
+ const missing = state.artifacts.filter((artifact) => artifact.required && !artifact.present);
530
+ if (missing.length > 0) {
531
+ throw new AheadEngineError(
532
+ "required_artifact_missing",
533
+ `complete first: ${missing.map((artifact) => artifact.title).join(", ")}`,
534
+ );
535
+ }
536
+
537
+ const destination = state.phase.next
538
+ ? engine.getWorkflow(run.workflow_id).phases.find((phase) => phase.id === state.phase.next)?.title ?? state.phase.next
539
+ : "close this AHEAD run";
540
+ const confirmed = await ctx.ui.confirm(
541
+ `Accept and continue from ${state.phase.title}?`,
542
+ [
543
+ state.gate.title,
544
+ "",
545
+ `Next: ${destination}`,
546
+ `Accountable human: ${humanActor(store.projectRoot).identity}`,
547
+ "",
548
+ "This records human acceptance. AI cannot perform this action.",
549
+ ].join("\n"),
550
+ );
551
+ if (!confirmed) return;
552
+
553
+ const actor = humanActor(store.projectRoot);
554
+ let updated = run;
555
+ if (!state.gate.accepted) {
556
+ updated = engine.applyEvent(updated, actor, {
557
+ type: "gate_accepted",
558
+ phase: state.phase.id,
559
+ gate: state.gate.id,
560
+ });
561
+ state = engine.deriveState(updated);
562
+ }
563
+ if (!state.can_advance) {
564
+ throw new AheadEngineError("cannot_advance", state.blockers.join("; ") || "the phase cannot advance");
565
+ }
566
+
567
+ const action: EventAction = state.phase.next
568
+ ? {
569
+ type: "phase_transitioned",
570
+ from: state.phase.id,
571
+ to: state.phase.next,
572
+ direction: "advance",
573
+ }
574
+ : { type: "run_closed", phase: state.phase.id };
575
+ updated = engine.applyEvent(updated, actor, action);
576
+ await store.save(updated);
577
+ await refreshUi(ctx, updated);
578
+
579
+ const nextState = engine.deriveState(updated);
580
+ if (nextState.closed) {
581
+ ctx.ui.notify("AHEAD work complete. The accountable human accepted the outcome and closed the run.", "info");
582
+ } else if (nextState.phase.id === "human-review") {
583
+ ctx.ui.notify(
584
+ [
585
+ "READY FOR INDEPENDENT HUMAN REVIEW",
586
+ "The AI review is recorded and its material findings were disposed by a human.",
587
+ "A draft branch may already exist, but a human must now request review or mark the PR ready.",
588
+ "The independent reviewer opens this repository, runs /ahead, and records the review.",
589
+ ].join("\n"),
590
+ "info",
591
+ );
592
+ } else {
593
+ ctx.ui.notify(
594
+ `Continued to ${nextState.phase.title}. AHEAD mode remains active; run /ahead for the next guided action.`,
595
+ "info",
596
+ );
597
+ }
598
+ }
599
+
600
+ async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarget: string): Promise<void> {
601
+ if (!ctx.hasUI) throw new Error("Returning an AHEAD phase requires interactive or RPC UI support");
602
+ const engine = await enginePromise;
603
+ const store = storeFor(ctx);
604
+ const run = await requireRun(ctx);
605
+ const state = engine.deriveState(run);
606
+ if (!state.return_targets.length) {
607
+ throw new AheadEngineError("no_return_target", `phase ${state.phase.id} has no return transition`);
608
+ }
609
+ const workflow = engine.getWorkflow(run.workflow_id);
610
+ const targetOptions = state.return_targets.map((target) => ({
611
+ id: target,
612
+ title: workflow.phases.find((phase) => phase.id === target)?.title ?? target,
613
+ }));
614
+ let target = requestedTarget.trim();
615
+ if (!target) {
616
+ const selected = await ctx.ui.select(
617
+ "Return to which phase?",
618
+ targetOptions.map((candidate) => candidate.title),
619
+ );
620
+ target = targetOptions.find((candidate) => candidate.title === selected)?.id ?? "";
621
+ }
622
+ if (!state.return_targets.includes(target)) {
623
+ throw new AheadEngineError(
624
+ "invalid_return",
625
+ `phase ${state.phase.id} can return only to: ${state.return_targets.join(", ")}`,
626
+ );
627
+ }
628
+ const reason = await ctx.ui.editor(
629
+ `Why return to ${workflow.phases.find((phase) => phase.id === target)?.title ?? target}?`,
630
+ );
631
+ if (!reason?.trim()) return;
632
+ const confirmed = await ctx.ui.confirm(
633
+ `Return to ${target}?`,
634
+ "This opens a new phase visit. Earlier artifacts remain as history but cannot satisfy the reopened gate.",
635
+ );
636
+ if (!confirmed) return;
637
+ const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
638
+ type: "phase_transitioned",
639
+ from: state.phase.id,
640
+ to: target,
641
+ direction: "return",
642
+ reason: reason.trim(),
643
+ });
644
+ await store.save(updated);
645
+ await refreshUi(ctx, updated);
646
+ ctx.ui.notify(`Returned to ${target}. AHEAD mode remains active with fresh evidence and gate requirements.`, "warning");
647
+ }
648
+
356
649
  function storeFor(ctx: ExtensionContext): RunStore {
357
650
  return new RunStore(projectRoot(ctx.cwd));
358
651
  }
359
652
 
360
653
  async function requireRun(ctx: ExtensionContext): Promise<Run> {
361
654
  const run = await storeFor(ctx).loadCurrent();
362
- if (!run) throw new AheadEngineError("no_active_run", "no active AHEAD run; use /ahead-start [title]");
655
+ if (!run) throw new AheadEngineError("no_active_run", "no active AHEAD run; use /ahead [title]");
363
656
  return run;
364
657
  }
365
658
 
@@ -378,26 +671,20 @@ async function refreshUi(ctx: ExtensionContext, supplied?: Run): Promise<void> {
378
671
  ctx.ui.setWidget("ahead", undefined);
379
672
  return;
380
673
  }
381
- const state = (await enginePromise).deriveState(run);
674
+ const engine = await enginePromise;
675
+ const state = engine.deriveState(run);
676
+ const workflow = engine.getWorkflow(run.workflow_id);
677
+ const position = phasePosition(state, workflow);
678
+ const action = nextAction(state, workflow);
382
679
  ctx.ui.setStatus(
383
680
  "ahead",
384
681
  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"}`,
682
+ ? `AHEAD · complete · ${state.workflow_id}`
683
+ : `AHEAD · ${position.current}/${position.total} · ${state.phase.id} · ${action.actor} action`,
387
684
  );
388
685
  ctx.ui.setWidget(
389
686
  "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
- ],
687
+ buildWidgetLines(run, state, workflow),
401
688
  { placement: "aboveEditor" },
402
689
  );
403
690
  }
@@ -426,19 +713,6 @@ function formatState(state: RunState): string {
426
713
  ].join("\n");
427
714
  }
428
715
 
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
716
  async function command(ctx: ExtensionCommandContext, action: () => Promise<void>): Promise<void> {
443
717
  try {
444
718
  await action();