@jwilger/pi-development-system 0.77.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.
package/README.md CHANGED
@@ -13,6 +13,18 @@ and adds per-spawn `model` and `thinkingLevel`. **Remove the original from the
13
13
  entry), then `/reload`; otherwise both register `agent_spawn` and pi refuses the
14
14
  duplicate.
15
15
 
16
+ ## You do not need the slash commands
17
+
18
+ Everything the system offers is reachable without remembering a command. Describe the work in
19
+ plain words: when a prompt asks for new work, a fix or a review, the system adds a guideline
20
+ naming the tool to use (`devsys_intake`, `devsys_review_start`). A `devsys` tool always shows
21
+ what the current phase expects. When you say a slice is finished with no review round recorded,
22
+ it tells you to start one. The slash commands (`/devsys-start`, `/devsys-plan`, `/devsys-review`)
23
+ are shortcuts to the same tools.
24
+
25
+ With `codemode` enabled (`"defaultTools": ["+codemode"]` in pi settings), rarely used tools and
26
+ the `judge_*` Jev wrappers are reached through scripts; without it they are declared directly.
27
+
16
28
  ## Development
17
29
 
18
30
  ```sh
@@ -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 { applyIntentGuideline } from "../src/context/intent-trigger.ts";
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 applyIntentGuideline(event, state.get(), jevHolder.forContext(ctx));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.77.0",
3
+ "version": "0.79.0",
4
4
  "description": "A pi extension package representing a seasoned approach to software development using a full AI SDLC.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -42,6 +42,11 @@ A warning is an error that has not been triaged yet. Before every commit:
42
42
  is made type-clean and lint-clean, dropped from the list, and removed from the
43
43
  `!src/subagents/...` exclusions in `biome.json`.
44
44
 
45
+ ## Running all the checks at once
46
+
47
+ `references/verify.md` has a codemode script that runs the project's checks in parallel and
48
+ returns `{tool, exit, firstFailures}` for each. Prefer it to five separate calls.
49
+
45
50
  ## What the extension does
46
51
 
47
52
  Editing a file so that it adds `#[allow(`, `#[expect(`, `// biome-ignore`,
@@ -0,0 +1,41 @@
1
+ # Verify script (codemode)
2
+
3
+ Run every check the project has in one go, in parallel, and get back only what failed.
4
+ Paste this into the `codemode` tool. Each nested `bash` call still passes through the
5
+ development-system guards, and the test-evidence tracker records each run, so a green result
6
+ here is evidence a later "all checks pass" claim can rest on.
7
+
8
+ Adjust `CHECKS` to the project profile (see the profile skill: TypeScript, Rust, ...).
9
+
10
+ ```js
11
+ const CHECKS = [
12
+ { tool: "tsc", command: "npx tsc --noEmit" },
13
+ { tool: "biome", command: "biome check --error-on-warnings ." },
14
+ { tool: "tests", command: "npm test" },
15
+ ];
16
+
17
+ const results = await Promise.all(
18
+ CHECKS.map(async ({ tool, command }) => {
19
+ const r = await tools.bash({ command });
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));
23
+ return {
24
+ tool,
25
+ exit: r.exit_code,
26
+ firstFailures: r.exit_code === 0 ? [] : (failing.length > 0 ? failing : lines.slice(-15)).slice(0, 15),
27
+ };
28
+ }),
29
+ );
30
+
31
+ return results;
32
+ ```
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
+
37
+ Read the result before saying anything passed: every `exit` must be `0`. A non-zero `exit`
38
+ means fix `firstFailures` first, then run the script again.
39
+
40
+ Keep to checks that only read: a script may call any tool, and its nested calls are guarded
41
+ like direct ones, but a verify script that edits files hides the edit inside one tool call.
@@ -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(state: DevsysState, cadence?: string): string | undefined {
13
- if (state.phase === "idle" && state.openDepartures.length === 0) return undefined;
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 = [...(cadence === undefined ? [] : [cadence]), REMINDER];
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
- /** Pure: the one guideline line an intent earns in a phase, or undefined. Questions and continuations earn none. */
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
- const idle = phase === "idle";
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" && idle) {
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
- /** Slash commands and empty prompts are not judged. */
27
- const judgeable = (prompt: string): boolean => {
28
- const trimmed = prompt.trim();
29
- return trimmed !== "" && !trimmed.startsWith("/");
30
- };
31
-
32
- /** Adapter: judge the prompt, and add the line to this run's guidelines. Jev trouble adds nothing. */
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<void> {
38
- if (!judgeable(event.prompt)) return;
39
- const judged = await judgeIntent(jev, { prompt: event.prompt, phase: state.phase });
40
- if (!judged.ok) return;
41
- const line = intentGuideline(judged.value.intent, judged.value.confidence, state.phase);
42
- if (line !== undefined) event.systemPromptOptions.promptGuidelines.push(line);
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 =>