@ask-llm/plugin 0.13.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 +20 -0
- package/.mcp.json +3 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/agents/antigravity-reviewer.md +139 -0
- package/agents/brainstorm-coordinator.md +305 -0
- package/agents/codex-reviewer.md +194 -0
- package/agents/codex-verifier.md +149 -0
- package/agents/fable-reviewer.md +44 -0
- package/agents/gemini-reviewer.md +130 -0
- package/agents/ollama-reviewer.md +131 -0
- package/agents/sol-reviewer.md +60 -0
- package/codex-pair-defaults.json +4 -0
- package/dist/antigravity-run.d.ts +3 -0
- package/dist/antigravity-run.d.ts.map +1 -0
- package/dist/antigravity-run.js +32 -0
- package/dist/antigravity-run.js.map +1 -0
- package/dist/codex-run.d.ts +3 -0
- package/dist/codex-run.d.ts.map +1 -0
- package/dist/codex-run.js +32 -0
- package/dist/codex-run.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/ollama-run.d.ts +3 -0
- package/dist/ollama-run.d.ts.map +1 -0
- package/dist/ollama-run.js +32 -0
- package/dist/ollama-run.js.map +1 -0
- package/dist/run.d.ts +3 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +32 -0
- package/dist/run.js.map +1 -0
- package/hooks/hooks.json +55 -0
- package/package.json +104 -0
- package/pi/extensions/codex-pair.ts +870 -0
- package/pi/extensions/index.ts +13 -0
- package/pi/extensions/provider-tools.ts +241 -0
- package/pi/tsconfig.json +10 -0
- package/prompts/review.txt +75 -0
- package/scripts/codex-pair-debounce-worker.mjs +103 -0
- package/scripts/codex-pair-log.mjs +271 -0
- package/scripts/codex-pair-prompt-drain.mjs +81 -0
- package/scripts/codex-pair-session.mjs +194 -0
- package/scripts/codex-pair-stop-gate.mjs +271 -0
- package/scripts/codex-pair-watch.mjs +1525 -0
- package/scripts/lib/broker-lifecycle.mjs +575 -0
- package/scripts/lib/broker-rpc.mjs +203 -0
- package/scripts/lib/broker-transport.mjs +407 -0
- package/scripts/lib/broker.mjs +537 -0
- package/scripts/lib/debounce-state.mjs +208 -0
- package/scripts/lib/parser.d.mts +12 -0
- package/scripts/lib/parser.mjs +229 -0
- package/scripts/lib/process.mjs +39 -0
- package/scripts/lib/prompt.d.mts +8 -0
- package/scripts/lib/prompt.mjs +41 -0
- package/scripts/lib/session-registry.mjs +162 -0
- package/scripts/lib/state.d.mts +58 -0
- package/scripts/lib/state.mjs +733 -0
- package/scripts/lib/stop-gate.mjs +134 -0
- package/skills/antigravity-review/SKILL.md +49 -0
- package/skills/brainstorm/SKILL.md +105 -0
- package/skills/brainstorm-all/SKILL.md +43 -0
- package/skills/codex-image/SKILL.md +120 -0
- package/skills/codex-pair/SKILL.md +315 -0
- package/skills/codex-pair-ack/SKILL.md +64 -0
- package/skills/codex-pair-pause/SKILL.md +62 -0
- package/skills/codex-pair-resume/SKILL.md +52 -0
- package/skills/codex-review/SKILL.md +52 -0
- package/skills/codex-verify/SKILL.md +110 -0
- package/skills/compare/SKILL.md +151 -0
- package/skills/fable-review/SKILL.md +42 -0
- package/skills/gemini-review/SKILL.md +40 -0
- package/skills/multi-review/SKILL.md +182 -0
- package/skills/ollama-review/SKILL.md +40 -0
- package/skills/sol-review/SKILL.md +41 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// codex-pair Stop-gate (#142, ADR-118). Blocks turn-end while unaddressed HIGH
|
|
3
|
+
// findings remain — opt-in via `blockOn: HIGH` in .codex-pair/context.md.
|
|
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
|
+
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { debounceRoot, drainPending, joinPendingForSurface, reviewingRoot } from "./lib/debounce-state.mjs";
|
|
14
|
+
import {
|
|
15
|
+
CONTEXT_FILENAME,
|
|
16
|
+
INFLIGHT_TTL_MIN_MS,
|
|
17
|
+
PAIR_ROOT_DIR,
|
|
18
|
+
contextPath,
|
|
19
|
+
inflightRoot,
|
|
20
|
+
logPath,
|
|
21
|
+
readAcks,
|
|
22
|
+
} from "./lib/state.mjs";
|
|
23
|
+
import {
|
|
24
|
+
collectBlockingHighs,
|
|
25
|
+
collectInFlight,
|
|
26
|
+
formatBlockMessage,
|
|
27
|
+
formatInFlightMessage,
|
|
28
|
+
parseGitPorcelain,
|
|
29
|
+
selectLatestEntries,
|
|
30
|
+
} from "./lib/stop-gate.mjs";
|
|
31
|
+
import { collectSessionMarkers } from "./lib/session-registry.mjs";
|
|
32
|
+
|
|
33
|
+
const MARKER_FILE = join(PAIR_ROOT_DIR, CONTEXT_FILENAME);
|
|
34
|
+
|
|
35
|
+
function findMarkerUp(startDir) {
|
|
36
|
+
const home = homedir();
|
|
37
|
+
let current = resolve(startDir);
|
|
38
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
39
|
+
if (existsSync(join(current, MARKER_FILE))) return current;
|
|
40
|
+
const parent = dirname(current);
|
|
41
|
+
if (parent === current || current === home) return null;
|
|
42
|
+
current = parent;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readStdin() {
|
|
48
|
+
return new Promise((r) => {
|
|
49
|
+
let data = "";
|
|
50
|
+
process.stdin.on("data", (c) => (data += c.toString()));
|
|
51
|
+
process.stdin.on("end", () => r(data));
|
|
52
|
+
process.stdin.on("error", () => r(""));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
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.
|
|
59
|
+
function readMarkerScalar(markerDir, key) {
|
|
60
|
+
let text;
|
|
61
|
+
try {
|
|
62
|
+
text = readFileSync(contextPath(markerDir), "utf8");
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/); // tolerate CRLF (Windows)
|
|
67
|
+
if (!fm) return null;
|
|
68
|
+
const m = fm[1].match(new RegExp(`^\\s*${key}:\\s*(\\S+)\\s*$`, "m"));
|
|
69
|
+
return m ? m[1].trim() : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readBlockOn(markerDir) {
|
|
73
|
+
return readMarkerScalar(markerDir, "blockOn");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function gitDirtySet(markerDir) {
|
|
77
|
+
// timeout guards against a hung git (locked index, slow FS) wedging turn-end.
|
|
78
|
+
const opts = { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 };
|
|
79
|
+
try {
|
|
80
|
+
const repoRoot = execFileSync("git", ["-C", markerDir, "rev-parse", "--show-toplevel"], opts).trim();
|
|
81
|
+
const porcelain = execFileSync("git", ["-C", markerDir, "status", "--porcelain=v1", "-z"], opts);
|
|
82
|
+
return parseGitPorcelain(porcelain, repoRoot);
|
|
83
|
+
} catch {
|
|
84
|
+
return null; // not a repo / git missing / timeout → skip the [B] filter
|
|
85
|
+
}
|
|
86
|
+
}
|
|
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.
|
|
91
|
+
function readInFlightInputs(markerDir) {
|
|
92
|
+
const records = [];
|
|
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)]) {
|
|
111
|
+
try {
|
|
112
|
+
for (const name of readdirSync(root)) {
|
|
113
|
+
try {
|
|
114
|
+
lockMtimes.push(statSync(join(root, name)).mtimeMs);
|
|
115
|
+
} catch {
|
|
116
|
+
// entry vanished between readdir and stat
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// dir doesn't exist yet
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { records, lockMtimes };
|
|
124
|
+
}
|
|
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).
|
|
131
|
+
function inflightFreshMs(markerDir) {
|
|
132
|
+
const fmTimeout = Number(readMarkerScalar(markerDir, "timeoutMs"));
|
|
133
|
+
const timeout =
|
|
134
|
+
Number.isFinite(fmTimeout) && fmTimeout > 0
|
|
135
|
+
? fmTimeout
|
|
136
|
+
: Number(process.env.ASK_CODEX_TIMEOUT_MS ?? 800_000);
|
|
137
|
+
return Math.max(timeout, INFLIGHT_TTL_MIN_MS) + 60_000;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function writeAndExit(obj) {
|
|
141
|
+
process.stdout.write(`${JSON.stringify(obj)}\n`, () => process.exit(0));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Canonicalize file paths so they align with git's realpath'd repo root — on
|
|
145
|
+
// macOS `/var` resolves to `/private/var`, which would otherwise make the [B]
|
|
146
|
+
// git-status filter and the [E] relPath ack hash mismatch (ADR-118). Missing
|
|
147
|
+
// files are left as-is; [A]'s existsFn drops them.
|
|
148
|
+
function canonicalizeEntries(entries) {
|
|
149
|
+
const out = new Map();
|
|
150
|
+
for (const [file, entry] of entries) {
|
|
151
|
+
let real = file;
|
|
152
|
+
try {
|
|
153
|
+
real = realpathSync(file);
|
|
154
|
+
} catch {
|
|
155
|
+
// missing/inaccessible → keep raw; collectBlockingHighs' existsFn drops it
|
|
156
|
+
}
|
|
157
|
+
out.set(real, { ...entry, file: real });
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Evaluate one project marker: drain its pending verdicts and, if it opted into
|
|
163
|
+
// blockOn:HIGH, compute whether it wants to block (unaddressed HIGH and/or an
|
|
164
|
+
// in-flight review). Returns marker-scoped text; aggregation happens in main().
|
|
165
|
+
// Reads in-flight state from the RAW markerDir (matches where the watch hook
|
|
166
|
+
// writes it); canonicalizes only for git/log path alignment (macOS /var).
|
|
167
|
+
// Only blocking I/O is gitDirtySet (two git calls, each hard-capped at 5s), so
|
|
168
|
+
// the sequential per-marker cost is bounded even for several registered repos.
|
|
169
|
+
function evaluateMarker(markerDir) {
|
|
170
|
+
// Return the RAW drained verdicts — main() accumulates across markers and caps
|
|
171
|
+
// the surfaced blob ONCE via joinPendingForSurface, so the MAX_SURFACE_VERDICTS
|
|
172
|
+
// bound is global (not 8-per-marker). Matches codex-pair-prompt-drain.mjs.
|
|
173
|
+
const pending = drainPending(markerDir);
|
|
174
|
+
|
|
175
|
+
if (readBlockOn(markerDir) !== "HIGH") {
|
|
176
|
+
return { pending, blockReason: null };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const inFlight = collectInFlight({
|
|
180
|
+
...readInFlightInputs(markerDir),
|
|
181
|
+
now: Date.now(),
|
|
182
|
+
freshMs: inflightFreshMs(markerDir),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
let canonical = markerDir;
|
|
186
|
+
try {
|
|
187
|
+
canonical = realpathSync(markerDir);
|
|
188
|
+
} catch {
|
|
189
|
+
// keep raw on the rare realpath failure
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let logText = "";
|
|
193
|
+
try {
|
|
194
|
+
logText = readFileSync(logPath(canonical), "utf8");
|
|
195
|
+
} catch {
|
|
196
|
+
// no log yet — an in-flight first-ever review can still block below
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const blocking = collectBlockingHighs({
|
|
200
|
+
entries: canonicalizeEntries(selectLatestEntries(logText)),
|
|
201
|
+
acks: readAcks(canonical),
|
|
202
|
+
existsFn: existsSync,
|
|
203
|
+
gitDirty: gitDirtySet(canonical),
|
|
204
|
+
markerDir: canonical,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
let blockReason = null;
|
|
208
|
+
if (blocking.length > 0) {
|
|
209
|
+
blockReason = formatBlockMessage(blocking, canonical);
|
|
210
|
+
if (inFlight.any) blockReason = `${formatInFlightMessage(inFlight, canonical)}\n\n${blockReason}`;
|
|
211
|
+
} else if (inFlight.any) {
|
|
212
|
+
blockReason = formatInFlightMessage(inFlight, canonical);
|
|
213
|
+
}
|
|
214
|
+
return { pending, blockReason };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function main() {
|
|
218
|
+
const raw = await readStdin();
|
|
219
|
+
let payload;
|
|
220
|
+
try {
|
|
221
|
+
payload = JSON.parse(raw);
|
|
222
|
+
} catch {
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
if (payload?.hook_event_name !== "Stop") process.exit(0);
|
|
226
|
+
if (payload?.stop_hook_active) process.exit(0);
|
|
227
|
+
|
|
228
|
+
// ADR-131 (#209): evaluate EVERY repo active this session, not just cwd. The
|
|
229
|
+
// cwd marker may even be null (cwd repo has no .codex-pair) while edits landed
|
|
230
|
+
// in a registered sibling repo — so we no longer early-exit on a missing cwd
|
|
231
|
+
// marker; collectSessionMarkers unions cwd (if any) with the registered set.
|
|
232
|
+
const cwdMarker = findMarkerUp(process.cwd());
|
|
233
|
+
const markers = collectSessionMarkers(cwdMarker, payload?.session_id);
|
|
234
|
+
if (markers.length === 0) process.exit(0);
|
|
235
|
+
|
|
236
|
+
const allPending = [];
|
|
237
|
+
const blockReasons = [];
|
|
238
|
+
for (const markerDir of markers) {
|
|
239
|
+
const { pending, blockReason } = evaluateMarker(markerDir);
|
|
240
|
+
allPending.push(...pending);
|
|
241
|
+
if (blockReason) blockReasons.push(blockReason);
|
|
242
|
+
}
|
|
243
|
+
// Cap the surfaced blob ONCE across all markers (MAX_SURFACE_VERDICTS is global,
|
|
244
|
+
// not per-repo) — mirrors codex-pair-prompt-drain.mjs.
|
|
245
|
+
const pendingCombined = allPending.length > 0 ? joinPendingForSurface(allPending) : null;
|
|
246
|
+
|
|
247
|
+
// Block if ANY marker blocks; each repo keeps its own blockOn/timeoutMs. The
|
|
248
|
+
// block reason folds in every blocking marker's message plus all pending text;
|
|
249
|
+
// a non-blocking turn with pending verdicts surfaces them as Stop
|
|
250
|
+
// additionalContext (preserved ADR-130 channel — block path uses `reason`).
|
|
251
|
+
if (blockReasons.length > 0) {
|
|
252
|
+
let reason = blockReasons.join("\n\n");
|
|
253
|
+
if (pendingCombined) reason = `${pendingCombined}\n\n${reason}`;
|
|
254
|
+
writeAndExit({ decision: "block", reason });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (pendingCombined) {
|
|
259
|
+
writeAndExit({
|
|
260
|
+
hookSpecificOutput: { hookEventName: "Stop", additionalContext: pendingCombined },
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
process.exit(0);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
main().catch((err) => {
|
|
269
|
+
process.stderr.write(`[codex-pair] WARNING: stop-gate failed (${err?.message ?? err}). Allowing turn end — HIGH findings may remain.\n`);
|
|
270
|
+
process.exit(0);
|
|
271
|
+
});
|