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.
- package/README.md +16 -41
- package/dist/ahead_wasm.wasm +0 -0
- package/generated/reference/CONSTITUTION.md +2 -0
- package/generated/reference/docs/evidence/README.md +17 -0
- package/generated/reference/docs/evidence/evidence-standard.md +2 -0
- package/generated/reference/docs/evidence/research-map.md +2 -0
- package/generated/reference/docs/{references → evidence/sources}/pragmatic-programmer-page-index.md +3 -1
- package/generated/reference/docs/{references → evidence/sources}/submitted-engineering-notes.md +3 -1
- package/generated/reference/docs/guide/README.md +28 -0
- package/generated/reference/docs/{acceptable-ai-use.md → guide/acceptable-ai-use.md} +4 -2
- package/generated/reference/docs/{engineering-practice.md → guide/engineering-practice.md} +5 -3
- package/generated/reference/docs/{rationale.md → guide/rationale.md} +3 -1
- package/generated/reference/docs/{recommended-skills.md → guide/recommended-skills.md} +2 -0
- package/generated/reference/docs/{workflows → guide/workflows}/README.md +3 -1
- package/generated/reference/docs/{workflows → guide/workflows}/corrective-debugging.md +2 -0
- package/generated/reference/docs/{workflows → guide/workflows}/decision.md +2 -0
- package/generated/reference/docs/{workflows → guide/workflows}/internal-improvement.md +2 -0
- package/generated/reference/docs/{workflows → guide/workflows}/investigation.md +2 -0
- package/generated/reference/docs/{workflows → guide/workflows}/operational-stabilization.md +2 -0
- package/generated/reference/docs/{workflows → guide/workflows}/product-change.md +2 -0
- package/generated/reference/index.json +127 -132
- package/package.json +1 -1
- package/src/engine.ts +1 -1
- package/src/guidance.ts +3 -7
- package/src/index.ts +255 -20
- package/src/reference.ts +12 -1
- package/src/storage.ts +63 -3
- package/generated/reference/docs/design/adapted-skill-guidance.md +0 -27
- package/generated/reference/docs/design/debugging-and-operations.md +0 -119
- package/generated/reference/docs/design/executable-workflows.md +0 -122
- package/generated/reference/docs/design/instruction-authoring.md +0 -28
- package/generated/reference/docs/design/process-taxonomy.md +0 -144
- package/generated/reference/docs/design/review-workbench.md +0 -37
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
217
|
-
"Use normal conversation to think and work with AI.
|
|
218
|
-
"The
|
|
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 }) => ({
|
|
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}.
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
869
|
-
"
|
|
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
|
|
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
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|