From 6f74e82b69a2bb135b299e9494c6f317395d5745 Mon Sep 17 00:00:00 2001 From: Daniel Ochoa Date: Thu, 15 Oct 2026 21:13:22 -0500 Subject: [PATCH] feat(vibe): Add cl-execute plan-review prompt and cost scaling gate - Ship the plan-review prompt as references/plan-review-prompt.md and pass it verbatim to the single plan reviewer, with a private review path and Claude Code and Codex worker forms. - Carry every reviewer HUMAN DECISIONS item into the numbered decisions returned with MANUAL_INTERVENTION_REQUIRED. - Treat a mechanism a ticket names as a proposal; a dominated one goes to the human as one decision instead of being built or swapped. - Add a Decision provenance readiness item and a Discovery Routes row for existing channels between surfaces. - Require a Cost Scaling table for periodic or cross-surface work and enforce it in check-plan.mjs, with a --no-periodic waiver. - Release vibe 1.0.46. Testing: node --test over every plugin test file (392 pass), vibe pytest (235 pass), check-plan.mjs run against sample plans. Risks: The keyword trigger can fire on plans that only mention a socket or feed; --no-periodic waives it on the record. --- CHANGELOG.md | 12 ++ plugins/vibe/.claude-plugin/plugin.json | 2 +- plugins/vibe/.codex-plugin/plugin.json | 2 +- plugins/vibe/skills/cl-execute/SKILL.md | 7 +- .../references/plan-review-prompt.md | 130 ++++++++++++++++++ .../references/planning-and-gates.md | 86 ++++++++++-- .../skills/cl-execute/scripts/check-plan.mjs | 76 +++++++++- .../cl-execute/scripts/check-plan.test.mjs | 42 +++++- 8 files changed, 333 insertions(+), 24 deletions(-) create mode 100644 plugins/vibe/skills/cl-execute/references/plan-review-prompt.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb59fa0..756f3597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to the claude-plugins project will be documented in this fil The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Entries are listed newest-first; each plugin section is treated as released when merged to `main`. +### vibe v1.0.46 + +#### Added +- `cl-execute` ships its plan-review prompt as `references/plan-review-prompt.md`. Planning step 20 gives the single independent plan reviewer that prompt verbatim, followed by the plan path, ticket, PRD slugs, and a private path for the review file. The prompt checks scope exclusions and mechanisms equally hard, scores cost by what it scales with at 10x and 100x plus the per-device idle term, requires decision provenance, and ends with numbered HUMAN DECISIONS. Its worker section gives both the Claude Code and the Codex (`spawn_agent`, `send_input`) forms. +- Plans that introduce periodic or cross-surface work carry a `### Cost Scaling` table (Option, Cost scales with, Per-device idle cost, At 10x / 100x, Existing mechanism reused) with at least two rows, one naming an existing mechanism. `scripts/check-plan.mjs` requires it when prose outside code fences names polling, intervals, cron, heartbeats, background lanes, sockets, subscriptions, feeds, or watchers, and `--no-periodic` waives it on the record. +- A readiness item, Decision provenance: each recorded decision needs a traceable human statement, a newer ticket supersedes an earlier ruling only by addressing it, and conflicting records go to the human instead of being settled by recency. +- A Discovery Routes row for existing channels between two surfaces (socket emit and subscribe calls, signal emitters, command dispatchers, interval lanes). + +#### Changed +- Planning step 13 treats only the outcome a ticket asks for as binding. A mechanism the ticket names is held to the same test as one the plan invents; a dominated one keeps the outcome and goes to the human as one numbered decision, and raising it is stated to be neither instruction inflation nor manufactured ambiguity. +- Planning step 20: a finding the plan reviewer lists under HUMAN DECISIONS is never resolved, downgraded, or absorbed by the worker; each becomes a numbered decision returned with `MANUAL_INTERVENTION_REQUIRED`, with the worker's view beside it. + ### vibe v1.0.45 #### Fixed diff --git a/plugins/vibe/.claude-plugin/plugin.json b/plugins/vibe/.claude-plugin/plugin.json index 26718d7a..672fbd8e 100644 --- a/plugins/vibe/.claude-plugin/plugin.json +++ b/plugins/vibe/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "vibe", "description": "Vibe sessions, handoff, seed refresh, and ClosedLoop ticket workflows", - "version": "1.0.45", + "version": "1.0.46", "dependencies": ["closedloop-core"], "author": {"name": "ClosedLoop", "email": "support@closedloop.ai"} } diff --git a/plugins/vibe/.codex-plugin/plugin.json b/plugins/vibe/.codex-plugin/plugin.json index 4f7ed42f..9133dda1 100644 --- a/plugins/vibe/.codex-plugin/plugin.json +++ b/plugins/vibe/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vibe", - "version": "1.0.45", + "version": "1.0.46", "description": "Vibe sessions, handoff, seed refresh, and ClosedLoop ticket workflows", "author": {"name": "ClosedLoop", "email": "support@closedloop.ai"}, "homepage": "https://github.com/closedloop-ai/claude-plugins", diff --git a/plugins/vibe/skills/cl-execute/SKILL.md b/plugins/vibe/skills/cl-execute/SKILL.md index 24109e9b..ef3c7aab 100644 --- a/plugins/vibe/skills/cl-execute/SKILL.md +++ b/plugins/vibe/skills/cl-execute/SKILL.md @@ -47,9 +47,11 @@ When invoked within a sweep, follow [display events](../cl-sweep/references/disp Read only the references relevant to the current phase: - Intake, readiness, requirements, a vibe ticket's frozen behavior, plan - drafting, contract audits, performance work, UI plan approval, or the - one-off cleanup-tooling churn guard: + drafting, contract audits, cost scaling, performance work, UI plan approval, + or the one-off cleanup-tooling churn guard: [references/planning-and-gates.md](references/planning-and-gates.md). +- The plan reviewer's prompt, passed verbatim: + [references/plan-review-prompt.md](references/plan-review-prompt.md). - CLI sweep context, support lanes, App Server worker continuity, callback protocol, or macOS GitHub transport fallback: [references/support-lanes-and-sweeps.md](references/support-lanes-and-sweeps.md). @@ -318,6 +320,7 @@ are never in it. Call `sync_status` when freshness could change the answer. | Callers and importers of a symbol or file | `code_symbols` to turn a bare name into a repo-qualified path, then `code_callers` (it drops test rows) and `code_importers` | `rg` in the worktree | | Tests to rerun for a file | repo related-test tooling when you hold the checkout; otherwise `code_tests_for` | `rg` over the test tree | | A string that is not a symbol (route, config key, flag, event, JSON field, IPC channel, error text) | `code_grep` with its literal fragments joined in one regex alternation | `rg` | +| Existing channels between two surfaces (push, pull, sockets, lanes, commands) | `code_grep` for the repo's channel primitives (for example socket emit and subscribe calls, signal emitters, command dispatchers, `setInterval`-based lanes) joined in one regex alternation | `rg` | | Blast radius | the code half above plus `blast_radius_tickets` on the repo-qualified path | `git log --follow` | | Tickets that touched a file | `blast_radius_tickets` | `git log -S`/`-L`, `git blame` | | One ticket's lineage, branches, PRs, changed files | `ticket_detail` | live ClosedLoop, `gh pr view` | diff --git a/plugins/vibe/skills/cl-execute/references/plan-review-prompt.md b/plugins/vibe/skills/cl-execute/references/plan-review-prompt.md new file mode 100644 index 00000000..85fd9de5 --- /dev/null +++ b/plugins/vibe/skills/cl-execute/references/plan-review-prompt.md @@ -0,0 +1,130 @@ +# Plan Review Prompt + +The single independent plan reviewer (Planning Workflow step 20 in +[planning-and-gates.md](planning-and-gates.md)) receives the text in the fence +below verbatim. After it, append the plan's path, the ticket slug and URL, the +applicable PRD slugs, and the private path for the review file. Do not +shorten, paraphrase, or add to the prompt. + +```text +Review the plan linked at the bottom. + +SOURCES +- Read the plan, its parent issue, and any PRD above it. Use closedloop-graph + for lineage, adjacent tickets, and collisions. +- Run `git fetch origin` first and reason against `origin/main`, not the local + ref. Check whether anything that landed since the plan was written already + did, obsoleted, or collided with part of it. +- Check the plan against the repo's AGENTS.md guardrails, especially + cross-surface web+desktop, version-skewed contracts, the closed-by-default + UI flag policy, and capability removal. +- Think through edge cases and unexpected behavior changes. +- Code is evidence; a comment describing code is a claim. Read the code. + +DEPTH +- A plan needs to be correct at a high level, not fully detailed. Do not ask + for detail it did not include; that review happens post-implementation. + Detail it does include must be correct. +- The exception is coverage: a missing step is fine, a missing outcome is a + finding. + +SCOPE EXCLUSIONS (check this as hard as mechanisms) +For every item the plan puts out of scope, classify it as: + (a) genuinely out of scope, + (b) covered by another live ticket, cite the slug, or + (c) a gap, nothing covers it. +Only (c) is a finding. Agents cut scope to make a plan look clean, so treat an +exclusion as unjustified until you have found the ticket that covers it. +Understand the prior history on this area of work before deciding. + +MECHANISMS (check this as hard as scope exclusions) +- The requirement is the outcome the ticket asks for. A mechanism the ticket + names is a proposal, held to the same test as one the plan invented. +- For every new mechanism (API route, table or column, cursor, poller or + background lane, cross-process channel, writes into files another tool + owns), name the existing mechanism that already does that job, or could, + and why the plan does not use it. New is unjustified until existing is + ruled out with evidence; reuse a connection before opening one. +- Cost: fleet size is unknown and growing, so never argue from a device + count. Judge by what steady-state cost scales with (idle installs, + connected clients, active sessions, events) at 10x and 100x, and by + per-device idle cost (timer wakeups, network, disk while nothing is + active; laptops on battery). Zero idle cost is the baseline; cost that + scales with idle clients is blocking unless no activity-scaled design + meets the outcome. +- One-way doors (a new data direction between surfaces, a contract installed + clients will depend on, a write into another tool's data) need a cited + human decision; without one, list a human decision item. + +DECISIONS +- A "Decisions" section in a ticket or plan is a record, not a ruling. For + each, find who made it, where, and which question it answered; no + traceable human statement means unratified. +- A newer ticket does not supersede an earlier explicit ruling by naming a + different mechanism; only a statement addressing that ruling does. When + records disagree, report the conflict; never resolve it by recency. +- A human decision item about a mechanism, consent, or an unratified or + conflicting decision is neither instruction inflation nor manufactured + ambiguity: those rules protect settled rulings, and this is not settled. + If the plan cites either rule to avoid a question, say so. + +SUBAGENTS +- Use workers from your own harness, and give every worker a name, or keep + its id, so you can reach it again later. + - Claude Code: use general-purpose or Explore workers and set model sonnet + or opus explicitly. The specialist reviewer agents cannot call MCP tools, + so route anything needing closedloop-graph to a worker that has them, and + tell workers to load the tools with ToolSearch first. + - Codex: use same-family `spawn_agent` workers with `fork_turns: "none"` + and `model: "gpt-6-sol"` at `high` or `xhigh`. They inherit no + conversation, so give each one the plan path, these sources, and its + question. + - When you run as a support lane, which never delegates, do the workers' + reads yourself. +- You own the conclusions. Verify load-bearing worker claims against primary + sources (files, git history, ticket records) before repeating them. +- When a claim does not hold up, message that worker by name or id (Claude + Code: SendMessage; Codex: `send_input`) with the specific objection and + what would settle it, instead of spawning a fresh agent. A named worker + resumes with its context, so a correction costs one follow-up turn rather + than a whole re-exploration. Repeat until the claim is proven or withdrawn. +- Workers: if you find something that changes what the rest of the review + should look at (the plan's premise already landed on main, the ticket it + defers to is closed, the file it targets no longer exists), tell the + reviewer immediately (Claude Code: SendMessage "main"; Codex: end your turn + with it first). Otherwise report once, at the end. +- No progress chatter, no "are you done" messages, no polling ListAgents or + worker status. + +CERTAINTY +- Resolve every question you can from the repo, the history, and + closedloop-graph. If one is genuinely unanswerable there (a product decision, + an unstated intent), state it as an open question with what you checked and + what would settle it. Do not turn an unresolved question into a confident + claim, and do not decide a product question yourself. +- A blocking finding cites the file and line, commit, or ticket record you + read yourself; a worker summary or code comment is not that. + +OUTPUT +- Write the review to -review.md, mode 0600, in the private + directory named at the bottom (never the repository or a shared temp + directory), print the path, and summarize the blocking findings in your + reply. +- The review has these sections in this order, each present ("none" if empty): + 1. Mechanism inventory, one row per new mechanism: what it does | cost + scales with | per-device idle cost | at 10x / 100x | existing mechanism + considered, why rejected | verdict. + 2. Decision provenance, one row per recorded decision: decision | who | + where | question answered | conflicts with | ratified / unratified / + conflicting. + 3. Scope exclusions, one row each: (a), (b) with slug, or (c). + 4. Blocking findings, each with what it was verified against. + 5. Should-fix findings. + 6. HUMAN DECISIONS, numbered H1, H2, ...: the question, what you checked, + what would settle it. The planner may not resolve, downgrade, or absorb + these; proceeding before an answer means naming the assumption built on. +- End with a verdict: approve as-is, approve with the listed changes, or + needs rework, then the count of open HUMAN DECISIONS. An open one rules + out approve as-is. +- Read-only. Do not edit the plan, the issue, or any source file. +``` diff --git a/plugins/vibe/skills/cl-execute/references/planning-and-gates.md b/plugins/vibe/skills/cl-execute/references/planning-and-gates.md index f3916002..0e0fc650 100644 --- a/plugins/vibe/skills/cl-execute/references/planning-and-gates.md +++ b/plugins/vibe/skills/cl-execute/references/planning-and-gates.md @@ -2,7 +2,8 @@ Use this reference for readiness classification, requirements contracts, a vibe ticket's frozen behavior, plan drafting/review, proposal contract audits, -performance work, UI plan approval, and cleanup-tooling churn control. +cost scaling, performance work, UI plan approval, and cleanup-tooling churn +control. ## Readiness Gate @@ -46,6 +47,12 @@ Before planning or execution, classify the ticket: existing answers and otherwise route exact missing decisions privately to the current user or engineering unless the user explicitly authorizes that exact Product comment. +- Decision provenance: for each decision a ticket or plan records, including a + `Decisions` section, establish who made it, where, and which question it + answered. A record with no traceable human statement is unratified. A newer + ticket does not supersede an earlier explicit ruling unless it addresses + that ruling. When records disagree, report the conflict as a human decision; + never resolve it by recency or by assuming one author meant to change it. - Latency, staleness, and disclosure contracts: verify exact ratified PRD, decision record, or linked owner before approving a plan that promises freshness ceilings, analytics lag, stale-but-shown behavior, disclosure, or @@ -207,6 +214,32 @@ If the audit fails, narrow the plan, defer invalid work, or return the normal blocker. Do not split the ticket, and do not encode unresolved behavior as an implementation detail. +## Cost Scaling + +When a plan introduces periodic or cross-surface work (a poller, interval, +cron, heartbeat, background lane, socket, subscription, feed, or watcher), its +Architecture Fit carries a `### Cost Scaling` table with these columns: + +| Option | Cost scales with | Per-device idle cost | At 10x / 100x | Existing mechanism reused | +| --- | --- | --- | --- | --- | + +It has at least two rows, and at least one names an existing mechanism. Find +the existing channels through the +[Discovery Routes](../SKILL.md#discovery-routes). + +- Write each recurring cost as a function of populations: installs, connected + clients, active sessions, events. +- Evaluate it at 10x and 100x. +- State the per-device idle term separately: timer wakeups, network, and disk + while nothing is active, on battery. +- Prefer activity-scaled designs with a zero idle term. +- Reuse an existing connection before opening one. +- Never justify a design by current fleet size; it is unknown and growing. + +`scripts/check-plan.mjs` requires the table when plan prose outside code +fences names periodic or cross-surface work. When a keyword fires on a plan +that adds no such work, pass `--no-periodic` and log why in the decision log. + ## Ephemeral Cleanup Tooling Guard Before approving a plan that adds one-off migration, cleanup, backfill, audit, @@ -367,9 +400,20 @@ through the [Discovery Routes](../SKILL.md#discovery-routes). state machine over independent booleans, a table or registry over branching, one typed model over repeated shape checks), and the strongest alternative you rejected with the concrete reason, grounded in code or - requirements. Skip this for established patterns, bug fixes, and choices - the ticket or PRD already makes, and never turn the alternative into a - question for the operator. + requirements. Skip this for established patterns and bug fixes. Only the + outcome a ticket or PRD asks for is binding. A mechanism it names (a + poller, feed, lane, socket, new route, table, cursor, or a write into + another tool's data) is a proposal held to the same test as one the plan + invents. If the named mechanism is dominated (for example its cost scales + with installs or idle time while an alternative scales with activity, or + a supported interface exists for a write into another tool's data), keep + the outcome and bring the mechanism choice to the human as one numbered + decision; never silently swap it and never silently build it. Raising that + decision is neither instruction inflation nor manufactured ambiguity: + those rules protect settled rulings, and a mechanism a ticket names is not + one. Otherwise never turn the alternative into a + question for the operator. Periodic or cross-surface work also gets the + [Cost Scaling](#cost-scaling) table. 14. One-way-door plans (Proposal Contract Audit `REQUIRED`, a new shared module or ownership boundary, or `High-complexity execution override: APPROVED`) get more design before drafting: @@ -427,23 +471,31 @@ through the [Discovery Routes](../SKILL.md#discovery-routes). [writing.md](writing.md). 19. Lint the draft before independent plan review and again before every upload or revision with this installed skill's `scripts/check-plan.mjs`: - `node /scripts/check-plan.mjs --template "/resources/plan_template.md" [--bug] [--safety-fact] [--narrow]`. + `node /scripts/check-plan.mjs --template "/resources/plan_template.md" [--bug] [--safety-fact] [--narrow] [--no-periodic]`. Pass `--bug` on a bug ticket, `--safety-fact` when the plan carries a - Safety fact, and `--narrow` only for simple or narrow work where step 16 - allows one flowchart. It checks the template's headings in order, left-over - template placeholders, Mermaid flowcharts (before and after unless - `--narrow`), that every `AC-` id maps to a task or test, revision narration - and sweep internals, and the Safety fact and red-first entries when - flagged. Fix every line it prints; a nonzero exit is an actionable plan - defect. Record the command and exit status as `Plan lint`. The plan + Safety fact, `--narrow` only for simple or narrow work where step 16 + allows one flowchart, and `--no-periodic` only when a periodic-work + keyword fired on a plan that adds no periodic or cross-surface work. It + checks the template's headings in order, left-over template placeholders, + Mermaid flowcharts (before and after unless `--narrow`), that every `AC-` + id maps to a task or test, revision narration and sweep internals, the + `### Cost Scaling` table when the prose names periodic or cross-surface + work, and the Safety fact and red-first entries when flagged. Fix every + line it prints; a nonzero exit is an actionable plan defect. Record the command and exit status as `Plan lint`. The plan reviewer no longer checks these mechanically and judges substance: whether the diagrams explain the scope, the approach is sound, and the evidence holds. 20. A plan gets exactly one independent plan review. The reviewer is a read-only same-family reviewer with a fresh context: a Codex worker uses a Codex sub-agent and a Claude Code worker uses a Claude Code sub-agent; - never call another harness's CLI for plan review. Two review generations - are for finished code before the PR, not for plans. After the single plan + never call another harness's CLI for plan review. Give the reviewer + [plan-review-prompt.md](plan-review-prompt.md) verbatim, followed by the + plan's path, the ticket slug and URL, the applicable PRD slugs, and the + private path for its review file (the ticket's private evidence directory + from Execution step 12 in + [implementation-review-pr.md](implementation-review-pr.md)). + Two review generations are for finished code before the PR, not for plans. + After the single plan review, verify each finding against the code before acting on it. Fix outline-level findings in the plan: a wrong premise or root cause, a missing outcome or acceptance criterion, an unjustified scope exclusion, a @@ -453,7 +505,11 @@ through the [Discovery Routes](../SKILL.md#discovery-routes). after fixing findings. Bring any finding that needs a product or scope call to the human as a numbered decision with the plan: return `MANUAL_INTERVENTION_REQUIRED` with the plan URL and the numbered - decisions instead of approving the plan. Never edit skill files from a ticket worker. When a cl-* skill + decisions instead of approving the plan. A finding the reviewer lists + under HUMAN DECISIONS is never the worker's to resolve, downgrade, or + absorb, including after verification: each one becomes one of those + numbered decisions, with the worker's own view beside it when it has + one. Never edit skill files from a ticket worker. When a cl-* skill instruction was missing, wrong, contradictory, or did not fire when it should have, record it in the result's `Process feedback` field: the skill file and section, the observed failure with an evidence pointer, and the diff --git a/plugins/vibe/skills/cl-execute/scripts/check-plan.mjs b/plugins/vibe/skills/cl-execute/scripts/check-plan.mjs index 3483bab6..20b58788 100644 --- a/plugins/vibe/skills/cl-execute/scripts/check-plan.mjs +++ b/plugins/vibe/skills/cl-execute/scripts/check-plan.mjs @@ -2,7 +2,7 @@ // Mechanical linter for cl-execute implementation plans. It checks only rules // a script can decide; the plan reviewer still judges substance. // -// Usage: check-plan.mjs --template [--bug] [--safety-fact] [--narrow] +// Usage: check-plan.mjs --template [--bug] [--safety-fact] [--narrow] [--no-periodic] // Exit 0 when clean, 1 when it printed problems, 2 on a usage or input error. // // The approach (a plan linter run before hand-back) follows pstack's @@ -29,6 +29,33 @@ const SWEEP_INTERNALS = [ /\bsupport lanes?\b/i, ]; +// Prose that names periodic or cross-surface work. Kept conservative: each +// hit asks for a Cost Scaling table, and --no-periodic waives it on the record. +const PERIODIC_WORK = [ + /\bpoll(?:s|ed|ing|ers?)?\b/i, + /\bintervals?\b/i, + /\bsetInterval\b/, + /\bevery\s+(?:\d+|N)\s*(?:ms|s|secs?|seconds?|m|mins?|minutes?|h|hrs?|hours?)\b/i, + /\bcron(?:tab)?s?\b/i, + /\bheartbeats?\b/i, + /\bbackground lanes?\b/i, + /\b(?:web)?sockets?\b/i, + /\bsubscri(?:be|bes|bed|bing|ptions?)\b/i, + /\bfeeds?\b/i, + /\bwatchers?\b/i, +]; + +const COST_SCALING_COLUMNS = [ + 'Option', + 'Cost scales with', + 'Per-device idle cost', + 'At 10x / 100x', + 'Existing mechanism reused', +]; + +// A cell that claims no existing mechanism. +const NO_MECHANISM = /^(?:|-+|none|n\/a|na|no|not applicable)$/i; + const FENCE = /^\s*(```|~~~)/; function scan(text) { @@ -116,6 +143,45 @@ function mermaidFlowcharts(info) { .filter((fence) => fence.isFlowchart); } +function tableCells(line) { + return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((cell) => cell.trim()); +} + +function costScaling(plan, add) { + const trigger = plan.findIndex((entry) => !entry.inFence && PERIODIC_WORK.some((re) => re.test(entry.line))); + const heading = headings(plan, 3).find((h) => h.title === 'Cost Scaling'); + if (!heading) { + if (trigger >= 0) { + const word = PERIODIC_WORK.map((re) => plan[trigger].line.match(re)).find(Boolean)[0]; + add( + trigger + 1, + `"${word}" names periodic or cross-surface work; add a ### Cost Scaling table (${COST_SCALING_COLUMNS.join(' | ')}), or pass --no-periodic when the plan adds none`, + ); + } + return; + } + const rows = []; + for (let n = heading.line; n < plan.length; n += 1) { + const entry = plan[n]; + if (!entry.inFence && /^#{1,3} /.test(entry.line)) break; + if (!entry.inFence && entry.line.trim().startsWith('|')) rows.push(tableCells(entry.line)); + else if (rows.length) break; + } + const normalize = (cell) => cell.toLowerCase().replace(/\s+/g, ' '); + const header = (rows[0] ?? []).map(normalize); + const missing = COST_SCALING_COLUMNS.filter((column) => !header.includes(normalize(column))); + if (missing.length) { + add(heading.line, `### Cost Scaling table is missing column(s): ${missing.join(', ')}`); + return; + } + const optionRows = rows.slice(1).filter((cells) => !cells.every((cell) => /^:?-+:?$/.test(cell))); + if (optionRows.length < 2) add(heading.line, '### Cost Scaling table needs at least two option rows'); + const reused = header.indexOf(normalize('Existing mechanism reused')); + if (!optionRows.some((cells) => !NO_MECHANISM.test(cells[reused] ?? ''))) { + add(heading.line, '### Cost Scaling table needs a row that names an existing mechanism under "Existing mechanism reused"'); + } +} + export function lintPlan(planText, templateText, options = {}) { const problems = []; const add = (line, message) => problems.push({ line, message }); @@ -217,17 +283,21 @@ export function lintPlan(planText, templateText, options = {}) { flagged('--bug', /red-first/i, 'name the red-first test in backticks'); } + // 7. Periodic or cross-surface work carries a Cost Scaling table. + if (!options.noPeriodic) costScaling(plan, add); + return problems.sort((a, b) => a.line - b.line); } export function parseArgs(argv) { - const options = { bug: false, safetyFact: false, narrow: false, template: '' }; + const options = { bug: false, safetyFact: false, narrow: false, noPeriodic: false, template: '' }; const files = []; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--bug') options.bug = true; else if (arg === '--safety-fact') options.safetyFact = true; else if (arg === '--narrow') options.narrow = true; + else if (arg === '--no-periodic') options.noPeriodic = true; else if (arg === '--template') { options.template = argv[i + 1]; i += 1; @@ -246,7 +316,7 @@ function main(argv) { parsed = parseArgs(argv); } catch (error) { process.stderr.write( - `check-plan: ${error.message}\nusage: check-plan.mjs --template [--bug] [--safety-fact] [--narrow]\n`, + `check-plan: ${error.message}\nusage: check-plan.mjs --template [--bug] [--safety-fact] [--narrow] [--no-periodic]\n`, ); return 2; } diff --git a/plugins/vibe/skills/cl-execute/scripts/check-plan.test.mjs b/plugins/vibe/skills/cl-execute/scripts/check-plan.test.mjs index 2deabb95..56c4de8b 100644 --- a/plugins/vibe/skills/cl-execute/scripts/check-plan.test.mjs +++ b/plugins/vibe/skills/cl-execute/scripts/check-plan.test.mjs @@ -49,6 +49,7 @@ function plan({ testPlan = '- [ ] Unit: `uploader.test.ts` covers AC-001 and AC-002', summary = 'Uploads retry once before failing.', extraTop = '', + architecture = '', } = {}) { return `${title} @@ -67,7 +68,7 @@ ${acceptance} The uploader owns retries. ${diagrams} - +${architecture} ## Tasks ${tasks} @@ -152,11 +153,48 @@ test('--safety-fact and --bug require a named, backticked proof in the Test Plan assert.deepEqual(messages(red, { bug: true }), []); }); +const costHeader = '| Option | Cost scales with | Per-device idle cost | At 10x / 100x | Existing mechanism reused |\n| --- | --- | --- | --- | --- |'; +const pollRow = '| Poll the list every 60 s | connected clients | one wakeup and one request per minute | linear in installs | none |'; +const relayRow = '| Hint over the relay socket | renames | none | linear in renames | relay socket |'; +const costTable = (...rows) => `\n### Cost Scaling\n\n${[costHeader, ...rows].join('\n')}\n`; +const polling = 'Desktop polls the rename feed every 60 s.'; + +test('periodic or cross-surface work needs a Cost Scaling table', () => { + assert.deepEqual(messages(plan({ summary: polling, architecture: costTable(pollRow, relayRow) })), []); + + const missing = lintPlan(plan({ summary: polling }), template); + assert.equal(missing.length, 1); + assert.equal(missing[0].line, 5); + assert.match(missing[0].message, /^"polls" names periodic or cross-surface work; add a ### Cost Scaling table/); + + assert.deepEqual(messages(plan({ summary: polling, architecture: costTable(relayRow) })), [ + '### Cost Scaling table needs at least two option rows', + ]); + const nothingReused = relayRow.replace(/relay socket \|$/, 'none |'); + assert.deepEqual(messages(plan({ summary: polling, architecture: costTable(pollRow, nothingReused) })), [ + '### Cost Scaling table needs a row that names an existing mechanism under "Existing mechanism reused"', + ]); + const noIdleColumn = costTable(pollRow, relayRow).replace(' Per-device idle cost |', ''); + assert.deepEqual(messages(plan({ summary: polling, architecture: noIdleColumn })), [ + '### Cost Scaling table is missing column(s): Per-device idle cost', + ]); +}); + +test('plans without periodic work, or waived with --no-periodic, need no Cost Scaling table', () => { + assert.deepEqual(messages(plan()), []); + assert.deepEqual(messages(plan({ summary: '```text\nsetInterval(poll, 5000)\n```' })), []); + for (const line of ['A watcher re-reads the file.', 'Open a websocket per tab.', 'Run it from cron.', 'A heartbeat every 30s.']) { + assert.ok(messages(plan({ summary: line })).some((m) => /Cost Scaling/.test(m)), line); + } + assert.deepEqual(messages(plan({ summary: polling }), { noPeriodic: true }), []); +}); + test('parses flags and rejects bad arguments', () => { assert.deepEqual(parseArgs(['p.md', '--bug', '--narrow', '--template', 't.md']), { file: 'p.md', - options: { bug: true, safetyFact: false, narrow: true, template: 't.md' }, + options: { bug: true, safetyFact: false, narrow: true, noPeriodic: false, template: 't.md' }, }); + assert.equal(parseArgs(['p.md', '--no-periodic', '--template', 't.md']).options.noPeriodic, true); assert.throws(() => parseArgs([]), /exactly one plan file/); assert.throws(() => parseArgs(['p.md', '--nope']), /unknown flag/); assert.throws(() => parseArgs(['p.md']), /pass --template/);