@jwilger/pi-development-system 0.36.1 → 0.38.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.36.1",
3
+ "version": "0.38.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"
@@ -1,8 +1,7 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { redactSecrets } from "../core/redact.ts";
4
- import { summarizeOutput } from "../core/test-runner.ts";
5
- import type { Phase } from "../core/types.ts";
3
+ import { exitCodeOf, summarizeOutput } from "../core/test-runner.ts";
4
+ import type { DevsysState, Phase } from "../core/types.ts";
6
5
  import type { Jev } from "../jev/client.ts";
7
6
  import { judgeTurn, type ToolEvidence } from "../jev/questions/turn.ts";
8
7
  import type { SessionState } from "../state/session-state.ts";
@@ -29,16 +28,29 @@ export function assistantParts(message: AgentMessage): Parts | undefined {
29
28
  return { text, callsTools: message.content.some((c) => c.type === "toolCall") };
30
29
  }
31
30
 
32
- /** True when the last non-empty line of the message ends with a question mark. */
31
+ const LIST_ITEM = /^(?:\d+[.)]|[-*+])\s/;
32
+
33
+ /**
34
+ * True when the message ends by asking the user something: the last non-empty line that is not an
35
+ * option-list item ends with a question mark, optionally followed by a short parenthetical ("(y/n)").
36
+ */
33
37
  export function asksUser(text: string): boolean {
34
38
  const last = text
35
39
  .split("\n")
36
40
  .map((l) => l.trim())
37
- .filter((l) => l !== "")
41
+ .filter((l) => l !== "" && !LIST_ITEM.test(l))
38
42
  .at(-1);
39
- return last !== undefined && /\?[\s)"'*_`]*$/.test(last);
43
+ return last !== undefined && /\?(?:\s*\([^()]{0,40}\))?[\s)"'*_`]*$/.test(last);
40
44
  }
41
45
 
46
+ /** True when a scope.expansion departure already covers the slice: the remedy the note asks for exists. */
47
+ const expansionRecorded = (state: DevsysState, slice: string): boolean =>
48
+ state.openDepartures.some(
49
+ (d) =>
50
+ d.gate === "scope.expansion" &&
51
+ (d.scope.kind === "session" || (d.scope.kind === "slice" && d.scope.slice === slice)),
52
+ );
53
+
42
54
  const claimMessage = (): string =>
43
55
  "Development system: your last message claims something was run, passed or done, but no tool evidence in this run shows it. Run the verification now, or restate without the claim.";
44
56
 
@@ -74,10 +86,18 @@ export function registerTurnVerifier(deps: TurnVerifierDeps): void {
74
86
  });
75
87
  deps.pi.on("tool_result", (event) => {
76
88
  const text = event.content.flatMap((c) => (c.type === "text" ? [c.text] : [])).join("\n");
77
- const summary = redactSecrets(summarizeOutput(text));
89
+ const summary = summarizeOutput(text);
90
+ const command = typeof event.input.command === "string" ? event.input.command : undefined;
91
+ // exitCodeOf sees through `npm test | tail`, which would otherwise show a failing run as exit 0.
92
+ const exitCode = exitCodeOf({
93
+ isError: event.isError,
94
+ text,
95
+ structured: event.structuredContent,
96
+ ...(command === undefined ? {} : { command }),
97
+ });
78
98
  const item: ToolEvidence =
79
99
  event.toolName === "bash"
80
- ? { tool: "bash", summary, exitCode: event.isError ? 1 : 0 }
100
+ ? { tool: "bash", summary, exitCode }
81
101
  : { tool: event.toolName, summary };
82
102
  evidence = [...evidence, item].slice(-MAX_EVIDENCE);
83
103
  });
@@ -104,7 +124,9 @@ export function registerTurnVerifier(deps: TurnVerifierDeps): void {
104
124
  if (!judged.ok) return undefined;
105
125
  const entries = [
106
126
  ...(judged.value.unverifiedClaim >= VERIFIER_THRESHOLD ? [note(claimMessage())] : []),
107
- ...(judged.value.driftFromSlice >= VERIFIER_THRESHOLD && state.activeSlice !== undefined
127
+ ...(judged.value.driftFromSlice >= VERIFIER_THRESHOLD &&
128
+ state.activeSlice !== undefined &&
129
+ !expansionRecorded(state, state.activeSlice)
108
130
  ? [note(driftMessage(state.activeSlice))]
109
131
  : []),
110
132
  ];
@@ -86,9 +86,10 @@ function reportedExit(result: ResultLike): number {
86
86
 
87
87
  /** The last few non-empty output lines, redacted and bounded: what the runner said at the end. */
88
88
  export function summarizeOutput(output: string): string {
89
- const lines = output
89
+ // Redact first: a multi-line secret (PEM key) must be recognised before the tail is cut off its BEGIN line.
90
+ const lines = redactSecrets(output)
90
91
  .split("\n")
91
92
  .map((l) => l.trim())
92
93
  .filter((l) => l !== "" && !/^Command exited with code \d+$/.test(l));
93
- return redactSecrets(lines.slice(-SUMMARY_LINES).join(" | ")).slice(0, SUMMARY_MAX);
94
+ return lines.slice(-SUMMARY_LINES).join(" | ").slice(0, SUMMARY_MAX);
94
95
  }