@ask-llm/plugin 0.18.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 +16 -0
- package/package.json +5 -4
- 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 +1016 -1370
- 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/skills/codex-pair/SKILL.md +1 -1
|
@@ -1,194 +1,155 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// the broker + handshake + descriptor write (Milestone 2 PR 2);
|
|
5
|
-
// SessionEnd teardown remains TODO (Milestone 2 PR 3).
|
|
6
|
-
//
|
|
7
|
-
// The hook MUST exit 0 on every path. A broker spawn failure is logged
|
|
8
|
-
// silently to broker.log but doesn't break the session — the per-edit
|
|
9
|
-
// path keeps working via per-edit codex spawns (ADR-077).
|
|
10
|
-
|
|
2
|
+
// Source of truth: codex-pair-session.mts. The sibling .mjs is generated by `yarn workspace @ask-llm/plugin build:hooks`; never edit it.
|
|
3
|
+
// Session lifecycle hook. Broker failures must not break per-edit reviews.
|
|
11
4
|
import { access } from "node:fs/promises";
|
|
12
5
|
import { homedir } from "node:os";
|
|
13
6
|
import { dirname, join, resolve } from "node:path";
|
|
14
|
-
import {
|
|
7
|
+
import { resolveBrokerPreference } from "./lib/broker.mjs";
|
|
8
|
+
import { bootstrapBroker, teardownBroker } from "./lib/broker-lifecycle.mjs";
|
|
15
9
|
import { clearAllDebounceState } from "./lib/debounce-state.mjs";
|
|
16
10
|
import { clearSession } from "./lib/session-registry.mjs";
|
|
17
|
-
import {
|
|
18
|
-
appendLog,
|
|
19
|
-
CONTEXT_FILENAME,
|
|
20
|
-
clearAutoPause,
|
|
21
|
-
PAIR_ROOT_DIR,
|
|
22
|
-
readPauseInfo,
|
|
23
|
-
readPluginVersion,
|
|
24
|
-
resolveAutoResume,
|
|
25
|
-
} from "./lib/state.mjs";
|
|
26
|
-
|
|
11
|
+
import { appendLog, CONTEXT_FILENAME, clearAutoPause, PAIR_ROOT_DIR, readPauseInfo, readPluginVersion, resolveAutoResume, } from "./lib/state.mjs";
|
|
27
12
|
const MARKER_FILE = join(PAIR_ROOT_DIR, CONTEXT_FILENAME);
|
|
28
|
-
|
|
29
|
-
// Walk up from startDir looking for `.codex-pair/context.md`. Returns
|
|
30
|
-
// the marker directory (the directory CONTAINING `.codex-pair/`) or
|
|
31
|
-
// null. Mirrors codex-pair-watch.mjs and codex-pair-log.mjs — duplicated
|
|
32
|
-
// because zero-workspace-imports + the helper is too small to extract
|
|
33
|
-
// (15 LOC × 3 callers).
|
|
13
|
+
// Keep marker discovery local so marketplace installs need no workspace dependencies.
|
|
34
14
|
async function findMarkerUp(startDir) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
15
|
+
const home = homedir();
|
|
16
|
+
let current = resolve(startDir);
|
|
17
|
+
for (let depth = 0; depth < 20; depth++) {
|
|
18
|
+
const candidate = join(current, MARKER_FILE);
|
|
19
|
+
try {
|
|
20
|
+
await access(candidate);
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// not found here
|
|
25
|
+
}
|
|
26
|
+
const parent = dirname(current);
|
|
27
|
+
if (parent === current)
|
|
28
|
+
return null;
|
|
29
|
+
if (current === home)
|
|
30
|
+
return null;
|
|
31
|
+
current = parent;
|
|
44
32
|
}
|
|
45
|
-
|
|
46
|
-
if (parent === current) return null;
|
|
47
|
-
if (current === home) return null;
|
|
48
|
-
current = parent;
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
33
|
+
return null;
|
|
51
34
|
}
|
|
52
|
-
|
|
53
35
|
async function readStdin() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
let data = "";
|
|
38
|
+
process.stdin.on("data", (c) => {
|
|
39
|
+
data += c.toString();
|
|
40
|
+
});
|
|
41
|
+
process.stdin.on("end", () => resolve(data));
|
|
42
|
+
process.stdin.on("error", () => resolve(""));
|
|
58
43
|
});
|
|
59
|
-
process.stdin.on("end", () => resolve(data));
|
|
60
|
-
process.stdin.on("error", () => resolve(""));
|
|
61
|
-
});
|
|
62
44
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// "stale" both result in a clean slate; bootstrapBroker handles
|
|
72
|
-
// the spawn + handshake from there.
|
|
73
|
-
const state = clearStaleBrokerState(markerDir);
|
|
74
|
-
if (state === "live") return;
|
|
75
|
-
await bootstrapBroker(markerDir);
|
|
45
|
+
async function handleSessionStart(sessionId) {
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
const markerDir = await findMarkerUp(cwd);
|
|
48
|
+
if (!markerDir)
|
|
49
|
+
return; // no opt-in marker, nothing to do
|
|
50
|
+
if (typeof sessionId !== "string" || !sessionId)
|
|
51
|
+
return;
|
|
52
|
+
await bootstrapBroker(markerDir, { sessionId });
|
|
76
53
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// unlinks the descriptor + socket + lock. Returns the descriptor
|
|
85
|
-
// that was torn down (or null if none existed) — we ignore it; the
|
|
86
|
-
// hook just needs to exit 0 either way per ADR-077.
|
|
87
|
-
await teardownBroker(markerDir);
|
|
54
|
+
async function handleSessionEnd(sessionId) {
|
|
55
|
+
const cwd = process.cwd();
|
|
56
|
+
const markerDir = await findMarkerUp(cwd);
|
|
57
|
+
if (!markerDir)
|
|
58
|
+
return;
|
|
59
|
+
// SessionEnd teardown is best-effort; broker failure must not block the session.
|
|
60
|
+
await teardownBroker(markerDir, { sessionId: typeof sessionId === "string" ? sessionId : undefined });
|
|
88
61
|
}
|
|
89
|
-
|
|
90
62
|
async function main() {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
payload = JSON.parse(raw);
|
|
95
|
-
} catch {
|
|
96
|
-
process.exit(0);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const event = payload?.hook_event_name;
|
|
100
|
-
if (event !== "SessionStart" && event !== "SessionEnd") {
|
|
101
|
-
process.exit(0);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// SessionStart pause visibility (2026-07-02 seamless-pairing design; un-gated
|
|
105
|
-
// by the broker flag). An auto-pause used to be notify-ONCE and manual-resume-
|
|
106
|
-
// only — miss that single message and pairing is silently dead forever (the
|
|
107
|
-
// dogfood repo spent 18 days that way). Now: an expired auto-pause self-heals
|
|
108
|
-
// right here; a still-active pause gets a reminder the model actually sees
|
|
109
|
-
// (SessionStart supports additionalContext; it does NOT support systemMessage).
|
|
110
|
-
if (event === "SessionStart") {
|
|
63
|
+
const raw = await readStdin();
|
|
64
|
+
let payload;
|
|
111
65
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
66
|
+
payload = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
const event = payload?.hook_event_name;
|
|
72
|
+
if (event !== "SessionStart" && event !== "SessionEnd") {
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
// SessionStart reminds the model of active pauses via additionalContext and resumes expired ones.
|
|
76
|
+
if (event === "SessionStart") {
|
|
77
|
+
try {
|
|
78
|
+
const markerDir = await findMarkerUp(process.cwd());
|
|
79
|
+
const pauseInfo = markerDir ? readPauseInfo(markerDir) : null;
|
|
80
|
+
if (markerDir && pauseInfo) {
|
|
81
|
+
const decision = resolveAutoResume(pauseInfo, {
|
|
82
|
+
now: Date.now(),
|
|
83
|
+
currentVersion: readPluginVersion(),
|
|
84
|
+
});
|
|
85
|
+
let context = null;
|
|
86
|
+
// A changed pause sentinel requires rereading current state before notifying.
|
|
87
|
+
if (decision.resume && clearAutoPause(markerDir, pauseInfo)) {
|
|
88
|
+
await appendLog(markerDir, {
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
verdict: "auto_resumed",
|
|
91
|
+
reason: `${decision.why} (paused ${pauseInfo.at ?? "unknown"}, kind: ${pauseInfo.kind})`,
|
|
92
|
+
});
|
|
93
|
+
context = `codex-pair auto-resumed (${decision.why}): was ${pauseInfo.kind}-paused since ${pauseInfo.at ?? "unknown"}. Reviews are live again.`;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const current = decision.resume ? readPauseInfo(markerDir) : pauseInfo;
|
|
97
|
+
if (current) {
|
|
98
|
+
const since = current.manual ? "" : ` since ${current.at}`;
|
|
99
|
+
const kind = current.manual ? "manually" : `auto (${current.kind})`;
|
|
100
|
+
const reason = current.manual ? "" : ` Reason: ${current.reason}.`;
|
|
101
|
+
context = `codex-pair is paused — ${kind}${since}.${reason} Edits are NOT being reviewed. Resume with /codex-pair-resume.`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (context) {
|
|
105
|
+
await new Promise((resolveWrite) => {
|
|
106
|
+
const out = JSON.stringify({
|
|
107
|
+
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context },
|
|
108
|
+
});
|
|
109
|
+
process.stdout.write(`${out}\n`, () => resolveWrite());
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
139
113
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const out = JSON.stringify({
|
|
143
|
-
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context },
|
|
144
|
-
});
|
|
145
|
-
process.stdout.write(`${out}\n`, () => resolveWrite());
|
|
146
|
-
});
|
|
114
|
+
catch {
|
|
115
|
+
// best-effort (ADR-077) — pause visibility must never break the session
|
|
147
116
|
}
|
|
148
|
-
}
|
|
149
|
-
} catch {
|
|
150
|
-
// best-effort (ADR-077) — pause visibility must never break the session
|
|
151
117
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
118
|
+
// Clear debounce state only on SessionEnd; SessionStart could erase a queued verdict.
|
|
119
|
+
if (event === "SessionEnd") {
|
|
120
|
+
const dbMarkerDir = await findMarkerUp(process.cwd());
|
|
121
|
+
if (dbMarkerDir) {
|
|
122
|
+
try {
|
|
123
|
+
clearAllDebounceState(dbMarkerDir);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// best-effort (ADR-077)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Clear the registry by session ID so cwd does not affect cleanup.
|
|
130
|
+
try {
|
|
131
|
+
clearSession(payload?.session_id);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// best-effort (ADR-077)
|
|
135
|
+
}
|
|
167
136
|
}
|
|
168
|
-
|
|
169
|
-
|
|
137
|
+
const brokerMarkerDir = await findMarkerUp(process.cwd());
|
|
138
|
+
if (!brokerMarkerDir)
|
|
139
|
+
process.exit(0);
|
|
170
140
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
141
|
+
if (event === "SessionStart") {
|
|
142
|
+
if (resolveBrokerPreference(brokerMarkerDir))
|
|
143
|
+
await handleSessionStart(payload?.session_id);
|
|
144
|
+
else
|
|
145
|
+
await teardownBroker(brokerMarkerDir, { onlyIfCredentialMissing: true });
|
|
146
|
+
}
|
|
147
|
+
else if (event === "SessionEnd")
|
|
148
|
+
await handleSessionEnd(payload?.session_id);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Bootstrap failure must not break the session.
|
|
174
152
|
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Broker is disabled until ASK_CODEX_BROKER=1. Production behavior
|
|
178
|
-
// unchanged: SessionStart/SessionEnd are silent no-ops.
|
|
179
|
-
if (process.env.ASK_CODEX_BROKER !== "1") {
|
|
180
153
|
process.exit(0);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
if (event === "SessionStart") await handleSessionStart();
|
|
185
|
-
else if (event === "SessionEnd") await handleSessionEnd();
|
|
186
|
-
} catch {
|
|
187
|
-
// ADR-077 silent-on-error: a failed bootstrap MUST NOT break the
|
|
188
|
-
// session. bootstrapBroker already catches internally, but defense
|
|
189
|
-
// in depth.
|
|
190
|
-
}
|
|
191
|
-
process.exit(0);
|
|
192
154
|
}
|
|
193
|
-
|
|
194
155
|
main().catch(() => process.exit(0));
|