ahead-pi 0.8.0 → 0.8.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ahead-pi",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "AHEAD workflow enforcement and context for Pi",
5
5
  "keywords": [
6
6
  "ahead",
package/src/examples.ts CHANGED
@@ -8,7 +8,14 @@ export interface FieldExamplesInput {
8
8
  fields: string[];
9
9
  }
10
10
 
11
- const EXAMPLES_TIMEOUT_MS = 25_000;
11
+ const EXAMPLES_TIMEOUT_MS = 12_000;
12
+
13
+ export type FieldExamplesSkipped = "no-model" | "no-auth" | "failed";
14
+
15
+ export interface FieldExamplesResult {
16
+ examples?: string[][];
17
+ skipped?: FieldExamplesSkipped;
18
+ }
12
19
 
13
20
  /**
14
21
  * Draft two inspiration-only example lines per artifact field.
@@ -21,10 +28,10 @@ const EXAMPLES_TIMEOUT_MS = 25_000;
21
28
  export async function draftFieldExamples(
22
29
  ctx: ExtensionCommandContext,
23
30
  input: FieldExamplesInput,
24
- ): Promise<string[][] | undefined> {
31
+ ): Promise<FieldExamplesResult> {
25
32
  const model = ctx.model;
26
33
  if (!model) {
27
- return undefined;
34
+ return { skipped: "no-model" };
28
35
  }
29
36
 
30
37
  const prompt = [
@@ -46,9 +53,13 @@ export async function draftFieldExamples(
46
53
  ].join("\n");
47
54
 
48
55
  try {
56
+ // getProviderAuth returning undefined means no usable credential of any
57
+ // kind could be resolved — only then is a heads-up accurate. A resolved
58
+ // auth object without an apiKey (e.g. OAuth) may still succeed through
59
+ // the adapter's own credential resolution, so we attempt the call.
49
60
  const auth = await ctx.modelRegistry.getProviderAuth(model.provider);
50
61
  if (!auth) {
51
- return undefined;
62
+ return { skipped: "no-auth" };
52
63
  }
53
64
  const message = await Promise.race([
54
65
  completeSimple(
@@ -74,11 +85,15 @@ export async function draftFieldExamples(
74
85
  }),
75
86
  ]);
76
87
  if (!message) {
77
- return undefined;
88
+ return { skipped: "failed" };
89
+ }
90
+ const examples = parseExampleLines(message, input.fields.length);
91
+ if (!examples) {
92
+ return { skipped: "failed" };
78
93
  }
79
- return parseExampleLines(message, input.fields.length);
94
+ return { examples };
80
95
  } catch {
81
- return undefined;
96
+ return { skipped: "failed" };
82
97
  }
83
98
  }
84
99
 
@@ -99,7 +114,8 @@ function textBlocks(content: unknown): string[] {
99
114
  return blocks;
100
115
  }
101
116
 
102
- function parseExampleLines(message: unknown, fieldCount: number): string[][] | undefined {
117
+ /** Exported for unit testing; the wire shape is validated defensively at runtime. */
118
+ export function parseExampleLines(message: unknown, fieldCount: number): string[][] | undefined {
103
119
  const text = textBlocks(isRecord(message) ? message.content : undefined).join("\n");
104
120
 
105
121
  const perField: string[][] = Array.from({ length: fieldCount }, () => []);
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
- const state = engine.deriveState(run);
658
- const workflow = engine.getWorkflow(run.workflow_id);
659
- const guidance = phaseGuide(run.workflow_id, state.phase.id);
660
- const action = nextAction(state, workflow);
661
- const actions: GuidedAction[] = [];
662
- const missingRequired = state.artifacts.filter(
663
- (artifact) => artifact.required && !artifact.present,
664
- );
665
- if (action.artifactKind) {
666
- const actionArtifact = state.artifacts.find(
667
- (artifact) => artifact.kind === action.artifactKind,
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
- actions.push({
670
- label: action.label,
671
- run:
672
- action.actor === "ai"
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: `Write ${actionArtifact.title} yourself`,
681
- run: async () => recordHumanArtifact(ctx, actionArtifact.kind),
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
- if (action.optional) {
687
- const nextHumanArtifact = missingRequired.find((artifact) => artifact.actor !== "ai");
688
- if (nextHumanArtifact) {
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: `Continue without optional AI challenge · Write ${nextHumanArtifact.title}`,
691
- run: async () => recordHumanArtifact(ctx, nextHumanArtifact.kind),
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: `Continue without optional AI contribution · Accept ${state.gate.title}`,
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
- if (state.work_item_required_for_next_phase && state.gate.accepted) {
702
- actions.push({
703
- label: action.label,
704
- run: async () => manageWorkItem(ctx, ""),
705
- });
706
- } else if (missingRequired.length === 0 && !action.artifactKind) {
707
- actions.push({
708
- label: state.gate.accepted ? action.label : `Accept and continue · ${state.gate.title}`,
709
- run: async () => acceptAndContinue(ctx),
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
- if (
714
- state.allowed_ai_capabilities.length > 0 &&
715
- action.actor !== "ai" &&
716
- !missingRequired.some((artifact) => artifact.actor === "ai") &&
717
- state.phase.id !== "implement"
718
- ) {
719
- actions.push({
720
- label: `Ask AI to assist · ${state.phase.title}`,
721
- run: async () => requestAiAssistance(pi, state),
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
- if (state.phase.id === "implement") {
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: "Save this ready-to-implement run for a later sprint",
729
- run: async () => saveImplementationHandoff(ctx),
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
- if (state.phase.id === "ai-review" || state.phase.id === "human-review") {
739
- actions.push({
740
- label: "Open the changeset review workbench",
741
- run: async () => openReviewWorkbench(pi, ctx),
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: "Ask AI to challenge the latest artifact",
801
+ label: "Help · policy, guidance, skills, phase explanation",
753
802
  run: async () => {
754
- const artifact = [...state.artifacts]
755
- .toReversed()
756
- .find(
757
- (candidate) =>
758
- candidate.present && candidate.recorded_by?.kind === "human" && candidate.path,
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
-
774
- if (state.return_targets.length > 0) {
775
- actions.push({
776
- label: "Return to an earlier phase",
777
- run: async () => returnToEarlierPhase(ctx, ""),
778
- });
779
- }
780
831
 
781
- if (!state.work_item_required_for_next_phase || !state.gate.accepted) {
782
832
  actions.push({
783
- label: state.work_item
784
- ? "View or replace the linked work item"
785
- : "Link or create a work item",
786
- run: async () => manageWorkItem(ctx, ""),
833
+ label: "Stop AHEAD mode",
834
+ run: async () => stopAheadMode(ctx),
787
835
  });
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
836
 
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 cola-api cold-start time”";
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
- const examples = await draftFieldExamples(ctx, {
1620
+ if (ctx.model) {
1621
+ ctx.ui.notify("Drafting two example prompts per field (inspiration only)…", "info");
1622
+ }
1623
+ const draft = await draftFieldExamples(ctx, {
1607
1624
  workflowTitle: engine.getWorkflow(state.workflow_id).title,
1608
1625
  phaseTitle: state.phase.title,
1609
1626
  runTitle: run.title,
1610
1627
  fields: prompts,
1611
1628
  });
1612
- if (examples) {
1613
- template = insertFieldExamples(template, examples);
1629
+ if (draft.examples) {
1630
+ template = insertFieldExamples(template, draft.examples);
1631
+ } else if (draft.skipped === "no-auth") {
1632
+ ctx.ui.notify(
1633
+ "Example prompts unavailable: no usable model credential could be resolved for this session.",
1634
+ "info",
1635
+ );
1636
+ } else if (draft.skipped === "no-model") {
1637
+ ctx.ui.notify("Example prompts unavailable: no active model in this session.", "info");
1614
1638
  }
1639
+ // "failed" (timeout, unparseable output, transient error) stays silent:
1640
+ // the plain template is the fallback and needs no apology.
1615
1641
  }
1616
1642
  let content = await ctx.ui.editor(
1617
1643
  `AHEAD mode · ${artifact.title} · write in your own words`,