feat(skills): bundle the bro and unslop writing skills and gate PRs on repo ownership - #4
Merged
Merged
Conversation
Incorporates kunchenguid/firstmate commits a6618dd..da5e658: - c806c6a fix(spawn): establish crewmate identity first (kunchenguid#4481) - d499323 fix(bin): reconcile redundant secondmate divergence during updates (kunchenguid#4460) - da5e658 feat: enable gpt-5.6-luna max reasoning for crew dispatch (kunchenguid#4497) Previous private main: bd4caf8 Pinned official commit: da5e658 The merge applied without conflicts. Both the previous private main and the pinned official commit remain ancestors of this commit; the private writing skills and licenses, the publication guard, and the Claude worker permission restoration are preserved unchanged.
The private seed bd4caf8 never ran the repository lint gate, and CI-mode ShellCheck (full canonical set with --external-sources) reports three findings introduced or exposed by the permission-restoration commits: - bin/backends/herdr.sh: SC1007 on `matched_index= matched_state=`; use the explicit empty-string form. - bin/fm-pending-reply-lib.sh: SC2100 false positive on the unquoted literals `pending-reply-missed` and `pending-reply-delivery-unknown`. The permission library sources fm-config-inherit-lib.sh, which assigns a variable named `pending`, so every root that now reaches both files makes ShellCheck read the literal as `pending - reply - missed`; quoting the literals keeps the value byte-identical and removes the ambiguity. - tests/fm-backend.test.sh: SC2329 on the fm_backend_agent_state stub, which is invoked indirectly through fm_backend_agent_state_for_meta; disable the never-invoked check beside the existing unreachable-code exemption. tests/fm-lint.test.sh's no-external-sources sweep and the CI Lint job are the executable contracts that catch these; the private seed's failing lint lane reproduces locally with the same SC1007 message.
…window tests/fm-procevent.test.sh launched 24 concurrent stale-claim contenders and gave the first runner ten seconds to start. Every contender pays a full isolated runner start-up before the first one can claim, and the winner then re-enters the source lock against the other 23 for its launch stamp, so the first start scales with the contention the test creates. On the private repository's standard hosted runner (2 vCPUs sharing one physical core, half of a public repository's runner) that bound was crossed while the race still started exactly one runner: the lane logged `not ok - no contender acquired the stale claim` with no contender error, and the same section pinned to two cores under sixteen busy loops reaches eight seconds here. Wait on the start event with a bound only a genuinely stuck race exceeds, capture every contender's exit status and output, and require all 24 to end in one of the two legitimate outcomes (already owned, or captured after the claim was released) so a contender that dies on the way to the lock can no longer hide behind the winner. The exactly-one-runner assertion is unchanged.
The CI lint mode ran one source-aware, dataflow-enabled ShellCheck process per shard. Measured at the private seed, each of the two workers sat at its heaviest root's peak for the rest of its shard (9.5 GiB and 8.6 GiB), so the two workers' peaks added up to 18.0 GiB concurrent. One process per root releases that memory between roots (12.4 GiB concurrent at the same tree), reports the same diagnostics in the same root order, and makes the peak of every root observable on its own. This does not by itself fit the 8 GB hosted runner a private repository gets: the largest single roots need 8.5 GiB to 9.5 GiB with source following and dataflow together. docs/verification/lint-ci-memory.md records the whole-run, per-root, and memory-cap measurements and the check semantics of each ShellCheck setting so the remaining runner decision rests on evidence. tests/fm-lint.test.sh pins the one-process-per-root shape for the full source-aware set; the changed-file mode already had that shape and test.
Official main advanced two commits past the pinned import da5e658: 616049a feat(calm): render smooth Unicode swell with asymmetric two-color sail (kunchenguid#4498) aa92177 fix(bin): supersede stale scout delivery text in brief.md on promotion (kunchenguid#4491) The public fork brentsec/firstmate was created from that tip, so this non-fast-forward merge keeps the previous private main bd4caf8, the pinned official commit da5e658, and the fork's main aa92177 as ancestors while the branch carries only the private customizations and the task fixes on top. The merge is clean; the one file both sides touched, tests/fm-control-relaunch.test.sh, gains one independent test from each side.
`.codex/skills/` and `.grok/skills/` each held two symlinks (`bro`, `unslop`) into `.agents/skills/`, which is the one canonical skill catalog this repository publishes. Two links do not make a compatibility surface; they make a second, partial catalog whose contents silently disagree with the canonical one as skills are added. Remove the link directories and keep the hook configuration under `.codex/` and `.grok/` untouched.
…r degrade a live endpoint The Claude permission-posture check compared a running worker against the mutable current `config/claude-permission-mode`, let a failed or malformed process read downgrade an endpoint already proven live, paid a second `pane process-info` round-trip per endpoint at session start, attributed any `claude-*` process name as the worker, dropped its notification marker on a temporary unobserved foreground, and reported conflicting flags as duplicate processes. Correct all of it within the accepted intent: - Every Claude launch records the mode it actually carried as `claude_permission_mode=` in the task's meta (`fm-spawn.sh` owns the field), and every posture check compares the process against that record. A config edit therefore changes only the next launch and never replaces a worker that still carries the posture it was launched with; a record with no recorded mode has no drift expectation and keeps its generic state. - An unreadable, malformed, foreign-pane, or absent argv is an observability gap, not drift: the endpoint keeps its proven live state and ordinary lifecycle control stays available. Drift and ambiguity come only from an exact argv the read actually described. - The liveness classifier shares its one `pane process-info` snapshot with the posture read (`FM_BACKEND_HERDR_SNAPSHOT_FILE`), and the session-start digest adds exactly one narrow posture read for a live Claude endpoint with a recorded mode instead of a full classification. - Attribution accepts only the exact executable name `claude`; a `claude-usage` helper or claude-named script is never the worker. - The watcher keeps its one-per-episode drift marker through an unobserved poll and clears it only on a conforming observation or a gone endpoint, and its ambiguity reason names both causes (more than one foreground Claude process, or one process carrying both permission flags). Docs, skills, and the regression tests follow the recorded-launch contract, with new regressions for the config-edit, unrecorded-mode, unreadable-read, snapshot-reuse, helper-process, and bounded-digest cases.
…apping The writing skills ship their license text beside SKILL.md. The changed selection had no arm for that path, so every branch carrying a skill license refused the whole selection as an unmapped source. A license is documentation with no executable reader, so it now selects no suite, the same rule the top-level LICENSE already follows.
bin/fm-test-run.sh builds every coverage-guard set with LC_ALL=C sort but compared them with comm under the ambient collation. GNU comm checks input order under that collation and exits non-zero when a C-sorted list looks unsorted to a dictionary-collating locale, so --check-coverage failed on an untouched tree in any en_US.UTF-8 developer shell while the C.UTF-8 CI runners never saw it. Every comm in the runner and in its contract test now runs under LC_ALL=C, and a regression test pins both the static invariant and a real run under an installed dictionary-collating locale.
…zation branch The fork's protected main landed the upstream-only sync PR as a merge commit, so this merge carries the three official commits b85e28b, 8b10b61, and 2da3c5e into the customization branch without rewriting either ancestry. The one conflict was in bin/fm-spawn.sh, where the official autoformat re-indented the inline permission-mode block that the customization had already moved into bin/fm-claude-permission-lib.sh; the resolution keeps the two helper calls.
The Herdr permission-posture classifier judged every foreground process whose executable name was `claude`. A worker's own shell tool can run a nested claude CLI inside the same foreground group, so a healthy worker read as an ambiguous pair, which refused every lifecycle action while the drift check stayed blind, and a nested invocation without the flag read as the worker's own drift. Attribution is now anchored on the foreground process group leader: the job the pane shell (or the nested worktree shell under it) started in the foreground, which is the process the recorded launch or Herdr's restoration of it produced. Only that process's exact argv is judged, so a nested claude CLI is never the worker, a genuine top-level drift is still found beside a conforming child, and `ambiguous` now means exactly one thing: the top-level worker carries both permission flags. A snapshot with no numeric group id, or one listing that pid twice, is unreadable and keeps the proven live verdict. The `permission-drift` arms in the two three-state compatibility views were unreachable: every caller passes a bare target, so the generic read never produces that state. Both views now take only the target and document that they never read a posture. The startup sweep's remote drift recovery handed the configured effort token to the far host unvalidated. It now applies the same guards as bin/fm-secondmate-restart.sh: an unrecognized token falls back to the default, and an Ultra pin that does not select native Codex through Pi is refused before anything on the host is stopped. Fixtures carry the foreground process group id the anchor reads, and new regressions cover a nested claude CLI under a conforming worker, top-level drift beside a conforming nested child, a leader-less group, a conflicting top-level posture, and the remote effort guards.
…lag stale Herdr record
fm_claude_argv_permission_state accepted any array of strings as a readable command line, so a zero-length argv fell through to the drifted verdict with no flag evidence at all. Through the Herdr classifier that verdict is not inert: the startup sweep relaunches the worker, the remote control path relaunches the remote copy, and the watcher raises a stale wake. An empty argv is the same observability gap as an absent or malformed one, reachable when a provider cannot read a command line, such as a leader caught defunct mid-exit whose name still reads claude, so it now reads unreadable and keeps the proven live verdict. The Herdr suite gains the empty-argv case beside the absent and null ones; it read permission-drift before this change.
…ization branch
…dation) into the customization branch
…-then-restart owner
… recovery feature
…ish guard Restore bin/fm-lint.sh and tests/fm-lint.test.sh to fork main, and remove docs/verification/lint-ci-memory.md along with its documentation-audiences entry. Every reference to that record lived inside the restored or removed files, so no live document or script loses a fact it still needs. Reduce the Lavish live guard to the provider's own suppression contract plus the cleanup ordering: export LAVISH_AXI_NO_OPEN=1, drop the test-local pass-through wrapper, the audit plumbing, the redundant --no-open on the direct open, and the two audit assertions. The verification record keeps the accurate remainder of its sentence without the tripwire clause. The retained retirement step demanded a successful home sweep at the exact instant the detached listener was exiting, where the sweep reads an alive but unreadable owner as uncertain and refuses to retire. That failed about half of all runs. It now waits for the exit to settle within the same bound the session end already uses.
…ule, and the license mapping Make the skill-license regression in tests/fm-test-run.test.sh check the runner's exit status before asserting empty output. The previous form could not tell "selected nothing and exited 0" apart from "refused the whole selection and exited 2", so it passed with the mapping arm removed. It now fails with the arm removed and passes with it in place. Restore everything outside the branch's purpose to fork main: the live Lavish board guard and its verification record, bin/fm-pending-reply-lib.sh, the procevent, bootstrap, and secondmate-liveness suites, the comm collation changes in bin/fm-test-run.sh, and every other hunk of tests/fm-test-run.test.sh. Fork main already launches Claude with --dangerously-skip-permissions through config/claude-permission-mode, so no Claude recovery, posture, or attribution code remains. What stays is the bro and unslop skills with their licenses and documentation-audience entries, the AGENTS.md public-repository ownership rule, the skill-license mapping arm in bin/fm-test-run.sh, and its regression.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
do research to figure out the best plan to make a private mirror of firstmate that we can then use locally and update from the official mainline firstmate from time to time. then implement this plan
please use the gh cli to make a new private repo named firstmate
the gh cli only has one account attached to it, brentsec
okay i upgraded to pro
can you have an agent implement to protected branch on the private repo
okay lets change firstmate to a public fork of firstmate rather than a private repo. fork firstmate on github, and then add all my current changes
make sure to also sync the firstmate repo with the upstream firstmate repo before adding my customizations to my fork.. its currently 1 commit behind firstmate's upstream currently.
The approved plan is the public fork
brentsec/firstmatewith protectedmainas the tested distribution branch, forkorigin, fetch-only officialupstream, all five custom commits preserved, merge-based official imports, and guarded/updatefirstmatedeployment.related to the firstmate approvals previously mentioned, both are approved
can you now apply the customizations that we previously made with bro/unslop and the claude changes?
for claude restoration change, option 1 is approved
Option 1 means fixing process attribution so only the actual Claude worker is checked, removing unreachable permission-drift compatibility code, and adding regression tests that prevent nested Claude CLI commands from triggering a false worker restart.
why are we still working on this? firstmate seems fine now that its launching claude code with
--dangerously-skip-permissionswhich is all that was needed to fix the original problemWhat Changed
.agents/skills/:bro(restate the last reply in plain language,disable-model-invocation: trueso it only runs when invoked as/bro) andunslop(AI-tell removal, declared always-applicable so the agent's own loader applies it). Each ships its upstreamLICENSEand carriesmetadata.internal: trueto stay hidden from installer discovery, and both are registered asagent-runtimedocs indocs/documentation-audiences.json.AGENTS.md: never create, update, or merge a PR in a GitHub repository the captain does not personally own without explicit per-repository authorization, and a project'syoloposture never relaxes that ownership gate.README.mdandCONTRIBUTING.mdare updated to explain the vendored-skill rationale for themetadata.internalflag, list/broin the skill table, and record the third-party license notice..agents/skills/*/LICENSEin thebin/fm-test-run.shchanged-path selector so a license-only change selects no suites instead of refusing the whole selection as an unmapped source path, covered by a new case intests/fm-test-run.test.sh.Risk Assessment
✅ Low: The branch is reduced to 161 purely additive lines across eight paths - two vendored MIT prose skills with upstream-accurate licenses, their required documentation-audience entries, one AGENTS.md policy sentence whose wording you directed twice, and a single changed-test mapping arm whose regression I traced and confirmed genuinely fails without the arm.
Testing
I derived seven scenarios from the intent (ship the bro/unslop skills, keep the changed-test selection working for their licenses, and make the public-repo ownership rule override yolo) and drove six of them against the running product. The skill-LICENSE mapping behaves correctly end to end: a real edit to
.agents/skills/bro/LICENSEnow yields "no tests selected" and exit 0, and with the mapping arm deleted the same command refuses with exit 2 and the new regression guard fails with its intended message - so the guard actually protects the arm. The audience inventory consumer accepts the two new skill entries and fails by name when one is removed, andtests/fm-documentation-audiences.test.shis green. In a live Claude session in this home both skills resolve through the.claude/skillssymlink and act on their instructions, while an invented command returns "Unknown command", which rules out the model simply improvising. For the AGENTS.md rule I ran a real before/after: with the new sentence the agent refuses the non-owned-repo merge and cites the ownership gate as overriding yolo, with fork main's AGENTS.md it refuses only by judgment and explicitly notes the contract has no such rule, and on the captain's own repo under yolo it still proceeds, so the gate is targeted rather than a blanket block. The seventh scenario - whether unslop auto-applies to an unprompted writing task - is reported untested: a single model run cannot establish a live pass or fail for discretionary auto-invocation. No screenshots apply - this change has no UI surface; the reviewer-visible artifacts are CLI transcripts and agent session transcripts. Two local conditions kepttests/fm-test-run.test.shfrom completing in the default environment: a UTF-8 locale collation mismatch in--check-coveragethat I proved identical on fork main and worked around withLC_ALL=C, and a missingrubyinterpreter on this host; neither comes from this change.bin/fm-test-run.sh --list --changed --base HEADand--changed --base HEADafter appending to .agents/skills/bro/LICENSE: "no tests selected for changes vs HEAD", exit 0 (01-skill-license-selects-n…bin/fm-test-run.sh --list --changed --base b6104e3dlists 38 suites, exit 0, no unmapped-source refusal (15-branch-changed-selection.txt)bin/fm-doc-audience-check.sh-> ok surfaces=102; with the bro entry removed -> "unclassified: .agents/skills/bro/SKILL.md", exit 1;tests/fm-documentation-audiences.test.shgreen (07-doc-audience-…claude -p "/bro"andclaude -p "/unslop"load and act on each skill's instruction, while the control/unslop-not-a-real-skillreturns "Unknown command" (16-claude-skill-resolution.txt)claude -pwriting-task run, which does not establish a live pass or fail: automatic skill invocation is model discretion rather than a deterministic product surfa…Evidence: Skill LICENSE edit accepted by the changed-test selection (exit 0, nothing selected)
Source: Skill LICENSE edit accepted by the changed-test selection (exit 0, nothing selected)
Evidence: Adversarial: mapping arm removed, runner refuses the whole selection (exit 2)
Source: Adversarial: mapping arm removed, runner refuses the whole selection (exit 2)
$ bin/fm-test-run.sh --list --changed --base HEAD fm-test-run: no changed-test mapping for source path: .agents/skills/bro/LICENSE (exit=2)Evidence: Adversarial: the new regression guard fails without the arm
Source: Adversarial: the new regression guard fails without the arm
not ok - a skill license change refused the selectionEvidence: Targeted suite with the arm present (guard ok; run under LC_ALL=C)
Source: Targeted suite with the arm present (guard ok; run under LC_ALL=C)
Evidence: Documentation-audience inventory: accepts the new skills, fails when the bro entry is dropped
Source: Documentation-audience inventory: accepts the new skills, fails when the bro entry is dropped
$ bin/fm-doc-audience-check.sh fm-doc-audience-check: ok surfaces=102 local_links=399 (exit=0) # with the bro entry removed fm-doc-audience-check: unclassified: .agents/skills/bro/SKILL.md (exit=1)Evidence: Live Claude sessions: /bro and /unslop resolve, unknown command control
Source: Live Claude sessions: /bro and /unslop resolve, unknown command control
$ claude -p "/bro" -> "Captain, there's nothing to restate - this is the first message of the session..." $ claude -p "/unslop" -> "Captain, I'm ready to unslop. What text would you like me to edit..." $ claude -p "/unslop-not-a-real-skill" -> "Unknown command: /unslop-not-a-real-skill"Evidence: Ownership gate under yolo: refusal on a non-owned repo, merge on the captain's own repo, and the fork-main baseline
Source: Ownership gate under yolo: refusal on a non-owned repo, merge on the captain's own repo, and the fork-main baseline
branch HEAD, non-owned public repo, yolo ON: "No, captain. ... never create, update, or merge a PR in any public repository the captain does not personally own unless the captain explicitly authorizes that exact repository, and a project's approvedyoloposture never relaxes that ownership gate." branch HEAD, captain's own repo, yolo ON: "Yes, captain. ... brentsec/firstmate is your own repository, so the gate never engages here." fork main b6104e3d, non-owned public repo, yolo ON: refuses only by inference and notes "The contract has no rule that names third-party public repositories as their own gate."Evidence: Locale failure of --check-coverage is pre-existing on fork main
Source: Locale failure of --check-coverage is pre-existing on fork main
Evidence: Branch-wide changed-test selection accepted (38 suites, exit 0)
Source: Branch-wide changed-test selection accepted (38 suites, exit 0)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
.agents/skills/bro/LICENSE- branch carries 5 commit(s) that exist on your local main branch but were never pushed to origin/main; these may be unintended bundled work (proposed PR changes 8 file(s)):Confirm these commits belong in this PR before approving, or manually separate the intended work onto origin/main before gating.
🔧 **Review** - 3 issues found → auto-fixed ✅
AGENTS.md:33- The new hard-rule sentence and the pre-existing sentence directly below it give opposite answers for the most consequential case the guard exists to cover, and the new sentence is placed first so the older one reads as the resolving clause. Line 33 says "Never create, update, or merge a PR in any GitHub or other public repository the captain does not personally own unless the captain explicitly authorizes that exact repository." Line 34 (unchanged) says "A project's captain-approvedyoloposture is the only standing relaxation for merge authority", and section 7 defines that posture concretely at AGENTS.md:352: "with it on, firstmate merges green, in-scope work itself." Concrete sequence: project P is registered with delivery modeprand yolo on, and P's repository is a public repo under an org the captain contributes to but does not personally own. Firstmate finishes green, in-scope work and reads rule 2 top-down: line 33 forbids the merge absent per-repository authorization, line 34 states that P's yolo approval is a standing relaxation of exactly that authority. Nothing in either sentence says whether approving yolo for a project counts as "explicitly authorizes that exact repository", so the agent can merge into a third party's public repository under a standing posture, which is the outcome the new rule was added to prevent. The repository is itself a live instance of this shape: it is a public fork whose parent iskunchenguid/firstmate, a repo the captain does not own. The remedy is a policy decision about which sentence governs (for example ending line 33 with "including under a project'syoloposture", or naming the ownership gate as a non-relaxable carve-out in line 34), so it needs your word rather than a silent edit..agents/skills/unslop/SKILL.md:54-unslopships as a model-invocable skill (nodisable-model-invocation, unlike.agents/skills/bro/SKILL.md:8) whose description is "Cut AI tells from any writing. Must always apply.", and.agents/skills/is loaded by a running firstmate (AGENTS.md:557). Its rule 13 instructs "Use periods or commas only (no parentheses, no en dashes, no hyphen-as-dash substitutes)", which contradicts this repository's own stated prose convention of plain dashes, restated in each of your round instructions as "docs keep one sentence per line and plain dashes". Concrete sequence: an agent edits AGENTS.md or a docs/ page in this repo, auto-invokes the always-apply skill, and is told to strip the hyphen-as-dash separators that the repo's prose uses throughout, including the section 13 entries it would be editing ("diagnostic-reasoning- load before scoping a reported bug"). Editing the vendored text is not the right remedy since it is a verbatim MIT copy whosemetadata.sourceclaims upstream fidelity; the options are to gate invocation the waybroalready does (disable-model-invocation: true, so it applies only when you ask for it) or to accept the conflict. Either way it is your call.AGENTS.md:33- The approved plan element is "protectedmainas the tested distribution branch", and protection is in place on brentsec/firstmate main (PR required, force-push and deletion blocked, admins enforced, conversation resolution required), but the protection payload has norequired_status_checksentry. Both theCIworkflow and theRequire no-mistakesworkflow do run and are green on PRs fix(bin): sync upstream captain-hold, codex composer, and empty-steer fixes #1-chore(no-mistakes): exclude opt-in live tests from routine validation #3 and on main pushes, so the branch is in fact tested; they are simply advisory at the branch level, which leaves a red or unrun CI merge-able into the distribution branch by the same route the pipeline uses. The remedy is a GitHub repository-settings change (addCIandRequire no-mistakesas required checks on main), which is external lifecycle state not owned by this run and not something I would change on your behalf. Nothing in the branch diff causes this; I am reporting it because the plan names "tested" as the property of that branch.🔧 Fix applied.
✅ Re-checked - no issues remain.
bin/fm-test-run.sh:978- Pre-existing, not from this branch:bin/fm-test-run.sh --check-coveragefails under a UTF-8 locale (comm: file 2 is not in sorted order) because the sets are written withLC_ALL=C sortbut compared bycommunder the caller's locale. I proved it is not introduced here by running the fork-main copy of the runner (b6104e3) in the same shell - identical failure - and the branch runner underLC_ALL=C- cleanFM_TEST_COVERAGE ok. It blockstests/fm-test-run.test.shlocally in the default locale, so I ran that suite underLC_ALL=C. A fix for exactly this was deliberately dropped from the branch by your round-6 scope reduction, so I left it alone; CI's C.UTF-8 collation is unaffected.tests/fm-test-run.test.sh:1678- Host capability gap, not a code defect:tests/fm-test-run.test.shhard-fails its last-but-one assertion withnot ok - ruby is required to parse .github/workflows/ci.yml as YAMLbecause norubyinterpreter is installed on this machine, and installing system packages is outside this worktree's boundary. Everything else in that suite passes (37 ok), including the branch's new skill-LICENSE guard. To close it locally, install ruby (for examplesudo pacman -S ruby) and re-runLC_ALL=C bin/fm-test-run.sh tests/fm-test-run.test.sh; GitHub CI already has ruby and covers this assertion.bin/fm-test-run.sh --list --changed --base HEADand--changed --base HEADafter appending to .agents/skills/bro/LICENSE: "no tests selected for changes vs HEAD", exit 0 (01-skill-license-selects-n…bin/fm-test-run.sh --list --changed --base b6104e3dlists 38 suites, exit 0, no unmapped-source refusal (15-branch-changed-selection.txt)bin/fm-doc-audience-check.sh-> ok surfaces=102; with the bro entry removed -> "unclassified: .agents/skills/bro/SKILL.md", exit 1;tests/fm-documentation-audiences.test.shgreen (07-doc-audience-…claude -p "/bro"andclaude -p "/unslop"load and act on each skill's instruction, while the control/unslop-not-a-real-skillreturns "Unknown command" (16-claude-skill-resolution.txt)claude -pwriting-task run, which does not establish a live pass or fail: automatic skill invocation is model discretion rather than a deterministic product surfa…bin/fm-test-run.sh --list --changed --base HEADandbin/fm-test-run.sh --changed --base HEADafter appending to.agents/skills/bro/LICENSE(exit 0, nothing selected)Adversarial: deleted the.agents/skills/*/LICENSEarm frombin/fm-test-run.sh, re-ran the same two commands (exit 2,no changed-test mapping for source path: .agents/skills/bro/LICENSE), then restored the fileLC_ALL=C bin/fm-test-run.sh tests/fm-test-run.test.shwith the arm removed (not ok - a skill license change refused the selection) and with the arm present (that guardok; 37 ok total)bin/fm-test-run.sh --check-coveragewith the branch runner, the fork-main runner (b6104e3d), and underLC_ALL=C, to place the locale failure on fork main rather than this branchbin/fm-test-run.sh --list --changed --base b6104e3don the branch's real content (exit 0, 38 suites, no unmapped refusal)bin/fm-doc-audience-check.shon the real repo (ok, 102 surfaces) and again with thebroentry removed fromdocs/documentation-audiences.json(unclassified: .agents/skills/bro/SKILL.md, exit 1)bin/fm-test-run.sh tests/fm-documentation-audiences.test.sh(4 ok, exit 0)claude -p "/bro",claude -p "/unslop", and aclaude -p "/unslop-not-a-real-skill"control in this firstmate homeThree live firstmate sessions asking whether to merge PR #12 under yolo: non-owned public repo on branch HEAD, captain-owned repo on branch HEAD, and non-owned public repo with AGENTS.md reverted to fork mainclaude -pwriting task with only the Skill tool allowed, to see whetherunslopauto-invokes (single run; outcome not decisive either way)bin/fm-doc-audience-check.sh- Out-of-scope follow-up worth considering: nothing mechanically enforces that a new.agents/skills/*/SKILL.mdgets a documented invocation path.docs/documentation-audiences.jsonenforces classification only, and the trigger-hygiene rule in.agents/skills/firstmate-coding-guidelines/SKILL.md("Every new skill needs its load trigger declared inline: section 13 for agent-only reference skills, or the relevant operating section for anything else") is enforced by agent memory alone. This branch is the evidence:broandunslopwere added in 638b144 and classified in bd4caf8, yet shipped through several review rounds with no entry in README's user-invocable table and no trigger anywhere, so a captain reading README would not have known/broexisted. A narrow check inbin/fm-doc-audience-check.shasserting that every.agents/skills/*/SKILL.mdis either named in README's built-in skills table, listed in AGENTS.md section 13, or explicitly recorded as always-applicable would close it fail-closed. I did not implement it because this phase may only edit documentation and doc comments, not executable behavior.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.