feat(v-onboard): add Operations/Deployment coverage dimension to /v:onboard - #4
feat(v-onboard): add Operations/Deployment coverage dimension to /v:onboard#4khymerao wants to merge 1 commit into
Conversation
Review: the gap is real, and one change would make the fix honestVerified the premise against the code before judging it, and it holds:
So the material arrives and evaporates between stages. Your framing — "becoming confident partial truth downstream" — is the accurate one, and it's the part that matters: an agent reading our KB gets a document that looks like a complete description of the system and contains no production domain and no deployment path. The implementation follows the existing precedent ( The one change I'd ask for
That is the same bug this PR fixes, moved down one floor. Today the KB is silently missing the ops layer; after this it would silently assert there isn't one — and a stated absence reads more confidently than a gap. Ask: don't let the detector conclude "no ops" — let it report "no signals found", and surface that at the gate as a question rather than a verdict: "no explicit ops files detected — if this project does deploy, point me at it." Then the hardcoded list works as an accelerator for the common case instead of a verdict on the uncommon one, and the human stays the backstop exactly where the heuristic is blind. This is the same rule we applied elsewhere this week: an incomplete scan must never read as a clean one. (Fair disclosure: the same critique applies to the MinorThe body says 69/69 selftest checks; VerdictTake it. Five files, no overlap with the in-flight v2.18 branch or with #5–#8, real gap, house pattern. This is the cleanest merge in the queue. Please add the "no signals found" framing and rebase, and I'll merge. |
5188dd1 to
47234a4
Compare
|
Addressed both, thanks for the sharp read. "no signals found", not "no ops" —
The hardcoded list now works as the common-case accelerator you described, with the human as backstop where it's blind. Rebase — rebased onto Follow-up (config-driven signal lists for both |
…oor, Workflow orchestration Four decisions locked in brainstorm: a third SCOPED tier; a narrow mechanically-checkable auto-route class that amends Iron Invariant #4 rather than deleting it; the impacted-tests reversal bought with an unconditional floor plus the CI backstop; and Workflow as the dispatch engine with Engine A retained for the subagent/headless/epic paths that provably cannot launch one. Corrects the recon on one point: the scope gate CAN be interposed. The script has no shell, but the agents it spawns do, so the gate becomes a pipeline stage rather than script logic -- which makes it mechanical for the first time on the claude path that carried 73 of 73 real jobs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
r1 is superseded, not patched -- spec drift by patching is a repeat failure here. Killed by the pre-flights: the headless justification for Engine A (workflows ARE available in -p and the SDK; only the ultracode keyword is route-restricted); the name "Engine B" (taken by the rejected claude -p shell-out; the Workflow engine is Engine C in nine places); a false citation of a || true contract that lives on the v2.18 branch, not main; and AC-8, which had no schema field and no transport. Found and closed: a Gate stage that throws would skip Record entirely and lose the audit trail on exactly the jobs that failed (v2.6.4 recurring); agent() returning null would read as "no violations"; override #4 returns low/low bands beside override_fired=4 and would have granted auto-route to an overridden record; the auto-route class gated on shape with no checks-passed and no history predicate, and re-validated only line count post-diff, leaving three TOCTOU windows open; docs-only is a self-modification hole here because skills/agents markdown IS the enforcement layer; the floor was missing previously-failing and newly-added; and none of tests/ is executed by CI at all. v2.18-autonomy is absorbed: Feature C already exists as _enforcement_rule in its hook, and two blocking Stop registrations are undefined behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ut DIRECT Why: v3.0 gives the scorer a third tier and a narrow class of changes that land unattended. The old invariant — "The score only OFFERS, never auto-routes" — was the charter every consumer read, and once the DIRECT auto-route class exists that sentence is not a stricter policy, it is a false one. A prose rule contradicted by the code it governs teaches agents to discount the whole file, so it is replaced everywhere it is asserted in this lane rather than left standing beside its own exception. The amended wording (spec §A4) is verbatim: the score OFFERS by default; it auto-routes only inside the DIRECT auto-route class, whose membership is decided by mechanically checkable predicates and never by model judgement; every other tier still requires a human offer and acceptance. - phase-preeval.md: the canonical statement (Invariant #4), the Stan Edgar framing that restated it, and Gate 3's two-valued verdict enum, now three-valued. - SKILL.md: Stage -1 rewritten to the three tiers, plus the pipeline diagram. Edited strictly in place — business-logic.md and tech-context.md cite this file by line range and are in other lanes, so the line count is unchanged. - rationalization-table.md: the "design was already settled" row said the fast-path "never auto-routes"; new row refutes "the change is tiny, I'll skip the triage record" — the record is what buys the DIRECT branch, and it is the numerator the miscalibration breaker counts. - routing-policy.md: triage decides whether anything reaches this table at all, and the scorecard's own "never auto-route to a lower-trust backend" is disambiguated from #4 rather than silently colliding with it. - project-config.py: the fail-closed comments claimed a bad value can "never become an auto-route"; the accurate claim is that it can never ENLARGE the class. - architecture.md: the citation-verified claim "it never auto-routes" was about to become false. - pre-eval-config.md: stops declaring a closed two-value decision enum, and states the record schema's taxonomy conditional in its NEGATIVE form (decision != FULL_PIPELINE). Keyed on FASTPATH_ELIGIBLE it was correct only while the enum had two values: with a third, a null-taxonomy record would validate as SCOPED — a real routing decision taken without the protections the taxonomy is the only source of. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…anded in 3c15b8c) EMPTY BY NECESSITY, not by choice. This job's four files were staged when a concurrent commit in the same working tree (3c15b8c, the r7 spec commit) ran `git commit` and swept the index, so the diff is already in history under an unrelated message. Nothing is amended — that commit also carries another agent's work. This records the WHY the swept commit does not. WHY THIS JOB EXISTS: v2.18 shipped the mechanism and left the surface unwired. `--arm-goal`, `--disarm-goal` and `--goal-status` exist in scripts/compound-v-epic-state.py and hooks/epic-goal-stop.sh reads them, but no command ever CALLED them — an armed goal was reachable only by hand-running the script. Feature F's headless shim was likewise an orphan, referenced by README and CI and invoked by nothing. A mechanism nothing invokes is prose with a test suite, which is the exact defect this release exists to remove. WHAT LANDED: - commands/v-epic.md — §0d arms the goal (offered, never silent): session id from the harness, refuse rather than invent one when it is empty, the two shipping conditions, max_continues > 0 with `0` stated as invalid, the §9 commit rule, and a hook-store check that can FALSIFY the arm rather than assume it took. §0e prints the shim artifact and installs nothing, never pre-filling --allow-build. "Goal disarm" pops the record at every terminal exit and takes its wording from `met`, never from "the hook stopped blocking". - commands/v-status.md — an armed-goal column off the read-only --goal-status, degrading to an em-dash on every failure path. Continuations are always "—": that counter lives in the hook's own store keyed on a session id this command does not have, and a guessed count is the fabricated metric the charter forbids. - commands/v-init.md — `enforcement.*` seeded OFF as a MAP of named gates, so 3.0's triage_gate joins 2.18's pipeline_bypass by adding a key, not by changing shape. /v:init never offers to turn one on: a false positive costs every teammate a blocked turn. Also amends Iron Invariant #4 to its v3.0 wording at the three sites here that still asserted the deleted absolute (coordinator hand-off from task-5). - skills/compound-v/epic-mode.md — the armed-goal lifecycle, one armed epic per project (per-file refusal plus fail-open cross-file discovery), session_id matching, and the distinction that pays for all of it: a terminal-but-unmet epic yields "do not continue", not "met". Calling a stopped epic finished is a fabricated completion claim, and should_continue is not a completion answer. Every flag documented was run before it was written about, and the terminal-but-unmet JSON quoted in epic-mode.md is a real probe, not an example. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… from becoming a DIRECT
The band matrix collapsed 8 of its 9 cells into FULL. A medium/medium change —
the ordinary case — bought the whole pipeline, and small work escaped triage
entirely rather than being over-processed. `DECISION_SCOPED` ("SCOPED_PIPELINE")
un-collapses the middle: manifest, run dir, scope gate, floor and one combined
review, without recon or the three pre-flights.
WHY each piece is shaped the way it is:
* The matrix is a lookup INSIDE `score`, applied at Layer B on the bands as
computed. It is not a post-hoc read of the verdict dict, and that distinction
is the whole job. Override #4 (semantic-vs-path disagreement) returns the
GENUINE low/low bands beside `override_fired=4` — a reader that re-derived the
tier from the record's two band fields would hand DIRECT, and with it the
auto-route class, to a record whose own audit trail says a hard override
fired. The selftest asserts exactly that: the bands really are low/low, a
post-hoc read of them really would say DIRECT, and the engine really says FULL.
* ANY FIRED OVERRIDE FORCES FULL is enforced in `_verdict`, the single
construction point of every verdict, not repeated at each of the seven return
sites. Today every Layer-A row already passes FULL, so it changes no behaviour;
it exists so no future edit can produce the pairing at all. The record schema
now machine-checks the same invariant, so a regressed producer cannot write one
either.
* Unknown bands stay fail-closed twice over: override #6 catches them first, and
the matrix lookup's default is FULL, so the table cannot fail open on a band a
future taxonomy invents.
* A low/low change that fails a DIRECT predicate (fan_out, single literal path)
demotes ONE tier, to SCOPED — not to FULL. The matrix owns the tier; Layer B
only gates the DIRECT cell, and SCOPED is where the matrix already puts this
cell's low/medium and medium/low neighbours. The spec does not state this case;
it is called out in the handoff report as a resolved ambiguity.
* `fast_path: "off"` names SCOPED explicitly rather than letting it fall through
the `== FASTPATH` test into the untouched arm. An operator who set that switch
asked for the full pipeline; handing them a tier that also skips recon and the
pre-flights would be the kill-switch failing open on a value that did not exist
when the switch was written.
* `postdiff-reclassify` returns `escalate_to` beside `escalate`. A bare boolean
was a complete answer only while "not the fast path" could mean one thing.
DIRECT demotes to SCOPED (spec §A4 predicate 8), SCOPED to FULL, and an absent
`from_decision` — every pre-3.0 caller — falls closed to FULL, so existing
callers keep exactly their old meaning while a 3.0 caller gets the one-tier
demotion. The tier strings are duplicated there as a wire vocabulary rather
than imported, and a selftest compares them against the scorer's constants so
the two cannot drift silently.
* The schema's taxonomy conditional is re-keyed from `decision ==
FASTPATH_ELIGIBLE` to `decision != FULL_PIPELINE`. Keyed positively it silently
stopped applying the moment a third tier existed, and a SCOPED record with a
null taxonomy would have validated — the exact fail-open that check exists to
prevent. Forged records now prove the schema refuses it in both tiers while the
real absent-taxonomy FULL record still validates.
* Iron Invariant #4 is amended here to the §A4 wording (the score OFFERS by
default; it auto-routes only inside the DIRECT auto-route class, decided by
mechanically checkable predicates and never by model judgement) in this lane's
two carriers only — the engine docstring and the record schema. The other
copies belong to task-5.
The new value is documented in the module docstring as an interface, because
tasks 2, 3 and 4 consume it and cannot read this code.
Selftests: preeval 85 -> 131 cases, postdiff-reclassify 64 -> 78. All nine matrix
cells, seven fail-closed band pairs, both halves of the override hazard, the
`_verdict` backstop across all 7 override rows x both proportionate decisions,
and end-to-end SCOPED record write + schema validation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spec §C says the triage record binds session_id, base_commit and declared_paths, and the Stop-hook rule task-8 landed reads exactly those to decide whether a record covers the current diff. The schema is additionalProperties:false and carried none of them, so no record could express them and no record could ever cover anything. The gate is off by default, so nothing was broken — Feature C would simply have shipped inert, a correct mechanism with nothing to feed it. WHY each field is shaped the way it is: * `session_id` is optional and nullable, never required. This engine never sees a session; only /v:triage does. Requiring it would invalidate every record the scorer writes outside a triage command. Absent or null means the record binds no session and therefore covers nothing, which is the fail-closed direction the gate's exact-match test already produces. The EMPTY STRING is rejected: it looks like a binding and can never match, and that is the one shape that misleads a reader. * `declared_paths` items are constrained to what the consumer can actually read. The hook serializes candidates as `tier<US>run_id<US>path` and DROPS any entry containing U+001F, LF or CR. Dropping narrows the declared set, which is safe for the gate but silent for the producer — so the schema rejects every C0 control character here, where the producer still finds out. Absolute paths and `..` segments are rejected for the same reason: git never reports either, so such an entry could only ever match nothing while looking like coverage. * `base_commit` is expressible and deliberately inert. task-8 reads it and derives no freshness rule, because HEAD legitimately advances mid-session — /v:triage's own commit of the record moves it — so a mismatch is not evidence of staleness. The description says so, and says not to add such a rule without evidence that a mismatch correlates with a bad decision. * `tier` is added and PINNED to `decision` by three conditionals. The hook prefers `.tier` over `.decision` when present, so an unconstrained `tier` would be a second, higher-priority source of truth for a safety-relevant classification: a record could say FULL_PIPELINE and wear `tier: DIRECT` and be exempted as the auto-route class. Pinned, the hook's preferred branch is provably identical to its fallback instead of a way around it. This is the same hazard as override #4 one layer up, and it gets the same answer — make the disagreement unrepresentable rather than trusted. * `build_record` gains an optional `binding` kwarg, and it is a footgun removal rather than a convenience. `digest` covers the whole record, so a producer that attached these fields after building would ship a record whose self-integrity digest silently no longer verifies — silently, because `digest` is optional and checked only when present. The kwarg folds the binding in before the digest is taken. It does NOT accept `tier`: that is derived from the decision, so the producer cannot introduce the disagreement the schema forbids. Without a binding the record is byte-for-byte and digest-for-digest what it was. Verified against the consumer, not just asserted: the hook's own `_TRIAGE_JQ` was extracted verbatim and run over records built to this schema. All three decisions map to DIRECT/SCOPED/FULL, a record carrying explicit `tier` produces output identical to its decision-only twin, and a foreign-session record emits nothing. Selftests: preeval 131 -> 174 cases. Covers tier agreement and disagreement for all three decisions, session_id null/absent/empty, base_commit shapes, the three declared-path forms the gate understands, twelve rejected path shapes including the separator and line breaks it would otherwise drop, and the digest staying correct through build_record while breaking when bolted on afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… orphan task-1 closed with 174 selftests and made the tier/decision disagreement UNREPRESENTABLE rather than merely discouraged: task-8 prefers a record's .tier over its .decision, so an unconstrained tier field would have been a second, higher-priority source of truth for a safety classification -- a record could read FULL_PIPELINE, wear tier DIRECT, and be exempted as the auto-route class. Three schema conditionals pin them together. That is override #4's hazard one layer up, answered the same way. Two rulings it asked for: 1. RATIFIED, with the rule narrowed. My "consumers read the constants from the engine" was too broad. A sibling analyser that imports nothing and consumes the value as JSON off a record may declare its own constants, PROVIDED a selftest loads the scorer by path and asserts equality. The hazard named was drift; that test closes drift directly, while a runtime import for three strings adds a dependency whose None fallback reintroduces the literals. 2. ASSIGNED. Spec C says a DIRECT record binds the realised diff digest and named no owner -- the fifth mechanism this release would have shipped with nothing to feed it. /v:triage owns it: it computes predicate 8's post-diff re-validation and is the only place the realised diff exists. Also worth recording: it closed a footgun nobody asked about. `digest` covers the whole record, so a producer that built a record and then attached the binding shipped one whose self-integrity digest silently no longer verified -- silently, because digest is optional and only checked when present. build_record now takes the binding before the digest is taken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@khymerao — the idea is accepted and we want this in: an Operations/Deployment layer that today is silently dropped from the generated docs is a real gap. The branch cannot merge as-is because
The spec/plan files under |
Rebase of procoders#4 onto v3.5.1 (branch had drifted two /v:onboard rewrites: 3.4.x, 3.5.0). Re-placed against current anchors; addresses review. - scripts/compound-v-onboard.py: detect-ops next to detect-ui in build_parser() + main() dispatch; detect_ops/_ops_category/_OPS_* + 6 selftest rows (165 ok). detect_ops reads only filenames (os.walk), never file contents, so the hardened _open_regular/_read_bounded path does not apply — noted in the docstring. - Reviewer's core ask (accelerator, not verdict): signals_found (not 'present'); CLI prints 'no-signals' not 'no-ops'; GATE surfaces BOTH branches — found -> confirm, no-signals -> open question ('point me at your deployer, e.g. ship.sh'). An empty scan is never a silent skip. - onboarding.md: re-placed DETECT / EXTRACT (5th claim type 'operations') / GATE / WRITE surface / operations.md doc section / INDEX docmap / intro against the 3.5.0 restructure. operations.md registered as a docmap key for .onboard-manifest.json ({"docs": {operations.md: [cited]}}); citations obey the new Tier-1 containment gate. - specs: base-spec table row + conditional-fourth prose; CHANGELOG Unreleased entry; commands/v-onboard.md non-negotiable procoders#7. Gates green locally: --selftest (165 ok), lint-frontmatter, rules-lint, dead-link.
47234a4 to
c66b21c
Compare
|
Rebased onto v3.5.1 and re-placed against the current code - thanks for the detailed map, it made this quick.
Also carried the review point from the previous round: the detector is an accelerator, not a verdict. Heads-up: the ~80-line script insert and the DETECT block shift a few |
What
Adds an explicit Operations / Deployment coverage dimension to the
/v:onboardpipeline. When a repo has CI/CD, container, or deploy files, onboarding now produces a citeddocs/superpowers/architecture/operations.mdinstead of silently dropping that layer from the generated knowledge base.Additive & gated —
operations.mdis only generated when ops files are present and the maintainer confirms it at the existing HUMAN GATE; an unattended/auto-approve run includes it with no new code path.Why
packalready includesdocker/**,.github/workflows/*, Terraform, and deploy scripts, so the raw material reached EXTRACT — but DETECT never inventoried it, EXTRACT had no claim type for it, and the arch doc set was fixed atarchitecture.md/business-logic.md/tech-context.md. Net effect: Docker topology, CI/CD, production domain, and runbooks were silently skipped — becoming confident partial truth downstream. This closes that blind spot.How it works
detect-opssubcommand (mirrorsdetect-ui) inventories three categories — CI/CD (.github/workflows/*,.gitlab-ci.yml,.circleci/config.yml,Jenkinsfile, …), containers/infra (Dockerfile*,docker-compose*/compose.*,*.tf/*.tfvars, k8s heuristics), and deploy/PaaS (Procfile,fly.toml,vercel.json,deploy*.sh, …). Returns{signals_found, ci_cd[], containers[], deploy[]}; CLI printsops/no-signalsor--json. Empty scan = open question at the gate, never ano-opsverdict.type(operations) targetingoperations.md; load-bearing rules still bite (deploy-secret path, prod/branch gate, fail-closed CI check).typeis free-form toverify-citations, so no schema change.operations.mdas its own explicit per-artifact confirm; declining drops it. k8s detection is an honest filename/dir heuristic, flagged as such.Contents
scripts/compound-v-onboard.py—detect_ops()+_ops_category()+detect-opsCLI + selftest (walks the filesystem excludingVENDOR_DIRS, so the non-git--selftesttrees detect too)skills/compound-v/onboarding.md— DETECT bullet, EXTRACT type,operations.mddoc section, §6 gate confirm, §7 write surface, refresh notedocs/superpowers/specs/2026-06-30-v-onboard-design.md— artifacts-table row + conditional-fourth prosedocs/superpowers/{specs,plans}/2026-07-17-*Conventions / gates (verified locally)
main;python3 scripts/compound-v-onboard.py --selftest— 77/77 checks passdetect-ops --jsonverified on this repo (detects its own.github/workflows/*);no-signalson a bare treeverify-citations/ claims-schema change; no new runtime deps, hooks, or scriptsscripts/lint-frontmatter.pynot run — nopyyamlin the local env; N/A regardless (diff touches no frontmatter)