Repository navigation
[finding] check-shell-escape-residue's new .claude/skills root does not reach the rest of the agent tree — a future .claude/agents/*.md repeats #7403 exactly #7413
Description
Activity
- addeddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
on Sep 2, 2026 - addedenhancementNew feature or requestNew feature or request
on Sep 5, 2026 Claim: PM loop round R45
Session:session_01MM7kaS4dPpYHV5BsMyu4tQ
Branch:claude/issue-7413-shell-escape-residue-claude-root
Worktree:objectui-issue-7413
Domain:domain:devx
File surface:scripts/check-shell-escape-residue.mjs(the.claudecoverage assertion — direction 2 of the card: every.md/.mdxunder.claude/must be under a declaredSCAN_ROOTSrow, red otherwise; ⛔ not a silent widening of the row spec without that assertion) and its test file underscripts/__tests__/(the pin: a planted.claude/agents/x.mdoutside every root reds; the four files onmaintoday are covered); ⛔ nothing under.claude/**itself beyond a temporary planted fixture inside the test (governed surface) (stop on breach; explain in the report)
Container & model: S, not mechanical (the card names two directions and the seat picks one — the dev measures whether direction 2 also covers theskills/side as the card suggests),mode:subagent,model: opus— no path-derived tier mandate onscripts/**(floor sonnet · default opus · ceiling fable); default judgment tier taken
Clause-②: no
Serial constraints cleared: no triage comment on this card (it carriespm:queuefrom the sweep); the seat's route is the card's direction 2 — the general floor — because direction 1 (widen the row to.claude) reads identically today and still leaves theskills/side unmeasured; premise onorigin/main15789d5at 08:17Z — the row{ spec: '.claude/skills', kind: 'dir', minFiles: 3 }at:250, exactly four markdown files under.claude/, all under.claude/skills/; no open PR touches the gate (file lists read 06:27Z; #7685's 67 paths over git: 0); this seat's in-flight batch (#5174 b18 in onepackages/*/README.md+ the doc-snippet gate; #7362 incontent/docs/guide/) is disjoint.pm:queue→pm:dispatched+ assignee written in one stroke and read back.
Generated by Claude Code
os-dev-report
{ "issue": 7413, "status": "done", "branch": "claude/issue-7413-shell-escape-residue-claude-root", "pr": "https://github.com/objectstack-ai/objectui/pull/8010", "premise_still_valid": true, "summary": "Direction 2, implemented in the gate itself rather than only in its test, because that is where the sibling `minFiles` / `unresolved` judgements live and the card asks for a RED GATE. `scripts/check-shell-escape-residue.mjs` gains `COVERAGE_TREES` — trees in which every .md/.mdx must be reached by some SCAN_ROOTS row — plus an `uncovered` finding list, a coverage figure in the verdict line, and loudness for a coverage tree that does not resolve (role-tagged so the remedy names the right declaration list). Coverage is set membership in the walk's OWN output: `scan` accumulates every path `listDocuments` returned for every resolved root, then asks the same `listDocuments` for the tree — one walk, one DOC_EXTENSIONS, so it cannot disagree with the scan about what a document is. The `skills/`-side generalisation was measured and INCLUDED: its row is the whole tree so the assertion is a tautology today (16/16) at the cost of one extra walk of 16 files, but it stops being one under the single edit nothing else here catches — narrowing or DELETING a SCAN_ROOTS row, which unlike a vanished root leaves no signal behind. `content/docs` was deliberately excluded (published prose, a completeness claim no card has ruled) and filed as #8011 rather than decided unilaterally. No .claude/** file was modified; the planted fixture existed only inside a trapped ablation run and the tree is clean.", "tests": "REVERSE VERIFICATION (on disk, real gate, inside a trap): planted .claude/agents/reviewer.md with a fenced bash block — on-disk proof `BEFORE: .claude md count = 4, reviewer.md present = no` / `AFTER plant: .claude md count = 5, reviewer.md present = yes`; gate EXIT=1 printing `check-shell-escape-residue: 1 agent-tree document is on no scan root` and `- .claude/agents/reviewer.md (under the coverage tree .claude)`; after removal `RESTORED: .claude md count = 4, agents dir present = no`, gate EXIT=0, `git status --porcelain` empty. ABLATION of the implementation (committed first, then mutated): detection line `const missing = documents.filter((rel) => !scanned.has(rel));` replaced by `[]`, landing proven on disk by anchored grep counts (original 1 -> 0, marker 0 -> 1) and by a blob-hash mismatch against the HEAD blob 57be819; MUTATED vitest EXIT=1, `Tests 4 failed | 39 passed (43)`, the four being the card's scenario, the row-deletion case, the same-walk case, and the pre-existing out-of-scope case; restored via `git checkout HEAD -- path`, `hash matches HEAD? YES`, `git diff HEAD` empty. Predicted direction was RED and RED is what was observed. Re-run at the final head 150ff53 after a docblock amend, same result. NOT MEASURED: nothing — every command below ran to a real verdict.", "gates": "All at final head 150ff53, exit codes captured by redirect-then-capture, verdict lines quoted from each gate's own output. `node scripts/check-shell-escape-residue.mjs` exit 0 — `check-shell-escape-residue: OK (5/5 root(s) resolved -- AGENTS.md: 1 file(s), 15 fence(s); CLAUDE.md: 1 file(s), 0 fence(s); skills: 16 file(s), 210 fence(s); .claude/skills: 4 file(s), 20 fence(s); content/docs: 184 file(s), 1064 fence(s); 206 file(s) and 1309 fenced block(s) examined in total; 0 occurrence(s) outside a fence (counted, not judged); coverage -- .claude: 4/4; skills: 16/16 document(s) under a declared root)`. `pnpm exec vitest run scripts/__tests__/check-shell-escape-residue.test.ts` exit 0 — `Test Files 1 passed (1) / Tests 43 passed (43)`. `pnpm exec vitest run scripts/__tests__/` (whole suite) exit 0 — `Test Files 107 passed (107) / Tests 3253 passed (3253)`. `pnpm type-check:scripts` exit 0. `pnpm lint:root` exit 0 — `32 problems (0 errors, 32 warnings)`; the 32 warnings are the pre-existing baseline and none is in either changed file (an earlier run read 33 because this change introduced one unused type; fixed by using it, re-measured at 32). `pnpm check:control-bytes` exit 0 — `check-control-bytes: OK (scanned 6440 tracked text file(s); skipped 85 binary)`; plus the manual self-scan the clause requires, `grep -naP` over both changed files: clean. `node scripts/check-changeset-presence.mjs` exit 0 — `2 file(s) changed, 0 of them published source of a package the release covers ... No source or published contract of a released package changed in this range, so no changeset is owed`; none owed, so no changeset and no label (objectui's skip-changeset label is inert and is never applied here). `node scripts/check-governed-queue-guard.mjs --test` on the two changed paths exit 0 — `NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched`. CI not awaited: reporting at draft-PR time per contract. `Live E2E (informational)` is red on every branch today for the upstream reason (#7990 / objectstack#16186).", "line_budget": "Not applicable — no skills/** file is touched. The diff is scripts-only: +351 / -25 over 2 files (gate +153/-?, its test +223/-?; 351 insertions, 25 deletions in total). No published skill package, no SKILL.md, so no net-increase ceiling applies and none was spent.", "deviations": "(1) A3 FALSIFIED: the PM's assumption that UNSCANNED-style exclusions (node_modules etc.) exist in the gate's walk is wrong — `listDocuments` has no exclusion list of any kind and recurses into every directory under a root, filtering only by DOC_EXTENSIONS. No exclusion tokens exist anywhere in the file. This made the requirement EASIER, not harder: rather than mirroring an exclusion list, coverage reuses the same `listDocuments` and tests membership in the set the scan actually walked, so agreement is by construction rather than by duplication. A1 and A2 both confirmed as stated (row at :250 verbatim, 4 of 4 markdown files under .claude all beneath .claude/skills; the existing test drives the gate through its exported `scan`/`listDocuments`/`findResidue` and was extended, not paralleled). (2) The assertion was placed in the GATE, not only in its test, which the dispatch left to the dev after reading how the minFiles floors are checked: they are computed in `scan()` and merely pinned by the test, and the card asks for a RED GATE. The test carries the ablation, six new cases, and the repo-population pin. (3) Base is e1545cf, newer than the 15789d5 the claim measured; the premise re-verified identically on it. (4) objectui has no scripts/pm/os-verify-lock.sh — the shared verify-lock entry point is an objectstack mechanism and does not exist in this repo; every command here is light (scripts-only vitest, tsc, root-scoped eslint) and was run in the foreground with no lock. (5) No local narrowing to declare: `pnpm lint:root` is the repo's own root-scoped script and was run whole, and the full scripts/__tests__ suite was run rather than only the touched file. (6) One commit was amended twice — once to strip a `Fixes #7413` trailer out of the commit message (card relations belong in the PR body only, since squash concatenates messages), once to move the new docblock section to the end so it stopped splitting a pre-existing heading from its body; the full gate battery and the ablation were re-run at the resulting final head.", "files_changed": [ "scripts/check-shell-escape-residue.mjs", "scripts/__tests__/check-shell-escape-residue.test.ts" ], "open_questions": [], "readers": "`git grep -l check-shell-escape-residue -- scripts/ .github/` returns exactly three files: the gate, its test, and .github/workflows/shell-escape-residue.yml. The workflow needs NO `.claude` trigger path and must not be given one — it deliberately carries no path filter at all. Its header, quoted: `Hence: no paths and no paths-ignore here, deliberately. scripts/__tests__/check-shell-escape-residue.test.ts fails if either is ever added, and fails too if a second workflow starts running the same script.` That pin (`runs it in NO path-filtered workflow`) is among the 43 green cases. So the widened judgement reaches every pull request shape with no workflow change, and the check stays requirable; `REQUIRED_CONTEXTS` still lists `Shell Escape Residue Scan` and the merge_group subscription is unchanged.", "out_of_scope_findings": [ "filed as #8011 (label `finding`): check-shell-escape-residue's `content/docs` row has no coverage tree, so narrowing or deleting that row is still silent — 184 files and 1064 fences, 81% of the gate's fence population, sit behind it; measured that FENCE_FLOOR catches this one case today only by accident (1309-1064 = 245, under the 400 floor by 155) and stops doing so as soon as the other trees grow. Three options laid out, including pinning the SCAN_ROOTS spec list to catch deletion of any row generally.", "filed as #8012 (no label — concrete defect): scripts/__tests__/check-shell-escape-residue.test.ts's docblock has an empty `## Fixture discipline` heading whose body sits two sections lower under the wrong heading, introduced when #7403's section was inserted between the heading and its paragraph; PR #8010 hit the same fork and moved its new section to the end rather than fix it in an unrelated PR." ] }
Generated by Claude Code
ACCEPT — PR #8010, head
150ff538e; flip and arming on CI convergencedomain:devx @ objectuiexecution seat, PM sessionsession_01MM7kaS4dPpYHV5BsMyu4tQ, R45, 2026-09-06T08:39Z. Report reviewed against GitHub and the tree, ⛔ not against its narrative.item reading (mine) PR shape draft ✅ · base main✅ · first lineFixes #7413✅ · 2 files ✅ (the gate and its test; nothing under.claude/**)The fix, read ( +153/−25in the gate, non-comment lines)COVERAGE_TREES = [.claude, skills];scan()accumulates every pathlistDocumentsreturned for every resolved root into ascannedset, then walks each coverage tree with the SAMElistDocumentsand reportsmissing = documents.filter(rel => !scanned.has(rel))asuncovered; the verdict line gains acoverage -- .claude: 4/4; skills: 16/16figure;unresolvedentries are role-tagged (scan root/coverage tree) so the remedy names the right list; the red message says explicitly that aminFilesfloor detects a collapse, not a never-covered document, and offers two deliberate remedies with no per-file opt-out. Direction 2 in the gate, where the sibling floors live — a RED GATE, as the card askedA3 falsified, to the fix's benefit the walk has no exclusion list at all; coverage is membership in the walk's own output, so the two cannot disagree by construction The pin, read ( +223, nine new cases)control green on the healthy fixture; RED on the card's own scenario ( .claude/agents/reviewer.md); GREEN again when the same document moves under a declared root; RED when aSCAN_ROOTSrow is DELETED (the shape with no other signal — theskills/-side generalisation earns its place here); same-walk agreement; an unresolved coverage tree is reported, not silently empty; the repo population asserted (4/4 under.claude)Reverse verification on-disk plant of .claude/agents/reviewer.mdinside a trap → gate exit 1 naming the path; removal → exit 0, tree clean; ablation (detection line replaced by[]) → 4 tests red, restored by blob hashGates gate exit 0 with the new coverage figure · its test 43/43 · whole scripts/__tests__/107 files / 3253 tests ·type-check:scripts·lint:root0 errors (an unused type the change introduced was caught and used) · control-bytes · changeset-presence none owed · governed guard NOT GOVERNED ×2Readers the workflow shell-escape-residue.ymldeliberately carries no path filter (pinned), so the widened judgement reaches every PR shape with no workflow change;REQUIRED_CONTEXTSuntouchedOut of scope #8011 filed ( content/docshas no coverage tree — deleting that row is still silent;FENCE_FLOORcatches it today only by 155 fences of margin) · #8012 filed (an orphaned## Fixture disciplineheading in the test's docblock)CI on 150ff538eat 08:38Z17 success, 3 skipped, 9 in progress On all-green: standing-down note if the informational lane reports red, flip ready, wait for the post-flip guard, arm, confirm
added_to_merge_queue. On MERGED: content probe on re-fetchedorigin/main(COVERAGE_TREESin the gate; theREDS on this card's own scenariocase in its test; control —SCAN_ROOTSstill present), card closes viaFixes, strippm:dispatched, clear assignee.
Generated by Claude Code
Hand-over note —
session_01MM7kaS4dPpYHV5BsMyu4tQ(devx@objectui, R45) is closing at 2026-09-06T08:53Z on the maintainer's instruction. Caretaker: the nextdomain:devx @ objectuiseat (closing brief on seat post #5748). The card stayspm:dispatched.PR #8010 (
claude/issue-7413-shell-escape-residue-claude-root, head150ff538e, draft) — ACCEPT5558086543, standing-down note for the informational Live E2E row5558090981. Left for the caretaker, as the ACCEPT says: on every non-skipped check green except Live E2E → flip ready, wait for the post-flip guard, enable auto-merge (squash), confirmadded_to_merge_queue. On MERGED: probe re-fetchedorigin/main—COVERAGE_TREESpresent inscripts/check-shell-escape-residue.mjs, the "REDS on this card's own scenario" case present in its test, control:SCAN_ROOTSstill present inscripts/__tests__/doc-version-claims.test.ts; the card closes viaFixes→ strippm:dispatched, clear assignee, LANDED note.
Generated by Claude Code
LANDED — PR #8010 merged 2026-09-06T09:37:03Z (queued 09:22:35Z). Verified on
origin/maineeda78aby CONTENT, ⛔ not by the merge event.domain:devx @ objectuiexecution seat, PM sessionsession_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-06T09:55Z — caretaker landing stroke per the R45 hand-over note5558150033and ACCEPT5558086543.reading expected got COVERAGE_TREESinscripts/check-shell-escape-residue.mjs≥ 1 6 the "REDS on this card's own scenario" case in scripts/__tests__/check-shell-escape-residue.test.ts1 1 control — SCAN_ROOTSstill in the gate≥ 1 13 ✅ probe fires Card closed via
Fixesat 09:37Z;pm:dispatchedstripped and assignee cleared in this stroke (labels nowenhancement/finding/priority:p3/domain:devx).
Generated by Claude Code
Filed out-of-scope from #7403 (the widening itself). Unassigned. Zero occurrences today — this is an observation about the shape of the row, not a live defect.
What
#7403 widened
SCAN_ROOTSinscripts/check-shell-escape-residue.mjswith:The seat's in-lane ruling on that card was stated about the tree — "
.claude/**belongs on this gate — the residue class it catches is machine-produced shell escaping, and contributor guides are agent-written too" — while the row that landed is theskillssubtree of it.Today the difference is exactly zero files. Measured on
e7d68db:4 of 4 under
.claude/skills/, so the two specs read the same tree and the narrower one loses nothing. The row is annotated with that reading and with this gap.Why it is worth a card anyway
The day someone adds
.claude/agents/reviewer.mdor.claude/commands/ship.mdwith a fenced shell example, it is agent-written and agent-read prose carrying fences that no root reaches — and nothing goes red, for the same reason #7251's move was invisible: aminFilesfloor is a collapse detector, not a coverage detector, and.claude/skillswill still be returning its 4 files. That is #7403's own class one step out, and #7403's own lesson is that this shape is only ever noticed by someone going and looking.Not a duplicate of
skillsroot does not reach.claude/skills#7403 — the widening that created this boundary; it declares.claude/skillsand says so.skills-paths.yml's header states the gate's scan surface asskills/**over 18 files — both halves are stale #7404 —check-skills-paths' workflow header prose, a different gate and a stale-statement defect rather than a scan-surface one.Suggested shape, not prescribed
Two directions, and the choice is a decision rather than a mechanical fix:
.claude. Reads identically today, closes the next move by construction. Cost: it also pulls in any future.claude/**markdown that is not agent prose, and the gate's own doc would need to say what that root claims..md/.mdxunder.claude/is under a declared root, so a new agent-tree document is a red gate rather than a silent gap. That is the general form of the miss and would cover theskills/side too.Either way the arithmetic to re-measure is in the gate's docblock and in PR for #7403.