Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
"email": "hi@okis.dev"
},
"description": "Multi-model orchestration marketplace for Claude Code.",
"version": "0.0.32",
"version": "0.0.33",
"plugins": [
{
"name": "grok",
"source": "./plugins/grok",
"displayName": "Grok Companion",
"description": "Local Grok CLI delegation: task, review, resumable history, best-of-n tournaments, background jobs, stats, and setup health checks.",
"version": "0.0.32",
"version": "0.0.33",
"author": {
"name": "Harry Yep"
},
Expand All @@ -34,7 +34,7 @@
"source": "./plugins/codex",
"displayName": "Codex Companion",
"description": "First party local Codex CLI delegation for tasks, reviews, resumable threads, and durable background jobs.",
"version": "0.0.32",
"version": "0.0.33",
"author": {
"name": "Harry Yep"
},
Expand All @@ -55,7 +55,7 @@
"source": "./plugins/fusion",
"displayName": "Fusion Orchestrator",
"description": "Multi-model orchestration: tier agents, routing rules, blind panel, ultra fleet, model config, and drift doctor.",
"version": "0.0.32",
"version": "0.0.33",
"author": {
"name": "Harry Yep"
},
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# changelog

## 0.0.33

- notification collection reads the transcript the notification names: every peer wrapper record in a live traffic window had a null `transcriptPath`, so the completed path extracted nothing, mislabeled clean consult wrappers (nine to eleven turns of a sixty budget, backing peer jobs all clean) as `turn_limit` incomplete, demanded manual raw transcript reads, and skipped peer footer seeding; the reconciler now stamps the record's `transcriptPath` from the notification's own output file element before transition, and a no-text completion classifies `missing_final_text` unless real turn evidence supports `turn_limit`
- the collection demand is always satisfiable: the old matcher required a full file read of a transcript that can exceed the Read tool's token cap, a physically impossible demand observed re-firing at every stop boundary for an hour; any Read of the record's `outputFile` (or `transcriptPath` when no artifact exists) now marks collection regardless of offset, limit, or a size-refused response, the demand wording drops the full-read requirement, and the final text artifact caps shrink from 192KB to 72KB so a maximum artifact always fits one read
- stray probes against reaped workers are redirected: the guard's PreToolUse now intercepts `TaskOutput` and `TaskStop` aimed at a terminal fusion worker and points at the record's output file and settlement command instead, ending the "No task found" error class the harness's one minute wrapper reap produced
- peer footer capture runs on incomplete completions too, so `peerJobId` seeds even when extraction fails and an engine job id settlement propagates to the wrapper ledger row
- workers get a wind-down instead of an overshoot: at two turns before the budget the lifecycle injects a final-deliverable instruction once per record, replacing the reactive cap that let workers run to sixty-two and sixty-four of sixty
- stop output consolidates: multiple pending records emit one combined block, and rows that are collected but unsettled carry a `settle-only:` prefix so the two obligations read differently

## 0.0.32

- worker agent turn caps stop undercutting their budgets: the harness enforces frontmatter `maxTurns` as a hard cap that ends an async worker silently as completed mid run, and `fast-worker`'s 40 sat below its own 60 turn base budget, so standard and large briefs were killed with no deliverable while the honest budget classifier watched a limit that could never fire; frontmatter now carries the doubled large sizing values (120, 60, 24) and a consistency test pins them to the budget table
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "codex",
"displayName": "Codex Companion",
"version": "0.0.32",
"version": "0.0.33",
"description": "First party local Codex CLI delegation for tasks, reviews, resumable threads, and durable background jobs.",
"author": {
"name": "Harry Yep"
Expand Down
2 changes: 1 addition & 1 deletion plugins/fusion/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "fusion",
"displayName": "Fusion Orchestrator",
"version": "0.0.32",
"version": "0.0.33",
"description": "Multi-model orchestration: tier agents, routing rules, blind panel, ultra fleet, model config, and drift doctor.",
"author": {
"name": "Harry Yep"
Expand Down
10 changes: 10 additions & 0 deletions plugins/fusion/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@
"timeout": 10
}
]
},
{
"matcher": "^(TaskOutput|TaskStop)$",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/inline-delegation-guard.mjs\"",
"timeout": 10
}
]
}
],
"PostToolUse": [
Expand Down
95 changes: 95 additions & 0 deletions plugins/fusion/scripts/inline-delegation-guard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ const WRITE_TOOLS = new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
const DELEGATION_TOOLS = new Set(["Agent", "Task"]);
const BASH_TOOL = "Bash";
const ENFORCEMENT_AUDIT_TOOLS = new Set([...WRITE_TOOLS, BASH_TOOL]);
const TASK_CONTROL_TOOLS = new Set(["TaskOutput", "TaskStop"]);
const BUILTIN_LANE = "builtin";
const MAIN_LANE = "main";
const WORKER_TERMINAL_STATUSES = new Set(["done", "incomplete", "failed", "cancelled", "owner_ended"]);
const REAPED_WORKER_TERMINAL_STATUSES = new Set(["done", "incomplete", "failed", "cancelled"]);
const NO_OP_BASH_COMMANDS = new Set(["", "true", ":"]);
const NO_OP_HEARTBEAT_REASON = "Fusion tasks are in flight for this session, so emit a text-only heartbeat instead of this no-op Bash command.";
const REAPED_WORKER_REDIRECT_AUDIT_DESCRIPTION = "reaped-worker-redirect";
const NARROW_WAVE_ADVISORY_SUFFIX = "consecutive narrow waves; fleet default applies: consider /fusion:ultra for the remaining packages or state fleet-decline: <reason>.";
const MISSING_LAUNCH_RECOVERY_REASON = "missing-launch-recovered";

Expand Down Expand Up @@ -126,6 +129,53 @@ function sessionHasInFlightWorkerTasks(sessionId, env = process.env) {
return false;
}

function extractTaskControlId(toolInput) {
if (!toolInput || typeof toolInput !== "object") {
return null;
}
return typeof toolInput.task_id === "string" && toolInput.task_id.length > 0 ? toolInput.task_id : null;
}

function findWorkerRecordForTaskControlId(taskId, env = process.env) {
const jobsDir = path.join(resolveWorkerStateDir(env), "jobs");
let entries;
try {
entries = fs.readdirSync(jobsDir, { withFileTypes: true });
} catch {
return null;
}
const matches = [];
for (const entry of entries) {
if (!entry.isFile() || !entry.name.startsWith("fusion-") || !entry.name.endsWith(".json")) {
continue;
}
try {
const record = JSON.parse(fs.readFileSync(path.join(jobsDir, entry.name), "utf8"));
if (!record || typeof record !== "object" || Array.isArray(record)) {
continue;
}
if (record.backgroundTaskId === taskId || record.agentId === taskId) {
matches.push(record);
}
} catch {
continue;
}
}
return matches.find((record) => !REAPED_WORKER_TERMINAL_STATUSES.has(record.transportStatus)) ?? matches[0] ?? null;
}

function reapedWorkerRedirectReason(record, taskId) {
const settlementTaskId = typeof record.taskId === "string" && record.taskId.length > 0 ? record.taskId : taskId;
const settlement = `/fusion:stats --record ${settlementTaskId}=<verdict>`;
if (typeof record.outputFile === "string" && record.outputFile.length > 0) {
return `This worker already completed and was reaped; its result is at ${record.outputFile}; Read that file and settle via ${settlement} instead of probing the reaped agent.`;
}
if (typeof record.transcriptPath === "string" && record.transcriptPath.length > 0) {
return `This worker already completed and was reaped; its transcript is at ${record.transcriptPath}; Read that file and settle via ${settlement} instead of probing the reaped agent.`;
}
return `This worker already completed and was reaped; settle via ${settlement} instead of probing the reaped agent.`;
}

function extractBashCommand(toolInput) {
if (!toolInput || typeof toolInput !== "object") {
return null;
Expand Down Expand Up @@ -417,6 +467,24 @@ function normalizeAuditEvent(event) {
? { schemaVersion: AUDIT_SCHEMA_VERSION, at: new Date(atMs).toISOString(), session, event: "warn", lane, tool: event.tool, reason: MISSING_LAUNCH_RECOVERY_REASON }
: null;
}
if (
(event.event === "deny" || event.event === "warn") &&
TASK_CONTROL_TOOLS.has(event.tool) &&
event.lane === MAIN_LANE &&
event.description === REAPED_WORKER_REDIRECT_AUDIT_DESCRIPTION &&
(event.mode === "enforce" || event.mode === "advisory")
) {
return {
schemaVersion: AUDIT_SCHEMA_VERSION,
at: new Date(atMs).toISOString(),
session,
event: event.event,
lane: MAIN_LANE,
tool: event.tool,
description: REAPED_WORKER_REDIRECT_AUDIT_DESCRIPTION,
mode: event.mode
};
}
if (
(event.event === "deny" || event.event === "warn") &&
ENFORCEMENT_AUDIT_TOOLS.has(event.tool) &&
Expand Down Expand Up @@ -952,6 +1020,33 @@ function runHook(env = process.env, input = readHookInput()) {
return;
}

if (TASK_CONTROL_TOOLS.has(toolName)) {
if (input.hook_event_name && input.hook_event_name !== "PreToolUse") {
return;
}
const taskId = extractTaskControlId(input.tool_input);
const record = taskId ? findWorkerRecordForTaskControlId(taskId, env) : null;
if (!record || !REAPED_WORKER_TERMINAL_STATUSES.has(record.transportStatus)) {
return;
}
const mode = resolveMode(env);
const reason = reapedWorkerRedirectReason(record, taskId);
recordAuditEvent(
{
at: now,
session: sessionId,
event: mode === "advisory" ? "warn" : "deny",
lane: MAIN_LANE,
tool: toolName,
description: REAPED_WORKER_REDIRECT_AUDIT_DESCRIPTION,
mode
},
env
);
process.stdout.write(`${JSON.stringify(mode === "advisory" ? allowOutput(reason) : denyOutput(reason))}\n`);
return;
}

if (toolName === BASH_TOOL) {
if (input.hook_event_name && input.hook_event_name !== "PreToolUse") {
return;
Expand Down
2 changes: 1 addition & 1 deletion plugins/fusion/scripts/lib/worker-state.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ export function refreshWorkerTranscript(record, transcriptPath) {
snapshot.toolUseIds = snapshot.toolUseIds.slice(-2048);
const usageEntries = Object.entries(snapshot.usageMessages).slice(-512);
snapshot.usageMessages = Object.fromEntries(usageEntries);
snapshot.turns = snapshot.turnIds.length;
snapshot.turns = Math.max(integer(record.turns), snapshot.turnIds.length);
if (record.toolCallsSource !== "tool-response") {
snapshot.toolCalls = snapshot.toolUseIds.length;
snapshot.toolCallsSource = snapshot.toolUseIds.length > 0 ? "agent-transcript" : record.toolCallsSource;
Expand Down
Loading
Loading