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.31",
"version": "0.0.32",
"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.31",
"version": "0.0.32",
"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.31",
"version": "0.0.32",
"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.31",
"version": "0.0.32",
"author": {
"name": "Harry Yep"
},
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# changelog

## 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
- notification driven collection produces a readable deliverable: the task notification completed path extracts the final assistant text from a bounded transcript tail, writes the bounded final text artifact, and points `outputFile` at it, matching the `SubagentStop` path; a transcript ending on a dangling tool result with no final text classifies as `incomplete` with `turn_limit` instead of done and collected, so a truncated worker no longer reports as a success with nothing to read
- the wrapper footer is parsed on the notification path too: peer job identity seeds from the extracted final text for rescue wrappers, so an engine job id settlement propagates to the wrapper ledger row instead of leaving it demanding a second manual settle
- fleet wave width measures launches, not completions: the guard records dispatches at `PreToolUse` launch time and confirms them at `PostToolUse`, because async completion delivery arrives minutes after a batched launch and made every genuine wide wave read as staggered solo dispatches, inflating the narrow streak counter to nine in a session whose transcript showed batched waves throughout; wave derivation counts confirmed entries only, launches that never confirm expire after fifteen minutes, a missing launch recovers in place with a warn audit event instead of silently leaving the write budget exhausted, and the narrow wave advisory re fires at every even streak milestone
- `collectionMethod` is one enum: writers canonicalize to lowercase snake case, legacy spellings fold on load and in stats reads, because four different literals were live in one ninety minute production window
- codex acceptance carries provenance: `record-acceptance` stamps `acceptanceSource` and `acceptanceRecordedAt` on the job record, every writeback path enforces the `--accept-failed-transport` gate, and fusion-stats threads `--source` through the companion argv, closing a split brain where the fusion ledger said `collector` or `main-loop` while the codex record said `stats`
- stats gains an `--unsettled` view listing terminal jobs without an explicit verdict and renders acceptance provenance when present
- grok `turn_limit` survives into history output instead of coercing to null, the review runner treats an empty raw request as the default managed review instead of stopping to ask, and both rescue agents relay companion stdout verbatim in a fenced block after a production wrapper retyped `foreground` as `preground`
- the orchestration rules describe the async wire shape without weakening obligations: a launch receipt is not delivery, completion notifications arm mandatory collection, and a dispatch whose receipt is never followed by same goal collection and verification is the contract failure; new rules cover settling consumed reviews, a broad verification pass after any timeout salvage in a wave, and checking the codex single flight slot before dispatch; troubleshooting documents the two worker truncation signatures with the SendMessage recovery recipe, and the README development section names the bench manifest check CI already runs

## 0.0.31

- the fleet default gets a per turn signal: a new `fleet-posture.mjs` UserPromptSubmit hook injects one advisory context line each turn naming the three package threshold, `/fusion:ultra`, and the `fleet-decline: <reason>` protocol, because the default previously lived as one sentence deep inside a document loaded once at session start and fired almost never; `FUSION_FLEET_MODE=off` or a `fleet-mode` state file disables it, and the compact recovery echo now restates the fleet default at the moment of maximum context loss
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ The Fusion plugin keeps model routing and reconciliation data under `~/.claude/p

`npm test` runs the suite against fake Codex and Grok binaries with no real peer CLI or network. `claude plugin validate plugins/codex`, `claude plugin validate plugins/grok`, `claude plugin validate plugins/fusion`, and `claude plugin validate .` check every plugin and the marketplace. Iterate with `claude --plugin-dir ./plugins/codex --plugin-dir ./plugins/grok --plugin-dir ./plugins/fusion` plus `/reload-plugins`; after committing and pushing, `claude plugin marketplace update claude-code-fusion` refreshes the installed copy.

CI also runs `node bench/manifest.mjs --check`; contributors should run it after touching `bench/tasks`.

## Layout

- `.claude-plugin/marketplace.json`: the marketplace manifest; installs as marketplace `claude-code-fusion`.
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.31",
"version": "0.0.32",
"description": "First party local Codex CLI delegation for tasks, reviews, resumable threads, and durable background jobs.",
"author": {
"name": "Harry Yep"
Expand Down
1 change: 1 addition & 0 deletions plugins/codex/agents/codex-rescue.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Forwarding rules:
- Do not use Read for any path except the newly allocated empty transport file, and do not read it after writing. Do not search, run Git, execute tests, inspect job state, or perform any check, collection, cancellation, or companion operation beyond the fixed task operation.
- When an explicit background request returns a receipt, return it unchanged. A direct slash command user inspects progress through status and collects the deliverable through result; when Fusion is installed, its monitor can notify them of completion. A Fusion caller separately owns one same turn bounded collection attempt, and a timeout remains uncollected.
- Return the companion stdout exactly as received. Do not summarize, paraphrase, prefix, suffix, or continue the work.
- Relay the companion's stdout verbatim inside a fenced block. Never retype, summarize, or re-spell any part of it, including footers. Put commentary outside the fence.
- If the companion invocation fails, return the failure exactly as Bash reports it. Do not generate a substitute answer.

Response style:
Expand Down
17 changes: 13 additions & 4 deletions plugins/codex/scripts/codex-companion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const TRANSPORT_OWNER_MAX_BYTES = 4096;
const TRANSPORT_MAX_AGE_MS = 60 * 60 * 1000;
const RECORD_ACCEPTANCE_JOB_ID_PATTERN = /^[a-f0-9]{32}$/;
const RECORD_ACCEPTANCE_VALUES = new Set(["accepted", "rejected", "unverified"]);
const RECORD_ACCEPTANCE_SOURCES = new Set(["collector", "main-loop", "stats"]);
const MODELS_CACHE_SCHEMA_DRIFT_NEXT_STEP = "Codex CLI cannot parse the current models cache (schema drift). Upgrade the Codex CLI, then run /codex:setup again.";
const SETUP_LOG_PROBE_BYTES = 64 * 1024;
const SETUP_LOG_PROBE_LIMIT = 5;
Expand All @@ -117,7 +118,7 @@ function printUsage() {
" node scripts/codex-companion.mjs status [job-id] [--all] [--cwd <dir>] [--json]",
" node scripts/codex-companion.mjs result <job-id> [--wait] [--wait-timeout-ms <ms>] [--cwd <dir>] [--json]",
" node scripts/codex-companion.mjs cancel <job-id> [--cwd <dir>] [--json]",
" node scripts/codex-companion.mjs record-acceptance --job-id <32 lowercase hex> --acceptance <accepted|rejected|unverified> [--reason <text>] [--accept-failed-transport]",
" node scripts/codex-companion.mjs record-acceptance --job-id <32 lowercase hex> --acceptance <accepted|rejected|unverified> [--reason <text>] [--source <collector|main-loop|stats>] [--accept-failed-transport]",
" node scripts/codex-companion.mjs history [--json]",
" node scripts/codex-companion.mjs setup [--cwd <dir>] [--json]"
].join("\n") + "\n"
Expand Down Expand Up @@ -161,9 +162,9 @@ function recordAcceptanceArgs(argv) {
options.acceptFailedTransport = true;
continue;
}
const key = token === "--job-id" ? "jobId" : token === "--acceptance" ? "acceptance" : token === "--reason" ? "reason" : null;
const key = token === "--job-id" ? "jobId" : token === "--acceptance" ? "acceptance" : token === "--reason" ? "reason" : token === "--source" ? "source" : null;
if (!key) {
throw new CompanionError("The record-acceptance command accepts only --job-id, --acceptance, --reason, and --accept-failed-transport.", "input");
throw new CompanionError("The record-acceptance command accepts only --job-id, --acceptance, --reason, --source, and --accept-failed-transport.", "input");
}
if (Object.hasOwn(options, key)) {
throw new CompanionError(`${token} may be provided only once.`, "input");
Expand All @@ -181,6 +182,10 @@ function recordAcceptanceArgs(argv) {
if (!RECORD_ACCEPTANCE_VALUES.has(options.acceptance)) {
throw new CompanionError("The --acceptance option must be accepted, rejected, or unverified.", "input");
}
options.source ??= "stats";
if (!RECORD_ACCEPTANCE_SOURCES.has(options.source)) {
throw new CompanionError("The --source option must be collector, main-loop, or stats.", "input");
}
return options;
}

Expand Down Expand Up @@ -1993,7 +1998,7 @@ async function handleCancel(rawArgv) {
}

async function handleRecordAcceptance(argv) {
const { acceptance, acceptFailedTransport, jobId, reason } = recordAcceptanceArgs(argv);
const { acceptance, acceptFailedTransport, jobId, reason, source } = recordAcceptanceArgs(argv);
const found = findJobRecordById(resolveDataDir(), jobId);
if (!found) {
throw new CompanionError(`No job record found for ${jobId}.`, "input");
Expand All @@ -2004,13 +2009,17 @@ async function handleRecordAcceptance(argv) {
}
if (acceptance !== "rejected") {
return {
acceptanceRecordedAt: nowIso(),
acceptanceSource: source,
semanticFailureKind: null,
semanticFailureMessage: null,
semanticStatus: acceptance
};
}
return {
...(reason === undefined ? {} : { semanticFailureMessage: reason }),
acceptanceRecordedAt: nowIso(),
acceptanceSource: source,
semanticStatus: acceptance
};
}, { allowTerminal: true });
Expand Down
13 changes: 13 additions & 0 deletions plugins/codex/scripts/lib/state.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const STATE_MAX_RECORDS_ENV = "CODEX_COMPANION_HISTORY_MAX_RECORDS";
const JOB_STATUSES = new Set(["running", "done", "error", "cancelled"]);
const DELIVERY_MODES = new Set(["foreground", "manual", "managed"]);
const SEMANTIC_STATUSES = new Set(["accepted", "rejected", "unverified"]);
const ACCEPTANCE_SOURCES = new Set(["collector", "main-loop", "stats"]);
const JOB_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const WORKSPACE_SLUG_PATTERN = /^.+-[a-f0-9]{16}$/;

Expand Down Expand Up @@ -433,6 +434,12 @@ function assertJobRecord(record) {
if (record.semanticStatus != null && !SEMANTIC_STATUSES.has(record.semanticStatus)) {
throw new TypeError("Job semantic status is invalid.");
}
if (record.acceptanceSource != null && !ACCEPTANCE_SOURCES.has(record.acceptanceSource)) {
throw new TypeError("Job acceptance source is invalid.");
}
if (record.acceptanceRecordedAt != null && (typeof record.acceptanceRecordedAt !== "string" || !Number.isFinite(Date.parse(record.acceptanceRecordedAt)))) {
throw new TypeError("Job acceptance recorded timestamp is invalid.");
}
}

function ownerSessionId(record) {
Expand All @@ -447,6 +454,8 @@ function normalizeRecord(record) {
delivery: record.delivery ?? (record.background ? "manual" : "foreground"),
deliveryCollectedAt: record.deliveryCollectedAt ?? null,
schemaVersion: record.schemaVersion ?? JOB_SCHEMA_VERSION,
acceptanceSource: record.acceptanceSource ?? null,
acceptanceRecordedAt: record.acceptanceRecordedAt ?? null,
semanticStatus: record.semanticStatus ?? "unverified",
semanticFailureKind: record.semanticFailureKind ?? null,
semanticFailureMessage: record.semanticFailureMessage ?? null,
Expand Down Expand Up @@ -486,6 +495,8 @@ function terminalPatch(existing, patch, timestamp) {
codexPidOwnsProcessGroup: false,
finishedAt: existing.finishedAt ?? patch.finishedAt ?? timestamp,
updatedAt: patch.updatedAt ?? timestamp,
acceptanceSource: patch.acceptanceSource ?? existing.acceptanceSource ?? null,
acceptanceRecordedAt: patch.acceptanceRecordedAt ?? existing.acceptanceRecordedAt ?? null,
failureKind: status === "cancelled" ? "cancelled" : patch.failureKind ?? existing.failureKind ?? null,
cancelRequestedAt: null
};
Expand Down Expand Up @@ -698,6 +709,8 @@ export function createJobRecord(fields) {
resolvedEffort: fields.resolvedEffort ?? fields.request?.effort ?? null,
...(fields.modelDrift ? { modelDrift: fields.modelDrift } : {}),
rolloutRecoveryStatus: fields.rolloutRecoveryStatus ?? null,
acceptanceSource: fields.acceptanceSource ?? null,
acceptanceRecordedAt: fields.acceptanceRecordedAt ?? null,
semanticStatus: fields.semanticStatus ?? "unverified",
semanticFailureKind: fields.semanticFailureKind ?? null,
semanticFailureMessage: fields.semanticFailureMessage ?? null,
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.31",
"version": "0.0.32",
"description": "Multi-model orchestration: tier agents, routing rules, blind panel, ultra fleet, model config, and drift doctor.",
"author": {
"name": "Harry Yep"
Expand Down
2 changes: 1 addition & 1 deletion plugins/fusion/agents/deep-reasoner.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: deep-reasoner
description: Read only advisory lane on Fable at maximum effort. Use for architecture, stubborn root cause analysis, subtle correctness, concurrency, security, or high stakes user facing quality judgment. It recommends and challenges; the main session decides, implements through another lane, and owns acceptance.
model: fable
effort: xhigh
maxTurns: 30
maxTurns: 60
background: false
tools: Read, Grep, Glob
---
Expand Down
2 changes: 1 addition & 1 deletion plugins/fusion/agents/fast-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: fast-worker
description: Claude execution fallback on Sonnet for resolved implementation briefs that require the Claude Code tool surface (hooks, subagent files, MCP), remain inside the Claude privacy boundary, or cannot use an eligible peer lane. It is not the default for spec grade multi file packages or quick scoped fixes, which use the Codex primary lane or an eligible Grok protected role. Use for bounded edits, refactors with a clear recipe, approved plan packages, test fixes, codemods, boilerplate, and docs. Every brief includes a verification command; do not send open ended design work.
model: sonnet
effort: medium
maxTurns: 40
maxTurns: 120
background: false
disallowedTools: Agent
---
Expand Down
2 changes: 1 addition & 1 deletion plugins/fusion/agents/trivial-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: trivial-worker
description: Fallback tier only for exact, low risk, tiny packages when no eligible peer lane is available or Claude-only tools or privacy are required. Cheapest tier worker pinned to true Haiku for trivial single file edits, renames, small doc fixes, log digestion, and short mechanical checks where speed and cost matter more than depth. Change briefs include a verification command; analysis, drafting, and research briefs include explicit acceptance criteria. Anything ambiguous goes to fast-worker instead.
model: claude-haiku-4-5
effort: low
maxTurns: 12
maxTurns: 24
background: false
disallowedTools: Agent
---
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 @@ -63,6 +63,16 @@
"timeout": 10
}
]
},
{
"matcher": "^(Agent|Task)$",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/inline-delegation-guard.mjs\"",
"timeout": 10
}
]
}
],
"PostToolUse": [
Expand Down
1 change: 1 addition & 0 deletions plugins/fusion/rules-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"8294a7d94ed809153bbe005ac02ade391d810a767ec019d1580ad344cff1863d",
"8a1187402340f5d6c9c01ff2eb966d2a1c0420b0879b236f9ec7ff0e8be50ae7",
"8f6e24a12f32b5ad6099b7f9e16ebf751c8d1bc22313848bbd9811dbb059bdc2",
"93e7b5692737fcd82946ab75ee173783c2fbdefd5f3da954d4d676c97101c4f3",
"955aea2e9890bf79c5ebabb499e9ae170c4bfe372d7e35f44e6f3c4096225a83",
"9581481f2f57e522fbfa6c120056289ddcc36c29bd7aade8575308a3045e64c1",
"9b6d1f1e857020604219a1095928748dcad5d25070a2b33a2e9747273370038b",
Expand Down
Loading
Loading