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