From 4a1f315707ab306dd22a1d6568e954a574502090 Mon Sep 17 00:00:00 2001 From: Daviddev Date: Wed, 15 Jul 2026 18:37:02 +0200 Subject: [PATCH 1/6] fix: normalize Windows fingerprint paths --- scripts/voltflow.mjs | 12 +++++++----- test/voltflow.test.mjs | 30 ++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/scripts/voltflow.mjs b/scripts/voltflow.mjs index fe13cd3..1a873fa 100644 --- a/scripts/voltflow.mjs +++ b/scripts/voltflow.mjs @@ -866,9 +866,10 @@ function compilePaths(value, root) { if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) { throw new Error("fingerprintPaths must be an array of relative file paths"); } + const normalizedRoot = path.resolve(root); for (const entry of value) { - const resolved = path.resolve(root, entry); - if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) { + const resolved = path.resolve(normalizedRoot, entry); + if (resolved !== normalizedRoot && !resolved.startsWith(`${normalizedRoot}${path.sep}`)) { throw new Error("fingerprintPaths entries must stay inside the Git worktree"); } } @@ -876,8 +877,9 @@ function compilePaths(value, root) { } function hashWorkspacePath(hash, root, relativePath, allowMissing = false) { - const resolved = path.resolve(root, relativePath); - if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) return false; + const normalizedRoot = path.resolve(root); + const resolved = path.resolve(normalizedRoot, relativePath); + if (resolved !== normalizedRoot && !resolved.startsWith(`${normalizedRoot}${path.sep}`)) return false; hashField(hash, "path", relativePath); if (!existsSync(resolved)) { if (!allowMissing) return false; @@ -886,7 +888,7 @@ function hashWorkspacePath(hash, root, relativePath, allowMissing = false) { } const stats = lstatSync(resolved); if (stats.isDirectory()) return false; - const object = git(root, ["hash-object", "--no-filters", "--", relativePath]); + const object = git(normalizedRoot, ["hash-object", "--no-filters", "--", relativePath]); if (!object.ok) return false; hashField(hash, "mode", String(stats.mode)); hashField(hash, "object", object.stdout); diff --git a/test/voltflow.test.mjs b/test/voltflow.test.mjs index b50a190..656c9a7 100644 --- a/test/voltflow.test.mjs +++ b/test/voltflow.test.mjs @@ -4,6 +4,7 @@ import { createHash } from "node:crypto"; import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import test from "node:test"; import { @@ -143,7 +144,7 @@ test("prompt injection starts session state and names the exact controller", () fx.options, ); - assert.match(output.hookSpecificOutput.additionalContext, /node ['"]\/plugin\/scripts\/voltflow\.mjs['"]/); + assert.match(output.hookSpecificOutput.additionalContext, /node ['"][^'"]*plugin[\\/]scripts[\\/]voltflow\.mjs['"]/); assert.match(output.hookSpecificOutput.additionalContext, /--session ['"]session-1['"]/); assert.match(output.hookSpecificOutput.additionalContext, /external permission/i); assert.match(output.hookSpecificOutput.additionalContext, /theoretical edge cases are advisory/i); @@ -983,11 +984,19 @@ test("controller state cannot cross repository roots", () => { assert.equal(result.exitCode, 1); }); -test("controller recognizes symlink aliases for the same workspace", () => { +test("controller recognizes symlink aliases for the same workspace", (t) => { const fx = fixture(); const other = fixture(); const alias = `${fx.root}-alias`; - symlinkSync(fx.root, alias, "dir"); + try { + symlinkSync(fx.root, alias, "dir"); + } catch (error) { + if (error?.code === "EPERM" || error?.code === "EACCES" || error?.code === "ENOSYS") { + t.skip(`symlinks unavailable: ${error.code}`); + return; + } + throw error; + } const started = runController( ["start", "--session", "aliased", "--tier", "standard", "--tdd", "exempt", "--review", "single"], { ...fx.options, cwd: alias }, @@ -1020,7 +1029,7 @@ test("quoted override language does not arm deployment", () => { assert.doesNotMatch(output.hookSpecificOutput.additionalContext, /override armed/i); }); -test("fingerprints distinguish large changes and untracked executable modes", () => { +test("fingerprints distinguish large changes and untracked executable modes", (t) => { const root = mkdtempSync(path.join(tmpdir(), "voltflow-fingerprint-")); git(root, "init", "-q"); git(root, "config", "user.email", "qa@example.com"); @@ -1036,6 +1045,11 @@ test("fingerprints distinguish large changes and untracked executable modes", () assert.equal(first, null); assert.equal(second, null); + if (process.platform === "win32") { + t.skip("Windows does not preserve executable mode changes with chmodSync"); + return; + } + const modeRoot = mkdtempSync(path.join(tmpdir(), "voltflow-mode-")); git(modeRoot, "init", "-q"); const script = path.join(modeRoot, "deploy.sh"); @@ -1076,7 +1090,7 @@ test("fingerprints frame untracked path fields unambiguously", () => { writeFileSync(secondPath, "same"); const twoFiles = workspaceFingerprint(root); const mode = String(lstatSync(firstPath).mode); - const object = git(root, "hash-object", "--no-filters", "--", "a"); + const object = git(root, "hash-object", "--no-filters", "--", "a").trim(); unlinkSync(firstPath); unlinkSync(secondPath); writeFileSync(path.join(root, `a${mode}${object}b`), "same"); @@ -1126,10 +1140,10 @@ function git(root, ...args) { function hookProcess(cwd, dataDir, payload) { return new Promise((resolve, reject) => { - const entry = new URL("../scripts/voltflow.mjs", import.meta.url); - const child = spawn(process.execPath, [entry.pathname, "hook"], { + const entryPath = fileURLToPath(new URL("../scripts/voltflow.mjs", import.meta.url)); + const child = spawn(process.execPath, [entryPath, "hook"], { cwd, - env: { ...process.env, PLUGIN_DATA: dataDir, PLUGIN_ROOT: path.dirname(path.dirname(entry.pathname)) }, + env: { ...process.env, PLUGIN_DATA: dataDir, PLUGIN_ROOT: path.dirname(path.dirname(entryPath)) }, stdio: ["pipe", "pipe", "pipe"], }); let stderr = ""; From 3f9cdbd9209477e34cfaadd73beb0bd204a7a1c0 Mon Sep 17 00:00:00 2001 From: Daviddev Date: Wed, 15 Jul 2026 20:10:09 +0200 Subject: [PATCH 2/6] feat: add cost-aware review routing --- README.md | 11 +++- scripts/voltflow.mjs | 73 +++++++++++++++++++++++++-- skills/voltflow/SKILL.md | 2 +- skills/voltflow/references/routing.md | 6 +-- test/voltflow.test.mjs | 67 +++++++++++++++++++++++- 5 files changed, 149 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8d4801b..7c8a68d 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,14 @@ The bootstrap router uses five profiles: | --- | --- | | Discovery, tests, mechanical edits | Luna high | | Standard implementation | Terra max | -| Planning, integration, composite review | Sol medium | -| Routine adversarial or correctness review | Sol high | +| Planning and integration | Sol medium | +| Routine adversarial or correctness review | Terra high | | Security, architecture, migrations, difficult ambiguity | Sol max | Direct `model` and `reasoning_effort` overrides are used when the active spawn schema supports them. Otherwise VoltFlow selects a matching agent profile when possible; if neither route exists, it inherits the parent and reports `routing degraded` instead of pretending the requested profile ran. +Routine review-only agents are cost-controlled: they must explicitly use Terra high. Sol is reserved for a named authorization, private-data, payment, booking, destructive/data-integrity migration, or cross-cutting-architecture risk, or a direct user request. + This table starts from the community [Codex quota frontier analysis](https://www.reddit.com/r/codex/comments/1ut3bnp/the_codex_pareto_frontier_luna_high_terra_max_sol/), checked against the different [Artificial Analysis API-cost frontier](https://artificialanalysis.ai/articles/gpt-5-6-intelligence-vs-cost-across-sol-terra-luna). It is a prior, not a permanent benchmark result. OpenAI's [GPT-5.6 prompt guidance](https://developers.openai.com/api/docs/guides/prompt-guidance-gpt-5p6) supplies the prompt shape: one outcome, explicit constraints, evidence, and a stopping condition, with each rule stated once. ## Multi-agent compatibility @@ -88,6 +90,9 @@ node /scripts/voltflow.mjs validate \ --data-dir --session \ --evidence "npm test" +node /scripts/voltflow.mjs cost \ + --data-dir --session + node /scripts/voltflow.mjs review \ --data-dir --session \ --lane composite @@ -102,6 +107,8 @@ node /scripts/voltflow.mjs gate \ Give the token returned by `review` to that reviewer. Its final line must be `VOLTFLOW_REVIEW: PASS ` or `VOLTFLOW_REVIEW: FAIL `. +`cost` reports the planned routine-review profile, current review-round pressure, and likely usage drivers. It does not report exact tokens because the Codex session telemetry does not expose them to the plugin. + Put the final `gate` command immediately before the provider's deploy command in the same local session or deploy wrapper. Plugin hooks are useful guardrails, but `PreToolUse` cannot intercept every possible side effect. VoltFlow does not yet provide a portable signed receipt for a separate remote CI machine, so do not describe the local state file as an authoritative remote boundary. ## Project-specific deploy surfaces diff --git a/scripts/voltflow.mjs b/scripts/voltflow.mjs index 1a873fa..2e630ce 100644 --- a/scripts/voltflow.mjs +++ b/scripts/voltflow.mjs @@ -50,7 +50,7 @@ const OVERRIDE_INTENT = /^\s*(?:(?:voltflow[:,]?\s+)?(?:please\s+)?(?:override|b const OVERRIDE_QUESTION = /^\s*(?:how|what|when|where|why|who|(?:can|could|should|would|do)\s+(?:i|we)|is\s+it)\b/i; const OVERRIDE_NEGATION = /\b(?:do\s+not|don't|dont|never)\s+(?:\w+\s+){0,2}(?:override|bypass|ignore|overrule|waive|deploy|release|ship)\b/i; const USAGE = [ - "Usage: voltflow.mjs start|skip|red|validate|review|approve|status|gate --session ", + "Usage: voltflow.mjs start|skip|red|validate|review|approve|status|cost|gate --session ", "start: --tier trivial|standard|high --tdd required|exempt --review self|single|split", "skip: --evidence (simple work only, before any change)", "red|validate: --evidence ", @@ -143,6 +143,7 @@ export function runController(argv, options = {}) { validation: null, reviewAssignments: [], reviewPasses: [], + reviewHistory: [], reviewFailure: null, approval: null, override: null, @@ -207,6 +208,8 @@ export function runController(argv, options = {}) { const token = randomUUID(); state.reviewAssignments = state.reviewAssignments.filter((entry) => entry.lane !== flags.lane); state.reviewAssignments.push({ lane: flags.lane, token, fingerprint: currentFingerprint, at: timestamp() }); + state.reviewHistory ??= []; + state.reviewHistory.push({ lane: flags.lane, at: timestamp() }); saveState(dataDir, state); return success(`VoltFlow review assigned: ${flags.lane} token=${token}`); } @@ -226,6 +229,10 @@ export function runController(argv, options = {}) { return success(JSON.stringify(state, null, 2)); } + if (command === "cost") { + return success(JSON.stringify(costReport(state), null, 2)); + } + if (command === "gate") { const current = fingerprint(cwd); const gate = gateStatus(state, current); @@ -235,6 +242,39 @@ export function runController(argv, options = {}) { return failure(USAGE); } +function costReport(state) { + const reviewers = state.reviewMode === "single" ? 1 : state.reviewMode === "split" ? 2 : 0; + const reviewHistory = Array.isArray(state.reviewHistory) ? state.reviewHistory : []; + const likelyDrivers = []; + if (state.reviewAssignments.length > 0) likelyDrivers.push("A pending independent review is active."); + if (reviewHistory.length > reviewers && reviewers > 0) { + likelyDrivers.push("Repeated review assignments are likely increasing context and reasoning usage."); + } + if (state.reviewMode === "split") { + likelyDrivers.push("Split review doubles independent reviewer context by design."); + } + if (likelyDrivers.length === 0) likelyDrivers.push("No independent review activity is currently recorded."); + + return { + exactTokenTelemetry: "unavailable", + forecast: { + routineReview: { + model: "gpt-5.6-terra", + reasoningEffort: "high", + reviewers, + }, + costBand: reviewers === 0 ? "low" : reviewers === 1 ? "medium" : "high", + solEscalation: "Require an explicit high-risk reason or direct user request.", + }, + observed: { + reviewAssignments: state.reviewAssignments.length, + reviewPasses: state.reviewPasses.length, + reviewRounds: reviewHistory.length, + }, + likelyDrivers, + }; +} + export function loadState(dataDir, sessionId) { const file = statePath(dataDir, sessionId); if (!existsSync(file)) return null; @@ -264,6 +304,22 @@ export function isDeployInvocation(toolName, toolInput, cwd) { return false; } +function routineReviewSpawnViolation(toolInput) { + const taskName = typeof toolInput.task_name === "string" ? toolInput.task_name : ""; + const message = typeof toolInput.message === "string" ? toolInput.message : ""; + const scope = `${taskName}\n${message}`; + if (!/(?:\b(?:review|reviewer|adversarial review|correctness-security|validation-scope|composite review)\b|WORK LAYER:\s*review\b)/i.test(scope)) { + return null; + } + if (toolInput.model === "gpt-5.6-sol" && namedSolReviewException(scope)) return null; + if (toolInput.model === "gpt-5.6-terra" && toolInput.reasoning_effort === "high") return null; + return "VoltFlow cost policy requires routine review agents to explicitly use model=gpt-5.6-terra and reasoning_effort=high. Sol review requires SOL_EXCEPTION with a named high-risk boundary or a direct user request."; +} + +function namedSolReviewException(scope) { + return /SOL_EXCEPTION:\s*(?:direct user request|authorization|authentication|private data|private uploads?|payments?|refunds?|booking lifecycle|destructive migration|data[- ]integrity migration|cross[- ]cutting architecture)\b/i.test(scope); +} + export function workspaceFingerprint(cwd) { const rootResult = git(cwd, ["rev-parse", "--show-toplevel"]); if (!rootResult.ok) return null; @@ -339,11 +395,11 @@ function onUserPromptLocked(input, context) { const prefix = `node ${quote(script)} --data-dir ${quote(context.dataDir)} --session ${quote(input.session_id)}`; const configNote = context.configError === null ? "" : ` Config warning: ${context.configError}`; return userContext( - `VoltFlow is active. Before editing, run ${prefix.replace("", "start")} --tier --tdd --review . ` + + `VoltFlow is active. Before editing, run ${prefix.replace("", "start")} --tier --tdd --review . ` + `If the controller cannot access PLUGIN_DATA inside the sandbox, rerun the exact command with external permission; do not relocate the approval state. ` + `For a simple, low-risk edit with no deployment intent, replace start with skip and add --evidence ; skip must happen before any change and does not approve deployment. ` + `For manual evidence, replace start with red or validate and add --evidence ; self review uses approve --self --evidence . ` + - `Before an independent review, replace start with review and add --lane ; give its returned token to the reviewer, whose final receipt must be VOLTFLOW_REVIEW: PASS|FAIL . ` + + `Before an independent review, run ${prefix.replace("", "cost")} for the forecast. Routine review agents must explicitly use gpt-5.6-terra at high reasoning; Sol needs a named high-risk exception or direct user request. Then replace start with review and add --lane ; give its returned token to the reviewer, whose final receipt must be VOLTFLOW_REVIEW: PASS|FAIL . ` + `Completion bar: finish when current evidence shows the result is safe and satisfies the requested scope. Theoretical edge cases are advisory unless they are reproducible in ordinary documented use and break requested behavior, a repository invariant, or a material safety boundary. Do not reopen validated work for speculative improvement.${configNote}`, ); } @@ -372,6 +428,10 @@ function onPreToolUse(input, context) { ) { return deny('VoltFlow requires v2 subagents to use fork_turns: "none".'); } + if (state?.active === true && input.tool_name.endsWith("spawn_agent") && isRecord(input.tool_input)) { + const violation = routineReviewSpawnViolation(input.tool_input); + if (violation !== null) return deny(violation); + } if (isDeployInvocation(input.tool_name, input.tool_input, input.cwd)) { if (state === null) return deny("VoltFlow blocked deployment: no workflow state or review receipt exists."); @@ -699,6 +759,7 @@ function validState(value, sessionId) { && value.reviewAssignments.every(validReviewAssignment) && Array.isArray(value.reviewPasses) && value.reviewPasses.every(validReviewPass) + && (value.reviewHistory === undefined || (Array.isArray(value.reviewHistory) && value.reviewHistory.every(validReviewHistoryEntry))) && (value.reviewFailure === null || (isRecord(value.reviewFailure) && typeof value.reviewFailure.lane === "string")) && (value.approval === null || validApproval(value.approval)) && (value.override === null || validOverride(value.override)) @@ -728,6 +789,12 @@ function validReviewPass(value) { && typeof value.at === "string"; } +function validReviewHistoryEntry(value) { + return isRecord(value) + && typeof value.lane === "string" + && typeof value.at === "string"; +} + function validApproval(value) { return isRecord(value) && typeof value.fingerprint === "string" diff --git a/skills/voltflow/SKILL.md b/skills/voltflow/SKILL.md index f8c880b..5a329d0 100644 --- a/skills/voltflow/SKILL.md +++ b/skills/voltflow/SKILL.md @@ -71,7 +71,7 @@ Before spawning a reviewer, state the supported product boundary and material co When failures share one mechanism, fix and test that mechanism once. Do not keep adding syntax-specific patterns after the boundary is characterized. If complete enforcement would require a shell parser, a general solver, or control over an interception surface the host does not provide, keep the local guard conservative, document the limit, and route authoritative enforcement to the controller or project configuration. -After a failed review, group findings by root cause and rerun only affected coverage. If a rerun finds another variant of the same bounded limitation, stop patching variants and reassess the abstraction or accepted boundary. Finish when the original acceptance criteria have current evidence and no material ordinary-use blocker remains. Use Sol high for routine independent review; raise effort only for a named risk that changes the completion bar. +After a failed review, group findings by root cause and rerun only affected coverage. If a rerun finds another variant of the same bounded limitation, stop patching variants and reassess the abstraction or accepted boundary. Finish when the original acceptance criteria have current evidence and no material ordinary-use blocker remains. Use Terra high for routine independent review. Use Sol only when the review names an authorization, private-data, payment, booking, destructive-migration, data-integrity, or cross-cutting-architecture risk that changes the completion bar, or when the user explicitly requests Sol. ## Gate deployment diff --git a/skills/voltflow/references/routing.md b/skills/voltflow/references/routing.md index 8b16909..02a3d3d 100644 --- a/skills/voltflow/references/routing.md +++ b/skills/voltflow/references/routing.md @@ -6,8 +6,8 @@ Use the smallest profile that clears the task. These profiles seed VoltFlow's ro | --- | --- | | `gpt-5.6-luna`, `high` | Read-heavy discovery, focused test execution, mechanical edits, and simple pattern-following work. | | `gpt-5.6-terra`, `max` | Standard implementation inside established architecture and bounded multi-file changes. | -| `gpt-5.6-sol`, `medium` | Planning, integration, and composite review that need global coherence without max-cost reasoning. | -| `gpt-5.6-sol`, `high` | Routine adversarial review, correctness checks, and bounded security review without a named high-risk boundary. | +| `gpt-5.6-terra`, `high` | Routine independent review, adversarial correctness checks, and bounded security review without a named high-risk boundary. | +| `gpt-5.6-sol`, `medium` | Planning and integration that need global coherence without max-cost reasoning. | | `gpt-5.6-sol`, `max` | High-risk architecture, security, concurrency, migrations, and ambiguous work where a missed constraint is expensive. | ## Routing rules @@ -15,7 +15,7 @@ Use the smallest profile that clears the task. These profiles seed VoltFlow's ro 1. Choose by difficulty, not by role title. A mechanical security-file edit can use Luna; a subtle one-file race can require Sol max. 2. Set `model` and `reasoning_effort` directly when the spawn schema exposes them. Omit `service_tier`. 3. If only `agent_type` is available, select a profile that pins the required model and effort. If neither override surface exists, inherit the parent and state `routing degraded` once in the result. -4. Use Sol medium for the normal planner and composite reviewer, and Sol high for routine independent review lanes. Raise to Sol max only for a named high-risk fact. +4. Use Terra high for routine review-only agents. The controller requires it explicitly, so do not inherit the parent model. Use Sol only with a named high-risk exception (authorization, private data, payments, bookings, destructive/data-integrity migrations, or cross-cutting architecture) or when the user explicitly requests it. 5. Size each wave to the useful independent slices and the host's available concurrency. Do not create slices solely to increase agent count. ## Spawn schema diff --git a/test/voltflow.test.mjs b/test/voltflow.test.mjs index 656c9a7..cce54aa 100644 --- a/test/voltflow.test.mjs +++ b/test/voltflow.test.mjs @@ -149,6 +149,7 @@ test("prompt injection starts session state and names the exact controller", () assert.match(output.hookSpecificOutput.additionalContext, /external permission/i); assert.match(output.hookSpecificOutput.additionalContext, /theoretical edge cases are advisory/i); assert.match(output.hookSpecificOutput.additionalContext, /safe and satisfies the requested scope/i); + assert.match(output.hookSpecificOutput.additionalContext, /cost.*gpt-5\.6-terra.*high/i); assert.equal(loadState(fx.dataDir, "session-1").tier, "unclassified"); }); @@ -485,7 +486,71 @@ test("an unfinished workflow reactivates when the user says continue", () => { test("controller help lists the workflow commands", () => { const result = runController(["--help"]); assert.equal(result.exitCode, 0); - assert.match(result.stdout, /start\|skip\|red\|validate\|review\|approve\|status\|gate/); + assert.match(result.stdout, /start\|skip\|red\|validate\|review\|approve\|status\|cost\|gate/); +}); + +test("cost report plans Terra high reviews and records review-round pressure", () => { + const fx = fixture(); + start(fx, { tier: "standard", tdd: "exempt", review: "single" }); + + const initial = runController(["cost", "--session", "session-1"], { ...fx.options, cwd: "/repo" }); + assert.equal(initial.exitCode, 0, initial.stderr); + const initialReport = JSON.parse(initial.stdout); + assert.equal(initialReport.exactTokenTelemetry, "unavailable"); + assert.deepEqual(initialReport.forecast.routineReview, { + model: "gpt-5.6-terra", + reasoningEffort: "high", + reviewers: 1, + }); + + const validated = runController( + ["validate", "--session", "session-1", "--evidence", "closest relevant check passed"], + { ...fx.options, cwd: "/repo" }, + ); + assert.equal(validated.exitCode, 0, validated.stderr); + const assigned = runController( + ["review", "--session", "session-1", "--lane", "composite"], + { ...fx.options, cwd: "/repo" }, + ); + assert.equal(assigned.exitCode, 0, assigned.stderr); + + const report = JSON.parse(runController(["cost", "--session", "session-1"], { ...fx.options, cwd: "/repo" }).stdout); + assert.equal(report.observed.reviewAssignments, 1); + assert.equal(report.observed.reviewPasses, 0); + assert.equal(report.likelyDrivers[0], "A pending independent review is active."); +}); + +test("routine review agents require Terra high unless a named Sol exception applies", () => { + const fx = fixture(); + handleHook(input("UserPromptSubmit", { prompt: "Review the parser" }), fx.options); + start(fx, { tier: "standard", tdd: "exempt", review: "single" }); + + const reviewSpawn = (model, reasoningEffort, message) => handleHook( + input("PreToolUse", { + tool_name: "agentsspawn_agent", + tool_input: { + task_name: "reviewer", + message, + fork_turns: "none", + model, + reasoning_effort: reasoningEffort, + }, + }), + fx.options, + ); + + const denied = reviewSpawn("gpt-5.6-sol", "high", "WORK LAYER: review"); + assert.equal(denied.hookSpecificOutput.permissionDecision, "deny"); + assert.match(denied.hookSpecificOutput.permissionDecisionReason, /gpt-5\.6-terra.*high/i); + assert.equal(reviewSpawn("gpt-5.6-terra", "high", "WORK LAYER: review"), null); + assert.equal( + reviewSpawn("gpt-5.6-sol", "high", "WORK LAYER: review\nSOL_EXCEPTION: authorization boundary"), + null, + ); + assert.equal( + reviewSpawn("gpt-5.6-sol", "high", "WORK LAYER: review\nSOL_EXCEPTION: direct user request"), + null, + ); }); function productionPatchDecision(fx) { From 2788e018ab6046a9f88b4dc70684055862144b2c Mon Sep 17 00:00:00 2001 From: Daviddev Date: Wed, 15 Jul 2026 20:51:08 +0200 Subject: [PATCH 3/6] feat: add whole-session cost report --- README.md | 7 +- scripts/voltflow.mjs | 195 ++++++++++++++++++++++++++++++++++++++- skills/voltflow/SKILL.md | 6 ++ test/voltflow.test.mjs | 47 +++++++++- 4 files changed, 251 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7c8a68d..959ad7f 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,9 @@ node /scripts/voltflow.mjs validate \ node /scripts/voltflow.mjs cost \ --data-dir --session +node /scripts/voltflow.mjs report \ + --data-dir --session + node /scripts/voltflow.mjs review \ --data-dir --session \ --lane composite @@ -107,7 +110,9 @@ node /scripts/voltflow.mjs gate \ Give the token returned by `review` to that reviewer. Its final line must be `VOLTFLOW_REVIEW: PASS ` or `VOLTFLOW_REVIEW: FAIL `. -`cost` reports the planned routine-review profile, current review-round pressure, and likely usage drivers. It does not report exact tokens because the Codex session telemetry does not expose them to the plugin. +`cost` reports the planned routine-review profile, current review-round pressure, and likely usage drivers. `report` adds whole-session proxies: observed parent model metadata, visible prompt size, tool input/output byte totals grouped by category, requested subagent profiles and handoff size, test/validation activity, and review waves. Run it after discovery, before review, and at finish. + +Neither command reports exact tokens, billing, quota, or hidden reasoning usage because the Codex session telemetry does not expose those values to the plugin. VoltFlow persists counts and byte sizes only; it does not persist prompt, tool, or reviewer content for this report. Put the final `gate` command immediately before the provider's deploy command in the same local session or deploy wrapper. Plugin hooks are useful guardrails, but `PreToolUse` cannot intercept every possible side effect. VoltFlow does not yet provide a portable signed receipt for a separate remote CI machine, so do not describe the local state file as an authoritative remote boundary. diff --git a/scripts/voltflow.mjs b/scripts/voltflow.mjs index 2e630ce..49090c0 100644 --- a/scripts/voltflow.mjs +++ b/scripts/voltflow.mjs @@ -50,7 +50,7 @@ const OVERRIDE_INTENT = /^\s*(?:(?:voltflow[:,]?\s+)?(?:please\s+)?(?:override|b const OVERRIDE_QUESTION = /^\s*(?:how|what|when|where|why|who|(?:can|could|should|would|do)\s+(?:i|we)|is\s+it)\b/i; const OVERRIDE_NEGATION = /\b(?:do\s+not|don't|dont|never)\s+(?:\w+\s+){0,2}(?:override|bypass|ignore|overrule|waive|deploy|release|ship)\b/i; const USAGE = [ - "Usage: voltflow.mjs start|skip|red|validate|review|approve|status|cost|gate --session ", + "Usage: voltflow.mjs start|skip|red|validate|review|approve|status|cost|report|gate --session ", "start: --tier trivial|standard|high --tdd required|exempt --review self|single|split", "skip: --evidence (simple work only, before any change)", "red|validate: --evidence ", @@ -130,6 +130,7 @@ export function runController(argv, options = {}) { saveState(dataDir, state); return success(`VoltFlow updated: ${flags.tier}, tdd=${flags.tdd}, review=${flags.review}`); } + const sessionMetrics = state.active === true ? sessionMetricsFor(state) : freshSessionMetrics(); Object.assign(state, { active: true, tier: flags.tier, @@ -148,6 +149,7 @@ export function runController(argv, options = {}) { approval: null, override: null, skip: null, + sessionMetrics, lastFingerprint: currentFingerprint, stopBlocks: 0, reworkCycles: 0, @@ -196,6 +198,7 @@ export function runController(argv, options = {}) { if (!textFlag(flags.evidence)) return failure("validate requires --evidence"); if (currentFingerprint === null) return failure("Git fingerprint unavailable"); state.validation = evidence(flags.evidence, currentFingerprint); + sessionMetricsFor(state).workflow.validationRecords += 1; saveState(dataDir, state); return success("VoltFlow validation recorded"); } @@ -233,6 +236,10 @@ export function runController(argv, options = {}) { return success(JSON.stringify(costReport(state), null, 2)); } + if (command === "report") { + return success(JSON.stringify(sessionReport(state), null, 2)); + } + if (command === "gate") { const current = fingerprint(cwd); const gate = gateStatus(state, current); @@ -275,6 +282,68 @@ function costReport(state) { }; } +function sessionReport(state) { + const metrics = sessionMetricsFor(state); + const reviewHistory = Array.isArray(state.reviewHistory) ? state.reviewHistory : []; + const likelyDrivers = []; + const recommendedImprovements = []; + const solRequests = Object.entries(metrics.delegation.requestedProfiles) + .filter(([profile]) => profile.startsWith("gpt-5.6-sol:")) + .reduce((total, [, count]) => total + count, 0); + + if (metrics.delegation.spawnRequests > 0) { + likelyDrivers.push(`${metrics.delegation.spawnRequests} delegated agent request(s) added independent context and reasoning work.`); + } + if (state.reviewMode === "split") { + likelyDrivers.push("Split review uses two independent reviewer contexts by design."); + recommendedImprovements.push("Use split review only for high-risk work; use one composite Terra review when the selected tier permits it."); + } + if (reviewHistory.length > 2) { + likelyDrivers.push("Repeated review assignments are likely replaying change context."); + recommendedImprovements.push("Batch related fixes before requesting another review wave."); + } + if (metrics.toolContext.outputBytes > metrics.toolContext.inputBytes * 3 && metrics.toolContext.outputBytes > 50000) { + likelyDrivers.push("Large tool outputs are likely expanding visible working context."); + recommendedImprovements.push("Ask discovery and test commands for focused output, then inspect only the relevant files or failures."); + } + if (metrics.visiblePrompt.bytes > 20000) { + likelyDrivers.push("Long user prompts are contributing substantial visible context."); + recommendedImprovements.push("Move stable workflow rules into repository guidance and keep issue prompts to outcome, boundaries, gates, and stop conditions."); + } + if (solRequests > 0) { + likelyDrivers.push(`${solRequests} requested Sol delegation(s) may be a major usage driver.`); + recommendedImprovements.push("Reserve Sol delegations for named high-risk exceptions; default review and scoped delivery to Terra."); + } + if (likelyDrivers.length === 0) likelyDrivers.push("No material session cost proxy is recorded yet."); + if (recommendedImprovements.length === 0) recommendedImprovements.push("Keep delegation narrow and request this report at discovery, pre-review, and finish checkpoints."); + + return { + exactTokenTelemetry: "unavailable", + limitations: "This report excludes hidden system, reasoning, billing, and quota telemetry. It stores counts and byte sizes only, never prompt, tool, or reviewer content.", + parentModel: { + observed: metrics.parentModels, + note: "Observed from user-prompt hook metadata; absent values are reported as unknown.", + }, + visiblePrompt: metrics.visiblePrompt, + toolContext: metrics.toolContext, + delegation: { + ...metrics.delegation, + requestedProfiles: metrics.delegation.requestedProfiles, + note: "Profiles are requested at spawn time, not verified runtime billing telemetry.", + }, + workflow: { + ...metrics.workflow, + reviewMode: state.reviewMode, + reviewAssignments: state.reviewAssignments.length, + reviewPasses: state.reviewPasses.length, + reviewRounds: reviewHistory.length, + validationRecorded: state.validation !== null, + }, + likelyDrivers, + recommendedImprovements, + }; +} + export function loadState(dataDir, sessionId) { const file = statePath(dataDir, sessionId); if (!existsSync(file)) return null; @@ -385,6 +454,10 @@ function onUserPromptLocked(input, context) { : freshState(input.session_id, input.cwd, previous, current); state.active = true; state.promptHash = createHash("sha256").update(input.prompt).digest("hex"); + const metrics = sessionMetricsFor(state); + metrics.visiblePrompt.submissions += 1; + metrics.visiblePrompt.bytes += byteSize(input.prompt); + increment(metrics.parentModels, typeof input.model === "string" ? input.model : "unknown"); state.stopBlocks = 0; state.reworkCycles = 0; state.valueWarningIssued = false; @@ -399,7 +472,7 @@ function onUserPromptLocked(input, context) { `If the controller cannot access PLUGIN_DATA inside the sandbox, rerun the exact command with external permission; do not relocate the approval state. ` + `For a simple, low-risk edit with no deployment intent, replace start with skip and add --evidence ; skip must happen before any change and does not approve deployment. ` + `For manual evidence, replace start with red or validate and add --evidence ; self review uses approve --self --evidence . ` + - `Before an independent review, run ${prefix.replace("", "cost")} for the forecast. Routine review agents must explicitly use gpt-5.6-terra at high reasoning; Sol needs a named high-risk exception or direct user request. Then replace start with review and add --lane ; give its returned token to the reviewer, whose final receipt must be VOLTFLOW_REVIEW: PASS|FAIL . ` + + `Before an independent review, run ${prefix.replace("", "cost")} for the forecast. Routine review agents must explicitly use gpt-5.6-terra at high reasoning; Sol needs a named high-risk exception or direct user request. Run ${prefix.replace("", "report")} at discovery, pre-review, and finish for whole-session cost proxies. Then replace start with review and add --lane ; give its returned token to the reviewer, whose final receipt must be VOLTFLOW_REVIEW: PASS|FAIL . ` + `Completion bar: finish when current evidence shows the result is safe and satisfies the requested scope. Theoretical edge cases are advisory unless they are reproducible in ordinary documented use and break requested behavior, a repository invariant, or a material safety boundary. Do not reopen validated work for speculative improvement.${configNote}`, ); } @@ -481,6 +554,7 @@ function onPostToolUseLocked(input, context) { const state = loadState(context.dataDir, input.session_id); if (state?.active !== true || !sameWorkspace(state.cwd, input.cwd)) return null; const failed = toolFailed(input.tool_response); + recordToolUsage(state, input, failed); const current = context.fingerprint(input.cwd); const recoveredViolation = recoverRevertedViolation(state, current); @@ -491,6 +565,7 @@ function onPostToolUseLocked(input, context) { const fingerprintChanged = current !== null && state.lastFingerprint !== null && current !== state.lastFingerprint; let valueWarning = null; if ((!failed && editTool) || fingerprintChanged) { + sessionMetricsFor(state).workflow.changeEvents += 1; const paths = editedPaths(input.tool_input); const testOnlyEdit = editTool && paths.length > 0 && paths.every(isTestPath); const touchesTest = (editTool && paths.some(isTestPath)) || (command !== null && commandMentionsTestPath(command)); @@ -523,6 +598,9 @@ function onPostToolUseLocked(input, context) { } if (command !== null && isTestCommand(command)) { + const workflow = sessionMetricsFor(state).workflow; + workflow.testRuns += 1; + if (failed) workflow.failedTestRuns += 1; if (failed && state.tdd === "required" && state.tddViolation !== true) { state.red = evidence(command, current); state.redObserved = state.red; @@ -655,6 +733,7 @@ function freshState(sessionId, cwd, previous, currentFingerprint) { validation: null, reviewAssignments: [], reviewPasses: [], + reviewHistory: [], reviewFailure: null, approval: previous?.approval?.fingerprint === currentFingerprint ? previous.approval : null, override: @@ -665,12 +744,84 @@ function freshState(sessionId, cwd, previous, currentFingerprint) { stopBlocks: 0, reworkCycles: 0, valueWarningIssued: false, + sessionMetrics: validSessionMetrics(previous?.sessionMetrics) ? previous.sessionMetrics : freshSessionMetrics(), lastFingerprint: currentFingerprint, createdAt: timestamp(), updatedAt: timestamp(), }; } +function freshSessionMetrics() { + return { + parentModels: {}, + visiblePrompt: { submissions: 0, bytes: 0 }, + toolContext: { calls: 0, inputBytes: 0, outputBytes: 0, categories: {} }, + delegation: { spawnRequests: 0, handoffBytes: 0, requestedProfiles: {} }, + workflow: { changeEvents: 0, testRuns: 0, failedTestRuns: 0, validationRecords: 0 }, + }; +} + +function sessionMetricsFor(state) { + const metrics = validSessionMetrics(state.sessionMetrics) ? state.sessionMetrics : freshSessionMetrics(); + state.sessionMetrics = metrics; + return metrics; +} + +function recordToolUsage(state, input, failed) { + if (typeof input.tool_name !== "string") return; + const metrics = sessionMetricsFor(state); + const category = toolCategory(input.tool_name, input.tool_input); + const inputBytes = byteSize(input.tool_input); + const outputBytes = byteSize(input.tool_response); + metrics.toolContext.calls += 1; + metrics.toolContext.inputBytes += inputBytes; + metrics.toolContext.outputBytes += outputBytes; + const bucket = metrics.toolContext.categories[category] ?? { calls: 0, inputBytes: 0, outputBytes: 0, failures: 0 }; + bucket.calls += 1; + bucket.inputBytes += inputBytes; + bucket.outputBytes += outputBytes; + if (failed) bucket.failures += 1; + metrics.toolContext.categories[category] = bucket; + + if (category !== "delegation" || failed || !isRecord(input.tool_input)) return; + metrics.delegation.spawnRequests += 1; + metrics.delegation.handoffBytes += byteSize(input.tool_input.task_name) + byteSize(input.tool_input.message); + const model = typeof input.tool_input.model === "string" ? input.tool_input.model : "inherited-or-unknown"; + const reasoningEffort = typeof input.tool_input.reasoning_effort === "string" + ? input.tool_input.reasoning_effort + : typeof input.tool_input.reasoningEffort === "string" + ? input.tool_input.reasoningEffort + : "inherited-or-unknown"; + increment(metrics.delegation.requestedProfiles, `${model}:${reasoningEffort}`); +} + +function toolCategory(toolName, toolInput) { + const name = toolName.toLowerCase(); + const command = isCommandTool(toolName) ? commandFrom(toolInput) : null; + if (name.endsWith("spawn_agent")) return "delegation"; + if (name.includes("codebase_memory") || name.includes("search_graph") || name.includes("trace_path") || name.includes("get_code_snippet")) return "discovery"; + if (name.includes("github") || name.startsWith("gh_")) return "github"; + if (name.includes("browser") || name.includes("playwright") || name.includes("chrome")) return "browser"; + if (command !== null && isTestCommand(command)) return "test"; + if (isEditTool(toolName)) return "edit"; + if (isCommandTool(toolName)) return "command"; + return "other"; +} + +function byteSize(value) { + if (value === undefined) return 0; + if (typeof value === "string") return Buffer.byteLength(value, "utf8"); + try { + return Buffer.byteLength(JSON.stringify(value), "utf8"); + } catch { + return 0; + } +} + +function increment(record, key) { + record[key] = (record[key] ?? 0) + 1; +} + function gateStatus(state, currentFingerprint) { if (currentFingerprint === null) return { allowed: false, reason: "Git fingerprint unavailable" }; if (state.approval?.fingerprint === currentFingerprint) return { allowed: true, source: "review receipt" }; @@ -760,6 +911,7 @@ function validState(value, sessionId) { && Array.isArray(value.reviewPasses) && value.reviewPasses.every(validReviewPass) && (value.reviewHistory === undefined || (Array.isArray(value.reviewHistory) && value.reviewHistory.every(validReviewHistoryEntry))) + && (value.sessionMetrics === undefined || validSessionMetrics(value.sessionMetrics)) && (value.reviewFailure === null || (isRecord(value.reviewFailure) && typeof value.reviewFailure.lane === "string")) && (value.approval === null || validApproval(value.approval)) && (value.override === null || validOverride(value.override)) @@ -789,6 +941,45 @@ function validReviewPass(value) { && typeof value.at === "string"; } +function validSessionMetrics(value) { + return isRecord(value) + && isCountRecord(value.parentModels) + && isRecord(value.visiblePrompt) + && validCount(value.visiblePrompt.submissions) + && validCount(value.visiblePrompt.bytes) + && isRecord(value.toolContext) + && validCount(value.toolContext.calls) + && validCount(value.toolContext.inputBytes) + && validCount(value.toolContext.outputBytes) + && isRecord(value.toolContext.categories) + && Object.values(value.toolContext.categories).every(validToolCategoryMetrics) + && isRecord(value.delegation) + && validCount(value.delegation.spawnRequests) + && validCount(value.delegation.handoffBytes) + && isCountRecord(value.delegation.requestedProfiles) + && isRecord(value.workflow) + && validCount(value.workflow.changeEvents) + && validCount(value.workflow.testRuns) + && validCount(value.workflow.failedTestRuns) + && validCount(value.workflow.validationRecords); +} + +function validToolCategoryMetrics(value) { + return isRecord(value) + && validCount(value.calls) + && validCount(value.inputBytes) + && validCount(value.outputBytes) + && validCount(value.failures); +} + +function isCountRecord(value) { + return isRecord(value) && Object.values(value).every(validCount); +} + +function validCount(value) { + return Number.isSafeInteger(value) && value >= 0; +} + function validReviewHistoryEntry(value) { return isRecord(value) && typeof value.lane === "string" diff --git a/skills/voltflow/SKILL.md b/skills/voltflow/SKILL.md index 5a329d0..bdcb308 100644 --- a/skills/voltflow/SKILL.md +++ b/skills/voltflow/SKILL.md @@ -55,6 +55,12 @@ Each spawn prompt contains five labeled fields and nothing repetitive: Treat examples as examples, not hidden requirements. When instructions conflict, follow the user outcome, repository invariants, and the bounded assignment in that order. Return a concise result with evidence; do not invent neighboring work to make the assignment look complete. +## Monitor whole-session cost honestly + +Run the injected `report` command after discovery, before independent review, and at finish. It is a proxy ledger, not a token meter: it records visible prompt bytes, tool input/output byte totals by category, requested agent profiles and handoff bytes, validation activity, and review waves. It never stores prompt, tool, or reviewer content, and it cannot see hidden reasoning, billing, quota, or actual provider token usage. + +Use the report to remove avoidable repetition: narrow discovery output, keep delegations to one bounded outcome, batch related fixes before a review wave, and use split review only for high-risk work. Do not claim that requested subagent profiles are verified runtime billing telemetry. + ## Review in proportion to risk Review the final diff against the request, not against an imagined ideal rewrite. Cover correctness, regression risk, relevant security boundaries, validation quality, and unnecessary scope. diff --git a/test/voltflow.test.mjs b/test/voltflow.test.mjs index cce54aa..0c690d2 100644 --- a/test/voltflow.test.mjs +++ b/test/voltflow.test.mjs @@ -486,7 +486,7 @@ test("an unfinished workflow reactivates when the user says continue", () => { test("controller help lists the workflow commands", () => { const result = runController(["--help"]); assert.equal(result.exitCode, 0); - assert.match(result.stdout, /start\|skip\|red\|validate\|review\|approve\|status\|cost\|gate/); + assert.match(result.stdout, /start\|skip\|red\|validate\|review\|approve\|status\|cost\|report\|gate/); }); test("cost report plans Terra high reviews and records review-round pressure", () => { @@ -520,6 +520,51 @@ test("cost report plans Terra high reviews and records review-round pressure", ( assert.equal(report.likelyDrivers[0], "A pending independent review is active."); }); +test("session report tracks metadata-only whole-session cost proxies", () => { + const fx = fixture(); + const secret = "do-not-persist-this-content"; + handleHook( + input("UserPromptSubmit", { prompt: `Investigate ${secret}`, model: "gpt-5.6-terra" }), + fx.options, + ); + start(fx, { tier: "standard", tdd: "exempt", review: "single" }); + + handleHook( + input("PostToolUse", { + tool_name: "mcp__codebase_memory_mcp__search_graph", + tool_input: { query: "Asset" }, + tool_response: { output: "matched symbol" }, + }), + fx.options, + ); + handleHook( + input("PostToolUse", { + tool_name: "agentsspawn_agent", + tool_input: { + task_name: "reviewer", + message: `WORK LAYER: review ${secret}`, + fork_turns: "none", + model: "gpt-5.6-terra", + reasoning_effort: "high", + }, + tool_response: { agent_id: "agent-1" }, + }), + fx.options, + ); + + const result = runController(["report", "--session", "session-1"], { ...fx.options, cwd: "/repo" }); + assert.equal(result.exitCode, 0, result.stderr); + const report = JSON.parse(result.stdout); + assert.equal(report.exactTokenTelemetry, "unavailable"); + assert.equal(report.parentModel.observed["gpt-5.6-terra"], 1); + assert.ok(report.visiblePrompt.bytes > 0); + assert.equal(report.toolContext.categories.discovery.calls, 1); + assert.equal(report.delegation.requestedProfiles["gpt-5.6-terra:high"], 1); + assert.equal(report.delegation.handoffBytes > 0, true); + assert.equal(JSON.stringify(report).includes(secret), false); + assert.equal(JSON.stringify(loadState(fx.dataDir, "session-1")).includes(secret), false); +}); + test("routine review agents require Terra high unless a named Sol exception applies", () => { const fx = fixture(); handleHook(input("UserPromptSubmit", { prompt: "Review the parser" }), fx.options); From 69b33070ad6e31bc7187d20c720941e111f8ce27 Mon Sep 17 00:00:00 2001 From: Daviddev Date: Wed, 15 Jul 2026 20:57:33 +0200 Subject: [PATCH 4/6] chore: record marketplace install metadata --- .codex-marketplace-install.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .codex-marketplace-install.json diff --git a/.codex-marketplace-install.json b/.codex-marketplace-install.json new file mode 100644 index 0000000..f0db2f9 --- /dev/null +++ b/.codex-marketplace-install.json @@ -0,0 +1,7 @@ +{ + "source_type": "git", + "source": "https://github.com/Team-Volt/voltflow.git", + "ref_name": null, + "sparse_paths": [], + "revision": "f6ac8e442b0897af3e4de3c2598eaf22107b112b" +} \ No newline at end of file From dc4022984380496a85ea6845e86ebb033c3a1a80 Mon Sep 17 00:00:00 2001 From: Daviddev Date: Thu, 16 Jul 2026 11:44:41 +0200 Subject: [PATCH 5/6] test: retain report metrics across completed turns --- test/voltflow.test.mjs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/voltflow.test.mjs b/test/voltflow.test.mjs index 0c690d2..93f4bfb 100644 --- a/test/voltflow.test.mjs +++ b/test/voltflow.test.mjs @@ -565,6 +565,20 @@ test("session report tracks metadata-only whole-session cost proxies", () => { assert.equal(JSON.stringify(loadState(fx.dataDir, "session-1")).includes(secret), false); }); +test("session report retains proxy metrics across completed turns", () => { + const fx = fixture(); + handleHook(input("UserPromptSubmit", { prompt: "first turn", model: "gpt-5.6-terra" }), fx.options); + start(fx, { tier: "trivial", tdd: "exempt", review: "self" }); + handleHook(input("Stop", { last_assistant_message: "Done" }), fx.options); + + handleHook(input("UserPromptSubmit", { prompt: "second turn", model: "gpt-5.6-terra" }), fx.options); + const result = runController(["report", "--session", "session-1"], { ...fx.options, cwd: "/repo" }); + assert.equal(result.exitCode, 0, result.stderr); + const report = JSON.parse(result.stdout); + assert.equal(report.visiblePrompt.submissions, 2); + assert.equal(report.parentModel.observed["gpt-5.6-terra"], 2); +}); + test("routine review agents require Terra high unless a named Sol exception applies", () => { const fx = fixture(); handleHook(input("UserPromptSubmit", { prompt: "Review the parser" }), fx.options); From ce177613a997a11797a63577aa4703c86c3bc34f Mon Sep 17 00:00:00 2001 From: Daviddev Date: Thu, 16 Jul 2026 12:06:03 +0200 Subject: [PATCH 6/6] fix: bind review receipts to spawned agents --- README.md | 2 +- hooks/hooks.json | 2 +- scripts/voltflow.mjs | 61 ++++++++++++++--- skills/voltflow/SKILL.md | 2 +- test/voltflow.test.mjs | 140 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 187 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 959ad7f..68e6ba2 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ node /scripts/voltflow.mjs gate \ --data-dir --session ``` -Give the token returned by `review` to that reviewer. Its final line must be `VOLTFLOW_REVIEW: PASS ` or `VOLTFLOW_REVIEW: FAIL `. +Give the token returned by `review` to that reviewer and include `VOLTFLOW_REVIEW_ASSIGNMENT: ` in that spawn prompt. VoltFlow binds the successful spawn's agent ID, requested model, and reasoning effort to the assignment; only that agent's matching final line is accepted: `VOLTFLOW_REVIEW: PASS ` or `VOLTFLOW_REVIEW: FAIL `. `cost` reports the planned routine-review profile, current review-round pressure, and likely usage drivers. `report` adds whole-session proxies: observed parent model metadata, visible prompt size, tool input/output byte totals grouped by category, requested subagent profiles and handoff size, test/validation activity, and review waves. Run it after discovery, before review, and at finish. diff --git a/hooks/hooks.json b/hooks/hooks.json index 69f7780..93379d1 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -29,7 +29,7 @@ ], "PostToolUse": [ { - "matcher": "^(Bash|exec|exec_command|apply_patch|Edit|Write)$", + "matcher": "*", "hooks": [ { "type": "command", diff --git a/scripts/voltflow.mjs b/scripts/voltflow.mjs index 49090c0..68dfb3c 100644 --- a/scripts/voltflow.mjs +++ b/scripts/voltflow.mjs @@ -214,7 +214,7 @@ export function runController(argv, options = {}) { state.reviewHistory ??= []; state.reviewHistory.push({ lane: flags.lane, at: timestamp() }); saveState(dataDir, state); - return success(`VoltFlow review assigned: ${flags.lane} token=${token}`); + return success(`VoltFlow review assigned: ${flags.lane} token=${token}\nInclude VOLTFLOW_REVIEW_ASSIGNMENT: ${flags.lane} ${token} in the reviewer spawn prompt.`); } if (command === "approve") { @@ -373,18 +373,44 @@ export function isDeployInvocation(toolName, toolInput, cwd) { return false; } -function routineReviewSpawnViolation(toolInput) { - const taskName = typeof toolInput.task_name === "string" ? toolInput.task_name : ""; +function reviewAssignmentReference(toolInput) { const message = typeof toolInput.message === "string" ? toolInput.message : ""; - const scope = `${taskName}\n${message}`; - if (!/(?:\b(?:review|reviewer|adversarial review|correctness-security|validation-scope|composite review)\b|WORK LAYER:\s*review\b)/i.test(scope)) { - return null; - } + const match = /(?:^|\n)\s*VOLTFLOW_REVIEW_ASSIGNMENT:\s*([a-z0-9][a-z0-9_-]*)\s+([a-f0-9-]+)\s*$/im.exec(message); + return match === null ? null : { lane: match[1], token: match[2] }; +} + +function boundReviewSpawnViolation(state, toolInput, reference, currentFingerprint) { + const assignment = state.reviewAssignments.find( + (entry) => entry.lane === reference.lane && entry.token === reference.token && entry.fingerprint === currentFingerprint, + ); + if (assignment === undefined) return "VoltFlow rejected reviewer spawn: assignment is missing, stale, or belongs to a different lane."; + if (assignment.spawn !== undefined) return "VoltFlow rejected reviewer spawn: this assignment is already bound to another agent."; + const scope = typeof toolInput.message === "string" ? toolInput.message : ""; if (toolInput.model === "gpt-5.6-sol" && namedSolReviewException(scope)) return null; if (toolInput.model === "gpt-5.6-terra" && toolInput.reasoning_effort === "high") return null; return "VoltFlow cost policy requires routine review agents to explicitly use model=gpt-5.6-terra and reasoning_effort=high. Sol review requires SOL_EXCEPTION with a named high-risk boundary or a direct user request."; } +function bindReviewSpawn(state, input, currentFingerprint) { + if (!isRecord(input.tool_input)) return null; + const reference = reviewAssignmentReference(input.tool_input); + if (reference === null) return null; + const assignment = state.reviewAssignments.find( + (entry) => entry.lane === reference.lane && entry.token === reference.token && entry.fingerprint === currentFingerprint, + ); + if (assignment === undefined || assignment.spawn !== undefined) return null; + const response = isRecord(input.tool_response) ? input.tool_response : {}; + const agentId = typeof response.agent_id === "string" ? response.agent_id : typeof response.agentId === "string" ? response.agentId : null; + if (agentId === null) return { systemMessage: "VoltFlow could not bind the reviewer: the spawn response did not include an agent id." }; + assignment.spawn = { + agentId, + model: typeof input.tool_input.model === "string" ? input.tool_input.model : "inherited-or-unknown", + reasoningEffort: typeof input.tool_input.reasoning_effort === "string" ? input.tool_input.reasoning_effort : "inherited-or-unknown", + at: timestamp(), + }; + return null; +} + function namedSolReviewException(scope) { return /SOL_EXCEPTION:\s*(?:direct user request|authorization|authentication|private data|private uploads?|payments?|refunds?|booking lifecycle|destructive migration|data[- ]integrity migration|cross[- ]cutting architecture)\b/i.test(scope); } @@ -502,7 +528,8 @@ function onPreToolUse(input, context) { return deny('VoltFlow requires v2 subagents to use fork_turns: "none".'); } if (state?.active === true && input.tool_name.endsWith("spawn_agent") && isRecord(input.tool_input)) { - const violation = routineReviewSpawnViolation(input.tool_input); + const reference = reviewAssignmentReference(input.tool_input); + const violation = reference === null ? null : boundReviewSpawnViolation(state, input.tool_input, reference, context.fingerprint(input.cwd)); if (violation !== null) return deny(violation); } @@ -556,6 +583,7 @@ function onPostToolUseLocked(input, context) { const failed = toolFailed(input.tool_response); recordToolUsage(state, input, failed); const current = context.fingerprint(input.cwd); + const spawnBindingWarning = failed ? null : bindReviewSpawn(state, input, current); const recoveredViolation = recoverRevertedViolation(state, current); const editTool = typeof input.tool_name === "string" && isEditTool(input.tool_name); @@ -610,7 +638,7 @@ function onPostToolUseLocked(input, context) { if (current !== null) state.lastFingerprint = current; state.updatedAt = timestamp(); saveState(context.dataDir, state); - return valueWarning; + return valueWarning ?? spawnBindingWarning; } function onSubagentStart() { @@ -646,6 +674,11 @@ function onSubagentStopLocked(input, context) { return { systemMessage: "VoltFlow ignored an unassigned or stale review receipt." }; } + const agentId = typeof input.agent_id === "string" ? input.agent_id : "unknown"; + if (assignment.spawn === undefined || assignment.spawn.agentId !== agentId) { + return { systemMessage: "VoltFlow ignored review receipt: it must come from the bound reviewer agent." }; + } + if (outcome === "FAIL") { state.reviewFailure = { lane, at: timestamp() }; state.reviewPasses = []; @@ -659,7 +692,6 @@ function onSubagentStopLocked(input, context) { if (blocker !== null) return { systemMessage: `VoltFlow ignored review pass: ${blocker}` }; if (current === null) return { systemMessage: "VoltFlow ignored review pass: Git fingerprint unavailable." }; - const agentId = typeof input.agent_id === "string" ? input.agent_id : "unknown"; state.reviewPasses = state.reviewPasses.filter((entry) => entry.fingerprint === current); if (!state.reviewPasses.some((entry) => entry.agentId === agentId)) { state.reviewPasses.push({ agentId, lane, fingerprint: current, at: timestamp() }); @@ -930,6 +962,15 @@ function validReviewAssignment(value) { && typeof value.lane === "string" && typeof value.token === "string" && typeof value.fingerprint === "string" + && typeof value.at === "string" + && (value.spawn === undefined || validReviewSpawn(value.spawn)); +} + +function validReviewSpawn(value) { + return isRecord(value) + && typeof value.agentId === "string" + && typeof value.model === "string" + && typeof value.reasoningEffort === "string" && typeof value.at === "string"; } diff --git a/skills/voltflow/SKILL.md b/skills/voltflow/SKILL.md index bdcb308..ff5500e 100644 --- a/skills/voltflow/SKILL.md +++ b/skills/voltflow/SKILL.md @@ -69,7 +69,7 @@ Review the final diff against the request, not against an imagined ideal rewrite - `single`: one independent reviewer covers the full checklist. - `split`: two reviewers use non-overlapping lanes: correctness/security and validation/scope. -Before spawning each independent reviewer, run the injected controller with `review --lane `. Put the returned token in the review prompt. The reviewer ends with `VOLTFLOW_REVIEW: PASS ` only when no blocking finding remains, or `VOLTFLOW_REVIEW: FAIL ` after its findings. Receipts without a current assignment are ignored, a failed lane clears earlier passes, and any edit invalidates every receipt. +Before spawning each independent reviewer, run the injected controller with `review --lane `. Put `VOLTFLOW_REVIEW_ASSIGNMENT: ` in the spawn prompt. VoltFlow binds the successful spawn's agent ID, requested model, and reasoning effort to that assignment. The bound reviewer ends with `VOLTFLOW_REVIEW: PASS ` only when no blocking finding remains, or `VOLTFLOW_REVIEW: FAIL ` after its findings. Receipts without a current assignment or bound agent are ignored, a failed lane clears earlier passes, and any edit invalidates every receipt. ### Bound review exploration diff --git a/test/voltflow.test.mjs b/test/voltflow.test.mjs index 93f4bfb..8dceb86 100644 --- a/test/voltflow.test.mjs +++ b/test/voltflow.test.mjs @@ -116,6 +116,22 @@ function review(fx, agentId, lane) { assert.equal(assigned.exitCode, 0, assigned.stderr); const token = /token=(\S+)/.exec(assigned.stdout)?.[1]; assert.ok(token); + const toolInput = { + task_name: "checker", + message: `VOLTFLOW_REVIEW_ASSIGNMENT: ${lane} ${token}`, + fork_turns: "none", + model: "gpt-5.6-terra", + reasoning_effort: "high", + }; + assert.equal(handleHook(input("PreToolUse", { tool_name: "agentsspawn_agent", tool_input: toolInput }), fx.options), null); + handleHook( + input("PostToolUse", { + tool_name: "agentsspawn_agent", + tool_input: toolInput, + tool_response: { agent_id: agentId }, + }), + fx.options, + ); return handleHook( input("SubagentStop", { agent_id: agentId, @@ -584,11 +600,24 @@ test("routine review agents require Terra high unless a named Sol exception appl handleHook(input("UserPromptSubmit", { prompt: "Review the parser" }), fx.options); start(fx, { tier: "standard", tdd: "exempt", review: "single" }); + const validated = runController( + ["validate", "--session", "session-1", "--evidence", "closest relevant check passed"], + { ...fx.options, cwd: "/repo" }, + ); + assert.equal(validated.exitCode, 0, validated.stderr); + const assigned = runController( + ["review", "--session", "session-1", "--lane", "composite"], + { ...fx.options, cwd: "/repo" }, + ); + assert.equal(assigned.exitCode, 0, assigned.stderr); + const token = /token=(\S+)/.exec(assigned.stdout)?.[1]; + assert.ok(token); + const reviewSpawn = (model, reasoningEffort, message) => handleHook( input("PreToolUse", { tool_name: "agentsspawn_agent", tool_input: { - task_name: "reviewer", + task_name: "checker", message, fork_turns: "none", model, @@ -598,18 +627,73 @@ test("routine review agents require Terra high unless a named Sol exception appl fx.options, ); - const denied = reviewSpawn("gpt-5.6-sol", "high", "WORK LAYER: review"); + const assignment = `VOLTFLOW_REVIEW_ASSIGNMENT: composite ${token}`; + const denied = reviewSpawn("gpt-5.6-sol", "high", assignment); assert.equal(denied.hookSpecificOutput.permissionDecision, "deny"); assert.match(denied.hookSpecificOutput.permissionDecisionReason, /gpt-5\.6-terra.*high/i); - assert.equal(reviewSpawn("gpt-5.6-terra", "high", "WORK LAYER: review"), null); - assert.equal( - reviewSpawn("gpt-5.6-sol", "high", "WORK LAYER: review\nSOL_EXCEPTION: authorization boundary"), - null, + assert.equal(reviewSpawn("gpt-5.6-terra", "high", assignment), null); +}); + +test("review receipts require the agent and profile bound to an explicit assignment", () => { + const fx = fixture(); + handleHook(input("UserPromptSubmit", { prompt: "Review the parser" }), fx.options); + start(fx, { tier: "standard", tdd: "exempt", review: "single" }); + const validated = runController( + ["validate", "--session", "session-1", "--evidence", "closest relevant check passed"], + { ...fx.options, cwd: "/repo" }, + ); + assert.equal(validated.exitCode, 0, validated.stderr); + const assigned = runController( + ["review", "--session", "session-1", "--lane", "composite"], + { ...fx.options, cwd: "/repo" }, + ); + const token = /token=(\S+)/.exec(assigned.stdout)?.[1]; + assert.ok(token); + const toolInput = { + task_name: "inspection", + message: `VOLTFLOW_REVIEW_ASSIGNMENT: composite ${token}`, + fork_turns: "none", + model: "gpt-5.6-terra", + reasoning_effort: "high", + }; + assert.equal(handleHook(input("PreToolUse", { tool_name: "agentsspawn_agent", tool_input: toolInput }), fx.options), null); + handleHook( + input("PostToolUse", { tool_name: "agentsspawn_agent", tool_input: toolInput, tool_response: { agent_id: "bound-agent" } }), + fx.options, ); + assert.deepEqual(loadState(fx.dataDir, "session-1").reviewAssignments[0].spawn, { + agentId: "bound-agent", + model: "gpt-5.6-terra", + reasoningEffort: "high", + at: loadState(fx.dataDir, "session-1").reviewAssignments[0].spawn.at, + }); + + const wrongAgent = handleHook( + input("SubagentStop", { + agent_id: "other-agent", + last_assistant_message: `VOLTFLOW_REVIEW: PASS composite ${token}`, + }), + fx.options, + ); + assert.match(wrongAgent.systemMessage, /bound reviewer agent/i); + assert.equal(loadState(fx.dataDir, "session-1").approval, null); + assert.equal( - reviewSpawn("gpt-5.6-sol", "high", "WORK LAYER: review\nSOL_EXCEPTION: direct user request"), + handleHook( + input("SubagentStop", { + agent_id: "bound-agent", + last_assistant_message: `VOLTFLOW_REVIEW: PASS composite ${token}`, + }), + fx.options, + ), null, ); + assert.equal(loadState(fx.dataDir, "session-1").approval.source, "subagent"); +}); + +test("installed PostToolUse hook captures every tool category", () => { + const hooks = JSON.parse(readFileSync(new URL("../hooks/hooks.json", import.meta.url), "utf8")); + assert.equal(hooks.hooks.PostToolUse[0].matcher, "*"); }); function productionPatchDecision(fx) { @@ -765,6 +849,29 @@ test("review receipts require a fingerprint-bound assignment token", () => { assert.equal(assigned.exitCode, 0, assigned.stderr); const token = /token=(\S+)/.exec(assigned.stdout)?.[1]; assert.ok(token); + const reviewToolInput = { + task_name: "checker", + message: `VOLTFLOW_REVIEW_ASSIGNMENT: correctness-security ${token}`, + fork_turns: "none", + model: "gpt-5.6-terra", + reasoning_effort: "high", + }; + handleHook( + input("SubagentStop", { + agent_id: "reviewer-1", + last_assistant_message: `VOLTFLOW_REVIEW: PASS correctness-security ${token}`, + }), + fx.options, + ); + assert.equal(loadState(fx.dataDir, "session-1").reviewPasses.length, 0); + handleHook( + input("PostToolUse", { + tool_name: "agentsspawn_agent", + tool_input: reviewToolInput, + tool_response: { agent_id: "reviewer-1" }, + }), + fx.options, + ); handleHook( input("SubagentStop", { agent_id: "reviewer-1", @@ -845,6 +952,25 @@ test("concurrent split reviews retain both receipts", async () => { const validationToken = /token=(\S+)/.exec(validation.stdout)?.[1]; assert.ok(correctnessToken, correctness.stderr); assert.ok(validationToken, validation.stderr); + for (const [lane, token, agentId] of [ + ["correctness-security", correctnessToken, "reviewer-a"], + ["validation-scope", validationToken, "reviewer-b"], + ]) { + handleHook({ + hook_event_name: "PostToolUse", + session_id: "parallel", + cwd: root, + tool_name: "agentsspawn_agent", + tool_input: { + task_name: "checker", + message: `VOLTFLOW_REVIEW_ASSIGNMENT: ${lane} ${token}`, + fork_turns: "none", + model: "gpt-5.6-terra", + reasoning_effort: "high", + }, + tool_response: { agent_id: agentId }, + }, { dataDir }); + } await Promise.all([ hookProcess(root, dataDir, { hook_event_name: "SubagentStop",