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