@jwilger/pi-development-system 0.21.0 → 0.22.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.
|
@@ -3,6 +3,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
3
3
|
import { appendContextTail, renderContextTail } from "../src/context/context-tail.ts";
|
|
4
4
|
import { renderStatus, renderStatusLine, STATUS_KEY } from "../src/context/status.ts";
|
|
5
5
|
import { applyPromptSection } from "../src/context/system-prompt.ts";
|
|
6
|
+
import { type Exec, timeoutAsFailure } from "../src/core/exec.ts";
|
|
6
7
|
import { detectProfiles } from "../src/core/profile.ts";
|
|
7
8
|
import { createApprovalStore } from "../src/gates/approvals.ts";
|
|
8
9
|
import { registerCommitGuard } from "../src/gates/commit-guard.ts";
|
|
@@ -77,24 +78,26 @@ export function createDevelopmentSystem(pi: ExtensionAPI) {
|
|
|
77
78
|
return messages === undefined ? undefined : { messages };
|
|
78
79
|
});
|
|
79
80
|
|
|
81
|
+
const exec: Exec = async (command, args, options) =>
|
|
82
|
+
timeoutAsFailure(await pi.exec(command, args, options));
|
|
80
83
|
registerGitGuard({ pi, approvals, jev: (ctx) => jevHolder.forContext(ctx) });
|
|
81
84
|
registerCommitGuard({
|
|
82
85
|
pi,
|
|
83
86
|
state,
|
|
84
87
|
jev: (ctx) => jevHolder.forContext(ctx),
|
|
85
|
-
exec
|
|
88
|
+
exec,
|
|
86
89
|
});
|
|
87
90
|
registerCiCommand({
|
|
88
91
|
pi,
|
|
89
92
|
state,
|
|
90
|
-
exec
|
|
93
|
+
exec,
|
|
91
94
|
});
|
|
92
95
|
registerPushGuard({
|
|
93
96
|
pi,
|
|
94
97
|
state,
|
|
95
98
|
approvals,
|
|
96
99
|
jev: (ctx) => jevHolder.forContext(ctx),
|
|
97
|
-
exec
|
|
100
|
+
exec,
|
|
98
101
|
});
|
|
99
102
|
registerTestGuard({ pi, state, approvals, jev: (ctx) => jevHolder.forContext(ctx) });
|
|
100
103
|
registerTestEvidence({ pi, state });
|
|
@@ -107,8 +110,7 @@ export function createDevelopmentSystem(pi: ExtensionAPI) {
|
|
|
107
110
|
const reviewDeps = {
|
|
108
111
|
state,
|
|
109
112
|
jev: (ctx: ExtensionContext) => jevHolder.forContext(ctx),
|
|
110
|
-
exec
|
|
111
|
-
pi.exec(command, args, options),
|
|
113
|
+
exec,
|
|
112
114
|
} satisfies Parameters<typeof createReviewStartTool>[0];
|
|
113
115
|
pi.registerTool(createReviewStartTool(reviewDeps));
|
|
114
116
|
pi.registerTool(createReviewRecordTool(reviewDeps));
|
package/package.json
CHANGED
package/src/core/exec.ts
CHANGED
|
@@ -4,3 +4,24 @@ export type Exec = (
|
|
|
4
4
|
args: string[],
|
|
5
5
|
options?: { cwd?: string; timeout?: number },
|
|
6
6
|
) => Promise<{ code: number; stdout: string; stderr: string }>;
|
|
7
|
+
|
|
8
|
+
/** Exit code reported for a command that was killed for running too long (as `timeout(1)` does). */
|
|
9
|
+
export const TIMEOUT_CODE = 124;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* pi reports a killed command as `code: 0, killed: true`, so a timed-out `git diff` would look like an
|
|
13
|
+
* empty diff. A guard must never read that as "nothing changed": make the kill a failure.
|
|
14
|
+
*/
|
|
15
|
+
export const timeoutAsFailure = (result: {
|
|
16
|
+
code: number;
|
|
17
|
+
stdout: string;
|
|
18
|
+
stderr: string;
|
|
19
|
+
killed?: boolean;
|
|
20
|
+
}): { code: number; stdout: string; stderr: string } =>
|
|
21
|
+
result.killed === true
|
|
22
|
+
? {
|
|
23
|
+
code: TIMEOUT_CODE,
|
|
24
|
+
stdout: result.stdout,
|
|
25
|
+
stderr: `${result.stderr}\ntimed out`.trim(),
|
|
26
|
+
}
|
|
27
|
+
: { code: result.code, stdout: result.stdout, stderr: result.stderr };
|
package/src/core/review-flow.ts
CHANGED
|
@@ -56,6 +56,12 @@ export function reviewGap(state: DevsysState, slice: SliceRef, now: DiffNow): st
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
const ESCAPES: Record<string, string> = { t: "\t", n: "\n", '"': '"', "\\": "\\" };
|
|
60
|
+
|
|
61
|
+
/** Undo git's C-style quoting of a path (`\"`, `\\`, `\t`, `\n`; octal bytes were already turned off with quotePath=false). */
|
|
62
|
+
const unquote = (path: string): string =>
|
|
63
|
+
path.replace(/\\([tn"\\])/g, (_m, c: string) => ESCAPES[c] ?? c);
|
|
64
|
+
|
|
59
65
|
/** A unified diff cut into one text per file, keyed by the file's new path. */
|
|
60
66
|
export function splitDiffByFile(diff: string): Record<string, string> {
|
|
61
67
|
const out: Record<string, string> = {};
|
|
@@ -69,7 +75,7 @@ export function splitDiffByFile(diff: string): Record<string, string> {
|
|
|
69
75
|
const header = /^diff --git "?a\/.*?"? "?b\/(.*?)"?$/.exec(line);
|
|
70
76
|
if (header !== null) {
|
|
71
77
|
flush();
|
|
72
|
-
path = header[1] ?? line;
|
|
78
|
+
path = unquote(header[1] ?? line);
|
|
73
79
|
lines = [];
|
|
74
80
|
}
|
|
75
81
|
lines.push(line);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
parseConventionalCommit,
|
|
14
14
|
} from "../core/commit-message.ts";
|
|
15
15
|
import type { Exec } from "../core/exec.ts";
|
|
16
|
+
import { resolveGit } from "../core/git-invocations.ts";
|
|
16
17
|
import { reviewGap } from "../core/review-flow.ts";
|
|
17
18
|
import { type GateId, isParseError, parseGateId } from "../core/types.ts";
|
|
18
19
|
import type { Jev } from "../jev/client.ts";
|
|
@@ -152,16 +153,40 @@ const stagedThenEdited = (
|
|
|
152
153
|
names: { staged: readonly string[]; unstaged: readonly string[] } | undefined,
|
|
153
154
|
command: string,
|
|
154
155
|
): string[] =>
|
|
155
|
-
names === undefined ||
|
|
156
|
+
names === undefined || stagesAllTracked(command)
|
|
156
157
|
? []
|
|
157
|
-
:
|
|
158
|
+
: // A path named in the command is committed (or re-staged) from the work tree, which was reviewed.
|
|
159
|
+
names.staged.filter((name) => names.unstaged.includes(name) && !command.includes(name));
|
|
160
|
+
|
|
161
|
+
const STAGE_ALL = new Set(["-A", "--all", "-u", "--update", "."]);
|
|
162
|
+
|
|
163
|
+
/** The command re-stages every tracked change itself: `commit -a`, or `git add -A|-u|.`. Read from parsed invocations, not message text. */
|
|
164
|
+
function stagesAllTracked(command: string): boolean {
|
|
165
|
+
return resolveGit(command).invocations.some(
|
|
166
|
+
(i) =>
|
|
167
|
+
(i.sub === "commit" &&
|
|
168
|
+
i.args.some(
|
|
169
|
+
(a) =>
|
|
170
|
+
a === "--all" || a === "--include" || (/^-[a-zA-Z]{1,4}$/.test(a) && a.includes("a")),
|
|
171
|
+
)) ||
|
|
172
|
+
(i.sub === "add" && i.args.some((a) => STAGE_ALL.has(a))),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
158
175
|
|
|
159
176
|
async function changedNames(
|
|
160
177
|
exec: Exec,
|
|
161
178
|
cwd: string,
|
|
162
179
|
): Promise<{ staged: string[]; unstaged: string[] } | undefined> {
|
|
163
180
|
try {
|
|
164
|
-
const base = [
|
|
181
|
+
const base = [
|
|
182
|
+
"-c",
|
|
183
|
+
"core.quotePath=false",
|
|
184
|
+
"diff",
|
|
185
|
+
"--no-ext-diff",
|
|
186
|
+
"--name-only",
|
|
187
|
+
"-z",
|
|
188
|
+
"--ignore-submodules=dirty",
|
|
189
|
+
];
|
|
165
190
|
const [staged, unstaged] = await Promise.all([
|
|
166
191
|
exec("git", [...base, "--cached"], { cwd, timeout: 10_000 }),
|
|
167
192
|
exec("git", base, { cwd, timeout: 10_000 }),
|