ahead-pi 0.7.0 → 0.8.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/package.json +4 -1
- package/src/examples.ts +143 -0
- package/src/guidance.ts +26 -0
- package/src/index.ts +59 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ahead-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "AHEAD workflow enforcement and context for Pi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ahead",
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
"verify": "npm run build && npm run format:check && npm run lint && node --test ./test/*.test.mjs",
|
|
46
46
|
"test": "npm run verify && npm run pack:verify"
|
|
47
47
|
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@earendil-works/pi-ai": "^0.84.4"
|
|
50
|
+
},
|
|
48
51
|
"devDependencies": {
|
|
49
52
|
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
50
53
|
"@earendil-works/pi-tui": "^0.84.1",
|
package/src/examples.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
export interface FieldExamplesInput {
|
|
5
|
+
workflowTitle: string;
|
|
6
|
+
phaseTitle: string;
|
|
7
|
+
runTitle: string;
|
|
8
|
+
fields: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
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
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Draft two inspiration-only example lines per artifact field.
|
|
22
|
+
*
|
|
23
|
+
* The model is called without repository context on purpose: the examples are
|
|
24
|
+
* creative sparks to react to, not guidance the human should trust. They are
|
|
25
|
+
* rendered as HTML comments inside the field so form validation still treats
|
|
26
|
+
* an untouched field as empty.
|
|
27
|
+
*/
|
|
28
|
+
export async function draftFieldExamples(
|
|
29
|
+
ctx: ExtensionCommandContext,
|
|
30
|
+
input: FieldExamplesInput,
|
|
31
|
+
): Promise<FieldExamplesResult> {
|
|
32
|
+
const model = ctx.model;
|
|
33
|
+
if (!model) {
|
|
34
|
+
return { skipped: "no-model" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const prompt = [
|
|
38
|
+
"You are helping a human begin a short written artifact in a software workflow.",
|
|
39
|
+
`Run title: ${input.runTitle}`,
|
|
40
|
+
`Workflow: ${input.workflowTitle} · Phase: ${input.phaseTitle}`,
|
|
41
|
+
"",
|
|
42
|
+
"You have NO access to the codebase or real project details. For each numbered field, write exactly 2 short example answers that are creative sparks only — plausibly generic and concrete, never claimed as fact about this project. Keep each line under 15 words.",
|
|
43
|
+
"",
|
|
44
|
+
"Output format, nothing else:",
|
|
45
|
+
"1: <example line>",
|
|
46
|
+
"1: <example line>",
|
|
47
|
+
"2: <example line>",
|
|
48
|
+
"2: <example line>",
|
|
49
|
+
"...one pair per field, in order.",
|
|
50
|
+
"",
|
|
51
|
+
"Fields:",
|
|
52
|
+
...input.fields.map((field, index) => `${index + 1}. ${field}`),
|
|
53
|
+
].join("\n");
|
|
54
|
+
|
|
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.
|
|
60
|
+
const auth = await ctx.modelRegistry.getProviderAuth(model.provider);
|
|
61
|
+
if (!auth) {
|
|
62
|
+
return { skipped: "no-auth" };
|
|
63
|
+
}
|
|
64
|
+
const message = await Promise.race([
|
|
65
|
+
completeSimple(
|
|
66
|
+
model,
|
|
67
|
+
{
|
|
68
|
+
messages: [
|
|
69
|
+
{
|
|
70
|
+
role: "user",
|
|
71
|
+
content: [{ type: "text", text: prompt }],
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
apiKey: auth.auth.apiKey,
|
|
78
|
+
headers: auth.auth.headers,
|
|
79
|
+
env: auth.env,
|
|
80
|
+
reasoning: "minimal",
|
|
81
|
+
},
|
|
82
|
+
),
|
|
83
|
+
new Promise<undefined>((resolve) => {
|
|
84
|
+
setTimeout(() => resolve(undefined), EXAMPLES_TIMEOUT_MS);
|
|
85
|
+
}),
|
|
86
|
+
]);
|
|
87
|
+
if (!message) {
|
|
88
|
+
return { skipped: "failed" };
|
|
89
|
+
}
|
|
90
|
+
const examples = parseExampleLines(message, input.fields.length);
|
|
91
|
+
if (!examples) {
|
|
92
|
+
return { skipped: "failed" };
|
|
93
|
+
}
|
|
94
|
+
return { examples };
|
|
95
|
+
} catch {
|
|
96
|
+
return { skipped: "failed" };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
101
|
+
return typeof value === "object" && value !== null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function textBlocks(content: unknown): string[] {
|
|
105
|
+
if (!Array.isArray(content)) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const blocks: string[] = [];
|
|
109
|
+
for (const block of content) {
|
|
110
|
+
if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
|
|
111
|
+
blocks.push(block.text);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return blocks;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Exported for unit testing; the wire shape is validated defensively at runtime. */
|
|
118
|
+
export function parseExampleLines(message: unknown, fieldCount: number): string[][] | undefined {
|
|
119
|
+
const text = textBlocks(isRecord(message) ? message.content : undefined).join("\n");
|
|
120
|
+
|
|
121
|
+
const perField: string[][] = Array.from({ length: fieldCount }, () => []);
|
|
122
|
+
for (const line of text.split("\n")) {
|
|
123
|
+
const match = /^\s*(\d+)\s*:\s*(.+)$/.exec(line);
|
|
124
|
+
if (!match) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const fieldIndex = Number.parseInt(match[1] ?? "", 10) - 1;
|
|
128
|
+
const example = match[2]?.trim();
|
|
129
|
+
if (
|
|
130
|
+
Number.isInteger(fieldIndex) &&
|
|
131
|
+
fieldIndex >= 0 &&
|
|
132
|
+
fieldIndex < fieldCount &&
|
|
133
|
+
example &&
|
|
134
|
+
perField[fieldIndex].length < 2
|
|
135
|
+
) {
|
|
136
|
+
perField[fieldIndex].push(example);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (perField.every((examples) => examples.length === 0)) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
return perField;
|
|
143
|
+
}
|
package/src/guidance.ts
CHANGED
|
@@ -532,6 +532,32 @@ export function buildArtifactTemplate(
|
|
|
532
532
|
].join("\n");
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Insert inspiration-only example lines into each field as HTML comments.
|
|
537
|
+
* Validation strips comments, so an untouched field still counts as empty.
|
|
538
|
+
*/
|
|
539
|
+
export function insertFieldExamples(template: string, perField: string[][]): string {
|
|
540
|
+
let updated = template;
|
|
541
|
+
for (const [index, examples] of perField.entries()) {
|
|
542
|
+
if (!examples || examples.length === 0) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const marker = `<!-- AHEAD-FIELD:${index + 1}:BEGIN -->`;
|
|
546
|
+
if (!updated.includes(marker)) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
updated = updated.replace(
|
|
550
|
+
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"),
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
return updated;
|
|
559
|
+
}
|
|
560
|
+
|
|
535
561
|
export function validateArtifactForm(content: string, prompts: string[]): string[] {
|
|
536
562
|
const errors: string[] = [];
|
|
537
563
|
for (const [index, prompt] of prompts.entries()) {
|
package/src/index.ts
CHANGED
|
@@ -14,12 +14,14 @@ import { AheadEngine, AheadEngineError } from "./engine.js";
|
|
|
14
14
|
import {
|
|
15
15
|
buildArtifactTemplate,
|
|
16
16
|
buildHeaderLines,
|
|
17
|
+
insertFieldExamples,
|
|
17
18
|
nextAction,
|
|
18
19
|
phaseGuide,
|
|
19
20
|
phasePosition,
|
|
20
21
|
promptsForArtifact,
|
|
21
22
|
validateArtifactForm,
|
|
22
23
|
} from "./guidance.js";
|
|
24
|
+
import { draftFieldExamples } from "./examples.js";
|
|
23
25
|
import {
|
|
24
26
|
findReference,
|
|
25
27
|
loadReferenceIndex,
|
|
@@ -740,6 +742,35 @@ async function openAheadMode(
|
|
|
740
742
|
});
|
|
741
743
|
}
|
|
742
744
|
|
|
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
|
+
actions.push({
|
|
752
|
+
label: "Ask AI to challenge the latest artifact",
|
|
753
|
+
run: async () => {
|
|
754
|
+
const artifact = [...state.artifacts]
|
|
755
|
+
.toReversed()
|
|
756
|
+
.find(
|
|
757
|
+
(candidate) =>
|
|
758
|
+
candidate.present && candidate.recorded_by?.kind === "human" && candidate.path,
|
|
759
|
+
);
|
|
760
|
+
if (!artifact?.path) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
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
|
+
},
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
743
774
|
if (state.return_targets.length > 0) {
|
|
744
775
|
actions.push({
|
|
745
776
|
label: "Return to an earlier phase",
|
|
@@ -1475,8 +1506,8 @@ async function startRun(ctx: ExtensionCommandContext, request: string): Promise<
|
|
|
1475
1506
|
linkedTitle ||
|
|
1476
1507
|
(ctx.hasUI
|
|
1477
1508
|
? await ctx.ui.input(
|
|
1478
|
-
`
|
|
1479
|
-
|
|
1509
|
+
`AHEAD · ${workflow.title} · Name this work — e.g. ${titleExample(workflow.id)}`,
|
|
1510
|
+
"",
|
|
1480
1511
|
)
|
|
1481
1512
|
: parsed.workItemUrl);
|
|
1482
1513
|
if (!title?.trim()) {
|
|
@@ -1569,7 +1600,31 @@ async function recordHumanArtifact(
|
|
|
1569
1600
|
);
|
|
1570
1601
|
}
|
|
1571
1602
|
|
|
1572
|
-
const
|
|
1603
|
+
const prompts = promptsForArtifact(state.workflow_id, state.phase.id, artifact.kind);
|
|
1604
|
+
let template = await humanArtifactTemplate(store, state, run, artifact.kind, artifact.title);
|
|
1605
|
+
if (ctx.hasUI && artifact.kind !== "review-disposition" && prompts.length > 0) {
|
|
1606
|
+
if (ctx.model) {
|
|
1607
|
+
ctx.ui.notify("Drafting two example prompts per field (inspiration only)…", "info");
|
|
1608
|
+
}
|
|
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
|
+
if (draft.examples) {
|
|
1616
|
+
template = insertFieldExamples(template, draft.examples);
|
|
1617
|
+
} else if (draft.skipped === "no-auth") {
|
|
1618
|
+
ctx.ui.notify(
|
|
1619
|
+
"Example prompts unavailable: no usable model credential could be resolved for this session.",
|
|
1620
|
+
"info",
|
|
1621
|
+
);
|
|
1622
|
+
} else if (draft.skipped === "no-model") {
|
|
1623
|
+
ctx.ui.notify("Example prompts unavailable: no active model in this session.", "info");
|
|
1624
|
+
}
|
|
1625
|
+
// "failed" (timeout, unparseable output, transient error) stays silent:
|
|
1626
|
+
// the plain template is the fallback and needs no apology.
|
|
1627
|
+
}
|
|
1573
1628
|
let content = await ctx.ui.editor(
|
|
1574
1629
|
`AHEAD mode · ${artifact.title} · write in your own words`,
|
|
1575
1630
|
template,
|
|
@@ -1581,10 +1636,7 @@ async function recordHumanArtifact(
|
|
|
1581
1636
|
// Keep the form open until validation passes or the human explicitly cancels,
|
|
1582
1637
|
// so partial input is never discarded by a validation failure.
|
|
1583
1638
|
while (true) {
|
|
1584
|
-
const formErrors = validateArtifactForm(
|
|
1585
|
-
content,
|
|
1586
|
-
promptsForArtifact(state.workflow_id, state.phase.id, artifact.kind),
|
|
1587
|
-
);
|
|
1639
|
+
const formErrors = validateArtifactForm(content, prompts);
|
|
1588
1640
|
if (formErrors.length === 0) {
|
|
1589
1641
|
break;
|
|
1590
1642
|
}
|