ahead-pi 0.7.0 → 0.8.0
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 +127 -0
- package/src/guidance.ts +26 -0
- package/src/index.ts +47 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ahead-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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,127 @@
|
|
|
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 = 25_000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Draft two inspiration-only example lines per artifact field.
|
|
15
|
+
*
|
|
16
|
+
* The model is called without repository context on purpose: the examples are
|
|
17
|
+
* creative sparks to react to, not guidance the human should trust. They are
|
|
18
|
+
* rendered as HTML comments inside the field so form validation still treats
|
|
19
|
+
* an untouched field as empty.
|
|
20
|
+
*/
|
|
21
|
+
export async function draftFieldExamples(
|
|
22
|
+
ctx: ExtensionCommandContext,
|
|
23
|
+
input: FieldExamplesInput,
|
|
24
|
+
): Promise<string[][] | undefined> {
|
|
25
|
+
const model = ctx.model;
|
|
26
|
+
if (!model) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const prompt = [
|
|
31
|
+
"You are helping a human begin a short written artifact in a software workflow.",
|
|
32
|
+
`Run title: ${input.runTitle}`,
|
|
33
|
+
`Workflow: ${input.workflowTitle} · Phase: ${input.phaseTitle}`,
|
|
34
|
+
"",
|
|
35
|
+
"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.",
|
|
36
|
+
"",
|
|
37
|
+
"Output format, nothing else:",
|
|
38
|
+
"1: <example line>",
|
|
39
|
+
"1: <example line>",
|
|
40
|
+
"2: <example line>",
|
|
41
|
+
"2: <example line>",
|
|
42
|
+
"...one pair per field, in order.",
|
|
43
|
+
"",
|
|
44
|
+
"Fields:",
|
|
45
|
+
...input.fields.map((field, index) => `${index + 1}. ${field}`),
|
|
46
|
+
].join("\n");
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const auth = await ctx.modelRegistry.getProviderAuth(model.provider);
|
|
50
|
+
if (!auth) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const message = await Promise.race([
|
|
54
|
+
completeSimple(
|
|
55
|
+
model,
|
|
56
|
+
{
|
|
57
|
+
messages: [
|
|
58
|
+
{
|
|
59
|
+
role: "user",
|
|
60
|
+
content: [{ type: "text", text: prompt }],
|
|
61
|
+
timestamp: Date.now(),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
apiKey: auth.auth.apiKey,
|
|
67
|
+
headers: auth.auth.headers,
|
|
68
|
+
env: auth.env,
|
|
69
|
+
reasoning: "minimal",
|
|
70
|
+
},
|
|
71
|
+
),
|
|
72
|
+
new Promise<undefined>((resolve) => {
|
|
73
|
+
setTimeout(() => resolve(undefined), EXAMPLES_TIMEOUT_MS);
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
if (!message) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return parseExampleLines(message, input.fields.length);
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
86
|
+
return typeof value === "object" && value !== null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function textBlocks(content: unknown): string[] {
|
|
90
|
+
if (!Array.isArray(content)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const blocks: string[] = [];
|
|
94
|
+
for (const block of content) {
|
|
95
|
+
if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
|
|
96
|
+
blocks.push(block.text);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return blocks;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseExampleLines(message: unknown, fieldCount: number): string[][] | undefined {
|
|
103
|
+
const text = textBlocks(isRecord(message) ? message.content : undefined).join("\n");
|
|
104
|
+
|
|
105
|
+
const perField: string[][] = Array.from({ length: fieldCount }, () => []);
|
|
106
|
+
for (const line of text.split("\n")) {
|
|
107
|
+
const match = /^\s*(\d+)\s*:\s*(.+)$/.exec(line);
|
|
108
|
+
if (!match) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const fieldIndex = Number.parseInt(match[1] ?? "", 10) - 1;
|
|
112
|
+
const example = match[2]?.trim();
|
|
113
|
+
if (
|
|
114
|
+
Number.isInteger(fieldIndex) &&
|
|
115
|
+
fieldIndex >= 0 &&
|
|
116
|
+
fieldIndex < fieldCount &&
|
|
117
|
+
example &&
|
|
118
|
+
perField[fieldIndex].length < 2
|
|
119
|
+
) {
|
|
120
|
+
perField[fieldIndex].push(example);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (perField.every((examples) => examples.length === 0)) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return perField;
|
|
127
|
+
}
|
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,19 @@ 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
|
+
const examples = await draftFieldExamples(ctx, {
|
|
1607
|
+
workflowTitle: engine.getWorkflow(state.workflow_id).title,
|
|
1608
|
+
phaseTitle: state.phase.title,
|
|
1609
|
+
runTitle: run.title,
|
|
1610
|
+
fields: prompts,
|
|
1611
|
+
});
|
|
1612
|
+
if (examples) {
|
|
1613
|
+
template = insertFieldExamples(template, examples);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1573
1616
|
let content = await ctx.ui.editor(
|
|
1574
1617
|
`AHEAD mode · ${artifact.title} · write in your own words`,
|
|
1575
1618
|
template,
|
|
@@ -1581,10 +1624,7 @@ async function recordHumanArtifact(
|
|
|
1581
1624
|
// Keep the form open until validation passes or the human explicitly cancels,
|
|
1582
1625
|
// so partial input is never discarded by a validation failure.
|
|
1583
1626
|
while (true) {
|
|
1584
|
-
const formErrors = validateArtifactForm(
|
|
1585
|
-
content,
|
|
1586
|
-
promptsForArtifact(state.workflow_id, state.phase.id, artifact.kind),
|
|
1587
|
-
);
|
|
1627
|
+
const formErrors = validateArtifactForm(content, prompts);
|
|
1588
1628
|
if (formErrors.length === 0) {
|
|
1589
1629
|
break;
|
|
1590
1630
|
}
|