ahead-pi 0.2.0 → 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 +29 -9
- 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 +12 -6
- package/generated/product-change/ai-review.md +20 -7
- package/generated/product-change/decision.md +16 -4
- package/generated/product-change/define.md +9 -4
- package/generated/product-change/deploy.md +9 -4
- package/generated/product-change/human-review.md +16 -4
- package/generated/product-change/implement.md +14 -5
- package/generated/product-change/manifest.json +9 -4
- package/generated/product-change/options.md +16 -4
- package/generated/product-change/outcome.md +9 -4
- package/generated/product-change/plan.md +22 -4
- package/generated/product-change/questions.md +22 -4
- package/generated/product-change/research.md +16 -4
- package/generated/product-change/verify.md +9 -4
- package/generated/recommended-skills.json +24 -0
- package/generated/reference/CONSTITUTION.md +43 -0
- package/generated/reference/docs/acceptable-ai-use.md +217 -0
- package/generated/reference/docs/design/adapted-skill-guidance.md +27 -0
- package/generated/reference/docs/design/debugging-and-operations.md +119 -0
- package/generated/reference/docs/design/executable-workflows.md +122 -0
- package/generated/reference/docs/design/instruction-authoring.md +28 -0
- package/generated/reference/docs/design/process-taxonomy.md +144 -0
- package/generated/reference/docs/design/review-workbench.md +37 -0
- package/generated/reference/docs/engineering-practice.md +163 -0
- package/generated/reference/docs/evidence/evidence-standard.md +123 -0
- package/generated/reference/docs/evidence/research-map.md +98 -0
- package/generated/reference/docs/rationale.md +210 -0
- package/generated/reference/docs/recommended-skills.md +19 -0
- package/generated/reference/docs/references/pragmatic-programmer-page-index.md +113 -0
- package/generated/reference/docs/references/submitted-engineering-notes.md +306 -0
- package/generated/reference/docs/releasing-pi.md +89 -0
- package/generated/reference/docs/workflows/README.md +142 -0
- package/generated/reference/docs/workflows/corrective-debugging.md +221 -0
- package/generated/reference/docs/workflows/decision.md +149 -0
- package/generated/reference/docs/workflows/internal-improvement.md +219 -0
- package/generated/reference/docs/workflows/investigation.md +161 -0
- package/generated/reference/docs/workflows/operational-stabilization.md +187 -0
- package/generated/reference/docs/workflows/product-change.md +216 -0
- package/generated/reference/index.json +298 -0
- package/package.json +36 -25
- package/src/engine.ts +26 -7
- package/src/flow-guides.ts +168 -0
- package/src/guidance.ts +220 -72
- package/src/index.ts +696 -140
- package/src/reference-viewer.ts +73 -0
- package/src/reference.ts +107 -0
- 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
|
@@ -14,12 +14,35 @@ import {
|
|
|
14
14
|
phaseGuide,
|
|
15
15
|
phasePosition,
|
|
16
16
|
} from "./guidance.js";
|
|
17
|
+
import {
|
|
18
|
+
findReference,
|
|
19
|
+
loadReferenceIndex,
|
|
20
|
+
readReference,
|
|
21
|
+
relevantReferences,
|
|
22
|
+
} from "./reference.js";
|
|
23
|
+
import { showReferenceViewer } from "./reference-viewer.js";
|
|
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";
|
|
17
40
|
import { humanActor, projectRoot, RunStore } from "./storage.js";
|
|
18
41
|
import type { Actor, Capability, EventAction, Run, RunState } from "./types.js";
|
|
19
42
|
|
|
20
43
|
const wasmPath =
|
|
21
44
|
process.env.AHEAD_WASM_PATH || fileURLToPath(new URL("../dist/ahead_wasm.wasm", import.meta.url));
|
|
22
|
-
const instructionDirectory = fileURLToPath(new URL("../generated
|
|
45
|
+
const instructionDirectory = fileURLToPath(new URL("../generated", import.meta.url));
|
|
23
46
|
const enginePromise = AheadEngine.load(wasmPath);
|
|
24
47
|
const instructions = new Map<string, string>();
|
|
25
48
|
|
|
@@ -38,97 +61,146 @@ const RecordArtifactParams = Type.Object({
|
|
|
38
61
|
kind: Type.String({ description: "Artifact kind permitted for AI in the active phase" }),
|
|
39
62
|
content: Type.String({ description: "Complete Markdown artifact content", maxLength: 100_000 }),
|
|
40
63
|
});
|
|
64
|
+
const ReferenceParams = Type.Object({
|
|
65
|
+
topic: Type.Optional(
|
|
66
|
+
Type.String({
|
|
67
|
+
description: "Reference id, path, or title; omit to list phase-relevant references",
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
70
|
+
});
|
|
41
71
|
|
|
42
72
|
export default function aheadExtension(pi: ExtensionAPI): void {
|
|
43
73
|
pi.registerCommand("ahead", {
|
|
44
74
|
description: "Enter or continue the guided AHEAD mode",
|
|
45
|
-
handler: async (args, ctx) =>
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
handler: async (args, ctx) =>
|
|
76
|
+
command(ctx, async () => {
|
|
77
|
+
await openAheadMode(pi, args, ctx);
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
pi.registerCommand("ahead-guide", {
|
|
82
|
+
description: "Read the AHEAD framework guidance relevant to the active phase",
|
|
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
|
+
}),
|
|
48
103
|
});
|
|
49
104
|
|
|
50
105
|
pi.registerCommand("ahead-start", {
|
|
51
|
-
description: "Advanced: start a
|
|
52
|
-
handler: async (args, ctx) =>
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
}),
|
|
55
111
|
});
|
|
56
112
|
|
|
57
113
|
pi.registerCommand("ahead-status", {
|
|
58
114
|
description: "Advanced: show the raw active AHEAD phase contract",
|
|
59
|
-
handler: async (_args, ctx) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}),
|
|
65
122
|
});
|
|
66
123
|
|
|
67
124
|
pi.registerCommand("ahead-record", {
|
|
68
125
|
description: "Advanced: record a human-owned artifact directly",
|
|
69
|
-
handler: async (args, ctx) =>
|
|
70
|
-
|
|
71
|
-
|
|
126
|
+
handler: async (args, ctx) =>
|
|
127
|
+
command(ctx, async () => {
|
|
128
|
+
await recordHumanArtifact(ctx, args.trim());
|
|
129
|
+
}),
|
|
72
130
|
});
|
|
73
131
|
|
|
74
132
|
pi.registerCommand("ahead-accept", {
|
|
75
133
|
description: "Advanced: accept the active gate without advancing",
|
|
76
|
-
handler: async (_args, ctx) =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
}),
|
|
96
159
|
});
|
|
97
160
|
|
|
98
161
|
pi.registerCommand("ahead-advance", {
|
|
99
162
|
description: "Advanced: advance an already accepted gate",
|
|
100
|
-
handler: async (_args, ctx) =>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
state
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}),
|
|
125
196
|
});
|
|
126
197
|
|
|
127
198
|
pi.registerCommand("ahead-return", {
|
|
128
199
|
description: "Advanced: return to an earlier phase with a reason",
|
|
129
|
-
handler: async (args, ctx) =>
|
|
130
|
-
|
|
131
|
-
|
|
200
|
+
handler: async (args, ctx) =>
|
|
201
|
+
command(ctx, async () => {
|
|
202
|
+
await returnToEarlierPhase(ctx, args);
|
|
203
|
+
}),
|
|
132
204
|
});
|
|
133
205
|
|
|
134
206
|
pi.registerCommand("ahead-help", {
|
|
@@ -136,7 +208,10 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
136
208
|
handler: async (_args, ctx) => {
|
|
137
209
|
ctx.ui.notify(
|
|
138
210
|
[
|
|
139
|
-
"/ahead [title] —
|
|
211
|
+
"/ahead [title] — choose a workflow for new work, or resume guided AHEAD mode",
|
|
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",
|
|
140
215
|
"",
|
|
141
216
|
"Once started, the repository run remains in AHEAD mode until an accountable human closes the outcome.",
|
|
142
217
|
"Use normal conversation to think and work with AI. Run /ahead whenever you want the next valid action.",
|
|
@@ -153,7 +228,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
153
228
|
pi.registerTool({
|
|
154
229
|
name: "ahead_get_context",
|
|
155
230
|
label: "AHEAD context",
|
|
156
|
-
description:
|
|
231
|
+
description:
|
|
232
|
+
"Read the authoritative active AHEAD workflow state, phase contract, artifacts, gate, and blockers.",
|
|
157
233
|
promptSnippet: "Read the active AHEAD workflow state and human/AI boundaries.",
|
|
158
234
|
parameters: EmptyParams,
|
|
159
235
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
@@ -165,10 +241,84 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
165
241
|
},
|
|
166
242
|
});
|
|
167
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
|
+
|
|
284
|
+
pi.registerTool({
|
|
285
|
+
name: "ahead_get_reference",
|
|
286
|
+
label: "AHEAD framework reference",
|
|
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.",
|
|
291
|
+
parameters: ReferenceParams,
|
|
292
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
293
|
+
return toolResult(async () => {
|
|
294
|
+
const run = await storeFor(ctx).loadCurrent();
|
|
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;
|
|
298
|
+
if (!params.topic?.trim()) {
|
|
299
|
+
const index = await loadReferenceIndex();
|
|
300
|
+
return {
|
|
301
|
+
phase: phase ?? null,
|
|
302
|
+
workflow: workflowId ?? null,
|
|
303
|
+
recommended: await relevantReferences(workflowId, phase),
|
|
304
|
+
available: index.references.map(({ id, title, path }) => ({ id, title, path })),
|
|
305
|
+
instruction: "Request one reference by id, path, or title. Load only what is relevant.",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const entry = await findReference(params.topic);
|
|
309
|
+
if (!entry) {
|
|
310
|
+
throw new Error(`No packaged AHEAD reference matches ${params.topic}`);
|
|
311
|
+
}
|
|
312
|
+
return { reference: entry, content: await readReference(entry) };
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
168
317
|
pi.registerTool({
|
|
169
318
|
name: "ahead_record_artifact",
|
|
170
319
|
label: "Record AHEAD artifact",
|
|
171
|
-
description:
|
|
320
|
+
description:
|
|
321
|
+
"Persist an AI-owned or shared artifact permitted by the active phase. Cannot record human-owned artifacts.",
|
|
172
322
|
promptSnippet: "Record an AI-permitted artifact in the active AHEAD run.",
|
|
173
323
|
parameters: RecordArtifactParams,
|
|
174
324
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -184,6 +334,13 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
184
334
|
`AI cannot record ${params.kind} in phase ${state.phase.id}`,
|
|
185
335
|
);
|
|
186
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
|
+
}
|
|
187
344
|
const path = store.artifactPath(run, state.phase.id, artifact.kind);
|
|
188
345
|
const updated = engine.applyEvent(run, aiActor(ctx), {
|
|
189
346
|
type: "artifact_recorded",
|
|
@@ -206,7 +363,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
206
363
|
pi.registerTool({
|
|
207
364
|
name: "ahead_request_transition",
|
|
208
365
|
label: "Request AHEAD transition",
|
|
209
|
-
description:
|
|
366
|
+
description:
|
|
367
|
+
"Report whether a human can advance the active AHEAD phase. This tool never accepts a gate or transitions state.",
|
|
210
368
|
parameters: EmptyParams,
|
|
211
369
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
212
370
|
return toolResult(async () => {
|
|
@@ -226,10 +384,14 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
226
384
|
pi.registerTool({
|
|
227
385
|
name: "ahead_validate",
|
|
228
386
|
label: "Validate AHEAD run",
|
|
229
|
-
description:
|
|
387
|
+
description:
|
|
388
|
+
"Replay and validate the active AHEAD event log against the embedded workflow contract.",
|
|
230
389
|
parameters: EmptyParams,
|
|
231
390
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
232
|
-
return toolResult(async () => ({
|
|
391
|
+
return toolResult(async () => ({
|
|
392
|
+
valid: true,
|
|
393
|
+
state: (await enginePromise).validateRun(await requireRun(ctx)),
|
|
394
|
+
}));
|
|
233
395
|
},
|
|
234
396
|
});
|
|
235
397
|
|
|
@@ -252,17 +414,22 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
252
414
|
|
|
253
415
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
254
416
|
const run = await storeFor(ctx).loadCurrent();
|
|
255
|
-
if (!run)
|
|
417
|
+
if (!run) {
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
256
420
|
const engine = await enginePromise;
|
|
257
421
|
const state = engine.deriveState(run);
|
|
258
|
-
if (state.closed)
|
|
422
|
+
if (state.closed) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
259
425
|
const workflow = engine.getWorkflow(run.workflow_id);
|
|
260
|
-
const guidance = phaseGuide(state.phase.id);
|
|
426
|
+
const guidance = phaseGuide(run.workflow_id, state.phase.id);
|
|
261
427
|
const action = nextAction(state, workflow);
|
|
262
|
-
const phaseInstructions = await loadInstructions(state.phase.id);
|
|
428
|
+
const phaseInstructions = await loadInstructions(run.workflow_id, state.phase.id);
|
|
263
429
|
const liveContext = [
|
|
264
430
|
"# Live AHEAD run",
|
|
265
431
|
`- Run: ${run.id} — ${run.title}`,
|
|
432
|
+
`- Workflow: ${workflow.title} (${workflow.id})`,
|
|
266
433
|
`- Phase: ${state.phase.id} visit ${state.phase.visit}`,
|
|
267
434
|
`- Gate accepted: ${state.gate.accepted}`,
|
|
268
435
|
`- Current blockers: ${state.blockers.length ? state.blockers.join("; ") : "none"}`,
|
|
@@ -277,15 +444,24 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
277
444
|
"- Never author a human-owned artifact, make a human decision, accept a gate, transition the run, approve a change, or claim accountability.",
|
|
278
445
|
"- When required AI-owned work is ready, record it with ahead_record_artifact and explain what the human must validate or decide.",
|
|
279
446
|
"- Treat AI review findings as hypotheses. Independent human review remains required for lasting engineering changes.",
|
|
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.",
|
|
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.",
|
|
280
450
|
].join("\n");
|
|
281
451
|
return { systemPrompt: `${event.systemPrompt}\n\n${phaseInstructions}\n\n${liveContext}\n` };
|
|
282
452
|
});
|
|
283
453
|
|
|
284
454
|
pi.on("tool_call", async (event, ctx) => {
|
|
285
|
-
if (event.toolName.startsWith("ahead_"))
|
|
455
|
+
if (event.toolName.startsWith("ahead_")) {
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
286
458
|
const run = await storeFor(ctx).loadCurrent();
|
|
287
|
-
if (!run)
|
|
288
|
-
|
|
459
|
+
if (!run) {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
if ((await enginePromise).deriveState(run).closed) {
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
289
465
|
const capability = toolCapabilities[event.toolName];
|
|
290
466
|
if (!capability) {
|
|
291
467
|
return {
|
|
@@ -295,10 +471,13 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
295
471
|
}
|
|
296
472
|
try {
|
|
297
473
|
const decision = (await enginePromise).toolAllowed(run, capability);
|
|
298
|
-
if (!decision.allowed)
|
|
474
|
+
if (!decision.allowed) {
|
|
475
|
+
return { block: true, reason: `AHEAD: ${decision.reason}` };
|
|
476
|
+
}
|
|
299
477
|
} catch (error) {
|
|
300
478
|
return { block: true, reason: `AHEAD state validation failed: ${errorMessage(error)}` };
|
|
301
479
|
}
|
|
480
|
+
return undefined;
|
|
302
481
|
});
|
|
303
482
|
}
|
|
304
483
|
|
|
@@ -307,31 +486,42 @@ interface GuidedAction {
|
|
|
307
486
|
run: () => Promise<void>;
|
|
308
487
|
}
|
|
309
488
|
|
|
310
|
-
async function openAheadMode(
|
|
489
|
+
async function openAheadMode(
|
|
490
|
+
pi: ExtensionAPI,
|
|
491
|
+
args: string,
|
|
492
|
+
ctx: ExtensionCommandContext,
|
|
493
|
+
): Promise<void> {
|
|
311
494
|
const engine = await enginePromise;
|
|
312
495
|
const store = storeFor(ctx);
|
|
313
496
|
let run = await store.loadCurrent();
|
|
314
497
|
|
|
315
498
|
if (run && engine.deriveState(run).closed) {
|
|
316
499
|
if (!ctx.hasUI) {
|
|
317
|
-
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
|
+
);
|
|
318
504
|
return;
|
|
319
505
|
}
|
|
320
506
|
const choice = await ctx.ui.select("AHEAD work is complete", [
|
|
321
|
-
"Start
|
|
507
|
+
"Start new AHEAD work",
|
|
322
508
|
"View the completed run",
|
|
323
509
|
]);
|
|
324
510
|
if (choice === "View the completed run") {
|
|
325
511
|
ctx.ui.notify(formatState(engine.deriveState(run)), "info");
|
|
326
512
|
return;
|
|
327
513
|
}
|
|
328
|
-
if (choice !== "Start
|
|
514
|
+
if (choice !== "Start new AHEAD work") {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
329
517
|
run = undefined;
|
|
330
518
|
}
|
|
331
519
|
|
|
332
520
|
if (!run) {
|
|
333
521
|
run = await startRun(ctx, args);
|
|
334
|
-
if (!run)
|
|
522
|
+
if (!run) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
335
525
|
}
|
|
336
526
|
|
|
337
527
|
await refreshUi(ctx, run);
|
|
@@ -342,42 +532,60 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
|
|
|
342
532
|
|
|
343
533
|
const state = engine.deriveState(run);
|
|
344
534
|
const workflow = engine.getWorkflow(run.workflow_id);
|
|
345
|
-
const guidance = phaseGuide(state.phase.id);
|
|
535
|
+
const guidance = phaseGuide(run.workflow_id, state.phase.id);
|
|
346
536
|
const action = nextAction(state, workflow);
|
|
347
537
|
const actions: GuidedAction[] = [];
|
|
348
|
-
const missingRequired = state.artifacts.filter(
|
|
538
|
+
const missingRequired = state.artifacts.filter(
|
|
539
|
+
(artifact) => artifact.required && !artifact.present,
|
|
540
|
+
);
|
|
349
541
|
if (action.artifactKind) {
|
|
542
|
+
const actionArtifact = state.artifacts.find(
|
|
543
|
+
(artifact) => artifact.kind === action.artifactKind,
|
|
544
|
+
);
|
|
350
545
|
actions.push({
|
|
351
546
|
label: action.label,
|
|
352
|
-
run:
|
|
353
|
-
|
|
354
|
-
|
|
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 ?? ""),
|
|
355
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
|
+
}
|
|
356
560
|
}
|
|
357
561
|
|
|
358
562
|
if (action.optional) {
|
|
359
|
-
const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor
|
|
563
|
+
const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor !== "ai");
|
|
360
564
|
if (nextHumanArtifact) {
|
|
361
565
|
actions.push({
|
|
362
566
|
label: `Continue without optional AI challenge · Write ${nextHumanArtifact.title}`,
|
|
363
567
|
run: async () => recordHumanArtifact(ctx, nextHumanArtifact.kind),
|
|
364
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
|
+
});
|
|
365
574
|
}
|
|
366
575
|
}
|
|
367
576
|
|
|
368
577
|
if (missingRequired.length === 0 && !action.artifactKind) {
|
|
369
578
|
actions.push({
|
|
370
|
-
label: state.gate.accepted
|
|
371
|
-
? action.label
|
|
372
|
-
: `Accept and continue · ${state.gate.title}`,
|
|
579
|
+
label: state.gate.accepted ? action.label : `Accept and continue · ${state.gate.title}`,
|
|
373
580
|
run: async () => acceptAndContinue(ctx),
|
|
374
581
|
});
|
|
375
582
|
}
|
|
376
583
|
|
|
377
584
|
if (
|
|
378
|
-
state.allowed_ai_capabilities.length > 0
|
|
379
|
-
|
|
380
|
-
|
|
585
|
+
state.allowed_ai_capabilities.length > 0 &&
|
|
586
|
+
action.actor !== "ai" &&
|
|
587
|
+
!missingRequired.some((artifact) => artifact.actor === "ai") &&
|
|
588
|
+
state.phase.id !== "implement"
|
|
381
589
|
) {
|
|
382
590
|
actions.push({
|
|
383
591
|
label: `Ask AI to assist · ${state.phase.title}`,
|
|
@@ -385,6 +593,20 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
|
|
|
385
593
|
});
|
|
386
594
|
}
|
|
387
595
|
|
|
596
|
+
if (state.phase.id === "implement") {
|
|
597
|
+
actions.push({
|
|
598
|
+
label: "Ask AI for help understanding or solving a problem",
|
|
599
|
+
run: async () => askImplementationQuestion(pi, ctx, state),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
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
|
+
|
|
388
610
|
if (state.return_targets.length > 0) {
|
|
389
611
|
actions.push({
|
|
390
612
|
label: "Return to an earlier phase",
|
|
@@ -392,6 +614,16 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
|
|
|
392
614
|
});
|
|
393
615
|
}
|
|
394
616
|
|
|
617
|
+
actions.push({
|
|
618
|
+
label: "Read AHEAD framework guidance for this phase",
|
|
619
|
+
run: async () => showAheadGuide(ctx, ""),
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
actions.push({
|
|
623
|
+
label: "Inspect optional skills reviewed for this phase",
|
|
624
|
+
run: async () => showRecommendedSkills(ctx),
|
|
625
|
+
});
|
|
626
|
+
|
|
395
627
|
actions.push({
|
|
396
628
|
label: "Explain this phase and its expectations",
|
|
397
629
|
run: async () => {
|
|
@@ -413,10 +645,167 @@ async function openAheadMode(pi: ExtensionAPI, args: string, ctx: ExtensionComma
|
|
|
413
645
|
actions.map((candidate) => candidate.label),
|
|
414
646
|
);
|
|
415
647
|
const chosen = actions.find((candidate) => candidate.label === selected);
|
|
416
|
-
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
|
+
);
|
|
417
668
|
}
|
|
418
669
|
|
|
419
|
-
async function
|
|
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
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function askImplementationQuestion(
|
|
723
|
+
pi: ExtensionAPI,
|
|
724
|
+
ctx: ExtensionCommandContext,
|
|
725
|
+
state: RunState,
|
|
726
|
+
): Promise<void> {
|
|
727
|
+
if (!ctx.hasUI) {
|
|
728
|
+
throw new Error("Implementation coaching requires interactive or RPC UI support");
|
|
729
|
+
}
|
|
730
|
+
const question = await ctx.ui.editor(
|
|
731
|
+
"AHEAD implementation help · human first",
|
|
732
|
+
[
|
|
733
|
+
"## What are you trying to understand or solve?",
|
|
734
|
+
"",
|
|
735
|
+
"",
|
|
736
|
+
"## What do you currently think is happening or should happen?",
|
|
737
|
+
"",
|
|
738
|
+
"",
|
|
739
|
+
"## What have you tried or inspected so far?",
|
|
740
|
+
"",
|
|
741
|
+
"",
|
|
742
|
+
"## What kind of help would be useful?",
|
|
743
|
+
"",
|
|
744
|
+
"<!-- Ask for explanation, a hint, competing approaches, debugging help, or a bounded suggestion. -->",
|
|
745
|
+
"",
|
|
746
|
+
].join("\n"),
|
|
747
|
+
);
|
|
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
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function showAheadGuide(ctx: ExtensionCommandContext, requestedTopic: string): Promise<void> {
|
|
763
|
+
if (!ctx.hasUI) {
|
|
764
|
+
throw new Error("Reading AHEAD framework guidance requires interactive or RPC UI support");
|
|
765
|
+
}
|
|
766
|
+
const run = await storeFor(ctx).loadCurrent();
|
|
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;
|
|
770
|
+
const index = await loadReferenceIndex();
|
|
771
|
+
let entry =
|
|
772
|
+
requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all"
|
|
773
|
+
? await findReference(requestedTopic)
|
|
774
|
+
: undefined;
|
|
775
|
+
|
|
776
|
+
if (requestedTopic.trim() && requestedTopic.trim().toLowerCase() !== "all" && !entry) {
|
|
777
|
+
throw new Error(`No packaged AHEAD reference matches ${requestedTopic.trim()}`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (!entry) {
|
|
781
|
+
const recommended =
|
|
782
|
+
requestedTopic.trim().toLowerCase() === "all"
|
|
783
|
+
? index.references
|
|
784
|
+
: await relevantReferences(workflowId, phase);
|
|
785
|
+
const browseAll = "Browse all packaged AHEAD Markdown";
|
|
786
|
+
const selected = await ctx.ui.select(
|
|
787
|
+
phase ? `AHEAD guidance · ${phase}` : "AHEAD framework guidance",
|
|
788
|
+
[
|
|
789
|
+
...recommended.map((candidate) => candidate.title),
|
|
790
|
+
...(recommended.length < index.references.length ? [browseAll] : []),
|
|
791
|
+
],
|
|
792
|
+
);
|
|
793
|
+
if (!selected) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (selected === browseAll) {
|
|
797
|
+
return showAheadGuide(ctx, "all");
|
|
798
|
+
}
|
|
799
|
+
entry = recommended.find((candidate) => candidate.title === selected);
|
|
800
|
+
}
|
|
801
|
+
if (!entry) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
await showReferenceViewer(ctx, `AHEAD reference · ${entry.title}`, await readReference(entry));
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function startRun(ctx: ExtensionCommandContext, request: string): Promise<Run | undefined> {
|
|
420
809
|
const engine = await enginePromise;
|
|
421
810
|
const store = storeFor(ctx);
|
|
422
811
|
const current = await store.loadCurrent();
|
|
@@ -427,9 +816,40 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
|
|
|
427
816
|
);
|
|
428
817
|
}
|
|
429
818
|
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
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
|
+
}
|
|
433
853
|
|
|
434
854
|
const owner = humanActor(store.projectRoot);
|
|
435
855
|
const run = engine.createRun({
|
|
@@ -437,13 +857,13 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
|
|
|
437
857
|
title: title.trim(),
|
|
438
858
|
owner,
|
|
439
859
|
timestamp: new Date().toISOString(),
|
|
440
|
-
workflow_id:
|
|
860
|
+
workflow_id: workflow.id,
|
|
441
861
|
});
|
|
442
862
|
await store.save(run);
|
|
443
863
|
await refreshUi(ctx, run);
|
|
444
864
|
ctx.ui.notify(
|
|
445
865
|
[
|
|
446
|
-
`AHEAD mode started · ${run.title}`,
|
|
866
|
+
`AHEAD mode started · ${workflow.title} · ${run.title}`,
|
|
447
867
|
"Human leads · AI assists",
|
|
448
868
|
"This run remains active in the repository until an accountable human closes the outcome.",
|
|
449
869
|
"Use /ahead for the next guided action; use normal conversation to think and work with AI.",
|
|
@@ -453,19 +873,27 @@ async function startRun(ctx: ExtensionCommandContext, requestedTitle: string): P
|
|
|
453
873
|
return run;
|
|
454
874
|
}
|
|
455
875
|
|
|
456
|
-
async function recordHumanArtifact(
|
|
457
|
-
|
|
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
|
+
}
|
|
458
883
|
const engine = await enginePromise;
|
|
459
884
|
const store = storeFor(ctx);
|
|
460
885
|
const run = await requireRun(ctx);
|
|
461
886
|
const state = engine.deriveState(run);
|
|
462
|
-
const allowed = state.artifacts.filter(
|
|
887
|
+
const allowed = state.artifacts.filter(
|
|
888
|
+
(artifact) => artifact.actor !== "ai" && !artifact.present,
|
|
889
|
+
);
|
|
463
890
|
let kind = requestedKind.trim();
|
|
464
891
|
if (!kind) {
|
|
465
|
-
kind =
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
892
|
+
kind =
|
|
893
|
+
(await ctx.ui.select(
|
|
894
|
+
`AHEAD mode · Write for ${state.phase.title}`,
|
|
895
|
+
allowed.map((artifact) => artifact.title),
|
|
896
|
+
)) ?? "";
|
|
469
897
|
kind = allowed.find((artifact) => artifact.title === kind)?.kind ?? kind;
|
|
470
898
|
}
|
|
471
899
|
const artifact = allowed.find((candidate) => candidate.kind === kind);
|
|
@@ -476,11 +904,15 @@ async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind:
|
|
|
476
904
|
);
|
|
477
905
|
}
|
|
478
906
|
|
|
907
|
+
const template = await humanArtifactTemplate(store, state, run, artifact.kind, artifact.title);
|
|
479
908
|
const content = await ctx.ui.editor(
|
|
480
909
|
`AHEAD mode · ${artifact.title} · write in your own words`,
|
|
481
|
-
|
|
910
|
+
template,
|
|
482
911
|
);
|
|
483
|
-
if (!content?.trim())
|
|
912
|
+
if (!content?.trim()) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
await validateHumanReviewArtifact(store, state, artifact.kind, content);
|
|
484
916
|
const path = store.artifactPath(run, state.phase.id, artifact.kind);
|
|
485
917
|
const action: EventAction = {
|
|
486
918
|
type: "artifact_recorded",
|
|
@@ -498,14 +930,81 @@ async function recordHumanArtifact(ctx: ExtensionCommandContext, requestedKind:
|
|
|
498
930
|
);
|
|
499
931
|
}
|
|
500
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
|
+
|
|
501
1000
|
function requestAiAssistance(pi: ExtensionAPI, state: RunState, requiredKind?: string): void {
|
|
502
|
-
const guidance = phaseGuide(state.phase.id);
|
|
1001
|
+
const guidance = phaseGuide(state.workflow_id, state.phase.id);
|
|
503
1002
|
const artifact = requiredKind
|
|
504
1003
|
? state.artifacts.find((candidate) => candidate.kind === requiredKind)
|
|
505
1004
|
: undefined;
|
|
506
1005
|
const request = artifact
|
|
507
1006
|
? [
|
|
508
|
-
`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.`,
|
|
509
1008
|
`Produce ${artifact.title}.`,
|
|
510
1009
|
`Follow the active human/AI boundary: ${guidance.ai}`,
|
|
511
1010
|
`Use ahead_get_context first, then record the completed artifact as ${artifact.kind} with ahead_record_artifact.`,
|
|
@@ -521,7 +1020,9 @@ function requestAiAssistance(pi: ExtensionAPI, state: RunState, requiredKind?: s
|
|
|
521
1020
|
}
|
|
522
1021
|
|
|
523
1022
|
async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
524
|
-
if (!ctx.hasUI)
|
|
1023
|
+
if (!ctx.hasUI) {
|
|
1024
|
+
throw new Error("Accepting an AHEAD gate requires interactive or RPC UI support");
|
|
1025
|
+
}
|
|
525
1026
|
const engine = await enginePromise;
|
|
526
1027
|
const store = storeFor(ctx);
|
|
527
1028
|
const run = await requireRun(ctx);
|
|
@@ -535,7 +1036,8 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
535
1036
|
}
|
|
536
1037
|
|
|
537
1038
|
const destination = state.phase.next
|
|
538
|
-
? 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)
|
|
539
1041
|
: "close this AHEAD run";
|
|
540
1042
|
const confirmed = await ctx.ui.confirm(
|
|
541
1043
|
`Accept and continue from ${state.phase.title}?`,
|
|
@@ -548,7 +1050,9 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
548
1050
|
"This records human acceptance. AI cannot perform this action.",
|
|
549
1051
|
].join("\n"),
|
|
550
1052
|
);
|
|
551
|
-
if (!confirmed)
|
|
1053
|
+
if (!confirmed) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
552
1056
|
|
|
553
1057
|
const actor = humanActor(store.projectRoot);
|
|
554
1058
|
let updated = run;
|
|
@@ -561,7 +1065,10 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
561
1065
|
state = engine.deriveState(updated);
|
|
562
1066
|
}
|
|
563
1067
|
if (!state.can_advance) {
|
|
564
|
-
throw new AheadEngineError(
|
|
1068
|
+
throw new AheadEngineError(
|
|
1069
|
+
"cannot_advance",
|
|
1070
|
+
state.blockers.join("; ") || "the phase cannot advance",
|
|
1071
|
+
);
|
|
565
1072
|
}
|
|
566
1073
|
|
|
567
1074
|
const action: EventAction = state.phase.next
|
|
@@ -578,7 +1085,10 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
578
1085
|
|
|
579
1086
|
const nextState = engine.deriveState(updated);
|
|
580
1087
|
if (nextState.closed) {
|
|
581
|
-
ctx.ui.notify(
|
|
1088
|
+
ctx.ui.notify(
|
|
1089
|
+
"AHEAD work complete. The accountable human accepted the outcome and closed the run.",
|
|
1090
|
+
"info",
|
|
1091
|
+
);
|
|
582
1092
|
} else if (nextState.phase.id === "human-review") {
|
|
583
1093
|
ctx.ui.notify(
|
|
584
1094
|
[
|
|
@@ -597,14 +1107,22 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
597
1107
|
}
|
|
598
1108
|
}
|
|
599
1109
|
|
|
600
|
-
async function returnToEarlierPhase(
|
|
601
|
-
|
|
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
|
+
}
|
|
602
1117
|
const engine = await enginePromise;
|
|
603
1118
|
const store = storeFor(ctx);
|
|
604
1119
|
const run = await requireRun(ctx);
|
|
605
1120
|
const state = engine.deriveState(run);
|
|
606
1121
|
if (!state.return_targets.length) {
|
|
607
|
-
throw new AheadEngineError(
|
|
1122
|
+
throw new AheadEngineError(
|
|
1123
|
+
"no_return_target",
|
|
1124
|
+
`phase ${state.phase.id} has no return transition`,
|
|
1125
|
+
);
|
|
608
1126
|
}
|
|
609
1127
|
const workflow = engine.getWorkflow(run.workflow_id);
|
|
610
1128
|
const targetOptions = state.return_targets.map((target) => ({
|
|
@@ -628,12 +1146,16 @@ async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarge
|
|
|
628
1146
|
const reason = await ctx.ui.editor(
|
|
629
1147
|
`Why return to ${workflow.phases.find((phase) => phase.id === target)?.title ?? target}?`,
|
|
630
1148
|
);
|
|
631
|
-
if (!reason?.trim())
|
|
1149
|
+
if (!reason?.trim()) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
632
1152
|
const confirmed = await ctx.ui.confirm(
|
|
633
1153
|
`Return to ${target}?`,
|
|
634
1154
|
"This opens a new phase visit. Earlier artifacts remain as history but cannot satisfy the reopened gate.",
|
|
635
1155
|
);
|
|
636
|
-
if (!confirmed)
|
|
1156
|
+
if (!confirmed) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
637
1159
|
const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
|
|
638
1160
|
type: "phase_transitioned",
|
|
639
1161
|
from: state.phase.id,
|
|
@@ -643,7 +1165,10 @@ async function returnToEarlierPhase(ctx: ExtensionCommandContext, requestedTarge
|
|
|
643
1165
|
});
|
|
644
1166
|
await store.save(updated);
|
|
645
1167
|
await refreshUi(ctx, updated);
|
|
646
|
-
ctx.ui.notify(
|
|
1168
|
+
ctx.ui.notify(
|
|
1169
|
+
`Returned to ${target}. AHEAD mode remains active with fresh evidence and gate requirements.`,
|
|
1170
|
+
"warning",
|
|
1171
|
+
);
|
|
647
1172
|
}
|
|
648
1173
|
|
|
649
1174
|
function storeFor(ctx: ExtensionContext): RunStore {
|
|
@@ -652,7 +1177,9 @@ function storeFor(ctx: ExtensionContext): RunStore {
|
|
|
652
1177
|
|
|
653
1178
|
async function requireRun(ctx: ExtensionContext): Promise<Run> {
|
|
654
1179
|
const run = await storeFor(ctx).loadCurrent();
|
|
655
|
-
if (!run)
|
|
1180
|
+
if (!run) {
|
|
1181
|
+
throw new AheadEngineError("no_active_run", "no active AHEAD run; use /ahead [title]");
|
|
1182
|
+
}
|
|
656
1183
|
return run;
|
|
657
1184
|
}
|
|
658
1185
|
|
|
@@ -682,24 +1209,44 @@ async function refreshUi(ctx: ExtensionContext, supplied?: Run): Promise<void> {
|
|
|
682
1209
|
? `AHEAD · complete · ${state.workflow_id}`
|
|
683
1210
|
: `AHEAD · ${position.current}/${position.total} · ${state.phase.id} · ${action.actor} action`,
|
|
684
1211
|
);
|
|
685
|
-
ctx.ui.setWidget(
|
|
686
|
-
"ahead",
|
|
687
|
-
buildWidgetLines(run, state, workflow),
|
|
688
|
-
{ placement: "aboveEditor" },
|
|
689
|
-
);
|
|
1212
|
+
ctx.ui.setWidget("ahead", buildWidgetLines(run, state, workflow), { placement: "aboveEditor" });
|
|
690
1213
|
}
|
|
691
1214
|
|
|
692
|
-
async function loadInstructions(phase: string): Promise<string> {
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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);
|
|
697
1223
|
return content;
|
|
698
1224
|
}
|
|
699
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
|
+
|
|
700
1244
|
function formatState(state: RunState): string {
|
|
701
1245
|
const artifacts = state.artifacts
|
|
702
|
-
.map(
|
|
1246
|
+
.map(
|
|
1247
|
+
(artifact) =>
|
|
1248
|
+
`${artifact.present ? "✓" : artifact.required ? "○" : "·"} ${artifact.kind} (${artifact.actor})`,
|
|
1249
|
+
)
|
|
703
1250
|
.join("\n");
|
|
704
1251
|
return [
|
|
705
1252
|
`${state.title} · ${state.workflow_id}@${state.workflow_version}`,
|
|
@@ -724,14 +1271,23 @@ async function command(ctx: ExtensionCommandContext, action: () => Promise<void>
|
|
|
724
1271
|
async function toolResult(action: () => Promise<unknown>) {
|
|
725
1272
|
try {
|
|
726
1273
|
const result = await action();
|
|
727
|
-
return {
|
|
1274
|
+
return {
|
|
1275
|
+
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
1276
|
+
details: result,
|
|
1277
|
+
};
|
|
728
1278
|
} catch (error) {
|
|
729
1279
|
const message = errorMessage(error);
|
|
730
|
-
return {
|
|
1280
|
+
return {
|
|
1281
|
+
content: [{ type: "text" as const, text: `AHEAD error: ${message}` }],
|
|
1282
|
+
details: { error: message },
|
|
1283
|
+
isError: true,
|
|
1284
|
+
};
|
|
731
1285
|
}
|
|
732
1286
|
}
|
|
733
1287
|
|
|
734
1288
|
function errorMessage(error: unknown): string {
|
|
735
|
-
if (error instanceof AheadEngineError)
|
|
1289
|
+
if (error instanceof AheadEngineError) {
|
|
1290
|
+
return `${error.code}: ${error.message}`;
|
|
1291
|
+
}
|
|
736
1292
|
return error instanceof Error ? error.message : String(error);
|
|
737
1293
|
}
|