@ask-llm/plugin 0.17.0 → 0.19.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/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +36 -0
- package/README.md +8 -8
- package/agents/brainstorm-coordinator.md +17 -16
- package/agents/codex-reviewer.md +1 -1
- package/agents/sol-reviewer.md +5 -5
- package/codex-pair-defaults.json +1 -1
- package/dist/brainstorm-panel.d.ts +1 -1
- package/dist/brainstorm-panel.d.ts.map +1 -1
- package/dist/brainstorm-panel.js +8 -8
- package/dist/brainstorm-panel.js.map +1 -1
- package/dist/brainstorm-run.js +1 -1
- package/dist/brainstorm-run.js.map +1 -1
- package/package.json +11 -10
- package/pi/extensions/codex-pair.ts +2 -1
- package/pi/extensions/provider-tools.ts +1 -1
- package/scripts/codex-pair-debounce-worker.mjs +60 -88
- package/scripts/codex-pair-prompt-drain.mjs +50 -64
- package/scripts/codex-pair-session.mjs +129 -168
- package/scripts/codex-pair-stop-gate.mjs +183 -233
- package/scripts/codex-pair-watch.mjs +1018 -1371
- package/scripts/lib/broker-lifecycle.mjs +677 -0
- package/scripts/lib/broker-rpc.mjs +173 -0
- package/scripts/lib/broker-transport.mjs +327 -0
- package/scripts/lib/broker.mjs +327 -0
- package/scripts/lib/debounce-state.mjs +206 -0
- package/scripts/lib/frontmatter.mjs +57 -0
- package/scripts/lib/parser.mjs +229 -0
- package/scripts/lib/process.mjs +56 -0
- package/scripts/lib/prompt.mjs +32 -0
- package/scripts/lib/session-registry.mjs +161 -0
- package/scripts/lib/state.mjs +720 -0
- package/scripts/lib/stop-gate.mjs +134 -0
- package/scripts/sol-review-transport.mjs +1 -1
- package/skills/brainstorm/SKILL.md +9 -9
- package/skills/codex-image/SKILL.md +2 -2
- package/skills/codex-pair/SKILL.md +5 -4
- package/skills/codex-review/SKILL.md +1 -1
- package/skills/grok-pair/SKILL.md +3 -3
- package/skills/sol-review/SKILL.md +5 -5
|
@@ -1,271 +1,221 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// codex-pair
|
|
3
|
-
//
|
|
4
|
-
// MUST exit 0 on every path: a throw/non-zero here would wedge every turn-end.
|
|
5
|
-
// Fail-open and LOUD (warn to stderr) on any internal error.
|
|
6
|
-
//
|
|
7
|
-
// findMarkerUp is duplicated by design (zero-workspace-imports; see prompt-drain).
|
|
8
|
-
|
|
2
|
+
// Source of truth: codex-pair-stop-gate.mts. The sibling .mjs is generated by `yarn workspace @ask-llm/plugin build:hooks`; never edit it.
|
|
3
|
+
// The Stop-gate blocks opted-in HIGH findings but fails open on internal errors.
|
|
9
4
|
import { execFileSync } from "node:child_process";
|
|
10
5
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
11
6
|
import { homedir } from "node:os";
|
|
12
7
|
import { dirname, join, resolve } from "node:path";
|
|
13
8
|
import { debounceRoot, drainPending, joinPendingForSurface, reviewingRoot } from "./lib/debounce-state.mjs";
|
|
14
9
|
import { collectSessionMarkers } from "./lib/session-registry.mjs";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
contextPath,
|
|
18
|
-
INFLIGHT_TTL_MIN_MS,
|
|
19
|
-
inflightRoot,
|
|
20
|
-
logPath,
|
|
21
|
-
PAIR_ROOT_DIR,
|
|
22
|
-
readAcks,
|
|
23
|
-
} from "./lib/state.mjs";
|
|
24
|
-
import {
|
|
25
|
-
collectBlockingHighs,
|
|
26
|
-
collectInFlight,
|
|
27
|
-
formatBlockMessage,
|
|
28
|
-
formatInFlightMessage,
|
|
29
|
-
parseGitPorcelain,
|
|
30
|
-
selectLatestEntries,
|
|
31
|
-
} from "./lib/stop-gate.mjs";
|
|
32
|
-
|
|
10
|
+
import { CONTEXT_FILENAME, contextPath, INFLIGHT_TTL_MIN_MS, inflightRoot, logPath, PAIR_ROOT_DIR, readAcks, } from "./lib/state.mjs";
|
|
11
|
+
import { collectBlockingHighs, collectInFlight, formatBlockMessage, formatInFlightMessage, parseGitPorcelain, selectLatestEntries, } from "./lib/stop-gate.mjs";
|
|
33
12
|
const MARKER_FILE = join(PAIR_ROOT_DIR, CONTEXT_FILENAME);
|
|
34
|
-
|
|
35
13
|
function findMarkerUp(startDir) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
14
|
+
const home = homedir();
|
|
15
|
+
let current = resolve(startDir);
|
|
16
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
17
|
+
if (existsSync(join(current, MARKER_FILE)))
|
|
18
|
+
return current;
|
|
19
|
+
const parent = dirname(current);
|
|
20
|
+
if (parent === current || current === home)
|
|
21
|
+
return null;
|
|
22
|
+
current = parent;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
45
25
|
}
|
|
46
|
-
|
|
47
26
|
function readStdin() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
27
|
+
return new Promise((r) => {
|
|
28
|
+
let data = "";
|
|
29
|
+
process.stdin.on("data", (c) => (data += c.toString()));
|
|
30
|
+
process.stdin.on("end", () => r(data));
|
|
31
|
+
process.stdin.on("error", () => r(""));
|
|
32
|
+
});
|
|
54
33
|
}
|
|
55
|
-
|
|
56
|
-
// Minimal frontmatter scalar read — the gate needs `blockOn` and `timeoutMs`
|
|
57
|
-
// only, so a full parser stays unnecessary. Looks for `<key>: X` inside the
|
|
58
|
-
// leading `---` block.
|
|
34
|
+
// Read only the frontmatter scalars needed by the Stop-gate.
|
|
59
35
|
function readMarkerScalar(markerDir, key) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
36
|
+
let text;
|
|
37
|
+
try {
|
|
38
|
+
text = readFileSync(contextPath(markerDir), "utf8");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/); // tolerate CRLF (Windows)
|
|
44
|
+
if (!fm)
|
|
45
|
+
return null;
|
|
46
|
+
const m = fm[1].match(new RegExp(`^\\s*${key}:\\s*(\\S+)\\s*$`, "m"));
|
|
47
|
+
return m ? m[1].trim() : null;
|
|
70
48
|
}
|
|
71
|
-
|
|
72
49
|
function readBlockOn(markerDir) {
|
|
73
|
-
|
|
50
|
+
return readMarkerScalar(markerDir, "blockOn");
|
|
74
51
|
}
|
|
75
|
-
|
|
76
52
|
function gitDirtySet(markerDir) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
53
|
+
// timeout guards against a hung git (locked index, slow FS) wedging turn-end.
|
|
54
|
+
const opts = {
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
57
|
+
timeout: 5000,
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
const repoRoot = execFileSync("git", ["-C", markerDir, "rev-parse", "--show-toplevel"], opts).trim();
|
|
61
|
+
const porcelain = execFileSync("git", ["-C", markerDir, "status", "--porcelain=v1", "-z"], opts);
|
|
62
|
+
return parseGitPorcelain(porcelain, repoRoot);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null; // not a repo / git missing / timeout → skip the [B] filter
|
|
66
|
+
}
|
|
86
67
|
}
|
|
87
|
-
|
|
88
|
-
// Read the raw inputs for collectInFlight: parsed debounce records + inflight
|
|
89
|
-
// lock mtimes. Uses the RAW (pre-realpath) markerDir — the watch hook writes
|
|
90
|
-
// this state under the same un-canonicalized root that findMarkerUp returns.
|
|
68
|
+
// Use the raw marker path because the watch hook writes in-flight state there.
|
|
91
69
|
function readInFlightInputs(markerDir) {
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const root = debounceRoot(markerDir);
|
|
95
|
-
for (const name of readdirSync(root)) {
|
|
96
|
-
if (!name.endsWith(".json")) continue;
|
|
97
|
-
try {
|
|
98
|
-
records.push(JSON.parse(readFileSync(join(root, name), "utf8")));
|
|
99
|
-
} catch {
|
|
100
|
-
// malformed record — collectInFlight tolerates junk anyway
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
} catch {
|
|
104
|
-
// no debounce dir yet
|
|
105
|
-
}
|
|
106
|
-
// Inflight locks AND worker `reviewing` markers count as running reviews —
|
|
107
|
-
// the marker covers the worker→forced-sync-hook handoff gap where the
|
|
108
|
-
// debounce record is already consumed but the lock not yet taken.
|
|
109
|
-
const lockMtimes = [];
|
|
110
|
-
for (const root of [inflightRoot(markerDir), reviewingRoot(markerDir)]) {
|
|
70
|
+
const records = [];
|
|
111
71
|
try {
|
|
112
|
-
|
|
72
|
+
const root = debounceRoot(markerDir);
|
|
73
|
+
for (const name of readdirSync(root)) {
|
|
74
|
+
if (!name.endsWith(".json"))
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
records.push(JSON.parse(readFileSync(join(root, name), "utf8")));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// malformed record — collectInFlight tolerates junk anyway
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// no debounce dir yet
|
|
86
|
+
}
|
|
87
|
+
// Reviewing markers cover the gap between debounce consumption and lock acquisition.
|
|
88
|
+
const lockMtimes = [];
|
|
89
|
+
for (const root of [inflightRoot(markerDir), reviewingRoot(markerDir)]) {
|
|
113
90
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
91
|
+
for (const name of readdirSync(root)) {
|
|
92
|
+
try {
|
|
93
|
+
lockMtimes.push(statSync(join(root, name)).mtimeMs);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// entry vanished between readdir and stat
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// dir doesn't exist yet
|
|
117
102
|
}
|
|
118
|
-
}
|
|
119
|
-
} catch {
|
|
120
|
-
// dir doesn't exist yet
|
|
121
103
|
}
|
|
122
|
-
|
|
123
|
-
return { records, lockMtimes };
|
|
104
|
+
return { records, lockMtimes };
|
|
124
105
|
}
|
|
125
|
-
|
|
126
|
-
// Lock freshness mirrors the watch hook's inflight-lock TTL — same precedence
|
|
127
|
-
// (marker frontmatter `timeoutMs` > ASK_CODEX_TIMEOUT_MS > 800s default, then
|
|
128
|
-
// the 10-min floor plus buffer) so the gate and the lock lifecycle agree on
|
|
129
|
-
// what "still reviewing" means even for projects that pin a longer per-review
|
|
130
|
-
// timeout (PR #208 review).
|
|
106
|
+
// Match the watch hook lock TTL so both agree when a review remains in flight.
|
|
131
107
|
function inflightFreshMs(markerDir) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return Math.max(timeout, INFLIGHT_TTL_MIN_MS) + 60_000;
|
|
108
|
+
const fmTimeout = Number(readMarkerScalar(markerDir, "timeoutMs"));
|
|
109
|
+
const timeout = Number.isFinite(fmTimeout) && fmTimeout > 0 ? fmTimeout : Number(process.env.ASK_CODEX_TIMEOUT_MS ?? 800_000);
|
|
110
|
+
return Math.max(timeout, INFLIGHT_TTL_MIN_MS) + 60_000;
|
|
136
111
|
}
|
|
137
|
-
|
|
138
112
|
function writeAndExit(obj) {
|
|
139
|
-
|
|
113
|
+
process.stdout.write(`${JSON.stringify(obj)}\n`, () => process.exit(0));
|
|
140
114
|
}
|
|
141
|
-
|
|
142
|
-
// Canonicalize file paths so they align with git's realpath'd repo root — on
|
|
143
|
-
// macOS `/var` resolves to `/private/var`, which would otherwise make the [B]
|
|
144
|
-
// git-status filter and the [E] relPath ack hash mismatch (ADR-118). Missing
|
|
145
|
-
// files are left as-is; [A]'s existsFn drops them.
|
|
115
|
+
// Canonicalize for git paths; macOS may resolve /var to /private/var.
|
|
146
116
|
function canonicalizeEntries(entries) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
117
|
+
const out = new Map();
|
|
118
|
+
for (const [file, entry] of entries) {
|
|
119
|
+
let real = file;
|
|
120
|
+
try {
|
|
121
|
+
real = realpathSync(file);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// missing/inaccessible → keep raw; collectBlockingHighs' existsFn drops it
|
|
125
|
+
}
|
|
126
|
+
out.set(real, { ...entry, file: real });
|
|
154
127
|
}
|
|
155
|
-
out
|
|
156
|
-
}
|
|
157
|
-
return out;
|
|
128
|
+
return out;
|
|
158
129
|
}
|
|
159
|
-
|
|
160
|
-
// Evaluate one project marker: drain its pending verdicts and, if it opted into
|
|
161
|
-
// blockOn:HIGH, compute whether it wants to block (unaddressed HIGH and/or an
|
|
162
|
-
// in-flight review). Returns marker-scoped text; aggregation happens in main().
|
|
163
|
-
// Reads in-flight state from the RAW markerDir (matches where the watch hook
|
|
164
|
-
// writes it); canonicalizes only for git/log path alignment (macOS /var).
|
|
165
|
-
// Only blocking I/O is gitDirtySet (two git calls, each hard-capped at 5s), so
|
|
166
|
-
// the sequential per-marker cost is bounded even for several registered repos.
|
|
130
|
+
// Read in-flight state from the raw marker path and canonicalize only for git/log matching.
|
|
167
131
|
function evaluateMarker(markerDir) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} else if (inFlight.any) {
|
|
210
|
-
blockReason = formatInFlightMessage(inFlight, canonical);
|
|
211
|
-
}
|
|
212
|
-
return { pending, blockReason };
|
|
132
|
+
// Aggregate raw verdicts before applying the global surface cap.
|
|
133
|
+
const pending = drainPending(markerDir);
|
|
134
|
+
if (readBlockOn(markerDir) !== "HIGH") {
|
|
135
|
+
return { pending, blockReason: null };
|
|
136
|
+
}
|
|
137
|
+
const inFlight = collectInFlight({
|
|
138
|
+
...readInFlightInputs(markerDir),
|
|
139
|
+
now: Date.now(),
|
|
140
|
+
freshMs: inflightFreshMs(markerDir),
|
|
141
|
+
});
|
|
142
|
+
let canonical = markerDir;
|
|
143
|
+
try {
|
|
144
|
+
canonical = realpathSync(markerDir);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// keep raw on the rare realpath failure
|
|
148
|
+
}
|
|
149
|
+
let logText = "";
|
|
150
|
+
try {
|
|
151
|
+
logText = readFileSync(logPath(canonical), "utf8");
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// no log yet — an in-flight first-ever review can still block below
|
|
155
|
+
}
|
|
156
|
+
const blocking = collectBlockingHighs({
|
|
157
|
+
entries: canonicalizeEntries(selectLatestEntries(logText)),
|
|
158
|
+
acks: readAcks(canonical),
|
|
159
|
+
existsFn: existsSync,
|
|
160
|
+
gitDirty: gitDirtySet(canonical),
|
|
161
|
+
markerDir: canonical,
|
|
162
|
+
});
|
|
163
|
+
let blockReason = null;
|
|
164
|
+
if (blocking.length > 0) {
|
|
165
|
+
blockReason = formatBlockMessage(blocking, canonical);
|
|
166
|
+
if (inFlight.any)
|
|
167
|
+
blockReason = `${formatInFlightMessage(inFlight, canonical)}\n\n${blockReason}`;
|
|
168
|
+
}
|
|
169
|
+
else if (inFlight.any) {
|
|
170
|
+
blockReason = formatInFlightMessage(inFlight, canonical);
|
|
171
|
+
}
|
|
172
|
+
return { pending, blockReason };
|
|
213
173
|
}
|
|
214
|
-
|
|
215
174
|
async function main() {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
175
|
+
const raw = await readStdin();
|
|
176
|
+
let payload;
|
|
177
|
+
try {
|
|
178
|
+
payload = JSON.parse(raw);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
if (payload?.hook_event_name !== "Stop")
|
|
184
|
+
process.exit(0);
|
|
185
|
+
if (payload?.stop_hook_active)
|
|
186
|
+
process.exit(0);
|
|
187
|
+
// Include registered sibling repositories even when cwd has no marker.
|
|
188
|
+
const cwdMarker = findMarkerUp(process.cwd());
|
|
189
|
+
const markers = collectSessionMarkers(cwdMarker, payload?.session_id);
|
|
190
|
+
if (markers.length === 0)
|
|
191
|
+
process.exit(0);
|
|
192
|
+
const allPending = [];
|
|
193
|
+
const blockReasons = [];
|
|
194
|
+
for (const markerDir of markers) {
|
|
195
|
+
const { pending, blockReason } = evaluateMarker(markerDir);
|
|
196
|
+
allPending.push(...pending);
|
|
197
|
+
if (blockReason)
|
|
198
|
+
blockReasons.push(blockReason);
|
|
199
|
+
}
|
|
200
|
+
// Apply the verdict surface cap once across all markers.
|
|
201
|
+
const pendingCombined = allPending.length > 0 ? joinPendingForSurface(allPending) : null;
|
|
202
|
+
// Any blocking marker blocks the turn; pending text uses the matching Stop output channel.
|
|
203
|
+
if (blockReasons.length > 0) {
|
|
204
|
+
let reason = blockReasons.join("\n\n");
|
|
205
|
+
if (pendingCombined)
|
|
206
|
+
reason = `${pendingCombined}\n\n${reason}`;
|
|
207
|
+
writeAndExit({ decision: "block", reason });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (pendingCombined) {
|
|
211
|
+
writeAndExit({
|
|
212
|
+
hookSpecificOutput: { hookEventName: "Stop", additionalContext: pendingCombined },
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
221
216
|
process.exit(0);
|
|
222
|
-
}
|
|
223
|
-
if (payload?.hook_event_name !== "Stop") process.exit(0);
|
|
224
|
-
if (payload?.stop_hook_active) process.exit(0);
|
|
225
|
-
|
|
226
|
-
// ADR-131 (#209): evaluate EVERY repo active this session, not just cwd. The
|
|
227
|
-
// cwd marker may even be null (cwd repo has no .codex-pair) while edits landed
|
|
228
|
-
// in a registered sibling repo — so we no longer early-exit on a missing cwd
|
|
229
|
-
// marker; collectSessionMarkers unions cwd (if any) with the registered set.
|
|
230
|
-
const cwdMarker = findMarkerUp(process.cwd());
|
|
231
|
-
const markers = collectSessionMarkers(cwdMarker, payload?.session_id);
|
|
232
|
-
if (markers.length === 0) process.exit(0);
|
|
233
|
-
|
|
234
|
-
const allPending = [];
|
|
235
|
-
const blockReasons = [];
|
|
236
|
-
for (const markerDir of markers) {
|
|
237
|
-
const { pending, blockReason } = evaluateMarker(markerDir);
|
|
238
|
-
allPending.push(...pending);
|
|
239
|
-
if (blockReason) blockReasons.push(blockReason);
|
|
240
|
-
}
|
|
241
|
-
// Cap the surfaced blob ONCE across all markers (MAX_SURFACE_VERDICTS is global,
|
|
242
|
-
// not per-repo) — mirrors codex-pair-prompt-drain.mjs.
|
|
243
|
-
const pendingCombined = allPending.length > 0 ? joinPendingForSurface(allPending) : null;
|
|
244
|
-
|
|
245
|
-
// Block if ANY marker blocks; each repo keeps its own blockOn/timeoutMs. The
|
|
246
|
-
// block reason folds in every blocking marker's message plus all pending text;
|
|
247
|
-
// a non-blocking turn with pending verdicts surfaces them as Stop
|
|
248
|
-
// additionalContext (preserved ADR-130 channel — block path uses `reason`).
|
|
249
|
-
if (blockReasons.length > 0) {
|
|
250
|
-
let reason = blockReasons.join("\n\n");
|
|
251
|
-
if (pendingCombined) reason = `${pendingCombined}\n\n${reason}`;
|
|
252
|
-
writeAndExit({ decision: "block", reason });
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (pendingCombined) {
|
|
257
|
-
writeAndExit({
|
|
258
|
-
hookSpecificOutput: { hookEventName: "Stop", additionalContext: pendingCombined },
|
|
259
|
-
});
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
process.exit(0);
|
|
264
217
|
}
|
|
265
|
-
|
|
266
218
|
main().catch((err) => {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
);
|
|
270
|
-
process.exit(0);
|
|
219
|
+
process.stderr.write(`[codex-pair] WARNING: stop-gate failed (${err?.message ?? err}). Allowing turn end — HIGH findings may remain.\n`);
|
|
220
|
+
process.exit(0);
|
|
271
221
|
});
|