Skip to content

Commit 47234a4

Browse files
committed
fix(v-onboard): detect-ops reports 'no signals found', never a 'no ops' verdict
Per review: a fixed signal list is an accelerator for the common case, not a verdict on the uncommon one. A project deploying via a bespoke ship.sh matched nothing and the pipeline would silently assert 'no ops layer' — a stated absence reads more confidently than a gap. - detect_ops: rename present -> signals_found; falsity documented as 'no signals found', not 'no ops'. Empty result carries empty lists, no verdict. - CLI: print 'no-signals' (an open question), not 'no-ops' (a false absence). - onboarding.md §1/§3/§6/operations.md: empty scan is never a silent skip; the GATE surfaces an open question ('point me at your deployer, e.g. ship.sh'). Autonomous run records 'no signals found (not confirmed absent)'. - specs updated; selftest asserts empty lists on bare (no false verdict).
1 parent 022dc2e commit 47234a4

4 files changed

Lines changed: 100 additions & 53 deletions

File tree

docs/superpowers/specs/2026-06-30-v-onboard-design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ it in would change that command's character. They stay composable: `/v:init`'s c
4949
| File | Location | When | Verification |
5050
|---|---|---|---|
5151
| `architecture.md`, `business-logic.md`, `tech-context.md` | `docs/superpowers/architecture/` | always | citation hybrid (§7) |
52-
| `operations.md` | `docs/superpowers/architecture/` | ops files present, confirmed at gate | citation hybrid (§7) |
52+
| `operations.md` | `docs/superpowers/architecture/` | ops signals found (or maintainer-pointed), confirmed at gate | citation hybrid (§7) |
5353
| `CONVENTIONS.md` | repo root | code present | derived from real config evidence |
5454
| `DESIGN.md` (Google format) | repo root | UI repo only | `@google/design.md lint` (§8 caveat) |
5555
| `AGENTS.md` (primary, confirmable) + thin `CLAUDE.md` (`@AGENTS.md`) | repo root | always | detect-and-bridge (§6) |

docs/superpowers/specs/2026-07-17-v-onboard-operations-dimension-design.md

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,11 @@ bare bool:
6464

6565
```
6666
detect_ops(repo) -> {
67-
"present": bool, # true iff any category non-empty
68-
"ci_cd": [paths...],
69-
"containers": [paths...],
70-
"deploy": [paths...],
67+
"signals_found": bool, # true iff >=1 KNOWN signal matched. FALSE = "no signals found",
68+
# NOT "no ops layer" — a bespoke ship.sh matches nothing yet exists.
69+
"ci_cd": [paths...],
70+
"containers": [paths...],
71+
"deploy": [paths...],
7172
}
7273
```
7374

@@ -88,11 +89,13 @@ always tracked, so this does not diverge from the git-tracked PACK/scope-gate in
8889
CLI wiring (mirrors `detect-ui`):
8990

9091
- `add_parser("detect-ops")` with `--repo` (default `.`) and `--json`.
91-
- `main()`: `detect-ops` prints `ops` / `no-ops` by default; with `--json`, prints the
92-
grouped inventory dict. Exit 0.
93-
- **Selftest** in the existing selftest block: `detect_ops(...)["present"] is True` on a
94-
fixture containing a `.github/workflows/ci.yml` (or `Dockerfile`); `... is False` on a
95-
bare tree — matching the shape of the existing `detect_ui` true/false selftests.
92+
- `main()`: `detect-ops` prints `ops` / `no-signals` by default (deliberately **not** `no-ops`
93+
the empty case is an open question, not an absence verdict); with `--json`, prints the grouped
94+
inventory dict. Exit 0.
95+
- **Selftest** in the existing selftest block: `detect_ops(...)["signals_found"] is True` on a
96+
fixture containing a `.github/workflows/ci.yml` (or `Dockerfile`); `... is False` **with empty
97+
category lists** on a bare tree (asserting the empty result carries no false verdict) — matching
98+
the shape of the existing `detect_ui` true/false selftests.
9699

97100
### 4.2 `onboarding.md` authority-doc edits
98101

@@ -107,17 +110,25 @@ CLI wiring (mirrors `detect-ui`):
107110
**load-bearing** (`security` / `fail-closed`) and blocks on unsupported per the existing
108111
two-tier gate.
109112
- **New "operations.md" section** (parallel to the CONVENTIONS.md / DESIGN.md section) —
110-
`operations.md` is generated **only when `detect-ops` is `ops`**; on a repo with no ops
111-
files it is **skipped** (verify this negative path on a non-ops dogfood, mirroring the
112-
DESIGN.md negative-path note). Read-then-cite from real workflow / Docker / deploy files.
113-
Covers: container topology, CI/CD stages, deploy target + production domain, runbook
114-
pointers. Never extracts a credential into the doc — `scan-output` (§7) still refuses.
115-
- **§6 HUMAN GATE**`operations.md` is presented as its **own explicit per-artifact
116-
confirm**, framed with the detected inventory: *"DevOps/deployment tooling detected:
117-
`<ci_cd / containers / deploy inventory>` — include `operations.md`?"* Decline → the doc
118-
is dropped and nothing is written for it. Under a fully autonomous / unattended run the
119-
gate auto-approves it, same as every other artifact — this is the "take them into account
120-
without asking in an autonomous cycle" behavior, and it needs no new code path.
113+
`operations.md` is generated when `detect-ops` found signals (`signals_found: true`) **or** when
114+
the maintainer answers the GATE's open question by naming a bespoke deployer the signal list
115+
missed. It is skipped **only** when `signals_found: false` **and** the human confirmed there is
116+
genuinely nothing — never silently on an empty scan (verify BOTH the found path and the
117+
open-question path on dogfoods). Read-then-cite from real workflow / Docker / deploy files (or the
118+
file the maintainer pointed at). Covers: container topology, CI/CD stages, deploy target +
119+
production domain, runbook pointers. Never extracts a credential — `scan-output` (§7) still refuses.
120+
- **§6 HUMAN GATE** — the detector is an accelerator, never a verdict, so the gate surfaces ops in
121+
**both** branches:
122+
- `signals_found: true``operations.md` as its **own explicit per-artifact confirm**, framed
123+
with the detected inventory: *"DevOps/deployment tooling detected: `<ci_cd / containers / deploy
124+
inventory>` — include `operations.md`?"* Decline → dropped.
125+
- `signals_found: false`**not** a silent skip but an **open question**: *"No explicit ops files
126+
detected — if this project deploys, point me at it (e.g. a hand-rolled `ship.sh`)."* Human names
127+
it → documented; human confirms nothing → skipped. The human, not the heuristic, decides.
128+
129+
Under a fully autonomous / unattended run the gate auto-approves the `signals_found: true` doc
130+
(ops taken into account without asking — no new code path); with `signals_found: false` and no
131+
human, it records "no signals found (not confirmed absent)" rather than asserting no ops layer.
121132
- **§7 WRITE surface** — add `docs/superpowers/architecture/operations.md` to the approved
122133
v1 write set. It is a normal cited architecture doc: provenance header, output secret
123134
gate, commit-before-index all apply unchanged.
@@ -127,7 +138,7 @@ CLI wiring (mirrors `detect-ui`):
127138
### 4.3 Spec `2026-06-30-v-onboard-design.md`
128139

129140
- Add an artifacts-table row:
130-
`operations.md | docs/superpowers/architecture/ | ops files present, confirmed | citation hybrid (§7)`.
141+
`operations.md | docs/superpowers/architecture/ | ops signals found or maintainer-pointed, confirmed | citation hybrid (§7)`.
131142
- Note `operations.md` as the **conditional fourth** architecture doc (the durable set is
132143
three-always + `operations.md`-when-ops), consistent with the Cline Memory Bank framing.
133144

@@ -143,9 +154,12 @@ No `verify-citations` / claims-schema change. No pre-flight change.
143154

144155
## 6. Verification
145156

146-
- `detect_ops` selftest passes (present-true on fixture, present-false on bare) inside the
147-
existing `python3 scripts/compound-v-onboard.py selftest` run; whole selftest stays green.
157+
- `detect_ops` selftest passes (`signals_found`-true on fixture; `signals_found`-false **with empty
158+
lists** on bare) inside the existing `python3 scripts/compound-v-onboard.py selftest` run; whole
159+
selftest stays green.
148160
- `detect-ops --json` returns the grouped inventory on a real ops repo (e.g. the Laravel+Vue
149-
dogfood with `docker/**` + `.github/workflows/ci.yml`); `no-ops` on a bare tree.
150-
- Manual pipeline read-through: an ops repo surfaces the gate confirm and, on approval,
151-
writes a cited `operations.md`; a non-ops repo skips it (negative path).
161+
dogfood with `docker/**` + `.github/workflows/ci.yml`); `no-signals` on a bare tree.
162+
- Manual pipeline read-through, **both** gate branches: an ops repo surfaces the confirm and, on
163+
approval, writes a cited `operations.md`; a signal-less repo surfaces the **open question** (not a
164+
silent skip) — the doc is written if the maintainer points at a bespoke deployer, skipped only if
165+
they confirm none.

scripts/compound-v-onboard.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,13 @@ def _ops_category(rel: str):
272272

273273
def detect_ops(repo: str) -> dict:
274274
"""Inventory CI/CD + container/infra + deploy files. Walks the filesystem (excluding VENDOR_DIRS)
275-
so it works on non-git trees too. `present` is True iff any category matched."""
275+
so it works on non-git trees too.
276+
277+
`signals_found` is True iff at least one KNOWN signal matched. Its falsity means "no signals
278+
found" — NOT a verdict that the project has no ops layer. The signal list is a fixed accelerator
279+
for the common case; a bespoke deployer (e.g. `ship.sh`) matches nothing, so an empty result is
280+
an OPEN QUESTION the gate must surface ("no explicit ops files — if this project deploys, point
281+
me at it"), never a confident "no ops". An incomplete scan must never read as a clean one."""
276282
found = {"ci_cd": [], "containers": [], "deploy": []}
277283
for dirpath, dirnames, filenames in os.walk(repo):
278284
dirnames[:] = [d for d in dirnames if d not in VENDOR_DIRS]
@@ -283,7 +289,7 @@ def detect_ops(repo: str) -> dict:
283289
found[cat].append(rel)
284290
for k in ("ci_cd", "containers", "deploy"):
285291
found[k].sort()
286-
found["present"] = any(found[k] for k in ("ci_cd", "containers", "deploy"))
292+
found["signals_found"] = any(found[k] for k in ("ci_cd", "containers", "deploy"))
287293
return found
288294

289295

@@ -970,13 +976,18 @@ def check(name, cond):
970976
with open(os.path.join(d5b, "Dockerfile"), "w") as fh: fh.write("FROM alpine\n")
971977
with open(os.path.join(d5b, "fly.toml"), "w") as fh: fh.write("app='x'\n")
972978
r_ops = detect_ops(d5b)
973-
check("detect_ops present true on ci+docker", r_ops["present"] is True)
979+
check("detect_ops signals_found true on ci+docker", r_ops["signals_found"] is True)
974980
check("detect_ops finds ci_cd workflow", ".github/workflows/ci.yml" in r_ops["ci_cd"])
975981
check("detect_ops finds container Dockerfile", "Dockerfile" in r_ops["containers"])
976982
check("detect_ops finds deploy fly.toml", "fly.toml" in r_ops["deploy"])
977983
finally:
978984
shutil.rmtree(d5b, ignore_errors=True)
979-
check("detect_ops present false on bare", detect_ops(tempfile.mkdtemp())["present"] is False)
985+
# Bare tree ⇒ signals_found False. This is "no signals found" (an open question for the gate),
986+
# never a "no ops layer" verdict — a bespoke deployer would match nothing yet still exist.
987+
d5c = detect_ops(tempfile.mkdtemp())
988+
check("detect_ops signals_found false on bare", d5c["signals_found"] is False)
989+
check("detect_ops empty lists on bare (no false verdict, just no signals)",
990+
d5c["ci_cd"] == [] and d5c["containers"] == [] and d5c["deploy"] == [])
980991

981992
# OUTPUT-side secret gate: blocks a secret in a GENERATED doc, passes clean prose.
982993
d6 = tempfile.mkdtemp()
@@ -1290,7 +1301,8 @@ def main(argv) -> int:
12901301
if args.json:
12911302
print(json.dumps(result, indent=2))
12921303
else:
1293-
print("ops" if result["present"] else "no-ops")
1304+
# "no-signals" (an open question for the gate), NOT "no-ops" (a false absence verdict).
1305+
print("ops" if result["signals_found"] else "no-signals")
12941306
return 0
12951307
if args.cmd == "scan-output":
12961308
result = scan_output_files(os.path.abspath(args.repo), args.files)

skills/compound-v/onboarding.md

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,21 @@ Inventory the ground truth, write nothing:
5555
- **Existing instruction files** (treat per the cardinal rule above), stack, git remote origin.
5656
- **UI presence** via `python3 scripts/compound-v-onboard.py detect-ui --repo .``ui` / `no-ui`.
5757
This is the only thing that decides whether the DESIGN.md branch runs (step 9 / §DESIGN below).
58-
- **Operations / Deployment presence** via `python3 scripts/compound-v-onboard.py detect-ops
59-
--repo . --json``{present, ci_cd[], containers[], deploy[]}`. Inventories CI/CD
58+
- **Operations / Deployment signals** via `python3 scripts/compound-v-onboard.py detect-ops
59+
--repo . --json``{signals_found, ci_cd[], containers[], deploy[]}`. Inventories CI/CD
6060
(`.github/workflows/*`, `.gitlab-ci.yml`, `.circleci/config.yml`, `Jenkinsfile`,
6161
`azure-pipelines.yml`, `.travis.yml`, `bitbucket-pipelines.yml`), container/infra
6262
(`Dockerfile*`, `docker-compose*`/`compose.*`, `*.tf`/`*.tfvars`, and k8s heuristics —
6363
`k8s/`, `kustomization.yaml`, Helm `Chart.yaml`), and deploy/PaaS (`Procfile`, `fly.toml`,
6464
`vercel.json`, `netlify.toml`, `render.yaml`, `serverless.yml`, `app.yaml`, `deploy*.sh`).
6565
Silent inventory like `detect-ui` — the *include-it?* ask lives at the GATE (§6), not here.
66-
`present: true` is what gates the operations.md branch (§operations.md below). k8s detection
67-
is a filename/dir heuristic (it cannot see manifest content) — stated as such here and in the
68-
operations.md doc section.
66+
The fixed signal list is a **common-case accelerator, not a verdict**:
67+
`signals_found: true` proposes the operations.md branch with the found files; `signals_found:
68+
false` means **"no signals found," NOT "this project has no ops layer"** — a bespoke deployer
69+
(e.g. a hand-rolled `ship.sh`) matches nothing yet still exists. So the empty case is **never a
70+
silent skip**; it becomes an **open question at the GATE** (§6). An incomplete scan must never
71+
read as a clean one. k8s detection is a filename/dir heuristic (it cannot see manifest content) —
72+
stated as such here and in the operations.md doc section.
6973
- **Style configs**: eslint / prettier / ruff / editorconfig / tsconfig / lockfiles — the
7074
deterministic evidence `CONVENTIONS.md` is later derived from.
7175
- **Cross-tool signal** for the bridge decision: presence of `.cursor*`, `.windsurf*`, `GEMINI.md`,
@@ -101,7 +105,9 @@ A claim is **load-bearing** when it concerns **security, fail-closed behavior, o
101105
the claims where being confidently wrong is dangerous.
102106

103107
`operations` claims (CI/CD, container topology, deploy target, runbook pointers) target
104-
`operations.md` and are emitted **only when DETECT's `detect-ops` reported `present: true`**. The
108+
`operations.md` and are emitted when DETECT found ops signals (`signals_found: true`) **or** when
109+
the maintainer answered the GATE's open question by pointing at a bespoke deployer the signal list
110+
missed (§6). The
105111
load-bearing rule still bites: a deploy-secret path, a production/branch deploy gate, or a
106112
fail-closed CI check is **load-bearing** (`security` / `fail-closed`) and blocks on unsupported
107113
per the two-tier gate (§4) like any other load-bearing claim. `type` is free-form to
@@ -154,13 +160,25 @@ Also flag drift from `python3 scripts/compound-v-onboard.py staleness --repo .`
154160
Present, for approval, a **per-artifact AND per-section diff**, alongside confidence/staleness and
155161
the diagnosis. **Nothing is written before explicit approval** — no auto-apply, ever.
156162

157-
When `detect-ops` reported `present: true`, present `operations.md` as its **own explicit
158-
per-artifact confirm**, framed with the detected inventory: *"DevOps/deployment tooling detected —
159-
`<ci_cd / containers / deploy counts + paths>` — include `operations.md`?"* Declining drops the doc
160-
and writes nothing for it; this is the *"ask the user whether to take DevOps into account"* decision.
163+
**Operations coverage — surface it in BOTH branches; the detector is an accelerator, never a verdict:**
164+
165+
- **`signals_found: true`** — present `operations.md` as its **own explicit per-artifact confirm**,
166+
framed with the detected inventory: *"DevOps/deployment tooling detected —
167+
`<ci_cd / containers / deploy counts + paths>` — include `operations.md`?"* Declining drops the doc
168+
and writes nothing for it; this is the *"ask the user whether to take DevOps into account"* decision.
169+
- **`signals_found: false`** — do **not** conclude "no ops layer" and skip silently. The fixed signal
170+
list is blind to bespoke deployers, so surface the gap as an **open question**, not a verdict:
171+
*"No explicit ops files detected. If this project does deploy, point me at it (e.g. a hand-rolled
172+
`ship.sh`, a Makefile target, an internal runbook) and I'll document it in `operations.md`."* If the
173+
maintainer names something, EXTRACT reads-then-cites it into `operations.md`; if they confirm there
174+
is genuinely nothing, it is skipped — but the **human**, not the heuristic, made that call.
175+
161176
A fully autonomous / unattended run (auto-approve / `--permission-mode dontAsk` — today the headless
162-
marathon, or any future autonomous onboarding cycle) approves it like every other artifact, so ops
163-
is taken into account **without asking** — no separate code path is needed.
177+
marathon, or any future autonomous onboarding cycle) auto-approves the `signals_found: true` doc like
178+
every other artifact (ops taken into account **without asking** — no separate code path). With
179+
`signals_found: false` and no human to answer, it records **"ops coverage: no signals found (not
180+
confirmed absent)"** rather than asserting there is no ops layer — the open question survives to the
181+
next interactive pass instead of being silently resolved as "none".
164182

165183
Critically, the diff **expands every `@import` target** (to the 4-hop limit). `@import` is **not a
166184
token optimization** — an imported file loads in **full** at launch; only path-scoped rules and
@@ -273,15 +291,18 @@ explore → ask → propose → write.
273291
green). Therefore the gate states **"token pairs pass WCAG AA structurally"****never
274292
"accessible."** Document the linter's blindness in the gate output, and flag multi-theme / arbitrary
275293
Tailwind class colors as "partial capture" rather than implying full coverage.
276-
- **`operations.md`** (`docs/superpowers/architecture/`, ops repos) is generated **only when
277-
`detect-ops` reported `present: true`.** On a repo with no CI/CD, container, or deploy files it
278-
is **skipped** (verify this negative path on a non-ops dogfood, mirroring the DESIGN.md negative
279-
path). Read-then-cite from the real workflow / Docker / compose / Terraform / deploy files
280-
DETECT inventoried — never from the model's prior. Cover: container topology (services, ports,
281-
volumes), CI/CD stages (build → test → deploy triggers and branch/environment gates), the deploy
282-
target + production domain, and runbook pointers. **No credential is ever extracted into the
283-
doc** — the blocking `scan-output` gate (§7) refuses a generated file that contains one, and a
284-
deploy-secret reference is documented by *path*, not value.
294+
- **`operations.md`** (`docs/superpowers/architecture/`, ops repos) is generated when `detect-ops`
295+
found signals (`signals_found: true`) **or** when the maintainer answered the GATE's open question
296+
(§6) by naming a bespoke deployer the signal list missed. It is skipped **only** when
297+
`signals_found: false` **and** the human confirmed there is genuinely nothing — never silently on an
298+
empty scan alone (that "no signals found ≠ no ops layer" distinction is the whole point; verify both
299+
the found path and the open-question path on dogfoods). Read-then-cite from the real workflow /
300+
Docker / compose / Terraform / deploy files DETECT inventoried (or the file the maintainer pointed
301+
at) — never from the model's prior. Cover: container topology (services, ports, volumes), CI/CD
302+
stages (build → test → deploy triggers and branch/environment gates), the deploy target +
303+
production domain, and runbook pointers. **No credential is ever extracted into the doc** — the
304+
blocking `scan-output` gate (§7) refuses a generated file that contains one, and a deploy-secret
305+
reference is documented by *path*, not value.
285306

286307
---
287308

0 commit comments

Comments
 (0)