ahead-pi 0.3.0 → 0.3.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.
Files changed (34) hide show
  1. package/README.md +16 -41
  2. package/dist/ahead_wasm.wasm +0 -0
  3. package/generated/reference/CONSTITUTION.md +2 -0
  4. package/generated/reference/docs/evidence/README.md +17 -0
  5. package/generated/reference/docs/evidence/evidence-standard.md +2 -0
  6. package/generated/reference/docs/evidence/research-map.md +2 -0
  7. package/generated/reference/docs/{references → evidence/sources}/pragmatic-programmer-page-index.md +3 -1
  8. package/generated/reference/docs/{references → evidence/sources}/submitted-engineering-notes.md +3 -1
  9. package/generated/reference/docs/guide/README.md +28 -0
  10. package/generated/reference/docs/{acceptable-ai-use.md → guide/acceptable-ai-use.md} +4 -2
  11. package/generated/reference/docs/{engineering-practice.md → guide/engineering-practice.md} +5 -3
  12. package/generated/reference/docs/{rationale.md → guide/rationale.md} +3 -1
  13. package/generated/reference/docs/{recommended-skills.md → guide/recommended-skills.md} +2 -0
  14. package/generated/reference/docs/{workflows → guide/workflows}/README.md +3 -1
  15. package/generated/reference/docs/{workflows → guide/workflows}/corrective-debugging.md +2 -0
  16. package/generated/reference/docs/{workflows → guide/workflows}/decision.md +2 -0
  17. package/generated/reference/docs/{workflows → guide/workflows}/internal-improvement.md +2 -0
  18. package/generated/reference/docs/{workflows → guide/workflows}/investigation.md +2 -0
  19. package/generated/reference/docs/{workflows → guide/workflows}/operational-stabilization.md +2 -0
  20. package/generated/reference/docs/{workflows → guide/workflows}/product-change.md +2 -0
  21. package/generated/reference/index.json +127 -132
  22. package/package.json +1 -1
  23. package/src/engine.ts +1 -1
  24. package/src/guidance.ts +3 -7
  25. package/src/index.ts +255 -20
  26. package/src/reference.ts +12 -1
  27. package/src/storage.ts +63 -3
  28. package/generated/reference/docs/design/adapted-skill-guidance.md +0 -27
  29. package/generated/reference/docs/design/debugging-and-operations.md +0 -119
  30. package/generated/reference/docs/design/executable-workflows.md +0 -122
  31. package/generated/reference/docs/design/instruction-authoring.md +0 -28
  32. package/generated/reference/docs/design/process-taxonomy.md +0 -144
  33. package/generated/reference/docs/design/review-workbench.md +0 -37
  34. package/generated/reference/docs/releasing-pi.md +0 -89
package/src/index.ts CHANGED
@@ -4,12 +4,14 @@ import type {
4
4
  ExtensionAPI,
5
5
  ExtensionCommandContext,
6
6
  ExtensionContext,
7
+ Theme,
7
8
  } from "@earendil-works/pi-coding-agent";
9
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
8
10
  import { Type } from "typebox";
9
11
  import { AheadEngine, AheadEngineError } from "./engine.js";
10
12
  import {
11
13
  buildArtifactTemplate,
12
- buildWidgetLines,
14
+ buildHeaderLines,
13
15
  nextAction,
14
16
  phaseGuide,
15
17
  phasePosition,
@@ -19,6 +21,7 @@ import {
19
21
  loadReferenceIndex,
20
22
  readReference,
21
23
  relevantReferences,
24
+ type ReferenceEntry,
22
25
  } from "./reference.js";
23
26
  import { showReferenceViewer } from "./reference-viewer.js";
24
27
  import {
@@ -71,7 +74,7 @@ const ReferenceParams = Type.Object({
71
74
 
72
75
  export default function aheadExtension(pi: ExtensionAPI): void {
73
76
  pi.registerCommand("ahead", {
74
- description: "Enter or continue the guided AHEAD mode",
77
+ description: "Enter AHEAD mode or open its action menu",
75
78
  handler: async (args, ctx) =>
76
79
  command(ctx, async () => {
77
80
  await openAheadMode(pi, args, ctx);
@@ -102,8 +105,24 @@ export default function aheadExtension(pi: ExtensionAPI): void {
102
105
  }),
103
106
  });
104
107
 
108
+ pi.registerCommand("ahead-stop", {
109
+ description: "Exit AHEAD mode; discard the unfinished record or explicitly save it",
110
+ handler: async (_args, ctx) =>
111
+ command(ctx, async () => {
112
+ await stopAheadMode(ctx);
113
+ }),
114
+ });
115
+
116
+ pi.registerCommand("ahead-resume", {
117
+ description: "Resume unfinished AHEAD work that was explicitly saved",
118
+ handler: async (args, ctx) =>
119
+ command(ctx, async () => {
120
+ await resumeSavedRun(ctx, args.trim());
121
+ }),
122
+ });
123
+
105
124
  pi.registerCommand("ahead-start", {
106
- description: "Advanced: start a workflow directly with [workflow-id ::] title",
125
+ description: "Advanced: start directly with <workflow-id> :: <title>",
107
126
  handler: async (args, ctx) =>
108
127
  command(ctx, async () => {
109
128
  await startRun(ctx, args);
@@ -208,14 +227,16 @@ export default function aheadExtension(pi: ExtensionAPI): void {
208
227
  handler: async (_args, ctx) => {
209
228
  ctx.ui.notify(
210
229
  [
211
- "/ahead [title] — choose a workflow for new work, or resume guided AHEAD mode",
230
+ "/ahead [title] — choose a workflow for new work, or open the active action menu",
212
231
  "/ahead-guide [topic] — read the applicable AHEAD framework Markdown",
213
232
  "/ahead-skills — inspect optional reviewed skills relevant to this phase",
214
233
  "/ahead-review — inspect the exact changeset and review handoff",
234
+ "/ahead-stop — leave AHEAD mode; discard the unfinished record by default or explicitly save it",
235
+ "/ahead-resume [run-id] — resume an unfinished run that you explicitly saved",
215
236
  "",
216
- "Once started, the repository run remains in AHEAD mode until an accountable human closes the outcome.",
217
- "Use normal conversation to think and work with AI. Run /ahead whenever you want the next valid action.",
218
- "The persistent guide explains what you own, what AI may do, required evidence, and what happens next.",
237
+ "Once started, the repository run remains in AHEAD mode until an accountable human closes it or uses /ahead-stop.",
238
+ "Use normal conversation to think and work with AI. AHEAD remains active and guides every turn.",
239
+ "The compact header shows the goal, required evidence, and next owner. /ahead reopens the action menu when you need a recorded action.",
219
240
  "",
220
241
  "Advanced fallback commands: /ahead-status, /ahead-record, /ahead-accept, /ahead-advance, /ahead-return.",
221
242
  "AI can record only AI/shared artifacts allowed in the active phase. It cannot accept gates, transition, approve, deploy, or close the run.",
@@ -301,7 +322,13 @@ export default function aheadExtension(pi: ExtensionAPI): void {
301
322
  phase: phase ?? null,
302
323
  workflow: workflowId ?? null,
303
324
  recommended: await relevantReferences(workflowId, phase),
304
- available: index.references.map(({ id, title, path }) => ({ id, title, path })),
325
+ available: index.references.map(({ id, title, path, audience, authority }) => ({
326
+ id,
327
+ title,
328
+ path,
329
+ audience,
330
+ authority,
331
+ })),
305
332
  instruction: "Request one reference by id, path, or title. Load only what is relevant.",
306
333
  };
307
334
  }
@@ -402,7 +429,7 @@ export default function aheadExtension(pi: ExtensionAPI): void {
402
429
  if (run && !(await enginePromise).deriveState(run).closed && ctx.hasUI) {
403
430
  const state = (await enginePromise).deriveState(run);
404
431
  ctx.ui.notify(
405
- `AHEAD mode resumed · ${state.phase.title}. Human leads, AI assists. Run /ahead for the next guided action.`,
432
+ `AHEAD mode resumed · ${state.phase.title}. Continue in normal conversation; /ahead is available when you need the action menu.`,
406
433
  "info",
407
434
  );
408
435
  }
@@ -517,6 +544,23 @@ async function openAheadMode(
517
544
  run = undefined;
518
545
  }
519
546
 
547
+ if (!run) {
548
+ if (!args.trim() && ctx.hasUI) {
549
+ const saved = await loadResumableRuns(ctx);
550
+ if (saved.length > 0) {
551
+ const choice = await ctx.ui.select("No active AHEAD run", [
552
+ "Resume explicitly saved AHEAD work",
553
+ "Start new AHEAD work",
554
+ ]);
555
+ if (choice === "Resume explicitly saved AHEAD work") {
556
+ run = await resumeSavedRun(ctx, "", saved);
557
+ } else if (choice !== "Start new AHEAD work") {
558
+ return;
559
+ }
560
+ }
561
+ }
562
+ }
563
+
520
564
  if (!run) {
521
565
  run = await startRun(ctx, args);
522
566
  if (!run) {
@@ -640,6 +684,11 @@ async function openAheadMode(
640
684
  },
641
685
  });
642
686
 
687
+ actions.push({
688
+ label: "Stop AHEAD mode",
689
+ run: async () => stopAheadMode(ctx),
690
+ });
691
+
643
692
  const selected = await ctx.ui.select(
644
693
  `AHEAD mode · ${state.phase.title}\nNext (${action.actor === "human" ? "you" : "AI"}): ${action.label}`,
645
694
  actions.map((candidate) => candidate.label),
@@ -650,6 +699,126 @@ async function openAheadMode(
650
699
  }
651
700
  }
652
701
 
702
+ async function stopAheadMode(ctx: ExtensionCommandContext): Promise<void> {
703
+ if (!ctx.hasUI) {
704
+ throw new Error("/ahead-stop requires interactive or RPC UI support");
705
+ }
706
+ const store = storeFor(ctx);
707
+ const run = await requireRun(ctx);
708
+ const state = (await enginePromise).deriveState(run);
709
+ if (state.closed) {
710
+ ctx.ui.notify("This AHEAD run is already complete.", "info");
711
+ return;
712
+ }
713
+
714
+ const discard = "Stop and discard the unfinished AHEAD record";
715
+ const save = "Stop and save the unfinished run for later";
716
+ const selected = await ctx.ui.select("Stop AHEAD mode", [discard, save]);
717
+ if (selected === discard) {
718
+ const confirmed = await ctx.ui.confirm(
719
+ "Discard this unfinished AHEAD record?",
720
+ [
721
+ `${run.title} · ${state.phase.title}`,
722
+ "",
723
+ "This removes only this run's .ahead workflow state and artifacts.",
724
+ "It does not delete, reset, or revert source code or other repository changes.",
725
+ ].join("\n"),
726
+ );
727
+ if (!confirmed) {
728
+ return;
729
+ }
730
+ await store.discardCurrent(run.id);
731
+ await refreshUi(ctx);
732
+ ctx.ui.notify(
733
+ "AHEAD mode stopped and its unfinished workflow record was discarded. Repository changes were left untouched.",
734
+ "info",
735
+ );
736
+ } else if (selected === save) {
737
+ await store.saveCurrentForResume(run.id);
738
+ await refreshUi(ctx);
739
+ ctx.ui.notify(
740
+ `AHEAD mode stopped and run ${run.id} was saved. Resume it with /ahead-resume ${run.id}.`,
741
+ "info",
742
+ );
743
+ }
744
+ }
745
+
746
+ async function resumeSavedRun(
747
+ ctx: ExtensionCommandContext,
748
+ requestedRunId: string,
749
+ supplied?: Run[],
750
+ ): Promise<Run | undefined> {
751
+ const store = storeFor(ctx);
752
+ const current = await store.loadCurrent();
753
+ if (current && !(await enginePromise).deriveState(current).closed) {
754
+ throw new AheadEngineError(
755
+ "active_run_exists",
756
+ `run ${current.id} is still active; stop it before resuming another run`,
757
+ );
758
+ }
759
+
760
+ const saved = supplied ?? (await loadResumableRuns(ctx));
761
+ if (saved.length === 0) {
762
+ ctx.ui.notify("No saved unfinished AHEAD runs are available.", "info");
763
+ return undefined;
764
+ }
765
+
766
+ let selected = requestedRunId
767
+ ? saved.find((candidate) => candidate.id === requestedRunId)
768
+ : undefined;
769
+ if (requestedRunId && !selected) {
770
+ throw new AheadEngineError(
771
+ "saved_run_not_found",
772
+ `saved unfinished run ${requestedRunId} was not found`,
773
+ );
774
+ }
775
+ if (!selected) {
776
+ if (!ctx.hasUI) {
777
+ throw new Error("/ahead-resume requires a run id without interactive UI");
778
+ }
779
+ const options = saved.map(savedRunOption);
780
+ const choice = await ctx.ui.select("Resume saved AHEAD work", options);
781
+ selected = saved.find((candidate) => savedRunOption(candidate) === choice);
782
+ }
783
+ if (!selected) {
784
+ return undefined;
785
+ }
786
+
787
+ const resumed = await store.resume(selected.id);
788
+ const state = (await enginePromise).deriveState(resumed);
789
+ await refreshUi(ctx, resumed);
790
+ ctx.ui.notify(
791
+ `AHEAD mode resumed · ${state.phase.title}. Existing artifacts and unmet gates were preserved.`,
792
+ "info",
793
+ );
794
+ return resumed;
795
+ }
796
+
797
+ async function loadResumableRuns(ctx: ExtensionContext): Promise<Run[]> {
798
+ const store = storeFor(ctx);
799
+ const engine = await enginePromise;
800
+ const resumable: Run[] = [];
801
+ const invalid: string[] = [];
802
+ for (const runId of await store.listRunIds()) {
803
+ try {
804
+ const run = await store.load(runId);
805
+ if (!engine.deriveState(run).closed) {
806
+ resumable.push(run);
807
+ }
808
+ } catch {
809
+ invalid.push(runId);
810
+ }
811
+ }
812
+ if (invalid.length > 0 && ctx.hasUI) {
813
+ ctx.ui.notify(`Ignored invalid saved AHEAD runs: ${invalid.join(", ")}`, "warning");
814
+ }
815
+ return resumable;
816
+ }
817
+
818
+ function savedRunOption(run: Run): string {
819
+ return `${run.title} · ${run.workflow_id} · ${run.id}`;
820
+ }
821
+
653
822
  async function showRecommendedSkills(ctx: ExtensionCommandContext): Promise<void> {
654
823
  if (!ctx.hasUI) {
655
824
  throw new Error("Inspecting recommended skills requires interactive or RPC UI support");
@@ -782,11 +951,11 @@ async function showAheadGuide(ctx: ExtensionCommandContext, requestedTopic: stri
782
951
  requestedTopic.trim().toLowerCase() === "all"
783
952
  ? index.references
784
953
  : await relevantReferences(workflowId, phase);
785
- const browseAll = "Browse all packaged AHEAD Markdown";
954
+ const browseAll = "Browse all practitioner and evidence Markdown";
786
955
  const selected = await ctx.ui.select(
787
956
  phase ? `AHEAD guidance · ${phase}` : "AHEAD framework guidance",
788
957
  [
789
- ...recommended.map((candidate) => candidate.title),
958
+ ...recommended.map(referenceOption),
790
959
  ...(recommended.length < index.references.length ? [browseAll] : []),
791
960
  ],
792
961
  );
@@ -796,7 +965,7 @@ async function showAheadGuide(ctx: ExtensionCommandContext, requestedTopic: stri
796
965
  if (selected === browseAll) {
797
966
  return showAheadGuide(ctx, "all");
798
967
  }
799
- entry = recommended.find((candidate) => candidate.title === selected);
968
+ entry = recommended.find((candidate) => referenceOption(candidate) === selected);
800
969
  }
801
970
  if (!entry) {
802
971
  return;
@@ -805,6 +974,11 @@ async function showAheadGuide(ctx: ExtensionCommandContext, requestedTopic: stri
805
974
  await showReferenceViewer(ctx, `AHEAD reference · ${entry.title}`, await readReference(entry));
806
975
  }
807
976
 
977
+ function referenceOption(entry: ReferenceEntry): string {
978
+ const classification = entry.audience === "evidence" ? "evidence" : entry.authority;
979
+ return `${classification} · ${entry.title}`;
980
+ }
981
+
808
982
  async function startRun(ctx: ExtensionCommandContext, request: string): Promise<Run | undefined> {
809
983
  const engine = await enginePromise;
810
984
  const store = storeFor(ctx);
@@ -836,10 +1010,15 @@ async function startRun(ctx: ExtensionCommandContext, request: string): Promise<
836
1010
  workflows.map((candidate) => candidate.title),
837
1011
  );
838
1012
  workflow = workflows.find((candidate) => candidate.title === selected);
1013
+ if (!workflow) {
1014
+ return undefined;
1015
+ }
839
1016
  }
840
- workflow ??= workflows.find((candidate) => candidate.id === "product-change");
841
1017
  if (!workflow) {
842
- throw new AheadEngineError("missing_workflow", "the engine did not provide Product Change");
1018
+ throw new AheadEngineError(
1019
+ "workflow_required",
1020
+ `choose a workflow explicitly: ${workflows.map((candidate) => candidate.id).join(", ")}. Noninteractive usage: /ahead-start <workflow-id> :: <title>`,
1021
+ );
843
1022
  }
844
1023
 
845
1024
  const title =
@@ -865,8 +1044,8 @@ async function startRun(ctx: ExtensionCommandContext, request: string): Promise<
865
1044
  [
866
1045
  `AHEAD mode started · ${workflow.title} · ${run.title}`,
867
1046
  "Human leads · AI assists",
868
- "This run remains active in the repository until an accountable human closes the outcome.",
869
- "Use /ahead for the next guided action; use normal conversation to think and work with AI.",
1047
+ "This run remains active until an accountable human closes the outcome or uses /ahead-stop.",
1048
+ "Continue in normal conversation. /ahead is available when you need the action menu.",
870
1049
  ].join("\n"),
871
1050
  "info",
872
1051
  );
@@ -925,7 +1104,7 @@ async function recordHumanArtifact(
925
1104
  await store.save(updated);
926
1105
  await refreshUi(ctx, updated);
927
1106
  ctx.ui.notify(
928
- `Saved ${artifact.title}. AHEAD mode remains active; continue the conversation or run /ahead for the next guided action.`,
1107
+ `Saved ${artifact.title}. AHEAD mode remains active; continue the conversation.`,
929
1108
  "info",
930
1109
  );
931
1110
  }
@@ -1095,13 +1274,13 @@ async function acceptAndContinue(ctx: ExtensionCommandContext): Promise<void> {
1095
1274
  "READY FOR INDEPENDENT HUMAN REVIEW",
1096
1275
  "The AI review is recorded and its material findings were disposed by a human.",
1097
1276
  "A draft branch may already exist, but a human must now request review or mark the PR ready.",
1098
- "The independent reviewer opens this repository, runs /ahead, and records the review.",
1277
+ "The independent reviewer opens this repository; AHEAD resumes at Human Review and guides the review record.",
1099
1278
  ].join("\n"),
1100
1279
  "info",
1101
1280
  );
1102
1281
  } else {
1103
1282
  ctx.ui.notify(
1104
- `Continued to ${nextState.phase.title}. AHEAD mode remains active; run /ahead for the next guided action.`,
1283
+ `Continued to ${nextState.phase.title}. AHEAD mode remains active; continue the conversation.`,
1105
1284
  "info",
1106
1285
  );
1107
1286
  }
@@ -1196,6 +1375,9 @@ async function refreshUi(ctx: ExtensionContext, supplied?: Run): Promise<void> {
1196
1375
  if (!run) {
1197
1376
  ctx.ui.setStatus("ahead", undefined);
1198
1377
  ctx.ui.setWidget("ahead", undefined);
1378
+ if (ctx.mode === "tui") {
1379
+ ctx.ui.setHeader(undefined);
1380
+ }
1199
1381
  return;
1200
1382
  }
1201
1383
  const engine = await enginePromise;
@@ -1209,7 +1391,60 @@ async function refreshUi(ctx: ExtensionContext, supplied?: Run): Promise<void> {
1209
1391
  ? `AHEAD · complete · ${state.workflow_id}`
1210
1392
  : `AHEAD · ${position.current}/${position.total} · ${state.phase.id} · ${action.actor} action`,
1211
1393
  );
1212
- ctx.ui.setWidget("ahead", buildWidgetLines(run, state, workflow), { placement: "aboveEditor" });
1394
+ if (state.closed) {
1395
+ ctx.ui.setWidget("ahead", undefined);
1396
+ if (ctx.mode === "tui") {
1397
+ ctx.ui.setHeader(undefined);
1398
+ }
1399
+ return;
1400
+ }
1401
+
1402
+ const lines = buildHeaderLines(run, state, workflow);
1403
+ if (ctx.mode === "tui") {
1404
+ ctx.ui.setWidget("ahead", undefined);
1405
+ ctx.ui.setHeader((_tui, theme) => aheadHeader(lines, theme));
1406
+ } else {
1407
+ ctx.ui.setWidget("ahead", lines, { placement: "aboveEditor" });
1408
+ }
1409
+ }
1410
+
1411
+ function aheadHeader(lines: string[], theme: Theme) {
1412
+ return {
1413
+ render(width: number): string[] {
1414
+ const [heading = "AHEAD", ...fields] = lines;
1415
+ const styledHeading = `${theme.fg("accent", theme.bold("AHEAD"))}${theme.fg("muted", heading.slice("AHEAD".length))}`;
1416
+ const ruleWidth = Math.max(0, width - visibleWidth(styledHeading) - 1);
1417
+ const headingLine = `${styledHeading}${
1418
+ ruleWidth > 0 ? ` ${theme.fg("borderMuted", "─".repeat(ruleWidth))}` : ""
1419
+ }`;
1420
+
1421
+ return [
1422
+ truncateToWidth(headingLine, width),
1423
+ ...fields.map((field) => formatAheadHeaderField(field, width, theme)),
1424
+ ];
1425
+ },
1426
+ invalidate() {},
1427
+ };
1428
+ }
1429
+
1430
+ function formatAheadHeaderField(field: string, width: number, theme: Theme): string {
1431
+ const separator = field.indexOf(":");
1432
+ if (separator < 0) {
1433
+ return truncateToWidth(theme.fg("text", field), width);
1434
+ }
1435
+
1436
+ const label = field.slice(0, separator).toUpperCase().padEnd(8);
1437
+ let value = field.slice(separator + 1).trimStart();
1438
+ let styledValue = theme.fg("text", value);
1439
+ if (label.trim() === "NEXT") {
1440
+ const actor = value.startsWith("You →") ? "You →" : value.startsWith("AI →") ? "AI →" : "";
1441
+ if (actor) {
1442
+ value = value.slice(actor.length);
1443
+ styledValue = `${theme.fg(actor.startsWith("You") ? "success" : "accent", theme.bold(actor))}${theme.fg("text", value)}`;
1444
+ }
1445
+ }
1446
+
1447
+ return truncateToWidth(`${theme.fg("muted", theme.bold(label))} ${styledValue}`, width);
1213
1448
  }
1214
1449
 
1215
1450
  async function loadInstructions(workflowId: string, phase: string): Promise<string> {
package/src/reference.ts CHANGED
@@ -8,11 +8,15 @@ export interface ReferenceEntry {
8
8
  path: string;
9
9
  title: string;
10
10
  summary: string;
11
+ audience: "practitioner" | "evidence";
12
+ authority: "binding" | "guidance" | "supporting";
13
+ distribution: "agent-and-human";
11
14
  phases: string[];
12
15
  workflows: string[];
13
16
  }
14
17
 
15
18
  interface ReferenceIndex {
19
+ api_version: "ahead.references/v0.1";
16
20
  generated_from: string[];
17
21
  references: ReferenceEntry[];
18
22
  }
@@ -75,9 +79,11 @@ function parseReferenceIndex(content: string): ReferenceIndex {
75
79
  if (!isRecord(value)) {
76
80
  throw new Error("Invalid packaged AHEAD reference index");
77
81
  }
82
+ const apiVersion = value.api_version;
78
83
  const generatedFrom = value.generated_from;
79
84
  const references = value.references;
80
85
  if (
86
+ apiVersion !== "ahead.references/v0.1" ||
81
87
  !Array.isArray(generatedFrom) ||
82
88
  !generatedFrom.every((entry) => typeof entry === "string") ||
83
89
  !Array.isArray(references) ||
@@ -85,7 +91,7 @@ function parseReferenceIndex(content: string): ReferenceIndex {
85
91
  ) {
86
92
  throw new Error("Invalid packaged AHEAD reference index");
87
93
  }
88
- return { generated_from: generatedFrom, references };
94
+ return { api_version: apiVersion, generated_from: generatedFrom, references };
89
95
  }
90
96
 
91
97
  function isReferenceEntry(value: unknown): value is ReferenceEntry {
@@ -95,6 +101,11 @@ function isReferenceEntry(value: unknown): value is ReferenceEntry {
95
101
  typeof value.path === "string" &&
96
102
  typeof value.title === "string" &&
97
103
  typeof value.summary === "string" &&
104
+ (value.audience === "practitioner" || value.audience === "evidence") &&
105
+ (value.authority === "binding" ||
106
+ value.authority === "guidance" ||
107
+ value.authority === "supporting") &&
108
+ value.distribution === "agent-and-human" &&
98
109
  Array.isArray(value.phases) &&
99
110
  value.phases.every((phase) => typeof phase === "string") &&
100
111
  Array.isArray(value.workflows) &&
package/src/storage.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, relative, resolve } from "node:path";
5
5
  import type { Actor, Run } from "./types.js";
6
6
 
@@ -49,6 +49,39 @@ export class RunStore {
49
49
  }
50
50
  }
51
51
 
52
+ async listRunIds(): Promise<string[]> {
53
+ try {
54
+ const entries = await readdir(join(this.aheadDirectory, "runs"), { withFileTypes: true });
55
+ return entries
56
+ .filter((entry) => entry.isDirectory() && isSafeRunId(entry.name))
57
+ .map((entry) => entry.name)
58
+ .toSorted((left, right) => right.localeCompare(left));
59
+ } catch (error) {
60
+ if (isMissing(error)) {
61
+ return [];
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ async saveCurrentForResume(runId: string): Promise<void> {
68
+ await this.load(runId);
69
+ await this.clearCurrent(runId);
70
+ }
71
+
72
+ async resume(runId: string): Promise<Run> {
73
+ const run = await this.load(runId);
74
+ const pointer: CurrentRunPointer = { api_version: "ahead.current/v0", run_id: runId };
75
+ await atomicJson(join(this.aheadDirectory, "current.json"), pointer);
76
+ return run;
77
+ }
78
+
79
+ async discardCurrent(runId: string): Promise<void> {
80
+ const directory = this.runDirectory(runId);
81
+ await this.clearCurrent(runId);
82
+ await rm(directory, { recursive: true, force: false });
83
+ }
84
+
52
85
  artifactPath(run: Run, phase: string, kind: string): { absolute: string; relative: string } {
53
86
  const sequence = String(run.events.length + 1).padStart(4, "0");
54
87
  const absolute = join(
@@ -81,10 +114,33 @@ export class RunStore {
81
114
  }
82
115
 
83
116
  private runPath(runId: string): string {
84
- if (!/^[A-Za-z0-9._-]+$/.test(runId)) {
117
+ return join(this.runDirectory(runId), "run.json");
118
+ }
119
+
120
+ private runDirectory(runId: string): string {
121
+ if (!isSafeRunId(runId)) {
85
122
  throw new Error("unsafe AHEAD run id");
86
123
  }
87
- return join(this.aheadDirectory, "runs", runId, "run.json");
124
+ return join(this.aheadDirectory, "runs", runId);
125
+ }
126
+
127
+ private async clearCurrent(expectedRunId: string): Promise<void> {
128
+ const path = join(this.aheadDirectory, "current.json");
129
+ let pointer: CurrentRunPointer;
130
+ try {
131
+ pointer = parseCurrentRunPointer(await readFile(path, "utf8"));
132
+ } catch (error) {
133
+ if (isMissing(error)) {
134
+ return;
135
+ }
136
+ throw error;
137
+ }
138
+ if (pointer.run_id !== expectedRunId) {
139
+ throw new Error(
140
+ `active AHEAD run changed from ${expectedRunId} to ${pointer.run_id}; stop or resume again`,
141
+ );
142
+ }
143
+ await unlink(path);
88
144
  }
89
145
  }
90
146
 
@@ -137,6 +193,10 @@ function isMissing(error: unknown): boolean {
137
193
  return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
138
194
  }
139
195
 
196
+ function isSafeRunId(runId: string): boolean {
197
+ return runId !== "." && runId !== ".." && /^[A-Za-z0-9._-]+$/.test(runId);
198
+ }
199
+
140
200
  function parseCurrentRunPointer(content: string): CurrentRunPointer {
141
201
  const value: unknown = JSON.parse(content);
142
202
  if (
@@ -1,27 +0,0 @@
1
- # Adapted Skill Guidance
2
-
3
- Status: approved first adaptation set, 2026-08-12
4
-
5
- AHEAD reviewed the [Matt Pocock skills collection](https://www.skills.sh/mattpocock/skills), including the specifically discussed [grill-me](https://www.skills.sh/mattpocock/skills/grill-me), [prototype](https://www.skills.sh/mattpocock/skills/prototype), and [ask-matt](https://www.skills.sh/mattpocock/skills/ask-matt) skills. It also reviewed [Ponytail](https://github.com/DietrichGebert/ponytail/tree/2ed6c52c9d7e5e56942508591085fd45dea277d3/skills/ponytail). The useful ideas were treated as design input, not installed wholesale or made authoritative.
6
-
7
- ## Adapted into AHEAD now
8
-
9
- - Guided questioning became a dependency-frontier method: AI discovers facts, humans answer consequential judgment questions, and rounds scale with risk.
10
- - Research became provenance inside existing workflow artifacts: primary sources, direct observations, contradictions, applicability, and uncertainty.
11
- - Planning became human-first vertical decomposition with dependencies, acceptance criteria, rollout, recovery, and expand-migrate-verify-contract stages for broad changes.
12
- - Debugging gained tighter safe feedback loops, minimized reproductions, ranked falsifiable hypotheses, one-variable probes, tagged instrumentation, regression evidence, and an explicit no-safe-reproduction path.
13
- - Prototyping became an Investigation technique with an explicit learning question, deliberately disposable code, visible outcomes, and mandatory human disposition. Prototype code cannot be promoted directly.
14
- - Review became an exact-changeset workbench with structured AI findings, a separate implementing-human disposition, and later independent human judgment.
15
- - Instruction design became progressive disclosure: binding profile, applicable phase and method fragments, then on-demand framework references.
16
-
17
- These adaptations live in AHEAD's own workflow specs, `policy/methods`, and host-neutral contracts. They require only artifacts already justified by an AHEAD phase; no source skill's private artifact system or issue format was imported.
18
-
19
- ## Deliberately not adopted
20
-
21
- - Persona emulation such as “ask Matt” is not framework authority or a substitute for project evidence.
22
- - No skill may skip the human's initial model, decision, plan, understanding, tests, or review.
23
- - A quick, lazy, or persistent implementation is not production-ready merely because it runs.
24
- - AHEAD does not silently install third-party skills, create extra artifacts, post GitHub comments, create issues, push code, or mark a pull request ready.
25
- - Issue intake, domain or architecture specialization, formal understanding handoff, and wayfinding remain deferred until their AHEAD-native process is separately approved.
26
-
27
- The recommendation catalog records external code AHEAD suggests installing. The method overlays record guidance AHEAD owns. Keeping those separate allows a project to use any editor or model without depending on a particular skill package.