triflux 10.20.0 → 10.20.2
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/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +34 -42
- package/README.md +30 -36
- package/bin/triflux.mjs +13 -4
- package/hooks/hook-orchestrator.mjs +1 -1
- package/hooks/hook-registry.json +76 -2
- package/hooks/hooks.json +49 -1
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +355 -16
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/session-end-cleanup.mjs +201 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +4 -0
- package/hub/account-broker.mjs +297 -8
- package/hub/codex-adapter.mjs +62 -2
- package/hub/server.mjs +51 -4
- package/hub/team/cli/services/native-control.mjs +1 -0
- package/hub/team/conductor.mjs +94 -29
- package/hub/team/native-supervisor.mjs +20 -7
- package/hub/team/swarm-cli.mjs +98 -4
- package/hub/team/swarm-hypervisor.mjs +112 -10
- package/hub/team/swarm-locks.mjs +7 -2
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/synapse-cli.mjs +243 -6
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worktree-lifecycle.mjs +49 -8
- package/hub/workers/delegator-mcp.mjs +14 -1
- package/package.json +1 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +4 -0
- package/scripts/__tests__/skill-surface.test.mjs +61 -0
- package/scripts/gen-skill-manifest.mjs +3 -0
- package/scripts/lib/skill-template.mjs +2 -0
- package/scripts/session-stale-cleanup.mjs +111 -2
- package/skills/star-prompt/skill.json +1 -1
- package/skills/tfx-analysis/skill.json +11 -3
- package/skills/tfx-auto/skill.json +2 -2
- package/skills/tfx-autopilot/skill.json +9 -4
- package/skills/tfx-consensus/skill.json +4 -3
- package/skills/tfx-debate/skill.json +11 -4
- package/skills/tfx-doctor/skill.json +3 -1
- package/skills/tfx-fullcycle/skill.json +10 -4
- package/skills/tfx-hooks/skill.json +3 -1
- package/skills/tfx-hub/skill.json +5 -3
- package/skills/tfx-index/skill.json +6 -1
- package/skills/tfx-interview/skill.json +6 -1
- package/skills/tfx-multi/skill.json +7 -3
- package/skills/tfx-panel/skill.json +11 -4
- package/skills/tfx-persist/skill.json +11 -4
- package/skills/tfx-plan/skill.json +13 -3
- package/skills/tfx-profile/skill.json +3 -1
- package/skills/tfx-prune/skill.json +7 -1
- package/skills/tfx-psmux-rules/SKILL.md +2 -0
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +22 -307
- package/skills/tfx-psmux-rules/skill.json +7 -2
- package/skills/tfx-qa/skill.json +12 -3
- package/skills/tfx-ralph/skill.json +4 -1
- package/skills/tfx-remote/skill.json +8 -0
- package/skills/tfx-remote-setup/SKILL.md +2 -0
- package/skills/tfx-remote-setup/SKILL.md.tmpl +23 -570
- package/skills/tfx-remote-setup/skill.json +7 -3
- package/skills/tfx-remote-spawn/SKILL.md +2 -0
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +31 -242
- package/skills/tfx-remote-spawn/skill.json +7 -3
- package/skills/tfx-research/skill.json +11 -4
- package/skills/tfx-review/skill.json +12 -3
- package/skills/tfx-setup/skill.json +3 -1
- package/skills/tfx-ship/skill.json +13 -0
- package/skills/tfx-swarm/skill.json +13 -0
- package/skills/tfx-wt/skill.json +12 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hooks/subagent-tracker.mjs — SubagentStart/SubagentStop lifecycle tracker
|
|
3
|
+
//
|
|
4
|
+
// Maintains a small repo-local JSON store of running and recently completed
|
|
5
|
+
// subagents so paired lifecycle hooks can add bounded context without blocking.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
const STATE_VERSION = 1;
|
|
17
|
+
const DEFAULT_TTL_MS = 2 * 60 * 60 * 1000;
|
|
18
|
+
const MAX_COMPLETED = 20;
|
|
19
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 750;
|
|
20
|
+
const LOCK_RETRY_MS = 20;
|
|
21
|
+
|
|
22
|
+
function readStdin() {
|
|
23
|
+
try {
|
|
24
|
+
return readFileSync(0, "utf8");
|
|
25
|
+
} catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isObject(value) {
|
|
31
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stateDirFromEnv(env = process.env, cwd = process.cwd()) {
|
|
35
|
+
return (
|
|
36
|
+
env.TRIFLUX_SUBAGENT_STATE_DIR ||
|
|
37
|
+
join(env.CLAUDE_CWD || cwd, ".triflux", "subagents")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function emptyState(now = new Date()) {
|
|
42
|
+
return {
|
|
43
|
+
version: STATE_VERSION,
|
|
44
|
+
updatedAt: now.toISOString(),
|
|
45
|
+
agents: {},
|
|
46
|
+
completed: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeState(value, now) {
|
|
51
|
+
const state = isObject(value) ? value : {};
|
|
52
|
+
return {
|
|
53
|
+
version: STATE_VERSION,
|
|
54
|
+
updatedAt:
|
|
55
|
+
typeof state.updatedAt === "string" ? state.updatedAt : now.toISOString(),
|
|
56
|
+
agents: isObject(state.agents) ? state.agents : {},
|
|
57
|
+
completed: Array.isArray(state.completed) ? state.completed : [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readState(path, now) {
|
|
62
|
+
try {
|
|
63
|
+
return normalizeState(JSON.parse(readFileSync(path, "utf8")), now);
|
|
64
|
+
} catch {
|
|
65
|
+
return emptyState(now);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeState(path, state) {
|
|
70
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
71
|
+
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
72
|
+
writeFileSync(tmpPath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
73
|
+
renameSync(tmpPath, path);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sleepSync(ms) {
|
|
77
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function withStateLock(statePath, fn, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS) {
|
|
81
|
+
const lockDir = `${statePath}.lock`;
|
|
82
|
+
const started = Date.now();
|
|
83
|
+
while (true) {
|
|
84
|
+
try {
|
|
85
|
+
mkdirSync(lockDir, { recursive: false });
|
|
86
|
+
try {
|
|
87
|
+
return fn();
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error?.code !== "EEXIST") throw error;
|
|
93
|
+
if (Date.now() - started >= timeoutMs) return undefined;
|
|
94
|
+
sleepSync(LOCK_RETRY_MS);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function lifecycleKey(input) {
|
|
100
|
+
if (typeof input.agent_id === "string" && input.agent_id.trim()) {
|
|
101
|
+
return input.agent_id;
|
|
102
|
+
}
|
|
103
|
+
if (typeof input.subagent_id === "string" && input.subagent_id.trim()) {
|
|
104
|
+
return input.subagent_id;
|
|
105
|
+
}
|
|
106
|
+
const sessionId =
|
|
107
|
+
typeof input.session_id === "string" && input.session_id.trim()
|
|
108
|
+
? input.session_id
|
|
109
|
+
: "unknown";
|
|
110
|
+
const agentType =
|
|
111
|
+
(typeof input.agent_type === "string" && input.agent_type.trim()) ||
|
|
112
|
+
(typeof input.subagent_type === "string" && input.subagent_type.trim()) ||
|
|
113
|
+
"unknown";
|
|
114
|
+
return `${sessionId}:${agentType}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function agentType(input, existing) {
|
|
118
|
+
return (
|
|
119
|
+
(typeof input.agent_type === "string" && input.agent_type.trim()) ||
|
|
120
|
+
(typeof input.subagent_type === "string" && input.subagent_type.trim()) ||
|
|
121
|
+
existing?.agentType ||
|
|
122
|
+
"unknown"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function expectedDeliverables(input) {
|
|
127
|
+
if (input.expectedDeliverables !== undefined)
|
|
128
|
+
return input.expectedDeliverables;
|
|
129
|
+
if (input.expected_deliverables !== undefined)
|
|
130
|
+
return input.expected_deliverables;
|
|
131
|
+
if (input.expected_deliverable !== undefined)
|
|
132
|
+
return input.expected_deliverable;
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function cleanupStaleAgents(state, nowMs, ttlMs) {
|
|
137
|
+
for (const [key, agent] of Object.entries(state.agents)) {
|
|
138
|
+
const startedMs = Date.parse(agent?.startedAt || "");
|
|
139
|
+
if (!Number.isFinite(startedMs) || nowMs - startedMs > ttlMs) {
|
|
140
|
+
delete state.agents[key];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function boundedCompleted(completed) {
|
|
146
|
+
return completed.slice(-MAX_COMPLETED);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function completionSummary({ key, input, running, now, durationMs }) {
|
|
150
|
+
const summary = {
|
|
151
|
+
key,
|
|
152
|
+
stoppedAt: now.toISOString(),
|
|
153
|
+
sessionId:
|
|
154
|
+
(typeof input.session_id === "string" && input.session_id.trim()) ||
|
|
155
|
+
running?.sessionId ||
|
|
156
|
+
undefined,
|
|
157
|
+
agentType: agentType(input, running),
|
|
158
|
+
durationMs,
|
|
159
|
+
};
|
|
160
|
+
if (running?.startedAt) summary.startedAt = running.startedAt;
|
|
161
|
+
if (running?.expectedDeliverables !== undefined) {
|
|
162
|
+
summary.expectedDeliverables = running.expectedDeliverables;
|
|
163
|
+
}
|
|
164
|
+
return Object.fromEntries(
|
|
165
|
+
Object.entries(summary).filter(([, value]) => value !== undefined),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function stopOutput({ summary, runningCount, completedCount }) {
|
|
170
|
+
const message =
|
|
171
|
+
`[subagent-tracker] ${summary.agentType} ` +
|
|
172
|
+
`duration=${summary.durationMs}ms ` +
|
|
173
|
+
`running=${runningCount} completed=${completedCount}`;
|
|
174
|
+
return { systemMessage: message };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function recordLifecycle(input, opts = {}) {
|
|
178
|
+
if (!isObject(input)) return undefined;
|
|
179
|
+
const eventName = input.hook_event_name;
|
|
180
|
+
if (eventName !== "SubagentStart" && eventName !== "SubagentStop") {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const now = opts.now instanceof Date ? opts.now : new Date();
|
|
185
|
+
const nowMs = now.getTime();
|
|
186
|
+
const ttlMs = Number.isFinite(opts.ttlMs) ? opts.ttlMs : DEFAULT_TTL_MS;
|
|
187
|
+
const stateDir = opts.stateDir || stateDirFromEnv(opts.env, opts.cwd);
|
|
188
|
+
const statePath = join(stateDir, "subagents.json");
|
|
189
|
+
mkdirSync(stateDir, { recursive: true });
|
|
190
|
+
return withStateLock(
|
|
191
|
+
statePath,
|
|
192
|
+
() => {
|
|
193
|
+
const state = readState(statePath, now);
|
|
194
|
+
|
|
195
|
+
cleanupStaleAgents(state, nowMs, ttlMs);
|
|
196
|
+
|
|
197
|
+
const key = lifecycleKey(input);
|
|
198
|
+
let output;
|
|
199
|
+
|
|
200
|
+
if (eventName === "SubagentStart") {
|
|
201
|
+
const existing = state.agents[key];
|
|
202
|
+
const deliverables = expectedDeliverables(input);
|
|
203
|
+
state.agents[key] = {
|
|
204
|
+
key,
|
|
205
|
+
startedAt: existing?.startedAt || now.toISOString(),
|
|
206
|
+
sessionId:
|
|
207
|
+
(typeof input.session_id === "string" && input.session_id.trim()) ||
|
|
208
|
+
existing?.sessionId ||
|
|
209
|
+
undefined,
|
|
210
|
+
agentType: agentType(input, existing),
|
|
211
|
+
...(deliverables !== undefined
|
|
212
|
+
? { expectedDeliverables: deliverables }
|
|
213
|
+
: existing?.expectedDeliverables !== undefined
|
|
214
|
+
? { expectedDeliverables: existing.expectedDeliverables }
|
|
215
|
+
: {}),
|
|
216
|
+
};
|
|
217
|
+
} else {
|
|
218
|
+
const running = state.agents[key];
|
|
219
|
+
if (running) delete state.agents[key];
|
|
220
|
+
const startedMs = Date.parse(running?.startedAt || "");
|
|
221
|
+
const durationMs = Number.isFinite(startedMs)
|
|
222
|
+
? Math.max(0, nowMs - startedMs)
|
|
223
|
+
: 0;
|
|
224
|
+
const summary = completionSummary({
|
|
225
|
+
key,
|
|
226
|
+
input,
|
|
227
|
+
running,
|
|
228
|
+
now,
|
|
229
|
+
durationMs,
|
|
230
|
+
});
|
|
231
|
+
state.completed = boundedCompleted([...state.completed, summary]);
|
|
232
|
+
output = stopOutput({
|
|
233
|
+
summary,
|
|
234
|
+
runningCount: Object.keys(state.agents).length,
|
|
235
|
+
completedCount: state.completed.length,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
state.updatedAt = now.toISOString();
|
|
240
|
+
state.completed = boundedCompleted(state.completed);
|
|
241
|
+
writeState(statePath, state);
|
|
242
|
+
return output;
|
|
243
|
+
},
|
|
244
|
+
opts.lockTimeoutMs,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function main() {
|
|
249
|
+
const raw = readStdin();
|
|
250
|
+
if (!raw.trim()) return;
|
|
251
|
+
|
|
252
|
+
let input;
|
|
253
|
+
try {
|
|
254
|
+
input = JSON.parse(raw);
|
|
255
|
+
} catch {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const output = recordLifecycle(input);
|
|
260
|
+
if (output) process.stdout.write(JSON.stringify(output));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
main();
|
|
265
|
+
} catch {
|
|
266
|
+
process.exit(0);
|
|
267
|
+
}
|
|
@@ -27,6 +27,10 @@ function main() {
|
|
|
27
27
|
process.exit(0);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
if (input.hook_event_name && input.hook_event_name !== "SubagentStop") {
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
const agentType = input.agent_type || input.subagent_type || "unknown";
|
|
31
35
|
const result = input.tool_output || input.result || "";
|
|
32
36
|
const resultStr =
|