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.
Files changed (86) hide show
  1. package/README.md +29 -1
  2. package/dist/ahead_wasm.wasm +0 -0
  3. package/generated/corrective-debugging/ai-audit.md +4 -2
  4. package/generated/corrective-debugging/ai-review.md +4 -2
  5. package/generated/corrective-debugging/characterize.md +4 -2
  6. package/generated/corrective-debugging/conclude.md +4 -2
  7. package/generated/corrective-debugging/correction.md +4 -2
  8. package/generated/corrective-debugging/deploy.md +4 -2
  9. package/generated/corrective-debugging/human-review.md +4 -2
  10. package/generated/corrective-debugging/implement.md +4 -2
  11. package/generated/corrective-debugging/investigate.md +4 -2
  12. package/generated/corrective-debugging/model.md +4 -2
  13. package/generated/corrective-debugging/outcome.md +4 -2
  14. package/generated/corrective-debugging/plan.md +4 -2
  15. package/generated/corrective-debugging/verify.md +4 -2
  16. package/generated/decision/compare.md +4 -2
  17. package/generated/decision/criteria.md +4 -2
  18. package/generated/decision/decide.md +4 -2
  19. package/generated/decision/frame.md +4 -2
  20. package/generated/decision/options.md +4 -2
  21. package/generated/decision/publish.md +4 -2
  22. package/generated/decision/research.md +4 -2
  23. package/generated/internal-improvement/ai-audit.md +4 -2
  24. package/generated/internal-improvement/ai-review.md +4 -2
  25. package/generated/internal-improvement/baseline.md +4 -2
  26. package/generated/internal-improvement/decision.md +4 -2
  27. package/generated/internal-improvement/deploy.md +4 -2
  28. package/generated/internal-improvement/human-review.md +4 -2
  29. package/generated/internal-improvement/implement.md +4 -2
  30. package/generated/internal-improvement/invariants.md +4 -2
  31. package/generated/internal-improvement/options.md +4 -2
  32. package/generated/internal-improvement/outcome.md +4 -2
  33. package/generated/internal-improvement/plan.md +4 -2
  34. package/generated/internal-improvement/target.md +4 -2
  35. package/generated/internal-improvement/verify.md +4 -2
  36. package/generated/investigation/bound.md +4 -2
  37. package/generated/investigation/conclude.md +4 -2
  38. package/generated/investigation/explore.md +4 -2
  39. package/generated/investigation/frame.md +4 -2
  40. package/generated/investigation/gather.md +4 -2
  41. package/generated/investigation/synthesize.md +4 -2
  42. package/generated/operational-stabilization/assess.md +4 -2
  43. package/generated/operational-stabilization/execute-observe.md +4 -2
  44. package/generated/operational-stabilization/monitor.md +4 -2
  45. package/generated/operational-stabilization/outcome.md +4 -2
  46. package/generated/operational-stabilization/respond.md +4 -2
  47. package/generated/operational-stabilization/verify-recovery.md +4 -2
  48. package/generated/product-change/ai-audit.md +4 -2
  49. package/generated/product-change/ai-review.md +4 -2
  50. package/generated/product-change/decision.md +4 -2
  51. package/generated/product-change/define.md +4 -2
  52. package/generated/product-change/deploy.md +4 -2
  53. package/generated/product-change/human-review.md +4 -2
  54. package/generated/product-change/implement.md +4 -2
  55. package/generated/product-change/options.md +4 -2
  56. package/generated/product-change/outcome.md +4 -2
  57. package/generated/product-change/plan.md +4 -2
  58. package/generated/product-change/questions.md +4 -2
  59. package/generated/product-change/research.md +4 -2
  60. package/generated/product-change/verify.md +4 -2
  61. package/generated/reference/CONSTITUTION.md +4 -0
  62. package/generated/reference/docs/evidence/README.md +1 -0
  63. package/generated/reference/docs/evidence/research-map.md +12 -0
  64. package/generated/reference/docs/evidence/sources/tigerstyle.md +65 -0
  65. package/generated/reference/docs/guide/README.md +2 -1
  66. package/generated/reference/docs/guide/engineering-practice.md +35 -13
  67. package/generated/reference/docs/guide/work-items.md +65 -0
  68. package/generated/reference/docs/guide/workflows/README.md +1 -0
  69. package/generated/reference/index.json +28 -0
  70. package/generated/skills/diagnosing-bugs/LICENSE.ahead +21 -0
  71. package/generated/skills/diagnosing-bugs/LICENSE.mattpocock +21 -0
  72. package/generated/skills/diagnosing-bugs/SKILL.md +126 -0
  73. package/generated/skills/manifest.json +21 -0
  74. package/generated/skills/research/LICENSE.ahead +21 -0
  75. package/generated/skills/research/LICENSE.mattpocock +21 -0
  76. package/generated/skills/research/SKILL.md +116 -0
  77. package/generated/skills/to-tickets/LICENSE.ahead +21 -0
  78. package/generated/skills/to-tickets/LICENSE.mattpocock +21 -0
  79. package/generated/skills/to-tickets/SKILL.md +132 -0
  80. package/package.json +4 -1
  81. package/src/config.ts +171 -0
  82. package/src/engine.ts +2 -0
  83. package/src/guidance.ts +60 -6
  84. package/src/index.ts +540 -9
  85. package/src/storage.ts +153 -2
  86. 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 { Actor, Capability, EventAction, Run, RunState } from "./types.js";
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 directly with <workflow-id> :: <title>",
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 (missingRequired.length === 0 && !action.artifactKind) {
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
- : undefined);
1479
+ : parsed.workItemUrl);
1029
1480
  if (!title?.trim()) {
1030
1481
  return undefined;
1031
1482
  }
1032
1483
 
1033
1484
  const owner = humanActor(store.projectRoot);
1034
- const run = engine.createRun({
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(8);
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: trimmed.slice(separator + 2).trim(),
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:",