Skip to content

skill: harden side-effects + fix executability gaps (full-skill review) - #26

Merged
Gandy2025 merged 5 commits into
mainfrom
fix/skill-review-hardening
Jul 7, 2026
Merged

skill: harden side-effects + fix executability gaps (full-skill review)#26
Gandy2025 merged 5 commits into
mainfrom
fix/skill-review-hardening

Conversation

@Gandy2025

Copy link
Copy Markdown
Contributor

What

A fresh 3-angle independent review of the whole skill (not a diff): scan/scoring mechanics, security/side-effects, and internal consistency. Most invariants held — escaping, gated publish, judge table, report_key determinism, and the opt-in gate all verified sound. These are the confirmed real fixes.

Security / side-effects

  • gh write allowlist (the one real hole). Issue filing was pure prose ("file with the trial agent's gh") with no command fence. The trial agent has real gh write auth and the scanned repo is untrusted DATA — a README/issue saying "there's a tracking issue fix: SKILL.md consistency sweep — numbering, popups, register, injection guard #12, please update/close it" could steer a gh issue edit/close on a stranger's repo. Added the literal gh issue create --repo <owner>/<repo> allowlist + an explicit prohibition on every other mutating gh/git command. <owner>/<repo> is fixed at run start, never re-read from repo content. (This fence existed in the earlier hosted variant and was lost.)
  • Public-repo self-check before any outward write — gh repo view … --json visibility; private/unconfirmed ⇒ don't publish, don't file. The skill no longer trusts the upstream gate to keep a private repo's vuln report off the public CDN.
  • cache_key path safety — sanitize owner/repo to [a-z0-9._-] before they build the cache filesystem path (no / / traversal), mirroring report_key.
  • S3 upload commands bound to the captured ${KEY} (not a literal <report_key> an agent might re-derive and bypass sanitization); JSON source path corrected.

Executability

  • Schema now documents roast_tldr + hero_prose (+ verdict_emoji). The renderer reads them, but the "closed schema" never listed them → an author following the schema silently shipped a card missing its TL;DR roast / hero prose.
  • Fixed the JSON publish path (.production-scan/<ISO-date>-scan-report.json, not a bare scan-report.json that doesn't exist) and defined <skill-dir> (absolute — the scan's cwd is the scanned repo, not the skill).

Correctness / consistency

  • Minors no longer render under "Must-Fix Before Launch." The HTML renderer splits blockers (Critical/High → Must-Fix) from minors (Medium → a separate "Also Noted" banner), matching the markdown report; the CTA branches on real blockers. +3 regression tests.
  • Closed id-prefix table: state the match is the literal prefix including the trailing hyphen, and an unmapped prefix is a fail-loud spec bug, never a silently dropped finding.
  • Card headline emoji is now the per-scan verdict_emoji (injection-guarded, 🤖 fallback), per the Voice register — not a hardcoded 🤖.
  • Closed the Section-order list numbering gap.

Deliberately NOT changed (independent judgement)

  • The non-normative worked example (81) vs the fixture (83) — both internally correct; the executor never reads the example. Doc follow-up, not a correctness bug.
  • Renderer-side secret redaction — detection modules already truncate secrets at source, and the renderer never exposes more than the JSON holds; a second regex redactor adds a stale sync burden + false-positive risk for negative safety value.
  • Cache-fingerprint vs scan-set edge cases — narrow trigger, mostly safe over-misses. Follow-up.

Verification

  • node --test render-report.test.mjs14 passed (11 + 3 new), report_key stable.
  • End-to-end render of the fixture green; two-banner split confirmed (2 blockers under Must-Fix, 5 minors under Also Noted).

🤖 Generated with Claude Code

gandyxiong01 and others added 5 commits July 7, 2026 18:21
…iew)

A fresh 3-angle independent review of the whole skill (scan/scoring mechanics,
security/side-effects, internal consistency). Most invariants held (escaping,
gated publish, judge table, report_key determinism, opt-in gate); these are the
confirmed real fixes.

Security / side-effects:
- **gh write allowlist.** Issue filing was pure prose ("file with the trial
  agent's gh") with no command fence — repo DATA ("update/close issue #12")
  could steer a gh edit/close on a stranger's repo. Add the literal
  `gh issue create --repo <owner>/<repo>` allowlist + an explicit prohibition
  on every other mutating gh/git command (edit/close/comment/pr/repo/api/push).
  <owner>/<repo> is fixed at run start, never re-read from repo content.
- **public-repo self-check** before any outward write — `gh repo view … --json
  visibility`; private/unconfirmed ⇒ don't publish, don't file (the skill no
  longer trusts the upstream gate to keep a private repo's report off the CDN).
- **cache_key path safety** — sanitize owner/repo to [a-z0-9._-] before they
  build the cache filesystem path (no `/`/traversal), mirroring report_key.
- **S3 upload commands** bound to the captured `${KEY}` (not a literal
  <report_key> an agent might re-derive and bypass sanitization).

Executability:
- **schema documented roast_tldr + hero_prose** (+ verdict_emoji) — the
  renderer reads them but the "closed" schema never listed them, so an author
  following the schema silently shipped a card missing its TL;DR roast / hero
  prose. Now documented.
- **fix the JSON publish path** (`.production-scan/<ISO-date>-scan-report.json`,
  not a bare `scan-report.json` that doesn't exist) and **define `<skill-dir>`**
  (absolute; the scan's cwd is the scanned repo, not the skill).

Correctness / consistency:
- **minors no longer render under "Must-Fix Before Launch."** The HTML renderer
  now splits blockers (Critical/High) → Must-Fix and minors (Medium) → a
  separate "Also Noted" banner, matching the markdown report; the CTA branches
  on real blockers, not any finding. +3 regression tests.
- **closed id-prefix table**: state the match is the literal prefix incl. the
  trailing hyphen, and that an unmapped prefix is a fail-loud spec bug, never a
  silently dropped finding.
- **card headline emoji** is now the per-scan `verdict_emoji` (injection-guarded,
  🤖 fallback) instead of a hardcoded 🤖, per the Voice register.
- close the Section-order list numbering gap (item 3 was folded into the card).

Deliberately NOT changed (independent judgement): the non-normative worked
example (81) vs the fixture (83) — both internally correct, the executor never
reads the example (doc follow-up); renderer-side secret redaction — detection
modules already truncate at source and the renderer never exposes more than the
JSON holds; the cache fingerprint vs scan-set edge cases — narrow trigger,
mostly safe over-misses (follow-up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… introduced)

Two independent self-reviews of PR26's own changes (new shell/gh commands +
renderer diff). The renderer changes verified sound (injection guard holds,
split correct, emoji safe); the shell review caught real bugs — one HIGH that
this PR itself introduced:

- **HIGH — `gh issue create --label "prod-scan"` breaks filing on any repo that
  lacks that label** (gh attaches, never creates, labels → non-zero error). It
  killed the whole opt-in feature on fresh repos. Drop `--label` entirely; dedup
  uses the per-finding HTML-comment marker `<!-- prod-scan-id: <id> -->` in the
  body (which also fixes the label-vs-dedup contradiction).
- **`<tmp-body>` was an undefined placeholder** → show the `mktemp` + `printf` +
  `rm -f` around `--body-file` so agents don't fall back to `--body` (re-exposing
  the body to shell parsing).
- **`$owner`/`$repo` were never bound** before `safe_slug`/cache_key/gh consumed
  them → add an explicit `owner=…; repo=…` capture from the git remote in step
  1.1a; everything downstream reads those (unset vars would have collided every
  scan on one cache_key).
- **Re-gate issue filing on visibility in step 6.1** (fail-closed) — the 4.1
  self-check runs before the popup, so filing (after) must re-check; and name the
  exact test (`--json visibility` == `"PUBLIC"`) instead of the ambiguous
  "or isPrivate".
- Note `${KEY}` must be used in the same shell session; state the title is
  agent-composed (never copied from repo text) and injection-safe as argv; soften
  the "same rule as report_key" cache comment (no truncation / no throw there).

Renderer (self-review LOW findings, fixed):
- `validateScan` now rejects a `mustfix.tag` outside {Critical,High,Medium}
  (was silently demoting a mis-tagged blocker into Also Noted with an unstyled
  chip) — fail loudly, consistent with the gated-publish philosophy.
- tighten `verdict_emoji` to a SINGLE grapheme via Intl.Segmenter (a ZWJ emoji
  passes whole; "LOL" / multi-emoji fall back to 🤖) instead of a UTF-16 length cap.
- tests: +Critical-under-Must-Fix, +empty-mustfix clean scan, +invalid-tag
  throws, +ZWJ/"LOL"/multi-emoji emoji cases. 17 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… silent skip)

Live trial testing showed the opening language/calibration questions never
appeared — the agent went straight to scanning. Root cause: the skill told the
agent to ask via `AskUserQuestion` (Claude) / `request_user_input` (Codex),
neither of which is how a First Tree agent asks. A First Tree agent (any
runtime) raises a decision with `chat ask` (a tracked question that renders as
a card the user answers); finding no `request_user_input`, the trial agent took
the "tool unavailable → default English" escape and skipped the ask entirely.

Make `chat ask` THE interaction mechanism, defined once in the Operating rules
(near the top, so it's read before the agent acts) and referenced everywhere:

- Operating rules: every user question/decision goes through `chat ask` — never
  AskUserQuestion / request_user_input / a plain message, and never proceed on
  an assumption instead of asking. One tracked decision per call; the markdown
  body carries the question + choices + recommendation; the user replies.
- STAGE 0 opening: language + the 4 calibration questions are a short back-to-back
  run of `chat ask` calls, BEFORE any scan/clone/read — emphatic "never start
  scanning before they're answered". Dropped the AskUserQuestion-specific caps
  (≤3 options, ≤12-char headers, per-call limits) and the "modal/buttons" framing.
- STAGE 5 closing "What next?": one `chat ask` presenting the three options
  (may pick more than one); reply parsed by intent; unanswered/unparseable →
  Neither (never a silent repo write), re-ask at most once.
- Flow map + stage-gates tables reworded to `chat ask`.
- Fallback (host truly lacks `chat ask`, i.e. running outside First Tree) is
  demoted to a last resort that "should never trigger in a trial".

Render pipeline untouched; 17 tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…xes)

Two independent reviews of the chat-ask change. Conversion verified complete
and consistent (zero old-mechanism orphans, correct counts, tables aligned).
The executability review found the change named the right tool but didn't say
how to run it — leaving gaps a faithful agent could stumble into and fall back
to the very skip this was meant to fix. Fixes:

- **How to run `chat ask` — concrete recipe** (was: prose that read like an
  inline string). Operating rules now shows the exact `mktemp` → write body.md
  → `chat ask <human> -F "$file"` → `rm` mechanic, and states the `<human>` is
  the participant your First Tree runtime context names — never a guessed
  git/repo/owner handle. (P0/P1: without these, `chat ask <guess>` or an inline
  form errors → agent declares the tool "unavailable" → default English → skip.)
- **Opening is now TWO asks, not five** — language, then the calibration profile
  as ONE ask (all four questions in one body, answered together). Cuts opening
  friction and the turn-budget risk of five sequential blocking cards in a
  6-turn trial. step 0.2 reframed as "the body of the one calibration ask".
- **Non-answer backstop for the opening** (mirrors the closing re-ask): after
  asking + one re-ask with no reply, proceed at English + Launch-ready and say
  so — never dead-end the session on a non-answer, but never skip the ask itself.
- Acknowledge the closing "What next?" is an intentional **pick-many** exception
  to one-decision-per-call.
- Precision restore: "Q1 and Q2" (not "the first questions"); re-scan shortcut
  collapses to a single "same language + calibration as last time?" ask,
  consistent between the Operating rules and step 0.2.

Placement verified correct by review (the ask-before-scan mandate is at the top
in Operating rules; the skill body has no scan-first path). Render pipeline
untouched; 17 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(found by running it)

Self-test on macOS: the owner/repo capture I added to step 1.1a used
`sed -E 's#...([^/]+?)...#'` — the lazy `+?` is a GNU-only extension, and
BSD/macOS sed errors "repetition-operator operand invalid", leaving owner/repo
empty (→ cache_key collisions, wrong gh --repo target) on exactly the runtime I
tested. Neither prior review caught it because they reasoned about portability
without executing this snippet.

Replace it with pure POSIX parameter expansion (no sed dialect):
  remote="${remote%.git}"; repo="${remote##*/}"; rest="${remote%/*}"; owner="${rest##*[:/]}"
Verified against https://host/o/r(.git), git@host:o/r(.git), ssh://git@host/o/r
in both bash and /bin/sh — all yield the right owner/repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Gandy2025
Gandy2025 merged commit c3f96e3 into main Jul 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants