ahead-pi 0.2.1 → 0.3.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.
- package/README.md +26 -41
- package/dist/ahead_wasm.wasm +0 -0
- package/generated/corrective-debugging/ai-audit.md +39 -0
- package/generated/corrective-debugging/ai-review.md +46 -0
- package/generated/corrective-debugging/characterize.md +53 -0
- package/generated/corrective-debugging/conclude.md +59 -0
- package/generated/corrective-debugging/correction.md +53 -0
- package/generated/corrective-debugging/deploy.md +38 -0
- package/generated/corrective-debugging/human-review.md +45 -0
- package/generated/corrective-debugging/implement.md +42 -0
- package/generated/corrective-debugging/investigate.md +59 -0
- package/generated/corrective-debugging/manifest.json +30 -0
- package/generated/corrective-debugging/model.md +53 -0
- package/generated/corrective-debugging/outcome.md +38 -0
- package/generated/corrective-debugging/plan.md +53 -0
- package/generated/corrective-debugging/verify.md +47 -0
- package/generated/decision/compare.md +45 -0
- package/generated/decision/criteria.md +45 -0
- package/generated/decision/decide.md +45 -0
- package/generated/decision/frame.md +45 -0
- package/generated/decision/manifest.json +21 -0
- package/generated/decision/options.md +47 -0
- package/generated/decision/publish.md +38 -0
- package/generated/decision/research.md +45 -0
- package/generated/internal-improvement/ai-audit.md +39 -0
- package/generated/internal-improvement/ai-review.md +46 -0
- package/generated/internal-improvement/baseline.md +46 -0
- package/generated/internal-improvement/decision.md +45 -0
- package/generated/internal-improvement/deploy.md +38 -0
- package/generated/internal-improvement/human-review.md +45 -0
- package/generated/internal-improvement/implement.md +42 -0
- package/generated/internal-improvement/invariants.md +38 -0
- package/generated/internal-improvement/manifest.json +29 -0
- package/generated/internal-improvement/options.md +47 -0
- package/generated/internal-improvement/outcome.md +38 -0
- package/generated/internal-improvement/plan.md +53 -0
- package/generated/internal-improvement/target.md +45 -0
- package/generated/internal-improvement/verify.md +45 -0
- package/generated/investigation/bound.md +45 -0
- package/generated/investigation/conclude.md +45 -0
- package/generated/investigation/explore.md +60 -0
- package/generated/investigation/frame.md +45 -0
- package/generated/investigation/gather.md +45 -0
- package/generated/investigation/manifest.json +21 -0
- package/generated/investigation/synthesize.md +51 -0
- package/generated/operational-stabilization/assess.md +46 -0
- package/generated/operational-stabilization/execute-observe.md +45 -0
- package/generated/operational-stabilization/manifest.json +19 -0
- package/generated/operational-stabilization/monitor.md +45 -0
- package/generated/operational-stabilization/outcome.md +38 -0
- package/generated/operational-stabilization/respond.md +40 -0
- package/generated/operational-stabilization/verify-recovery.md +45 -0
- package/generated/product-change/ai-audit.md +7 -4
- package/generated/product-change/ai-review.md +15 -5
- package/generated/product-change/decision.md +11 -2
- package/generated/product-change/define.md +4 -2
- package/generated/product-change/deploy.md +4 -2
- package/generated/product-change/human-review.md +11 -2
- package/generated/product-change/implement.md +4 -2
- package/generated/product-change/manifest.json +8 -3
- package/generated/product-change/options.md +11 -2
- package/generated/product-change/outcome.md +4 -2
- package/generated/product-change/plan.md +17 -2
- package/generated/product-change/questions.md +17 -2
- package/generated/product-change/research.md +11 -2
- package/generated/product-change/verify.md +4 -2
- package/generated/recommended-skills.json +24 -0
- package/generated/reference/CONSTITUTION.md +2 -0
- package/generated/reference/docs/evidence/README.md +17 -0
- package/generated/reference/docs/evidence/evidence-standard.md +2 -0
- package/generated/reference/docs/evidence/research-map.md +2 -0
- package/generated/reference/docs/{references → evidence/sources}/pragmatic-programmer-page-index.md +3 -1
- package/generated/reference/docs/{references → evidence/sources}/submitted-engineering-notes.md +3 -1
- package/generated/reference/docs/guide/README.md +28 -0
- package/generated/reference/docs/{acceptable-ai-use.md → guide/acceptable-ai-use.md} +4 -2
- package/generated/reference/docs/{engineering-practice.md → guide/engineering-practice.md} +5 -3
- package/generated/reference/docs/{rationale.md → guide/rationale.md} +3 -1
- package/generated/reference/docs/guide/recommended-skills.md +21 -0
- package/generated/reference/docs/{workflows → guide/workflows}/README.md +6 -4
- package/generated/reference/docs/{workflows → guide/workflows}/corrective-debugging.md +39 -19
- package/generated/reference/docs/{workflows → guide/workflows}/decision.md +4 -2
- package/generated/reference/docs/{workflows → guide/workflows}/internal-improvement.md +37 -23
- package/generated/reference/docs/{workflows → guide/workflows}/investigation.md +5 -1
- package/generated/reference/docs/{workflows → guide/workflows}/operational-stabilization.md +16 -12
- package/generated/reference/docs/{workflows → guide/workflows}/product-change.md +16 -3
- package/generated/reference/index.json +200 -87
- package/package.json +34 -25
- package/src/engine.ts +27 -8
- package/src/flow-guides.ts +168 -0
- package/src/guidance.ts +220 -78
- package/src/index.ts +852 -189
- package/src/reference-viewer.ts +20 -18
- package/src/reference.ts +76 -14
- package/src/review.ts +360 -0
- package/src/skills.ts +133 -0
- package/src/storage.ts +139 -15
- package/src/types.ts +1 -0
- package/generated/reference/docs/design/debugging-and-operations.md +0 -119
- package/generated/reference/docs/design/executable-workflows.md +0 -110
- package/generated/reference/docs/design/process-taxonomy.md +0 -144
- package/generated/reference/docs/releasing-pi.md +0 -89
package/src/index.ts
CHANGED
|
@@ -4,12 +4,14 @@ import type {
|
|
|
4
4
|
ExtensionAPI,
|
|
5
5
|
ExtensionCommandContext,
|
|
6
6
|
ExtensionContext,
|
|
7
|
+
Theme,
|
|
7
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
8
10
|
import { Type } from "typebox";
|
|
9
11
|
import { AheadEngine, AheadEngineError } from "./engine.js";
|
|
10
12
|
import {
|
|
11
13
|
buildArtifactTemplate,
|
|
12
|
-
|
|
14
|
+
buildHeaderLines,
|
|
13
15
|
nextAction,
|
|
14
16
|
phaseGuide,
|
|
15
17
|
phasePosition,
|
|
@@ -19,14 +21,31 @@ import {
|
|
|
19
21
|
loadReferenceIndex,
|
|
20
22
|
readReference,
|
|
21
23
|
relevantReferences,
|
|
24
|
+
type ReferenceEntry,
|
|
22
25
|
} from "./reference.js";
|
|
23
26
|
import { showReferenceViewer } from "./reference-viewer.js";
|
|
27
|
+
import {
|
|
28
|
+
collectReviewSnapshot,
|
|
29
|
+
extractFindingIds,
|
|
30
|
+
extractReviewFingerprint,
|
|
31
|
+
openInConfiguredEditor,
|
|
32
|
+
reviewDispositionTemplate,
|
|
33
|
+
reviewRequest,
|
|
34
|
+
reviewSnapshotMarkdown,
|
|
35
|
+
validateAiReviewArtifact,
|
|
36
|
+
validateReviewDisposition,
|
|
37
|
+
} from "./review.js";
|
|
38
|
+
import {
|
|
39
|
+
loadRecommendedSkills,
|
|
40
|
+
recommendedSkillsMarkdown,
|
|
41
|
+
relevantRecommendedSkills,
|
|
42
|
+
} from "./skills.js";
|
|
24
43
|
import { humanActor, projectRoot, RunStore } from "./storage.js";
|
|
25
44
|
import type { Actor, Capability, EventAction, Run, RunState } from "./types.js";
|
|
26
45
|
|
|
27
46
|
const wasmPath =
|
|
28
47
|
process.env.AHEAD_WASM_PATH || fileURLToPath(new URL("../dist/ahead_wasm.wasm", import.meta.url));
|
|
29
|
-
const instructionDirectory = fileURLToPath(new URL("../generated
|
|
48
|
+
const instructionDirectory = fileURLToPath(new URL("../generated", import.meta.url));
|
|
30
49
|
const enginePromise = AheadEngine.load(wasmPath);
|
|
31
50
|
const instructions = new Map<string, string>();
|
|
32
51
|
|
|
@@ -46,106 +65,161 @@ const RecordArtifactParams = Type.Object({
|
|
|
46
65
|
content: Type.String({ description: "Complete Markdown artifact content", maxLength: 100_000 }),
|
|
47
66
|
});
|
|
48
67
|
const ReferenceParams = Type.Object({
|
|
49
|
-
topic: Type.Optional(
|
|
68
|
+
topic: Type.Optional(
|
|
69
|
+
Type.String({
|
|
70
|
+
description: "Reference id, path, or title; omit to list phase-relevant references",
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
50
73
|
});
|
|
51
74
|
|
|
52
75
|
export default function aheadExtension(pi: ExtensionAPI): void {
|
|
53
76
|
pi.registerCommand("ahead", {
|
|
54
|
-
description: "Enter or
|
|
55
|
-
handler: async (args, ctx) =>
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
description: "Enter AHEAD mode or open its action menu",
|
|
78
|
+
handler: async (args, ctx) =>
|
|
79
|
+
command(ctx, async () => {
|
|
80
|
+
await openAheadMode(pi, args, ctx);
|
|
81
|
+
}),
|
|
58
82
|
});
|
|
59
83
|
|
|
60
84
|
pi.registerCommand("ahead-guide", {
|
|
61
85
|
description: "Read the AHEAD framework guidance relevant to the active phase",
|
|
62
|
-
handler: async (args, ctx) =>
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
handler: async (args, ctx) =>
|
|
87
|
+
command(ctx, async () => {
|
|
88
|
+
await showAheadGuide(ctx, args);
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
pi.registerCommand("ahead-skills", {
|
|
93
|
+
description: "Inspect optional skills reviewed for the active AHEAD phase",
|
|
94
|
+
handler: async (_args, ctx) =>
|
|
95
|
+
command(ctx, async () => {
|
|
96
|
+
await showRecommendedSkills(ctx);
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
pi.registerCommand("ahead-review", {
|
|
101
|
+
description: "Inspect and perform the current changeset review handoff",
|
|
102
|
+
handler: async (_args, ctx) =>
|
|
103
|
+
command(ctx, async () => {
|
|
104
|
+
await openReviewWorkbench(pi, ctx);
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
pi.registerCommand("ahead-stop", {
|
|
109
|
+
description: "Exit AHEAD mode; discard the unfinished record or explicitly save it",
|
|
110
|
+
handler: async (_args, ctx) =>
|
|
111
|
+
command(ctx, async () => {
|
|
112
|
+
await stopAheadMode(ctx);
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
pi.registerCommand("ahead-resume", {
|
|
117
|
+
description: "Resume unfinished AHEAD work that was explicitly saved",
|
|
118
|
+
handler: async (args, ctx) =>
|
|
119
|
+
command(ctx, async () => {
|
|
120
|
+
await resumeSavedRun(ctx, args.trim());
|
|
121
|
+
}),
|
|
65
122
|
});
|
|
66
123
|
|
|
67
124
|
pi.registerCommand("ahead-start", {
|
|
68
|
-
description: "Advanced: start
|
|
69
|
-
handler: async (args, ctx) =>
|
|
70
|
-
|
|
71
|
-
|
|
125
|
+
description: "Advanced: start directly with <workflow-id> :: <title>",
|
|
126
|
+
handler: async (args, ctx) =>
|
|
127
|
+
command(ctx, async () => {
|
|
128
|
+
await startRun(ctx, args);
|
|
129
|
+
}),
|
|
72
130
|
});
|
|
73
131
|
|
|
74
132
|
pi.registerCommand("ahead-status", {
|
|
75
133
|
description: "Advanced: show the raw active AHEAD phase contract",
|
|
76
|
-
handler: async (_args, ctx) =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
134
|
+
handler: async (_args, ctx) =>
|
|
135
|
+
command(ctx, async () => {
|
|
136
|
+
const run = await requireRun(ctx);
|
|
137
|
+
const state = (await enginePromise).deriveState(run);
|
|
138
|
+
await refreshUi(ctx, run);
|
|
139
|
+
ctx.ui.notify(formatState(state), state.blockers.length ? "warning" : "info");
|
|
140
|
+
}),
|
|
82
141
|
});
|
|
83
142
|
|
|
84
143
|
pi.registerCommand("ahead-record", {
|
|
85
144
|
description: "Advanced: record a human-owned artifact directly",
|
|
86
|
-
handler: async (args, ctx) =>
|
|
87
|
-
|
|
88
|
-
|
|
145
|
+
handler: async (args, ctx) =>
|
|
146
|
+
command(ctx, async () => {
|
|
147
|
+
await recordHumanArtifact(ctx, args.trim());
|
|
148
|
+
}),
|
|
89
149
|
});
|
|
90
150
|
|
|
91
151
|
pi.registerCommand("ahead-accept", {
|
|
92
152
|
description: "Advanced: accept the active gate without advancing",
|
|
93
|
-
handler: async (_args, ctx) =>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
153
|
+
handler: async (_args, ctx) =>
|
|
154
|
+
command(ctx, async () => {
|
|
155
|
+
if (!ctx.hasUI) {
|
|
156
|
+
throw new Error("/ahead-accept requires interactive or RPC UI support");
|
|
157
|
+
}
|
|
158
|
+
const engine = await enginePromise;
|
|
159
|
+
const store = storeFor(ctx);
|
|
160
|
+
const run = await requireRun(ctx);
|
|
161
|
+
const state = engine.deriveState(run);
|
|
162
|
+
const confirmed = await ctx.ui.confirm(
|
|
163
|
+
`Accept ${state.gate.id}?`,
|
|
164
|
+
`${state.gate.title}\n\nThis records human acceptance as ${humanActor(store.projectRoot).identity}.`,
|
|
165
|
+
);
|
|
166
|
+
if (!confirmed) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
|
|
170
|
+
type: "gate_accepted",
|
|
171
|
+
phase: state.phase.id,
|
|
172
|
+
gate: state.gate.id,
|
|
173
|
+
});
|
|
174
|
+
await store.save(updated);
|
|
175
|
+
await refreshUi(ctx, updated);
|
|
176
|
+
ctx.ui.notify(`Accepted gate ${state.gate.id}. Use /ahead-advance when ready.`, "info");
|
|
177
|
+
}),
|
|
113
178
|
});
|
|
114
179
|
|
|
115
180
|
pi.registerCommand("ahead-advance", {
|
|
116
181
|
description: "Advanced: advance an already accepted gate",
|
|
117
|
-
handler: async (_args, ctx) =>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
state
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
182
|
+
handler: async (_args, ctx) =>
|
|
183
|
+
command(ctx, async () => {
|
|
184
|
+
if (!ctx.hasUI) {
|
|
185
|
+
throw new Error("/ahead-advance requires interactive or RPC UI support");
|
|
186
|
+
}
|
|
187
|
+
const engine = await enginePromise;
|
|
188
|
+
const store = storeFor(ctx);
|
|
189
|
+
const run = await requireRun(ctx);
|
|
190
|
+
const state = engine.deriveState(run);
|
|
191
|
+
const destination = state.phase.next ?? "closed";
|
|
192
|
+
const confirmed = await ctx.ui.confirm(
|
|
193
|
+
state.phase.next ? `Advance to ${state.phase.next}?` : "Close this AHEAD run?",
|
|
194
|
+
`Current phase: ${state.phase.title}\nDestination: ${destination}\nActor: ${humanActor(store.projectRoot).identity}`,
|
|
195
|
+
);
|
|
196
|
+
if (!confirmed) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const action: EventAction = state.phase.next
|
|
200
|
+
? {
|
|
201
|
+
type: "phase_transitioned",
|
|
202
|
+
from: state.phase.id,
|
|
203
|
+
to: state.phase.next,
|
|
204
|
+
direction: "advance",
|
|
205
|
+
}
|
|
206
|
+
: { type: "run_closed", phase: state.phase.id };
|
|
207
|
+
const updated = engine.applyEvent(run, humanActor(store.projectRoot), action);
|
|
208
|
+
await store.save(updated);
|
|
209
|
+
await refreshUi(ctx, updated);
|
|
210
|
+
ctx.ui.notify(
|
|
211
|
+
state.phase.next ? `Advanced to ${state.phase.next}.` : "AHEAD run closed.",
|
|
212
|
+
"info",
|
|
213
|
+
);
|
|
214
|
+
}),
|
|
142
215
|
});
|
|
143
216
|
|
|
144
217
|
pi.registerCommand("ahead-return", {
|
|
145
218
|
description: "Advanced: return to an earlier phase with a reason",
|
|
146
|
-
handler: async (args, ctx) =>
|
|
147
|
-
|
|
148
|
-
|
|
219
|
+
handler: async (args, ctx) =>
|
|
220
|
+
command(ctx, async () => {
|
|
221
|
+
await returnToEarlierPhase(ctx, args);
|
|
222
|
+
}),
|
|
149
223
|
});
|
|
150
224
|
|
|
151
225
|
pi.registerCommand("ahead-help", {
|
|
@@ -153,12 +227,16 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
153
227
|
handler: async (_args, ctx) => {
|
|
154
228
|
ctx.ui.notify(
|
|
155
229
|
[
|
|
156
|
-
"/ahead [title] —
|
|
230
|
+
"/ahead [title] — choose a workflow for new work, or open the active action menu",
|
|
157
231
|
"/ahead-guide [topic] — read the applicable AHEAD framework Markdown",
|
|
232
|
+
"/ahead-skills — inspect optional reviewed skills relevant to this phase",
|
|
233
|
+
"/ahead-review — inspect the exact changeset and review handoff",
|
|
234
|
+
"/ahead-stop — leave AHEAD mode; discard the unfinished record by default or explicitly save it",
|
|
235
|
+
"/ahead-resume [run-id] — resume an unfinished run that you explicitly saved",
|
|
158
236
|
"",
|
|
159
|
-
"Once started, the repository run remains in AHEAD mode until an accountable human closes
|
|
160
|
-
"Use normal conversation to think and work with AI.
|
|
161
|
-
"The
|
|
237
|
+
"Once started, the repository run remains in AHEAD mode until an accountable human closes it or uses /ahead-stop.",
|
|
238
|
+
"Use normal conversation to think and work with AI. AHEAD remains active and guides every turn.",
|
|
239
|
+
"The compact header shows the goal, required evidence, and next owner. /ahead reopens the action menu when you need a recorded action.",
|
|
162
240
|
"",
|
|
163
241
|
"Advanced fallback commands: /ahead-status, /ahead-record, /ahead-accept, /ahead-advance, /ahead-return.",
|
|
164
242
|
"AI can record only AI/shared artifacts allowed in the active phase. It cannot accept gates, transition, approve, deploy, or close the run.",
|
|
@@ -171,7 +249,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
171
249
|
pi.registerTool({
|
|
172
250
|
name: "ahead_get_context",
|
|
173
251
|
label: "AHEAD context",
|
|
174
|
-
description:
|
|
252
|
+
description:
|
|
253
|
+
"Read the authoritative active AHEAD workflow state, phase contract, artifacts, gate, and blockers.",
|
|
175
254
|
promptSnippet: "Read the active AHEAD workflow state and human/AI boundaries.",
|
|
176
255
|
parameters: EmptyParams,
|
|
177
256
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
@@ -183,29 +262,80 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
183
262
|
},
|
|
184
263
|
});
|
|
185
264
|
|
|
265
|
+
pi.registerTool({
|
|
266
|
+
name: "ahead_get_recommended_skills",
|
|
267
|
+
label: "AHEAD recommended skills",
|
|
268
|
+
description:
|
|
269
|
+
"List optional third-party skills reviewed for the active phase. Never installs a skill.",
|
|
270
|
+
promptSnippet:
|
|
271
|
+
"Discover reviewed optional skills only when they may help the active AHEAD work.",
|
|
272
|
+
parameters: EmptyParams,
|
|
273
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
274
|
+
return toolResult(async () => {
|
|
275
|
+
const catalog = await loadRecommendedSkills();
|
|
276
|
+
const run = await storeFor(ctx).loadCurrent();
|
|
277
|
+
const state = run ? (await enginePromise).deriveState(run) : undefined;
|
|
278
|
+
const phaseId = state && !state.closed ? state.phase.id : undefined;
|
|
279
|
+
const workflowId = state && !state.closed ? state.workflow_id : undefined;
|
|
280
|
+
return {
|
|
281
|
+
reviewed_at: catalog.reviewed_at,
|
|
282
|
+
phase: phaseId ?? null,
|
|
283
|
+
workflow: workflowId ?? null,
|
|
284
|
+
recommended: relevantRecommendedSkills(catalog, workflowId, phaseId),
|
|
285
|
+
available: catalog.skills,
|
|
286
|
+
instruction:
|
|
287
|
+
"Do not install automatically. Explain why a skill applies, show the pinned source and command, and let the human opt in. AHEAD remains authoritative.",
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
pi.registerTool({
|
|
294
|
+
name: "ahead_get_review_snapshot",
|
|
295
|
+
label: "AHEAD review snapshot",
|
|
296
|
+
description:
|
|
297
|
+
"Capture the exact current Git changeset, merge base, status, paths, diff, and stable fingerprint without modifying it.",
|
|
298
|
+
promptSnippet: "Bind review findings to the exact current AHEAD changeset fingerprint.",
|
|
299
|
+
parameters: EmptyParams,
|
|
300
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
301
|
+
return toolResult(async () => collectReviewSnapshot(storeFor(ctx).projectRoot));
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
186
305
|
pi.registerTool({
|
|
187
306
|
name: "ahead_get_reference",
|
|
188
307
|
label: "AHEAD framework reference",
|
|
189
|
-
description:
|
|
190
|
-
|
|
308
|
+
description:
|
|
309
|
+
"List or read packaged AHEAD Constitution, philosophy, acceptable-use, engineering-practice, workflow, and evidence Markdown.",
|
|
310
|
+
promptSnippet:
|
|
311
|
+
"Retrieve relevant AHEAD framework guidance when the phase or policy is unclear.",
|
|
191
312
|
parameters: ReferenceParams,
|
|
192
313
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
193
314
|
return toolResult(async () => {
|
|
194
315
|
const run = await storeFor(ctx).loadCurrent();
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
316
|
+
const state = run ? (await enginePromise).deriveState(run) : undefined;
|
|
317
|
+
const phase = state && !state.closed ? state.phase.id : undefined;
|
|
318
|
+
const workflowId = state && !state.closed ? state.workflow_id : undefined;
|
|
198
319
|
if (!params.topic?.trim()) {
|
|
199
320
|
const index = await loadReferenceIndex();
|
|
200
321
|
return {
|
|
201
322
|
phase: phase ?? null,
|
|
202
|
-
|
|
203
|
-
|
|
323
|
+
workflow: workflowId ?? null,
|
|
324
|
+
recommended: await relevantReferences(workflowId, phase),
|
|
325
|
+
available: index.references.map(({ id, title, path, audience, authority }) => ({
|
|
326
|
+
id,
|
|
327
|
+
title,
|
|
328
|
+
path,
|
|
329
|
+
audience,
|
|
330
|
+
authority,
|
|
331
|
+
})),
|
|
204
332
|
instruction: "Request one reference by id, path, or title. Load only what is relevant.",
|
|
205
333
|
};
|
|
206
334
|
}
|
|
207
335
|
const entry = await findReference(params.topic);
|
|
208
|
-
if (!entry)
|
|
336
|
+
if (!entry) {
|
|
337
|
+
throw new Error(`No packaged AHEAD reference matches ${params.topic}`);
|
|
338
|
+
}
|
|
209
339
|
return { reference: entry, content: await readReference(entry) };
|
|
210
340
|
});
|
|
211
341
|
},
|
|
@@ -214,7 +344,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
214
344
|
pi.registerTool({
|
|
215
345
|
name: "ahead_record_artifact",
|
|
216
346
|
label: "Record AHEAD artifact",
|
|
217
|
-
description:
|
|
347
|
+
description:
|
|
348
|
+
"Persist an AI-owned or shared artifact permitted by the active phase. Cannot record human-owned artifacts.",
|
|
218
349
|
promptSnippet: "Record an AI-permitted artifact in the active AHEAD run.",
|
|
219
350
|
parameters: RecordArtifactParams,
|
|
220
351
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -230,6 +361,13 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
230
361
|
`AI cannot record ${params.kind} in phase ${state.phase.id}`,
|
|
231
362
|
);
|
|
232
363
|
}
|
|
364
|
+
if (artifact.kind === "ai-review") {
|
|
365
|
+
const snapshot = await collectReviewSnapshot(store.projectRoot);
|
|
366
|
+
const errors = validateAiReviewArtifact(params.content, snapshot.fingerprint);
|
|
367
|
+
if (errors.length) {
|
|
368
|
+
throw new AheadEngineError("invalid_ai_review", errors.join("; "));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
233
371
|
const path = store.artifactPath(run, state.phase.id, artifact.kind);
|
|
234
372
|
const updated = engine.applyEvent(run, aiActor(ctx), {
|
|
235
373
|
type: "artifact_recorded",
|
|
@@ -252,7 +390,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
252
390
|
pi.registerTool({
|
|
253
391
|
name: "ahead_request_transition",
|
|
254
392
|
label: "Request AHEAD transition",
|
|
255
|
-
description:
|
|
393
|
+
description:
|
|
394
|
+
"Report whether a human can advance the active AHEAD phase. This tool never accepts a gate or transitions state.",
|
|
256
395
|
parameters: EmptyParams,
|
|
257
396
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
258
397
|
return toolResult(async () => {
|
|
@@ -272,10 +411,14 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
272
411
|
pi.registerTool({
|
|
273
412
|
name: "ahead_validate",
|
|
274
413
|
label: "Validate AHEAD run",
|
|
275
|
-
description:
|
|
414
|
+
description:
|
|
415
|
+
"Replay and validate the active AHEAD event log against the embedded workflow contract.",
|
|
276
416
|
parameters: EmptyParams,
|
|
277
417
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
278
|
-
return toolResult(async () => ({
|
|
418
|
+
return toolResult(async () => ({
|
|
419
|
+
valid: true,
|
|
420
|
+
state: (await enginePromise).validateRun(await requireRun(ctx)),
|
|
421
|
+
}));
|
|
279
422
|
},
|
|
280
423
|
});
|
|
281
424
|
|
|
@@ -286,7 +429,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
286
429
|
if (run && !(await enginePromise).deriveState(run).closed && ctx.hasUI) {
|
|
287
430
|
const state = (await enginePromise).deriveState(run);
|
|
288
431
|
ctx.ui.notify(
|
|
289
|
-
`AHEAD mode resumed · ${state.phase.title}.
|
|
432
|
+
`AHEAD mode resumed · ${state.phase.title}. Continue in normal conversation; /ahead is available when you need the action menu.`,
|
|
290
433
|
"info",
|
|
291
434
|
);
|
|
292
435
|
}
|
|
@@ -298,17 +441,22 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
298
441
|
|
|
299
442
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
300
443
|
const run = await storeFor(ctx).loadCurrent();
|
|
301
|
-
if (!run)
|
|
444
|
+
if (!run) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
302
447
|
const engine = await enginePromise;
|
|
303
448
|
const state = engine.deriveState(run);
|
|
304
|
-
if (state.closed)
|
|
449
|
+
if (state.closed) {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
305
452
|
const workflow = engine.getWorkflow(run.workflow_id);
|
|
306
|
-
const guidance = phaseGuide(state.phase.id);
|
|
453
|
+
const guidance = phaseGuide(run.workflow_id, state.phase.id);
|
|
307
454
|
const action = nextAction(state, workflow);
|
|
308
|
-
const phaseInstructions = await loadInstructions(state.phase.id);
|
|
455
|
+
const phaseInstructions = await loadInstructions(run.workflow_id, state.phase.id);
|
|
309
456
|
const liveContext = [
|
|
310
457
|
"# Live AHEAD run",
|
|
311
458
|
`- Run: ${run.id} — ${run.title}`,
|
|
459
|
+
`- Workflow: ${workflow.title} (${workflow.id})`,
|
|
312
460
|
`- Phase: ${state.phase.id} visit ${state.phase.visit}`,
|
|
313
461
|
`- Gate accepted: ${state.gate.accepted}`,
|
|
314
462
|
`- Current blockers: ${state.blockers.length ? state.blockers.join("; ") : "none"}`,
|
|
@@ -325,15 +473,22 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
325
473
|
"- Treat AI review findings as hypotheses. Independent human review remains required for lasting engineering changes.",
|
|
326
474
|
"- 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
475
|
"- When AHEAD policy or rationale is unclear, use ahead_get_reference to retrieve only the applicable packaged Markdown.",
|
|
476
|
+
"- 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
477
|
].join("\n");
|
|
329
478
|
return { systemPrompt: `${event.systemPrompt}\n\n${phaseInstructions}\n\n${liveContext}\n` };
|
|
330
479
|
});
|
|
331
480
|
|
|
332
481
|
pi.on("tool_call", async (event, ctx) => {
|
|
333
|
-
if (event.toolName.startsWith("ahead_"))
|
|
482
|
+
if (event.toolName.startsWith("ahead_")) {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
334
485
|
const run = await storeFor(ctx).loadCurrent();
|
|
335
|
-
if (!run)
|
|
336
|
-
|
|
486
|
+
if (!run) {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
if ((await enginePromise).deriveState(run).closed) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
337
492
|
const capability = toolCapabilities[event.toolName];
|
|
338
493
|
if (!capability) {
|
|
339
494
|
return {
|
|
@@ -343,10 +498,13 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
343
498
|
}
|
|
344
499
|
try {
|
|
345
500
|
const decision = (await enginePromise).toolAllowed(run, capability);
|
|
346
|
-
if (!decision.allowed)
|
|
501
|
+
if (!decision.allowed) {
|
|
502
|
+
return { block: true, reason: `AHEAD: ${decision.reason}` };
|
|
503
|
+
}
|
|
347
504
|
} catch (error) {
|
|
348
505
|
return { block: true, reason: `AHEAD state validation failed: ${errorMessage(error)}` };
|
|
349
506
|
}
|
|
507
|
+
return undefined;
|
|
350
508
|
});
|
|
351
509
|
}
|
|
352
510
|
|
|
@@ -355,31 +513,59 @@ interface GuidedAction {
|
|
|
355
513
|
run: () => Promise<void>;
|
|
356
514
|
}
|
|
357
515
|
|
|
358
|
-
async function openAheadMode(
|
|
516
|
+
async function openAheadMode(
|
|
517
|
+
pi: ExtensionAPI,
|
|
518
|
+
args: string,
|
|
519
|
+
ctx: ExtensionCommandContext,
|
|
520
|
+
): Promise<void> {
|
|
359
521
|
const engine = await enginePromise;
|
|
360
522
|
const store = storeFor(ctx);
|
|
361
523
|
let run = await store.loadCurrent();
|
|
362
524
|
|
|
363
525
|
if (run && engine.deriveState(run).closed) {
|
|
364
526
|
if (!ctx.hasUI) {
|
|
365
|
-
ctx.ui.notify(
|
|
527
|
+
ctx.ui.notify(
|
|
528
|
+
"The current AHEAD run is complete. Start new work in an interactive Pi session.",
|
|
529
|
+
"info",
|
|
530
|
+
);
|
|
366
531
|
return;
|
|
367
532
|
}
|
|
368
533
|
const choice = await ctx.ui.select("AHEAD work is complete", [
|
|
369
|
-
"Start
|
|
534
|
+
"Start new AHEAD work",
|
|
370
535
|
"View the completed run",
|
|
371
536
|
]);
|
|
372
537
|
if (choice === "View the completed run") {
|
|
373
538
|
ctx.ui.notify(formatState(engine.deriveState(run)), "info");
|
|
374
539
|
return;
|
|
375
540
|
}
|
|
376
|
-
if (choice !== "Start
|
|
541
|
+
if (choice !== "Start new AHEAD work") {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
377
544
|
run = undefined;
|
|
378
545
|
}
|
|
379
546
|
|
|
547
|
+
if (!run) {
|
|
548
|
+
if (!args.trim() && ctx.hasUI) {
|
|
549
|
+
const saved = await loadResumableRuns(ctx);
|
|
550
|
+
if (saved.length > 0) {
|
|
551
|
+
const choice = await ctx.ui.select("No active AHEAD run", [
|
|
552
|
+
"Resume explicitly saved AHEAD work",
|
|
553
|
+
"Start new AHEAD work",
|
|
554
|
+
]);
|
|
555
|
+
if (choice === "Resume explicitly saved AHEAD work") {
|
|
556
|
+
run = await resumeSavedRun(ctx, "", saved);
|
|
557
|
+
} else if (choice !== "Start new AHEAD work") {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
380
564
|
if (!run) {
|
|
381
565
|
run = await startRun(ctx, args);
|
|
382
|
-
if (!run)
|
|
566
|
+
if (!run) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
383
569
|
}
|
|
384
570
|
|
|
385
571
|
await refreshUi(ctx, run);
|
|
@@ -390,43 +576,60 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
|
|
|
390
576
|
|
|
391
577
|
const state = engine.deriveState(run);
|
|
392
578
|
const workflow = engine.getWorkflow(run.workflow_id);
|
|
393
|
-
const guidance = phaseGuide(state.phase.id);
|
|
579
|
+
const guidance = phaseGuide(run.workflow_id, state.phase.id);
|
|
394
580
|
const action = nextAction(state, workflow);
|
|
395
581
|
const actions: GuidedAction[] = [];
|
|
396
|
-
const missingRequired = state.artifacts.filter(
|
|
582
|
+
const missingRequired = state.artifacts.filter(
|
|
583
|
+
(artifact) => artifact.required && !artifact.present,
|
|
584
|
+
);
|
|
397
585
|
if (action.artifactKind) {
|
|
586
|
+
const actionArtifact = state.artifacts.find(
|
|
587
|
+
(artifact) => artifact.kind === action.artifactKind,
|
|
588
|
+
);
|
|
398
589
|
actions.push({
|
|
399
590
|
label: action.label,
|
|
400
|
-
run:
|
|
401
|
-
|
|
402
|
-
|
|
591
|
+
run:
|
|
592
|
+
action.actor === "ai"
|
|
593
|
+
? state.phase.id === "ai-review"
|
|
594
|
+
? async () => openReviewWorkbench(pi, ctx)
|
|
595
|
+
: async () => requestAiAssistance(pi, state, action.artifactKind)
|
|
596
|
+
: async () => recordHumanArtifact(ctx, action.artifactKind ?? ""),
|
|
403
597
|
});
|
|
598
|
+
if (action.actor === "ai" && actionArtifact?.actor === "any") {
|
|
599
|
+
actions.push({
|
|
600
|
+
label: `Write ${actionArtifact.title} yourself`,
|
|
601
|
+
run: async () => recordHumanArtifact(ctx, actionArtifact.kind),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
404
604
|
}
|
|
405
605
|
|
|
406
606
|
if (action.optional) {
|
|
407
|
-
const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor
|
|
607
|
+
const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor !== "ai");
|
|
408
608
|
if (nextHumanArtifact) {
|
|
409
609
|
actions.push({
|
|
410
610
|
label: `Continue without optional AI challenge · Write ${nextHumanArtifact.title}`,
|
|
411
611
|
run: async () => recordHumanArtifact(ctx, nextHumanArtifact.kind),
|
|
412
612
|
});
|
|
613
|
+
} else if (missingRequired.length === 0) {
|
|
614
|
+
actions.push({
|
|
615
|
+
label: `Continue without optional AI contribution · Accept ${state.gate.title}`,
|
|
616
|
+
run: async () => acceptAndContinue(ctx),
|
|
617
|
+
});
|
|
413
618
|
}
|
|
414
619
|
}
|
|
415
620
|
|
|
416
621
|
if (missingRequired.length === 0 && !action.artifactKind) {
|
|
417
622
|
actions.push({
|
|
418
|
-
label: state.gate.accepted
|
|
419
|
-
? action.label
|
|
420
|
-
: `Accept and continue · ${state.gate.title}`,
|
|
623
|
+
label: state.gate.accepted ? action.label : `Accept and continue · ${state.gate.title}`,
|
|
421
624
|
run: async () => acceptAndContinue(ctx),
|
|
422
625
|
});
|
|
423
626
|
}
|
|
424
627
|
|
|
425
628
|
if (
|
|
426
|
-
state.allowed_ai_capabilities.length > 0
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
629
|
+
state.allowed_ai_capabilities.length > 0 &&
|
|
630
|
+
action.actor !== "ai" &&
|
|
631
|
+
!missingRequired.some((artifact) => artifact.actor === "ai") &&
|
|
632
|
+
state.phase.id !== "implement"
|
|
430
633
|
) {
|
|
431
634
|
actions.push({
|
|
432
635
|
label: `Ask AI to assist · ${state.phase.title}`,
|
|
@@ -441,6 +644,13 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
|
|
|
441
644
|
});
|
|
442
645
|
}
|
|
443
646
|
|
|
647
|
+
if (state.phase.id === "ai-review" || state.phase.id === "human-review") {
|
|
648
|
+
actions.push({
|
|
649
|
+
label: "Open the changeset review workbench",
|
|
650
|
+
run: async () => openReviewWorkbench(pi, ctx),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
444
654
|
if (state.return_targets.length > 0) {
|
|
445
655
|
actions.push({
|
|
446
656
|
label: "Return to an earlier phase",
|
|
@@ -453,6 +663,11 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
|
|
|
453
663
|
run: async () => showAheadGuide(ctx, ""),
|
|
454
664
|
});
|
|
455
665
|
|
|
666
|
+
actions.push({
|
|
667
|
+
label: "Inspect optional skills reviewed for this phase",
|
|
668
|
+
run: async () => showRecommendedSkills(ctx),
|
|
669
|
+
});
|
|
670
|
+
|
|
456
671
|
actions.push({
|
|
457
672
|
label: "Explain this phase and its expectations",
|
|
458
673
|
run: async () => {
|
|
@@ -469,12 +684,208 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
|
|
|
469
684
|
},
|
|
470
685
|
});
|
|
471
686
|
|
|
687
|
+
actions.push({
|
|
688
|
+
label: "Stop AHEAD mode",
|
|
689
|
+
run: async () => stopAheadMode(ctx),
|
|
690
|
+
});
|
|
691
|
+
|
|
472
692
|
const selected = await ctx.ui.select(
|
|
473
693
|
`AHEAD mode · ${state.phase.title}\nNext (${action.actor === "human" ? "you" : "AI"}): ${action.label}`,
|
|
474
694
|
actions.map((candidate) => candidate.label),
|
|
475
695
|
);
|
|
476
696
|
const chosen = actions.find((candidate) => candidate.label === selected);
|
|
477
|
-
if (chosen)
|
|
697
|
+
if (chosen) {
|
|
698
|
+
await chosen.run();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function stopAheadMode(ctx: ExtensionCommandContext): Promise<void> {
|
|
703
|
+
if (!ctx.hasUI) {
|
|
704
|
+
throw new Error("/ahead-stop requires interactive or RPC UI support");
|
|
705
|
+
}
|
|
706
|
+
const store = storeFor(ctx);
|
|
707
|
+
const run = await requireRun(ctx);
|
|
708
|
+
const state = (await enginePromise).deriveState(run);
|
|
709
|
+
if (state.closed) {
|
|
710
|
+
ctx.ui.notify("This AHEAD run is already complete.", "info");
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const discard = "Stop and discard the unfinished AHEAD record";
|
|
715
|
+
const save = "Stop and save the unfinished run for later";
|
|
716
|
+
const selected = await ctx.ui.select("Stop AHEAD mode", [discard, save]);
|
|
717
|
+
if (selected === discard) {
|
|
718
|
+
const confirmed = await ctx.ui.confirm(
|
|
719
|
+
"Discard this unfinished AHEAD record?",
|
|
720
|
+
[
|
|
721
|
+
`${run.title} · ${state.phase.title}`,
|
|
722
|
+
"",
|
|
723
|
+
"This removes only this run's .ahead workflow state and artifacts.",
|
|
724
|
+
"It does not delete, reset, or revert source code or other repository changes.",
|
|
725
|
+
].join("\n"),
|
|
726
|
+
);
|
|
727
|
+
if (!confirmed) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
await store.discardCurrent(run.id);
|
|
731
|
+
await refreshUi(ctx);
|
|
732
|
+
ctx.ui.notify(
|
|
733
|
+
"AHEAD mode stopped and its unfinished workflow record was discarded. Repository changes were left untouched.",
|
|
734
|
+
"info",
|
|
735
|
+
);
|
|
736
|
+
} else if (selected === save) {
|
|
737
|
+
await store.saveCurrentForResume(run.id);
|
|
738
|
+
await refreshUi(ctx);
|
|
739
|
+
ctx.ui.notify(
|
|
740
|
+
`AHEAD mode stopped and run ${run.id} was saved. Resume it with /ahead-resume ${run.id}.`,
|
|
741
|
+
"info",
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function resumeSavedRun(
|
|
747
|
+
ctx: ExtensionCommandContext,
|
|
748
|
+
requestedRunId: string,
|
|
749
|
+
supplied?: Run[],
|
|
750
|
+
): Promise<Run | undefined> {
|
|
751
|
+
const store = storeFor(ctx);
|
|
752
|
+
const current = await store.loadCurrent();
|
|
753
|
+
if (current && !(await enginePromise).deriveState(current).closed) {
|
|
754
|
+
throw new AheadEngineError(
|
|
755
|
+
"active_run_exists",
|
|
756
|
+
`run ${current.id} is still active; stop it before resuming another run`,
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const saved = supplied ?? (await loadResumableRuns(ctx));
|
|
761
|
+
if (saved.length === 0) {
|
|
762
|
+
ctx.ui.notify("No saved unfinished AHEAD runs are available.", "info");
|
|
763
|
+
return undefined;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
let selected = requestedRunId
|
|
767
|
+
? saved.find((candidate) => candidate.id === requestedRunId)
|
|
768
|
+
: undefined;
|
|
769
|
+
if (requestedRunId && !selected) {
|
|
770
|
+
throw new AheadEngineError(
|
|
771
|
+
"saved_run_not_found",
|
|
772
|
+
`saved unfinished run ${requestedRunId} was not found`,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
if (!selected) {
|
|
776
|
+
if (!ctx.hasUI) {
|
|
777
|
+
throw new Error("/ahead-resume requires a run id without interactive UI");
|
|
778
|
+
}
|
|
779
|
+
const options = saved.map(savedRunOption);
|
|
780
|
+
const choice = await ctx.ui.select("Resume saved AHEAD work", options);
|
|
781
|
+
selected = saved.find((candidate) => savedRunOption(candidate) === choice);
|
|
782
|
+
}
|
|
783
|
+
if (!selected) {
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const resumed = await store.resume(selected.id);
|
|
788
|
+
const state = (await enginePromise).deriveState(resumed);
|
|
789
|
+
await refreshUi(ctx, resumed);
|
|
790
|
+
ctx.ui.notify(
|
|
791
|
+
`AHEAD mode resumed · ${state.phase.title}. Existing artifacts and unmet gates were preserved.`,
|
|
792
|
+
"info",
|
|
793
|
+
);
|
|
794
|
+
return resumed;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async function loadResumableRuns(ctx: ExtensionContext): Promise<Run[]> {
|
|
798
|
+
const store = storeFor(ctx);
|
|
799
|
+
const engine = await enginePromise;
|
|
800
|
+
const resumable: Run[] = [];
|
|
801
|
+
const invalid: string[] = [];
|
|
802
|
+
for (const runId of await store.listRunIds()) {
|
|
803
|
+
try {
|
|
804
|
+
const run = await store.load(runId);
|
|
805
|
+
if (!engine.deriveState(run).closed) {
|
|
806
|
+
resumable.push(run);
|
|
807
|
+
}
|
|
808
|
+
} catch {
|
|
809
|
+
invalid.push(runId);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (invalid.length > 0 && ctx.hasUI) {
|
|
813
|
+
ctx.ui.notify(`Ignored invalid saved AHEAD runs: ${invalid.join(", ")}`, "warning");
|
|
814
|
+
}
|
|
815
|
+
return resumable;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function savedRunOption(run: Run): string {
|
|
819
|
+
return `${run.title} · ${run.workflow_id} · ${run.id}`;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function showRecommendedSkills(ctx: ExtensionCommandContext): Promise<void> {
|
|
823
|
+
if (!ctx.hasUI) {
|
|
824
|
+
throw new Error("Inspecting recommended skills requires interactive or RPC UI support");
|
|
825
|
+
}
|
|
826
|
+
const catalog = await loadRecommendedSkills();
|
|
827
|
+
const run = await storeFor(ctx).loadCurrent();
|
|
828
|
+
const state = run ? (await enginePromise).deriveState(run) : undefined;
|
|
829
|
+
const phaseId = state && !state.closed ? state.phase.id : undefined;
|
|
830
|
+
const workflowId = state && !state.closed ? state.workflow_id : undefined;
|
|
831
|
+
const relevant = relevantRecommendedSkills(catalog, workflowId, phaseId);
|
|
832
|
+
await showReferenceViewer(
|
|
833
|
+
ctx,
|
|
834
|
+
relevant.length ? `AHEAD skills · ${state?.phase.title}` : "AHEAD recommended skills",
|
|
835
|
+
recommendedSkillsMarkdown(catalog, relevant.length ? relevant : catalog.skills),
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function openReviewWorkbench(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
|
|
840
|
+
if (!ctx.hasUI) {
|
|
841
|
+
throw new Error("The review workbench requires interactive or RPC UI support");
|
|
842
|
+
}
|
|
843
|
+
const store = storeFor(ctx);
|
|
844
|
+
const run = await requireRun(ctx);
|
|
845
|
+
const state = (await enginePromise).deriveState(run);
|
|
846
|
+
const snapshot = await collectReviewSnapshot(store.projectRoot);
|
|
847
|
+
const aiReview = state.artifacts.find((artifact) => artifact.kind === "ai-review");
|
|
848
|
+
const disposition = state.artifacts.find((artifact) => artifact.kind === "review-disposition");
|
|
849
|
+
const options = ["View snapshot and changed files", "View exact terminal diff"];
|
|
850
|
+
if (snapshot.changed_files.length) {
|
|
851
|
+
options.push("Open a changed file in the configured editor");
|
|
852
|
+
}
|
|
853
|
+
if (state.phase.id === "ai-review" && !aiReview?.present) {
|
|
854
|
+
options.push("Request AI review of this exact snapshot");
|
|
855
|
+
}
|
|
856
|
+
if (state.phase.id === "ai-review" && aiReview?.present && !disposition?.present) {
|
|
857
|
+
options.push("Disposition the AI findings as the implementing human");
|
|
858
|
+
}
|
|
859
|
+
if (state.phase.id === "human-review") {
|
|
860
|
+
options.push("Record the independent human review of this snapshot");
|
|
861
|
+
}
|
|
862
|
+
const selected = await ctx.ui.select(
|
|
863
|
+
`AHEAD review · ${state.phase.title} · ${snapshot.changed_files.length} changed files`,
|
|
864
|
+
options,
|
|
865
|
+
);
|
|
866
|
+
if (selected === "View snapshot and changed files") {
|
|
867
|
+
await showReferenceViewer(ctx, "AHEAD exact review snapshot", reviewSnapshotMarkdown(snapshot));
|
|
868
|
+
} else if (selected === "View exact terminal diff") {
|
|
869
|
+
await showReferenceViewer(
|
|
870
|
+
ctx,
|
|
871
|
+
`AHEAD diff · ${snapshot.fingerprint.slice(0, 12)}`,
|
|
872
|
+
`${reviewSnapshotMarkdown(snapshot)}\n\n## Diff\n\n\`\`\`diff\n${snapshot.diff || "No tracked diff."}\n\`\`\``,
|
|
873
|
+
);
|
|
874
|
+
} else if (selected === "Open a changed file in the configured editor") {
|
|
875
|
+
const path = await ctx.ui.select("Open changed file", snapshot.changed_files);
|
|
876
|
+
if (path && !openInConfiguredEditor(store.projectRoot, { path })) {
|
|
877
|
+
ctx.ui.notify(
|
|
878
|
+
`No supported editor was detected. Open ${path} from the repository, or set AHEAD_EDITOR=vscode.`,
|
|
879
|
+
"info",
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
} else if (selected === "Request AI review of this exact snapshot") {
|
|
883
|
+
pi.sendUserMessage(reviewRequest(snapshot));
|
|
884
|
+
} else if (selected === "Disposition the AI findings as the implementing human") {
|
|
885
|
+
await recordHumanArtifact(ctx, "review-disposition");
|
|
886
|
+
} else if (selected === "Record the independent human review of this snapshot") {
|
|
887
|
+
await recordHumanArtifact(ctx, "human-review");
|
|
888
|
+
}
|
|
478
889
|
}
|
|
479
890
|
|
|
480
891
|
async function askImplementationQuestion(
|
|
@@ -482,7 +893,9 @@ async function askImplementationQuestion(
|
|
|
482
893
|
ctx: ExtensionCommandContext,
|
|
483
894
|
state: RunState,
|
|
484
895
|
): Promise<void> {
|
|
485
|
-
if (!ctx.hasUI)
|
|
896
|
+
if (!ctx.hasUI) {
|
|
897
|
+
throw new Error("Implementation coaching requires interactive or RPC UI support");
|
|
898
|
+
}
|
|
486
899
|
const question = await ctx.ui.editor(
|
|
487
900
|
"AHEAD implementation help · human first",
|
|
488
901
|
[
|
|
@@ -501,50 +914,72 @@ async function askImplementationQuestion(
|
|
|
501
914
|
"",
|
|
502
915
|
].join("\n"),
|
|
503
916
|
);
|
|
504
|
-
if (!question?.trim())
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
917
|
+
if (!question?.trim()) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
pi.sendUserMessage(
|
|
921
|
+
[
|
|
922
|
+
`AHEAD mode: help me with this ${state.phase.title} question while I remain the implementer.`,
|
|
923
|
+
"Use my current model and first attempt below. Help me understand or solve the problem with questions, explanation, evidence, hints, and bounded next steps.",
|
|
924
|
+
"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.",
|
|
925
|
+
"",
|
|
926
|
+
question.trim(),
|
|
927
|
+
].join("\n"),
|
|
928
|
+
);
|
|
512
929
|
}
|
|
513
930
|
|
|
514
931
|
async function showAheadGuide(ctx: ExtensionCommandContext, requestedTopic: string): Promise<void> {
|
|
515
|
-
if (!ctx.hasUI)
|
|
932
|
+
if (!ctx.hasUI) {
|
|
933
|
+
throw new Error("Reading AHEAD framework guidance requires interactive or RPC UI support");
|
|
934
|
+
}
|
|
516
935
|
const run = await storeFor(ctx).loadCurrent();
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
936
|
+
const state = run ? (await enginePromise).deriveState(run) : undefined;
|
|
937
|
+
const phase = state && !state.closed ? state.phase.id : undefined;
|
|
938
|
+
const workflowId = state && !state.closed ? state.workflow_id : undefined;
|
|
520
939
|
const index = await loadReferenceIndex();
|
|
521
|
-
let entry =
|
|
522
|
-
|
|
523
|
-
|
|
940
|
+
let entry =
|
|
941
|
+
requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all"
|
|
942
|
+
? await findReference(requestedTopic)
|
|
943
|
+
: undefined;
|
|
524
944
|
|
|
525
945
|
if (requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all" && !entry) {
|
|
526
946
|
throw new Error(`No packaged AHEAD reference matches ${requestedTopic.trim()}`);
|
|
527
947
|
}
|
|
528
948
|
|
|
529
949
|
if (!entry) {
|
|
530
|
-
const recommended =
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
950
|
+
const recommended =
|
|
951
|
+
requestedTopic.trim().toLowerCase() === "all"
|
|
952
|
+
? index.references
|
|
953
|
+
: await relevantReferences(workflowId, phase);
|
|
954
|
+
const browseAll = "Browse all practitioner and evidence Markdown";
|
|
534
955
|
const selected = await ctx.ui.select(
|
|
535
956
|
phase ? `AHEAD guidance · ${phase}` : "AHEAD framework guidance",
|
|
536
|
-
[
|
|
957
|
+
[
|
|
958
|
+
...recommended.map(referenceOption),
|
|
959
|
+
...(recommended.length < index.references.length ? [browseAll] : []),
|
|
960
|
+
],
|
|
537
961
|
);
|
|
538
|
-
if (!selected)
|
|
539
|
-
|
|
540
|
-
|
|
962
|
+
if (!selected) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (selected === browseAll) {
|
|
966
|
+
return showAheadGuide(ctx, "all");
|
|
967
|
+
}
|
|
968
|
+
entry = recommended.find((candidate) => referenceOption(candidate) === selected);
|
|
969
|
+
}
|
|
970
|
+
if (!entry) {
|
|
971
|
+
return;
|
|
541
972
|
}
|
|
542
|
-
if (!entry) return;
|
|
543
973
|
|
|
544
974
|
await showReferenceViewer(ctx, `AHEAD reference · ${entry.title}`, await readReference(entry));
|
|
545
975
|
}
|
|
546
976
|
|
|
547
|
-
|
|
977
|
+
function referenceOption(entry: ReferenceEntry): string {
|
|
978
|
+
const classification = entry.audience === "evidence" ? "evidence" : entry.authority;
|
|
979
|
+
return `${classification} · ${entry.title}`;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async function startRun(ctx: ExtensionCommandContext, request: string): Promise<Run | undefined> {
|
|
548
983
|
const engine = await enginePromise;
|
|
549
984
|
const store = storeFor(ctx);
|
|
550
985
|
const current = await store.loadCurrent();
|
|
@@ -555,9 +990,45 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
|
|
|
555
990
|
);
|
|
556
991
|
}
|
|
557
992
|
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
993
|
+
const workflows = engine.listWorkflows();
|
|
994
|
+
const parsed = parseStartRequest(
|
|
995
|
+
request,
|
|
996
|
+
workflows.map((workflow) => workflow.id),
|
|
997
|
+
);
|
|
998
|
+
let workflow = parsed.workflowId
|
|
999
|
+
? workflows.find((candidate) => candidate.id === parsed.workflowId)
|
|
1000
|
+
: undefined;
|
|
1001
|
+
if (parsed.workflowId && !workflow) {
|
|
1002
|
+
throw new AheadEngineError(
|
|
1003
|
+
"unknown_workflow",
|
|
1004
|
+
`unknown workflow ${parsed.workflowId}; choose one of: ${workflows.map((candidate) => candidate.id).join(", ")}`,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
if (!workflow && ctx.hasUI) {
|
|
1008
|
+
const selected = await ctx.ui.select(
|
|
1009
|
+
"Choose the AHEAD workflow that fits this work",
|
|
1010
|
+
workflows.map((candidate) => candidate.title),
|
|
1011
|
+
);
|
|
1012
|
+
workflow = workflows.find((candidate) => candidate.title === selected);
|
|
1013
|
+
if (!workflow) {
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (!workflow) {
|
|
1018
|
+
throw new AheadEngineError(
|
|
1019
|
+
"workflow_required",
|
|
1020
|
+
`choose a workflow explicitly: ${workflows.map((candidate) => candidate.id).join(", ")}. Noninteractive usage: /ahead-start <workflow-id> :: <title>`,
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const title =
|
|
1025
|
+
parsed.title ||
|
|
1026
|
+
(ctx.hasUI
|
|
1027
|
+
? await ctx.ui.input(`Enter AHEAD mode · ${workflow.title}`, "What work are you doing?")
|
|
1028
|
+
: undefined);
|
|
1029
|
+
if (!title?.trim()) {
|
|
1030
|
+
return undefined;
|
|
1031
|
+
}
|
|
561
1032
|
|
|
562
1033
|
const owner = humanActor(store.projectRoot);
|
|
563
1034
|
const run = engine.createRun({
|
|
@@ -565,35 +1036,43 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
|
|
|
565
1036
|
title: title.trim(),
|
|
566
1037
|
owner,
|
|
567
1038
|
timestamp: new Date().toISOString(),
|
|
568
|
-
workflow_id:
|
|
1039
|
+
workflow_id: workflow.id,
|
|
569
1040
|
});
|
|
570
1041
|
await store.save(run);
|
|
571
1042
|
await refreshUi(ctx, run);
|
|
572
1043
|
ctx.ui.notify(
|
|
573
1044
|
[
|
|
574
|
-
`AHEAD mode started · ${run.title}`,
|
|
1045
|
+
`AHEAD mode started · ${workflow.title} · ${run.title}`,
|
|
575
1046
|
"Human leads · AI assists",
|
|
576
|
-
"This run remains active
|
|
577
|
-
"
|
|
1047
|
+
"This run remains active until an accountable human closes the outcome or uses /ahead-stop.",
|
|
1048
|
+
"Continue in normal conversation. /ahead is available when you need the action menu.",
|
|
578
1049
|
].join("\n"),
|
|
579
1050
|
"info",
|
|
580
1051
|
);
|
|
581
1052
|
return run;
|
|
582
1053
|
}
|
|
583
1054
|
|
|
584
|
-
async function recordHumanArtifact(
|
|
585
|
-
|
|
1055
|
+
async function recordHumanArtifact(
|
|
1056
|
+
ctx: ExtensionCommandContext,
|
|
1057
|
+
requestedKind: string,
|
|
1058
|
+
): Promise<void> {
|
|
1059
|
+
if (!ctx.hasUI) {
|
|
1060
|
+
throw new Error("Recording a human artifact requires interactive or RPC UI support");
|
|
1061
|
+
}
|
|
586
1062
|
const engine = await enginePromise;
|
|
587
1063
|
const store = storeFor(ctx);
|
|
588
1064
|
const run = await requireRun(ctx);
|
|
589
1065
|
const state = engine.deriveState(run);
|
|
590
|
-
const allowed = state.artifacts.filter(
|
|
1066
|
+
const allowed = state.artifacts.filter(
|
|
1067
|
+
(artifact) => artifact.actor !== "ai" && !artifact.present,
|
|
1068
|
+
);
|
|
591
1069
|
let kind = requestedKind.trim();
|
|
592
1070
|
if (!kind) {
|
|
593
|
-
kind =
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1071
|
+
kind =
|
|
1072
|
+
(await ctx.ui.select(
|
|
1073
|
+
`AHEAD mode · Write for ${state.phase.title}`,
|
|
1074
|
+
allowed.map((artifact) => artifact.title),
|
|
1075
|
+
)) ?? "";
|
|
597
1076
|
kind = allowed.find((artifact) => artifact.title === kind)?.kind ?? kind;
|
|
598
1077
|
}
|
|
599
1078
|
const artifact = allowed.find((candidate) => candidate.kind === kind);
|
|
@@ -604,11 +1083,15 @@ async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind:
|
|
|
604
1083
|
);
|
|
605
1084
|
}
|
|
606
1085
|
|
|
1086
|
+
const template = await humanArtifactTemplate(store, state, run, artifact.kind, artifact.title);
|
|
607
1087
|
const content = await ctx.ui.editor(
|
|
608
1088
|
`AHEAD mode · ${artifact.title} · write in your own words`,
|
|
609
|
-
|
|
1089
|
+
template,
|
|
610
1090
|
);
|
|
611
|
-
if (!content?.trim())
|
|
1091
|
+
if (!content?.trim()) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
await validateHumanReviewArtifact(store, state, artifact.kind, content);
|
|
612
1095
|
const path = store.artifactPath(run, state.phase.id, artifact.kind);
|
|
613
1096
|
const action: EventAction = {
|
|
614
1097
|
type: "artifact_recorded",
|
|
@@ -621,19 +1104,86 @@ async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind:
|
|
|
621
1104
|
await store.save(updated);
|
|
622
1105
|
await refreshUi(ctx, updated);
|
|
623
1106
|
ctx.ui.notify(
|
|
624
|
-
`Saved ${artifact.title}. AHEAD mode remains active; continue the conversation
|
|
1107
|
+
`Saved ${artifact.title}. AHEAD mode remains active; continue the conversation.`,
|
|
625
1108
|
"info",
|
|
626
1109
|
);
|
|
627
1110
|
}
|
|
628
1111
|
|
|
1112
|
+
async function humanArtifactTemplate(
|
|
1113
|
+
store: RunStore,
|
|
1114
|
+
state: RunState,
|
|
1115
|
+
run: Run,
|
|
1116
|
+
kind: string,
|
|
1117
|
+
title: string,
|
|
1118
|
+
): Promise<string> {
|
|
1119
|
+
if (kind === "review-disposition") {
|
|
1120
|
+
const reviewArtifact = state.artifacts.find((artifact) => artifact.kind === "ai-review");
|
|
1121
|
+
if (!reviewArtifact?.path) {
|
|
1122
|
+
throw new AheadEngineError(
|
|
1123
|
+
"ai_review_missing",
|
|
1124
|
+
"the AI review must be recorded before human disposition",
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
const aiReview = await store.readArtifact(reviewArtifact.path);
|
|
1128
|
+
const snapshot = await collectReviewSnapshot(store.projectRoot);
|
|
1129
|
+
return reviewDispositionTemplate(snapshot, extractFindingIds(aiReview));
|
|
1130
|
+
}
|
|
1131
|
+
if (kind === "human-review") {
|
|
1132
|
+
const snapshot = await collectReviewSnapshot(store.projectRoot);
|
|
1133
|
+
return `${buildArtifactTemplate(run, state, kind, title)}\n\nAHEAD-Review-Snapshot: ${snapshot.fingerprint}\n`;
|
|
1134
|
+
}
|
|
1135
|
+
return buildArtifactTemplate(run, state, kind, title);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async function validateHumanReviewArtifact(
|
|
1139
|
+
store: RunStore,
|
|
1140
|
+
state: RunState,
|
|
1141
|
+
kind: string,
|
|
1142
|
+
content: string,
|
|
1143
|
+
): Promise<void> {
|
|
1144
|
+
if (kind !== "review-disposition" && kind !== "human-review") {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const snapshot = await collectReviewSnapshot(store.projectRoot);
|
|
1148
|
+
if (kind === "human-review") {
|
|
1149
|
+
if (extractReviewFingerprint(content) !== snapshot.fingerprint) {
|
|
1150
|
+
throw new AheadEngineError(
|
|
1151
|
+
"review_snapshot_stale",
|
|
1152
|
+
"the human review must identify the exact current AHEAD review snapshot",
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const reviewArtifact = state.artifacts.find((artifact) => artifact.kind === "ai-review");
|
|
1158
|
+
if (!reviewArtifact?.path) {
|
|
1159
|
+
throw new AheadEngineError("ai_review_missing", "the AI review artifact is missing");
|
|
1160
|
+
}
|
|
1161
|
+
const aiReview = await store.readArtifact(reviewArtifact.path);
|
|
1162
|
+
const aiFingerprint = extractReviewFingerprint(aiReview);
|
|
1163
|
+
if (!aiFingerprint || aiFingerprint !== snapshot.fingerprint) {
|
|
1164
|
+
throw new AheadEngineError(
|
|
1165
|
+
"review_snapshot_stale",
|
|
1166
|
+
"the changeset changed after AI review; return to implementation and review the new snapshot",
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
const errors = validateReviewDisposition(
|
|
1170
|
+
content,
|
|
1171
|
+
snapshot.fingerprint,
|
|
1172
|
+
extractFindingIds(aiReview),
|
|
1173
|
+
);
|
|
1174
|
+
if (errors.length) {
|
|
1175
|
+
throw new AheadEngineError("review_disposition_incomplete", errors.join("; "));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
629
1179
|
function requestAiAssistance(pi: ExtensionAPI, state: RunState, requiredKind?: string): void {
|
|
630
|
-
const guidance = phaseGuide(state.phase.id);
|
|
1180
|
+
const guidance = phaseGuide(state.workflow_id, state.phase.id);
|
|
631
1181
|
const artifact = requiredKind
|
|
632
1182
|
? state.artifacts.find((candidate) => candidate.kind === requiredKind)
|
|
633
1183
|
: undefined;
|
|
634
1184
|
const request = artifact
|
|
635
1185
|
? [
|
|
636
|
-
`AHEAD mode: perform the ${artifact.required ? "required" : "recommended"} ${state.phase.title} work for the exact current
|
|
1186
|
+
`AHEAD mode: perform the ${artifact.required ? "required" : "recommended"} ${state.phase.title} work for the exact current run and evidence.`,
|
|
637
1187
|
`Produce ${artifact.title}.`,
|
|
638
1188
|
`Follow the active human/AI boundary: ${guidance.ai}`,
|
|
639
1189
|
`Use ahead_get_context first, then record the completed artifact as ${artifact.kind} with ahead_record_artifact.`,
|
|
@@ -649,7 +1199,9 @@ function requestAiAssistance(pi: ExtensionAPI, state: RunState, requiredKind?: s
|
|
|
649
1199
|
}
|
|
650
1200
|
|
|
651
1201
|
async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
652
|
-
if (!ctx.hasUI)
|
|
1202
|
+
if (!ctx.hasUI) {
|
|
1203
|
+
throw new Error("Accepting an AHEAD gate requires interactive or RPC UI support");
|
|
1204
|
+
}
|
|
653
1205
|
const engine = await enginePromise;
|
|
654
1206
|
const store = storeFor(ctx);
|
|
655
1207
|
const run = await requireRun(ctx);
|
|
@@ -663,7 +1215,8 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
663
1215
|
}
|
|
664
1216
|
|
|
665
1217
|
const destination = state.phase.next
|
|
666
|
-
? engine.getWorkflow(run.workflow_id).phases.find((phase) => phase.id === state.phase.next)
|
|
1218
|
+
? (engine.getWorkflow(run.workflow_id).phases.find((phase) => phase.id === state.phase.next)
|
|
1219
|
+
?.title ?? state.phase.next)
|
|
667
1220
|
: "close this AHEAD run";
|
|
668
1221
|
const confirmed = await ctx.ui.confirm(
|
|
669
1222
|
`Accept and continue from ${state.phase.title}?`,
|
|
@@ -676,7 +1229,9 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
676
1229
|
"This records human acceptance. AI cannot perform this action.",
|
|
677
1230
|
].join("\n"),
|
|
678
1231
|
);
|
|
679
|
-
if (!confirmed)
|
|
1232
|
+
if (!confirmed) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
680
1235
|
|
|
681
1236
|
const actor = humanActor(store.projectRoot);
|
|
682
1237
|
let updated = run;
|
|
@@ -689,7 +1244,10 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
689
1244
|
state = engine.deriveState(updated);
|
|
690
1245
|
}
|
|
691
1246
|
if (!state.can_advance) {
|
|
692
|
-
throw new AheadEngineError(
|
|
1247
|
+
throw new AheadEngineError(
|
|
1248
|
+
"cannot_advance",
|
|
1249
|
+
state.blockers.join("; ") || "the phase cannot advance",
|
|
1250
|
+
);
|
|
693
1251
|
}
|
|
694
1252
|
|
|
695
1253
|
const action: EventAction = state.phase.next
|
|
@@ -706,33 +1264,44 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
706
1264
|
|
|
707
1265
|
const nextState = engine.deriveState(updated);
|
|
708
1266
|
if (nextState.closed) {
|
|
709
|
-
ctx.ui.notify(
|
|
1267
|
+
ctx.ui.notify(
|
|
1268
|
+
"AHEAD work complete. The accountable human accepted the outcome and closed the run.",
|
|
1269
|
+
"info",
|
|
1270
|
+
);
|
|
710
1271
|
} else if (nextState.phase.id === "human-review") {
|
|
711
1272
|
ctx.ui.notify(
|
|
712
1273
|
[
|
|
713
1274
|
"READY FOR INDEPENDENT HUMAN REVIEW",
|
|
714
1275
|
"The AI review is recorded and its material findings were disposed by a human.",
|
|
715
1276
|
"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
|
|
1277
|
+
"The independent reviewer opens this repository; AHEAD resumes at Human Review and guides the review record.",
|
|
717
1278
|
].join("\n"),
|
|
718
1279
|
"info",
|
|
719
1280
|
);
|
|
720
1281
|
} else {
|
|
721
1282
|
ctx.ui.notify(
|
|
722
|
-
`Continued to ${nextState.phase.title}. AHEAD mode remains active;
|
|
1283
|
+
`Continued to ${nextState.phase.title}. AHEAD mode remains active; continue the conversation.`,
|
|
723
1284
|
"info",
|
|
724
1285
|
);
|
|
725
1286
|
}
|
|
726
1287
|
}
|
|
727
1288
|
|
|
728
|
-
async function returnToEarlierPhase(
|
|
729
|
-
|
|
1289
|
+
async function returnToEarlierPhase(
|
|
1290
|
+
ctx: ExtensionCommandContext,
|
|
1291
|
+
requestedTarget: string,
|
|
1292
|
+
): Promise<void> {
|
|
1293
|
+
if (!ctx.hasUI) {
|
|
1294
|
+
throw new Error("Returning an AHEAD phase requires interactive or RPC UI support");
|
|
1295
|
+
}
|
|
730
1296
|
const engine = await enginePromise;
|
|
731
1297
|
const store = storeFor(ctx);
|
|
732
1298
|
const run = await requireRun(ctx);
|
|
733
1299
|
const state = engine.deriveState(run);
|
|
734
1300
|
if (!state.return_targets.length) {
|
|
735
|
-
throw new AheadEngineError(
|
|
1301
|
+
throw new AheadEngineError(
|
|
1302
|
+
"no_return_target",
|
|
1303
|
+
`phase ${state.phase.id} has no return transition`,
|
|
1304
|
+
);
|
|
736
1305
|
}
|
|
737
1306
|
const workflow = engine.getWorkflow(run.workflow_id);
|
|
738
1307
|
const targetOptions = state.return_targets.map((target) => ({
|
|
@@ -756,12 +1325,16 @@ async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarge
|
|
|
756
1325
|
const reason = await ctx.ui.editor(
|
|
757
1326
|
`Why return to ${workflow.phases.find((phase) => phase.id === target)?.title ?? target}?`,
|
|
758
1327
|
);
|
|
759
|
-
if (!reason?.trim())
|
|
1328
|
+
if (!reason?.trim()) {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
760
1331
|
const confirmed = await ctx.ui.confirm(
|
|
761
1332
|
`Return to ${target}?`,
|
|
762
1333
|
"This opens a new phase visit. Earlier artifacts remain as history but cannot satisfy the reopened gate.",
|
|
763
1334
|
);
|
|
764
|
-
if (!confirmed)
|
|
1335
|
+
if (!confirmed) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
765
1338
|
const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
|
|
766
1339
|
type: "phase_transitioned",
|
|
767
1340
|
from: state.phase.id,
|
|
@@ -771,7 +1344,10 @@ async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarge
|
|
|
771
1344
|
});
|
|
772
1345
|
await store.save(updated);
|
|
773
1346
|
await refreshUi(ctx, updated);
|
|
774
|
-
ctx.ui.notify(
|
|
1347
|
+
ctx.ui.notify(
|
|
1348
|
+
`Returned to ${target}. AHEAD mode remains active with fresh evidence and gate requirements.`,
|
|
1349
|
+
"warning",
|
|
1350
|
+
);
|
|
775
1351
|
}
|
|
776
1352
|
|
|
777
1353
|
function storeFor(ctx: ExtensionContext): RunStore {
|
|
@@ -780,7 +1356,9 @@ function storeFor(ctx: ExtensionContext): RunStore {
|
|
|
780
1356
|
|
|
781
1357
|
async function requireRun(ctx: ExtensionContext): Promise<Run> {
|
|
782
1358
|
const run = await storeFor(ctx).loadCurrent();
|
|
783
|
-
if (!run)
|
|
1359
|
+
if (!run) {
|
|
1360
|
+
throw new AheadEngineError("no_active_run", "no active AHEAD run; use /ahead [title]");
|
|
1361
|
+
}
|
|
784
1362
|
return run;
|
|
785
1363
|
}
|
|
786
1364
|
|
|
@@ -797,6 +1375,9 @@ async function refreshUi(ctx: ExtensionContext, supplied?: Run): Promise<void> {
|
|
|
797
1375
|
if (!run) {
|
|
798
1376
|
ctx.ui.setStatus("ahead", undefined);
|
|
799
1377
|
ctx.ui.setWidget("ahead", undefined);
|
|
1378
|
+
if (ctx.mode === "tui") {
|
|
1379
|
+
ctx.ui.setHeader(undefined);
|
|
1380
|
+
}
|
|
800
1381
|
return;
|
|
801
1382
|
}
|
|
802
1383
|
const engine = await enginePromise;
|
|
@@ -810,24 +1391,97 @@ async function refreshUi(ctx: ExtensionContext, supplied?: Run): Promise<void> {
|
|
|
810
1391
|
? `AHEAD · complete · ${state.workflow_id}`
|
|
811
1392
|
: `AHEAD · ${position.current}/${position.total} · ${state.phase.id} · ${action.actor} action`,
|
|
812
1393
|
);
|
|
813
|
-
|
|
814
|
-
"ahead",
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1394
|
+
if (state.closed) {
|
|
1395
|
+
ctx.ui.setWidget("ahead", undefined);
|
|
1396
|
+
if (ctx.mode === "tui") {
|
|
1397
|
+
ctx.ui.setHeader(undefined);
|
|
1398
|
+
}
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const lines = buildHeaderLines(run, state, workflow);
|
|
1403
|
+
if (ctx.mode === "tui") {
|
|
1404
|
+
ctx.ui.setWidget("ahead", undefined);
|
|
1405
|
+
ctx.ui.setHeader((_tui, theme) => aheadHeader(lines, theme));
|
|
1406
|
+
} else {
|
|
1407
|
+
ctx.ui.setWidget("ahead", lines, { placement: "aboveEditor" });
|
|
1408
|
+
}
|
|
818
1409
|
}
|
|
819
1410
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1411
|
+
function aheadHeader(lines: string[], theme: Theme) {
|
|
1412
|
+
return {
|
|
1413
|
+
render(width: number): string[] {
|
|
1414
|
+
const [heading = "AHEAD", ...fields] = lines;
|
|
1415
|
+
const styledHeading = `${theme.fg("accent", theme.bold("AHEAD"))}${theme.fg("muted", heading.slice("AHEAD".length))}`;
|
|
1416
|
+
const ruleWidth = Math.max(0, width - visibleWidth(styledHeading) - 1);
|
|
1417
|
+
const headingLine = `${styledHeading}${
|
|
1418
|
+
ruleWidth > 0 ? ` ${theme.fg("borderMuted", "─".repeat(ruleWidth))}` : ""
|
|
1419
|
+
}`;
|
|
1420
|
+
|
|
1421
|
+
return [
|
|
1422
|
+
truncateToWidth(headingLine, width),
|
|
1423
|
+
...fields.map((field) => formatAheadHeaderField(field, width, theme)),
|
|
1424
|
+
];
|
|
1425
|
+
},
|
|
1426
|
+
invalidate() {},
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function formatAheadHeaderField(field: string, width: number, theme: Theme): string {
|
|
1431
|
+
const separator = field.indexOf(":");
|
|
1432
|
+
if (separator < 0) {
|
|
1433
|
+
return truncateToWidth(theme.fg("text", field), width);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const label = field.slice(0, separator).toUpperCase().padEnd(8);
|
|
1437
|
+
let value = field.slice(separator + 1).trimStart();
|
|
1438
|
+
let styledValue = theme.fg("text", value);
|
|
1439
|
+
if (label.trim() === "NEXT") {
|
|
1440
|
+
const actor = value.startsWith("You →") ? "You →" : value.startsWith("AI →") ? "AI →" : "";
|
|
1441
|
+
if (actor) {
|
|
1442
|
+
value = value.slice(actor.length);
|
|
1443
|
+
styledValue = `${theme.fg(actor.startsWith("You") ? "success" : "accent", theme.bold(actor))}${theme.fg("text", value)}`;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
return truncateToWidth(`${theme.fg("muted", theme.bold(label))} ${styledValue}`, width);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async function loadInstructions(workflowId: string, phase: string): Promise<string> {
|
|
1451
|
+
const cacheKey = `${workflowId}/${phase}`;
|
|
1452
|
+
const cached = instructions.get(cacheKey);
|
|
1453
|
+
if (cached) {
|
|
1454
|
+
return cached;
|
|
1455
|
+
}
|
|
1456
|
+
const content = await readFile(`${instructionDirectory}/${cacheKey}.md`, "utf8");
|
|
1457
|
+
instructions.set(cacheKey, content);
|
|
825
1458
|
return content;
|
|
826
1459
|
}
|
|
827
1460
|
|
|
1461
|
+
function parseStartRequest(
|
|
1462
|
+
request: string,
|
|
1463
|
+
workflowIds: string[],
|
|
1464
|
+
): { workflowId?: string; title: string } {
|
|
1465
|
+
const trimmed = request.trim();
|
|
1466
|
+
const separator = trimmed.indexOf("::");
|
|
1467
|
+
if (separator >= 0) {
|
|
1468
|
+
return {
|
|
1469
|
+
workflowId: trimmed.slice(0, separator).trim(),
|
|
1470
|
+
title: trimmed.slice(separator + 2).trim(),
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
if (workflowIds.includes(trimmed)) {
|
|
1474
|
+
return { workflowId: trimmed, title: "" };
|
|
1475
|
+
}
|
|
1476
|
+
return { title: trimmed };
|
|
1477
|
+
}
|
|
1478
|
+
|
|
828
1479
|
function formatState(state: RunState): string {
|
|
829
1480
|
const artifacts = state.artifacts
|
|
830
|
-
.map(
|
|
1481
|
+
.map(
|
|
1482
|
+
(artifact) =>
|
|
1483
|
+
`${artifact.present ? "✓" : artifact.required ? "○" : "·"} ${artifact.kind} (${artifact.actor})`,
|
|
1484
|
+
)
|
|
831
1485
|
.join("\n");
|
|
832
1486
|
return [
|
|
833
1487
|
`${state.title} · ${state.workflow_id}@${state.workflow_version}`,
|
|
@@ -852,14 +1506,23 @@ async function command(ctx: ExtensionCommandContext, action: () => Promise<void>
|
|
|
852
1506
|
async function toolResult(action: () => Promise<unknown>) {
|
|
853
1507
|
try {
|
|
854
1508
|
const result = await action();
|
|
855
|
-
return {
|
|
1509
|
+
return {
|
|
1510
|
+
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
1511
|
+
details: result,
|
|
1512
|
+
};
|
|
856
1513
|
} catch (error) {
|
|
857
1514
|
const message = errorMessage(error);
|
|
858
|
-
return {
|
|
1515
|
+
return {
|
|
1516
|
+
content: [{ type: "text" as const, text: `AHEAD error: ${message}` }],
|
|
1517
|
+
details: { error: message },
|
|
1518
|
+
isError: true,
|
|
1519
|
+
};
|
|
859
1520
|
}
|
|
860
1521
|
}
|
|
861
1522
|
|
|
862
1523
|
function errorMessage(error: unknown): string {
|
|
863
|
-
if (error instanceof AheadEngineError)
|
|
1524
|
+
if (error instanceof AheadEngineError) {
|
|
1525
|
+
return `${error.code}: ${error.message}`;
|
|
1526
|
+
}
|
|
864
1527
|
return error instanceof Error ? error.message : String(error);
|
|
865
1528
|
}
|