ahead-pi 0.3.1 → 0.5.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 -1
- package/dist/ahead_wasm.wasm +0 -0
- package/generated/corrective-debugging/ai-audit.md +4 -2
- package/generated/corrective-debugging/ai-review.md +4 -2
- package/generated/corrective-debugging/characterize.md +4 -2
- package/generated/corrective-debugging/conclude.md +4 -2
- package/generated/corrective-debugging/correction.md +4 -2
- package/generated/corrective-debugging/deploy.md +4 -2
- package/generated/corrective-debugging/human-review.md +4 -2
- package/generated/corrective-debugging/implement.md +4 -2
- package/generated/corrective-debugging/investigate.md +4 -2
- package/generated/corrective-debugging/model.md +4 -2
- package/generated/corrective-debugging/outcome.md +4 -2
- package/generated/corrective-debugging/plan.md +4 -2
- package/generated/corrective-debugging/verify.md +4 -2
- package/generated/decision/compare.md +4 -2
- package/generated/decision/criteria.md +4 -2
- package/generated/decision/decide.md +4 -2
- package/generated/decision/frame.md +4 -2
- package/generated/decision/options.md +4 -2
- package/generated/decision/publish.md +4 -2
- package/generated/decision/research.md +4 -2
- package/generated/internal-improvement/ai-audit.md +4 -2
- package/generated/internal-improvement/ai-review.md +4 -2
- package/generated/internal-improvement/baseline.md +4 -2
- package/generated/internal-improvement/decision.md +4 -2
- package/generated/internal-improvement/deploy.md +4 -2
- package/generated/internal-improvement/human-review.md +4 -2
- package/generated/internal-improvement/implement.md +4 -2
- package/generated/internal-improvement/invariants.md +4 -2
- package/generated/internal-improvement/options.md +4 -2
- package/generated/internal-improvement/outcome.md +4 -2
- package/generated/internal-improvement/plan.md +4 -2
- package/generated/internal-improvement/target.md +4 -2
- package/generated/internal-improvement/verify.md +4 -2
- package/generated/investigation/bound.md +4 -2
- package/generated/investigation/conclude.md +4 -2
- package/generated/investigation/explore.md +4 -2
- package/generated/investigation/frame.md +4 -2
- package/generated/investigation/gather.md +4 -2
- package/generated/investigation/synthesize.md +4 -2
- package/generated/operational-stabilization/assess.md +4 -2
- package/generated/operational-stabilization/execute-observe.md +4 -2
- package/generated/operational-stabilization/monitor.md +4 -2
- package/generated/operational-stabilization/outcome.md +4 -2
- package/generated/operational-stabilization/respond.md +4 -2
- package/generated/operational-stabilization/verify-recovery.md +4 -2
- package/generated/product-change/ai-audit.md +4 -2
- package/generated/product-change/ai-review.md +4 -2
- package/generated/product-change/decision.md +4 -2
- package/generated/product-change/define.md +4 -2
- package/generated/product-change/deploy.md +4 -2
- package/generated/product-change/human-review.md +4 -2
- package/generated/product-change/implement.md +4 -2
- package/generated/product-change/options.md +4 -2
- package/generated/product-change/outcome.md +4 -2
- package/generated/product-change/plan.md +4 -2
- package/generated/product-change/questions.md +4 -2
- package/generated/product-change/research.md +4 -2
- package/generated/product-change/verify.md +4 -2
- package/generated/reference/CONSTITUTION.md +4 -0
- package/generated/reference/docs/evidence/README.md +1 -0
- package/generated/reference/docs/evidence/research-map.md +12 -0
- package/generated/reference/docs/evidence/sources/tigerstyle.md +65 -0
- package/generated/reference/docs/guide/README.md +2 -1
- package/generated/reference/docs/guide/engineering-practice.md +35 -13
- package/generated/reference/docs/guide/work-items.md +65 -0
- package/generated/reference/docs/guide/workflows/README.md +1 -0
- package/generated/reference/index.json +28 -0
- package/generated/skills/diagnosing-bugs/LICENSE.ahead +21 -0
- package/generated/skills/diagnosing-bugs/LICENSE.mattpocock +21 -0
- package/generated/skills/diagnosing-bugs/SKILL.md +126 -0
- package/generated/skills/manifest.json +21 -0
- package/generated/skills/research/LICENSE.ahead +21 -0
- package/generated/skills/research/LICENSE.mattpocock +21 -0
- package/generated/skills/research/SKILL.md +116 -0
- package/generated/skills/to-tickets/LICENSE.ahead +21 -0
- package/generated/skills/to-tickets/LICENSE.mattpocock +21 -0
- package/generated/skills/to-tickets/SKILL.md +132 -0
- package/package.json +4 -1
- package/src/config.ts +171 -0
- package/src/engine.ts +2 -0
- package/src/guidance.ts +60 -6
- package/src/index.ts +540 -9
- package/src/storage.ts +153 -2
- package/src/types.ts +24 -1
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import type {
|
|
@@ -8,6 +9,7 @@ import type {
|
|
|
8
9
|
} from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
11
|
import { Type } from "typebox";
|
|
12
|
+
import { projectConfigIssues, projectConfigMarkdown, runProjectConfigWizard } from "./config.js";
|
|
11
13
|
import { AheadEngine, AheadEngineError } from "./engine.js";
|
|
12
14
|
import {
|
|
13
15
|
buildArtifactTemplate,
|
|
@@ -15,6 +17,8 @@ import {
|
|
|
15
17
|
nextAction,
|
|
16
18
|
phaseGuide,
|
|
17
19
|
phasePosition,
|
|
20
|
+
promptsForArtifact,
|
|
21
|
+
validateArtifactForm,
|
|
18
22
|
} from "./guidance.js";
|
|
19
23
|
import {
|
|
20
24
|
findReference,
|
|
@@ -41,7 +45,15 @@ import {
|
|
|
41
45
|
relevantRecommendedSkills,
|
|
42
46
|
} from "./skills.js";
|
|
43
47
|
import { humanActor, projectRoot, RunStore } from "./storage.js";
|
|
44
|
-
import type {
|
|
48
|
+
import type {
|
|
49
|
+
Actor,
|
|
50
|
+
Capability,
|
|
51
|
+
EventAction,
|
|
52
|
+
Run,
|
|
53
|
+
RunState,
|
|
54
|
+
WorkflowDefinition,
|
|
55
|
+
WorkItem,
|
|
56
|
+
} from "./types.js";
|
|
45
57
|
|
|
46
58
|
const wasmPath =
|
|
47
59
|
process.env.AHEAD_WASM_PATH || fileURLToPath(new URL("../dist/ahead_wasm.wasm", import.meta.url));
|
|
@@ -81,6 +93,22 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
81
93
|
}),
|
|
82
94
|
});
|
|
83
95
|
|
|
96
|
+
pi.registerCommand("ahead-work-item", {
|
|
97
|
+
description: "Link an existing work item or create a GitHub issue",
|
|
98
|
+
handler: async (args, ctx) =>
|
|
99
|
+
command(ctx, async () => {
|
|
100
|
+
await manageWorkItem(ctx, args.trim());
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
pi.registerCommand("ahead-config", {
|
|
105
|
+
description: "Configure or migrate this project's AHEAD policy",
|
|
106
|
+
handler: async (_args, ctx) =>
|
|
107
|
+
command(ctx, async () => {
|
|
108
|
+
await manageProjectConfig(ctx);
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
|
|
84
112
|
pi.registerCommand("ahead-guide", {
|
|
85
113
|
description: "Read the AHEAD framework guidance relevant to the active phase",
|
|
86
114
|
handler: async (args, ctx) =>
|
|
@@ -122,7 +150,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
122
150
|
});
|
|
123
151
|
|
|
124
152
|
pi.registerCommand("ahead-start", {
|
|
125
|
-
description: "Advanced: start
|
|
153
|
+
description: "Advanced: start with <workflow-id> :: <title-or-work-item-url>",
|
|
126
154
|
handler: async (args, ctx) =>
|
|
127
155
|
command(ctx, async () => {
|
|
128
156
|
await startRun(ctx, args);
|
|
@@ -228,6 +256,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
228
256
|
ctx.ui.notify(
|
|
229
257
|
[
|
|
230
258
|
"/ahead [title] — choose a workflow for new work, or open the active action menu",
|
|
259
|
+
"/ahead-work-item [url] — link an existing work item or create a GitHub issue",
|
|
260
|
+
"/ahead-config — configure, replace, or migrate this project's AHEAD policy",
|
|
231
261
|
"/ahead-guide [topic] — read the applicable AHEAD framework Markdown",
|
|
232
262
|
"/ahead-skills — inspect optional reviewed skills relevant to this phase",
|
|
233
263
|
"/ahead-review — inspect the exact changeset and review handoff",
|
|
@@ -262,6 +292,47 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
262
292
|
},
|
|
263
293
|
});
|
|
264
294
|
|
|
295
|
+
pi.registerTool({
|
|
296
|
+
name: "ahead_get_work_item",
|
|
297
|
+
label: "AHEAD work item",
|
|
298
|
+
description:
|
|
299
|
+
"Read the linked provider-neutral work-item reference and resolve GitHub issue context when available.",
|
|
300
|
+
promptSnippet: "Read the human-linked work item when it is relevant to the active phase.",
|
|
301
|
+
parameters: EmptyParams,
|
|
302
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
303
|
+
return toolResult(async () => {
|
|
304
|
+
const store = storeFor(ctx);
|
|
305
|
+
const run = await requireRun(ctx);
|
|
306
|
+
const state = (await enginePromise).deriveState(run);
|
|
307
|
+
if (!state.work_item) {
|
|
308
|
+
throw new AheadEngineError("work_item_missing", "this AHEAD run has no linked work item");
|
|
309
|
+
}
|
|
310
|
+
if (state.work_item.provider !== "github") {
|
|
311
|
+
return {
|
|
312
|
+
resolved: false,
|
|
313
|
+
work_item: state.work_item,
|
|
314
|
+
instruction:
|
|
315
|
+
"No provider adapter is installed for this work item. Treat the URL as a human-selected coordination reference.",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const issue: unknown = JSON.parse(
|
|
319
|
+
await executeFile(
|
|
320
|
+
"gh",
|
|
321
|
+
[
|
|
322
|
+
"issue",
|
|
323
|
+
"view",
|
|
324
|
+
state.work_item.url,
|
|
325
|
+
"--json",
|
|
326
|
+
"number,title,body,state,url,labels,assignees",
|
|
327
|
+
],
|
|
328
|
+
store.projectRoot,
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
return { resolved: true, work_item: state.work_item, issue };
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
265
336
|
pi.registerTool({
|
|
266
337
|
name: "ahead_get_recommended_skills",
|
|
267
338
|
label: "AHEAD recommended skills",
|
|
@@ -458,6 +529,8 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
458
529
|
`- Run: ${run.id} — ${run.title}`,
|
|
459
530
|
`- Workflow: ${workflow.title} (${workflow.id})`,
|
|
460
531
|
`- Phase: ${state.phase.id} visit ${state.phase.visit}`,
|
|
532
|
+
`- Work item: ${state.work_item?.url ?? "none"}`,
|
|
533
|
+
`- Work-item boundary: ${state.policy.work_items.required_before_phase ?? "not required"}`,
|
|
461
534
|
`- Gate accepted: ${state.gate.accepted}`,
|
|
462
535
|
`- Current blockers: ${state.blockers.length ? state.blockers.join("; ") : "none"}`,
|
|
463
536
|
`- Allowed AI capabilities: ${state.allowed_ai_capabilities.length ? state.allowed_ai_capabilities.join(", ") : "none"}`,
|
|
@@ -473,6 +546,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
|
|
|
473
546
|
"- Treat AI review findings as hypotheses. Independent human review remains required for lasting engineering changes.",
|
|
474
547
|
"- 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.",
|
|
475
548
|
"- When AHEAD policy or rationale is unclear, use ahead_get_reference to retrieve only the applicable packaged Markdown.",
|
|
549
|
+
"- When a linked work item is relevant, use ahead_get_work_item for its available provider context. Treat it as coordination input, not as a substitute for human framing, evidence, or approval.",
|
|
476
550
|
"- 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.",
|
|
477
551
|
].join("\n");
|
|
478
552
|
return { systemPrompt: `${event.systemPrompt}\n\n${phaseInstructions}\n\n${liveContext}\n` };
|
|
@@ -568,6 +642,10 @@ async function openAheadMode(
|
|
|
568
642
|
}
|
|
569
643
|
}
|
|
570
644
|
|
|
645
|
+
if (isHttpUrl(args.trim())) {
|
|
646
|
+
run = await linkWorkItem(ctx, store, run, workItemFromUrl(args));
|
|
647
|
+
}
|
|
648
|
+
|
|
571
649
|
await refreshUi(ctx, run);
|
|
572
650
|
if (!ctx.hasUI) {
|
|
573
651
|
ctx.ui.notify(formatState(engine.deriveState(run)), "info");
|
|
@@ -618,7 +696,12 @@ async function openAheadMode(
|
|
|
618
696
|
}
|
|
619
697
|
}
|
|
620
698
|
|
|
621
|
-
if (
|
|
699
|
+
if (state.work_item_required_for_next_phase && state.gate.accepted) {
|
|
700
|
+
actions.push({
|
|
701
|
+
label: action.label,
|
|
702
|
+
run: async () => manageWorkItem(ctx, ""),
|
|
703
|
+
});
|
|
704
|
+
} else if (missingRequired.length === 0 && !action.artifactKind) {
|
|
622
705
|
actions.push({
|
|
623
706
|
label: state.gate.accepted ? action.label : `Accept and continue · ${state.gate.title}`,
|
|
624
707
|
run: async () => acceptAndContinue(ctx),
|
|
@@ -638,6 +721,12 @@ async function openAheadMode(
|
|
|
638
721
|
}
|
|
639
722
|
|
|
640
723
|
if (state.phase.id === "implement") {
|
|
724
|
+
if (!state.artifacts.some((artifact) => artifact.kind === "changeset" && artifact.present)) {
|
|
725
|
+
actions.push({
|
|
726
|
+
label: "Save this ready-to-implement run for a later sprint",
|
|
727
|
+
run: async () => saveImplementationHandoff(ctx),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
641
730
|
actions.push({
|
|
642
731
|
label: "Ask AI for help understanding or solving a problem",
|
|
643
732
|
run: async () => askImplementationQuestion(pi, ctx, state),
|
|
@@ -658,6 +747,20 @@ async function openAheadMode(
|
|
|
658
747
|
});
|
|
659
748
|
}
|
|
660
749
|
|
|
750
|
+
if (!state.work_item_required_for_next_phase || !state.gate.accepted) {
|
|
751
|
+
actions.push({
|
|
752
|
+
label: state.work_item
|
|
753
|
+
? "View or replace the linked work item"
|
|
754
|
+
: "Link or create a work item",
|
|
755
|
+
run: async () => manageWorkItem(ctx, ""),
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
actions.push({
|
|
760
|
+
label: "Configure project AHEAD policy for future runs",
|
|
761
|
+
run: async () => manageProjectConfig(ctx),
|
|
762
|
+
});
|
|
763
|
+
|
|
661
764
|
actions.push({
|
|
662
765
|
label: "Read AHEAD framework guidance for this phase",
|
|
663
766
|
run: async () => showAheadGuide(ctx, ""),
|
|
@@ -743,6 +846,40 @@ async function stopAheadMode(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
743
846
|
}
|
|
744
847
|
}
|
|
745
848
|
|
|
849
|
+
async function saveImplementationHandoff(ctx: ExtensionCommandContext): Promise<void> {
|
|
850
|
+
if (!ctx.hasUI) {
|
|
851
|
+
throw new Error("Saving an implementation handoff requires interactive or RPC UI support");
|
|
852
|
+
}
|
|
853
|
+
const store = storeFor(ctx);
|
|
854
|
+
const run = await requireRun(ctx);
|
|
855
|
+
const state = (await enginePromise).deriveState(run);
|
|
856
|
+
if (state.phase.id !== "implement") {
|
|
857
|
+
throw new AheadEngineError(
|
|
858
|
+
"implementation_handoff_unavailable",
|
|
859
|
+
"a ready-to-implement handoff can be saved only after the approved plan enters implementation",
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
const confirmed = await ctx.ui.confirm(
|
|
863
|
+
"Save this implementation handoff?",
|
|
864
|
+
[
|
|
865
|
+
`${run.title} · ${run.id}`,
|
|
866
|
+
`Work item: ${state.work_item?.url ?? "not linked"}`,
|
|
867
|
+
"",
|
|
868
|
+
"AHEAD mode will stop without discarding the approved plan or workflow state.",
|
|
869
|
+
"The implementing engineer can resume this run in a later sprint.",
|
|
870
|
+
].join("\n"),
|
|
871
|
+
);
|
|
872
|
+
if (!confirmed) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
await store.saveCurrentForResume(run.id);
|
|
876
|
+
await refreshUi(ctx);
|
|
877
|
+
ctx.ui.notify(
|
|
878
|
+
`Saved ready-to-implement AHEAD run ${run.id}. Resume it with /ahead-resume ${run.id}.`,
|
|
879
|
+
"info",
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
746
883
|
async function resumeSavedRun(
|
|
747
884
|
ctx: ExtensionCommandContext,
|
|
748
885
|
requestedRunId: string,
|
|
@@ -819,6 +956,313 @@ function savedRunOption(run: Run): string {
|
|
|
819
956
|
return `${run.title} · ${run.workflow_id} · ${run.id}`;
|
|
820
957
|
}
|
|
821
958
|
|
|
959
|
+
async function manageProjectConfig(ctx: ExtensionCommandContext): Promise<void> {
|
|
960
|
+
if (!ctx.hasUI) {
|
|
961
|
+
throw new Error("/ahead-config requires interactive or RPC UI support");
|
|
962
|
+
}
|
|
963
|
+
const store = storeFor(ctx);
|
|
964
|
+
const workflows = (await enginePromise).listWorkflows();
|
|
965
|
+
const inspection = await store.inspectProjectConfig();
|
|
966
|
+
if (inspection.status === "missing") {
|
|
967
|
+
await runProjectConfigWizard(ctx, store, workflows, false);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const issues =
|
|
972
|
+
inspection.status === "valid" ? projectConfigIssues(inspection.config, workflows) : [];
|
|
973
|
+
const problem =
|
|
974
|
+
inspection.status === "invalid"
|
|
975
|
+
? inspection.error
|
|
976
|
+
: issues.length > 0
|
|
977
|
+
? `invalid .ahead/config.json: ${issues.join("; ")}`
|
|
978
|
+
: undefined;
|
|
979
|
+
if (problem) {
|
|
980
|
+
const view = "View the current file and validation error";
|
|
981
|
+
const replace = "Run setup wizard and preserve the current file as a backup";
|
|
982
|
+
const selected = await ctx.ui.select(`AHEAD configuration needs attention\n${problem}`, [
|
|
983
|
+
view,
|
|
984
|
+
replace,
|
|
985
|
+
]);
|
|
986
|
+
if (selected === view) {
|
|
987
|
+
await showReferenceViewer(
|
|
988
|
+
ctx,
|
|
989
|
+
"AHEAD configuration · invalid",
|
|
990
|
+
[
|
|
991
|
+
"# AHEAD project configuration needs attention",
|
|
992
|
+
"",
|
|
993
|
+
problem,
|
|
994
|
+
"",
|
|
995
|
+
"The setup wizard can replace this file while preserving its exact contents under `.ahead/backups/`.",
|
|
996
|
+
"",
|
|
997
|
+
"~~~json",
|
|
998
|
+
inspection.content,
|
|
999
|
+
"~~~",
|
|
1000
|
+
].join("\n"),
|
|
1001
|
+
);
|
|
1002
|
+
} else if (selected === replace) {
|
|
1003
|
+
await runProjectConfigWizard(ctx, store, workflows, true);
|
|
1004
|
+
}
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (inspection.status !== "valid") {
|
|
1008
|
+
throw new Error("AHEAD project configuration inspection returned an inconsistent result");
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const view = "View current configuration";
|
|
1012
|
+
const replace = "Run setup wizard and replace configuration";
|
|
1013
|
+
const selected = await ctx.ui.select("AHEAD project configuration", [view, replace]);
|
|
1014
|
+
if (selected === view) {
|
|
1015
|
+
await showReferenceViewer(
|
|
1016
|
+
ctx,
|
|
1017
|
+
"AHEAD project configuration",
|
|
1018
|
+
projectConfigMarkdown(inspection.config),
|
|
1019
|
+
);
|
|
1020
|
+
} else if (selected === replace) {
|
|
1021
|
+
await runProjectConfigWizard(ctx, store, workflows, true);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async function ensureProjectConfiguration(
|
|
1026
|
+
ctx: ExtensionCommandContext,
|
|
1027
|
+
store: RunStore,
|
|
1028
|
+
workflows: WorkflowDefinition[],
|
|
1029
|
+
): Promise<boolean> {
|
|
1030
|
+
const inspection = await store.inspectProjectConfig();
|
|
1031
|
+
if (inspection.status === "valid") {
|
|
1032
|
+
const issues = projectConfigIssues(inspection.config, workflows);
|
|
1033
|
+
if (issues.length === 0) {
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
const problem = `invalid .ahead/config.json: ${issues.join("; ")}`;
|
|
1037
|
+
if (!ctx.hasUI) {
|
|
1038
|
+
throw new Error(`${problem}; run /ahead-config in an interactive session`);
|
|
1039
|
+
}
|
|
1040
|
+
ctx.ui.notify(problem, "warning");
|
|
1041
|
+
return repairProjectConfiguration(ctx, store, workflows);
|
|
1042
|
+
}
|
|
1043
|
+
if (inspection.status === "missing") {
|
|
1044
|
+
if (!ctx.hasUI) {
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
const configure = "Run AHEAD project setup wizard";
|
|
1048
|
+
const without = "Continue without project configuration";
|
|
1049
|
+
const selected = await ctx.ui.select("No .ahead/config.json was found", [configure, without]);
|
|
1050
|
+
if (selected === configure) {
|
|
1051
|
+
return runProjectConfigWizard(ctx, store, workflows, false);
|
|
1052
|
+
}
|
|
1053
|
+
return selected === without;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (!ctx.hasUI) {
|
|
1057
|
+
throw new Error(`${inspection.error}; run /ahead-config in an interactive session`);
|
|
1058
|
+
}
|
|
1059
|
+
ctx.ui.notify(inspection.error, "warning");
|
|
1060
|
+
return repairProjectConfiguration(ctx, store, workflows);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
async function repairProjectConfiguration(
|
|
1064
|
+
ctx: ExtensionCommandContext,
|
|
1065
|
+
store: RunStore,
|
|
1066
|
+
workflows: WorkflowDefinition[],
|
|
1067
|
+
): Promise<boolean> {
|
|
1068
|
+
const repair = "Run setup wizard and preserve the current file as a backup";
|
|
1069
|
+
const cancel = "Cancel this new AHEAD run";
|
|
1070
|
+
const selected = await ctx.ui.select("AHEAD project configuration must be repaired", [
|
|
1071
|
+
repair,
|
|
1072
|
+
cancel,
|
|
1073
|
+
]);
|
|
1074
|
+
if (selected !== repair) {
|
|
1075
|
+
return false;
|
|
1076
|
+
}
|
|
1077
|
+
return runProjectConfigWizard(ctx, store, workflows, true);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function manageWorkItem(ctx: ExtensionCommandContext, requestedUrl: string): Promise<void> {
|
|
1081
|
+
const engine = await enginePromise;
|
|
1082
|
+
const store = storeFor(ctx);
|
|
1083
|
+
let run = await requireRun(ctx);
|
|
1084
|
+
let state = engine.deriveState(run);
|
|
1085
|
+
|
|
1086
|
+
if (requestedUrl) {
|
|
1087
|
+
run = await linkWorkItem(ctx, store, run, workItemFromUrl(requestedUrl));
|
|
1088
|
+
await refreshUi(ctx, run);
|
|
1089
|
+
state = engine.deriveState(run);
|
|
1090
|
+
ctx.ui.notify(`Linked work item to AHEAD run ${run.id}: ${state.work_item?.url}`, "info");
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (!ctx.hasUI) {
|
|
1094
|
+
throw new Error("/ahead-work-item requires a URL without interactive UI");
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const linkExisting = state.work_item
|
|
1098
|
+
? "Replace the linked work item"
|
|
1099
|
+
: "Link an existing work item";
|
|
1100
|
+
const createGitHub = "Create a GitHub issue in this repository";
|
|
1101
|
+
const show = "Show the linked work item";
|
|
1102
|
+
const choices = state.work_item
|
|
1103
|
+
? [show, linkExisting, createGitHub]
|
|
1104
|
+
: [linkExisting, createGitHub];
|
|
1105
|
+
const selected = await ctx.ui.select("AHEAD work item", choices);
|
|
1106
|
+
if (selected === show && state.work_item) {
|
|
1107
|
+
ctx.ui.notify(`${state.work_item.title ?? "Linked work item"}\n${state.work_item.url}`, "info");
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (selected === linkExisting) {
|
|
1111
|
+
const url = await ctx.ui.input("Link an existing work item", "https://…");
|
|
1112
|
+
if (!url?.trim()) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
run = await linkWorkItem(ctx, store, run, workItemFromUrl(url));
|
|
1116
|
+
} else if (selected === createGitHub) {
|
|
1117
|
+
const body = await ctx.ui.editor(
|
|
1118
|
+
`Create GitHub issue · ${run.title}`,
|
|
1119
|
+
await workItemBodyTemplate(store, run, state),
|
|
1120
|
+
);
|
|
1121
|
+
if (!body?.trim()) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const confirmed = await ctx.ui.confirm(
|
|
1125
|
+
"Create this GitHub issue?",
|
|
1126
|
+
[
|
|
1127
|
+
`Repository: ${store.projectRoot}`,
|
|
1128
|
+
`Title: ${run.title}`,
|
|
1129
|
+
"",
|
|
1130
|
+
"This writes to GitHub using the current gh authentication.",
|
|
1131
|
+
].join("\n"),
|
|
1132
|
+
);
|
|
1133
|
+
if (!confirmed) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const url = (
|
|
1137
|
+
await executeFile(
|
|
1138
|
+
"gh",
|
|
1139
|
+
["issue", "create", "--title", run.title, "--body", body],
|
|
1140
|
+
store.projectRoot,
|
|
1141
|
+
)
|
|
1142
|
+
).trim();
|
|
1143
|
+
run = await linkWorkItem(ctx, store, run, workItemFromUrl(url, run.title));
|
|
1144
|
+
} else {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
state = engine.deriveState(run);
|
|
1149
|
+
await refreshUi(ctx, run);
|
|
1150
|
+
ctx.ui.notify(`Linked work item to AHEAD run ${run.id}: ${state.work_item?.url}`, "info");
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async function linkWorkItem(
|
|
1154
|
+
ctx: ExtensionCommandContext,
|
|
1155
|
+
store: RunStore,
|
|
1156
|
+
run: Run,
|
|
1157
|
+
workItem: WorkItem,
|
|
1158
|
+
): Promise<Run> {
|
|
1159
|
+
const engine = await enginePromise;
|
|
1160
|
+
const current = engine.deriveState(run).work_item;
|
|
1161
|
+
if (current && current.url !== workItem.url && ctx.hasUI) {
|
|
1162
|
+
const replace = await ctx.ui.confirm(
|
|
1163
|
+
"Replace the linked work item?",
|
|
1164
|
+
`${current.url}\n\nwill be replaced by\n\n${workItem.url}`,
|
|
1165
|
+
);
|
|
1166
|
+
if (!replace) {
|
|
1167
|
+
return run;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (current?.url === workItem.url) {
|
|
1171
|
+
return run;
|
|
1172
|
+
}
|
|
1173
|
+
const updated = engine.applyEvent(run, humanActor(store.projectRoot), {
|
|
1174
|
+
type: "work_item_linked",
|
|
1175
|
+
work_item: workItem,
|
|
1176
|
+
});
|
|
1177
|
+
await store.save(updated);
|
|
1178
|
+
return updated;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function workItemFromUrl(value: string, title?: string): WorkItem {
|
|
1182
|
+
let url: URL;
|
|
1183
|
+
try {
|
|
1184
|
+
url = new URL(value.trim());
|
|
1185
|
+
} catch {
|
|
1186
|
+
throw new AheadEngineError("invalid_work_item_url", "work item must be an absolute URL");
|
|
1187
|
+
}
|
|
1188
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
1189
|
+
throw new AheadEngineError("invalid_work_item_url", "work item URL must use HTTP or HTTPS");
|
|
1190
|
+
}
|
|
1191
|
+
if (url.username || url.password) {
|
|
1192
|
+
throw new AheadEngineError(
|
|
1193
|
+
"invalid_work_item_url",
|
|
1194
|
+
"work item URL cannot contain embedded credentials",
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const host = url.hostname.toLowerCase();
|
|
1198
|
+
const githubIssue =
|
|
1199
|
+
host === "github.com" ? /^\/[^/]+\/[^/]+\/issues\/(\d+)\/?$/.exec(url.pathname) : null;
|
|
1200
|
+
const provider = workItemProvider(host);
|
|
1201
|
+
return {
|
|
1202
|
+
provider,
|
|
1203
|
+
url: url.toString(),
|
|
1204
|
+
...(githubIssue?.[1] ? { external_id: githubIssue[1] } : {}),
|
|
1205
|
+
...(title?.trim() ? { title: title.trim() } : {}),
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function workItemProvider(host: string): string {
|
|
1210
|
+
if (host === "github.com") {
|
|
1211
|
+
return "github";
|
|
1212
|
+
}
|
|
1213
|
+
if (host === "linear.app") {
|
|
1214
|
+
return "linear";
|
|
1215
|
+
}
|
|
1216
|
+
if (host === "dev.azure.com") {
|
|
1217
|
+
return "azure-devops";
|
|
1218
|
+
}
|
|
1219
|
+
if (host.endsWith(".atlassian.net")) {
|
|
1220
|
+
return "jira";
|
|
1221
|
+
}
|
|
1222
|
+
return host;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
async function workItemBodyTemplate(store: RunStore, run: Run, state: RunState): Promise<string> {
|
|
1226
|
+
const planPath = run.events
|
|
1227
|
+
.toReversed()
|
|
1228
|
+
.find((event) => event.type === "artifact_recorded" && event.kind === "plan")?.path;
|
|
1229
|
+
const plan = planPath
|
|
1230
|
+
? (await store.readArtifact(planPath)).trim()
|
|
1231
|
+
: "<!-- Link or summarize the approved AHEAD plan when it is ready. -->";
|
|
1232
|
+
return [
|
|
1233
|
+
"## Outcome",
|
|
1234
|
+
"",
|
|
1235
|
+
"<!-- Describe the human-owned outcome this work item should coordinate. -->",
|
|
1236
|
+
"",
|
|
1237
|
+
"## Scope and acceptance",
|
|
1238
|
+
"",
|
|
1239
|
+
"<!-- Record the accepted scope, constraints, and observable success signals. -->",
|
|
1240
|
+
"",
|
|
1241
|
+
"## Plan",
|
|
1242
|
+
"",
|
|
1243
|
+
plan,
|
|
1244
|
+
"",
|
|
1245
|
+
"---",
|
|
1246
|
+
"",
|
|
1247
|
+
`AHEAD run: \`${run.id}\``,
|
|
1248
|
+
`Workflow: \`${run.workflow_id}\``,
|
|
1249
|
+
`Current phase: \`${state.phase.id}\``,
|
|
1250
|
+
"",
|
|
1251
|
+
].join("\n");
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function executeFile(file: string, args: string[], cwd: string): Promise<string> {
|
|
1255
|
+
return new Promise((resolve, reject) => {
|
|
1256
|
+
execFile(file, args, { cwd, encoding: "utf8" }, (error, stdout, stderr) => {
|
|
1257
|
+
if (error) {
|
|
1258
|
+
reject(new Error(stderr.trim() || error.message));
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
resolve(stdout);
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
822
1266
|
async function showRecommendedSkills(ctx: ExtensionCommandContext): Promise<void> {
|
|
823
1267
|
if (!ctx.hasUI) {
|
|
824
1268
|
throw new Error("Inspecting recommended skills requires interactive or RPC UI support");
|
|
@@ -991,6 +1435,9 @@ async function startRun(ctx: ExtensionCommandContext, request: string): Promise<
|
|
|
991
1435
|
}
|
|
992
1436
|
|
|
993
1437
|
const workflows = engine.listWorkflows();
|
|
1438
|
+
if (!(await ensureProjectConfiguration(ctx, store, workflows))) {
|
|
1439
|
+
return undefined;
|
|
1440
|
+
}
|
|
994
1441
|
const parsed = parseStartRequest(
|
|
995
1442
|
request,
|
|
996
1443
|
workflows.map((workflow) => workflow.id),
|
|
@@ -1017,33 +1464,51 @@ async function startRun(ctx: ExtensionCommandContext, request: string): Promise<
|
|
|
1017
1464
|
if (!workflow) {
|
|
1018
1465
|
throw new AheadEngineError(
|
|
1019
1466
|
"workflow_required",
|
|
1020
|
-
`choose a workflow explicitly: ${workflows.map((candidate) => candidate.id).join(", ")}. Noninteractive usage: /ahead-start <workflow-id> :: <title>`,
|
|
1467
|
+
`choose a workflow explicitly: ${workflows.map((candidate) => candidate.id).join(", ")}. Noninteractive usage: /ahead-start <workflow-id> :: <title-or-work-item-url>`,
|
|
1021
1468
|
);
|
|
1022
1469
|
}
|
|
1023
1470
|
|
|
1471
|
+
const linkedTitle = parsed.workItemUrl
|
|
1472
|
+
? await resolveWorkItemTitle(parsed.workItemUrl, store.projectRoot)
|
|
1473
|
+
: undefined;
|
|
1024
1474
|
const title =
|
|
1025
1475
|
parsed.title ||
|
|
1476
|
+
linkedTitle ||
|
|
1026
1477
|
(ctx.hasUI
|
|
1027
1478
|
? await ctx.ui.input(`Enter AHEAD mode · ${workflow.title}`, "What work are you doing?")
|
|
1028
|
-
:
|
|
1479
|
+
: parsed.workItemUrl);
|
|
1029
1480
|
if (!title?.trim()) {
|
|
1030
1481
|
return undefined;
|
|
1031
1482
|
}
|
|
1032
1483
|
|
|
1033
1484
|
const owner = humanActor(store.projectRoot);
|
|
1034
|
-
|
|
1485
|
+
let run = engine.createRun({
|
|
1035
1486
|
id: store.newRunId(),
|
|
1036
1487
|
title: title.trim(),
|
|
1037
1488
|
owner,
|
|
1038
1489
|
timestamp: new Date().toISOString(),
|
|
1039
1490
|
workflow_id: workflow.id,
|
|
1491
|
+
policy: await store.policyForWorkflow(workflow.id),
|
|
1040
1492
|
});
|
|
1493
|
+
if (parsed.workItemUrl) {
|
|
1494
|
+
run = engine.applyEvent(run, owner, {
|
|
1495
|
+
type: "work_item_linked",
|
|
1496
|
+
work_item: workItemFromUrl(parsed.workItemUrl, title.trim()),
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1041
1499
|
await store.save(run);
|
|
1042
1500
|
await refreshUi(ctx, run);
|
|
1501
|
+
const state = engine.deriveState(run);
|
|
1043
1502
|
ctx.ui.notify(
|
|
1044
1503
|
[
|
|
1045
1504
|
`AHEAD mode started · ${workflow.title} · ${run.title}`,
|
|
1046
1505
|
"Human leads · AI assists",
|
|
1506
|
+
...(state.work_item ? [`Work item: ${state.work_item.url}`] : []),
|
|
1507
|
+
...(state.policy.work_items.required_before_phase
|
|
1508
|
+
? [
|
|
1509
|
+
`Project policy requires a work item before ${state.policy.work_items.required_before_phase}.`,
|
|
1510
|
+
]
|
|
1511
|
+
: []),
|
|
1047
1512
|
"This run remains active until an accountable human closes the outcome or uses /ahead-stop.",
|
|
1048
1513
|
"Continue in normal conversation. /ahead is available when you need the action menu.",
|
|
1049
1514
|
].join("\n"),
|
|
@@ -1091,6 +1556,20 @@ async function recordHumanArtifact(
|
|
|
1091
1556
|
if (!content?.trim()) {
|
|
1092
1557
|
return;
|
|
1093
1558
|
}
|
|
1559
|
+
if (artifact.kind !== "review-disposition") {
|
|
1560
|
+
const formErrors = validateArtifactForm(
|
|
1561
|
+
content,
|
|
1562
|
+
promptsForArtifact(state.workflow_id, state.phase.id, artifact.kind),
|
|
1563
|
+
);
|
|
1564
|
+
if (formErrors.length > 0) {
|
|
1565
|
+
throw new AheadEngineError(
|
|
1566
|
+
"artifact_form_incomplete",
|
|
1567
|
+
`complete these required fields before saving ${artifact.title}:\n${formErrors
|
|
1568
|
+
.map((error) => `- ${error}`)
|
|
1569
|
+
.join("\n")}`,
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1094
1573
|
await validateHumanReviewArtifact(store, state, artifact.kind, content);
|
|
1095
1574
|
const path = store.artifactPath(run, state.phase.id, artifact.kind);
|
|
1096
1575
|
const action: EventAction = {
|
|
@@ -1244,6 +1723,15 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
1244
1723
|
state = engine.deriveState(updated);
|
|
1245
1724
|
}
|
|
1246
1725
|
if (!state.can_advance) {
|
|
1726
|
+
if (state.gate.accepted && state.work_item_required_for_next_phase) {
|
|
1727
|
+
await store.save(updated);
|
|
1728
|
+
await refreshUi(ctx, updated);
|
|
1729
|
+
ctx.ui.notify(
|
|
1730
|
+
`Accepted ${state.gate.title}. Link or create the required work item before entering ${state.phase.next}.`,
|
|
1731
|
+
"info",
|
|
1732
|
+
);
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1247
1735
|
throw new AheadEngineError(
|
|
1248
1736
|
"cannot_advance",
|
|
1249
1737
|
state.blockers.join("; ") || "the phase cannot advance",
|
|
@@ -1268,6 +1756,16 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
1268
1756
|
"AHEAD work complete. The accountable human accepted the outcome and closed the run.",
|
|
1269
1757
|
"info",
|
|
1270
1758
|
);
|
|
1759
|
+
} else if (nextState.phase.id === "implement") {
|
|
1760
|
+
ctx.ui.notify(
|
|
1761
|
+
[
|
|
1762
|
+
"READY FOR IMPLEMENTATION",
|
|
1763
|
+
"The human-approved plan and any configured work-item boundary are satisfied.",
|
|
1764
|
+
"Continue now, or use /ahead and save this ready-to-implement run for a later sprint.",
|
|
1765
|
+
`Work item: ${nextState.work_item?.url ?? "not linked"}`,
|
|
1766
|
+
].join("\n"),
|
|
1767
|
+
"info",
|
|
1768
|
+
);
|
|
1271
1769
|
} else if (nextState.phase.id === "human-review") {
|
|
1272
1770
|
ctx.ui.notify(
|
|
1273
1771
|
[
|
|
@@ -1433,7 +1931,7 @@ function formatAheadHeaderField(field: string, width: number, theme: Theme): str
|
|
|
1433
1931
|
return truncateToWidth(theme.fg("text", field), width);
|
|
1434
1932
|
}
|
|
1435
1933
|
|
|
1436
|
-
const label = field.slice(0, separator).toUpperCase().padEnd(
|
|
1934
|
+
const label = field.slice(0, separator).toUpperCase().padEnd(9);
|
|
1437
1935
|
let value = field.slice(separator + 1).trimStart();
|
|
1438
1936
|
let styledValue = theme.fg("text", value);
|
|
1439
1937
|
if (label.trim() === "NEXT") {
|
|
@@ -1461,21 +1959,52 @@ async function loadInstructions(workflowId: string, phase: string): Promise<stri
|
|
|
1461
1959
|
function parseStartRequest(
|
|
1462
1960
|
request: string,
|
|
1463
1961
|
workflowIds: string[],
|
|
1464
|
-
): { workflowId?: string; title: string } {
|
|
1962
|
+
): { workflowId?: string; title: string; workItemUrl?: string } {
|
|
1465
1963
|
const trimmed = request.trim();
|
|
1466
1964
|
const separator = trimmed.indexOf("::");
|
|
1467
1965
|
if (separator >= 0) {
|
|
1966
|
+
const value = trimmed.slice(separator + 2).trim();
|
|
1468
1967
|
return {
|
|
1469
1968
|
workflowId: trimmed.slice(0, separator).trim(),
|
|
1470
|
-
title:
|
|
1969
|
+
title: isHttpUrl(value) ? "" : value,
|
|
1970
|
+
...(isHttpUrl(value) ? { workItemUrl: value } : {}),
|
|
1471
1971
|
};
|
|
1472
1972
|
}
|
|
1473
1973
|
if (workflowIds.includes(trimmed)) {
|
|
1474
1974
|
return { workflowId: trimmed, title: "" };
|
|
1475
1975
|
}
|
|
1976
|
+
if (isHttpUrl(trimmed)) {
|
|
1977
|
+
return { title: "", workItemUrl: trimmed };
|
|
1978
|
+
}
|
|
1476
1979
|
return { title: trimmed };
|
|
1477
1980
|
}
|
|
1478
1981
|
|
|
1982
|
+
function isHttpUrl(value: string): boolean {
|
|
1983
|
+
try {
|
|
1984
|
+
const url = new URL(value);
|
|
1985
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
1986
|
+
} catch {
|
|
1987
|
+
return false;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
async function resolveWorkItemTitle(value: string, root: string): Promise<string | undefined> {
|
|
1992
|
+
const workItem = workItemFromUrl(value);
|
|
1993
|
+
if (workItem.provider !== "github") {
|
|
1994
|
+
return undefined;
|
|
1995
|
+
}
|
|
1996
|
+
try {
|
|
1997
|
+
const title = await executeFile(
|
|
1998
|
+
"gh",
|
|
1999
|
+
["issue", "view", workItem.url, "--json", "title", "--jq", ".title"],
|
|
2000
|
+
root,
|
|
2001
|
+
);
|
|
2002
|
+
return title.trim() || undefined;
|
|
2003
|
+
} catch {
|
|
2004
|
+
return undefined;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1479
2008
|
function formatState(state: RunState): string {
|
|
1480
2009
|
const artifacts = state.artifacts
|
|
1481
2010
|
.map(
|
|
@@ -1486,6 +2015,8 @@ function formatState(state: RunState): string {
|
|
|
1486
2015
|
return [
|
|
1487
2016
|
`${state.title} · ${state.workflow_id}@${state.workflow_version}`,
|
|
1488
2017
|
`Phase: ${state.phase.title} (${state.phase.id}) · visit ${state.phase.visit}`,
|
|
2018
|
+
`Work item: ${state.work_item?.url ?? "none"}`,
|
|
2019
|
+
`Work item required before: ${state.policy.work_items.required_before_phase ?? "not required"}`,
|
|
1489
2020
|
`Gate: ${state.gate.id} · ${state.gate.accepted ? `accepted by ${state.gate.accepted_by?.identity}` : "open"}`,
|
|
1490
2021
|
`AI capabilities: ${state.allowed_ai_capabilities.join(", ") || "none"}`,
|
|
1491
2022
|
"Artifacts:",
|