@jwilger/pi-development-system 0.8.9 → 0.9.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.8.9",
3
+ "version": "0.9.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"
@@ -52,7 +52,7 @@ hides every later failure. A push to a red trunk is a hard stop unless the
52
52
  commit is `fix(ci): …` (or another `fix`), touches only what the failing log names, and
53
53
  stays small (an unpushed diff over 8000 characters is never exempt); Jev must read it as
54
54
  only repairing the failing run, which needs the failing log and Jev online. Otherwise the
55
- push is a hard stop. If `gh`
55
+ push is a hard stop. A call that commits and pushes together on a red trunk is refused outright (commit in one call, push in the next). If `gh`
56
56
  is missing or unauthenticated, CI reads as unknown and the push is allowed:
57
57
  check CI by other means before relying on it.
58
58
 
@@ -26,7 +26,6 @@ export type GitInvocation = {
26
26
 
27
27
  export type GitResolution = {
28
28
  readonly invocations: readonly GitInvocation[];
29
- /** True when git may run through something the parser cannot see into (xargs, `$CMD`, …). */
30
29
  /** Words of segments that may run git in a way that cannot be read (`$GIT push`, `xargs git …`). */
31
30
  readonly opaque: readonly string[];
32
31
  };
@@ -5,6 +5,7 @@ import {
5
5
  isBashToolResult,
6
6
  isToolCallEventType,
7
7
  } from "@earendil-works/pi-coding-agent";
8
+ import { extractCommits } from "../core/commit-command.ts";
8
9
  import { parseConventionalCommit } from "../core/commit-message.ts";
9
10
  import type { Exec } from "../core/exec.ts";
10
11
  import { type PushTarget, pushTargets } from "../core/push-command.ts";
@@ -177,6 +178,12 @@ export function registerPushGuard(deps: PushGuardDeps): void {
177
178
  }
178
179
  const status = await getTrunkStatus(deps.exec, { branch: trunk, cwd: ctx.cwd });
179
180
  if (status.status !== "red") return undefined;
181
+ if (extractCommits(command).length > 0) {
182
+ return {
183
+ block: true,
184
+ reason: `CI on ${trunk} is red and this call commits and pushes together, so the push cannot be judged as a fix for it. Commit first in one call, then push in the next.`,
185
+ };
186
+ }
180
187
  if (await repairsRedTrunk(deps, ctx, loaded.value, status.runId)) return undefined;
181
188
  return hardStop(deps, {
182
189
  ctx,
@@ -35,7 +35,9 @@ export const MIX_QUESTION: ClassifierBoolQuestion = {
35
35
  },
36
36
  };
37
37
 
38
- const clip = (text: string, max: number): string => redactSecrets(text).slice(0, max);
38
+ // Bound the text before redacting (redaction cost grows with run length); keep margin so a secret at the cut is still seen whole.
39
+ const clip = (text: string, max: number): string =>
40
+ redactSecrets(text.slice(0, max * 2)).slice(0, max);
39
41
 
40
42
  export async function judgeCommit(
41
43
  jev: Jev,
@@ -23,9 +23,9 @@ export async function judgeFixRelated(
23
23
  ): Promise<Result<number, JevError>> {
24
24
  const asked = await jev.ask(
25
25
  {
26
- failingLog: redactSecrets(input.failingLog).slice(-4000),
27
- message: redactSecrets(input.message).slice(0, 2000),
28
- diff: redactSecrets(input.diff).slice(0, 8000),
26
+ failingLog: redactSecrets(input.failingLog.slice(-8000)).slice(-4000),
27
+ message: redactSecrets(input.message.slice(0, 4000)).slice(0, 2000),
28
+ diff: redactSecrets(input.diff.slice(0, 16000)).slice(0, 8000),
29
29
  },
30
30
  { related: FIX_RELATED_QUESTION },
31
31
  );
@@ -53,10 +53,14 @@ export async function runCiWatch(
53
53
  deps.pi.appendEntry(CI_ENTRY_TYPE, observed);
54
54
  },
55
55
  });
56
- ctx.ui.notify(
57
- `CI on ${config.value.delivery.trunk}: ${final.status}${final.sha === undefined ? "" : ` (${final.sha.slice(0, 7)})`}`,
58
- final.status === "red" ? "error" : "info",
59
- );
56
+ try {
57
+ ctx.ui.notify(
58
+ `CI on ${config.value.delivery.trunk}: ${final.status}${final.sha === undefined ? "" : ` (${final.sha.slice(0, 7)})`}`,
59
+ final.status === "red" ? "error" : "info",
60
+ );
61
+ } catch {
62
+ // stale context after a reload: the status line and entries were already updated
63
+ }
60
64
  return final;
61
65
  }
62
66
 
@@ -67,10 +71,15 @@ export function registerCiCommand(deps: CiCommandDeps): void {
67
71
  handler: async (_args, ctx) => {
68
72
  ctx.ui.notify("watching CI on the trunk…", "info");
69
73
  runCiWatch(deps, ctx).catch((cause: unknown) => {
70
- ctx.ui.notify(
71
- `CI watch failed: ${cause instanceof Error ? cause.message : String(cause)}`,
72
- "error",
73
- );
74
+ // ctx goes stale if the session is replaced or reloaded mid-watch; there is nobody left to tell.
75
+ try {
76
+ ctx.ui.notify(
77
+ `CI watch failed: ${cause instanceof Error ? cause.message : String(cause)}`,
78
+ "error",
79
+ );
80
+ } catch {
81
+ // stale context
82
+ }
74
83
  });
75
84
  },
76
85
  });