feat: adapter track (onboarding generator, gitea/forgejo, linear) + Lane C spin-off closures - #3056
Conversation
…7.0) Adds /work-items:onboard-adapter — the tail half of the hybrid adapter model. Bundled adapters cover the majors; this skill walks a consumer from "my tracker is not supported" to an adapter that lives in their repo, resolved consumer-local-first with no fork and no vendored engine. The deterministic half is scripts/generate-adapter.sh; the judgement (which verbs a provider can honestly support, what its fields mean, what a live instance returns) stays outside the script, and the adapter spec is the whole handoff between them. The generated security skeleton carries the bundled jira adapter's guards and proves them: the generated common.test.sh is real and passing at generation time, asserting the credential is absent from argv and present in the stdin config, that HTTPS is enforced by curl itself, that redirects are not followed, and that every credential-egress guard refuses what it should. The generator refuses an incoherent spec rather than emitting a manifest that lies, and stamps the manifest with the SEAM's contract version rather than the spec's, so a generated adapter cannot be born already skewed from the engine that will dispatch it. Two seam fixes this needed: - The dispatcher now exports WIT_SEAM_LIB_DIR, so a consumer-local adapter can source the libs of the engine dispatching it instead of a vendored copy the consumer never made. - run-conformance.sh resolves bindings consumer-local-first, so a generated binding is reachable at all; the binding name is constrained before it is interpolated into a path. Refs #2950. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
…dogfood (0.38.0) The first adapter produced BY /work-items:onboard-adapter rather than hand-written, which was the point of #2952: it is the run that tests the generator. Gitea/Forgejo is self-hostable and free, so it serves the no-paid-tool case for solo developers. Honest gating over convenient gating. sub_items is false because Gitea's issue has no parent field at all. leases is false because whether Gitea arbitrates concurrent assignment cannot be settled without a live instance and two identities, and an emulated lease over last-write-wins loses races silently — worse than not having one. Both are recorded with what would settle them. Every provider divergence was verified against the Gitea source rather than assumed from GitHub's API: a pull request IS an issue and is dropped from list-items; create-item takes label IDs, not names, and refuses an unknown name rather than dropping it; blocked_by_count costs one request per item because the issue carries no dependency data; and POST /issues/{index}/dependencies makes the URL issue depend on the BODY issue, so using the sibling /blocks endpoint would invert every edge. Findings the dogfood produced, fixed in the generator: - limits values may now be null — "supported, no provider-enforced ceiling", distinct from 0 ("unsupported"). Gitea caps dependencies at nothing, and without null a ceiling-free provider had to invent a number callers would then branch on. - Generated adapters now ship a capabilities.test.sh whose load-bearing case is that the manifest agrees with the filesystem. - A substitution value carrying a placeholder reached generated output verbatim; values now interpolate directly, with a regression case grepping every generated file under both host postures. Offline coverage is complete and passing: the generated guards, the manifest checks, and a mocked-transport suite per implemented verb. A live conformance pass is NOT run and is recorded as deferred — no Gitea or Forgejo instance is reachable from this environment. Refs #2952. Refs #2950. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
Reads, writes, the claim/renew/reclaim lease protocol, native sub-items, and dependency edges — so unlike gitea it IS a coordination surface and /work-items:work can claim on it. Issue numbering lives outside the repository, so GitHub's shared PR/issue numbering never bites. Auth posture settled explicitly, as #2946 asked: a personal API key, sent as the bare Authorization value and referenced by env-var name only. OAuth needs an interactive grant no unattended session can complete, so it is not the credential for a cloud agent. Host pinned to .linear.app. The lease carries ONE documented deviation from the contract's claim sequence. The contract detects a race at step 2 by re-reading the assignees; that depends on GitHub's assignee LIST, where both racers' assignments coexist. Linear's Issue.assignee is a SINGLE field — the second writer overwrites the first and then re-reads only itself, so a step-2 check would report "no race" to BOTH racers. Arbitration therefore rests on the lease comment ordering, which the contract already specifies as the same-login tiebreak. Linear's comment ids are unordered UUIDs, so lease_comment_id is minted from the comment's createdAt in epoch milliseconds (the local-markdown precedent), with same-millisecond ties broken on the comment UUID so the ordering stays total. A test asserts the tie resolves identically from both sides — without that, two racers in the same millisecond would each read themselves as earliest and both would claim. Other divergences, each verified against Linear's published GraphQL schema rather than assumed from GitHub's model: a GraphQL error arrives with HTTP 200, so the transport inspects errors before any caller sees data; state is classified on WorkflowState.type (stable) and never on .name (renameable per team); inverseRelations, not relations, is the blocked-by direction; create-item takes label IDs resolved from names; and the seam id is team-key + number, never the UUID. Fixed along the way: timestamp parsing assumed fractional seconds were present, which corrupted the whole-second form this adapter's own markers write. That is how reclaim's activity check silently saw no activity and would have released a lease whose holder was demonstrably still working. 367 offline cases pass, including the race, the tiebreak from both sides, and reclaim's revalidation window. A live-workspace conformance pass is NOT run and is recorded as deferred — no Linear workspace is reachable from this environment, and no test has run two genuinely concurrent sessions. Refs #2946. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
Both lanes shipped and closed but their Verdict cells still read OPEN, so the container's third acceptance criterion — no verdict left open — could not pass on a document that was already out of date rather than genuinely undecided. Lane F: ADOPTED via PR #2974 — the sixth route-away row in planning:draft-goal-condition, sending multi-window work to spec plus decomposed items, advisory in tone and carrying no folklore token figures. Lane Y: ADOPTED via PR #3010. The "Our surface" cell also said "undecided (session-flow / work-items / new)", which the design comment settled: a thin /work-items:ship router. Recorded with it: PR topology is per-container via the container's `Execution shape:` line rather than repo config, and the item/checkpoint/phase-boundary vocabulary landed in work-items/reference/execution-shape.md with the marketplace-wide glossary explicitly deferred. Verdicts are taken from each item's closing PR and design comment, not re-derived. Refs #2933. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
… exemptions shell-portability-lint flagged three `date -u -d` calls in the linear adapter. All three are genuine dual-dialect ladders with a BSD `-j -f` (or literal) fallback already in place, but the linter seeds ladder guards only for `readlink -f` and `stat -c` — `date` has no guard class yet, by design, so a correct ladder is indistinguishable from a bare GNU call and the per-site `portability-ok:` escape is the sanctioned resolution. Each was reviewed rather than blanket-suppressed. Worth recording why wit_linear_epoch's ladder is correct: the `case` above it strips fractional seconds off the timestamp specifically so the BSD rung's exact-match `-f` format can parse it — Linear returns both `…:00Z` and `…:00.500Z`, and BSD `date -f` fails on the fractional form. Refs #2946. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 15m 55s —— View job Security review complete
Scope: logic flaws, trust-boundary/authorization gaps, and injection surfaces only, per this repo's CI security-review lane ( CRITICAL —
|
|
Claude finished @kyle-sexton's task in 10m 25s —— View job Code review complete
This repo carries a Findings (posted inline)6 findings posted as inline comments, most severe first:
Each finding was independently re-verified by reading the actual code (not just the fanned-out review agent's claim) before posting. Not flagged (checked, found consistent)
Per the PR description, live conformance against a real Gitea/Linear instance has not been run in this environment — that's stated by the author as a known, deliberate gap, not something this review can independently verify either. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d0812c7c8f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude has reviewed this PR 5 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
…ec bit on generated-adapter scripts Two CI/review findings on the adapter track. Command injection in the generator (confirmed by exploit, not by reading). generate-adapter.sh validated api.scope_pattern only for anchoring and api.sample_scope only for matching it, then rendered both into single-quoted shell in the generated common.sh. A spec with scope_pattern ^';touch /tmp/PWNED;#'$ and the literal matching sample_scope passes both checks, and the emitted line readonly WIT_<P>_SCOPE_RE='^';touch /tmp/PWNED;#'$' is valid shell (bash -n clean) that runs the command when the file is sourced — which every generated verb does. Verified end to end: the file generated, and sourcing it with the seam lib reachable created the marker. The guard goes at render(), the one choke point every value passes through, rather than one validator per key: a single quote is the only character that can end a single-quoted context, so any key reaching one refuses it outright. Refuse rather than escape, per the doctrine common.sh.tmpl already states at its own scope guard — an escaping bug is silent, a rejection is loud. The QUOTED_CONTEXT_KEYS set was derived by grepping the templates for '…@@key@@…' rather than guessed, and it also covers the caller-supplied pairs. This subsumes the separately-reported display_name finding: "Bob's Tracker" would have broken the generated file's syntax outright, and is now rejected at the same choke point. Exec bit. 21 tracked shebang files were recorded 100644, which the hygiene lane's exec-bit check flags repo-wide. The rule is mechanical — shebang plus 100644 — and the split matches the github adapter's precedent exactly: common.sh and mock.sh carry no shebang (they are sourced) and stay 100644. Refs #2950, #2946, #2952. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Last security-reviewed head: |
|
Claude finished @kyle-sexton's task in 5m 49s —— View job Security review complete
Per Findings (posted inline)1 finding posted:
Already fixed, verified by reading the code (not re-reported)The PR's own last commit (
Not flagged as new findings (checked, found either safe-by-construction or out of scope)
|
…ths the quote guard missed The render()-level single-quote guard added in 764fc53 was incomplete, and a review finding is right about why: display_name is the one free-prose spec key that reaches all three dangerous context classes in the templates, and only one of them is a single-quoted string. - common.sh.tmpl:222 — single-quoted printf format. A quote breaks out. (Covered by the render() guard.) - conformance-binding.sh.tmpl:33 — a DOUBLE-quoted ${VAR:?…} expansion, where bash still performs command substitution. `X$(cmd)` executes with no quote to break, so the quote guard never fires. NOT covered. - Several `#` comment lines — a newline ends the comment and the remainder is live shell. NOT covered. So the constraint belongs where its neighbours already are, at validation: render()'s own comment says values are substituted literally so nothing downstream has to escape them, and every other spec field (provider, host_suffix, base_path, scope_pattern, auth_scheme, sample_*) is already anchored-regex or enum constrained for exactly that reason. display_name was simply missed. One anchored charset closes all three classes at once; a per-context escape would have to stay right in every template forever. Verified: "Bob's Tracker", "X$(touch …)", and an embedded newline are each rejected with a spec error, no command substitution executes, and real names ("Gitea/Forgejo", "Acme Tracker") still generate. The render() guard stays as defense in depth for the single-quoted class. Refs #2950. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
…as infra setup Two plugin-gate failures, both from the new skill and adapters not being reflected in generated/derived surfaces. docs/CATALOG.md was stale: the work-items entry still described "github and local-markdown adapters" while the plugin now ships jira, gitea, and linear too. Regenerated with scripts/generate-catalog.mjs, as the gate's own message says. The cheatsheet generator then had no classification for onboard-adapter. It is provisioning, not a dev-lifecycle action — the same class the `setup` name rule already excludes, and this skill's own description routes bundled providers to work-items/setup. It just is not named `setup`, so that rule does not reach it; hence a skill-level exclusion entry. An excluded skill must not also carry cheatsheet metadata, so metadata.summary comes off the frontmatter (category stays — different key, different consumer). Refs #2950, #2946, #2952. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 5m 18s —— View job Security review complete
Scope: logic flaws, trust-boundary/authorization gaps, and injection surfaces only, per this repo's CI security-review lane ( This is a re-review at HEAD CRITICAL finding from the prior pass — now fixed, independently re-verified
Confirmed fixed by reading the actual current source (not just trusting the commit message):
I traced every Still open — not addressed by the two fix commits (both were scoped only to the injection finding)IMPORTANT — Linear IMPORTANT — Linear IMPORTANT — Checked, no exploitable issue found (in addition to the prior pass's list)
Notes on environmentScript execution (running the generator or its test suite directly) wasn't approved in this run, so verification of the injection fix was done by static trace of the source rather than a live exploit re-run. Recommend widening |
…e claim Three independent review passes converged on this one, and it is real. reclaim.sh re-validated before mutating, but only against the lease it started with: revalidate() filters the re-read by the original HANDLE. A concurrent claimer does not renew that lease — it posts its OWN under a different handle, and wins its own arbitration without ever touching ours, precisely because ours is already expired and so never counts as competition. The handle filter therefore cannot see it, our lease is still present, un-superseded and expired, and every check passes. The unassign then compared against CURRENT_ASSIGNEE, read from an issue snapshot fetched before the activity round trip and both revalidate() calls — so it still named the old holder, matched, and cleared the rival's assignment. The item went back to the frontier while another session was working it, contradicting the invariant stated two lines above that code. Two changes, mirroring what claim.sh already does: - revalidate() also fails when ANY live lease exists on the item, whoever holds it — a live lease means this expired record is no longer the ownership state. - The assignee is re-fetched immediately before the comparison instead of reused from the pre-race snapshot (claim.sh:135 re-fetches for exactly this reason). Regression test seeds a rival lease under a different handle during the revalidation window. Confirmed discriminating: with the fix reverted it fails with `reclaimed` true (the rival is stripped); with the fix it is false and no issueUpdate is issued at all. Refs #2946. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 4m 9s —— View job Code review complete
Per
Findings (posted inline)2 new findings, both Important:
Verified correct (not flagged)
Not re-reported (already flagged in an earlier pass on this PR, still open, untouched by these 4 commits)To avoid duplicate noise, these previously-posted findings weren't re-posted — they still stand at this head:
Environment noteCould not execute the adapter/generator test suites directly in this run (test-runner |
…d, and stop swallowing failed test output Two review findings, plus the diagnostic gap that has made one CI failure on this PR unreproducible. Test coverage. The previous commit fixed the host-pin domain boundary in three places but tested one. The template scaffold matters most: a generated adapter would ship the FIXED helper with a test suite unable to catch a regression in it — the same "a future edit silently reopens it" argument that commit made about display_name, not applied to its own change. Linear needs its own copy regardless, since it is hand-built and inherits nothing from the template. Verified the template port the only way that proves it: generated an adapter from the gitea spec and ran the GENERATED common.test.sh, rather than asserting on the template text. check-skill diagnostics. Check 7 ran each scripts/*.test.sh with output sent to /dev/null and reported only "script test failed: <name>". That is undiagnosable anywhere the failure cannot be reproduced by hand — which is exactly the state generate-adapter.test.sh is in on this PR: green locally under every condition tried (fresh clone, merge result, minimal env, four working directories, with and without the lint tools) and red in CI, with no way to see which case breaks. Output is now captured and replayed on failure only; success stays silent, since the reason to suppress was log noise and that reason does not apply to the run that just went red. check-skill.test.sh reports 21 pre-existing assertion failures in this tree, identical with and without this change; that suite is not in CI's gate list and its redness is not introduced here. Refs #2950, #2946, #2952. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 1m 11s —— View job Security review complete
This is a re-review at HEAD Not security-relevant (checked, no exploitable issue found)
Still open — unchanged by this delta, previously reported on this PRIMPORTANT — Linear Previously reported, confirmed still fixed (spot-checked, not re-flagged)
|
|
Claude finished @kyle-sexton's task in 2m 9s —— View job Code review complete
Per What changed in this delta
Verified correct (not flagged)
Not flagged
Still open — not touched by this delta, previously reported on this PRTo avoid duplicate noise, these aren't re-posted — they still stand at this head and weren't in scope of
Environment noteRead-only tooling only for this pass ( |
…a lint failure The CI-only skill-quality-gate failure, diagnosed at last — by the diagnostic change in the previous commit, which paid for itself on its first run: FAIL: [121] generated shell is ShellCheck-clean — expected 0 got 3 SKIP: shfmt not available — generated-shell format case not run ShellCheck exit 3 is "invoked with bad syntax", NOT "issues found" (that is 1). The case passes `--rcfile`, which ShellCheck only gained in 0.10.0, and the repo's own .shellcheckrc header declares it targets 0.11.0+. The skill-quality-gate job installs no tooling, so it gets the runner's older build, which rejects the flag outright. Asserting on the exit status alone read that version mismatch as a lint failure — which is why the case was green locally under a fresh clone, the merge result, a minimal environment, four working directories, and with and without the optional tools. The test now probes whether THIS binary accepts the flag, rather than assuming it, and skips loudly when it does not — the same idiom the file already uses for an absent shellcheck or shfmt. Verified both paths: modern ShellCheck runs the case and passes; a stand-in that exits 3 on --rcfile skips and the suite exits 0. Exec bit, properly this time. The earlier attempt used `git update-index --chmod=+x`, which stages a mode — and the `git add -A` in the same commit then re-read the on-disk modes (still 644) and reverted every one of them. So the claim that the exec bit was fixed was wrong, and CI kept saying so. core.fileMode is true here, so the fix is to chmod the working-tree files; the index now holds zero tracked shebang files at 100644, repo-wide rather than only within this branch's diff. skill-quality 0.17.1 for the check-skill change (changelog parity). Refs #2950, #2946, #2952. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 1m 42s —— View job Code review complete
Per What changed in this delta
Verified correct (not flagged)
No new correctness or maintainability findings in this delta. Still open — not touched by this delta, previously reported on this PRNot re-posted, to avoid duplicate noise — still stand at this head:
Environment noteRead-only tooling only for this pass ( |
|
Claude finished @kyle-sexton's task in 1m 9s —— View job Security review complete
This is a re-review at HEAD Not security-relevant (checked, no exploitable issue found)
Still open — unchanged by this delta, previously reported on this PRIMPORTANT — Linear Previously reported, confirmed still fixed (spot-checked, not re-flagged)The |
…es from posing as scripts Two things, both surfaced by CI rather than by reading. Activity-check pagination (the finding both review lanes kept re-reporting; I verified it before acting). reclaim.sh read `comments(first: 50)` with no cursor and no hasNextPage loop, while wit_linear_lease_comments paginates exhaustively over the identical connection. Linear returns a comments connection oldest-first absent an explicit orderBy, so on a long-running item the comments that prove a holder is alive are on the LAST page — the read could not see recent activity at all past roughly one page, and reclaim then released a live lease. Now a paginating wit_linear_activity_since helper beside the one it mirrors. It cannot reuse that one: it keeps only lease markers and this wants precisely the opposite. Regression test seeds activity on page two, which needed a new lin_comments_page mock helper — the existing lin_comments hardcodes hasNextPage:false and so literally cannot express the failing shape. Confirmed discriminating: with the first-page-only read restored the run no longer detects the activity and the reason assertion fails. Note the reclaimed:false assertion passes either way, because the buggy path also declines for an unrelated reason, so the reason assertion is the real discriminator. Templates no longer carry a literal shebang. Making them executable in the previous commit fixed exec-bit and broke the repo's ShellCheck lane instead: a file that opens with a shebang gets discovered as a script, and a template full of @@Placeholder@@ tokens cannot parse. The two gates were in genuine conflict and the exec-bit action takes no exclusions, so the fix is to stop the file looking like a script at all — the shebang becomes a substituted value, which is what it actually is. Templates are back to 100644 with no shebang, so neither gate claims them; generated scripts still get the shebang and their exec bit from `emit`. Two templates already opened with `# shellcheck shell=bash` rather than a shebang, so this makes the set consistent rather than introducing a new idea. Verified end to end by generating an adapter: executable verbs get the shebang at 755, sourced files get `# shellcheck shell=bash` at 644 — matching the github adapter's precedent. 24/24 suites pass. Refs #2946, #2950, #2952. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 1m 35s —— View job Security review complete
This is a re-review at HEAD Fixed — verified by reading the code, not the commit messageIMPORTANT — Linear
Not security-relevant (checked, no exploitable issue found)
No new findings this passAll previously-reported IMPORTANT/CRITICAL findings on this PR ( |
…nt generator bug they exposed
Each verified against the code before acting.
Gitea README verb table called create-item, get-item, link-blocks and list-items
"scaffold — provider mapping to write". Per the README's own definition that means
the script still calls wit_gitea_unimplemented and exits 1. None of them does —
all four are real implementations with passing tests. The table was telling a
consumer that four core verbs do not work.
Label pagination in gitea/create-item.sh had no ceiling while every other
paginated loop in the adapter caps against WIT_GITEA_LIST_ITEMS_MAX. Capped —
but not by copying list-items' treatment, because the consequence differs: there,
truncation just returns fewer items; here, stopping early makes an unseen label
indistinguishable from a nonexistent one, and the next step tells the operator to
create a label that already exists. The ceiling therefore also qualifies that
message.
Conformance binding for basic auth baked auth_user to the literal
ci@example.invalid with no override, while host and scope are required from the
environment — so a generated basic-auth adapter would authenticate as the
placeholder against a real throwaway instance and 401 with nothing saying why. It
now takes the identity from the environment the same way.
Linear binding's scope comment said "e.g. owner/repo"; Linear's scope is
<workspace>/<TEAMKEY>. Fixed in the generated file and at the template that
propagates it, where it now shows the provider's own sample scope.
Two defects of my own, caught by generating a basic-auth adapter rather than
assuming the change was fine: SAMPLE_AUTH_EXTRA feeds BOTH the offline test
fixture (which needs a literal) and the conformance binding (which needs the jq
variable), so collapsing them emitted `$au` into the test's single-quoted JSON —
now two keys; and an apostrophe inside ${VAR:?word} is parsed as a quote by bash,
which broke the generated file's syntax.
That exercise also exposed a latent template bug worth its own note: the
generated test asserted a HARDCODED id number of 12, while the id comes from the
configurable api.sample_id. Both shipped specs happen to end in #12, so it never
showed — but any consumer with a different sample id got a failing test the moment
their adapter was generated, from a template that promises "every case here is
real and passes the moment the adapter is generated". Now derived from the id.
Verified both shapes: a #7 spec and a #12 spec each generate a passing test.
52/52 suites pass; portability, ShellCheck, markdownlint and check-skill clean.
Refs #2946, #2950, #2952.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 2m 8s —— View job Security review complete
This is a re-review at HEAD No new security issues foundChecked each change against the injection surface this PR's history has repeatedly turned up (the
Previously reported findings — final status, all fixed and verified across this PR's review history
Nothing new surfaced in this delta. |
The hygiene lane's shellcheck sub-check has been red since 764fc53, and I twice attributed the lane's failure to something else — first exec-bit, then the templates — without ever reading the sub-check's own output. Both of those were real and are fixed, but neither was this. Reading it took one log fetch and gave five findings, all in code I added: - SC2249 on quote_safe()'s `case`: no default branch. The repo enables add-default-case precisely so a fallthrough is stated rather than assumed, so it gets an explicit `*) ;;` saying why nothing happens there. - SC2016 x4 on deliberate single-quoted literals: two display_name test payloads whose whole point is that `$(id)` and backticks reach the generator UNEXPANDED (an expanded payload would test this shell's output instead of the guard), and two generator values that must be emitted verbatim into the generated binding — `$au` is a jq variable bound by --arg, and the --arg fragment is that file's own invocation, expanded when it runs rather than here. Per-site suppressions with reasons, not a file-wide disable: the file already carries one blanket SC2034 and a second would stop the check seeing anything. Verified with CI's exact invocation (shellcheck --rcfile=.shellcheckrc) over every changed shell file: exit 0. 52/52 suites pass, portability and shfmt clean. Refs #2946, #2950, #2952. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 1m 0s —— View job Security review complete
This is a re-review at HEAD No new security issues foundThis commit is purely ShellCheck-suppression annotations plus one explicit no-op default
Previously reported findings — all still fixed, unaffected by this deltaNothing in this commit touches any of the files where this PR's history found and fixed issues (CRITICAL |
…ow-ups #3046, #3047 and #3048 were filed as deferred follow-ups out of Lane C. They are worked to completion here instead. #3046 — review-lens tautology criterion. The anti-pattern was covered in prose and CLAIMED to be covered executably by testing:audit's cant-fail-scan.sh; that claim was false, and the scanner concedes it in its own header — rule-recomputed-expectation decides only textually-identical sides, and the canonical Khorikov shape (compute `expected` with the production algorithm, then assert) does not fire. A Code-quality criterion in review's code-reviewer now asks what the expected value's independent source is, names the round-trip/identity case, and cedes the identical-sides core to the scanner by name so the two cannot double-report. Placement went to the agent definition, not quality-gate/context/criteria.md, because that file is a routing doc carrying no criteria of its own. #3047 — pre-PR ordering conflict. pre-pr.md declared its step order unreorderable while implementation:implement reordered it at the handoff point, putting outcome verification before review. pre-pr.md's order is doctrine: steps 4-6 mutate the diff, so a verdict rendered before them describes code that no longer exists at PR time. This was a one-surface correction, not a coin flip — verification's own chaining table already fired on "review gate passes" then suggested confirm. Ownership moves to a new docs/conventions/pre-pr-ordering/ owner doc with a registry row, since the registry's own trigger (an owner doc before a second plugin adopts it) had already fired; pre-pr.md's override-boundary paragraph is corrected from "fixed plugin identity" to fleet identity; implement's two sites now read review -> verify -> PR with every presence gate intact. #3048 — SkillUse measurement. skill-pair-cooccurrence.sh is the repeatable reading, placed in audit-skill-visibility rather than observability because observability's own read-routing.md already assigns interpretation of skill-usage data there. It prints its proxy limit in both renderers and inherits the skill's WITHHELD discipline. Run against the only reachable store (17 events, 3-day span): implement fired zero times, so the denominator is empty and the verdict is WITHHELD, not 0%. The schema-widening question is DECIDED here rather than filed: do not widen the record — caller identity is absent from the hook's input, not just its schema, and recovering it would mean reading the transcript. OTEL's invocation_trigger already separates user-slash from claude-proactive and needs no change. 33 regression cases, guards verified to fail when removed. One case caught a real defect in the first draft: the header promised malformed rows cost only themselves while jq -s failed the whole file on the first bad line. Closes #3046 Closes #3047 Closes #3048 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
…on collisions Main landed six commits while this branch was open, three of which bumped plugins this branch also bumped, to the same numbers. Resolved by stacking this branch's entry above main's released one: claude-ops 0.34.0 (both) -> mine becomes 0.35.0, main's 0.34.0 kept session-flow 0.29.0 (both) -> mine becomes 0.30.0, main's 0.29.0 kept skill-quality 0.17.1 (both) -> mine becomes 0.17.2, main's 0.17.1 kept The two manifest conflicts were NOT the version field. Bumping with `jq` had rewritten each whole file, re-encoding main's — escapes as literal UTF-8 em-dashes in the description string. Resolved by taking main's exact bytes and editing only the version line, so no manifest carries an incidental encoding change. Also: main's audit-skill-visibility SKILL.md sat at exactly the 200-line soft target, so this branch's inline pair-cooccurrence section pushed it to 234 and tripped a warning. The detail moves to reference/pair-cooccurrence.md — the progressive-disclosure spoke the check recommends — and the pointer folds into the existing scope-boundary row rather than adding one, landing back at 200 with 0 errors and 0 warnings. Verified on the merged tree: 33/33 co-occurrence cases, changelog parity, shell portability (56 files), cross-plugin drift, fleet doc-grammar, fleet finding coverage, silent skips, markdownlint on every conflicted file, and check-skill on audit-skill-visibility. Main's mawk fix (#3058) also cleared the 21 assertion failures check-skill.test.sh was reporting in this tree; it now passes fully. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 2m 27s —— View job Security review complete
This is a re-review at HEAD Confirmed via Reviewed, no new security issues found
Previously reported findings — final status, all fixed and verified across this PR's review history, unaffected by this delta
Nothing new surfaced in this delta. No findings to post. |
… on a failed lease write Found while writing a regression test for a reviewer's partial-claim finding: the test would not go red when it should have, and the reason was worse than the finding it was written for. Every wit_linear_* helper signals failure by calling `exit`. claim.sh captured one as POSTED="$(wit_linear_post_comment …)" — and an `exit` inside a command substitution ends only the SUBSHELL. With `set -uo pipefail` and no `-e`, the script printed the API error to stderr and carried on: derived a handle from an empty response, wrote a lease marker, and emitted a normal success object with exit 0. The caller was told it held a lease that does not exist. The same swallow affected wit_linear_lease_comments at five more sites, where it inverts a safety check rather than a report: a failed read yields empty, the "is anything already claimed here?" loop iterates over nothing, and the claim proceeds as if the item were free — a double-claim caused by an API hiccup. All six sites now propagate the helper's exit status, preserving its code taxonomy. Also fixes the finding that started this: the assignment lands before the lease is posted, so a failure in between stranded an item assigned with no lease — excluded by list-frontier, refused by reclaim, unrecoverable through the seam. An EXIT-trap rollback guards that window, mirroring the github adapter's _wit_claim_rollback, disarmed at both settled outcomes. Disarming on the lost-race path matters as much as arming it: that branch already decides the assignee by re-fetching and comparing the holder, and a second unconditional unassign would strip the winner's live claim. Three regression cases, each verified to fail with its guard removed. 59/59 test files pass; shellcheck clean; portability lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 3m 44s —— View job Security review complete
This is a re-review at HEAD Finding (posted inline)1 new finding, IMPORTANT:
Not security-relevant (checked, no exploitable issue found)
Previously reported findings — unaffected by this delta, still fixed as verified in prior passesCRITICAL |
…inner's assignment The EXIT-trap rollback added in 0.39.1 closed the assigned-with-no-lease strand, but cleared the assignee unconditionally — reintroducing, from the rollback path, the exact bug reclaim.sh was fixed for earlier in this effort. Caught by the security reviewer on the previous commit; verified against the source before fixing. The trap stays armed across the update-comment write and the arbitration read, and 0.39.1's own `|| exit "$?"` additions WIDENED that window by making both of them exit on failure. Linear's assignee is a single field, so a concurrent session can legitimately win the claim inside the window — posting its own lease and overwriting the assignee — and a blind clear on the way out strips that live claim while the winner's lease stays untouched. The item silently returns to the frontier while someone is actively working it. The rollback now re-fetches the issue and clears only if the assignee is still this session, the same compare the LOSER branch already applies before its own unassign. The whole rollback body stays in one subshell so a helper's internal `exit` cannot abort the trap or overwrite the script's exit status. One regression case seeding a different assignee at rollback time, verified to fail with the compare removed. 59/59 test files pass; shellcheck clean; portability lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm
|
Claude finished @kyle-sexton's task in 1m 19s —— View job Security review complete
This is a re-review at HEAD This is a direct fix for the IMPORTANT finding this lane posted at the previous head ( Fixed — verified by reading the code, not the commit messageIMPORTANT —
Traced the race this closes: the trap stays armed across the New regression test ( Also confirmed the fix doesn't reopen the case it was originally patched for (assigned-with-no-lease strand): lines 200-208 (pre-existing case, unmodified by this commit) still asserts a single Not security-relevant (checked)
No new findingsThis closes the last IMPORTANT finding open on this PR. Every previously-reported CRITICAL/IMPORTANT finding across this PR's full review history ( |
Closes #3046
Closes #3047
Closes #3048
Summary
Two tracks on one branch.
The adapter track — the container's last three sub-items (#2950, #2952, #2946) plus the SSOT verdict reconciliation: a generator that lets a consumer onboard a tracker this plugin does not bundle, the Gitea/Forgejo adapter generated through it as the designated dogfood, and a hand-built Linear adapter with full verb parity. None of those three issues is closed by this PR, deliberately — each one's acceptance criteria say "passing conformance", and no live tracker instance is reachable from this environment. See "The blocker, stated plainly" below.
The Lane C spin-off closures — #3046, #3047 and #3048 were filed as deferred follow-ups during Lane C. They are worked to completion here rather than carried, and this PR closes all three.
Shipped versions:
work-items0.39.0,review0.24.0,session-flow0.30.0,implementation0.14.0,claude-ops0.35.0,skill-quality0.17.2.Fix
#2950 — adapter-onboarding skill (
work-items0.37.0 → 0.39.0)work-items:onboard-adapter— interview → live exploration → generate → verify. The generator (scripts/generate-adapter.sh) emits a hardened skeleton from templates rather than re-deriving security posture per provider: token via env-name indirection and never argv, host validation, deny-by-default egress — the jira adapter's guards as the starting point. It also emits theconformance/bindings/<name>.shthe suite needs, so a generated adapter is verifiable rather than merely written.Split by the interview's own constraint: the deterministic parts are scripted, and the parts that need reasoning — semantic mapping of provider states onto the normalized envelope, the interview itself — stay outside the script.
#2952 — Gitea/Forgejo adapter
Generated through
onboard-adapteras the dogfood. Its manifest is the honest part:add-sub-item,list-sub-items,claim,reclaim, andrenew-leaseare all declaredfalsewithsub_item_depth: 0, because Gitea has neither sub-issues nor a lease surface. No verb script exists for anyfalseverb — the contract's declared-false-exits-6gate handles them, so nothing is stubbed or faked to look supported.#2946 — Linear adapter
Hand-built as a bundled "major" under the hybrid shipping model. All ten verbs declared
true, each with an implementing script and a.test.sh— full parity, including the claim/renew/reclaim lease protocol.One design note worth surfacing:
wit_linear_epochnormalizes fractional seconds off a timestamp before parsing it. That is not tidiness — Linear returns both…:00Zand…:00.500Z, BSDdate -fdemands an exact format match and fails on the fractional form, and an earlier version of this shape is recorded in-comment as having "silently seen no activity at all". The lease's activity check depends on it.#3046 — tautological-expectation criterion in the review code lens (
review0.24.0)The anti-pattern was covered in prose (
tdd'santi-patterns-khorikov.md,testing'swrite.mdchecklist) and was claimed to also be covered executably bytesting:audit'scant-fail-scan.sh. That claim was false, and the scanner concedes it in its own header:rule-recomputed-expectation"detects the decidable core — textually identical sides — not every recomputation shape." A validator ran it over three canonical tautological tests for zero findings — the canonical Khorikov shape (computeexpectedwith the production algorithm in the arrange section, then assert against it) has non-identical sides and does not fire.The new Code-quality bullet in
review/agents/code-reviewer.mdasks the question that decides it: what is the expected value's independent source? The round-trip/identity case rides in the same criterion, matching howwrite.md:78already pairs them. It cedes ground to the scanner by name rather than overlapping it, per the plugin's existing skip-what-tooling-enforces posture — where both sides are the same expression, the scanner owns the finding. Widening the detector past textually-identical sides is explicitly not in scope: the general shape is undecidable.Placement went to the agent definition rather than
quality-gate/context/criteria.md, because that file is a routing doc — it resolves the project's standards index and carries no criteria of its own.#3047 — the pre-PR ordering conflict, settled (
session-flow0.30.0,implementation0.14.0)pre-pr.mddeclared its step order "fixed plugin identity … there is no seam to reorder it," while a sibling plugin was reordering it at the handoff point:implement's step 5, titled "Hand off to the pre-PR sequence," prescribed outcome verification before review.pre-pr.md's order is doctrine. Steps 4–6 (simplify, review the simplify diff, re-test) mutate the diff between review and verification, so a verdict rendered before them describes code that no longer exists by step 8 — and the simplify edits then ship carrying an outcome claim nothing tested them against. The competing reading ("confirm it works before spending review effort") is already served earlier, by step 1 and byimplement's own build check and full test pass.This was a one-surface correction, not a coin flip.
verification's own chaining table (skills/confirm/SKILL.md:125,128) already fires on "review gate passes (no blocking findings)" → suggest/verification:confirm, and suggests the PR flow only after a CONFIRMED verdict. The skill that renders the verdict, the skill that lists the sequence, and the plugin that opens the PR all agreed;implement's handoff step was the lone dissenter.Landed: a new owner doc
docs/conventions/pre-pr-ordering/plus a registry row inPLUGIN-PHILOSOPHY.md— the registry's own trigger ("a new cross-plugin convention lands in an owner doc before a second plugin adopts it") had already fired.pre-pr.mdkeeps ownership of what each step does and cites the owner for the order; its override-boundary paragraph is corrected from "fixed plugin identity" to fleet identity, since the seam it denied was being exercised by a sibling.implement/SKILL.md's two sites now read review → verify → PR with every presence gate and fallback intact.#3048 — the SkillUse measurement, taken (
claude-ops0.35.0)skill-pair-cooccurrence.shis the repeatable reading. Placement corrects the filing: the item proposedobservability, but that plugin's owncontext/read-routing.mdalready assigns interpretation of skill-usage data toaudit-skill-visibilityand keeps only the store, the pipeline, and retention.It is a proxy and refuses to be read as more. The
SkillUserecord carries no caller attribution — a PostToolUse hook on the Skill tool receivestool_name,tool_input, andtool_response, and nothing names the skill whose instructions caused the call. So it observes only that both skills fired in the same(project_id, branch)group, ordered by timestamp. The caveat prints in both renderers, prose and--json, because a machine consumer stripping it is the same defect as a human not seeing it. It inherits the skill's refusal: below the 30-day exposure floor (the same constantaudit_skill_visibility.pyuses) or below a minimum denominator it returnsWITHHELDwith a reason.The result, run against the only store reachable here (
.claude/observability/skill-usage.jsonl— 17 events, 2026-08-17 → 2026-08-20):implementation:implementfired zero times. The denominator is empty, so the verdict is WITHHELD, not 0% — an empty denominator is a population that was never observed, not a rate of zero. #2936's premise stays UNVERIFIED, now with an instrument attached rather than a hand-wave.The schema-widening question is decided here rather than filed: do not widen the record. Caller identity is absent from the hook's input, not merely its schema; recovering it would mean reading the session transcript, which turns a bounded telemetry hook into a conversation reader and crosses the boundary
observability/context/privacy.mdguards. The better signal needs no change and already ships — OTEL'sclaude_code.skill_activatedcarriesinvocation_trigger, separatinguser-slashfromclaude-proactive, which is the axis the premise actually asks about.Merging main, and a defect that surfaced doing it (
skill-quality0.17.2)Six commits landed on main while this branch was open, three of which bumped plugins this branch also bumps — to the same numbers. Resolved by stacking this branch's entry above main's released one:
claude-ops0.34.0→0.35.0,session-flow0.29.0→0.30.0,skill-quality0.17.1→0.17.2.The two
plugin.jsonconflicts were not the version field. Bumping withjqhad rewritten each whole file, re-encoding main's—escapes as literal UTF-8 em-dashes in the description string — an incidental encoding change riding along in a version bump. Resolved by taking main's exact bytes and editing only the version line;reviewandimplementationwere then verified to differ from main by the version line alone.Also caught here: main's
audit-skill-visibility/SKILL.mdsat at exactly the 200-line soft target, so this branch's inline pair-cooccurrence section pushed it to 234 and tripped a warning. The detail moved toreference/pair-cooccurrence.md— the progressive-disclosure spoke the check recommends — with the pointer folded into an existing scope-boundary row rather than added as a new one, landing back at 200 with 0 errors and 0 warnings.One incidental win: main's mawk fix (#3058) cleared the 21 assertion failures
check-skill.test.shhad been reporting in this tree. It now passes fully.SSOT reconciliation
Lanes F (#2938) and Y (#2948) were the last two
OPENverdict cells; both filled from each item's closing PR and design comment. Lane C's C10, chain-doc, and premise bullets now record the three closures above.Review findings fixed on this PR
Six review rounds across two lanes. Every finding is closed; the security lane's final pass at
ffc25bereports no new findings and lists each of these as independently re-verified against the source rather than against the commit message.Security (all confirmed before fixing, not taken on assertion):
api.scope_patternwas validated only for anchoring andapi.sample_scopeonly for matching it, then both were rendered into single-quoted shell. Confirmed by building the exploit and running it end to end: the generator accepted the spec, emittedreadonly …_SCOPE_RE='^';touch …;#'$'(valid shell,bash -nclean), and sourcing it — which every generated verb does — executed the command. Fixed atrender(), the one choke point every value passes through.display_namereached three dangerous contexts, not one — the first guard covered only the single-quoted case. A double-quoted${VAR:?…}expands$(…)with no quote to break, and a newline ends a#comment line. Constrained at validation instead, where every neighbouring spec field already is.reclaim.shcould strip a live claim —revalidate()filtered the re-read by the original handle, so a concurrent claimer's new lease (different handle) was structurally invisible; and the assignee came from a snapshot taken before every round trip. Both closed, mirroring whatclaim.shalready did.host_suffixwas a string suffix, not a domain boundary — a pin ofmycompany.comwas satisfied byevilmycompany.com, sending the credential to an attacker host without tripping the loudallow_custom_domainopt-out. Fixed in the generator template as well as both adapters, so future generated adapters inherit the fix. The apex is accepted separately, since under a pin ofmycompany.comthe hostmycompany.comis the pinned domain.Correctness:
reclaim.shactivity check read only the first page of comments while its sibling helper paginates exhaustively. Linear returns comments oldest-first, so the comments proving a holder is alive are on the last page — the check could not see recent activity at all on a busy item.auth_userto a placeholder with no override; the Linear binding's scope comment described GitHub's shape.12, while the id comes from the configurableapi.sample_id. Both shipped specs happen to end in#12, so it never showed — but any consumer with a different sample id got a failing test the moment their adapter was generated, from a template that promises the opposite.jq -sfailed the whole file on the first unparsable line. The read is nowjq -Rnwithfromjson?.Every fix that could be tested carries a regression test, and each new test was confirmed to fail without its fix rather than merely pass with it.
Verification
Re-run on the merged tree at
ffc25be:scripts/check-shell-portability.sh origin/main→ No unexcused GNU-only constructs in 57 shell file(s). Threedate -u -dsites in the Linear adapter carry per-siteportability-ok:markers with reasons — they are genuine dual-dialect ladders with BSD-j -ffallbacks, and the linter seeds ladder guards only forreadlink -fandstat -c, sodatehas no guard class to recognize themshellcheck --rcfile=.shellcheckrcover every changed shell file → exit 0check-skill.sh→ PASS, 0 errors foronboard-adapter,audit-skill-visibility(0 warnings),implement, andworkflow.implement's single warning is a pre-existing 219-line soft-target notice; its line count is unchanged by this PRmarkdownlint-cli2over all changed Markdown → 0 issuescheck-changelog-parity.sh --check,check-fleet-audit-doc-grammar.sh,check-fleet-finding-test-coverage.sh,check-cross-plugin-source-drift.sh,check-silent-skips.sh,check-discriminating-test-skips.sh→ all cleanSAMPLE_AUTH_EXTRAand the hardcoded-id-number bug were caughtOne note on a deliberate skip:
generate-adapter.test.shprobes whether the local ShellCheck accepts--rcfile(0.10.0+) before using it, and skips loudly otherwise. Theskill-quality-gatejob installs no tooling and gets the runner's older build, which rejects the flag and exits3— "invoked with bad syntax", not "issues found" — which the assertion was previously misreading as a lint failure.The blocker, stated plainly
The three adapter items share one external dependency: no live tracker instance of any kind is reachable from this environment — no
gh, no Linear workspace, no Gitea host. What is verified here is offline: mocked-transport unit tests against each adapter's ownmock.sh, plus honest capability manifests. What is not verified is a live conformance pass, which is what all three acceptance criteria actually ask for.This follows the precedent the jira adapter set — ship the adapter, defer the live pass, record the deferral — and it is why this PR closes none of those three. Closing one here would assert a criterion that has not been met. The close-out dry run posted to #2933 caught exactly this and refused to close them too.
To unblock: a reachable throwaway instance for any of the three. Then
conformance/run-conformance.sh --binding {gitea,linear}against it, close the corresponding item, and the container reaches 20/20.Related
🤖 Generated with Claude Code
https://claude.ai/code/session_01CnzwTKoTa6xNY7iyEzMYpm