@jwilger/pi-development-system 0.76.0 → 0.78.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.76.0",
3
+ "version": "0.78.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,36 @@
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 2>&1 | tail -n 60" },
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
+ return {
22
+ tool,
23
+ exit: r.exit_code,
24
+ firstFailures: r.exit_code === 0 ? [] : lines.slice(0, 15),
25
+ };
26
+ }),
27
+ );
28
+
29
+ return results;
30
+ ```
31
+
32
+ Read the result before saying anything passed: every `exit` must be `0`. A non-zero `exit`
33
+ means fix `firstFailures` first, then run the script again.
34
+
35
+ Keep to checks that only read: a script may call any tool, and its nested calls are guarded
36
+ like direct ones, but a verify script that edits files hides the edit inside one tool call.
@@ -5,6 +5,7 @@ import type {
5
5
  ToolResultEvent,
6
6
  } from "@earendil-works/pi-coding-agent";
7
7
  import { redactSecrets } from "../core/redact.ts";
8
+ import { reviewOf } from "../core/review-flow.ts";
8
9
  import { exitCodeOf, summarizeOutput } from "../core/test-runner.ts";
9
10
  import type { DevsysState, Phase } from "../core/types.ts";
10
11
  import type { Jev } from "../jev/client.ts";
@@ -58,6 +59,9 @@ export function asksUser(text: string): boolean {
58
59
  return QUESTION_END.test(lead) || (LIST_ITEM.test(last) && CHOICE_LEAD_IN.test(lead));
59
60
  }
60
61
 
62
+ const hasReviewRound = (state: DevsysState, slice: DevsysState["activeSlice"] & string): boolean =>
63
+ (reviewOf(state, slice)?.rounds.length ?? 0) > 0;
64
+
61
65
  /** True when a scope.expansion departure already covers the slice: the remedy the note asks for exists. */
62
66
  const expansionRecorded = (state: DevsysState, slice: string): boolean =>
63
67
  state.openDepartures.some(
@@ -72,6 +76,9 @@ const claimMessage = (): string =>
72
76
  const driftMessage = (slice: string): string =>
73
77
  `Development system: your last message describes work beyond the active slice "${slice}". Either return to the slice, or call devsys_record_departure (gate scope.expansion) to record why the scope is growing.`;
74
78
 
79
+ const reviewMessage = (slice: string): string =>
80
+ `Development system: your last message calls the slice "${slice}" finished, but no review round has been recorded for it. Call devsys_review_start for a fresh-context review before committing.`;
81
+
75
82
  const TARGET_MAX = 120;
76
83
 
77
84
  const note = (content: string) => ({
@@ -107,7 +114,7 @@ function evidenceOf(event: ToolResultEvent): ToolEvidence {
107
114
 
108
115
  /** The corrective notes a judged turn earns: an unverified claim, and drift not yet covered by a departure. */
109
116
  function correctionsFor(
110
- judged: { unverifiedClaim: number; driftFromSlice: number },
117
+ judged: { unverifiedClaim: number; driftFromSlice: number; sliceDone: number },
111
118
  state: DevsysState,
112
119
  ) {
113
120
  const slice = state.activeSlice;
@@ -118,6 +125,11 @@ function correctionsFor(
118
125
  return [
119
126
  ...(judged.unverifiedClaim >= VERIFIER_THRESHOLD ? [note(claimMessage())] : []),
120
127
  ...(drifted && slice !== undefined ? [note(driftMessage(slice))] : []),
128
+ ...(slice !== undefined &&
129
+ judged.sliceDone >= VERIFIER_THRESHOLD &&
130
+ !hasReviewRound(state, slice)
131
+ ? [note(reviewMessage(slice))]
132
+ : []),
121
133
  ];
122
134
  }
123
135
 
@@ -163,6 +175,10 @@ export function registerTurnVerifier(deps: TurnVerifierDeps): void {
163
175
  assistantText: parts.text,
164
176
  toolEvidence: evidence,
165
177
  ...(state.activeSlice === undefined ? {} : { activeSlice: state.activeSlice }),
178
+ checkDone:
179
+ state.phase === "implementing" &&
180
+ state.activeSlice !== undefined &&
181
+ !hasReviewRound(state, state.activeSlice),
166
182
  });
167
183
  if (!judged.ok) return undefined;
168
184
  const entries = correctionsFor(judged.value, state);
@@ -24,13 +24,31 @@ export const DRIFT_QUESTION: ClassifierBoolQuestion = {
24
24
  },
25
25
  };
26
26
 
27
+ export const DONE_QUESTION: ClassifierBoolQuestion = {
28
+ type: "bool",
29
+ instructions:
30
+ "`activeSlice` names the one slice of work the agent is doing. Does `assistantText` say that the slice's work is finished and ready to commit or hand over, rather than reporting progress, planning more work or asking something?",
31
+ criteria: {
32
+ true: "The message presents the slice as complete: implemented, tested and ready to commit, push or review",
33
+ false:
34
+ "The message reports partial progress, plans next steps, asks a question or is unrelated to finishing the slice",
35
+ },
36
+ };
37
+
27
38
  export type ToolEvidence = { tool: string; summary: string; exitCode?: number };
28
39
  export type TurnInput = {
29
40
  assistantText: string;
30
41
  toolEvidence: readonly ToolEvidence[];
31
42
  activeSlice?: string;
43
+ /** Also ask whether the message calls the slice finished (only meaningful with an active slice). */
44
+ checkDone?: boolean;
45
+ };
46
+ export type TurnJudgement = {
47
+ unverifiedClaim: number;
48
+ driftFromSlice: number;
49
+ /** 0 unless `checkDone` was set and a slice is active. */
50
+ sliceDone: number;
32
51
  };
33
- export type TurnJudgement = { unverifiedClaim: number; driftFromSlice: number };
34
52
 
35
53
  const TEXT_MAX = 4000;
36
54
  const SUMMARY_MAX = 240;
@@ -54,7 +72,7 @@ const probability = (
54
72
  : err({ kind: "provider", message: `malformed ${name} answer` });
55
73
  };
56
74
 
57
- /** Two independent narrow questions; drift is only asked when a slice is active. */
75
+ /** Narrow questions asked together; drift and done only when a slice is active (done only on request). */
58
76
  export async function judgeTurn(
59
77
  jev: Jev,
60
78
  input: TurnInput,
@@ -65,17 +83,23 @@ export async function judgeTurn(
65
83
  toolEvidence: input.toolEvidence.slice(-EVIDENCE_MAX).map(evidenceLine),
66
84
  ...(slice === undefined ? {} : { activeSlice: slice }),
67
85
  };
68
- const asked = await jev.ask(
69
- state,
70
- slice === undefined
71
- ? { claim: CLAIM_QUESTION }
72
- : { claim: CLAIM_QUESTION, drift: DRIFT_QUESTION },
73
- );
86
+ const wantDone = slice !== undefined && input.checkDone === true;
87
+ const asked = await jev.ask(state, {
88
+ claim: CLAIM_QUESTION,
89
+ ...(slice === undefined ? {} : { drift: DRIFT_QUESTION }),
90
+ ...(wantDone ? { done: DONE_QUESTION } : {}),
91
+ });
74
92
  if (!asked.ok) return asked;
75
93
  const claim = probability(asked.value.claim, "claim");
76
94
  if (!claim.ok) return claim;
77
- if (slice === undefined) return ok({ unverifiedClaim: claim.value, driftFromSlice: 0 });
95
+ if (slice === undefined)
96
+ return ok({ unverifiedClaim: claim.value, driftFromSlice: 0, sliceDone: 0 });
78
97
  const drift = probability(asked.value.drift, "drift");
79
98
  if (!drift.ok) return drift;
80
- return ok({ unverifiedClaim: claim.value, driftFromSlice: drift.value });
99
+ if (!wantDone) {
100
+ return ok({ unverifiedClaim: claim.value, driftFromSlice: drift.value, sliceDone: 0 });
101
+ }
102
+ const done = probability(asked.value.done, "done");
103
+ if (!done.ok) return done;
104
+ return ok({ unverifiedClaim: claim.value, driftFromSlice: drift.value, sliceDone: done.value });
81
105
  }