@jwilger/pi-development-system 0.78.0 → 0.79.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.
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
import { cadenceLine, DEFAULT_PUSH_MINUTES } from "../src/context/cadence.ts";
|
|
9
9
|
import { appendContextTail, renderContextTail } from "../src/context/context-tail.ts";
|
|
10
10
|
import { createPhaseTool } from "../src/context/devsys-tool.ts";
|
|
11
|
-
import {
|
|
11
|
+
import { intentLineFor } from "../src/context/intent-trigger.ts";
|
|
12
12
|
import { registerModelAdvice } from "../src/context/model-advice.ts";
|
|
13
13
|
import { renderStatus, renderStatusLine, STATUS_KEY } from "../src/context/status.ts";
|
|
14
14
|
import { applyPromptSection } from "../src/context/system-prompt.ts";
|
|
@@ -89,9 +89,15 @@ export function createDevelopmentSystem(pi: ExtensionAPI) {
|
|
|
89
89
|
pi.on("session_start", (_event, ctx) => rebuild(ctx));
|
|
90
90
|
pi.on("session_tree", (_event, ctx) => rebuild(ctx));
|
|
91
91
|
|
|
92
|
+
// The intent line rides in the context tail for this run only: a change to the system prompt
|
|
93
|
+
// would invalidate the provider's cache for the whole conversation.
|
|
94
|
+
let intentLine: string | undefined;
|
|
92
95
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
93
96
|
applyPromptSection(event, state.get(), nonNegotiables);
|
|
94
|
-
await
|
|
97
|
+
intentLine = await intentLineFor(event.prompt, state.get(), jevHolder.forContext(ctx));
|
|
98
|
+
});
|
|
99
|
+
pi.on("agent_end", () => {
|
|
100
|
+
intentLine = undefined;
|
|
95
101
|
});
|
|
96
102
|
|
|
97
103
|
pi.on("context", (event) => {
|
|
@@ -99,7 +105,7 @@ export function createDevelopmentSystem(pi: ExtensionAPI) {
|
|
|
99
105
|
const cadence = cadenceLine(state.get(), now, pushMinutes);
|
|
100
106
|
const messages = appendContextTail(
|
|
101
107
|
event.messages,
|
|
102
|
-
renderContextTail(state.get(), cadence),
|
|
108
|
+
renderContextTail(state.get(), cadence, intentLine),
|
|
103
109
|
now,
|
|
104
110
|
);
|
|
105
111
|
return messages === undefined ? undefined : { messages };
|
|
@@ -155,10 +161,13 @@ export function createDevelopmentSystem(pi: ExtensionAPI) {
|
|
|
155
161
|
createRouteTaskTool({ jev: (ctx) => jevHolder.forContext(ctx) }),
|
|
156
162
|
createTaskCheckTool({ jev: (ctx) => jevHolder.forContext(ctx) }),
|
|
157
163
|
createWorkItemTool({ exec }),
|
|
158
|
-
...createJudgeTools({ jev: (ctx) => jevHolder.forContext(ctx) }),
|
|
159
164
|
];
|
|
160
165
|
for (const tool of rarelyUsed) pi.registerTool(tool);
|
|
161
166
|
pi.on("session_start", () => declareWithoutCodemode(pi, rarelyUsed));
|
|
167
|
+
// The judge_* tools exist only for scripts: without codemode there is nothing to call them from.
|
|
168
|
+
for (const tool of createJudgeTools({ jev: (ctx) => jevHolder.forContext(ctx) })) {
|
|
169
|
+
pi.registerTool(tool);
|
|
170
|
+
}
|
|
162
171
|
pi.registerTool(createPhaseTool({ state }));
|
|
163
172
|
pi.registerTool(createBeginWorkTool({ state }));
|
|
164
173
|
pi.registerTool(createIntakeTool({ pi, state, jev: (ctx) => jevHolder.forContext(ctx) }));
|
package/package.json
CHANGED
|
@@ -11,17 +11,19 @@ Adjust `CHECKS` to the project profile (see the profile skill: TypeScript, Rust,
|
|
|
11
11
|
const CHECKS = [
|
|
12
12
|
{ tool: "tsc", command: "npx tsc --noEmit" },
|
|
13
13
|
{ tool: "biome", command: "biome check --error-on-warnings ." },
|
|
14
|
-
{ tool: "tests", command: "npm test
|
|
14
|
+
{ tool: "tests", command: "npm test" },
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
const results = await Promise.all(
|
|
18
18
|
CHECKS.map(async ({ tool, command }) => {
|
|
19
19
|
const r = await tools.bash({ command });
|
|
20
20
|
const lines = r.output.split("\n").filter((l) => l.trim() !== "");
|
|
21
|
+
// Failure lines first: the head of a test run is mostly passing tests.
|
|
22
|
+
const failing = lines.filter((l) => /error|fail|✖|not ok/i.test(l));
|
|
21
23
|
return {
|
|
22
24
|
tool,
|
|
23
25
|
exit: r.exit_code,
|
|
24
|
-
firstFailures: r.exit_code === 0 ? [] : lines.slice(0, 15),
|
|
26
|
+
firstFailures: r.exit_code === 0 ? [] : (failing.length > 0 ? failing : lines.slice(-15)).slice(0, 15),
|
|
25
27
|
};
|
|
26
28
|
}),
|
|
27
29
|
);
|
|
@@ -29,6 +31,9 @@ const results = await Promise.all(
|
|
|
29
31
|
return results;
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
Never pipe a check through `tail`, `head` or `grep` inside `command`: the exit code is then the
|
|
35
|
+
pipe's, not the check's, and a failing run reads as `0`. Trim the output in the script instead.
|
|
36
|
+
|
|
32
37
|
Read the result before saying anything passed: every `exit` must be `0`. A non-zero `exit`
|
|
33
38
|
means fix `firstFailures` first, then run the script again.
|
|
34
39
|
|
|
@@ -9,15 +9,25 @@ const scopeLabel = (d: Departure): string =>
|
|
|
9
9
|
d.scope.kind === "slice" ? `slice ${d.scope.slice}` : d.scope.kind;
|
|
10
10
|
|
|
11
11
|
/** Small, cache-friendly reminder appended at the end of the context; undefined when nothing to say. */
|
|
12
|
-
export function renderContextTail(
|
|
13
|
-
|
|
12
|
+
export function renderContextTail(
|
|
13
|
+
state: DevsysState,
|
|
14
|
+
cadence?: string,
|
|
15
|
+
note?: string,
|
|
16
|
+
): string | undefined {
|
|
17
|
+
if (state.phase === "idle" && state.openDepartures.length === 0 && note === undefined) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
14
20
|
const head = [
|
|
15
21
|
"[development-system]",
|
|
16
22
|
`phase: ${state.phase} · slice: ${state.activeSlice ?? "none"}`,
|
|
17
23
|
`Jev: ${state.jev}`,
|
|
18
24
|
...((state.profiles?.length ?? 0) > 0 ? [`profiles: ${state.profiles?.join(", ")}`] : []),
|
|
19
25
|
];
|
|
20
|
-
const tailLines = [
|
|
26
|
+
const tailLines = [
|
|
27
|
+
...(note === undefined ? [] : [note]),
|
|
28
|
+
...(cadence === undefined ? [] : [cadence]),
|
|
29
|
+
REMINDER,
|
|
30
|
+
];
|
|
21
31
|
const room = MAX_LINES - head.length - tailLines.length - 1;
|
|
22
32
|
const lines = state.openDepartures.map(
|
|
23
33
|
(d) => `- ${d.gate} — ${d.chosen.replace(/\s+/g, " ")} (${scopeLabel(d)})`,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { BeforeAgentStartEvent } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import type { DevsysState } from "../core/types.ts";
|
|
3
2
|
import type { Jev } from "../jev/client.ts";
|
|
4
3
|
import { type Intent, judgeIntent } from "../jev/questions/intent.ts";
|
|
@@ -6,38 +5,40 @@ import { type Intent, judgeIntent } from "../jev/questions/intent.ts";
|
|
|
6
5
|
/** Probability at or above which an intent is acted on. */
|
|
7
6
|
export const INTENT_AT = 0.7;
|
|
8
7
|
|
|
9
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Pure: the one line an intent earns, or undefined. Only an idle session is nudged: inside a running
|
|
10
|
+
* slice a request cannot be told apart from an instruction for that slice, and a wrong nudge would
|
|
11
|
+
* restart intake. Questions and continuations never earn a line.
|
|
12
|
+
*/
|
|
10
13
|
export function intentGuideline(
|
|
11
14
|
intent: Intent,
|
|
12
15
|
confidence: number,
|
|
13
16
|
phase: DevsysState["phase"],
|
|
14
17
|
): string | undefined {
|
|
15
|
-
if (confidence < INTENT_AT) return undefined;
|
|
16
|
-
|
|
17
|
-
if ((intent === "new-work" || intent === "fix") && (idle || intent === "new-work")) {
|
|
18
|
+
if (confidence < INTENT_AT || phase !== "idle") return undefined;
|
|
19
|
+
if (intent === "new-work" || intent === "fix") {
|
|
18
20
|
return "This prompt asks for new work: call devsys_intake with the request before editing anything, so it is sized and the planning artifacts it needs are named.";
|
|
19
21
|
}
|
|
20
|
-
if (intent === "review"
|
|
22
|
+
if (intent === "review") {
|
|
21
23
|
return "This prompt asks for a review: call devsys_review_start for a fresh-context review instead of reviewing in this conversation.";
|
|
22
24
|
}
|
|
23
25
|
return undefined;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
export async function applyIntentGuideline(
|
|
34
|
-
event: BeforeAgentStartEvent,
|
|
28
|
+
/**
|
|
29
|
+
* Adapter: judge the prompt and return the line for this run's context tail (not the system prompt,
|
|
30
|
+
* whose bytes every cached request shares). Idle sessions only, so a busy session pays no Jev call;
|
|
31
|
+
* an offline or failing Jev adds nothing instead of delaying the run.
|
|
32
|
+
*/
|
|
33
|
+
export async function intentLineFor(
|
|
34
|
+
prompt: string,
|
|
35
35
|
state: DevsysState,
|
|
36
36
|
jev: Jev,
|
|
37
|
-
): Promise<
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
37
|
+
): Promise<string | undefined> {
|
|
38
|
+
if (state.phase !== "idle" || prompt.trim() === "" || jev.availability() === "offline") {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const judged = await judgeIntent(jev, { prompt, phase: state.phase });
|
|
42
|
+
if (!judged.ok) return undefined;
|
|
43
|
+
return intentGuideline(judged.value.intent, judged.value.confidence, state.phase);
|
|
43
44
|
}
|
|
@@ -59,8 +59,14 @@ export function asksUser(text: string): boolean {
|
|
|
59
59
|
return QUESTION_END.test(lead) || (LIST_ITEM.test(last) && CHOICE_LEAD_IN.test(lead));
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** A recorded round, or a departure that waives review for the slice: either way the nudge has nothing to ask for. */
|
|
62
63
|
const hasReviewRound = (state: DevsysState, slice: DevsysState["activeSlice"] & string): boolean =>
|
|
63
|
-
(reviewOf(state, slice)?.rounds.length ?? 0) > 0
|
|
64
|
+
(reviewOf(state, slice)?.rounds.length ?? 0) > 0 ||
|
|
65
|
+
state.openDepartures.some(
|
|
66
|
+
(d) =>
|
|
67
|
+
d.gate === "review.unsatisfied" &&
|
|
68
|
+
(d.scope.kind === "session" || (d.scope.kind === "slice" && d.scope.slice === slice)),
|
|
69
|
+
);
|
|
64
70
|
|
|
65
71
|
/** True when a scope.expansion departure already covers the slice: the remedy the note asks for exists. */
|
|
66
72
|
const expansionRecorded = (state: DevsysState, slice: string): boolean =>
|