@windyroad/jtbd 0.14.3 → 0.14.4
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/hooks/hooks.json
CHANGED
|
@@ -76,6 +76,26 @@
|
|
|
76
76
|
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/jtbd-slide-marker.sh\""
|
|
77
77
|
}
|
|
78
78
|
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"matcher": "collaboration.spawn_agent|collaboration.wait_agent|collaboration.interrupt_agent|collaborationspawn_agent|collaborationwait_agent|collaborationinterrupt_agent|spawn_agent|wait_agent|interrupt_agent|close_agent|multi_agent_v1__spawn_agent|multi_agent_v1__wait_agent|multi_agent_v1__close_agent",
|
|
82
|
+
"hooks": [
|
|
83
|
+
{
|
|
84
|
+
"type": "command",
|
|
85
|
+
"command": "node \"${PLUGIN_ROOT}/hooks-codex/codex-agent-completion.mjs\""
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"SubagentStop": [
|
|
91
|
+
{
|
|
92
|
+
"matcher": "^wr-jtbd:agent$",
|
|
93
|
+
"hooks": [
|
|
94
|
+
{
|
|
95
|
+
"type": "command",
|
|
96
|
+
"command": "node \"${PLUGIN_ROOT}/hooks-codex/codex-agent-completion.mjs\""
|
|
97
|
+
}
|
|
98
|
+
]
|
|
79
99
|
}
|
|
80
100
|
]
|
|
81
101
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const SPAWN_TOOLS = new Set([
|
|
2
|
+
"collaboration.spawn_agent",
|
|
3
|
+
"collaborationspawn_agent",
|
|
4
|
+
"spawn_agent",
|
|
5
|
+
"multi_agent_v1__spawn_agent",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export const WAIT_TOOLS = new Set([
|
|
9
|
+
"collaboration.wait_agent",
|
|
10
|
+
"collaborationwait_agent",
|
|
11
|
+
"wait_agent",
|
|
12
|
+
"multi_agent_v1__wait_agent",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export const CLOSE_TOOLS = new Set([
|
|
16
|
+
"collaboration.interrupt_agent",
|
|
17
|
+
"collaborationinterrupt_agent",
|
|
18
|
+
"interrupt_agent",
|
|
19
|
+
"close_agent",
|
|
20
|
+
"multi_agent_v1__close_agent",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export function parsedResponse(value) {
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
const text = value
|
|
26
|
+
.filter((item) => item && typeof item === "object" && typeof item.text === "string")
|
|
27
|
+
.map((item) => item.text)
|
|
28
|
+
.join("\n");
|
|
29
|
+
return parsedResponse(text);
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "object" && value) return value;
|
|
32
|
+
try {
|
|
33
|
+
return parsedResponse(JSON.parse(value));
|
|
34
|
+
} catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function response(input) {
|
|
40
|
+
return parsedResponse(input?.tool_response);
|
|
41
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { CLOSE_TOOLS, SPAWN_TOOLS, WAIT_TOOLS, response } from "../hooks/lib/codex-completion-input.mjs";
|
|
8
|
+
|
|
9
|
+
const hookDir = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const role = "wr-jtbd:agent";
|
|
11
|
+
const writer = join(hookDir, "..", "hooks", "jtbd-mark-reviewed.sh");
|
|
12
|
+
const policy = "docs/jtbd";
|
|
13
|
+
const ttlSeconds = process.env.REVIEW_TTL ?? "3600";
|
|
14
|
+
const ttl = Number(ttlSeconds) * 1000;
|
|
15
|
+
|
|
16
|
+
function stateDir(sessionId) {
|
|
17
|
+
return join(process.env.TMPDIR || "/tmp", `claude-risk-${sessionId}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function statePath(input, target, suffix = "") {
|
|
21
|
+
return join(stateDir(input.session_id), `codex-review-${Buffer.from(role + ":" + target).toString("base64url")}${suffix}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeTarget(target) {
|
|
25
|
+
if (typeof target !== "string") return "";
|
|
26
|
+
return target.startsWith("/root/") ? target.slice("/root/".length) : target;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function policyHash(root) {
|
|
30
|
+
const result = spawnSync("bash", ["-c", 'source "$1"; _substance_hash_path "$2"', "review-policy",
|
|
31
|
+
join(hookDir, "..", "hooks", "lib", "gate-helpers.sh"), policy], { cwd: root, encoding: "utf8" });
|
|
32
|
+
const hash = result.stdout?.trim();
|
|
33
|
+
return result.status === 0 && /^[a-f0-9]{64}$/.test(hash || "") ? hash : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function diagnostic(reason, input) {
|
|
37
|
+
const dir = process.env.TMPDIR || "/tmp";
|
|
38
|
+
const path = join(dir, "codex-review-completion-diagnostic.json");
|
|
39
|
+
const temporary = join(dir, `.codex-review-completion-diagnostic-${process.pid}.tmp`);
|
|
40
|
+
try {
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
writeFileSync(temporary, JSON.stringify({
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
reason,
|
|
45
|
+
event: input?.hook_event_name === "SubagentStop" ? "SubagentStop" : input?.tool_name || "unknown",
|
|
46
|
+
role,
|
|
47
|
+
}), { mode: 0o600 });
|
|
48
|
+
renameSync(temporary, path);
|
|
49
|
+
} catch {
|
|
50
|
+
rmSync(temporary, { force: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkout(cwd) {
|
|
55
|
+
if (typeof cwd !== "string" || !cwd) return null;
|
|
56
|
+
let root;
|
|
57
|
+
try { root = realpathSync(cwd); } catch { return null; }
|
|
58
|
+
const git = spawnSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
59
|
+
if (git.status !== 0) return null;
|
|
60
|
+
let gitRoot;
|
|
61
|
+
try { gitRoot = realpathSync(git.stdout.trim()); } catch { return null; }
|
|
62
|
+
if (gitRoot !== root) return null;
|
|
63
|
+
const stat = statSync(root);
|
|
64
|
+
return { root, physical: `${stat.dev}:${stat.ino}` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function targetFromSpawn(input) {
|
|
68
|
+
const result = response(input);
|
|
69
|
+
return result.agent_id ?? result.task_name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clear(input, target) {
|
|
73
|
+
for (const suffix of ["", ".claim", ".done"]) rmSync(statePath(input, target, suffix), { force: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function remember(input) {
|
|
77
|
+
const target = normalizeTarget(targetFromSpawn(input));
|
|
78
|
+
if (!target) return;
|
|
79
|
+
mkdirSync(stateDir(input.session_id), { recursive: true });
|
|
80
|
+
clear(input, target);
|
|
81
|
+
if (input.tool_input?.agent_type !== role) return;
|
|
82
|
+
const bound = checkout(input.cwd || process.cwd());
|
|
83
|
+
if (!bound) {
|
|
84
|
+
diagnostic("invalid-spawn-checkout", input);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const hash = policyHash(bound.root);
|
|
88
|
+
if (!hash) {
|
|
89
|
+
diagnostic("policy-hash-failed", input);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(statePath(input, target), JSON.stringify({ role, target, ...bound, policyHash: hash }), { mode: 0o600 });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function claim(input, target) {
|
|
96
|
+
const path = statePath(input, target, ".claim");
|
|
97
|
+
const done = statePath(input, target, ".done");
|
|
98
|
+
if (existsSync(done)) return null;
|
|
99
|
+
try { writeFileSync(path, "", { flag: "wx", mode: 0o600 }); }
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error?.code === "EEXIST") return null;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
return { path, done };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function complete(input, target, output) {
|
|
108
|
+
target = normalizeTarget(target);
|
|
109
|
+
if (!target || typeof output !== "string" || !output) return;
|
|
110
|
+
const path = statePath(input, target);
|
|
111
|
+
if (existsSync(statePath(input, target, ".done"))) return;
|
|
112
|
+
if (!existsSync(path)) {
|
|
113
|
+
diagnostic("missing-parent-registration", input);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let registered, age;
|
|
118
|
+
try {
|
|
119
|
+
registered = JSON.parse(readFileSync(path, "utf8"));
|
|
120
|
+
age = Date.now() - Math.floor(statSync(path).mtimeMs);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
diagnostic("malformed-registration", input);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (registered.role !== role || registered.target !== target) {
|
|
127
|
+
diagnostic("registration-mismatch", input);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!Number.isFinite(age) || age < 0) {
|
|
131
|
+
diagnostic("invalid-registration-age", input);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (age >= ttl) {
|
|
135
|
+
diagnostic("stale-registration", input);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const current = checkout(input.cwd || process.cwd());
|
|
140
|
+
if (!current || current.root !== registered.root || current.physical !== registered.physical) {
|
|
141
|
+
diagnostic("checkout-mismatch", input);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const hash = policyHash(registered.root);
|
|
146
|
+
if (!hash || hash !== registered.policyHash) {
|
|
147
|
+
diagnostic(hash ? "policy-changed" : "policy-hash-failed", input);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const claimed = claim(input, target);
|
|
152
|
+
if (!claimed) return;
|
|
153
|
+
const synthetic = {
|
|
154
|
+
...input,
|
|
155
|
+
cwd: registered.root,
|
|
156
|
+
tool_name: "Agent",
|
|
157
|
+
tool_input: { subagent_type: role, prompt: "" },
|
|
158
|
+
tool_response: { content: [{ type: "text", text: output }] },
|
|
159
|
+
};
|
|
160
|
+
const result = spawnSync(writer, {
|
|
161
|
+
cwd: registered.root,
|
|
162
|
+
env: process.env,
|
|
163
|
+
input: JSON.stringify(synthetic),
|
|
164
|
+
encoding: "utf8",
|
|
165
|
+
});
|
|
166
|
+
if (result.status === 0) {
|
|
167
|
+
renameSync(claimed.path, claimed.done);
|
|
168
|
+
rmSync(path, { force: true });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
rmSync(claimed.path, { force: true });
|
|
172
|
+
diagnostic("marker-writer-failed", input);
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function close(input) {
|
|
177
|
+
complete(input, input.tool_input?.target, response(input).previous_status?.completed);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function wait(input) {
|
|
181
|
+
const statuses = response(input).status;
|
|
182
|
+
if (!statuses || typeof statuses !== "object") return;
|
|
183
|
+
for (const [target, status] of Object.entries(statuses)) complete(input, target, status?.completed);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let body = "";
|
|
187
|
+
process.stdin.setEncoding("utf8");
|
|
188
|
+
for await (const chunk of process.stdin) body += chunk;
|
|
189
|
+
let input;
|
|
190
|
+
try { input = JSON.parse(body); } catch { process.exit(0); }
|
|
191
|
+
if (!/^[A-Za-z0-9-]+$/.test(input.session_id || "")) process.exit(0);
|
|
192
|
+
if (!/^[0-9]+$/.test(ttlSeconds) || !Number.isSafeInteger(ttl) || ttl <= 0) {
|
|
193
|
+
diagnostic("invalid-review-ttl", input);
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (SPAWN_TOOLS.has(input.tool_name)) remember(input);
|
|
198
|
+
if (CLOSE_TOOLS.has(input.tool_name)) close(input);
|
|
199
|
+
if (WAIT_TOOLS.has(input.tool_name)) wait(input);
|
|
200
|
+
if (input.hook_event_name === "SubagentStop") {
|
|
201
|
+
if (input.agent_type !== role) diagnostic("unrelated-subagent-stop", input);
|
|
202
|
+
else complete(input, input.agent_id, input.last_assistant_message);
|
|
203
|
+
}
|
package/hooks-codex/hooks.json
CHANGED
|
@@ -76,6 +76,26 @@
|
|
|
76
76
|
"command": "bash \"${PLUGIN_ROOT}/hooks-codex/codex-adapter.sh\" \"${PLUGIN_ROOT}/hooks/jtbd-slide-marker.sh\""
|
|
77
77
|
}
|
|
78
78
|
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"matcher": "collaboration.spawn_agent|collaboration.wait_agent|collaboration.interrupt_agent|collaborationspawn_agent|collaborationwait_agent|collaborationinterrupt_agent|spawn_agent|wait_agent|interrupt_agent|close_agent|multi_agent_v1__spawn_agent|multi_agent_v1__wait_agent|multi_agent_v1__close_agent",
|
|
82
|
+
"hooks": [
|
|
83
|
+
{
|
|
84
|
+
"type": "command",
|
|
85
|
+
"command": "node \"${PLUGIN_ROOT}/hooks-codex/codex-agent-completion.mjs\""
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"SubagentStop": [
|
|
91
|
+
{
|
|
92
|
+
"matcher": "^wr-jtbd:agent$",
|
|
93
|
+
"hooks": [
|
|
94
|
+
{
|
|
95
|
+
"type": "command",
|
|
96
|
+
"command": "node \"${PLUGIN_ROOT}/hooks-codex/codex-agent-completion.mjs\""
|
|
97
|
+
}
|
|
98
|
+
]
|
|
79
99
|
}
|
|
80
100
|
]
|
|
81
101
|
}
|