skill: harden side-effects + fix executability gaps (full-skill review) - #26
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_keydeterminism, and the opt-in gate all verified sound. These are the confirmed real fixes.Security / side-effects
ghwrite allowlist (the one real hole). Issue filing was pure prose ("file with the trial agent'sgh") with no command fence. The trial agent has realghwrite 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 agh issue edit/closeon a stranger's repo. Added the literalgh issue create --repo <owner>/<repo>allowlist + an explicit prohibition on every other mutatinggh/gitcommand.<owner>/<repo>is fixed at run start, never re-read from repo content. (This fence existed in the earlier hosted variant and was lost.)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_keypath safety — sanitizeowner/repoto[a-z0-9._-]before they build the cache filesystem path (no// traversal), mirroringreport_key.${KEY}(not a literal<report_key>an agent might re-derive and bypass sanitization); JSON source path corrected.Executability
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..production-scan/<ISO-date>-scan-report.json, not a barescan-report.jsonthat doesn't exist) and defined<skill-dir>(absolute — the scan's cwd is the scanned repo, not the skill).Correctness / consistency
verdict_emoji(injection-guarded, 🤖 fallback), per the Voice register — not a hardcoded 🤖.Deliberately NOT changed (independent judgement)
Verification
node --test render-report.test.mjs— 14 passed (11 + 3 new),report_keystable.🤖 Generated with Claude Code