@jwilger/pi-development-system 0.23.0 → 0.25.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.23.0",
3
+ "version": "0.25.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"
@@ -6,5 +6,5 @@ Review the work in progress with the code-review skill.
6
6
 
7
7
  1. Call `devsys_review_start` (slice and diff range: $ARGUMENTS; omit either to use the active slice and `HEAD`).
8
8
  2. Run the `agent_spawn` payload it returns, unchanged.
9
- 3. Pass the reviewer's packet verbatim to `devsys_review_record`.
9
+ 3. Pass the reviewer's packet verbatim to `devsys_review_record` with the same `diffDigest` and `diffRange` the start reply gave.
10
10
  4. Report `review: N/R clean` and the next action. If the next action is `fix-findings`, list each blocking and should-fix finding with its `path:line`.
@@ -17,7 +17,8 @@ Review is a soft gate (`review.unsatisfied`): skipping it needs a recorded depar
17
17
  2. Run that `agent_spawn` unchanged. The reviewer is a top-level agent, not a
18
18
  child of this conversation, so it does not inherit your context.
19
19
  3. Pass the reviewer's packet, verbatim, to `devsys_review_record` with the
20
- `diffDigest` the start reply gave (`packets: [...]`; all lens packets of one
20
+ `diffDigest` the start reply gave, and the same `diffRange` if you gave one
21
+ (`packets: [...]`; all lens packets of one
21
22
  round in one call). A packet is recorded once, in the round its header names;
22
23
  if the diff changed since the start, the round is refused: start again.
23
24
  4. Read the reply: `review: N/R clean` and `next:`.
@@ -59,7 +60,7 @@ commit keeps the clean rounds already earned unless findings come back.
59
60
  ## Rules of thumb
60
61
 
61
62
  - Fix the findings before the next round. Do not argue a finding away; if it is
62
- wrong, the reviewer or Jev marks it a false positive with evidence.
63
+ wrong, the reviewer marks it a false positive with evidence (Jev can only suggest).
63
64
  - Do not widen a review into an audit of the tree. The reviewer reads the diff and
64
65
  what it depends on.
65
66
  - Never edit the packet to make it pass.
@@ -146,7 +146,7 @@ async function reviewNeeds(
146
146
  },
147
147
  ];
148
148
  }
149
- const stale = stagedThenEdited(names, command);
149
+ const stale = stagedThenEdited(names, command, ctx.cwd);
150
150
  return stale.length === 0
151
151
  ? []
152
152
  : [
@@ -157,21 +157,35 @@ async function reviewNeeds(
157
157
  ];
158
158
  }
159
159
 
160
+ type Invocations = ReturnType<typeof resolveGit>["invocations"];
161
+
160
162
  /** Files staged and also changed since staging; empty when the commit stages everything itself. */
161
163
  const stagedThenEdited = (
162
164
  names: { staged: readonly string[]; unstaged: readonly string[] },
163
165
  command: string,
166
+ cwd: string,
164
167
  ): string[] => {
165
- const invocations = resolveGit(command).invocations;
166
- if (stagesAllTracked(invocations)) return [];
167
- // A path that is an argument of the commit or add itself is committed (or re-staged) from the work tree,
168
- // which was reviewed. Message text is one argument and never equals a path.
169
- const named = new Set(
170
- invocations.filter((i) => i.sub === "commit" || i.sub === "add").flatMap((i) => i.args),
171
- );
172
- return names.staged.filter((name) => names.unstaged.includes(name) && !named.has(name));
168
+ // Only what runs before or at the first commit can change what that commit records.
169
+ const all = resolveGit(command).invocations;
170
+ const firstCommit = all.findIndex((i) => i.sub === "commit");
171
+ const upto = firstCommit === -1 ? all : all.slice(0, firstCommit + 1);
172
+ if (stagesAllTracked(upto, cwd)) return [];
173
+ // A path (or a directory holding it) that is an argument of the commit or add itself is committed or
174
+ // re-staged from the work tree, which was reviewed. Message text is one argument and never equals a path.
175
+ const named = upto
176
+ .filter((i) => (i.sub === "commit" || i.sub === "add") && !isDryRun(i) && inThisDir(i, cwd))
177
+ .flatMap((i) => i.args.filter((a) => !a.startsWith("-")).map(cleanPath));
178
+ const covered = (name: string) => named.some((n) => name === n || name.startsWith(`${n}/`));
179
+ return names.staged.filter((name) => names.unstaged.includes(name) && !covered(name));
173
180
  };
174
181
 
182
+ const cleanPath = (arg: string): string => arg.replace(/^(\.\/)+/, "").replace(/\/+$/, "");
183
+ const isDryRun = (i: Invocations[number]): boolean =>
184
+ i.sub === "add" && i.args.some((a) => a === "-n" || a === "--dry-run");
185
+ /** The invocation runs in the session's directory (no `cd`/`-C`, or one that leads back to it). */
186
+ const inThisDir = (i: Invocations[number], cwd: string): boolean =>
187
+ i.dir === undefined || resolve(cwd, i.dir) === resolve(cwd);
188
+
175
189
  const STAGE_ALL = new Set(["-A", "--all", "-u", "--update", "."]);
176
190
  const BOOLEAN_SHORT = /[enqsvz]/;
177
191
 
@@ -187,15 +201,13 @@ function clusterHasA(arg: string): boolean {
187
201
 
188
202
  /**
189
203
  * The command re-stages every tracked change of this repository before it commits: `commit -a`, or
190
- * `git add -A|-u|.` with no pathspec, in the same directory, ahead of the commit. Read from parsed
191
- * invocations, not message text.
204
+ * `git add -A|-u|.` with no pathspec, in this directory. Read from parsed invocations, not message text.
192
205
  */
193
- function stagesAllTracked(invocations: ReturnType<typeof resolveGit>["invocations"]): boolean {
194
- const firstCommit = invocations.findIndex((i) => i.sub === "commit");
195
- return invocations.some((i, index) => {
196
- if (i.dir !== undefined) return false;
206
+ function stagesAllTracked(invocations: Invocations, cwd: string): boolean {
207
+ return invocations.some((i) => {
208
+ if (!inThisDir(i, cwd) || isDryRun(i)) return false;
197
209
  if (i.sub === "commit") return i.args.some((a) => a === "--all" || clusterHasA(a));
198
- if (i.sub !== "add" || (firstCommit !== -1 && index > firstCommit)) return false;
210
+ if (i.sub !== "add") return false;
199
211
  const paths = i.args.filter((a) => !a.startsWith("-"));
200
212
  return i.args.some((a) => STAGE_ALL.has(a)) && paths.every((a) => a === ".");
201
213
  });
@@ -33,7 +33,16 @@ export async function snapshotDiff(
33
33
  try {
34
34
  // Plain, parseable output whatever the user's git config says: an external diff driver or
35
35
  // forced colour removes the `diff --git` headers the per-file digests are cut from.
36
- const plain = ["-c", "core.quotePath=false", "diff", "--no-ext-diff", "--no-color"];
36
+ const plain = [
37
+ "-c",
38
+ "core.quotePath=false",
39
+ "diff",
40
+ "--no-ext-diff",
41
+ "--no-color",
42
+ // diff.mnemonicPrefix / diff.noprefix change the `a/` `b/` header prefixes the file keys are cut from.
43
+ "--src-prefix=a/",
44
+ "--dst-prefix=b/",
45
+ ];
37
46
  const [diff, stat, others] = await Promise.all([
38
47
  exec("git", [...plain, "--full-index", "--no-renames", range], { cwd, timeout: 15_000 }),
39
48
  exec("git", [...plain, "--stat", range], { cwd, timeout: 15_000 }),
@@ -146,7 +146,7 @@ export function createReviewStartTool(
146
146
  [
147
147
  `${reviewLabel(review)}; round ${round}; diff ${snap.value.digest}. ${basis}`,
148
148
  `lenses: ${lenses.join(", ")}`,
149
- `Spawn the reviewer with agent_spawn using exactly this payload, then pass its packet to devsys_review_record with diffDigest "${snap.value.digest}":`,
149
+ `Spawn the reviewer with agent_spawn using exactly this payload, then pass its packet to devsys_review_record with diffDigest "${snap.value.digest}"${range === "HEAD" ? "" : ` and diffRange "${range}"`}:`,
150
150
  JSON.stringify(spawn),
151
151
  ].join("\n"),
152
152
  );