ahead-pi 0.8.1 → 0.8.3
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/package.json +1 -1
- package/src/guidance.ts +30 -10
- package/src/index.ts +181 -155
package/package.json
CHANGED
package/src/guidance.ts
CHANGED
|
@@ -533,8 +533,10 @@ export function buildArtifactTemplate(
|
|
|
533
533
|
}
|
|
534
534
|
|
|
535
535
|
/**
|
|
536
|
-
* Insert inspiration-only example lines into each field as
|
|
537
|
-
*
|
|
536
|
+
* Insert inspiration-only example lines into each field as plain text with
|
|
537
|
+
* an "ex. - " prefix. Validation strips these lines for the emptiness
|
|
538
|
+
* check and rejects any saved form that still contains them, so no example
|
|
539
|
+
* line can ever persist in a recorded artifact.
|
|
538
540
|
*/
|
|
539
541
|
export function insertFieldExamples(template: string, perField: string[][]): string {
|
|
540
542
|
let updated = template;
|
|
@@ -548,16 +550,27 @@ export function insertFieldExamples(template: string, perField: string[][]): str
|
|
|
548
550
|
}
|
|
549
551
|
updated = updated.replace(
|
|
550
552
|
marker,
|
|
551
|
-
[
|
|
552
|
-
marker,
|
|
553
|
-
"<!-- Examples — inspiration only, not requirements. Delete them and write your own answer. -->",
|
|
554
|
-
...examples.map((example) => `<!-- ~ ${example} ~ -->`),
|
|
555
|
-
].join("\n"),
|
|
553
|
+
[marker, ...examples.map((example) => `ex. - ${example}`)].join("\n"),
|
|
556
554
|
);
|
|
557
555
|
}
|
|
558
556
|
return updated;
|
|
559
557
|
}
|
|
560
558
|
|
|
559
|
+
const EXAMPLE_LINE_PATTERN = /^\s*ex\.\s*-\s?/;
|
|
560
|
+
|
|
561
|
+
function stripExampleLines(text: string): { cleaned: string; hadExamples: boolean } {
|
|
562
|
+
const kept: string[] = [];
|
|
563
|
+
let hadExamples = false;
|
|
564
|
+
for (const line of text.split("\n")) {
|
|
565
|
+
if (EXAMPLE_LINE_PATTERN.test(line)) {
|
|
566
|
+
hadExamples = true;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
kept.push(line);
|
|
570
|
+
}
|
|
571
|
+
return { cleaned: kept.join("\n").trim(), hadExamples };
|
|
572
|
+
}
|
|
573
|
+
|
|
561
574
|
export function validateArtifactForm(content: string, prompts: string[]): string[] {
|
|
562
575
|
const errors: string[] = [];
|
|
563
576
|
for (const [index, prompt] of prompts.entries()) {
|
|
@@ -575,11 +588,18 @@ export function validateArtifactForm(content: string, prompts: string[]): string
|
|
|
575
588
|
.slice(beginIndex + begin.length, endIndex)
|
|
576
589
|
.replace(/<!--[\s\S]*?-->/g, "")
|
|
577
590
|
.trim();
|
|
578
|
-
|
|
579
|
-
|
|
591
|
+
const { cleaned, hadExamples } = stripExampleLines(response);
|
|
592
|
+
if (!cleaned) {
|
|
593
|
+
errors.push(
|
|
594
|
+
hadExamples ? `${prompt} (replace the “ex. -” example lines with your own answer)` : prompt,
|
|
595
|
+
);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (hadExamples) {
|
|
599
|
+
errors.push(`${prompt} (remove the leftover “ex. -” example lines)`);
|
|
580
600
|
continue;
|
|
581
601
|
}
|
|
582
|
-
if (/^(?:n\/?a|not applicable)\s*[.!]?$/i.test(
|
|
602
|
+
if (/^(?:n\/?a|not applicable)\s*[.!]?$/i.test(cleaned)) {
|
|
583
603
|
errors.push(`${prompt} (explain why it is not applicable)`);
|
|
584
604
|
}
|
|
585
605
|
}
|
package/src/index.ts
CHANGED
|
@@ -648,186 +648,200 @@ async function openAheadMode(
|
|
|
648
648
|
run = await linkWorkItem(ctx, store, run, workItemFromUrl(args));
|
|
649
649
|
}
|
|
650
650
|
|
|
651
|
-
await refreshUi(ctx, run);
|
|
652
651
|
if (!ctx.hasUI) {
|
|
653
652
|
ctx.ui.notify(formatState(engine.deriveState(run)), "info");
|
|
654
653
|
return;
|
|
655
654
|
}
|
|
656
655
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
(
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
656
|
+
// Action-menu loop: the run is reloaded and the state re-derived after
|
|
657
|
+
// every action, then the menu reappears — help screens and completed
|
|
658
|
+
// actions never strand the user. Escaping the menu, or an action that
|
|
659
|
+
// clears the run, leaves the wizard.
|
|
660
|
+
while (true) {
|
|
661
|
+
const current = await store.loadCurrent();
|
|
662
|
+
if (!current) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
run = current;
|
|
666
|
+
await refreshUi(ctx, run);
|
|
667
|
+
const state = engine.deriveState(run);
|
|
668
|
+
const workflow = engine.getWorkflow(run.workflow_id);
|
|
669
|
+
const guidance = phaseGuide(run.workflow_id, state.phase.id);
|
|
670
|
+
const action = nextAction(state, workflow);
|
|
671
|
+
const actions: GuidedAction[] = [];
|
|
672
|
+
const missingRequired = state.artifacts.filter(
|
|
673
|
+
(artifact) => artifact.required && !artifact.present,
|
|
668
674
|
);
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
? state.phase.id === "ai-review"
|
|
674
|
-
? async () => openReviewWorkbench(pi, ctx)
|
|
675
|
-
: async () => requestAiAssistance(pi, state, action.artifactKind)
|
|
676
|
-
: async () => recordHumanArtifact(ctx, action.artifactKind ?? ""),
|
|
677
|
-
});
|
|
678
|
-
if (action.actor === "ai" && actionArtifact?.actor === "any") {
|
|
675
|
+
if (action.artifactKind) {
|
|
676
|
+
const actionArtifact = state.artifacts.find(
|
|
677
|
+
(artifact) => artifact.kind === action.artifactKind,
|
|
678
|
+
);
|
|
679
679
|
actions.push({
|
|
680
|
-
label:
|
|
681
|
-
run:
|
|
680
|
+
label: action.label,
|
|
681
|
+
run:
|
|
682
|
+
action.actor === "ai"
|
|
683
|
+
? state.phase.id === "ai-review"
|
|
684
|
+
? async () => openReviewWorkbench(pi, ctx)
|
|
685
|
+
: async () => requestAiAssistance(pi, state, action.artifactKind)
|
|
686
|
+
: async () => recordHumanArtifact(ctx, action.artifactKind ?? ""),
|
|
682
687
|
});
|
|
688
|
+
if (action.actor === "ai" && actionArtifact?.actor === "any") {
|
|
689
|
+
actions.push({
|
|
690
|
+
label: `Write ${actionArtifact.title} yourself`,
|
|
691
|
+
run: async () => recordHumanArtifact(ctx, actionArtifact.kind),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
683
694
|
}
|
|
684
|
-
}
|
|
685
695
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
696
|
+
if (action.optional) {
|
|
697
|
+
const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor !== "ai");
|
|
698
|
+
if (nextHumanArtifact) {
|
|
699
|
+
actions.push({
|
|
700
|
+
label: `Continue without optional AI challenge · Write ${nextHumanArtifact.title}`,
|
|
701
|
+
run: async () => recordHumanArtifact(ctx, nextHumanArtifact.kind),
|
|
702
|
+
});
|
|
703
|
+
} else if (missingRequired.length === 0) {
|
|
704
|
+
actions.push({
|
|
705
|
+
label: `Continue without optional AI contribution · Accept ${state.gate.title}`,
|
|
706
|
+
run: async () => acceptAndContinue(ctx),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (state.work_item_required_for_next_phase && state.gate.accepted) {
|
|
689
712
|
actions.push({
|
|
690
|
-
label:
|
|
691
|
-
run: async () =>
|
|
713
|
+
label: action.label,
|
|
714
|
+
run: async () => manageWorkItem(ctx, ""),
|
|
692
715
|
});
|
|
693
|
-
} else if (missingRequired.length === 0) {
|
|
716
|
+
} else if (missingRequired.length === 0 && !action.artifactKind) {
|
|
694
717
|
actions.push({
|
|
695
|
-
label:
|
|
718
|
+
label: state.gate.accepted ? action.label : `Accept and continue · ${state.gate.title}`,
|
|
696
719
|
run: async () => acceptAndContinue(ctx),
|
|
697
720
|
});
|
|
698
721
|
}
|
|
699
|
-
}
|
|
700
722
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
723
|
+
if (
|
|
724
|
+
state.allowed_ai_capabilities.length > 0 &&
|
|
725
|
+
action.actor !== "ai" &&
|
|
726
|
+
!missingRequired.some((artifact) => artifact.actor === "ai") &&
|
|
727
|
+
state.phase.id !== "implement"
|
|
728
|
+
) {
|
|
729
|
+
actions.push({
|
|
730
|
+
label: `Ask AI to assist · ${state.phase.title}`,
|
|
731
|
+
run: async () => requestAiAssistance(pi, state),
|
|
732
|
+
});
|
|
733
|
+
}
|
|
712
734
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
735
|
+
if (state.phase.id === "implement") {
|
|
736
|
+
if (!state.artifacts.some((artifact) => artifact.kind === "changeset" && artifact.present)) {
|
|
737
|
+
actions.push({
|
|
738
|
+
label: "Save this ready-to-implement run for a later sprint",
|
|
739
|
+
run: async () => saveImplementationHandoff(ctx),
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
actions.push({
|
|
743
|
+
label: "Ask AI for help understanding or solving a problem",
|
|
744
|
+
run: async () => askImplementationQuestion(pi, ctx, state),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
724
747
|
|
|
725
|
-
|
|
726
|
-
if (!state.artifacts.some((artifact) => artifact.kind === "changeset" && artifact.present)) {
|
|
748
|
+
if (state.phase.id === "ai-review" || state.phase.id === "human-review") {
|
|
727
749
|
actions.push({
|
|
728
|
-
label: "
|
|
729
|
-
run: async () =>
|
|
750
|
+
label: "Open the changeset review workbench",
|
|
751
|
+
run: async () => openReviewWorkbench(pi, ctx),
|
|
730
752
|
});
|
|
731
753
|
}
|
|
732
|
-
actions.push({
|
|
733
|
-
label: "Ask AI for help understanding or solving a problem",
|
|
734
|
-
run: async () => askImplementationQuestion(pi, ctx, state),
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
754
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
755
|
+
if (
|
|
756
|
+
state.allowed_ai_capabilities.length > 0 &&
|
|
757
|
+
state.artifacts.some(
|
|
758
|
+
(artifact) => artifact.present && artifact.recorded_by?.kind === "human" && artifact.path,
|
|
759
|
+
)
|
|
760
|
+
) {
|
|
761
|
+
actions.push({
|
|
762
|
+
label: "Ask AI to challenge the latest artifact",
|
|
763
|
+
run: async () => {
|
|
764
|
+
const artifact = [...state.artifacts]
|
|
765
|
+
.toReversed()
|
|
766
|
+
.find(
|
|
767
|
+
(candidate) =>
|
|
768
|
+
candidate.present && candidate.recorded_by?.kind === "human" && candidate.path,
|
|
769
|
+
);
|
|
770
|
+
if (!artifact?.path) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
pi.sendUserMessage(
|
|
774
|
+
[
|
|
775
|
+
`AHEAD mode: challenge my ${artifact.title} artifact before I accept the gate.`,
|
|
776
|
+
`Read ${artifact.path} and name the 2-3 weakest points: missing risks, vague claims, or things that would not survive implementation.`,
|
|
777
|
+
"Be specific and brief. Do not rewrite the artifact; I stay the author.",
|
|
778
|
+
].join("\n"),
|
|
779
|
+
);
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (state.return_targets.length > 0) {
|
|
785
|
+
actions.push({
|
|
786
|
+
label: "Return to an earlier phase",
|
|
787
|
+
run: async () => returnToEarlierPhase(ctx, ""),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (!state.work_item_required_for_next_phase || !state.gate.accepted) {
|
|
792
|
+
actions.push({
|
|
793
|
+
label: state.work_item
|
|
794
|
+
? "View or replace the linked work item"
|
|
795
|
+
: "Link or create a work item",
|
|
796
|
+
run: async () => manageWorkItem(ctx, ""),
|
|
797
|
+
});
|
|
798
|
+
}
|
|
744
799
|
|
|
745
|
-
if (
|
|
746
|
-
state.allowed_ai_capabilities.length > 0 &&
|
|
747
|
-
state.artifacts.some(
|
|
748
|
-
(artifact) => artifact.present && artifact.recorded_by?.kind === "human" && artifact.path,
|
|
749
|
-
)
|
|
750
|
-
) {
|
|
751
800
|
actions.push({
|
|
752
|
-
label: "
|
|
801
|
+
label: "Help · policy, guidance, skills, phase explanation",
|
|
753
802
|
run: async () => {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
803
|
+
// "← Back" and escaping both return to the action-menu loop above.
|
|
804
|
+
const helpChoice = await ctx.ui.select("Help · pick one", [
|
|
805
|
+
"← Back to action menu",
|
|
806
|
+
"Configure project AHEAD policy for future runs",
|
|
807
|
+
"Read AHEAD framework guidance for this phase",
|
|
808
|
+
"Inspect optional skills reviewed for this phase",
|
|
809
|
+
"Explain this phase and its expectations",
|
|
810
|
+
]);
|
|
811
|
+
if (helpChoice === "Configure project AHEAD policy for future runs") {
|
|
812
|
+
await manageProjectConfig(ctx);
|
|
813
|
+
} else if (helpChoice === "Read AHEAD framework guidance for this phase") {
|
|
814
|
+
await showAheadGuide(ctx, "");
|
|
815
|
+
} else if (helpChoice === "Inspect optional skills reviewed for this phase") {
|
|
816
|
+
await showRecommendedSkills(ctx);
|
|
817
|
+
} else if (helpChoice === "Explain this phase and its expectations") {
|
|
818
|
+
ctx.ui.notify(
|
|
819
|
+
[
|
|
820
|
+
state.phase.title,
|
|
821
|
+
`Goal: ${guidance.objective}`,
|
|
822
|
+
`You: ${guidance.human}`,
|
|
823
|
+
`AI: ${guidance.ai}`,
|
|
824
|
+
`Gate: ${state.gate.title}`,
|
|
825
|
+
].join("\n"),
|
|
826
|
+
"info",
|
|
759
827
|
);
|
|
760
|
-
if (!artifact?.path) {
|
|
761
|
-
return;
|
|
762
828
|
}
|
|
763
|
-
pi.sendUserMessage(
|
|
764
|
-
[
|
|
765
|
-
`AHEAD mode: challenge my ${artifact.title} artifact before I accept the gate.`,
|
|
766
|
-
`Read ${artifact.path} and name the 2-3 weakest points: missing risks, vague claims, or things that would not survive implementation.`,
|
|
767
|
-
"Be specific and brief. Do not rewrite the artifact; I stay the author.",
|
|
768
|
-
].join("\n"),
|
|
769
|
-
);
|
|
770
829
|
},
|
|
771
830
|
});
|
|
772
|
-
}
|
|
773
831
|
|
|
774
|
-
if (state.return_targets.length > 0) {
|
|
775
832
|
actions.push({
|
|
776
|
-
label: "
|
|
777
|
-
run: async () =>
|
|
833
|
+
label: "Stop AHEAD mode",
|
|
834
|
+
run: async () => stopAheadMode(ctx),
|
|
778
835
|
});
|
|
779
|
-
}
|
|
780
836
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
actions.push({
|
|
791
|
-
label: "Help · policy, guidance, skills, phase explanation",
|
|
792
|
-
run: async () => {
|
|
793
|
-
const helpChoice = await ctx.ui.select("Help · pick one", [
|
|
794
|
-
"Configure project AHEAD policy for future runs",
|
|
795
|
-
"Read AHEAD framework guidance for this phase",
|
|
796
|
-
"Inspect optional skills reviewed for this phase",
|
|
797
|
-
"Explain this phase and its expectations",
|
|
798
|
-
]);
|
|
799
|
-
if (helpChoice === "Configure project AHEAD policy for future runs") {
|
|
800
|
-
await manageProjectConfig(ctx);
|
|
801
|
-
} else if (helpChoice === "Read AHEAD framework guidance for this phase") {
|
|
802
|
-
await showAheadGuide(ctx, "");
|
|
803
|
-
} else if (helpChoice === "Inspect optional skills reviewed for this phase") {
|
|
804
|
-
await showRecommendedSkills(ctx);
|
|
805
|
-
} else if (helpChoice === "Explain this phase and its expectations") {
|
|
806
|
-
ctx.ui.notify(
|
|
807
|
-
[
|
|
808
|
-
state.phase.title,
|
|
809
|
-
`Goal: ${guidance.objective}`,
|
|
810
|
-
`You: ${guidance.human}`,
|
|
811
|
-
`AI: ${guidance.ai}`,
|
|
812
|
-
`Gate: ${state.gate.title}`,
|
|
813
|
-
].join("\n"),
|
|
814
|
-
"info",
|
|
815
|
-
);
|
|
816
|
-
}
|
|
817
|
-
},
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
actions.push({
|
|
821
|
-
label: "Stop AHEAD mode",
|
|
822
|
-
run: async () => stopAheadMode(ctx),
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
const selected = await ctx.ui.select(
|
|
826
|
-
`AHEAD mode · ${state.phase.title}\nNext (${action.actor === "human" ? "you" : "AI"}): ${action.label}`,
|
|
827
|
-
actions.map((candidate) => candidate.label),
|
|
828
|
-
);
|
|
829
|
-
const chosen = actions.find((candidate) => candidate.label === selected);
|
|
830
|
-
if (chosen) {
|
|
837
|
+
const selected = await ctx.ui.select(
|
|
838
|
+
`AHEAD mode · ${state.phase.title}\nNext (${action.actor === "human" ? "you" : "AI"}): ${action.label}`,
|
|
839
|
+
actions.map((candidate) => candidate.label),
|
|
840
|
+
);
|
|
841
|
+
const chosen = actions.find((candidate) => candidate.label === selected);
|
|
842
|
+
if (!chosen) {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
831
845
|
await chosen.run();
|
|
832
846
|
}
|
|
833
847
|
}
|
|
@@ -1555,7 +1569,7 @@ function titleExample(workflowId: string): string {
|
|
|
1555
1569
|
case "product-change":
|
|
1556
1570
|
return "“Add audit log viewer page”";
|
|
1557
1571
|
case "internal-improvement":
|
|
1558
|
-
return "“Reduce
|
|
1572
|
+
return "“Reduce cold-start time”";
|
|
1559
1573
|
case "corrective-debugging":
|
|
1560
1574
|
return "“Fix race in worker claim loop”";
|
|
1561
1575
|
case "operational-stabilization":
|
|
@@ -1603,15 +1617,27 @@ async function recordHumanArtifact(
|
|
|
1603
1617
|
const prompts = promptsForArtifact(state.workflow_id, state.phase.id, artifact.kind);
|
|
1604
1618
|
let template = await humanArtifactTemplate(store, state, run, artifact.kind, artifact.title);
|
|
1605
1619
|
if (ctx.hasUI && artifact.kind !== "review-disposition" && prompts.length > 0) {
|
|
1606
|
-
|
|
1607
|
-
|
|
1620
|
+
// Visible progress while drafting: the loader row and footer status show
|
|
1621
|
+
// until the call resolves, so the wait never looks like a hang.
|
|
1622
|
+
const showProgress = ctx.model !== undefined;
|
|
1623
|
+
if (showProgress) {
|
|
1624
|
+
ctx.ui.setWorkingMessage("Drafting example prompts (inspiration only)…");
|
|
1625
|
+
ctx.ui.setStatus("ahead-examples", "Drafting example prompts…");
|
|
1626
|
+
}
|
|
1627
|
+
let draft;
|
|
1628
|
+
try {
|
|
1629
|
+
draft = await draftFieldExamples(ctx, {
|
|
1630
|
+
workflowTitle: engine.getWorkflow(state.workflow_id).title,
|
|
1631
|
+
phaseTitle: state.phase.title,
|
|
1632
|
+
runTitle: run.title,
|
|
1633
|
+
fields: prompts,
|
|
1634
|
+
});
|
|
1635
|
+
} finally {
|
|
1636
|
+
if (showProgress) {
|
|
1637
|
+
ctx.ui.setWorkingMessage();
|
|
1638
|
+
ctx.ui.setStatus("ahead-examples", undefined);
|
|
1639
|
+
}
|
|
1608
1640
|
}
|
|
1609
|
-
const draft = await draftFieldExamples(ctx, {
|
|
1610
|
-
workflowTitle: engine.getWorkflow(state.workflow_id).title,
|
|
1611
|
-
phaseTitle: state.phase.title,
|
|
1612
|
-
runTitle: run.title,
|
|
1613
|
-
fields: prompts,
|
|
1614
|
-
});
|
|
1615
1641
|
if (draft.examples) {
|
|
1616
1642
|
template = insertFieldExamples(template, draft.examples);
|
|
1617
1643
|
} else if (draft.skipped === "no-auth") {
|