Skip to content

feat(tools): teardown completed worktrees - #619

Merged
thomasluizon merged 3 commits into
mainfrom
feature/orb-124-close-a-worktree-automatically-when-its
Jul 27, 2026
Merged

thomasluizon merged 3 commits into
mainfrom
feature/orb-124-close-a-worktree-automatically-when-its

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds teardown-worktree.mjs, which checks clean state, branch-owned content presence, live Done state, and terminal repaint activity before teardown.
  • Stops Orca terminals, removes the child worktree, prunes Git metadata, deletes the local branch, and verifies filesystem plus git worktree list results.
  • Adds per-ticket teardown to /orchestrate and hermetic decision-path coverage for success, refusals, squash merges, and Orca runtime disconnects.

Closes ORB-124.

Decisions taken unattended

  • Used the existing tools/lib/tui-repaint.mjs export directly, matching the ticket reconciliation note; launch-worker.mjs remains unchanged.
  • Treated the Orca removal response as non-authoritative and verified removal from disk and Git after attempting it.
  • Used npm run lint as the configured lint entry point because ESLint 9 has no root eslint.config.* for a direct npx eslint tools .claude --ext .mjs,.js invocation.
  • Scoped tree-presence verification to paths changed since the merge base, retaining content-based squash-merge support without requiring ancestry.
  • Normalized external terminal paths only after a string type guard, preserving relative selector resolution while ignoring incomplete fleet entries.
  • Used isolated real Git reference-transaction hooks to cover both post-removal branch cleanup refusals without stubbing Git behavior.
  • The GitHub review-thread query returned no inline threads. UNMET: no review threads were available to reply to or resolve; the review findings are sections of submitted top-level reviews and are addressed by the round comments.

Gate output

node tools/test-tools.mjs
ORBIT TOOLS GATE OK

node .claude/hooks/test-hooks.mjs
ORBIT HOOK PARITY OK

npm run lint
Tasks:    3 successful, 3 total

npm run type-check
Tasks:    3 successful, 3 total

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Jul 27, 2026 9:42pm

Request Review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #619 Review — feat(tools): teardown completed worktrees (closes ORB-124)

Scope reviewed: full diff — tools/teardown-worktree.mjs (new), tools/test-tools.mjs, tools/README.md, .claude/skills/orchestrate/SKILL.md. No prior reviews or resolved threads existed on this PR.

Recommendation: REQUEST CHANGES

Severity Count
Critical 0
High 1
Medium 1

High — tools/teardown-worktree.mjs:99-101 — tree-present check compares the whole repo tree, not the branch's own changes

git(path, ["fetch", "--quiet", "origin", base], { allowFailure: true })
const baseRef = git(path, ["rev-parse", "--verify", "--quiet", `origin/${base}`], { allowFailure: true }) ? `origin/${base}` : base
const treePresent = git(path, ["diff", "--quiet", baseRef, branch], { allowFailure: true }) !== null

git diff --quiet baseRef branch only exits 0 (no differences) when the entire trees are byte-identical. /orchestrate runs tickets in parallel waves forked from the same base commit. Once sibling ticket A merges to main first, origin/main's tree contains A's files, which ticket B's branch (forked before A merged) never had. A whole-tree diff between B's own branch and the now-updated main shows a difference from A's content alone — even though B's own work merged cleanly. treePresent comes back false and teardown refuses B, for every ticket in a wave except whichever merged first. There is no override flag in the documented CLI flow. This is fail-closed (no data loss), so High rather than Critical, but it defeats the tool's primary real-world use case: tearing down worktrees after a normal multi-ticket /orchestrate wave.

Verified directly against the code (not just the skill's report): the only fixture exercising the merged case is selector in tools/test-tools.mjs (stageTeardownWorktree(..., { changed: true, squashMerged: true })), which writes the same captured.txt content into both primary and child — i.e. it only proves exact-tree-match detection, and cannot catch a sibling ticket's unrelated content landing on main first.

Fix: scope the presence check to the branch's own changed paths (diff against its actual fork-point) rather than the entire tree, and add a fixture where an unrelated file lands on main from a sibling ticket before asserting the branch under test is still recognized as merged.

Medium — tools/teardown-worktree.mjs:47-52 — dead validation branch + silent fallback on malformed --base

const requestedIssue = argOf("--issue")
...
if ((requestedIssue && !argOf("--issue")) || (requestedWorktree && !argOf("--worktree")) || (requestedBase === null && process.argv.includes("--base"))) fail(2, ...)
  • requestedIssue && !argOf("--issue") is always x && !x (false) since requestedIssue was just assigned from argOf("--issue") — same for the --worktree clause. Both are unreachable dead code.
  • The --base clause checks requestedBase === null, but a trailing --base with no following value produces argOf returning undefined (out-of-bounds array access), not null. That malformed input never triggers the usage error and instead silently falls through to base = requestedBase ?? worktree.baseRef ?? "main", defaulting to main/the worktree's base rather than erroring on the typo.

Low real-world exposure, but a brand-new tool shipping unreachable code plus a silent fail-open path for malformed CLI input.

What's good

The four-check evidence gate (clean tree, tree-present, Linear Done, terminals idle) fails closed and verifies orca's removal response against the filesystem/git worktree list rather than trusting it, per tools/CONVENTIONS.md's "gate tools" discipline. test-tools.mjs coverage is thorough for the scenarios it does model: dirty tree, unmerged content, non-Done issue, a repainting terminal, a lying "removed" response from orca, and squash-merge without ancestry.

Not applicable to this diff: parity, i18n, contract drift, DESIGN.md, backend hard rules, FEATURES.md — this PR touches only tools/** and .claude/skills/**, no apps/* or orbit-api surface. Cross-repo dimensions (contract-aligner, orbit-api side of the backward-compat guard) are not verifiable in this CI job since orbit-api is not checked out here.

@thomasluizon

Copy link
Copy Markdown
Owner Author

Review round 1 complete in c4e7016.

High: tree presence now derives branch-owned paths from the merge base and compares only those paths against the target. The new sibling-target-advance fixture was verified against the old whole-tree comparison first: it failed with UNMET tree-present-in-target, then passes with the fix.

Medium: value-taking flags now distinguish absent from valueless input and reject valueless --issue, --worktree, and --base with exit 2. The dead validation branch was removed.

No inline review threads are available through GitHub to reply to or resolve; the submitted review contains these as two review sections. This is recorded in the PR decisions section.

node tools/test-tools.mjs
# structural coverage
PASS every tools/ script has coverage (25 scripts)
PASS the coverage guard actually enumerated scripts

# universal contract (tools/CONVENTIONS.md)
PASS a real bash is resolvable
PASS agent-review.ps1: --help exits 0 with usage on stdout
PASS agent-review.ps1: invalid input is refused
PASS agent-review.sh: --help exits 0 with usage on stdout
PASS agent-review.sh: invalid input is refused
PASS arch-map.mjs: --help exits 0 with usage on stdout
PASS arch-map.mjs: invalid input is refused
PASS capture-surfaces.mjs: --help exits 0 with usage on stdout
PASS capture-surfaces.mjs: invalid input is refused
PASS check-copy.mjs: --help exits 0 with usage on stdout
PASS check-copy.mjs: invalid input is refused
PASS check-dashes.mjs: --help exits 0 with usage on stdout
PASS check-dashes.mjs: invalid input is refused
PASS check-frontmatter.mjs: --help exits 0 with usage on stdout
PASS check-frontmatter.mjs: invalid input is refused
PASS check-push-target.mjs: --help exits 0 with usage on stdout
PASS check-push-target.mjs: invalid input is refused
PASS check-suppressions-ratchet.mjs: --help exits 0 with usage on stdout
PASS check-suppressions-ratchet.mjs: invalid input is refused
PASS check-ticket.mjs: --help exits 0 with usage on stdout
PASS check-ticket.mjs: invalid input is refused
PASS compose-prompt.mjs: --help exits 0 with usage on stdout
PASS compose-prompt.mjs: invalid input is refused
PASS launch-worker.mjs: --help exits 0 with usage on stdout
PASS launch-worker.mjs: invalid input is refused
PASS merge-sweep-cov.sh: --help exits 0 with usage on stdout
PASS merge-sweep-cov.sh: invalid input is refused
PASS merge-sweep.sh: --help exits 0 with usage on stdout
PASS merge-sweep.sh: invalid input is refused
PASS new-ticket.mjs: --help exits 0 with usage on stdout
PASS new-ticket.mjs: invalid input is refused
PASS nudge-worker.mjs: --help exits 0 with usage on stdout
PASS nudge-worker.mjs: invalid input is refused
PASS orca-web-port.mjs: --help exits 0 with usage on stdout
PASS orca-web-port.mjs: invalid input is refused
PASS pr-watch.mjs: --help exits 0 with usage on stdout
PASS pr-watch.mjs: invalid input is refused
PASS redesign-coverage.mjs: --help exits 0 with usage on stdout
PASS redesign-coverage.mjs: invalid input is refused
PASS rollup.sh: --help exits 0 with usage on stdout
PASS rollup.sh: invalid input is refused
PASS surface-manifest.mjs: --help exits 0 with usage on stdout
PASS surface-manifest.mjs: invalid input is refused
PASS teardown-worktree.mjs: --help exits 0 with usage on stdout
PASS teardown-worktree.mjs: invalid input is refused
PASS wave-plan.mjs: --help exits 0 with usage on stdout
PASS wave-plan.mjs: invalid input is refused
PASS worker-status.mjs: --help exits 0 with usage on stdout
PASS worker-status.mjs: invalid input is refused
PASS worker-watch.mjs: --help exits 0 with usage on stdout
PASS worker-watch.mjs: invalid input is refused

# decision paths
PASS new-ticket.mjs: validates the identifier orca reported
PASS new-ticket.mjs: a created but defective ticket exits 1 naming it
PASS new-ticket.mjs: an orca failure creates nothing and exits 3
PASS new-ticket.mjs: success with no identifier is a tool error, never a silent pass
PASS new-ticket.mjs: requires --project so the ticket cannot be orphaned
PASS launch-worker.mjs: resolves the repo from the repo:* label
PASS launch-worker.mjs: derives the contract branch from the title
PASS launch-worker.mjs: worker:sonnet swaps the configured opus
PASS launch-worker.mjs: refuses a repo:* label with no repos entry
PASS launch-worker.mjs: refuses a ticket with no repo:* label and no --repo
PASS launch-worker.mjs: refuses a prompt file inside a repo
PASS launch-worker.mjs: refuses an engine declaring interactive: false
PASS launch-worker.mjs: refuses an engine that omits interactive entirely
PASS launch-worker.mjs: refuses headless args behind an interactive declaration
PASS launch-worker.mjs: refuses a headless token hidden in the command field
PASS launch-worker.mjs: an interactive codex entry launches
PASS launch-worker.mjs: the codex plan's command carries no headless token
PASS launch-worker.mjs: accepts codex -p, which is --profile and not --print
PASS launch-worker.mjs: still refuses codex exec behind an interactive declaration
PASS launch-worker.mjs: refuses codex e, the documented alias for exec
PASS launch-worker.mjs: refuses an engine binary with no profile rather than waving it through
PASS launch-worker.mjs: refuses a missing prompt file
PASS launch-worker.mjs: refuses a non-Linear issue identifier
PASS launch-worker.mjs: injects the standing worker contract into a prompt that lacks it
PASS launch-worker.mjs: does not stack a second copy on relaunch
PASS launch-worker.mjs: answers claude's trust screen on the created-or-trust wording, from the terminal text alone
PASS launch-worker.mjs: answers claude's trust screen on the trust-the-files wording, from the terminal text alone
PASS launch-worker.mjs: answers claude's trust screen on the trust-this-folder wording, from the terminal text alone
PASS launch-worker.mjs: answers codex's trust screen on the trust-the-contents wording, from the terminal text alone
PASS launch-worker.mjs: a pointer that lands on the first send exits 0 with the plan unchanged
PASS launch-worker.mjs: a pointer that lands is sent exactly once
PASS launch-worker.mjs: a pointer the composer swallowed is re-sent, and the plan reports how many sends it took
PASS launch-worker.mjs: a pointer that never becomes a user turn is a launch FAILURE, not a success
PASS launch-worker.mjs: the undelivered launch is bounded, not retried forever
PASS launch-worker.mjs: a TUI that keeps painting is never sent to a second time
PASS launch-worker.mjs: a TUI that never goes quiet is a launch failure naming that cause
PASS launch-worker.mjs: the undelivered launch leaves no orphaned worktree
PASS launch-worker.mjs: the injected contract still forbids asking a question
PASS launch-worker.mjs: the injected contract still forbids dropping a blocked criterion
PASS launch-worker.mjs: the injected contract still forbids watching its own PR or another ticket
PASS launch-worker.mjs: the injected contract still forbids arming a monitor that outlives the contract
PASS launch-worker.mjs: the injected contract still forbids resolving a watch-and-stop conflict by doing both
PASS launch-worker.mjs: the injected contract still forbids merging or pushing to main
PASS launch-worker.mjs: the injected contract still forbids blanket staging that sweeps in a sibling's artifacts
PASS launch-worker.mjs: the injected contract still forbids pushing a commit it has not read back
PASS launch-worker.mjs: the injected contract still forbids writing into another worker's worktree
PASS launch-worker.mjs: the injected contract still forbids delegating independent slices while keeping conflicts and PR evidence inline
PASS nudge-worker.mjs: rejects multi-line text
PASS nudge-worker.mjs: rejects --text together with --prompt-file
PASS nudge-worker.mjs: rejects a non-positive --wait-attempts
PASS nudge-worker.mjs: refuses to send while the worker is busy
PASS nudge-worker.mjs: an orca failure that is not a timeout is a tool error
PASS nudge-worker.mjs: sends once the worker is idle
PASS nudge-worker.mjs: refuses a tui-idle that is still repainting, which is a worker mid-turn
PASS nudge-worker.mjs: --dry-run calls orca not at all
PASS pr-watch.mjs: a verdict sitting on an OLDER commit does not satisfy the watch
PASS pr-watch.mjs: a fresh CHANGES_REQUESTED on the current head fires, which is the silent-spin regression
PASS pr-watch.mjs: a fresh approval fires
PASS pr-watch.mjs: an approval the caller already acted on reports mergeable-and-approved instead of repeating itself
PASS pr-watch.mjs: a failing check beats an approval
PASS pr-watch.mjs: a merged PR ends the watch
PASS pr-watch.mjs: watching several PRs reports whichever one transitioned
PASS pr-watch.mjs: the polling loop sleeps, re-polls and times out reporting it, without --once
PASS pr-watch.mjs: the timed-out watch really polled more than once
PASS pr-watch.mjs: refuses a baseline for a PR it is not watching
PASS pr-watch.mjs: refuses a malformed baseline rather than ignoring it
PASS pr-watch.mjs: refuses a repo that is not an owner\/name slug
PASS worker-watch.mjs: an empty fleet says so rather than printing nothing
PASS worker-watch.mjs: a repainting terminal is BUSY, and its repaint tail yields no output lines
PASS worker-watch.mjs: the repaint tail is stripped to nothing rather than printed raw
PASS worker-watch.mjs: the ticket's Linear state is reported alongside liveness
PASS worker-watch.mjs: two identical samples are IDLE, and real content survives the stripping
PASS worker-watch.mjs: the last meaningful lines survive, so a worker stopped on a question is readable
PASS worker-watch.mjs: an unreadable contract verdict is reported, never silently dropped
PASS worker-watch.mjs: refuses a repo outside orchestrator.json
PASS worker-watch.mjs: refuses a non-positive --lines
PASS teardown-worktree.mjs: refuses a valueless issue selector
PASS teardown-worktree.mjs: refuses a valueless worktree selector
PASS teardown-worktree.mjs: refuses a valueless base
PASS teardown-worktree.mjs: runtime_unavailable is success when filesystem and git verification prove removal
PASS teardown-worktree.mjs: verified removal actually deleted the fixture
PASS teardown-worktree.mjs: a dirty tree is refused with its uncommitted path
PASS teardown-worktree.mjs: dirty refusal leaves the tree untouched
PASS teardown-worktree.mjs: content absent from the target branch is refused
PASS teardown-worktree.mjs: a closed-looking but non-Done Linear issue is refused
PASS teardown-worktree.mjs: a repainting terminal is refused because the worker is still working
PASS teardown-worktree.mjs: an ok removal response is failure when the directory survives
PASS teardown-worktree.mjs: a path selector accepts a squash-merged tree without ancestry
PASS teardown-worktree.mjs: a squash-merged tree is present when the target advanced on unrelated paths
PASS orca-web-port.mjs: rejects multiple operation flags
PASS orca-web-port.mjs: rejects --name without --derive
PASS orca-web-port.mjs: requires a name for --derive
PASS orca-web-port.mjs: derives the same port for the same name
PASS orca-web-port.mjs: keeps generated ports inside the guarded web window
PASS orca-web-port.mjs: a linked worktree without setup refuses to guess
PASS orca-web-port.mjs: setup assigns the first linked worktree
PASS orca-web-port.mjs: setup assigns a different linked worktree
PASS orca-web-port.mjs: linked worktrees report their own distinct assignments
PASS orca-web-port.mjs: setup does not clobber an existing local environment file
PASS orca-web-port.mjs: refuses a deterministic port collision before persisting
PASS orca-web-port.mjs: collision refusal leaves no marker behind
PASS orca-web-port.mjs: the primary checkout keeps the default port
PASS orca-web-port.mjs: setup refuses the primary checkout
PASS worker-status.mjs: requires --worktree
PASS worker-status.mjs: requires a Linear issue identifier
PASS compose-prompt.mjs: writes the body and chronological, attributed comments without changing fenced Markdown
PASS compose-prompt.mjs: comment order, attribution, and fences survive composition
PASS launch-worker.mjs: accepts a composed prompt file unchanged
PASS compose-prompt.mjs: omits the comments heading when the issue has no comments
PASS compose-prompt.mjs: zero comments add no empty heading
PASS wave-plan.mjs: orders a blockedBy pair into two waves
PASS wave-plan.mjs: wave 1 is the unblocked ticket
PASS wave-plan.mjs: reach counts the whole downstream chain, not just direct blockers
PASS wave-plan.mjs: a wave-1 ticket at the strike limit is reported, not dropped
PASS wave-plan.mjs: text mode marks the same strike-limit ticket
PASS wave-plan.mjs: an empty project is nothing to plan
PASS wave-plan.mjs: fetches 100 relations in a bounded pool while preserving the table order
PASS wave-plan.mjs: names a failing relation fetch without an execFile stack trace
PASS wave-plan.mjs: keeps planning when one external blocker cannot be fetched
PASS check-dashes.mjs: an em dash in text is rejected
PASS check-dashes.mjs: clean text passes
PASS capture-surfaces.mjs: uses the primary checkout default when no base URL is supplied
PASS capture-surfaces.mjs: uses the linked worktree port when no base URL is supplied
PASS capture-surfaces.mjs: keeps an explicit base URL over the assigned port
PASS capture-surfaces.mjs: refuses capture when a linked worktree has no assigned port
PASS check-ticket.mjs: an incomplete body is rejected
PASS check-ticket.mjs: a missing body file is a usage error
PASS check-push-target.mjs: a push to main is blocked
PASS check-push-target.mjs: a push to a feature branch is allowed
PASS check-frontmatter.mjs: runs from any cwd

ORBIT TOOLS GATE OK

node .claude/hooks/test-hooks.mjs
# _lib unit
PASS git: push main blocks
PASS git: push feature allows
PASS git: no-verify blocks
PASS git: commit -n blocks
PASS git: bare push on main blocks
PASS git: bare push on feature allows
PASS git: push main in an Orbit repo blocks
PASS git: push main in a non-Orbit repo allows
PASS git: cd to a non-Orbit repo then push main allows
PASS git: -C into a non-Orbit repo allows
PASS git: unresolvable remote still blocks (fails safe)
PASS git: throwing remote resolver still blocks (fails safe)
PASS git: bare push on main in a non-Orbit repo allows
PASS git: no-verify blocks even in a non-Orbit repo
PASS git: chained unprotected push then Orbit push main blocks
PASS git: chained Orbit feature push then unprotected push main allows
PASS git: chained unprotected push then bare Orbit push on main blocks
PASS git: heredoc message mentioning the flag allows
PASS git: heredoc message mentioning push main allows
PASS git: flag outside the heredoc still blocks
PASS git: shell heredoc keeps its body in scope
PASS git: body mentioning a shell heredoc still gets stripped
PASS git-worktree: --force blocks
PASS git-worktree: -f short form blocks
PASS git-worktree: no force allows
PASS git-worktree: unrelated git allows
PASS git-worktree: -f inside a path is not the flag
PASS git-worktree: heredoc message naming the flag allows
PASS git-worktree: force on a later chained command allows
PASS git-worktree: force in the same segment still blocks
PASS ef-index: raw CREATE UNIQUE INDEX blocks
PASS ef-index: raw DROP INDEX without IF EXISTS blocks
PASS ef-index: IF NOT EXISTS form allows
PASS ef-index: CreateIndex API call allows
PASS ef-index: off-path skipped
PASS ef-index: batched Sql with one non-idempotent statement blocks
PASS ef-index: batched Sql with all idempotent statements allows
PASS linear: issueCreate blocks
PASS linear: issueUpdate blocks
PASS linear: commentCreate blocks
PASS linear: named mutation with variables blocks
PASS linear: aliased field is judged on the real field
PASS linear: projectUpdate allows
PASS linear: project content read allows
PASS linear: bare selection-set read allows
PASS linear: a non-Linear endpoint is none of this gate's business
PASS linear: projectUpdate batched with issueCreate blocks
PASS linear: nested issueCreate-shaped selection allows
PASS linear: a brace inside a string argument cannot smuggle issueCreate
PASS linear: same smuggle inside a JSON payload blocks
PASS linear: projectUpdate whose content contains a brace still allows
PASS linear: file-reference payload blocks
PASS linear: -d @file payload blocks
PASS linear: double-quoted @file payload blocks
PASS linear: single-quoted @file payload blocks
PASS linear: --data=@file payload blocks
PASS linear: -T upload sends the file as the body and blocks
PASS linear: --upload-file blocks
PASS linear: attached -d@file blocks
PASS linear: attached -Tfile blocks
PASS linear: inline -d read is not mistaken for a file body
PASS linear: attached inline -d'{...}' read still allows
PASS linear: --json @file blocks
PASS linear: a subshell body blocks
PASS linear: an orca --json read line near the endpoint does not block
PASS linear: a subshell in the auth header does not block a read
PASS linear: the bare word mutation is prose, allows
PASS linear: the hook's own hyphenated name is not the keyword
PASS linear: shorthand projectCreate allows
PASS linear: shorthand issueCreate still blocks
PASS linear: non-string input allows

# claude code hooks (real files)
PASS cc git-guardrails: push main -> 2
PASS cc git-guardrails: feature -> 0
PASS cc git-guardrails: worktree remove --force -> 2
PASS cc git-guardrails: worktree remove (no force) -> 0
PASS cc ef-index: raw CREATE INDEX -> 2
PASS cc ef-index: IF NOT EXISTS -> 0
PASS cc linear: bash issueCreate -> 2
PASS cc linear: bash projectUpdate -> 0
PASS cc linear: bash content read -> 0
PASS cc linear: written script that mutates -> 2
PASS cc linear: MultiEdit that mutates -> 2
PASS cc linear: the gate's own fixtures -> 0
PASS cc linear: unrelated command -> 0

# agent frontmatter (fails-open Bash(...) guard)
PASS agents: audit-readonly.md declares no fails-open parenthesized specifier
PASS agents: completeness-critic.md declares no fails-open parenthesized specifier
PASS agents: contract-aligner.md declares no fails-open parenthesized specifier
PASS agents: design-reviewer.md declares no fails-open parenthesized specifier
PASS agents: design-specialist.md declares no fails-open parenthesized specifier
PASS agents: Explore.md declares no fails-open parenthesized specifier
PASS agents: i18n-syncer.md declares no fails-open parenthesized specifier
PASS agents: parity-checker.md declares no fails-open parenthesized specifier
PASS agents: product-manager.md declares no fails-open parenthesized specifier
PASS agents: security-reviewer.md declares no fails-open parenthesized specifier
PASS agents: web-researcher.md declares no fails-open parenthesized specifier
PASS agents: contract-aligner.md declares no fails-open parenthesized specifier
PASS agents: security-reviewer.md declares no fails-open parenthesized specifier
PASS agents: the guard actually scanned agent files

# linear gate false positives (this repo's tracked docs)
PASS linear gate: blocks none of this repo's tracked docs
PASS linear gate: the doc scan actually read files

ORBIT HOOK PARITY OK

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #619 - feat(tools): teardown completed worktrees

Recommendation: NEEDS WORK (1 High finding; everything else is clean)

Summary

Reviewed the full diff (.claude/skills/orchestrate/SKILL.md, tools/README.md,
tools/teardown-worktree.mjs, tools/test-tools.mjs) against the PR review rubric. This
is a tools-only change (no apps/*, packages/shared/src/types, or orbit-api touched),
so Parity, i18n, Contract-drift, DESIGN.md, backend-hard-rules, and FEATURES.md dimensions
are N/A. Harness Execution (dimension 15) is verified GREEN via this PR's own CI run
(Guards / Harness Execution: SUCCESS, matching the PR body's node tools/test-tools.mjs
and node .claude/hooks/test-hooks.mjs output), and the added test coverage for the new
tool is extensive (usage errors, all-good/runtime-unavailable removal, dirty tree,
unmerged tree, non-Done issue, repainting terminal, lying removal response, path selector
with squash merge, sibling-branch-advance).

Findings

High

[High] normalize() crashes on any terminal missing worktreePath, before the worktree-scoped filter even runs

  • dimension: Type safety (untyped external payload across a trust boundary)
  • location: orbit-ui-mobile/tools/teardown-worktree.mjs:79, 95
  • issue: normalize = (path) => resolve(selectorPath(path))... calls Node's path.resolve()
    on whatever orca terminal list returns, for EVERY terminal in the whole fleet
    (terminals.filter((terminal) => normalize(terminal.worktreePath) === normalize(path))
    runs over the unfiltered list before narrowing to this worktree). resolve(undefined)
    throws TypeError [ERR_INVALID_ARG_TYPE] — an unhandled crash, not one of this tool's
    own fail(code, message) exits.
  • risk: This is untyped external CLI output crossing a trust boundary with no narrowing.
    The sibling tool that already iterates the same terminal list, worker-watch.mjs,
    guards for exactly this: const normalize = (path) => (path ?? "").replaceAll(...)
    (tools/worker-watch.mjs:118) — it never calls resolve() and treats a missing path as
    an empty string. teardown-worktree.mjs's normalize diverges from that established,
    defensive convention and will hard-crash the automated /orchestrate teardown step for
    a whole ticket if ANY terminal in the live fleet (e.g. a main-repo/orchestrator terminal
    not tied to a child worktree) is returned without a worktreePath — not just terminals
    belonging to the worktree being torn down. No fixture in the added test-tools.mjs
    coverage exercises a terminal list with more than one entry or with a missing
    worktreePath, so this path is unexercised by the otherwise-thorough harness.
  • fix: Make normalize null-safe before resolving, matching worker-watch.mjs's pattern,
    e.g. const normalize = (path) => (path ? resolve(selectorPath(path)) : "").replaceAll(...),
    or filter out entries with no worktreePath before normalizing on line 95.
  • reference: CLAUDE.md rule 3 / rubric dimension 6; tools/worker-watch.mjs:118 as the
    established in-repo precedent.

Medium

[Medium] Several new refusal branches have no dedicated test case

  • dimension: Harness changes need EXECUTED evidence (missing test)
  • location: orbit-ui-mobile/tools/teardown-worktree.mjs:53, 54, 85, 86, 87, 141, 144
  • issue: the exactly-one-selector refusal (neither or both of --issue/--worktree), the
    --issue format guard, the not-found case, refusing a worktree with
    isMainWorktree: true, refusing a worktree with no linkedLinearIssue, and the
    branch-still-exists-after--D failure all have real, reachable code paths but no
    matching row in tools/test-tools.mjs's teardownWorktreeCases. The tool's other
    decision paths (dirty, tree-not-present, not-Done, repainting terminal, lying removal
    response, squash-merge equivalence) are all well covered — these six are the gap.
  • risk: an unexercised refusal branch is exactly the class of defect this rubric dimension
    exists to catch — it merges unexecuted and the next change to this tool inherits the
    same hole.
  • fix: add a check(...) row per branch, following the same stageTeardownWorktree /
    teardownPlan pattern already used for the other cases.
  • reference: rubric dimension 15; severity ladder "missing test".

Low

[Low] Unused import realpathSync

  • dimension: Dead / stale code
  • location: orbit-ui-mobile/tools/teardown-worktree.mjs:9
  • issue: realpathSync is imported from node:fs but never referenced anywhere else in
    the file. tools/ has no root eslint.config.*, so this is not caught by any lint gate.
  • fix: drop realpathSync from the import.
  • reference: CLAUDE.md rule 2.

Subagents

Agent Verdict
parity-checker N/A — no apps/web/** or apps/mobile/** files changed
i18n-syncer N/A — no user-facing strings changed
contract-aligner N/A — no orbit-api or packages/shared/src/types changes
security-reviewer N/A — no orbit-api code changed
design-reviewer N/A — no UI files changed

Validation

Sourced from this PR's own passing CI checks (local execution of node tools/test-tools.mjs
/ node .claude/hooks/test-hooks.mjs was not possible from this review session's sandbox).

Check Result
Lint PASS
Type check PASS
Unit tests PASS
Harness Execution (test-tools.mjs + test-hooks.mjs) PASS
Build PASS

Deferred

  • Parity, i18n, Contract-drift + backward-compat, DESIGN.md/AI-slop, Backend hard rules,
    FEATURES.md parity: N/A, diff touches none of their surfaces.
  • Validation above is sourced from this PR's own CI run rather than a fresh local
    execution, because node invocations were not available in this review session's
    sandbox.

What's good

  • The four-check evidence gate (clean tree, tree-present-in-target via content diff
    rather than ancestry, linear-done, terminals-idle) correctly handles squash-merges
    without requiring commit ancestry, with tests proving both the squash-merge-present and
    sibling-branch-advanced-on-unrelated-paths cases.
  • Orca's worktree rm response is correctly treated as non-authoritative; removal is
    independently verified from the filesystem and git worktree list, matching the tool's
    own doc comment and the PR's stated design decision.
  • SKILL.md and tools/README.md updates accurately describe the shipped tool's
    contract and refusal behavior.
  • Harness Execution CI check is green for this exact diff.

Recommendation

Fix the normalize() null-safety gap before merge (High — it can crash the automated
/orchestrate teardown step on a live fleet with unrelated open terminals). The Medium
test-coverage gap and the Low unused import are worth cleaning up in the same pass but do
not block on their own.

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon

Copy link
Copy Markdown
Owner Author

Review round 2 complete in f8d69f5.

High: terminal-path normalization now ignores absent or non-string paths before resolving. The new multi-entry-terminal fixture was run against the pre-fix normalizer and failed with ERR_INVALID_ARG_TYPE; it passes after the guard.

Medium: added harness rows for no selector, both selectors, malformed issue identifiers, absent worktrees, primary worktrees, missing linked issues, branch deletion failure, and a branch that remains after a reported deletion. The branch cases use isolated real Git reference-transaction hooks.

Low: removed the unused realpathSync import.

No inline review threads are available to reply to or resolve; the findings are top-level review sections.

node tools/test-tools.mjs
# structural coverage
PASS every tools/ script has coverage (25 scripts)
PASS the coverage guard actually enumerated scripts

# universal contract (tools/CONVENTIONS.md)
PASS a real bash is resolvable
PASS agent-review.ps1: --help exits 0 with usage on stdout
PASS agent-review.ps1: invalid input is refused
PASS agent-review.sh: --help exits 0 with usage on stdout
PASS agent-review.sh: invalid input is refused
PASS arch-map.mjs: --help exits 0 with usage on stdout
PASS arch-map.mjs: invalid input is refused
PASS capture-surfaces.mjs: --help exits 0 with usage on stdout
PASS capture-surfaces.mjs: invalid input is refused
PASS check-copy.mjs: --help exits 0 with usage on stdout
PASS check-copy.mjs: invalid input is refused
PASS check-dashes.mjs: --help exits 0 with usage on stdout
PASS check-dashes.mjs: invalid input is refused
PASS check-frontmatter.mjs: --help exits 0 with usage on stdout
PASS check-frontmatter.mjs: invalid input is refused
PASS check-push-target.mjs: --help exits 0 with usage on stdout
PASS check-push-target.mjs: invalid input is refused
PASS check-suppressions-ratchet.mjs: --help exits 0 with usage on stdout
PASS check-suppressions-ratchet.mjs: invalid input is refused
PASS check-ticket.mjs: --help exits 0 with usage on stdout
PASS check-ticket.mjs: invalid input is refused
PASS compose-prompt.mjs: --help exits 0 with usage on stdout
PASS compose-prompt.mjs: invalid input is refused
PASS launch-worker.mjs: --help exits 0 with usage on stdout
PASS launch-worker.mjs: invalid input is refused
PASS merge-sweep-cov.sh: --help exits 0 with usage on stdout
PASS merge-sweep-cov.sh: invalid input is refused
PASS merge-sweep.sh: --help exits 0 with usage on stdout
PASS merge-sweep.sh: invalid input is refused
PASS new-ticket.mjs: --help exits 0 with usage on stdout
PASS new-ticket.mjs: invalid input is refused
PASS nudge-worker.mjs: --help exits 0 with usage on stdout
PASS nudge-worker.mjs: invalid input is refused
PASS orca-web-port.mjs: --help exits 0 with usage on stdout
PASS orca-web-port.mjs: invalid input is refused
PASS pr-watch.mjs: --help exits 0 with usage on stdout
PASS pr-watch.mjs: invalid input is refused
PASS redesign-coverage.mjs: --help exits 0 with usage on stdout
PASS redesign-coverage.mjs: invalid input is refused
PASS rollup.sh: --help exits 0 with usage on stdout
PASS rollup.sh: invalid input is refused
PASS surface-manifest.mjs: --help exits 0 with usage on stdout
PASS surface-manifest.mjs: invalid input is refused
PASS teardown-worktree.mjs: --help exits 0 with usage on stdout
PASS teardown-worktree.mjs: invalid input is refused
PASS wave-plan.mjs: --help exits 0 with usage on stdout
PASS wave-plan.mjs: invalid input is refused
PASS worker-status.mjs: --help exits 0 with usage on stdout
PASS worker-status.mjs: invalid input is refused
PASS worker-watch.mjs: --help exits 0 with usage on stdout
PASS worker-watch.mjs: invalid input is refused

# decision paths
PASS new-ticket.mjs: validates the identifier orca reported
PASS new-ticket.mjs: a created but defective ticket exits 1 naming it
PASS new-ticket.mjs: an orca failure creates nothing and exits 3
PASS new-ticket.mjs: success with no identifier is a tool error, never a silent pass
PASS new-ticket.mjs: requires --project so the ticket cannot be orphaned
PASS launch-worker.mjs: resolves the repo from the repo:* label
PASS launch-worker.mjs: derives the contract branch from the title
PASS launch-worker.mjs: worker:sonnet swaps the configured opus
PASS launch-worker.mjs: refuses a repo:* label with no repos entry
PASS launch-worker.mjs: refuses a ticket with no repo:* label and no --repo
PASS launch-worker.mjs: refuses a prompt file inside a repo
PASS launch-worker.mjs: refuses an engine declaring interactive: false
PASS launch-worker.mjs: refuses an engine that omits interactive entirely
PASS launch-worker.mjs: refuses headless args behind an interactive declaration
PASS launch-worker.mjs: refuses a headless token hidden in the command field
PASS launch-worker.mjs: an interactive codex entry launches
PASS launch-worker.mjs: the codex plan's command carries no headless token
PASS launch-worker.mjs: accepts codex -p, which is --profile and not --print
PASS launch-worker.mjs: still refuses codex exec behind an interactive declaration
PASS launch-worker.mjs: refuses codex e, the documented alias for exec
PASS launch-worker.mjs: refuses an engine binary with no profile rather than waving it through
PASS launch-worker.mjs: refuses a missing prompt file
PASS launch-worker.mjs: refuses a non-Linear issue identifier
PASS launch-worker.mjs: injects the standing worker contract into a prompt that lacks it
PASS launch-worker.mjs: does not stack a second copy on relaunch
PASS launch-worker.mjs: answers claude's trust screen on the created-or-trust wording, from the terminal text alone
PASS launch-worker.mjs: answers claude's trust screen on the trust-the-files wording, from the terminal text alone
PASS launch-worker.mjs: answers claude's trust screen on the trust-this-folder wording, from the terminal text alone
PASS launch-worker.mjs: answers codex's trust screen on the trust-the-contents wording, from the terminal text alone
PASS launch-worker.mjs: a pointer that lands on the first send exits 0 with the plan unchanged
PASS launch-worker.mjs: a pointer that lands is sent exactly once
PASS launch-worker.mjs: a pointer the composer swallowed is re-sent, and the plan reports how many sends it took
PASS launch-worker.mjs: a pointer that never becomes a user turn is a launch FAILURE, not a success
PASS launch-worker.mjs: the undelivered launch is bounded, not retried forever
PASS launch-worker.mjs: a TUI that keeps painting is never sent to a second time
PASS launch-worker.mjs: a TUI that never goes quiet is a launch failure naming that cause
PASS launch-worker.mjs: the undelivered launch leaves no orphaned worktree
PASS launch-worker.mjs: the injected contract still forbids asking a question
PASS launch-worker.mjs: the injected contract still forbids dropping a blocked criterion
PASS launch-worker.mjs: the injected contract still forbids watching its own PR or another ticket
PASS launch-worker.mjs: the injected contract still forbids arming a monitor that outlives the contract
PASS launch-worker.mjs: the injected contract still forbids resolving a watch-and-stop conflict by doing both
PASS launch-worker.mjs: the injected contract still forbids merging or pushing to main
PASS launch-worker.mjs: the injected contract still forbids blanket staging that sweeps in a sibling's artifacts
PASS launch-worker.mjs: the injected contract still forbids pushing a commit it has not read back
PASS launch-worker.mjs: the injected contract still forbids writing into another worker's worktree
PASS launch-worker.mjs: the injected contract still forbids delegating independent slices while keeping conflicts and PR evidence inline
PASS nudge-worker.mjs: rejects multi-line text
PASS nudge-worker.mjs: rejects --text together with --prompt-file
PASS nudge-worker.mjs: rejects a non-positive --wait-attempts
PASS nudge-worker.mjs: refuses to send while the worker is busy
PASS nudge-worker.mjs: an orca failure that is not a timeout is a tool error
PASS nudge-worker.mjs: sends once the worker is idle
PASS nudge-worker.mjs: refuses a tui-idle that is still repainting, which is a worker mid-turn
PASS nudge-worker.mjs: --dry-run calls orca not at all
PASS pr-watch.mjs: a verdict sitting on an OLDER commit does not satisfy the watch
PASS pr-watch.mjs: a fresh CHANGES_REQUESTED on the current head fires, which is the silent-spin regression
PASS pr-watch.mjs: a fresh approval fires
PASS pr-watch.mjs: an approval the caller already acted on reports mergeable-and-approved instead of repeating itself
PASS pr-watch.mjs: a failing check beats an approval
PASS pr-watch.mjs: a merged PR ends the watch
PASS pr-watch.mjs: watching several PRs reports whichever one transitioned
PASS pr-watch.mjs: the polling loop sleeps, re-polls and times out reporting it, without --once
PASS pr-watch.mjs: the timed-out watch really polled more than once
PASS pr-watch.mjs: refuses a baseline for a PR it is not watching
PASS pr-watch.mjs: refuses a malformed baseline rather than ignoring it
PASS pr-watch.mjs: refuses a repo that is not an owner\/name slug
PASS worker-watch.mjs: an empty fleet says so rather than printing nothing
PASS worker-watch.mjs: a repainting terminal is BUSY, and its repaint tail yields no output lines
PASS worker-watch.mjs: the repaint tail is stripped to nothing rather than printed raw
PASS worker-watch.mjs: the ticket's Linear state is reported alongside liveness
PASS worker-watch.mjs: two identical samples are IDLE, and real content survives the stripping
PASS worker-watch.mjs: the last meaningful lines survive, so a worker stopped on a question is readable
PASS worker-watch.mjs: an unreadable contract verdict is reported, never silently dropped
PASS worker-watch.mjs: refuses a repo outside orchestrator.json
PASS worker-watch.mjs: refuses a non-positive --lines
PASS teardown-worktree.mjs: refuses no selector
PASS teardown-worktree.mjs: refuses both selectors
PASS teardown-worktree.mjs: refuses a malformed Linear issue selector
PASS teardown-worktree.mjs: refuses a valueless issue selector
PASS teardown-worktree.mjs: refuses a valueless worktree selector
PASS teardown-worktree.mjs: refuses a valueless base
PASS teardown-worktree.mjs: refuses an issue with no active worktree
PASS teardown-worktree.mjs: refuses a primary checkout
PASS teardown-worktree.mjs: refuses a worktree without a linked Linear issue
PASS teardown-worktree.mjs: runtime_unavailable is success when filesystem and git verification prove removal
PASS teardown-worktree.mjs: verified removal actually deleted the fixture
PASS teardown-worktree.mjs: ignores another fleet terminal without a worktree path
PASS teardown-worktree.mjs: a dirty tree is refused with its uncommitted path
PASS teardown-worktree.mjs: dirty refusal leaves the tree untouched
PASS teardown-worktree.mjs: content absent from the target branch is refused
PASS teardown-worktree.mjs: a closed-looking but non-Done Linear issue is refused
PASS teardown-worktree.mjs: a repainting terminal is refused because the worker is still working
PASS teardown-worktree.mjs: an ok removal response is failure when the directory survives
PASS teardown-worktree.mjs: a path selector accepts a squash-merged tree without ancestry
PASS teardown-worktree.mjs: a squash-merged tree is present when the target advanced on unrelated paths
PASS teardown-worktree.mjs: reports a branch deletion failure after removing the worktree
PASS teardown-worktree.mjs: reports a branch that remains after deletion
PASS orca-web-port.mjs: rejects multiple operation flags
PASS orca-web-port.mjs: rejects --name without --derive
PASS orca-web-port.mjs: requires a name for --derive
PASS orca-web-port.mjs: derives the same port for the same name
PASS orca-web-port.mjs: keeps generated ports inside the guarded web window
PASS orca-web-port.mjs: a linked worktree without setup refuses to guess
PASS orca-web-port.mjs: setup assigns the first linked worktree
PASS orca-web-port.mjs: setup assigns a different linked worktree
PASS orca-web-port.mjs: linked worktrees report their own distinct assignments
PASS orca-web-port.mjs: setup does not clobber an existing local environment file
PASS orca-web-port.mjs: refuses a deterministic port collision before persisting
PASS orca-web-port.mjs: collision refusal leaves no marker behind
PASS orca-web-port.mjs: the primary checkout keeps the default port
PASS orca-web-port.mjs: setup refuses the primary checkout
PASS worker-status.mjs: requires --worktree
PASS worker-status.mjs: requires a Linear issue identifier
PASS compose-prompt.mjs: writes the body and chronological, attributed comments without changing fenced Markdown
PASS compose-prompt.mjs: comment order, attribution, and fences survive composition
PASS launch-worker.mjs: accepts a composed prompt file unchanged
PASS compose-prompt.mjs: omits the comments heading when the issue has no comments
PASS compose-prompt.mjs: zero comments add no empty heading
PASS wave-plan.mjs: orders a blockedBy pair into two waves
PASS wave-plan.mjs: wave 1 is the unblocked ticket
PASS wave-plan.mjs: reach counts the whole downstream chain, not just direct blockers
PASS wave-plan.mjs: a wave-1 ticket at the strike limit is reported, not dropped
PASS wave-plan.mjs: text mode marks the same strike-limit ticket
PASS wave-plan.mjs: an empty project is nothing to plan
PASS wave-plan.mjs: fetches 100 relations in a bounded pool while preserving the table order
PASS wave-plan.mjs: names a failing relation fetch without an execFile stack trace
PASS wave-plan.mjs: keeps planning when one external blocker cannot be fetched
PASS check-dashes.mjs: an em dash in text is rejected
PASS check-dashes.mjs: clean text passes
PASS capture-surfaces.mjs: uses the primary checkout default when no base URL is supplied
PASS capture-surfaces.mjs: uses the linked worktree port when no base URL is supplied
PASS capture-surfaces.mjs: keeps an explicit base URL over the assigned port
PASS capture-surfaces.mjs: refuses capture when a linked worktree has no assigned port
PASS check-ticket.mjs: an incomplete body is rejected
PASS check-ticket.mjs: a missing body file is a usage error
PASS check-push-target.mjs: a push to main is blocked
PASS check-push-target.mjs: a push to a feature branch is allowed
PASS check-frontmatter.mjs: runs from any cwd

ORBIT TOOLS GATE OK

node .claude/hooks/test-hooks.mjs
# _lib unit
PASS git: push main blocks
PASS git: push feature allows
PASS git: no-verify blocks
PASS git: commit -n blocks
PASS git: bare push on main blocks
PASS git: bare push on feature allows
PASS git: push main in an Orbit repo blocks
PASS git: push main in a non-Orbit repo allows
PASS git: cd to a non-Orbit repo then push main allows
PASS git: -C into a non-Orbit repo allows
PASS git: unresolvable remote still blocks (fails safe)
PASS git: throwing remote resolver still blocks (fails safe)
PASS git: bare push on main in a non-Orbit repo allows
PASS git: no-verify blocks even in a non-Orbit repo
PASS git: chained unprotected push then Orbit push main blocks
PASS git: chained Orbit feature push then unprotected push main allows
PASS git: chained unprotected push then bare Orbit push on main blocks
PASS git: heredoc message mentioning the flag allows
PASS git: heredoc message mentioning push main allows
PASS git: flag outside the heredoc still blocks
PASS git: shell heredoc keeps its body in scope
PASS git: body mentioning a shell heredoc still gets stripped
PASS git-worktree: --force blocks
PASS git-worktree: -f short form blocks
PASS git-worktree: no force allows
PASS git-worktree: unrelated git allows
PASS git-worktree: -f inside a path is not the flag
PASS git-worktree: heredoc message naming the flag allows
PASS git-worktree: force on a later chained command allows
PASS git-worktree: force in the same segment still blocks
PASS ef-index: raw CREATE UNIQUE INDEX blocks
PASS ef-index: raw DROP INDEX without IF EXISTS blocks
PASS ef-index: IF NOT EXISTS form allows
PASS ef-index: CreateIndex API call allows
PASS ef-index: off-path skipped
PASS ef-index: batched Sql with one non-idempotent statement blocks
PASS ef-index: batched Sql with all idempotent statements allows
PASS linear: issueCreate blocks
PASS linear: issueUpdate blocks
PASS linear: commentCreate blocks
PASS linear: named mutation with variables blocks
PASS linear: aliased field is judged on the real field
PASS linear: projectUpdate allows
PASS linear: project content read allows
PASS linear: bare selection-set read allows
PASS linear: a non-Linear endpoint is none of this gate's business
PASS linear: projectUpdate batched with issueCreate blocks
PASS linear: nested issueCreate-shaped selection allows
PASS linear: a brace inside a string argument cannot smuggle issueCreate
PASS linear: same smuggle inside a JSON payload blocks
PASS linear: projectUpdate whose content contains a brace still allows
PASS linear: file-reference payload blocks
PASS linear: -d @file payload blocks
PASS linear: double-quoted @file payload blocks
PASS linear: single-quoted @file payload blocks
PASS linear: --data=@file payload blocks
PASS linear: -T upload sends the file as the body and blocks
PASS linear: --upload-file blocks
PASS linear: attached -d@file blocks
PASS linear: attached -Tfile blocks
PASS linear: inline -d read is not mistaken for a file body
PASS linear: attached inline -d'{...}' read still allows
PASS linear: --json @file blocks
PASS linear: a subshell body blocks
PASS linear: an orca --json read line near the endpoint does not block
PASS linear: a subshell in the auth header does not block a read
PASS linear: the bare word mutation is prose, allows
PASS linear: the hook's own hyphenated name is not the keyword
PASS linear: shorthand projectCreate allows
PASS linear: shorthand issueCreate still blocks
PASS linear: non-string input allows

# claude code hooks (real files)
PASS cc git-guardrails: push main -> 2
PASS cc git-guardrails: feature -> 0
PASS cc git-guardrails: worktree remove --force -> 2
PASS cc git-guardrails: worktree remove (no force) -> 0
PASS cc ef-index: raw CREATE INDEX -> 2
PASS cc ef-index: IF NOT EXISTS -> 0
PASS cc linear: bash issueCreate -> 2
PASS cc linear: bash projectUpdate -> 0
PASS cc linear: bash content read -> 0
PASS cc linear: written script that mutates -> 2
PASS cc linear: MultiEdit that mutates -> 2
PASS cc linear: the gate's own fixtures -> 0
PASS cc linear: unrelated command -> 0

# agent frontmatter (fails-open Bash(...) guard)
PASS agents: audit-readonly.md declares no fails-open parenthesized specifier
PASS agents: completeness-critic.md declares no fails-open parenthesized specifier
PASS agents: contract-aligner.md declares no fails-open parenthesized specifier
PASS agents: design-reviewer.md declares no fails-open parenthesized specifier
PASS agents: design-specialist.md declares no fails-open parenthesized specifier
PASS agents: Explore.md declares no fails-open parenthesized specifier
PASS agents: i18n-syncer.md declares no fails-open parenthesized specifier
PASS agents: parity-checker.md declares no fails-open parenthesized specifier
PASS agents: product-manager.md declares no fails-open parenthesized specifier
PASS agents: security-reviewer.md declares no fails-open parenthesized specifier
PASS agents: web-researcher.md declares no fails-open parenthesized specifier
PASS agents: contract-aligner.md declares no fails-open parenthesized specifier
PASS agents: security-reviewer.md declares no fails-open parenthesized specifier
PASS agents: the guard actually scanned agent files

# linear gate false positives (this repo's tracked docs)
PASS linear gate: blocks none of this repo's tracked docs
PASS linear gate: the doc scan actually read files

ORBIT HOOK PARITY OK

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Complete

Scope: PR #619 in thomasluizon/orbit-ui-mobile
Recommendation: APPROVE

Severity Count
Critical (incl. ⚠️ old-client breaks) 0
High 0
Medium 2
Low / Info 1

Summary

Adds tools/teardown-worktree.mjs (removes a completed Orca worktree/terminals/branch only after clean-tree, tree-present-in-target, Linear-Done, and terminals-idle all pass, verifying removal from the filesystem and git worktree list rather than trusting Orca's reply), wires it into /orchestrate's Advance step, and adds 21 hermetic test cases. Diff is tools/ + .claude/skills/ only — no apps/*, packages/shared, or orbit-api touched, so all five review subagents (parity, i18n, contract, security, design) are N/A. Hand-traced the safety-critical tree-presence logic (squash-merge, regular-merge, sibling-target-advance, partial-mismatch scenarios) and it fails closed in every case.

Findings (Medium)

  1. --base override flag has no dedicated test casetools/teardown-worktree.mjs:93. Every fixture in test-tools.mjs's teardownWorktreeRecord sets baseRef: "main" and no case ever passes --base on the CLI, so requestedBase taking precedence over worktree.baseRef is untested. Fix: add one case passing --base <ref> differing from the fixture default.
  2. worktree.branch git-rev-parse fallback has no dedicated test casetools/teardown-worktree.mjs:92. Every fixture supplies branch, so the git rev-parse --abbrev-ref HEAD fallback for an Orca payload missing .branch never executes in the suite. Fix: add one case with branch omitted from the stubbed record.

Both are per rubric dimension 15 ("a new decision path… needs its own case"), non-blocking.

Validation (read from PR's actual CI, not re-run locally)

Lint, Type Check, Unit Tests, Build, Harness Execution (both matrix legs), Cross-Platform Parity, Contract Drift, Expo SDK Pin, Suppressions Ratchet, Skill and Agent Frontmatter, Dash Ban, Copy Register, Design Token Guard, Architecture map drift — all SUCCESS. This matches the PR body's claimed gate output (ORBIT TOOLS GATE OK, ORBIT HOOK PARITY OK, lint 3/3, type-check 3/3).

What's good

  • Verification-not-trust design (filesystem + git worktree list over Orca's response) directly traces to a measured Orca runtime-disconnect failure, and is exercised end-to-end by a test that deletes a real fixture directory and checks the tool still reports success correctly.
  • Squash-merge and regular-merge both handled by the same tree-presence check; a partial mismatch on any one of the branch's own changed paths correctly refuses teardown rather than averaging it away.
  • All git/orca subprocess calls use execFileSync/spawnSync with argument arrays — no shell interpolation, no command-injection surface for branch/path values sourced from Orca/Linear data.
  • Coverage (21 cases + INVALID_INPUT row) lands in the same PR per tools/CONVENTIONS.md, and the SKILL.md prose update for /orchestrate accurately reflects the tool's real refusal/exit-code contract.

Deferred: DESIGN.md(#8), Parity(#9), i18n(#10), Contract drift(#11), Backend hard rules(#13), FEATURES.md(#14) — all N/A, no surface touched. All 4 changed files got a verdict. Local harness execution deferred to CI's own Harness Execution SUCCESS (session sandbox blocked running it directly).

@thomasluizon
thomasluizon merged commit fbf381f into main Jul 27, 2026
33 checks passed
@thomasluizon
thomasluizon deleted the feature/orb-124-close-a-worktree-automatically-when-its branch July 27, 2026 22:27
thomasluizon added a commit that referenced this pull request Jul 27, 2026
…fort high (#620)

* chore(orchestrator): flip the codex worker model to gpt-5.6-sol

Thomas's call on 2026-07-27. Reverses the "Sol never as the routine
executor" line in the 2026-07-26 ADR, which chose Terra medium on
throughput grounds (~75-450 msgs per 5h window on Sol vs ~1.3x that on
Terra) and named "PR-green-on-first-try vs retries per ticket" as the
metric that would settle it.

The ORB-124 run supplied that metric: Terra medium took two consecutive
CHANGES_REQUESTED rounds on a 3-point harness ticket (PR #619), shipping
an always-false `x && !x` validation branch, a whole-tree `git diff` that
refuses every worktree in a wave except the first to merge, an unused
import, and six reachable refusal branches with no test row. Each round
costs a full worker launch plus a review pass, so the cheaper tier was
not cheaper here.

Model id verified live against codex's own models cache: gpt-5.6-sol,
"Latest frontier agentic coding model". Reasoning effort stays medium.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zWQV8N53p7aUvvZL7rAe1

* chore(orchestrator): raise the codex worker reasoning effort to high

Sol's own default reasoning level is low, not medium, and its model card
advises starting low and turning it up for harder jobs. Ticket execution
is the harder job: the two review rounds Terra medium lost on ORB-124
were care failures (dead code, an unused import, an unguarded resolve(),
six untested refusal branches), which is what reasoning depth buys.

Rejected the tiers above it deliberately. `ultra` is "maximum reasoning
with automatic task delegation", and worker contract clause 10 already
governs when a worker fans work out to subagents, so an engine-level
auto-delegation would compete with it. `max` is priced for problems
harder than a 3-point ticket. Both stay available for per-ticket routing
once ORB-89 lands.

Supported levels read live from codex's models cache: low, medium, high,
xhigh, max, ultra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zWQV8N53p7aUvvZL7rAe1

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant