Add night-ops-directive and coderabbit-pr-gate skills - #5
Conversation
fm-herdr-spur.sh: detached daemon that watches herdr agent status via the native pane.agent_status_changed event stream (reusing herdr-eventwait.py) and, on a working->idle/done edge for a tracked EXTERNAL agent, enqueues a check wake into state/.wake-queue so fm-wake-drain surfaces it. Fills the gap where parlay-spawned herdr agents have no firstmate status file or turn-end hook. Poll fallback via herdr agent list when events incapable. Debounced, keyed by agent name, configurable via --agent / config/herdr-spur.agents / all-agents. Read-only against herdr; bash 3.2 safe.
Grooms ready ideas into dispatched work without the captain writing a brief. Per idea: formulate a concrete runnable brief via PAI Inference (plain prompts to dodge PromptGuard), classify safe (research/design/prototype) vs escalate (merge/deploy/production), then dispatch safe via fm-spawn scout or file unsafe to the review store. Safety rails: OFF by default (dry-run unless FM_GROOM_ENABLED=1), rate-limited (FM_GROOM_MAX_IN_FLIGHT), bounded per run (FM_GROOM_MAX_PER_RUN), idempotent (groom:* state label), fail-safe classify (any error escalates). 7 hermetic tests cover the rails; shellcheck-clean.
bin/fm-review-page.ts: for each open review item, write a self-contained, phone-readable HTML page under ~/pulse-pages/review/<id>/ served by Pulse, plus an index at the review root. --all sweeps every open item; positional ids render specific ones. Own dependency-free markdown renderer (headings, fenced code, lists, blockquote, links, bold/italic) in the GitHub-dark house style matching existing pulse-pages. Wires each page URL back onto the item (page_url metadata + a Page: note), idempotently. Read-only on item content except that safe append; parameterized by env for scheduling.
tests/fm-review-page.test.sh: network-free suite stubbing the review CLI on a fakebin PATH (read verbs emit canned JSON, write verbs log calls) and writing pages to a temp FM_REVIEW_PAGE_OUT. Covers single-id render, --all sweep over N items, idempotent re-render (no dup dirs, note skipped on unchanged url), artifact link rendering (url/brain/branch), markdown body rendering, wire-back assertions, empty-queue index, unknown id, and the no-args usage error. shellcheck-clean under the canonical whole-set invocation.
…nderer The inline renderer used a text placeholder to protect `code` spans across the bold/italic/link passes; that placeholder corrupted into NUL bytes and leaked bare CODE0/CODE1 tokens into rendered pages (visible on the live fm-groom item). Replace it with a split-based renderer: split on the code-span capture group so even indices are prose (escaped + emphasized) and odd indices are raw code (escaped, wrapped in <code>). No placeholder token can survive to output. Adds a regression test with a code-span-heavy body (adjacent spans, em-dashes, a multi-span list line, parenthesized spans) asserting every span renders and no CODE placeholder leaks.
…ate, file harvest
… large mined tails
…ry-run, empty, malformed, idempotency, cap
The firstmate-side half of the interactive review loop. Given <id> <verdict> [comment], it durably (a) enqueues a check-kind wake into state/.wake-queue via the sanctioned fm_wake_append helper, keyed review-decision:<id>, so fm-wake-drain surfaces it on the next supervision cycle; (b) annotates the item via 'review note'; (c) appends a JSONL audit record. Fails LOUDLY (non-zero) if the wake or annotation cannot land — never a silent ok (robots-5l8). 8 hermetic tests cover every verdict, the fail-loud path, and --stdin comments.
Turn read-only review pages into decision surfaces. Every page now carries an Approve/Decline/Comment panel that POSTs same-origin to /api/review/decision with in-page success/error feedback, plus a structured What/Why/Stakes/Recommendation/Artifact breakdown parsed from the body so the decision is answerable in place. An already-recorded 'Captain decision:' note renders as a standing-decision banner. Phone-friendly (48px tap targets, no horizontal scroll, self-contained inline JS/CSS). Also fixes the double-nav-bar bug: portal's injectShell stacked /_pulse/nav.js on top of the page's own .topbar. The page now emits <meta name="pulse-shell" content="off"> to opt out — exactly one top bar, matching how /status and /plans compose. 5 new tests (13 total).
Snapshot of uncommitted local edits (AGENTS.md, .claude/settings.json, .codex/hooks.json) so the working tree is clean for merging origin/main. Reversible; preserves local work per the never-discard rule.
…ential HOME override alone doesn't stop Claude Code's ancestor-directory CLAUDE.md walk from re-loading ~/.claude/CLAUDE.md, since firstmate's repo is nested under the real home dir. Launch from a detached worktree mirror under /private/tmp instead (refreshed to HEAD each run), with FM_ROOT_OVERRIDE so bin/ scripts still resolve real state/data/config/projects. Also implements the previously-comment-only keychain credential seeding for first-run auth. Root-caused via a background agent's /context-verified test; confirmed independently by checking the mirror's CLAUDE.md symlink and ancestor chain.
…autonomy The isolated session was still stopping for a tool-approval dialog on every command (e.g. bin/fm-session-start.sh) because its fresh $HOME had no bypass-permissions state, unlike ordinary crewmates which fm-spawn.sh already launches with --dangerously-skip-permissions. Fix: seed settings.json (permissions.defaultMode=bypassPermissions + skipDangerousModePermissionPrompt, re-applied every launch) and .claude.json's bypassPermissionsModeAccepted, plus add --dangerously-skip-permissions to the exec line for parity with fm-spawn.sh:319. Verified live: pane now shows 'bypass permissions on' at startup and runs a command with zero approval prompt.
The isolated session overrides HOME, but ~18 federated store wrapper scripts (brain, robots, task, decisions, ...) hardcode BEADS_DIR=$HOME/data/<store>/.beads at runtime, keyed off the actual process HOME rather than a baked-in path. Under isolation that resolved to a nonexistent path instead of the real Dolt-backed stores. Symlink $ISOLATED_HOME/data -> the real ~/data so those wrappers reach the real federated stores. Exposes only store data, not any PAI CLAUDE.md/hooks/skills/agent config - none of that lives under data/. Verified live: HOME inside the isolated session reported as the isolated home, yet 'brain list' and 'robots list' returned real open issues from the actual stores.
…ints Add bin/fm-bead-stamp.sh (fail-open: stamps dispatch=sent + assigns a linked bead on spawn) and a --beads <id> flag on fm-brief.sh and fm-spawn.sh. All bead-specific logic lives in two new hook directories rather than being spliced directly into fm-brief.sh/fm-spawn.sh, so those files stay a pure addition target with minimal upstream conflict surface: - bin/fm-brief-hooks.d/beads.sh: sourced before fm-brief.sh writes the Brief section; emits the Bead Receipt (dispatch=claimed on brief read) and Bead Closure (close the bead before done:) sections. - bin/fm-spawn-hooks.d/beads.sh: sourced after a successful spawn; stamps the bead via fm-bead-stamp.sh and registers a watcher check that polls the bead for status=closed and wakes firstmate for teardown. Both hook loops run each hook in its own subshell so a hook's `exit` never terminates the calling script, keeping every hook fail-open by construction. fm-spawn.sh records beads_id= in state/<id>.meta when set. --beads is rejected for --secondmate on both scripts.
…hook dir resolution
- bin/claude-account.sh: standalone launcher for per-account Claude Code isolation (CLAUDE_CONFIG_DIR, flock-serialized shared-config symlinks, onboarding/trust-dialog pre-write, settings.json flag pre-write). - bin/claude-1.sh, bin/claude-2.sh: one-line direct launchers. - fm-spawn.sh --account <N>: records account=N in meta, sets CLAUDE_TRUST_DIR to the task worktree, launches through claude-account.sh N. Optional; absent behavior is unchanged. - docs/configuration.md: Multi-account Claude Code section. - tests/claude-account.test.sh, tests/fm-spawn-account.test.sh.
…G.md Adds .agents/skills/herdr-navigation/SKILL.md so agents know how to navigate herdr panes using existing primitives (herdr pane current, neighbor, split, send-text, list). Agents commonly don't know these exist; the skill surfaces them with working examples. Also adds CHANGELOG.md to .gitignore — it is a generated session activity log, not source content.
Clarify that yolo's qualifying test for a routine gate, including a PR merge, is reversibility. Add a fork-first-push rule for non-writable default branches. Register two new agent-only skills: night-ops-directive captures the standing autonomous-dispatch directive (delegator discipline, own wake cadence via the watcher heartbeat, human-only task triage via bd transfer, fork-first pushes); coderabbit-pr-gate captures reacting to CodeRabbit's actionable review comments beyond pass/fail and handling its rate limits as a paused external wait rather than a failure.
📝 WalkthroughWalkthroughThe pull request adds Claude account isolation, Beads lifecycle integration, herdr monitoring, transcript idea mining, interactive review pages, review decision persistence, JSON grooming support, and related documentation and tests. ChangesOperational policy and repository guidance
Claude account isolation
Beads and herdr lifecycle integration
Transcript idea mining
Interactive review workflow
Shell utility portability
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Captain
participant ReviewPage
participant PulseAPI
participant ReviewCLI
Captain->>ReviewPage: select verdict and optional comment
ReviewPage->>PulseAPI: POST review decision
PulseAPI->>ReviewCLI: annotate review item
ReviewCLI-->>PulseAPI: decision result
PulseAPI-->>ReviewPage: return decision status
ReviewPage-->>Captain: display recorded decision
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 19
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (12)
.gitignore-7-7 (1)
7-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winScope the generated-file ignore rules to the repository root.
.fm-isolated-homeis only used byISOLATED_HOME="${FM_ISOLATED_HOME:-$FM_ROOT/.fm-isolated-home}", so ignore it at the root with/.fm-isolated-home.CHANGELOG.mdalso matches untracked changelogs anywhere in the tree. Use/CHANGELOG.mdif only the repository-root changelog is intended to be ignored.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.gitignore at line 7, Update the .gitignore entries for .fm-isolated-home and CHANGELOG.md to use leading slashes, restricting both generated-file ignore rules to the repository root.bin/fm-review-page.ts-824-835 (1)
824-835: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winForward the HTTP status so the failure message is not "request failed ()".
Line 832 concatenates an empty string where a status belongs, so a failure without a JSON
errorfield showsrequest failed (). The root cause is line 825: the handler forwards onlyokanddata, sores.statusis unavailable. Forward the status and use it.🐛 Proposed fix
}).then(function (res) { - return res.json().then(function (data) { return { ok: res.ok, data: data }; }); + return res.json().then(function (data) { + return { ok: res.ok, status: res.status, data: data }; + }); }).then(function (r) { if (r.ok && r.data && r.data.ok) { setFeedback('ok', 'Recorded — the first mate has been notified.'); var ts = new Date().toISOString().replace(/\\.\\d+Z$/, 'Z'); lockAfterDecision(verdict, comment, ts); } else { - var err = (r.data && r.data.error) ? r.data.error : ('request failed (' + '' + ')'); + var err = (r.data && r.data.error) ? r.data.error : ('request failed (' + r.status + ')'); setFeedback('err', 'Could not record that: ' + err); setButtons(false); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-review-page.ts` around lines 824 - 835, Update the response mapping in the promise chain before the failure handler to forward the HTTP status alongside ok and data, then use that status in the fallback error message within the r failure branch instead of concatenating an empty string.bin/fm-review-decision.sh-96-114 (1)
96-114: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winEscape the remaining control characters, or correct the comment.
json_escapecovers",\,\n,\r, and\t. Every other C0 control character passes through raw, which makes the emitted.decisions.jsonlline invalid JSON. The comment at lines 97-98 claims control-character coverage. A pasted captain comment can carry such a byte, and one bad line breaks any reader of the audit log.🐛 Proposed fix
$'\n') out+='\n' ;; $'\r') out+='\r' ;; $'\t') out+='\t' ;; + [$'\x01'-$'\x1f']) + out+="$(printf '\\u%04x' "'$ch'")" ;; *) out+="$ch" ;;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-review-decision.sh` around lines 96 - 114, Update json_escape to escape every remaining C0 control character required by JSON, not just quotes, backslashes, newline, carriage return, and tab; use JSON-compatible \u00XX escapes for control bytes below 0x20 while preserving existing handling. Ensure the audit output remains valid JSONL and keep the comment accurate about the implemented control-character coverage.bin/fm-herdr-spur.sh-259-300 (1)
259-300: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winKeep native push coverage up to date during FM_HERDR_SPUR_EVENT_BUDGET.
herdr-eventwait.pysubscriptions are built fromargv[3:], while the bash wrapper sends only the panes captured at the start offm_herdr_spur_event_block. If a watched agent starts a new pane during the event-wait block, the current native push subscription can miss its transition until the block returns. Use a shorter default budget or periodically reconcile while the native wait is active.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-herdr-spur.sh` around lines 259 - 300, Keep native push subscriptions current during fm_herdr_spur_event_block instead of passing only the initial pane_ids to herdr-eventwait.py for the entire event budget. Use a shorter default FM_HERDR_SPUR_EVENT_BUDGET or add periodic snapshot reconciliation so panes created for watched agents during the wait are subscribed before their transitions are missed..codex/hooks.json-12-21 (1)
12-21: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winAdd an existence guard and explicit timeout for the new
bd codex-hookhooks.The new
SessionStart,PreCompact,PostCompact, andUserPromptSubmithooks callbd codex-hook <Name>directly. Ifbdis unavailable, these hooks fail before the hooks context succeeds, unlike the other command hooks that guard before running. Addcommand -v bd >/dev/null 2>&1 || exit 0to each new hook and add an explicit"timeout"; keepUserPromptSubmitshort because it runs on every prompt.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.codex/hooks.json around lines 12 - 21, Update each new bd codex-hook entry for SessionStart, PreCompact, PostCompact, and UserPromptSubmit to first guard with command -v bd >/dev/null 2>&1 || exit 0, then invoke the hook. Add an explicit timeout to every entry, using a shorter timeout for UserPromptSubmit than the lifecycle hooks.bin/fm-idea-mine.ts-747-758 (1)
747-758: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
createIssueid extraction is fragile.The function returns the last whitespace-separated token of stdout. If the CLI does not support
--silent, or prints a trailing period or a hint line, the returned value is an arbitrary word rather than an id.fileHarvestthen records that value as the filed idea id, andmainpasses it toreview update. The function also returns""for empty stdout without raising an error, so the caller cannot tell success from silent failure.Validate the extracted id against the expected id shape and raise on a mismatch.
🛡️ Proposed id validation
- return (res.stdout || "").trim().split(/\s+/).pop() || ""; + const id = (res.stdout || "").trim().split(/\s+/).pop() || ""; + if (!/^[A-Za-z0-9][\w.-]*$/.test(id)) { + throw new Error(`${cmd} create printed no usable id: ${(res.stdout || "").slice(0, 200)}`); + } + return id;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-idea-mine.ts` around lines 747 - 758, Update createIssue to validate the extracted stdout value against the expected issue-id format before returning it. Raise an error when stdout is empty or the extracted value does not match that format, rather than returning an arbitrary token or an empty string; preserve the existing command failure handling.bin/fm-idea-mine.ts-331-336 (1)
331-336: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winThe token budget is not enforced when one message exceeds it.
The loop stops at
tail.length > 1. If the last message alone exceedscfg.tokenBudget, the code sends it in full. A single large pasted block or file dump then breaks the documented read-side bound and can overflow the inference context, which makesgenerateCandidatesfail and returns exit 1.Truncate the surviving message to the budget.
🛡️ Proposed truncation of the last surviving message
while (tail.length > 1 && approxTokens(charCount) > cfg.tokenBudget) { const dropped = tail.shift(); if (dropped) charCount -= dropped.text.length; } + // A single oversized message still has to respect the budget. + if (tail.length === 1 && approxTokens(charCount) > cfg.tokenBudget) { + const maxChars = cfg.tokenBudget * 4; + tail = [{ role: tail[0].role, text: `${tail[0].text.slice(-maxChars)}` }]; + charCount = tail[0].text.length; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-idea-mine.ts` around lines 331 - 336, Update the tail-trimming logic around tail, charCount, and approxTokens so the token budget is enforced even when one message remains and exceeds cfg.tokenBudget. After dropping older messages, truncate the surviving message’s text to fit the remaining budget before generateCandidates consumes it, preserving the existing behavior when the message is already within budget.tests/fm-idea-mine.test.sh-184-185 (1)
184-185: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winCover every store write in negative-path tests.
The fake store logs
create,update, andnote, but these checks do not reject all three verbs. A dry run could emitnoteand pass. Malformed or empty history could emitupdateornoteand pass. Use one assertion for\[(create|update|note)\]in all three cases.Proposed test fix
- if grep -qE '\[create\]|\[update\]' "$log"; then + if grep -qE '\[(create|update|note)\]' "$log"; then - if grep -qE '\[create\]' "$log"; then + if grep -qE '\[(create|update|note)\]' "$log"; thenAlso applies to: 212-213, 230-231
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/fm-idea-mine.test.sh` around lines 184 - 185, Update the dry-run, malformed-history, and empty-history assertions in the test to reject every fake-store write verb by matching create, update, or note with a single combined grep pattern. Apply the same \[(create|update|note)\] check to all three cases, preserving their existing failure messages and log output..agents/skills/herdr-navigation/SKILL.md-34-35 (1)
34-35: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake the split example match its description.
The comment says that a specific command runs in the new pane, but the command only supplies
--cwd. Change the comment to describe the current-directory split, or add the supported command argument.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.agents/skills/herdr-navigation/SKILL.md around lines 34 - 35, Update the comment above the herdr pane split invocation to accurately describe the current-directory split, or add a supported command argument so the new pane runs a specific command. Keep the example and its description consistent.docs/configuration.md-175-179 (1)
175-179: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUpdate the isolated-launch documentation to match the implementation.
The text says that first launch always requires a new login and that no authentication state is copied.
bin/fm-isolated-launch.shnow attempts to seed.credentials.jsonfrom the macOS Keychain and falls back to login only when extraction fails.Document the Keychain-seeding behavior. Also document the detached
FM_ISOLATED_CWDworktree and its requirement to remain outside the real home tree.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/configuration.md` around lines 175 - 179, Update the isolated-launch documentation around bin/fm-isolated-launch.sh to describe its macOS Keychain-based .credentials.json seeding and clarify that login is only required when extraction fails. Also document the detached FM_ISOLATED_CWD worktree and state that it must remain outside the real home directory tree.bin/fm-groom.sh-124-132 (1)
124-132: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winEmit valid JSON for every
--jsonrecord.
fm_groom_json_escapeescapes backslashes, quotes, newlines, and tabs. It does not escape carriage returns, backspace, form feed, or other JSON control characters. A title or model response containing these characters can produce invalid JSON.Use a JSON encoder or escape the complete control-character range.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-groom.sh` around lines 124 - 132, Update fm_groom_json_escape, used by emit_json, to produce valid JSON for every input by escaping the complete JSON control-character range, including carriage returns, backspace, and form feed, while preserving the existing handling of backslashes, quotes, newlines, and tabs. Prefer an available JSON encoder if the script already supports one; otherwise extend the escaping logic without changing emit_json’s field structure.bin/fm-brief.sh-92-95 (1)
92-95: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReject a missing or option-like
--beadsvalue.When
--beadsis last,${1-}becomes empty and the command silently creates a brief without the Beads link. When another option follows, that option can be consumed as the bead ID.Require a value before shifting. Reject values beginning with
--.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-brief.sh` around lines 92 - 95, Update the --beads handling branch to validate that a following argument exists and is not option-like before consuming it. Reject missing or --prefixed values with the script’s established argument-error behavior, and only then shift and assign BEADS_ID.
🧹 Nitpick comments (13)
.agents/skills/coderabbit-pr-gate/SKILL.md (1)
46-48: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winDefine the CodeRabbit stall bound.
“Reasonable number of backoff cycles” is not measurable. Different workers can wait or escalate at different times. Specify a maximum elapsed time or watcher-cycle count, and define the action after that bound.
Based on the PR objective, rate limits must remain paused external waits without creating an indefinite wait.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.agents/skills/coderabbit-pr-gate/SKILL.md around lines 46 - 48, Update the CodeRabbit review-waiting guidance near the stalled-review rule to define a measurable maximum watcher-cycle count or elapsed-time bound, accounting for paused external waits caused by rate limits. After that bound is reached without a review, stop waiting and report the stalled review to the captain as evidence rather than blocking indefinitely or merging silently.bin/fm-review-page.ts (4)
1026-1043: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDocument exit code 3 in HELP.
mainreturns 3 when a wire-back fails but pages were written (line 1108). The header block documents the cron use at lines 54-57, so a scheduler author needs to know that 3 means partial success rather than a hard failure. Add an EXIT section toHELP.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-review-page.ts` around lines 1026 - 1043, Update the HELP text in the `HELP` constant to add an EXIT section documenting that exit code 3 means pages were written but the store wire-back failed, indicating partial success rather than a complete failure.
851-878: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRender Stakes once per page.
extractStakes(line 853) andextractContextFields(line 855) both match a## Stakessection. For a body that carries one, the page shows the same prose twice: in the amber callout at line 921 and again as theStakesfield inside the context block. Suppress the callout when the context block already carries aStakesfield.♻️ Proposed change
- const stakes = extractStakes(item.description) const artifacts = extractArtifacts(item.description) const contextFields = extractContextFields(item.description) + const hasStakesField = contextFields.some((f) => f.label === "Stakes") + const stakes = hasStakesField ? "" : extractStakes(item.description)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-review-page.ts` around lines 851 - 878, Update the rendering flow around extractStakes and extractContextFields so the amber callout is omitted when contextFields already contains a Stakes field, while preserving the callout for stakes content not represented in the context block.
1009-1022: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winValidate the item id before it becomes a path segment and a URL segment.
item.idcomes straight from the store payload intojoin(OUT_ROOT, item.id)and into${BASE_URL}/${item.id}/. An id containing/or..writes outside the review root and yields a wrong public URL. A single shape check keeps the output root closed and costs nothing.🛡️ Proposed guard
+/** Store ids are slug-shaped; reject anything that could escape OUT_ROOT. */ +const ID_RE = /^[A-Za-z0-9._-]+$/ + /** Write one item's page and return its public URL. Idempotent (overwrite). */ function writeItemPage(item: ReviewItem): string { + if (!ID_RE.test(item.id)) { + throw new Error(`refusing to write page for unsafe item id: ${item.id}`) + } const dir = join(OUT_ROOT, item.id)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-review-page.ts` around lines 1009 - 1022, Validate item.id in writeItemPage before using it as a filesystem or URL segment, accepting only the expected single-segment identifier shape and rejecting values containing path separators or traversal components. Preserve the existing page-writing behavior for valid IDs and fail before mkdirSync or writeFileSync for invalid ones.
558-585: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename the local
escto remove the shadow of the HTML-escape helper.Line 561 shadows the module-level
escHTML escaper for the wholefor (const key of keys)block. Any future call to the HTML escaper inside that block hits a temporal dead zone or silently uses the regex-escaped key. Rename it to make the two roles distinct.♻️ Proposed rename
for (const key of keys) { - const esc = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + const keyRe = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // (1) Heading form: `## What\n...` up to the next heading, a blank-line // paragraph break, or end of body. Stopping at a blank line keeps the last // heading section from swallowing a following inline-label line (e.g. a // trailing `ARTIFACT: ...`). const headRe = new RegExp( - `(?:^|\\n)#{1,6}\\s*${esc}\\b[:\\s]*\\n([\\s\\S]*?)(?=\\n#{1,6}\\s|\\n\\s*\\n|$)`, + `(?:^|\\n)#{1,6}\\s*${keyRe}\\b[:\\s]*\\n([\\s\\S]*?)(?=\\n#{1,6}\\s|\\n\\s*\\n|$)`, "i", ) @@ - const inlineRe = new RegExp(`(?:^|\\n)\\s*${esc}\\s*:\\s*([^\\n]+)`, "i") + const inlineRe = new RegExp(`(?:^|\\n)\\s*${keyRe}\\s*:\\s*([^\\n]+)`, "i")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-review-page.ts` around lines 558 - 585, Rename the local regex-escaped key variable `esc` inside the `for (const key of keys)` loop to a distinct name, and update both `headRe` and `inlineRe` template expressions to use it, preserving the module-level HTML escape helper name.tests/fm-review-decision.test.sh (3)
68-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDrop the dead env prefix on
printf.Lines 70-72 export three variables to
printf, which does not read them. Only the right side of the pipe needs them. Removing the prefix makes the stdin branch match the non-stdin branch.♻️ Proposed cleanup
OUTPUT=$( - FM_HOME="$home" \ - FM_REVIEW_BIN="$fakebin/review" \ - FM_REVIEW_DECISIONS_LOG="$home/decisions.jsonl" \ - printf '%s' "$stdin_data" | \ + printf '%s' "$stdin_data" | \ FM_HOME="$home" \ FM_REVIEW_BIN="$fakebin/review" \ FM_REVIEW_DECISIONS_LOG="$home/decisions.jsonl" \ bash "$TOOL" "${args[@]}" 2>&1 )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/fm-review-decision.test.sh` around lines 68 - 79, Remove the FM_HOME, FM_REVIEW_BIN, and FM_REVIEW_DECISIONS_LOG environment-variable prefix from the printf command in the stdin branch; retain those variables on the bash "$TOOL" pipeline command so behavior matches the non-stdin path.
222-244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a case for the best-effort audit-log failure.
The script treats an audit-log append failure as warn-only and still exits 0 (lines 158-160). No case covers it. Point
FM_REVIEW_DECISIONS_LOGat an unwritable path, then assert exit 0, theWARNING:line, and that the wake and note still landed. That pins the asymmetry between the load-bearing steps and the best-effort step.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/fm-review-decision.test.sh` around lines 222 - 244, Extend the test suite with a best-effort audit-log failure case, using the existing run_decision and assertion helpers. Set FM_REVIEW_DECISIONS_LOG to an unwritable path, then verify exit code 0, a WARNING: message, and successful wake and note effects; register the new test with the other test calls.
191-202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the wake-queue state after the annotation failure.
Case (f) checks the exit code and the diagnostic, but not the queue. That is why the ordering behavior in
bin/fm-review-decision.sh(lines 118-145) is invisible today: the wake is already enqueued whenreview notefails. Add the assertion so the intended contract is pinned once the two steps are reordered.💚 Proposed assertion
run_decision "$home" "$fakebin" "$log" review-znt approve [ "$RC" -ne 0 ] || fail "note-failure: expected non-zero exit on store failure, got 0" assert_contains "$OUTPUT" "failed to annotate review item review-znt" "note-failure: loud diagnostic" + # A reported failure must not leave firstmate an actionable wake for a + # decision the captain was told did not record. + assert_absent "$(wake_queue "$home")" "note-failure: no wake left behind"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/fm-review-decision.test.sh` around lines 191 - 202, Update t_note_failure in the test to assert the wake-queue state after run_decision reports the annotation failure. Verify that no wake entry was enqueued when storing the review note fails, while preserving the existing non-zero exit and diagnostic assertions.bin/fm-idea-mine.ts (4)
99-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDerive the default transcript directory instead of hardcoding one machine's path.
defaultTranscriptDirembeds-Users-trilliumsmith-code-firstmate. On any other machine or checkout path the default resolves to a directory that does not exist.mineTranscriptthen returns no messages and the run exits with a benign no-op, so the failure is silent.Derive the encoded project directory from the repository root, and keep
FM_IDEA_MINE_TRANSCRIPT_DIRas the override.♻️ Proposed derivation from the script location
function defaultTranscriptDir(): string { - return join(homedir(), ".claude", "projects", "-Users-trilliumsmith-code-firstmate"); + // Claude encodes a project path by replacing every "/" with "-". + const repoRoot = resolve(fileURLToPath(import.meta.url), "..", ".."); + const encoded = repoRoot.replace(/\//g, "-"); + return join(homedir(), ".claude", "projects", encoded); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-idea-mine.ts` around lines 99 - 101, Update defaultTranscriptDir to derive the encoded project directory from the repository root based on the script location instead of hardcoding a user-specific path, while preserving the FM_IDEA_MINE_TRANSCRIPT_DIR override used by mineTranscript.
470-491: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe hardcoded 180000 ms outer timeout overrides
FM_IDEA_MINE_INFER_TIMEOUT_MS.
FM_IDEA_MINE_INFER_TIMEOUT_MSis documented as an operator control, butspawnSyncalways uses 180000 ms. If an operator raises the inner timeout above that value,spawnSynckills the child first and the run reports a generic inference failure. Derive the outer bound from the configured value plus a margin.♻️ Proposed derivation of the outer timeout
interface Config { inferCmd: string[]; + inferTimeoutMs: number;- const res = spawnSync(cmd, [...base, system, user], { - encoding: "utf8", - timeout: 180000, - maxBuffer: 32 * 1024 * 1024, - }); + const res = spawnSync(cmd, [...base, system, user], { + encoding: "utf8", + // Outer wall-clock bound must exceed the inner inference timeout. + timeout: cfg.inferTimeoutMs + 30000, + maxBuffer: 32 * 1024 * 1024, + });Set
inferTimeoutMsinloadConfigfromenvInt("FM_IDEA_MINE_INFER_TIMEOUT_MS", 150000)and reuse it indefaultInferCmd.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-idea-mine.ts` around lines 470 - 491, Update the configuration flow so loadConfig derives and stores inferTimeoutMs from FM_IDEA_MINE_INFER_TIMEOUT_MS with a 150000 ms default, and ensure defaultInferCmd reuses that value. In runInference, replace the hardcoded spawnSync timeout with the configured inferTimeoutMs plus an appropriate margin, preserving the existing inference error handling.
181-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the new
FM_IDEA_MINE_*env schema indocs/configuration.md.
loadConfigintroduces eleven new environment variables. The file header is the only place that describes them. The coding guidelines makedocs/configuration.mdthe single owner of configuration schemas, so add theFM_IDEA_MINE_*entries and the marker file location under<fm-home>/statethere.As per coding guidelines: "Keep
docs/configuration.mdas the single owner of top-level operational-home layout and configuration schemas".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-idea-mine.ts` around lines 181 - 198, Update docs/configuration.md, the designated owner of configuration schemas, to document all eleven FM_IDEA_MINE_* environment variables introduced by loadConfig, including their defaults or behavior, and document the marker file location under <fm-home>/state. Keep the schema and operational-home layout documentation centralized there.Source: Coding guidelines
948-950: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove or correct the stale
bdcomment.This comment claims
bd exposes update -d, but the file never callsbd. The update at line 923 targetscfg.reviewCmd, which defaults toreview. Reword the note to name thereviewCLI, or delete it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-idea-mine.ts` around lines 948 - 950, Correct the stale comment near the cfg.reviewCmd update to reference the review CLI rather than bd, reflecting that the command defaults to review and no bd invocation exists; alternatively, remove the comment.tests/fm-groom.test.sh (1)
193-210: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy liftAdd failure-path coverage for the grooming state machine.
These tests cover only successful spawn and successful review filing. Add mock failures for
ideas label list,ideas set-state,fm-spawn, andreview q. Assert that failed actions remain retryable and cannot create duplicate work.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/fm-groom.test.sh` around lines 193 - 210, Extend the grooming state-machine tests around the existing armed safe and unsafe cases to mock failures from ideas label list, ideas set-state, fm-spawn, and review q. For each failed action, assert the command reports failure without applying the completed-state label, preserves retryability, and does not create duplicate spawn or review work on retry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.agents/skills/coderabbit-pr-gate/SKILL.md:
- Around line 33-34: Revise the unresolved actionable CodeRabbit finding rule so
the `yolo` exception applies only to a standing `yolo` posture for routine,
reversible merges within accepted task criteria. Preserve the requirement for
captain escalation on security-sensitive actions, preventing this exception from
bypassing that authority boundary.
In @.agents/skills/night-ops-directive/SKILL.md:
- Around line 22-26: Update the direct-edit exception in the Firstmate
delegation policy to require an empty fleet and the firstmate-coding-guidelines
skill for ship tasks before editing shared tracked material. Preserve delegation
as the default, including for policy, skill, hook, and launcher files, and allow
direct edits only when these conditions are satisfied and the change is bounded
and low risk.
In `@AGENTS.md`:
- Around line 508-586: Replace both duplicated Beads sections in AGENTS.md with
one concise, scoped pointer stating that applicable project work uses Beads and
directing agents to the Beads skill or authoritative documentation for workflow
details. Remove the duplicated commands, lifecycle protocol, architecture
explanation, and profile policy so AGENTS.md is not a second owner of Beads
guidance and only one Beads heading remains.
In `@bin/claude-account.sh`:
- Around line 102-133: The settings update in bin/claude-account.sh, around the
SETTINGS handling block, must run only when the launch arguments include
--dangerously-skip-permissions; leave settings unchanged for normal launches
while preserving the existing symlink protection. Update
tests/claude-account.test.sh around the existing launcher coverage to assert a
non-flagged launch preserves the setting and add a separate
--dangerously-skip-permissions case verifying the setting is applied.
- Around line 62-86: Update the Python trust-handling block in claude-account.sh
to preserve onboarding pre-acceptance while only creating
projects.<path>.hasTrustDialogAccepted when CLAUDE_TRUST_DIR is explicitly
provided. Avoid using the default current working directory for trust
initialization, and add coverage for launching without CLAUDE_TRUST_DIR that
verifies no project trust entry is written.
In `@bin/fm-bead-stamp.sh`:
- Around line 34-42: Update the stamping flow around task set-state and task
assign so claiming the bead and marking dispatch=sent are handled as one atomic
operation, or persist a durable pending/error state that retries reconcile
before reporting success. Do not leave either metadata update partially applied
while exiting 0; ensure the script’s final status distinguishes a completed
stamp from an incomplete update.
In `@bin/fm-groom-json-field.sh`:
- Around line 24-27: Remove the unconditional process.exit(0) after the field
write in the JSON field extraction flow. Let the process exit naturally after
process.stdout.write completes, while preserving the existing object/key
validation and output behavior.
In `@bin/fm-groom.sh`:
- Around line 240-246: Update task ID generation near task_id in the enabled
dispatch path to preserve uniqueness for distinct idea IDs; do not rely solely
on deleting non-alphanumeric characters. Use a collision-free encoding or append
a deterministic hash derived from the complete id, while retaining the groom-
prefix and ensuring generated paths remain safe.
- Around line 198-203: Update the idea-record loading flow around IDEAS_BIN show
so failed or malformed responses stop processing instead of falling back to the
ID. Require a successful, valid JSON record, and extract both title and
description through fm-groom-json-field.sh using the appropriate FM_KEY values;
remove the regex-based title parsing and ID-based fallback values before
formulation or dispatch continues.
- Around line 115-119: Make the groom state machine fail closed and recoverable:
update fm_groom_already_done so label-list failures are treated as an
indeterminate/error state, not as “not processed.” In the dispatch, filing, and
review handlers, use explicit transitional and failure states; write terminal
labels only after the external action succeeds and its durable state update is
confirmed. Ensure failed state writes prevent further actions, failed spawns
remain retryable rather than terminal, and failed reviews cannot produce
groom:escalated without a valid review result.
- Around line 151-159: Update the ready-idea query in bin/fm-groom.sh to capture
the exit status of "$IDEAS_BIN" ready --json instead of masking failures with
the [] fallback. When the command fails, exit non-zero and preserve the
documented unreadable-store failure behavior; only parse IDs and handle an empty
backlog after a successful query.
In `@bin/fm-herdr-spur.sh`:
- Around line 336-350: Update the main loop around fm_herdr_spur_event_block to
measure each block invocation’s elapsed time and treat a successful return below
the normal budget as an immediate failure. For either a non-zero status or a
fast successful return, run fm_herdr_spur_reconcile "$watch_set" and sleep for
FM_HERDR_SPUR_POLL_INTERVAL before retrying; preserve normal blocking behavior
without the fallback delay when the full budget elapses.
In `@bin/fm-idea-mine.ts`:
- Around line 813-816: Update fileHarvest to compose the review body with
reviewDescription(result, filed.filedIdeas, mined) after filing ideas, then
create the review item once with that body instead of PLACEHOLDER. Pass mined at
the fileHarvest call site, and remove the later review update block in main so
the marker is written only after creation succeeds.
- Around line 366-390: The listExistingIdeas flow must distinguish a failed
ideas read from a successfully empty store, so fileHarvest skips filing or
aborts when the command is missing, exits non-zero, or returns invalid JSON.
Update the CLI invocation to match repository usage by using the supported ready
--json, show --json, and set-state operations instead of list --all --json,
while preserving normal filing only when existing-idea data was read
successfully.
In `@bin/fm-isolated-launch.sh`:
- Around line 115-123: Update the credential extraction block around the
security command to set umask 077, create a private unpredictable temporary file
with mktemp, and write credentials to that file instead of the predictable .tmp
path. Require both chmod and mv to succeed before completing the atomic rename,
and clean up the temporary file on any failure.
- Around line 73-74: Update the path override handling near ISOLATED_HOME and
ISOLATED_CWD to resolve both configured paths before setup and reject overrides
that equal or reside within REAL_HOME, including FM_ISOLATED_HOME set to
REAL_HOME. Preserve the existing defaults when overrides are absent, and fail
immediately with a clear error for unsafe values before creating or using the
isolated environment.
- Around line 106-108: Update the isolated-home data setup around the data
symlink creation so it does not expose the entire REAL_HOME/data tree. Link only
the required store directories using explicit per-store paths, and validate that
any existing isolated data path is an approved directory or symlink target
before using it; preserve the existing setup behavior for valid required stores.
In `@bin/fm-review-decision.sh`:
- Around line 118-145: Move the STORE ANNOTATION block using NOTE, NOTE_ERR, and
the review note command before the DURABLE WAKE block invoking fm_wake_append,
so annotation succeeds before any wake is queued. Preserve the existing failure
handling and payload construction, then update the ordering comments near the
script’s section descriptions to state that annotation is the first durable
write and wake enqueue follows it.
In `@bin/fm-review-page.ts`:
- Around line 142-158: Update runReview to treat any terminated child as a
failure by checking res.signal first, then rejecting cases where res.status is
not a number, before retaining the existing non-zero status check. Include the
review command and available termination details in the thrown errors so
fetchAllOpen cannot process incomplete output.
---
Minor comments:
In @.agents/skills/herdr-navigation/SKILL.md:
- Around line 34-35: Update the comment above the herdr pane split invocation to
accurately describe the current-directory split, or add a supported command
argument so the new pane runs a specific command. Keep the example and its
description consistent.
In @.codex/hooks.json:
- Around line 12-21: Update each new bd codex-hook entry for SessionStart,
PreCompact, PostCompact, and UserPromptSubmit to first guard with command -v bd
>/dev/null 2>&1 || exit 0, then invoke the hook. Add an explicit timeout to
every entry, using a shorter timeout for UserPromptSubmit than the lifecycle
hooks.
In @.gitignore:
- Line 7: Update the .gitignore entries for .fm-isolated-home and CHANGELOG.md
to use leading slashes, restricting both generated-file ignore rules to the
repository root.
In `@bin/fm-brief.sh`:
- Around line 92-95: Update the --beads handling branch to validate that a
following argument exists and is not option-like before consuming it. Reject
missing or --prefixed values with the script’s established argument-error
behavior, and only then shift and assign BEADS_ID.
In `@bin/fm-groom.sh`:
- Around line 124-132: Update fm_groom_json_escape, used by emit_json, to
produce valid JSON for every input by escaping the complete JSON
control-character range, including carriage returns, backspace, and form feed,
while preserving the existing handling of backslashes, quotes, newlines, and
tabs. Prefer an available JSON encoder if the script already supports one;
otherwise extend the escaping logic without changing emit_json’s field
structure.
In `@bin/fm-herdr-spur.sh`:
- Around line 259-300: Keep native push subscriptions current during
fm_herdr_spur_event_block instead of passing only the initial pane_ids to
herdr-eventwait.py for the entire event budget. Use a shorter default
FM_HERDR_SPUR_EVENT_BUDGET or add periodic snapshot reconciliation so panes
created for watched agents during the wait are subscribed before their
transitions are missed.
In `@bin/fm-idea-mine.ts`:
- Around line 747-758: Update createIssue to validate the extracted stdout value
against the expected issue-id format before returning it. Raise an error when
stdout is empty or the extracted value does not match that format, rather than
returning an arbitrary token or an empty string; preserve the existing command
failure handling.
- Around line 331-336: Update the tail-trimming logic around tail, charCount,
and approxTokens so the token budget is enforced even when one message remains
and exceeds cfg.tokenBudget. After dropping older messages, truncate the
surviving message’s text to fit the remaining budget before generateCandidates
consumes it, preserving the existing behavior when the message is already within
budget.
In `@bin/fm-review-decision.sh`:
- Around line 96-114: Update json_escape to escape every remaining C0 control
character required by JSON, not just quotes, backslashes, newline, carriage
return, and tab; use JSON-compatible \u00XX escapes for control bytes below 0x20
while preserving existing handling. Ensure the audit output remains valid JSONL
and keep the comment accurate about the implemented control-character coverage.
In `@bin/fm-review-page.ts`:
- Around line 824-835: Update the response mapping in the promise chain before
the failure handler to forward the HTTP status alongside ok and data, then use
that status in the fallback error message within the r failure branch instead of
concatenating an empty string.
In `@docs/configuration.md`:
- Around line 175-179: Update the isolated-launch documentation around
bin/fm-isolated-launch.sh to describe its macOS Keychain-based .credentials.json
seeding and clarify that login is only required when extraction fails. Also
document the detached FM_ISOLATED_CWD worktree and state that it must remain
outside the real home directory tree.
In `@tests/fm-idea-mine.test.sh`:
- Around line 184-185: Update the dry-run, malformed-history, and empty-history
assertions in the test to reject every fake-store write verb by matching create,
update, or note with a single combined grep pattern. Apply the same
\[(create|update|note)\] check to all three cases, preserving their existing
failure messages and log output.
---
Nitpick comments:
In @.agents/skills/coderabbit-pr-gate/SKILL.md:
- Around line 46-48: Update the CodeRabbit review-waiting guidance near the
stalled-review rule to define a measurable maximum watcher-cycle count or
elapsed-time bound, accounting for paused external waits caused by rate limits.
After that bound is reached without a review, stop waiting and report the
stalled review to the captain as evidence rather than blocking indefinitely or
merging silently.
In `@bin/fm-idea-mine.ts`:
- Around line 99-101: Update defaultTranscriptDir to derive the encoded project
directory from the repository root based on the script location instead of
hardcoding a user-specific path, while preserving the
FM_IDEA_MINE_TRANSCRIPT_DIR override used by mineTranscript.
- Around line 470-491: Update the configuration flow so loadConfig derives and
stores inferTimeoutMs from FM_IDEA_MINE_INFER_TIMEOUT_MS with a 150000 ms
default, and ensure defaultInferCmd reuses that value. In runInference, replace
the hardcoded spawnSync timeout with the configured inferTimeoutMs plus an
appropriate margin, preserving the existing inference error handling.
- Around line 181-198: Update docs/configuration.md, the designated owner of
configuration schemas, to document all eleven FM_IDEA_MINE_* environment
variables introduced by loadConfig, including their defaults or behavior, and
document the marker file location under <fm-home>/state. Keep the schema and
operational-home layout documentation centralized there.
- Around line 948-950: Correct the stale comment near the cfg.reviewCmd update
to reference the review CLI rather than bd, reflecting that the command defaults
to review and no bd invocation exists; alternatively, remove the comment.
In `@bin/fm-review-page.ts`:
- Around line 1026-1043: Update the HELP text in the `HELP` constant to add an
EXIT section documenting that exit code 3 means pages were written but the store
wire-back failed, indicating partial success rather than a complete failure.
- Around line 851-878: Update the rendering flow around extractStakes and
extractContextFields so the amber callout is omitted when contextFields already
contains a Stakes field, while preserving the callout for stakes content not
represented in the context block.
- Around line 1009-1022: Validate item.id in writeItemPage before using it as a
filesystem or URL segment, accepting only the expected single-segment identifier
shape and rejecting values containing path separators or traversal components.
Preserve the existing page-writing behavior for valid IDs and fail before
mkdirSync or writeFileSync for invalid ones.
- Around line 558-585: Rename the local regex-escaped key variable `esc` inside
the `for (const key of keys)` loop to a distinct name, and update both `headRe`
and `inlineRe` template expressions to use it, preserving the module-level HTML
escape helper name.
In `@tests/fm-groom.test.sh`:
- Around line 193-210: Extend the grooming state-machine tests around the
existing armed safe and unsafe cases to mock failures from ideas label list,
ideas set-state, fm-spawn, and review q. For each failed action, assert the
command reports failure without applying the completed-state label, preserves
retryability, and does not create duplicate spawn or review work on retry.
In `@tests/fm-review-decision.test.sh`:
- Around line 68-79: Remove the FM_HOME, FM_REVIEW_BIN, and
FM_REVIEW_DECISIONS_LOG environment-variable prefix from the printf command in
the stdin branch; retain those variables on the bash "$TOOL" pipeline command so
behavior matches the non-stdin path.
- Around line 222-244: Extend the test suite with a best-effort audit-log
failure case, using the existing run_decision and assertion helpers. Set
FM_REVIEW_DECISIONS_LOG to an unwritable path, then verify exit code 0, a
WARNING: message, and successful wake and note effects; register the new test
with the other test calls.
- Around line 191-202: Update t_note_failure in the test to assert the
wake-queue state after run_decision reports the annotation failure. Verify that
no wake entry was enqueued when storing the review note fails, while preserving
the existing non-zero exit and diagnostic assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 4ad5dac7-9b79-459b-b107-73f70f14e86f
📒 Files selected for processing (35)
.agents/skills/coderabbit-pr-gate/SKILL.md.agents/skills/herdr-navigation/SKILL.md.agents/skills/night-ops-directive/SKILL.md.claude/settings.json.codex/hooks.json.gitignoreAGENTS.mdREADME.mdbin/claude-1.shbin/claude-2.shbin/claude-account.shbin/fm-bead-stamp.shbin/fm-brief-hooks.d/beads.shbin/fm-brief.shbin/fm-groom-json-field.shbin/fm-groom-lib.shbin/fm-groom.shbin/fm-herdr-spur.shbin/fm-idea-mine.tsbin/fm-isolated-launch.shbin/fm-review-decision.shbin/fm-review-page.tsbin/fm-spawn-hooks.d/beads.shbin/fm-spawn.shbin/fm-watch.shdocs/configuration.mddocs/examples/herdr-spur.agentsdocs/scripts.mdtests/claude-account.test.shtests/fm-beads-hooks.test.shtests/fm-groom.test.shtests/fm-idea-mine.test.shtests/fm-review-decision.test.shtests/fm-review-page.test.shtests/fm-spawn-account.test.sh
| 4. Do not merge past an unresolved actionable CodeRabbit finding without either fixing it or | ||
| getting an explicit captain or `yolo`-authorized decision to proceed anyway. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Constrain the yolo exception to the documented authority boundary.
Lines 29-32 require captain escalation for security-sensitive actions. Line 34 also permits a yolo-authorized merge past an unresolved actionable finding without repeating the routine, reversible, and in-scope conditions. This can bypass the security-sensitive escalation rule.
Limit the exception to a standing yolo posture for a routine, reversible merge within accepted task criteria.
Based on the PR objective, reversibility is the yolo qualification for routine gates, including PR merges.
Proposed policy change
-4. Do not merge past an unresolved actionable CodeRabbit finding without either fixing it or
- getting an explicit captain or `yolo`-authorized decision to proceed anyway.
+4. Do not merge past an unresolved actionable CodeRabbit finding without either fixing it,
+ getting explicit captain approval, or using a standing `yolo` posture that qualifies the
+ merge as routine, reversible, and within accepted task criteria. `yolo` does not override
+ captain escalation for security-sensitive findings.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 4. Do not merge past an unresolved actionable CodeRabbit finding without either fixing it or | |
| getting an explicit captain or `yolo`-authorized decision to proceed anyway. | |
| 4. Do not merge past an unresolved actionable CodeRabbit finding without either fixing it, | |
| getting explicit captain approval, or using a standing `yolo` posture that qualifies the | |
| merge as routine, reversible, and within accepted task criteria. `yolo` does not override | |
| captain escalation for security-sensitive findings. |
🧰 Tools
🪛 SkillSpector (2.4.4)
[info] 30: [EA3] Scope Creep: Skill's behavior or capabilities extend beyond its stated purpose. Scope creep allows an agent to perform actions unrelated to its documented functionality, increasing the attack surface.
Remediation: Limit the skill's scope to its documented purpose. Remove instructions that enable the agent to perform actions outside its stated functionality.
(Excessive Agency (EA3))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/coderabbit-pr-gate/SKILL.md around lines 33 - 34, Revise the
unresolved actionable CodeRabbit finding rule so the `yolo` exception applies
only to a standing `yolo` posture for routine, reversible merges within accepted
task criteria. Preserve the requirement for captain escalation on
security-sensitive actions, preventing this exception from bypassing that
authority boundary.
| - Firstmate stays a delegator for the whole session, including overnight: read state, decide | ||
| routing, write briefs, spawn crewmates, supervise, and report; do not personally hand-edit | ||
| project files, grind through binary or database debugging, or `git diff` a project clone to | ||
| do a crewmate's job. If a bounded, low-risk edit to firstmate's own shared tracked material | ||
| is genuinely faster to author directly than to brief, treat that as the exception, not the norm. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Restrict direct edits to shared firstmate material.
The exception allows firstmate to edit shared tracked material whenever a direct edit is faster. It does not require an empty fleet or firstmate-coding-guidelines. It can include policy, skill, hook, and launcher files that enforce unattended-session safety. Add the fleet-empty guard, require the guidance skill for ship tasks, and delegate otherwise.
Proposed wording
- If a bounded, low-risk edit to firstmate's own shared tracked material
- is genuinely faster to author directly than to brief, treat that as the
- exception, not the norm.
+ Direct edits to firstmate's shared tracked material are allowed only when
+ the fleet is empty. For a ship task that touches this material, load
+ `firstmate-coding-guidelines` before editing; otherwise delegate the change
+ to a crewmate.As per coding guidelines, direct edits require an empty fleet, and ship tasks require firstmate-coding-guidelines before editing.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - Firstmate stays a delegator for the whole session, including overnight: read state, decide | |
| routing, write briefs, spawn crewmates, supervise, and report; do not personally hand-edit | |
| project files, grind through binary or database debugging, or `git diff` a project clone to | |
| do a crewmate's job. If a bounded, low-risk edit to firstmate's own shared tracked material | |
| is genuinely faster to author directly than to brief, treat that as the exception, not the norm. | |
| - Firstmate stays a delegator for the whole session, including overnight: read state, decide | |
| routing, write briefs, spawn crewmates, supervise, and report; do not personally hand-edit | |
| project files, grind through binary or database debugging, or `git diff` a project clone to | |
| do a crewmate's job. Direct edits to firstmate's shared tracked material are allowed only when | |
| the fleet is empty. For a ship task that touches this material, load | |
| `firstmate-coding-guidelines` before editing; otherwise delegate the change | |
| to a crewmate. |
🧰 Tools
🪛 SkillSpector (2.4.4)
[error] 14: [RA1] Self-Modification: Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.
Remediation: Prevent the skill from modifying its own code, SKILL.md, or configuration files. Treat skill files as read-only at runtime.
(Rogue Agent (RA1))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.agents/skills/night-ops-directive/SKILL.md around lines 22 - 26, Update the
direct-edit exception in the Firstmate delegation policy to require an empty
fleet and the firstmate-coding-guidelines skill for ship tasks before editing
shared tracked material. Preserve delegation as the default, including for
policy, skill, hook, and launcher files, and allow direct edits only when these
conditions are satisfied and the change is bounded and low risk.
Source: Coding guidelines
| <!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:970c3bf2 --> | ||
| ## Beads Issue Tracker | ||
|
|
||
| This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. | ||
|
|
||
| ### Quick Reference | ||
|
|
||
| ```bash | ||
| bd ready # Find available work | ||
| bd show <id> # View issue details | ||
| bd update <id> --claim # Claim work | ||
| bd close <id> # Complete work | ||
| ``` | ||
|
|
||
| ### Rules | ||
|
|
||
| - Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists | ||
| - Run `bd prime` for detailed command reference and session close protocol | ||
| - Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files | ||
|
|
||
| **Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns. | ||
|
|
||
| ## Agent Context Profiles | ||
|
|
||
| The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions. | ||
|
|
||
| - **Conservative (default)**: Use `bd` for task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands. | ||
| - **Minimal**: Keep tool instruction files as pointers to `bd prime`; use the same conservative git policy unless active instructions say otherwise. | ||
| - **Team-maintainer**: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins. | ||
|
|
||
| ## Session Completion | ||
|
|
||
| This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions. | ||
|
|
||
| 1. **File issues for remaining work** - Create beads for anything that needs follow-up | ||
| 2. **Run quality gates** (if code changed) - Tests, linters, builds | ||
| 3. **Update issue status** - Close finished work, update in-progress items | ||
| 4. **Handle git/sync by active profile**: | ||
| ```bash | ||
| # Conservative/minimal/default: report status and proposed commands; wait for approval. | ||
| git status | ||
|
|
||
| # Team-maintainer opt-in only, unless current instructions forbid it: | ||
| git pull --rebase | ||
| bd dolt push | ||
| git push | ||
| git status | ||
| ``` | ||
| 5. **Hand off** - Summarize changes, validation, issue status, and any blocked sync/commit/push step | ||
|
|
||
| **Critical rules:** | ||
| - Explicit user or orchestrator instructions override this Beads block. | ||
| - Do not commit or push without clear authority from the active profile or the current user request. | ||
| - If a required sync or push is blocked, stop and report the exact command and error. | ||
| <!-- END BEADS INTEGRATION --> | ||
|
|
||
| <!-- BEGIN BEADS CODEX SETUP: generated by bd setup codex --> | ||
| ## Beads Issue Tracker | ||
|
|
||
| Use Beads (`bd`) for durable task tracking in repositories that include it. Use the `beads` skill at `.agents/skills/beads/SKILL.md` (project install) or `~/.agents/skills/beads/SKILL.md` (global install) for Beads workflow guidance, then use the `bd` CLI for issue operations. | ||
|
|
||
| ### Quick Reference | ||
|
|
||
| ```bash | ||
| bd ready # Find available work | ||
| bd show <id> # View issue details | ||
| bd update <id> --claim # Claim work | ||
| bd close <id> # Complete work | ||
| bd prime # Refresh Beads context | ||
| ``` | ||
|
|
||
| ### Rules | ||
|
|
||
| - Use `bd` for all task tracking; do not create markdown TODO lists. | ||
| - Run `bd prime` when Beads context is missing or stale. Codex 0.129.0+ can load Beads context automatically through native hooks; use `/hooks` to inspect or toggle them. | ||
| - Keep persistent project memory in Beads via `bd remember`; do not create ad hoc memory files. | ||
|
|
||
| **Architecture in one line:** issues live in a local Dolt DB; sync uses `refs/dolt/data` on your git remote; `.beads/issues.jsonl` is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns. | ||
| <!-- END BEADS CODEX SETUP --> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Replace the duplicated Beads blocks with one scoped pointer.
Lines 508-586 add two ## Beads Issue Tracker sections. This triggers MD024. These blocks also duplicate command and lifecycle policy in an always-loaded file that already defines firstmate task tracking and completion behavior.
Keep one short instruction that scopes Beads to applicable project work and points to the Beads skill or its authoritative documentation. Do not make AGENTS.md a second owner of Beads workflow details.
As per coding guidelines, “Keep AGENTS.md for knowledge useful to almost every future agent session in this project; do not repeat what the codebase already shows” and “prefer rewriting or pruning over appending.”
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)
[warning] 565-565: Multiple headings with the same content
(MD024, no-duplicate-heading)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@AGENTS.md` around lines 508 - 586, Replace both duplicated Beads sections in
AGENTS.md with one concise, scoped pointer stating that applicable project work
uses Beads and directing agents to the Beads skill or authoritative
documentation for workflow details. Remove the duplicated commands, lifecycle
protocol, architecture explanation, and profile policy so AGENTS.md is not a
second owner of Beads guidance and only one Beads heading remains.
Sources: Coding guidelines, Linters/SAST tools
| CLAUDE_TRUST_DIR="${CLAUDE_TRUST_DIR:-$PWD}" python3 - "$CLAUDE_JSON" <<'PYEOF' | ||
| import json | ||
| import os | ||
| import sys | ||
| import tempfile | ||
|
|
||
| path = sys.argv[1] | ||
| trust_dir = os.environ["CLAUDE_TRUST_DIR"] | ||
|
|
||
| if os.path.exists(path): | ||
| with open(path) as f: | ||
| data = json.load(f) | ||
| else: | ||
| data = {} | ||
|
|
||
| changed = False | ||
| if not data.get("hasCompletedOnboarding"): | ||
| data["hasCompletedOnboarding"] = True | ||
| data.setdefault("numStartups", 1) | ||
| changed = True | ||
|
|
||
| projects = data.setdefault("projects", {}) | ||
| if not projects.get(trust_dir, {}).get("hasTrustDialogAccepted"): | ||
| projects.setdefault(trust_dir, {})["hasTrustDialogAccepted"] = True | ||
| changed = True |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
In Claude Code, does setting projects.<path>.hasTrustDialogAccepted in .claude.json bypass the workspace trust dialog for that path?
💡 Result:
Yes, manually setting hasTrustDialogAccepted to true within the project entry in your ~/.claude.json file can effectively bypass the workspace trust dialog for that specific directory [1][2][3]. In Claude Code, the ~/.claude.json file stores per-project state, including trust settings [4][5]. The structure for these settings is defined under a projects key, where each project path serves as a key containing its configuration, including the hasTrustDialogAccepted boolean [6][3][7]. If you are experiencing persistent prompts despite accepting the dialog, this manual configuration in ~/.claude.json is a recognized method to resolve the issue [1][2][3]. You should ensure the path is correctly formatted for your operating system (e.g., using double backslashes on Windows if necessary) [7] and placed under the projects object as follows [3][7]: { "projects": { "/path/to/your/project": { "hasTrustDialogAccepted": true } } } Please note that while this is a functional workaround, there have been documented bugs where the trust dialog acceptance failed to persist automatically to this file [1][3]. If the setting is correctly applied in the file but the prompt continues to appear, it may indicate a specific environment issue or a regression in how the file is read [1][3].
Citations:
- 1: [BUG] Trust dialog acceptance not persisted to .claude.json anthropics/claude-code#36403
- 2: [BUG] Trust prompt appears every session when running Claude Code from home directory (~) anthropics/claude-code#18942
- 3: [BUG] hasTrustDialogAccepted never persisted to ~/.claude.json despite repeated interactive sessions anthropics/claude-code#71562
- 4: https://code.claude.com/docs/en/settings
- 5: https://code.claude.com/docs/en/configuration
- 6: https://gist.github.com/Frisch12/03dea0f3920079723bacc7eae645e41a
- 7: [BUG] Claude on native Windows repeatedly asks to trust folder anthropics/claude-code#3366
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate claude-account and wrappers =="
fd -a 'claude-(account|1|2)\.sh$|\.envrc|AGENTS\.md|README\.md' . | sed 's#^\./##'
echo "== bin files size/contents =="
for f in bin/claude-account.sh bin/claude-1.sh bin/claude-2.sh; do
if [ -f "$f" ]; then
echo "--- $f ($(wc -l < "$f") lines) ---"
sed -n '1,140p' "$f" | cat -n
fi
done
echo "== search for hasTrustDialogAccepted / CLAUDE_TRUST_DIR =="
rg -n "hasTrustDialogAccepted|CLAUDE_TRUST_DIR|trust_dir|claude-(1|2|account)" .Repository: trillium/firstmate
Length of output: 11542
Do not pre-accept trust for the launcher directory by default.
bin/claude-1.sh and bin/claude-2.sh invoke claude-account.sh without CLAUDE_TRUST_DIR, so line 62 pre-trusts the current working directory before Claude evaluates it. Keep onboarding pre-acceptance, but only write projects.<path>.hasTrustDialogAccepted when an explicit trusted directory is supplied. Add a test that launches without CLAUDE_TRUST_DIR and checks that no project trust entry is created.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/claude-account.sh` around lines 62 - 86, Update the Python trust-handling
block in claude-account.sh to preserve onboarding pre-acceptance while only
creating projects.<path>.hasTrustDialogAccepted when CLAUDE_TRUST_DIR is
explicitly provided. Avoid using the default current working directory for trust
initialization, and add coverage for launching without CLAUDE_TRUST_DIR that
verifies no project trust entry is written.
| # If you plan to use --dangerously-skip-permissions, pre-accept its matching | ||
| # settings.json confirmation so the agent doesn't prompt at startup. Only a | ||
| # real per-account settings.json is patched in place - a symlinked shared | ||
| # settings.json is left alone so accounts never silently diverge from the | ||
| # single source of truth; set the flag once in ~/.claude/settings.json instead | ||
| # if every account should skip the prompt. | ||
| SETTINGS="$CLAUDE_CONFIG_DIR/settings.json" | ||
| if [ -f "$SETTINGS" ] && [ ! -L "$SETTINGS" ]; then | ||
| python3 - "$SETTINGS" <<'PYEOF' | ||
| import json | ||
| import os | ||
| import sys | ||
| import tempfile | ||
|
|
||
| path = sys.argv[1] | ||
| with open(path) as f: | ||
| data = json.load(f) | ||
|
|
||
| if not data.get("skipDangerousModePermissionPrompt"): | ||
| data["skipDangerousModePermissionPrompt"] = True | ||
| fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path) or ".", prefix=".claude-account.") | ||
| try: | ||
| with os.fdopen(fd, "w") as f: | ||
| json.dump(data, f, indent=2) | ||
| os.replace(tmp, path) | ||
| except Exception: | ||
| try: | ||
| os.unlink(tmp) | ||
| except OSError: | ||
| pass | ||
| raise | ||
| PYEOF |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file sizes =="
wc -l bin/claude-account.sh tests/claude-account.test.sh 2>/dev/null || true
echo "== relevant launcher source =="
sed -n '1,180p' bin/claude-account.sh
echo "== relevant test source =="
sed -n '130,200p' tests/claude-account.test.sh
echo "== dangerous mode occurrences =="
rg -n "dangerously|skipDangerousModePermissionPrompt|CLAUDE_CONFIG_DIR|DANGEROUS|dangerous" bin tests -SRepository: trillium/firstmate
Length of output: 15053
Gate dangerous-mode consent on the dangerous-mode flag.
bin/claude-account.sh#L102-L133 writes skipDangerousModePermissionPrompt even for normal Claude launches before exec claude "$@"; update tests/claude-account.test.sh#L161-L179 to cover a non-flagged launch preserving the setting and add a separate --dangerously-skip-permissions case for the launcher.
📍 Affects 2 files
bin/claude-account.sh#L102-L133(this comment)tests/claude-account.test.sh#L161-L179
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/claude-account.sh` around lines 102 - 133, The settings update in
bin/claude-account.sh, around the SETTINGS handling block, must run only when
the launch arguments include --dangerously-skip-permissions; leave settings
unchanged for normal launches while preserving the existing symlink protection.
Update tests/claude-account.test.sh around the existing launcher coverage to
assert a non-flagged launch preserves the setting and add a separate
--dangerously-skip-permissions case verifying the setting is applied.
| ISOLATED_HOME="${FM_ISOLATED_HOME:-$FM_ROOT/.fm-isolated-home}" | ||
| ISOLATED_CWD="${FM_ISOLATED_CWD:-/private/tmp/fm-isolated-worktree}" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Enforce the isolation invariant for path overrides.
FM_ISOLATED_CWD can resolve inside REAL_HOME, where Claude can rediscover the operator's global .claude/CLAUDE.md. FM_ISOLATED_HOME can also be set to REAL_HOME, which removes configuration isolation.
Resolve both paths before setup. Reject unsafe overrides instead of relying on the default values.
Also applies to: 189-196
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/fm-isolated-launch.sh` around lines 73 - 74, Update the path override
handling near ISOLATED_HOME and ISOLATED_CWD to resolve both configured paths
before setup and reject overrides that equal or reside within REAL_HOME,
including FM_ISOLATED_HOME set to REAL_HOME. Preserve the existing defaults when
overrides are absent, and fail immediately with a clear error for unsafe values
before creating or using the isolated environment.
| if [ ! -e "$ISOLATED_HOME/data" ] && [ -d "$REAL_HOME/data" ]; then | ||
| ln -s "$REAL_HOME/data" "$ISOLATED_HOME/data" | ||
| fi |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Restrict the data exposed to the isolated Claude session.
The symlink exposes the entire $REAL_HOME/data tree. The child is launched with --dangerously-skip-permissions, so a prompt injection or tool mistake can read or modify unrelated operational data.
Link only the required store directories, or provide explicit per-store paths. Validate any existing data path before using it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/fm-isolated-launch.sh` around lines 106 - 108, Update the isolated-home
data setup around the data symlink creation so it does not expose the entire
REAL_HOME/data tree. Link only the required store directories using explicit
per-store paths, and validate that any existing isolated data path is an
approved directory or symlink target before using it; preserve the existing
setup behavior for valid required stores.
| if [ ! -f "$ISOLATED_HOME/.claude/.credentials.json" ] && command -v security >/dev/null 2>&1; then | ||
| if security find-generic-password -a "$USER" -s "Claude Code-credentials" -w \ | ||
| > "$ISOLATED_HOME/.claude/.credentials.json.tmp" 2>/dev/null; then | ||
| chmod 600 "$ISOLATED_HOME/.claude/.credentials.json.tmp" | ||
| mv "$ISOLATED_HOME/.claude/.credentials.json.tmp" "$ISOLATED_HOME/.claude/.credentials.json" | ||
| else | ||
| rm -f "$ISOLATED_HOME/.claude/.credentials.json.tmp" | ||
| fi | ||
| fi |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Create the credential file with private permissions from the start.
Shell redirection creates the predictable .tmp file before chmod 600 runs. A permissive umask can expose the OAuth token during the write, and another process can race the predictable path.
Set umask 077 and use a private mktemp file followed by an atomic rename. Check both chmod and mv.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/fm-isolated-launch.sh` around lines 115 - 123, Update the credential
extraction block around the security command to set umask 077, create a private
unpredictable temporary file with mktemp, and write credentials to that file
instead of the predictable .tmp path. Require both chmod and mv to succeed
before completing the atomic rename, and clean up the temporary file on any
failure.
| # --------------------------------------------------------------------------- | ||
| # (a) DURABLE WAKE - load-bearing, fail loud on any failure. | ||
| # The payload is a compact, human-readable line firstmate reads directly off the | ||
| # drained wake: verdict first, then the comment if present. | ||
| # --------------------------------------------------------------------------- | ||
| WAKE_PAYLOAD="captain decided ${VERDICT} on ${ID}" | ||
| if [ -n "${COMMENT//[[:space:]]/}" ]; then | ||
| WAKE_PAYLOAD="${WAKE_PAYLOAD} - ${COMMENT}" | ||
| fi | ||
| if ! fm_wake_append check "review-decision:${ID}" "$WAKE_PAYLOAD"; then | ||
| die "failed to enqueue wake for ${ID} (queue: ${FM_WAKE_QUEUE})" | ||
| fi | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # (b) STORE ANNOTATION - load-bearing, fail loud. Makes the decision visible in | ||
| # `review show <id>` so the item carries its own decision trail. | ||
| # --------------------------------------------------------------------------- | ||
| NOTE="Captain decision: ${VERDICT}" | ||
| if [ -n "${COMMENT//[[:space:]]/}" ]; then | ||
| NOTE="${NOTE} - ${COMMENT}" | ||
| fi | ||
| NOTE="${NOTE} @ ${TS}" | ||
| NOTE_ERR="$(mktemp "${TMPDIR:-/tmp}/fm-review-decision-note.XXXXXX")" | ||
| if ! BEADS_ACTOR="$DECISION_ACTOR" "$REVIEW_BIN" note "$ID" "$NOTE" >/dev/null 2>"$NOTE_ERR"; then | ||
| err="$(cat "$NOTE_ERR" 2>/dev/null || true)"; rm -f "$NOTE_ERR" | ||
| die "failed to annotate review item ${ID}: ${err}" | ||
| fi | ||
| rm -f "$NOTE_ERR" |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Annotate the item before enqueuing the wake.
The wake lands first (line 127). If review note then fails (line 141), the script exits 1 and the endpoint returns non-2xx, but the check: review-decision:<id> wake is already durable in the queue. Two consequences follow:
- Firstmate drains that wake and acts on a decision that the captain was told did not record.
- The panel re-enables its buttons on a non-2xx response (
bin/fm-review-page.ts, line 834). Each retry appends another wake with the same key, so firstmate can act on the same decision several times.
Swap the order. The store annotation becomes the first durable write, and the wake is only enqueued once the decision is visible on the item. A failure then leaves no queued action that contradicts the reported error. Update the ordering description at lines 14-25 and 27-33 to match.
Based on learnings: "Report outcomes faithfully; if work failed, state it plainly with the evidence" — the current order reports failure after a side effect already landed.
🛡️ Proposed reorder
# ---------------------------------------------------------------------------
-# (a) DURABLE WAKE - load-bearing, fail loud on any failure.
-# The payload is a compact, human-readable line firstmate reads directly off the
-# drained wake: verdict first, then the comment if present.
-# ---------------------------------------------------------------------------
-WAKE_PAYLOAD="captain decided ${VERDICT} on ${ID}"
-if [ -n "${COMMENT//[[:space:]]/}" ]; then
- WAKE_PAYLOAD="${WAKE_PAYLOAD} - ${COMMENT}"
-fi
-if ! fm_wake_append check "review-decision:${ID}" "$WAKE_PAYLOAD"; then
- die "failed to enqueue wake for ${ID} (queue: ${FM_WAKE_QUEUE})"
-fi
-
-# ---------------------------------------------------------------------------
-# (b) STORE ANNOTATION - load-bearing, fail loud. Makes the decision visible in
+# (a) STORE ANNOTATION - load-bearing, fail loud, and FIRST. Nothing is queued
+# for firstmate until the decision is visible on the item, so a failure here
+# never leaves a wake that contradicts the error the captain sees. Visible in
# `review show <id>` so the item carries its own decision trail.
# ---------------------------------------------------------------------------
NOTE="Captain decision: ${VERDICT}"
@@
rm -f "$NOTE_ERR"
+
+# ---------------------------------------------------------------------------
+# (b) DURABLE WAKE - load-bearing, fail loud on any failure.
+# The payload is a compact, human-readable line firstmate reads directly off the
+# drained wake: verdict first, then the comment if present.
+# ---------------------------------------------------------------------------
+WAKE_PAYLOAD="captain decided ${VERDICT} on ${ID}"
+if [ -n "${COMMENT//[[:space:]]/}" ]; then
+ WAKE_PAYLOAD="${WAKE_PAYLOAD} - ${COMMENT}"
+fi
+if ! fm_wake_append check "review-decision:${ID}" "$WAKE_PAYLOAD"; then
+ die "failed to enqueue wake for ${ID} (queue: ${FM_WAKE_QUEUE}); the item was annotated"
+fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # --------------------------------------------------------------------------- | |
| # (a) DURABLE WAKE - load-bearing, fail loud on any failure. | |
| # The payload is a compact, human-readable line firstmate reads directly off the | |
| # drained wake: verdict first, then the comment if present. | |
| # --------------------------------------------------------------------------- | |
| WAKE_PAYLOAD="captain decided ${VERDICT} on ${ID}" | |
| if [ -n "${COMMENT//[[:space:]]/}" ]; then | |
| WAKE_PAYLOAD="${WAKE_PAYLOAD} - ${COMMENT}" | |
| fi | |
| if ! fm_wake_append check "review-decision:${ID}" "$WAKE_PAYLOAD"; then | |
| die "failed to enqueue wake for ${ID} (queue: ${FM_WAKE_QUEUE})" | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # (b) STORE ANNOTATION - load-bearing, fail loud. Makes the decision visible in | |
| # `review show <id>` so the item carries its own decision trail. | |
| # --------------------------------------------------------------------------- | |
| NOTE="Captain decision: ${VERDICT}" | |
| if [ -n "${COMMENT//[[:space:]]/}" ]; then | |
| NOTE="${NOTE} - ${COMMENT}" | |
| fi | |
| NOTE="${NOTE} @ ${TS}" | |
| NOTE_ERR="$(mktemp "${TMPDIR:-/tmp}/fm-review-decision-note.XXXXXX")" | |
| if ! BEADS_ACTOR="$DECISION_ACTOR" "$REVIEW_BIN" note "$ID" "$NOTE" >/dev/null 2>"$NOTE_ERR"; then | |
| err="$(cat "$NOTE_ERR" 2>/dev/null || true)"; rm -f "$NOTE_ERR" | |
| die "failed to annotate review item ${ID}: ${err}" | |
| fi | |
| rm -f "$NOTE_ERR" | |
| # --------------------------------------------------------------------------- | |
| # (a) STORE ANNOTATION - load-bearing, fail loud, and FIRST. Nothing is queued | |
| # for firstmate until the decision is visible on the item, so a failure here | |
| # never leaves a wake that contradicts the error the captain sees. Visible in | |
| # `review show <id>` so the item carries its own decision trail. | |
| # --------------------------------------------------------------------------- | |
| NOTE="Captain decision: ${VERDICT}" | |
| if [ -n "${COMMENT//[[:space:]]/}" ]; then | |
| NOTE="${NOTE} - ${COMMENT}" | |
| fi | |
| NOTE="${NOTE} @ ${TS}" | |
| NOTE_ERR="$(mktemp "${TMPDIR:-/tmp}/fm-review-decision-note.XXXXXX")" | |
| if ! BEADS_ACTOR="$DECISION_ACTOR" "$REVIEW_BIN" note "$ID" "$NOTE" >/dev/null 2>"$NOTE_ERR"; then | |
| err="$(cat "$NOTE_ERR" 2>/dev/null || true)"; rm -f "$NOTE_ERR" | |
| die "failed to annotate review item ${ID}: ${err}" | |
| fi | |
| rm -f "$NOTE_ERR" | |
| # --------------------------------------------------------------------------- | |
| # (b) DURABLE WAKE - load-bearing, fail loud on any failure. | |
| # The payload is a compact, human-readable line firstmate reads directly off the | |
| # drained wake: verdict first, then the comment if present. | |
| # --------------------------------------------------------------------------- | |
| WAKE_PAYLOAD="captain decided ${VERDICT} on ${ID}" | |
| if [ -n "${COMMENT//[[:space:]]/}" ]; then | |
| WAKE_PAYLOAD="${WAKE_PAYLOAD} - ${COMMENT}" | |
| fi | |
| if ! fm_wake_append check "review-decision:${ID}" "$WAKE_PAYLOAD"; then | |
| die "failed to enqueue wake for ${ID} (queue: ${FM_WAKE_QUEUE}); the item was annotated" | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/fm-review-decision.sh` around lines 118 - 145, Move the STORE ANNOTATION
block using NOTE, NOTE_ERR, and the review note command before the DURABLE WAKE
block invoking fm_wake_append, so annotation succeeds before any wake is queued.
Preserve the existing failure handling and payload construction, then update the
ordering comments near the script’s section descriptions to state that
annotation is the first durable write and wake enqueue follows it.
Source: Learnings
| function runReview(args: string[]): string { | ||
| const res = spawnSync(REVIEW_BIN, args, { | ||
| encoding: "utf8", | ||
| timeout: CMD_TIMEOUT_MS, | ||
| // Never inherit stdin; some bd verbs open a pager/editor if a tty is present. | ||
| stdio: ["ignore", "pipe", "pipe"], | ||
| env: { ...process.env, PAGER: "cat", GIT_PAGER: "cat" }, | ||
| }) | ||
| if (res.error) { | ||
| throw new Error(`review ${args.join(" ")}: ${res.error.message}`) | ||
| } | ||
| if (typeof res.status === "number" && res.status !== 0) { | ||
| const stderr = (res.stderr || "").trim() | ||
| throw new Error(`review ${args.join(" ")} exited ${res.status}${stderr ? `: ${stderr}` : ""}`) | ||
| } | ||
| return (res.stdout || "").trim() | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Bun node:child_process spawnSync timeout returns status null signal SIGTERM
💡 Result:
When a process spawned via Bun.spawnSync (or the Node.js compatibility layer child_process.spawnSync) hits a timeout, it is intentionally terminated by the runtime using a killSignal (which defaults to SIGTERM) [1][2][3]. The observed behavior—where status returns null and signal is SIGTERM—is expected and consistent with how Node.js represents a process that did not exit normally but was instead terminated by a signal [4][5]. Key points regarding this behavior: 1. Status vs. Signal: In Node.js and Bun's compatibility layer, when a process terminates due to a signal, the status (or exitCode) property is set to null to indicate that the process did not have a standard exit code [5]. The signal property is then populated with the signal name (e.g., 'SIGTERM') that caused the termination [4]. 2. Timeout Mechanism: When the timeout option is configured, Bun enforces this limit by sending the specified killSignal to the child process [1][6]. If the process is unresponsive to SIGTERM, it will remain in a state where it has been signaled, which the parent process waits for [7]. 3. Previous Issues: Earlier versions of Bun had bugs where signal was incorrectly reported as null in spawnSync [4], or where timeout behaviors varied on specific platforms like Windows [8]. Recent updates have improved compatibility to match Node.js behavior, ensuring that signal is correctly reported as the terminating signal [4][5]. If you are seeing status: null and signal: 'SIGTERM', your code is correctly identifying that the process was killed due to a timeout [1][2]. You can customize the signal used by providing the killSignal option (e.g., 'SIGKILL') in your spawnSync configuration if a stronger termination signal is required [1][9].
Citations:
- 1: https://bun.sh/reference/bun/spawnSync
- 2: https://bun.sh/docs/runtime/child-process
- 3: https://bun.sh/reference/bun/Spawn/SpawnSyncOptions/timeout
- 4: child_process.spawnSync does not provide signal oven-sh/bun#15111
- 5: node:child_process: match Node's spawnSync result shape when the spawn fails oven-sh/bun#32878
- 6: https://bun.com/docs/runtime/child-process
- 7: https://bun.sh/reference/node/child_process/spawnSync
- 8: Bun.spawnSync({ timeout }) returns exitCode: null with empty stdout/stderr for a real ssh.exe child on Windows — not reproduced with a synthetic closed-port case oven-sh/bun#33932
- 9: https://bun.com/reference/bun/Spawn/SpawnSyncOptions/killSignal
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '=== locate file ===\n'
git ls-files | rg '(^|/)bin/fm-review-page\.ts$|(^|/)AGENTS\.md$|(^|/)README\.md$|(^|/)bin/package\.json$|(^|/)bun\.lockb$|(^|/)package\.json$|(^|/)bun\.lock$' || true
printf '\n=== fm-review-page outline ===\n'
if [ -f bin/fm-review-page.ts ]; then
wc -l bin/fm-review-page.ts
ast-grep outline bin/fm-review-page.ts || true
printf '\n=== relevant fm-review-page sections ===\n'
sed -n '1,220p' bin/fm-review-page.ts | nl -ba
fi
printf '\n=== search runReview/parseJsonItems/index.html writes ===\n'
rg -n "function runReview|runReview\s*\(|parseJsonItems|index\.html|REVIEW_BIN|CMD_TIMEOUT_MS|parse" bin/fm-review-page.ts || trueRepository: trillium/firstmate
Length of output: 2963
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '=== relevant fm-review-page sections ===\n'
sed -n '132,185p' bin/fm-review-page.ts
printf '\n=== parsing/index write sections ===\n'
sed -n '930,1032p' bin/fm-review-page.ts
printf '\n=== main sections ===\n'
sed -n '1040,1110p' bin/fm-review-page.ts
printf '\n=== parseJsonItems behavioral probe ===\n'
python3 - <<'PY'
from pathlib import Path
import re
p = Path("bin/fm-review-page.ts")
text = p.read_text()
m = re.search(r'function\s+parseJsonItems\s*\([^)]*\)\s*:\s*[^{]+\{(?P<body>.*?)\n}', text, re.S)
if m:
print(m.group(0))
body = m.group("body")
for k in ("JSON.parse", ".map(", ".filter(", "return"):
print("contains", k, "=>", k in body)
else:
print("parseJsonItems function not found")
PY
printf '\n=== spawnSync/timeout/guard static invariant check ===\n'
python3 - <<'PY'
from pathlib import Path
import re
text = Path("bin/fm-review-page.ts").read_text()
checks = {
"has timeout option": bool(re.search(r'runReview\(\{\s*timeout:\s*CMD_TIMEOUT_MS\s*,?\s*', text, re.S)),
"has stdio ignore/pipe/pipe": 'stdio: ["ignore", "pipe", "pipe"]' in text,
"has res.error guard": re.search(r'if\s*\(\s*res\.error\s*\)', text) is not None,
"has signal guard": re.search(r'if\s*\(\s*res\.signal\s*\)', text) is not None,
"has non-number status guard": re.search(r'typeof\s+res\.status\s*!\s*===\s*["\']number["\']|\bstatus\s*===\s*null', text) is not None,
"has res.status integer nonzero guard": bool(re.search(r'typeof\s+res\.status\s*===\s*["\']number["\']\s*&&\s*res\.status\s*!==\s*0', text)),
}
for k, v in checks.items():
print(f"{k}: {v}")
PYRepository: trillium/firstmate
Length of output: 8600
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '=== fetchAllOpen/fetchByIds sections ===\n'
sed -n '215,245p' bin/fm-review-page.ts
printf '\n=== runReview call sites ===\n'
rg -n "runBenchmark\(|runReview\(|review \\.\\.\\." bin/fm-review-page.ts || true
rg -n "function run.*Benchmark|function runReview" bin/fm-review-page.ts || trueRepository: trillium/firstmate
Length of output: 1889
🌐 Web query:
Bun node:child_process spawnSync missing binary error ENOENT status null
💡 Result:
The ENOENT error with status null in Bun's child_process.spawnSync is a documented behavior indicating that the child process failed to start (e.g., the binary was not found) [1][2]. Recent updates to Bun have specifically aligned this behavior with Node.js to ensure consistency [1][2]. When spawnSync fails to start a process due to an ENOENT (or EACCES) error, it returns a result object with a standardized shape [1][2]: - pid: 0 - status: null - signal: null - output: null - stdout: undefined - stderr: undefined - error: [The error object containing code: 'ENOENT', syscall, and path] Key points regarding this error: 1. Behavioral Alignment: Earlier versions of Bun sometimes deviated from Node's expected return shape (e.g., returning undefined instead of null for status), leading to compatibility issues [1][2]. Current versions (post-fix PRs #31768 and #32878) now return the expected Node-compatible result [1][2]. 2. Troubleshooting Causes: If you encounter this error, it typically means the specified binary cannot be located or accessed [1][3]. Common causes include: - Invalid or missing PATH: Bun may not be resolving the binary correctly if the environment is restricted or if the PATH is not set as expected [4]. - Working Directory Issues: If an invalid or inaccessible cwd is provided, Bun may report an ENOENT error on the executable itself, even if the executable exists [3]. - Sandbox Restrictions: When running inside restricted environments (like macOS Seatbelt sandboxes), process.env can sometimes be empty, preventing path resolution [4]. Providing an explicit env object with PATH can often resolve this [4]. 3. Verification: You can use Bun.which(binary) to verify if the executable is discoverable by Bun before attempting to spawn it [5]. If you are using an older version of Bun, updating is recommended to ensure you have the improved spawn failure handling [2][6].
Citations:
- 1: child_process: match Node's spawnSync result shape when spawn fails oven-sh/bun#31768
- 2: node:child_process: match Node's spawnSync result shape when the spawn fails oven-sh/bun#32878
- 3: Bun.spawn throws incorrect ENOENT error when cwd does not exist oven-sh/bun#24012
- 4: process.env is empty (0 keys) when bun runs inside macOS Seatbelt sandbox oven-sh/bun#27802
- 5: https://bun.sh/reference/bun/spawnSync
- 6: child_process: match node's spawn-failure event contract oven-sh/bun#33304
Treat a timeout kill and missing exit status as failures.
res.status is null and res.signal is present when the child is terminated by the timeout, but the current guard ignores this case. On --all, fetchAllOpen() then uses truncated or empty stdout, parseJsonItems() returns [], and writeIndexPage() overwrites index.html with the empty queue message. Split the guard into res.signal, typeof res.status !== "number", then the non-zero check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@bin/fm-review-page.ts` around lines 142 - 158, Update runReview to treat any
terminated child as a failure by checking res.signal first, then rejecting cases
where res.status is not a number, before retaining the existing non-zero status
check. Include the review command and available termination details in the
thrown errors so fetchAllOpen cannot process incomplete output.
Source: Linters/SAST tools
- .gitignore: take origin/main's cleaner config/ wildcard approach, keep CHANGELOG.md - AGENTS.md: merge .meta documentation to include all features (endpoint_task_id, parlay-listen-pid, label, beads_id, account) - bin/fm-bead-stamp.sh: include both dispatch=sent and lifecycle=sent state updates - bin/fm-brief-hooks.d/beads.sh: include both dispatch=claimed and lifecycle=claimed in brief - bin/fm-spawn.sh: add --account and --beads documentation alongside origin/main's --label feature - bin/fm-watch.sh: take origin/main's improved md5 fallback chain (native md5 first, then /sbin/md5, then md5sum, then shasum) - docs/scripts.md: document all three beads-related scripts (fm-brief.sh, fm-brief-hooks.d/beads.sh, fm-bead-stamp.sh)
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
README.md (1)
142-142: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCorrect the isolated-home authentication guidance.
bin/fm-isolated-launch.shattempts to seed the isolated.credentials.jsonfrom the macOS Keychain. A new login is required only when that extraction is unavailable or fails.
README.md#L142-L142: State that login is conditional on credential seeding failure.docs/configuration.md#L200-L204: State the same conditional login behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` at line 142, Update the isolated-home authentication guidance to state that a new login is required only when credential seeding from the macOS Keychain is unavailable or fails; apply this wording at README.md lines 142-142 and docs/configuration.md lines 200-204..claude/settings.json (1)
12-20: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winRun the Beads hook from the project root and add a timeout.
Claude runs hooks with a 600s command timeout and uses the process current directory for relative paths.
bd prime --hook-jsoncan detect stale Linear data and pull whenLINEAR_API_KEYis set, so the session can block on startup. Nest this hook under the Firstmate startup matcher and run it from$CLAUDE_PROJECT_DIR.Proposed fix
- "command": "bd prime --hook-json" + "command": "cd \"$CLAUDE_PROJECT_DIR\" && exec bd prime --hook-json", + "timeout": 30🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.claude/settings.json around lines 12 - 20, Update the hook configuration containing the empty matcher so the Beads command is nested under the Firstmate startup matcher, executes from $CLAUDE_PROJECT_DIR, and has an explicit short timeout to prevent startup blocking during stale-data synchronization.bin/fm-brief.sh (1)
164-190: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftBase fork routing on repository permission data.
fork_repo_for_originonly parses the owner-like path component and skips the branch. Any non-Trillium owner emits the fork rule even if direct pushes work, while Trillium-owned origins do not emit it even if they are read-only. A non-GitHub URL ending in/trillium/<repo>can also avoid the fork rule.Require an authoritative permission check or repository-policy input before emitting
FORK_FIRST; keep parsed owner values as display data only.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bin/fm-brief.sh` around lines 164 - 190, Update fork_repo_for_origin so it emits a repository name only after an authoritative permission or repository-policy check confirms the origin requires forking; do not infer pushability from the parsed owner or URL shape. Use the existing policy/permission source, treating owner and repository parsing only as display metadata, and preserve the empty successful result when permission data is unavailable or direct pushes are allowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In @.claude/settings.json:
- Around line 12-20: Update the hook configuration containing the empty matcher
so the Beads command is nested under the Firstmate startup matcher, executes
from $CLAUDE_PROJECT_DIR, and has an explicit short timeout to prevent startup
blocking during stale-data synchronization.
In `@bin/fm-brief.sh`:
- Around line 164-190: Update fork_repo_for_origin so it emits a repository name
only after an authoritative permission or repository-policy check confirms the
origin requires forking; do not infer pushability from the parsed owner or URL
shape. Use the existing policy/permission source, treating owner and repository
parsing only as display metadata, and preserve the empty successful result when
permission data is unavailable or direct pushes are allowed.
In `@README.md`:
- Line 142: Update the isolated-home authentication guidance to state that a new
login is required only when credential seeding from the macOS Keychain is
unavailable or fails; apply this wording at README.md lines 142-142 and
docs/configuration.md lines 200-204.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6235b3f8-e6ad-4e3d-b13a-90147a8fb0fe
📒 Files selected for processing (8)
.claude/settings.json.gitignoreAGENTS.mdREADME.mdbin/fm-brief.shbin/fm-spawn.shdocs/configuration.mddocs/scripts.md
🚧 Files skipped from review as they are similar to previous changes (3)
- docs/scripts.md
- .gitignore
- AGENTS.md
…ills (#22) * Add night-ops-directive and coderabbit-pr-gate skills Salvage the still-relevant content from stale/broken PR #5, reshipped clean through no-mistakes rather than reopening it. Carried forward: - night-ops-directive: standing autonomous-dispatch directive (delegator discipline during unattended work, the watcher-heartbeat wake cadence since ScheduleWakeup/CronCreate are blocked for the primary session, human-only task triage via bd transfer). - coderabbit-pr-gate: CodeRabbit-clean-plus-green merge gating (react to actionable review comments beyond bare pass/fail, treat rate limits as a paused external wait). - AGENTS.md yolo reversibility clarification: the qualifying test for a routine yolo-covered gate, including a PR merge, is reversibility. Dropped as already covered by work landed the same day: - PR #5's general fork-first-push line in AGENTS.md section 7 is dropped; fork-first-brief-08 already implemented and documented (section 11) an automatic fork-first push rule injected into every push-mode ship brief via fm-brief.sh, which fully covers this case. - coderabbit-pr-gate is kept, not dropped, despite PR #16 (pr-review- comment-wake) landing the same day: PR #16 only added the wake mechanism (state/<id>.pr-review-seen) for a new bot review comment. It added no reaction policy, so coderabbit-pr-gate is complementary, not duplicative; the skill now cross-references that wake mechanism instead of re-describing it. * no-mistakes(test): Register two new SKILL.md files in documentation-audiences.json
|
Superseded by PR #22 which reships this content clean through no-mistakes (night-ops-directive + coderabbit-pr-gate skills, yolo-reversibility clarification; the general fork-first line was dropped as already covered by fork-first-brief-08). Closing. |
Clarifies the yolo reversibility test for routine gates (including PR merges), adds a fork-first-push rule for repos whose default branch isn't captain-writable, and registers two new agent-only skills:
night-ops-directive— the standing autonomous-dispatch directive: delegator discipline during unattended work, firstmate's own wake cadence (the watcher heartbeat, since ScheduleWakeup/CronCreate are blocked for the primary session), human-only task triage viabd transfer, and fork-first pushes.coderabbit-pr-gate— reacting to CodeRabbit's actionable review comments beyond a bare pass/fail check, and treating its rate limits as a paused external wait rather than a failure.Authored directly by firstmate per explicit captain override of the crew-live delegation coordination rule (this touches only firstmate's own shared tracked material, not a project).
Summary by CodeRabbit