@jwilger/pi-development-system 0.8.4 → 0.8.5

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.4",
3
+ "version": "0.8.5",
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"
@@ -101,10 +101,15 @@ export function findForbiddenTrailerKeys(text: string): string[] {
101
101
  return [...text.matchAll(AI_TRAILER_ANYWHERE)].map((m) => m[0].trim());
102
102
  }
103
103
 
104
- const isBullets = (paragraph: string): boolean =>
105
- paragraph.split("\n").every((line) => /^\s*(?:[-*+]|\d+[.)])\s/.test(line));
106
-
104
+ const BULLET = /^\s*(?:[-*+]|\d+[.)])\s+/;
107
105
  const wordCount = (text: string): number => text.split(/\s+/).filter((w) => w !== "").length;
106
+ const SHORT_BULLET_WORDS = 5;
107
+
108
+ /** A list of file names or other terse items; bullets that explain themselves are prose. */
109
+ const isTerseList = (paragraph: string): boolean =>
110
+ paragraph
111
+ .split("\n")
112
+ .every((line) => BULLET.test(line) && wordCount(line.replace(BULLET, "")) < SHORT_BULLET_WORDS);
108
113
 
109
114
  const PROSE_TRAILER_WORDS = 5;
110
115
 
@@ -124,6 +129,6 @@ export function hasRationaleBody(message: string): boolean {
124
129
  ?.split("\n") ?? [];
125
130
  const proseTrailer = trailers.length > 0 && trailerBlockLines.some(isProseTrailer);
126
131
  return (
127
- proseTrailer || body.some((paragraph) => !isBullets(paragraph) && wordCount(paragraph) >= 5)
132
+ proseTrailer || body.some((paragraph) => !isTerseList(paragraph) && wordCount(paragraph) >= 5)
128
133
  );
129
134
  }
@@ -58,14 +58,24 @@ export function splitLines(command: string): string[] {
58
58
 
59
59
  const isRedirect = (op: string): boolean => /^[<>]/.test(op) && !op.endsWith("(");
60
60
 
61
- /** Tokens of one line grouped into simple commands (operators separate segments). */
61
+ /** Tokens of one line grouped into simple commands (operators separate segments; redirects and their operands are dropped). */
62
62
  export function segments(line: string): string[][] {
63
63
  const result: string[][] = [[]];
64
+ let skipOperand = false;
64
65
  for (const token of parseShell(line, (name) => `$${name}`)) {
65
- if (typeof token === "string") result[result.length - 1]?.push(token);
66
- else if ("pattern" in token) result[result.length - 1]?.push(token.pattern);
67
- else if ("op" in token && isRedirect(token.op)) continue;
68
- else if (!("comment" in token)) result.push([]);
66
+ const current = result[result.length - 1];
67
+ if (typeof token === "string" || "pattern" in token) {
68
+ const text = typeof token === "string" ? token : token.pattern;
69
+ if (!skipOperand) current?.push(text);
70
+ skipOperand = false;
71
+ } else if ("op" in token && isRedirect(token.op)) {
72
+ // `2>&1`, `>/dev/null`: the file descriptor before it and the target after it are not arguments.
73
+ if (/^\d+$/.test(current?.at(-1) ?? "")) current?.pop();
74
+ skipOperand = true;
75
+ } else if (!("comment" in token)) {
76
+ result.push([]);
77
+ skipOperand = false;
78
+ }
69
79
  }
70
80
  return result.filter((s) => s.length > 0);
71
81
  }
@@ -63,7 +63,9 @@ function messageNeeds(message: string): Need[] {
63
63
 
64
64
  /** `-a`/`--all`, or a `git add` in the same command, widen the commit beyond what is staged. */
65
65
  const stagesEverything = (command: string): boolean =>
66
- /(?:^|\s)(?:-[a-zA-Z]*a[a-zA-Z]*|--all|--include)(?:\s|$)|\bgit\s+add\b/.test(command);
66
+ /\bgit\b[^;&|\n]*\bcommit\b[^;&|\n]*\s(?:-[a-zA-Z]*a[a-zA-Z]*|--all|--include)(?:\s|$)|\bgit\s+add\s+(?:-A|--all|-u|\.)(?:\s|$)/.test(
67
+ command,
68
+ );
67
69
 
68
70
  /** What is about to be committed: the staged changes, or every tracked change when the commit stages them itself. */
69
71
  async function pendingDiff(
@@ -66,7 +66,7 @@ async function pushesTrunk(
66
66
  }
67
67
 
68
68
  /** Whether the unpushed work is a `fix` commit whose diff Jev reads as only repairing the red build. */
69
- async function repairsRedTrunk(
69
+ async function judgeRepair(
70
70
  deps: PushGuardDeps,
71
71
  ctx: ExtensionContext,
72
72
  config: DevsysConfig,
@@ -94,6 +94,20 @@ async function repairsRedTrunk(
94
94
  return judged.ok && judged.value >= FIX_RELATED_THRESHOLD;
95
95
  }
96
96
 
97
+ /** Fails closed: any exec failure means the push is not shown to repair the red build. */
98
+ async function repairsRedTrunk(
99
+ deps: PushGuardDeps,
100
+ ctx: ExtensionContext,
101
+ config: DevsysConfig,
102
+ runId: number | undefined,
103
+ ): Promise<boolean> {
104
+ try {
105
+ return await judgeRepair(deps, ctx, config, runId);
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
97
111
  type HardStop = {
98
112
  readonly ctx: ExtensionContext;
99
113
  readonly toolCallId: string;