Skip to content

feat(bin): add five opt-in advisory Jev integrations - #51

Merged
talktejas merged 10 commits into
mainfrom
fm/fm-jev-batch-integration-2
Oct 6, 2026
Merged

talktejas merged 10 commits into
mainfrom
fm/fm-jev-batch-integration-2

Conversation

@talktejas

@talktejas talktejas commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Intent

A set of Jev (TypeSafe decision-only model) integrations for firstmate was ordered built ("implement" on the rows of a ten-row proposal table, and "yeah incorporate above 5 too" on an earlier five-row table) and ordered merged: "merge all jev items". Eight have landed on main. Five more are now finished - each built and tested on its own local branch, not pushed, with no pull request:

  • screening a question before it reaches the captain - branch fm/fm-jev-escalation-screen at 1f2e1de
  • a cheaper model for mechanical side jobs inside a worker - branch fm/fm-jev-helper-model-pick at e0e375e
  • routing an incoming request to its project and second mate - branch fm/fm-jev-intake-routing at bb9179f
  • worker health (working, stuck, waiting or finished) - branch fm/fm-jev-worker-health at ad3d54f
  • "does this already exist?" search before building - branch fm/fm-jev-already-exists-search at 0d2d950

They were deliberately not landed one by one: every Jev item touches the same shared files (bin/fm-jev-lib.sh, docs/configuration.md, AGENTS.md, bin/fm-test-run.sh, docs/documentation-audiences.json), so each landing put the next into conflict. The standing delivery rule for a multi-part effort is one integration branch that merges once: "You should be putting everything in a different work tree. Check everything in a different work tree. When everything is ready to be merged, do so only then."

Rulings every Jev item must satisfy: nothing a worker typed in its shell is sent to TypeSafe; no project's source or diff is sent unless that project is named in config/jev-code-projects (a repository matches only as owner/repo, a bare name only a firstmate project name); every line that looks like a credential is withheld before anything is sent, inside the one Jev caller; Jev advises and never becomes the only thing that blocks, merges, approves or discards anything; a small answer never adds or removes a stop for the captain's approval; a feature adds nothing beyond what its row describes.

What each of the five items is, so this reads without the branches: (1) the escalation screen is one opt-in command that sorts a question firstmate is about to put to the captain into a fixed kind and prints one advisory line (yours, captain's, or by hand), with merges, approvals, destructive and security questions decided as captain's by code without any request; (2) the helper model pick lets a Claude worker's helper hand-off that names no model run on a cheaper model when the work is judged mechanical, only for a project named in config/jev-code-projects, only ever lowering the model and never stating a permission decision; (3) intake routing is one opt-in command that prints an advisory project and second mate for an incoming request from the two registries, never dispatching anything; (4) worker health is one opt-in command that prints a task's deterministic current-state line and may add one advisory health line (working, stuck, waiting, or finished) built from status records only, never terminal content; (5) the already-exists search is an opt-in command, only for a project named in config/jev-code-projects, that asks a yes-or-no question of a project's functions and prints the likely matches so a worker finds what exists before writing a second copy.

What Changed

  • Adds four opt-in advisory commands, each with its own test file and verification note: bin/fm-escalation-screen.sh (prints one advisory line for a question before it goes to the captain), bin/fm-intake-route.sh (prints an advisory project and second mate for an incoming request), bin/fm-worker-health.sh (prints a task's fm-crew-state.sh line and may add one health: line), and bin/fm-exists-search.sh (asks a yes-or-no question of a project's functions and prints likely file:line matches, with a --change run over the worker's own change).
  • Adds bin/fm-helper-model.sh and wires it into bin/fm-spawn.sh: a ship or scout Claude worker gets a PreToolUse hook on Agent|Task only when the pick is enabled for that home, project and launch model; otherwise the hooks file is written as before. bin/fm-brief.sh likewise adds the already-exists search step to a brief only when that search is enabled for the project.
  • Supporting changes: bin/fm-project-mode.sh gains --list and a finished registry token that leaves a project out of that list, bin/fm-house-rules-check.sh exposes its base lookup as house_rules_base for the search to share, bin/fm-test-run.sh registers and maps the five new test files, and AGENTS.md, docs/configuration.md, docs/scripts.md, docs/documentation-audiences.json and two skills document the new commands.

The five items

  1. Escalation screen - bin/fm-escalation-screen.sh: screens a question before it is put to the owner.
  2. Helper model pick - bin/fm-helper-model.sh: a cheaper model for mechanical side jobs inside a worker.
  3. Intake routing - bin/fm-intake-route.sh: routes an incoming request to its project and second mate.
  4. Worker health - bin/fm-worker-health.sh: working, stuck, waiting or finished.
  5. Already-exists search - bin/fm-exists-search.sh: "does this already exist?" before building.

Decisions confirmed during review

  • The finished registry token is kept on purpose. The instruction given for this feature was "for project maintain a seperate list so when u have new project or complete work on some project u can update that", so a project marked finished stops being offered by intake routing, and the project-management skill now says to set and clear the mark.
  • The already-exists search's --change mode is kept on purpose. The accepted row reads "Run it at the start of a task, and again on the new code in a change", so the second run is part of the row.
  • The helper model pick's durable log (state/.helper-model.log) was removed: nothing in its row needs a record.
  • The helper model pick leaves a worker launched with no model named alone, so it only ever lowers a helper's model and can never raise one.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: Both fix-round changes are verified against the code (the helper-model log, its arguments, docs and tests are fully removed with the spawn hook command and usage consistent; the widened captain word list matches every added test case and only ever errs toward the captain), and all five features remain opt-in, advisory, exit-0 paths that send only what their rows allow through the one Jev caller.

Testing

Ran the five feature test files (all passed), then drove the real bin/ scripts by hand in an isolated fixture home: a real git project, real registries, and a real task on a private tmux server read by the real fm-crew-state.sh. The TypeSafe endpoint was a stand-in curl on PATH that recorded each request and returned a chosen answer, because no TYPESAFE_API_KEY is available here. All 75 checks in the transcript passed, but the checks that rest on a stand-in answer are reported as untested scenarios, not live passes. Live passes cover: off without a key, code-decided outcomes with zero requests, project opt-in (a bare name not matching an owner/repo entry), credential lines withheld, the key kept off argv and child environments, usage errors, and nothing written into the home. One real Claude Code 2.1 session (worker on opus) ran the real hook and its helper ran on sonnet. Real model answers from api.typesafe.ai and a real worker spawn are untested. No UI surface, so no screenshots; evidence is CLI transcripts. The fixture was removed and the worktree is clean.

  • Live validation: ✅ go - 12 of 19 scenarios driven live against the product
Scenario Result Live Evidence
Every Jev command is off without a key: each of the five prints its off/by-hand outcome, exits 0, and makes no request ✅ pass live live-drive-transcript.txt: the "no key" case in each of sections 1-5 (requests-made=0, exit=0)
Escalation screen decides merge, approval, destructive and security questions as captain's by code with no request, even when the endpoint would have answered cheap-to-reverse at 0.99 (adversarial: ap… ✅ pass live live-drive-transcript.txt section 1, eight "code rule" cases, each screen: captain's (... not asked) with requests-made=0
Escalation screen leaves a review-gate line and an over-4000-character question by hand without sending or cutting them, and rejects a missing question with exit 2 ✅ pass live live-drive-transcript.txt section 1: review-gate, over-long and usage cases
Escalation screen prints yours only for a confident cheap-to-reverse or setting answer, captain's for trade at any confidence, and by hand for low confidence, unclear, an unknown answer or a failed en… ⏸️ untested no The prior payload did not establish a live result: no TYPESAFE_API_KEY was available, so the model's answer came from a recording stand-in for api.typesafe.ai, not the real endpoint. The real script a…
Helper model pick is gated: the hook is enabled only with a key, a project listed in config/jev-code-projects, and a named worker model stronger than sonnet; a bare name does not match an owner/repo e… ✅ pass live live-drive-transcript.txt section 2, --enabled exit codes table (0 only for key + listed project + opus)
Helper model hook sends nothing and prints nothing when the hand-off names a model, uses a non-inheriting helper type, the project is not listed, the key is absent, or stdin is not JSON ✅ pass live live-drive-transcript.txt section 2: five cases with empty output, exit=0, requests-made=0
Helper model hook, for a confident mechanical answer, prints only an updated input with model sonnet and no permission decision, hands the prompt on whole, and sends just description, type and prompt;… ⏸️ untested no The prior payload did not establish a live result: no TYPESAFE_API_KEY was available, so the mechanical/judgement answer came from a recording stand-in, not api.typesafe.ai. The real hook script handl…
In a real Claude Code session on opus, a helper hand-off that names no model runs on sonnet once the hook answers ✅ pass live live-claude-code-helper-model.txt: main session messages all claude-opus-5-5, helper messages all claude-sonnet-5-5 (real Claude Code and real hook script; the mechanical answer came from the stand-in…
Spawning a Claude ship or scout worker writes the hook into the worker's settings only when the gate holds, and the file is unchanged otherwise ⏸️ untested no The prior payload did not establish a live result: this was covered only by bash tests/fm-helper-model.test.sh, which runs the real bin/fm-spawn.sh on a fake tmux and passed. A real spawn launches a…
Marking a project finished in data/projects.md removes it from the offered list while keeping its registered posture ✅ pass live live-drive-transcript.txt section 3: fm-project-mode.sh --list prints storefront, ledger, catalogue-scrape; fm-project-mode.sh oldblog still prints direct-PR
Intake routing leaves an empty request and an over-20000-byte request by hand without a request, and never dispatches or writes anything ✅ pass live live-drive-transcript.txt section 3 (requests-made=0) and the HOME UNTOUCHED section (file hashes of the home identical before and after)
Intake routing prints an advisory project and second mate from the two registries, never offers the finished project, forces main for a local-only project, and goes by hand on low confidence, an off-l… ⏸️ untested no The prior payload did not establish a live result: no TYPESAFE_API_KEY was available, so the routing answer came from a recording stand-in, not api.typesafe.ai. The real script handled the stand-in an…
Worker health prints the real crew-state line unchanged and never asks about a task with no worktree or a secondmate; a bad task id is a usage error ✅ pass live live-drive-transcript.txt section 4: real fm-crew-state.sh read of a fixture task on a private tmux server; gone-worktree and secondmate cases with requests-made=0
Worker health adds one advisory health line (working, stuck, waiting or finished) only for a confident answer, built from status records only: the pane's content, text typed in the worker's shell and… ⏸️ untested no The prior payload did not establish a live result: no TYPESAFE_API_KEY was available, so the health answer came from a recording stand-in, not api.typesafe.ai. The crew-state read, the task records an…
Already-exists search sends no code for a project not named in config/jev-code-projects, including a bare name whose only entry is owner/repo ✅ pass live live-drive-transcript.txt section 5: otherproj and widgets cases, off line, requests-made=0
Already-exists search prints likely matches as file:line for a question, honours a path limit, names the existing function a newly added one may repeat under --change, never offers a secret-named file… ⏸️ untested no The prior payload did not establish a live result: no TYPESAFE_API_KEY was available, so the yes/no answers came from a recording stand-in, not api.typesafe.ai. The real script handled the stand-in an…
A ship brief gains the search lines only for an opted-in project with a key, and nothing otherwise ✅ pass live live-drive-transcript.txt section 5: fm_brief_exists_search_step output for proj (lines naming the command and --change) and for otherproj (empty)
Every credential-looking line is withheld before anything is sent, and the key reaches the endpoint only as a header read from a file descriptor, in all five commands ✅ pass live live-drive-transcript.txt: the "credential line withheld" and "key only on the fd header" checks in each of sections 1-5 (a planted GitHub token in the question, hand-off prompt, request text, registr…
Real answers from api.typesafe.ai sort real questions, hand-offs, requests, workers and functions sensibly ⏸️ untested no No TYPESAFE_API_KEY is available in this run's environment, and I did not go looking for one in another home. The author's own live runs are recorded in docs/verification/ (escalation-screen.md, helpe…
Evidence: Transcript of every hand-driven run (commands, stdout, stderr, exit code, requests made, what was sent)

Source: Transcript of every hand-driven run (commands, stdout, stderr, exit code, requests made, what was sent)

=================== 1. ESCALATION SCREEN ===================

\### no key: off, by hand, nothing sent
$ env FM_HOME=/tmp/fmdrive/off bin/fm-escalation-screen.sh Should dates show as DD/MM/YYYY or MM/DD/YYYY?
  stdout| screen: by hand (off, TYPESAFE_API_KEY absent from the environment and /tmp/fmdrive/off/.env)
  exit=0 requests-made=0
  PASS: by hand (off), exit 0, 0 requests

\### code rule (key present, stand-in would say cheap-to-reverse 0.99)
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.99 bin/fm-escalation-screen.sh Should I merge the pull request now?
  stdout| screen: captain's (names a merge, an approval, or a destructive, irreversible, or security-sensitive act; not asked)
  exit=0 requests-made=0
  PASS: captain's by code, 0 requests

\### code rule (key present, stand-in would say cheap-to-reverse 0.99)
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.99 bin/fm-escalation-screen.sh Has the captain approved shipping this?
  stdout| screen: captain's (names a merge, an approval, or a destructive, irreversible, or security-sensitive act; not asked)
  exit=0 requests-made=0
  PASS: captain's by code, 0 requests

\### code rule (key present, stand-in would say cheap-to-reverse 0.99)
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.99 bin/fm-escalation-screen.sh Should the old rows be deleted?
  stdout| screen: captain's (names a merge, an approval, or a destructive, irreversible, or security-sensitive act; not asked)
  exit=0 requests-made=0
  PASS: captain's by code, 0 requests

\### code rule (key present, stand-in would say cheap-to-reverse 0.99)
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.99 bin/fm-escalation-screen.sh Should the stale branch be dropped?
  stdout| screen: captain's (names a merge, an approval, or a destructive, irreversible, or security-sensitive act; not asked)
  exit=0 requests-made=0
  PASS: captain's by code, 0 requests

\### code rule (key present, stand-in would say cheap-to-reverse 0.99)
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.99 bin/fm-escalation-screen.sh Should I remove the legacy table?
  stdout| screen: captain's (names a merge, an approval, or a destructive, irreversible, or security-sensitive act; not asked)
  exit=0 requests-made=0
  PASS: captain's by code, 0 requests

\### code rule (key present, stand-in would say cheap-to-reverse 0.99)
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.99 bin/fm-escalation-screen.sh Rotate the API token now?
  stdout| screen: captain's (names a merge, an approval, or a destructive, irreversible, or security-sensitive act; not asked)
  exit=0 requests-made=0
  PASS: captain's by code, 0 requests

\### code rule (key present, stand-in would say cheap-to-reverse 0.99)
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.99 bin/fm-escalation-screen.sh Is it OK to FORCE-PUSH over the branch?
  stdout| screen: captain's (names a merge, an approval, or a destructive, irreversible, or security-sensitive act; not asked)
  exit=0 requests-made=0
  PASS: captain's by code, 0 requests

\### code rule (key present, stand-in would say cheap-to-reverse 0.99)
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.99 bin/fm-escalation-screen.sh Which password policy do we want?
  stdout| screen: captain's (names a merge, an approval, or a destructive, irreversible, or security-sensitive act; not asked)
  exit=0 requests-made=0
  PASS: captain's by code, 0 requests

\### review-gate line is by hand, not asked
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.99 bin/fm-escalation-screen.sh ask-user findings=2 on the storefront change
  stdout| screen: by hand (a review gate: bin/fm-finding-sort.sh and ask-user-authority own it)
  exit=0 requests-made=0
  PASS: by hand, 0 requests

\### over-long question is by hand, not cut and sent
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.99 bin/fm-escalation-screen.sh Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout? Which layout?
  stdout| screen: by hand (the question is over 4000 characters)
  exit=0 requests-made=0
  PASS: by hand, 0 requests

\### cheap-to-reverse at 0.95 -> yours
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.95 bin/fm-escalation-screen.sh Should the helper live in utils.py or a new export module?
export GH_TOKEN=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
  stdout| screen: yours (cheap-to-reverse, confidence 0.95)
  exit=0 requests-made=1
  PASS: yours, one request
  sent state: {"question":"Should the helper live in utils.py or a new export module?\n[line withheld: looks like a credential]"}
  PASS: state holds only the question; credential line withheld
  PASS: key only on the fd header: not on curl argv, not in child env

\### cheap-to-reverse below the floor (0.4) -> by hand
$ env FM_HOME=/tmp/fmdrive/home CHOICE=cheap-to-reverse CONF=0.4 bin/fm-escalation-screen.sh Should the empty-state text say No members yet?
  stdout| screen: by hand (cheap-to-reverse below the confidence floor, 0.4)
  exit=0 requests-made=1
  PASS: by hand

\### trade at low confidence (0.3) -> captain's (a small answer never removes the stop)
$ env FM_HOME=/tmp/fmdrive/home CHOICE=trade CONF=0.3 bin/fm-escalation-screen.sh Is commission clawed back on returned stock?
  stdout| screen: captain's (trade, confidence 0.3)
  exit=0 requests-made=1
  PASS: captain's

\### unclear -> by hand
$ env FM_HOME=/tmp/fmdrive/home CHOICE=unclear CONF=0.95 bin/fm-escalation-screen.sh Which one do you prefer?
  stdout| screen: by hand (unclear, confidence 0.95)
  exit=0 requests-made=1
  PASS: by hand

\### endpoint times out -> by hand, exit 0
$ env FM_HOME=/tmp/fmdrive/home STANDIN_FAIL=1 CHOICE=x CONF=1 bin/fm-escalation-screen.sh Should the helper live in utils.py?
  stdout| screen: by hand (no answer: http 000 after 12 ms: )
  exit=0 requests-made=1
  PASS: by hand, exit 0

\### answer outside the fixed kinds -> by hand
$ env FM_HOME=/tmp/fmdrive/home CHOICE=approve-it CONF=0.99 bin/fm-escalation-screen.sh Should the helper live in utils.py?
  stdout| screen: by hand (no answer: response is not a kind Choice answer)
  exit=0 requests-made=1
  PASS: by hand, never yours

\### no question -> usage error
$ env FM_HOME=/tmp/fmdrive/home bin/fm-escalation-screen.sh
  stderr| error: a question is required (see --help)
  exit=2 requests-made=0
  PASS: exit 2

=================== 2. HELPER MODEL PICK ===================
--enabled exit codes (0 = spawn adds the hook):
  key + listed project + opus worker        : 0
  no key                                    : 1
  project not listed                        : 1
  bare name 'widgets' vs listed acme/widgets: 1
  worker already sonnet                     : 1
  worker model not named                    : 1
  PASS: --enabled gate

\### mechanical 0.95, listed project -> lowered to sonnet
$ env CHOICE=mechanical CONF=0.95 bin/fm-helper-model.sh --hook /tmp/fmdrive/home proj
  stdout| {"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":{"description":"Find callers","prompt":"List every file under src/ that calls parse_rate and report file and line.\nexport GH_TOKEN=ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","subagent_type":"general-purpose","run_in_background":true,"model":"sonnet"}}}
  exit=0 requests-made=1
  PASS: prints updatedInput with model sonnet, other fields intact, no permission decision
  PASS: prompt handed on whole (credential line still in the worker's own hand-off)
  sent state: {"helper":{"description":"Find callers","type":"general-purpose","prompt":"List every file under src/ that calls parse_rate and report file and line.\n[line withheld: looks like a credential]"}}
  PASS: request holds only description, type, prompt; credential line withheld
  PASS: key only on the fd header: not on curl argv, not in child env

\### judgement -> prints nothing
$ env CHOICE=judgement CONF=0.99 bin/fm-helper-model.sh --h

... [9890 bytes truncated] ...

idence (0.5) -> no health line
$ env FM_HOME=/tmp/fmdrive/home CHOICE=stuck CONF=0.5 bin/fm-worker-health.sh t1
  stdout| state: working · source: status-log · note GH_TOKEN="ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
  stderr| worker-health: no advice; stuck is below the confidence floor (0.5 < 0.6)
  exit=0 requests-made=1
  PASS: crew-state line alone

\### answer outside the four kinds -> no health line
$ env FM_HOME=/tmp/fmdrive/home CHOICE=kill-it CONF=0.99 bin/fm-worker-health.sh t1
  stdout| state: working · source: status-log · note GH_TOKEN="ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
  stderr| worker-health: no advice; no answer: response is not a health Choice answer
  exit=0 requests-made=1
  PASS: crew-state line alone

\### endpoint fails -> no health line, exit 0
$ env FM_HOME=/tmp/fmdrive/home STANDIN_FAIL=1 CHOICE=stuck CONF=0.9 bin/fm-worker-health.sh t1
  stdout| state: working · source: status-log · note GH_TOKEN="ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
  stderr| worker-health: no advice; no answer: http 000 after 13 ms: 
  exit=0 requests-made=1
  PASS: crew-state line alone

\### harness busy (real pane read) -> still asked, working
$ env FM_HOME=/tmp/fmdrive/home CHOICE=working CONF=0.95 bin/fm-worker-health.sh t1
  stdout| state: working · source: pane · harness busy (claude-hook)
  stdout| health: working (confidence 0.95, advice only)
  stderr| worker-health: working from one question in one request (finished=0.01666666666666668 stuck=0.01666666666666668 waiting=0.01666666666666668 working=0.95, 20 ms); advice only
  exit=0 requests-made=1
  PASS: source: pane line then health: working

\### worktree gone (source: none) -> code decides, never asked
$ env FM_HOME=/tmp/fmdrive/home CHOICE=stuck CONF=0.99 bin/fm-worker-health.sh t2
  stdout| state: unknown · source: none · worktree gone (torn down?)
  stderr| worker-health: no advice; code decided (source: none)
  exit=0 requests-made=0
  PASS: one line, 0 requests

\### secondmate -> never asked
$ env FM_HOME=/tmp/fmdrive/home CHOICE=stuck CONF=0.99 bin/fm-worker-health.sh t3
  stdout| state: working · source: status-log · note GH_TOKEN="ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
  stderr| worker-health: no advice; code decided (kind=secondmate is never asked about)
  exit=0 requests-made=0
  PASS: 0 requests, no health line

\### bad task id -> usage error
$ env FM_HOME=/tmp/fmdrive/home bin/fm-worker-health.sh ../etc
  stderr| error: a task id is required (see --help)
  exit=2 requests-made=0
  PASS: exit 2

=================== 5. ALREADY-EXISTS SEARCH ===================

\### no key: off
$ env FM_HOME=/tmp/fmdrive/off YES_RE=rate CONF=0.9 bin/fm-exists-search.sh proj Does this function parse a rate string into a number?
  stderr| exists-search: off (TYPESAFE_API_KEY absent from the environment and /tmp/fmdrive/off/.env); search by hand
  exit=0 requests-made=0
  PASS: off line, nothing printed, 0 requests

\### project not opted in: off, no code leaves
$ env FM_HOME=/tmp/fmdrive/home YES_RE=rate CONF=0.9 bin/fm-exists-search.sh otherproj Does this function parse a rate string into a number?
  stderr| exists-search: off (project "otherproj" is not a line of /tmp/fmdrive/home/config/jev-code-projects); search by hand
  exit=0 requests-made=0
  PASS: off, 0 requests

\### bare name 'widgets' does not match the owner/repo entry acme/widgets
$ env FM_HOME=/tmp/fmdrive/home YES_RE=rate CONF=0.9 bin/fm-exists-search.sh widgets Does this function parse a rate string into a number?
  stderr| exists-search: off (project "widgets" is not a line of /tmp/fmdrive/home/config/jev-code-projects); search by hand
  exit=0 requests-made=0
  PASS: off, 0 requests

\### opted-in project, a question
$ env FM_HOME=/tmp/fmdrive/home YES_RE=rate string CONF=0.93 bin/fm-exists-search.sh proj Does this function parse a rate string into a number?
  stdout| lib/new.sh:1: yes 0.93: rate_from_string() {
  stdout| lib/rates.sh:1: yes 0.93: parse_rate() {
  stderr| exists-search: 2 advisory match(es) from 4 of 4 function(s) asked; a match is a place to read, and no match is not proof
  exit=0 requests-made=1
  PASS: prints the two functions that do it, as file:line
  files sent: lib/new.sh lib/rates.sh lib/util.py 
  PASS: secret-named file never offered; credential line inside a function withheld
  PASS: key only on the fd header: not on curl argv, not in child env

\### path limit
$ env FM_HOME=/tmp/fmdrive/home YES_RE=. CONF=0.9 bin/fm-exists-search.sh proj Does this function parse a rate string into a number? lib/util.py
  stdout| lib/util.py:1: yes 0.9: def slugify(name):
  stderr| exists-search: 1 advisory match(es) from 1 of 1 function(s) asked; a match is a place to read, and no match is not proof
  exit=0 requests-made=1
  PASS: only lib/util.py asked about

\### yes below the floor -> no match
$ env FM_HOME=/tmp/fmdrive/home YES_RE=rate string CONF=0.5 bin/fm-exists-search.sh proj Does this function parse a rate string into a number?
  stderr| exists-search: 0 advisory match(es) from 4 of 4 function(s) asked; a match is a place to read, and no match is not proof
  exit=0 requests-made=1
  PASS: nothing printed, exit 0

\### endpoint fails -> no match, exit 0, says how many went unasked
$ env FM_HOME=/tmp/fmdrive/home STANDIN_FAIL=1 YES_RE=rate CONF=0.9 bin/fm-exists-search.sh proj Does this function parse a rate string into a number?
  stderr| exists-search: 0 advisory match(es) from 0 of 4 function(s) asked; 4 not asked because 1 request(s) failed, last: http 000 after 14 ms: ; a match is a place to read, and no match is not proof
  exit=0 requests-made=1
  PASS: exit 0, nothing printed

\### --change on the feat branch (adds rate_from_string)
$ env FM_HOME=/tmp/fmdrive/home YES_RE=rate string CONF=0.96 bin/fm-exists-search.sh proj --change
  stdout| lib/new.sh:1: may repeat lib/rates.sh:1 (yes 0.96): parse_rate() {
  stderr| exists-search: 1 advisory match(es) for 1 function(s) added since main, from 3 comparison(s); a match is a place to read, and no match is not proof
  exit=0 requests-made=1
  PASS: names the existing function the new one may repeat

\### --enabled gate
$ env FM_HOME=/tmp/fmdrive/home bin/fm-exists-search.sh --enabled proj
  exit=0 requests-made=0
  PASS: exit 0 opted in; 1 when not

\### no question -> usage error
$ env FM_HOME=/tmp/fmdrive/home bin/fm-exists-search.sh proj
  stderr| error: a yes/no question or --change is required (see --help)
  exit=2 requests-made=0
  PASS: exit 2

\### ship brief gains the search lines only when opted in
$ bash -c . bin/fm-dod-lib.sh; fm_brief_exists_search_step /tmp/fmdrive/home /tmp/fmdrive/proj ship
  stdout| 
  stdout| For step 1 this project also has a search: `FM_HOME=/tmp/fmdrive/home ~/.no-mistakes/worktrees/3605d2c32b02/01M47BDF651PPA16VFF4HRAYP1/bin/fm-exists-search.sh proj "<yes/no question about one function>" [<path>...]`, run in the worktree, asks that question of the project's functions and prints the likely ones, one `file:line` per line, best first.
  stdout| It is advice and it misses things: read each function it names, and search by hand as well. No match, or a search that could not run, changes nothing.
  stdout| Once your work is committed, run `FM_HOME=/tmp/fmdrive/home ~/.no-mistakes/worktrees/3605d2c32b02/01M47BDF651PPA16VFF4HRAYP1/bin/fm-exists-search.sh proj --change`: it names each function your change adds that an existing one may already do; read each pair, reuse the existing function where it really is the same job, and leave alone a match you judge wrong.  exit=0 requests-made=0
  PASS: brief lines name the command and --change

\### brief for a project not opted in
$ bash -c . bin/fm-dod-lib.sh; fm_brief_exists_search_step /tmp/fmdrive/home /x/otherproj ship
  exit=0 requests-made=0
  PASS: adds nothing

=================== HOME UNTOUCHED ===================
files in the home after every run above:
  ./.env
  ./config/jev-code-projects
  ./data/projects.md
  ./data/secondmates.md
  ./state/t1.busy-gen
  ./state/t1.busy-state
  ./state/t1.meta
  ./state/t1.status
  PASS: no command wrote a record into the home (only the fixture's own busy-state changed)

TOTAL: 75 passed, 0 failed
Evidence: Real Claude Code session: worker on opus, helper lowered to sonnet by the hook

Source: Real Claude Code session: worker on opus, helper lowered to sonnet by the hook

main session assistant-message models: 2 claude-opus-5-5 helper (subagent) assistant-message models: 5 claude-sonnet-5-5 hand-off the hook sent: {"helper":{"description":"Find parse_rate callers","type":"general-purpose","prompt":"List every file under src/ that calls parse_rate and report file and line. Do not change anything."}}

Real Claude Code session (2.1.290 (Claude Code)), worker --model opus, PreToolUse hook = bin/fm-helper-model.sh --hook <home> proj
(TypeSafe endpoint replaced by a stand-in answering mechanical 0.95; Claude Code and the hook script are real)

hand-off the hook saw and sent to the endpoint:
{"helper":{"description":"Find parse_rate callers","type":"general-purpose","prompt":"List every file under src/ that calls parse_rate and report file and line. Do not change anything."}}

main session assistant-message models:
      2 claude-opus-5-5
helper (subagent) assistant-message models:
      5 claude-sonnet-5-5

session result:
`parse_rate` is called in one file under `src/`: `src/b.sh:1` (`parse_rate 5`); `src/a.sh:1` only defines it.
Evidence: Stand-in endpoint: the recording curl shim

Source: Stand-in endpoint: the recording curl shim

#!/usr/bin/env bash
# Stand-in for the TypeSafe endpoint: records what the product sends, answers from $STANDIN_JQ.
set -u
log=${STANDIN_LOG:?}
n=$(( $(cat "$log/calls" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$log/calls"
if [ -n "${TYPESAFE_API_KEY+x}" ] || [ -n "${TYPESAFE_API_KEY_PRIVATE+x}" ]; then echo key-in-child-env >> "$log/env"; else echo clean >> "$log/env"; fi
out=''
while [ $# -gt 0 ]; do case "$1" in -o) out=$2; shift 2 ;; *) printf '%s\n' "$1" >> "$log/argv"; shift ;; esac; done
cat > "$log/body.$n"
cat /dev/fd/3 > "$log/header" 2>/dev/null
[ "${STANDIN_FAIL:-0}" = 0 ] || exit 28
jq -f "${STANDIN_JQ:?}" "$log/body.$n" > "$out"
printf 200
Evidence: Stand-in endpoint: the answer program

Source: Stand-in endpoint: the answer program

. as $req | {model: "stand-in", answers: (.questions | to_entries | map(.key as $q | (.value.criteria | keys) as $k |
  (if env.YES_RE != null and ($req.state.functions[$q]? != null)
   then (if ($req.state.functions[$q].code | test(env.YES_RE)) then "yes" else "no" end)
   else (env["CHOICE_" + $q] // env.CHOICE) end) as $c | (env.CONF | tonumber) as $p |
  {key: $q, value: {choice: $c, confidence: $p, probabilities: ($k | map({key: ., value: (if . == $c then $p else ((1 - $p) / (($k | length) - 1)) end)}) | from_entries)}}) | from_entries)}
Evidence: Escalation screen: code-decided and withheld-credential excerpt
$ fm-escalation-screen.sh "Has the captain approved shipping this?" (key present, endpoint would say cheap-to-reverse 0.99)
screen: captain's (names a merge, an approval, or a destructive, irreversible, or security-sensitive act; not asked)
exit=0 requests-made=0

$ fm-escalation-screen.sh "Should the helper live in utils.py or a new export module?\nexport GH_TOKEN=ghp_aaaa..."
screen: yours (cheap-to-reverse, confidence 0.95)
sent state: {"question":"Should the helper live in utils.py or a new export module?\n[line withheld: looks like a credential]"}
Evidence: Worker health: what was sent for a real fixture task
$ fm-worker-health.sh t1
state: working · source: status-log · note GH_TOKEN="ghp_aaaa..."
health: stuck (confidence 0.9, advice only)
sent state: {"worker":{"kind":"ship","current_state":"[line withheld: looks like a credential]","status_events":["working: [2026-10-06T10:00:00Z] setup done, starting on the fix","[line withheld: looks like a credential]"],"minutes_since_last_status_event":0,"minutes_since_turn_ended":null,"minutes_since_activity":null,"unread_instructions":1,"oldest_unread_instruction_minutes":0}}
(pane held the marker PANE-TYPED-MARKER-zz91 and the inbox held INBOX-MESSAGE-TEXT-q7; neither appears in the request)
Evidence: Already-exists search: opt-in gate and matches
$ fm-exists-search.sh widgets "Does this function parse a rate string into a number?" (config lists acme/widgets)
exists-search: off (project "widgets" is not a line of .../config/jev-code-projects); search by hand
exit=0 requests-made=0

$ fm-exists-search.sh proj "Does this function parse a rate string into a number?"
lib/new.sh:1: yes 0.93: rate_from_string() {
lib/rates.sh:1: yes 0.93: parse_rate() {
files sent: lib/new.sh lib/rates.sh lib/util.py (lib/secrets.sh never offered)

$ fm-exists-search.sh proj --change
lib/new.sh:1: may repeat lib/rates.sh:1 (yes 0.96): parse_rate() {
Evidence: Feature test file results

Source: Feature test file results

escalation-screen, helper-model, intake-route, worker-health, exists-search: each file exited 0 with no not ok line (logs test-*.log in the same directory)

ok - off without the key: no call, by hand
ok - only a confident setting or cheap-to-reverse is yours; trade and costly-to-undo are the captain's
ok - code decides a merge, approval, destructive or security question, a review gate, and an over-long question without asking
ok - executable: one fixed five-way question through the shared caller; key on the fd header only
ok - executable: a malformed answer is by hand, and a credential line is withheld from the request
# all fm-escalation-screen tests passed

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 5 issues found → auto-fixed ✅
  • ⚠️ bin/fm-escalation-screen.sh:56 - ESCALATION_SCREEN_CAPTAIN_WORDS lists every inflection of merge but only the base forms of the other words, and omits common destructive/security terms. Checked with the same grep -Eiqw: "Has the captain approved shipping this?", "Should the old rows be deleted?", "Should the stale branch be dropped?", "Should I remove the legacy table?" and "Rotate the API token now?" all miss the code rule, so the question is sent to Jev and a confident cheap-to-reverse answer prints screen: yours. The intent requires "merges, approvals, destructive and security questions decided as captain's by code without any request". Fix: add the missing inflections (approves/approved/approving, deleted/deletion, dropped/drops, discarded, destroyed, wiped, overwritten/overwrites, forced) and the obvious missing terms (remove/removed/removal, token, key, revert, reset), and extend the test loop at tests/fm-escalation-screen.test.sh:91 with those cases.
  • ⚠️ bin/fm-project-mode.sh:81 - Simplification: the change introduces a new finished token in the data/projects.md bracket grammar (parsed in --list at line 81, tolerated by the main parser at line 115, documented in docs/configuration.md "Intake routing"). The intake-routing row only requires printing "an advisory project and second mate for an incoming request from the two registries"; no requirement needs a new registry marker for hiding completed projects, and the intent says "a feature adds nothing beyond what its row describes". Recommend removing the finished token and having --list print every entry.
  • ⚠️ bin/fm-exists-search.sh:266 - Simplification: fm-exists-search.sh &lt;project&gt; --change is a second mode that compares each function the change already added against existing ones (up to 25 new x 16 candidates), plus the ship-brief line that tells the worker to run it (bin/fm-dod-lib.sh:278) and the house_rules_base extraction in bin/fm-house-rules-check.sh that exists only to serve it. The row describes a command "that asks a yes-or-no question of a project's functions and prints the likely matches so a worker finds what exists before writing a second copy" - the question mode alone satisfies that; --change runs after the copy is written and exceeds the row. Recommend removing --change, its brief line, and the house_rules_base extraction.
  • ⚠️ bin/fm-helper-model.sh:83 - Simplification: the helper model pick adds a new durable record, state/.helper-model.log (_fm_hm_record: one JSON line per asked hand-off with the description's first 80 characters, size-capped rotation, AGENTS.md state-layout entry). The row only requires letting a no-model helper hand-off "run on a cheaper model when the work is judged mechanical ... only ever lowering the model and never stating a permission decision"; no requirement needs a persisted log, and the other four items write no record. Recommend removing the log (or confirm it is wanted as the evidence trail the ponytail comment at line 56 refers to).
  • ℹ️ bin/fm-exists-search.sh:93 - git grep with no pathspec searches and reports paths relative to the current directory, while the --change git diff reports repo-root-relative paths, so a --change run from a subdirectory of the worktree silently finds 0 added functions (and a question run covers only that subtree). The brief and header say to run it in the worktree root, so intended usage is unaffected; noted only as a tradeoff.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 12 of 19 scenarios driven live against the product
Scenario Result Live Evidence
Every Jev command is off without a key: each of the five prints its off/by-hand outcome, exits 0, and makes no request ✅ pass live live-drive-transcript.txt: the "no key" case in each of sections 1-5 (requests-made=0, exit=0)
Escalation screen decides merge, approval, destructive and security questions as captain's by code with no request, even when the endpoint would have answered cheap-to-reverse at 0.99 (adversarial: ap… ✅ pass live live-drive-transcript.txt section 1, eight "code rule" cases, each screen: captain&#39;s (... not asked) with requests-made=0
Escalation screen leaves a review-gate line and an over-4000-character question by hand without sending or cutting them, and rejects a missing question with exit 2 ✅ pass live live-drive-transcript.txt section 1: review-gate, over-long and usage cases
Escalation screen prints yours only for a confident cheap-to-reverse or setting answer, captain's for trade at any confidence, and by hand for low confidence, unclear, an unknown answer or a failed en… ⏸️ untested no The prior payload did not establish a live result: no TYPESAFE_API_KEY was available, so the model's answer came from a recording stand-in for api.typesafe.ai, not the real endpoint. The real script a…
Helper model pick is gated: the hook is enabled only with a key, a project listed in config/jev-code-projects, and a named worker model stronger than sonnet; a bare name does not match an owner/repo e… ✅ pass live live-drive-transcript.txt section 2, --enabled exit codes table (0 only for key + listed project + opus)
Helper model hook sends nothing and prints nothing when the hand-off names a model, uses a non-inheriting helper type, the project is not listed, the key is absent, or stdin is not JSON ✅ pass live live-drive-transcript.txt section 2: five cases with empty output, exit=0, requests-made=0
Helper model hook, for a confident mechanical answer, prints only an updated input with model sonnet and no permission decision, hands the prompt on whole, and sends just description, type and prompt;… ⏸️ untested no The prior payload did not establish a live result: no TYPESAFE_API_KEY was available, so the mechanical/judgement answer came from a recording stand-in, not api.typesafe.ai. The real hook script handl…
In a real Claude Code session on opus, a helper hand-off that names no model runs on sonnet once the hook answers ✅ pass live live-claude-code-helper-model.txt: main session messages all claude-opus-5-5, helper messages all claude-sonnet-5-5 (real Claude Code and real hook script; the mechanical answer came from the stand-in…
Spawning a Claude ship or scout worker writes the hook into the worker's settings only when the gate holds, and the file is unchanged otherwise ⏸️ untested no The prior payload did not establish a live result: this was covered only by bash tests/fm-helper-model.test.sh, which runs the real bin/fm-spawn.sh on a fake tmux and passed. A real spawn launches a…
Marking a project finished in data/projects.md removes it from the offered list while keeping its registered posture ✅ pass live live-drive-transcript.txt section 3: fm-project-mode.sh --list prints storefront, ledger, catalogue-scrape; fm-project-mode.sh oldblog still prints direct-PR
Intake routing leaves an empty request and an over-20000-byte request by hand without a request, and never dispatches or writes anything ✅ pass live live-drive-transcript.txt section 3 (requests-made=0) and the HOME UNTOUCHED section (file hashes of the home identical before and after)
Intake routing prints an advisory project and second mate from the two registries, never offers the finished project, forces main for a local-only project, and goes by hand on low confidence, an off-l… ⏸️ untested no The prior payload did not establish a live result: no TYPESAFE_API_KEY was available, so the routing answer came from a recording stand-in, not api.typesafe.ai. The real script handled the stand-in an…
Worker health prints the real crew-state line unchanged and never asks about a task with no worktree or a secondmate; a bad task id is a usage error ✅ pass live live-drive-transcript.txt section 4: real fm-crew-state.sh read of a fixture task on a private tmux server; gone-worktree and secondmate cases with requests-made=0
Worker health adds one advisory health line (working, stuck, waiting or finished) only for a confident answer, built from status records only: the pane's content, text typed in the worker's shell and… ⏸️ untested no The prior payload did not establish a live result: no TYPESAFE_API_KEY was available, so the health answer came from a recording stand-in, not api.typesafe.ai. The crew-state read, the task records an…
Already-exists search sends no code for a project not named in config/jev-code-projects, including a bare name whose only entry is owner/repo ✅ pass live live-drive-transcript.txt section 5: otherproj and widgets cases, off line, requests-made=0
Already-exists search prints likely matches as file:line for a question, honours a path limit, names the existing function a newly added one may repeat under --change, never offers a secret-named file… ⏸️ untested no The prior payload did not establish a live result: no TYPESAFE_API_KEY was available, so the yes/no answers came from a recording stand-in, not api.typesafe.ai. The real script handled the stand-in an…
A ship brief gains the search lines only for an opted-in project with a key, and nothing otherwise ✅ pass live live-drive-transcript.txt section 5: fm_brief_exists_search_step output for proj (lines naming the command and --change) and for otherproj (empty)
Every credential-looking line is withheld before anything is sent, and the key reaches the endpoint only as a header read from a file descriptor, in all five commands ✅ pass live live-drive-transcript.txt: the "credential line withheld" and "key only on the fd header" checks in each of sections 1-5 (a planted GitHub token in the question, hand-off prompt, request text, registr…
Real answers from api.typesafe.ai sort real questions, hand-offs, requests, workers and functions sensibly ⏸️ untested no No TYPESAFE_API_KEY is available in this run's environment, and I did not go looking for one in another home. The author's own live runs are recorded in docs/verification/ (escalation-screen.md, helpe…
  • bash tests/fm-escalation-screen.test.sh
  • bash tests/fm-helper-model.test.sh
  • bash tests/fm-intake-route.test.sh
  • bash tests/fm-worker-health.test.sh
  • bash tests/fm-exists-search.test.sh
  • bin/fm-escalation-screen.sh &#34;&lt;question&gt;&#34; by hand: no key, 8 merge/approval/destructive/security questions, review-gate line, over-4000-character question, stand-in answers at high and low confidence, endpoint failure, unknown answer, no argument
  • bin/fm-helper-model.sh --enabled &lt;home&gt; &lt;project&gt; &lt;model&gt; over 6 gate combinations, and --hook &lt;home&gt; &lt;project&gt; with hook JSON on stdin: mechanical, judgement, low confidence, model already named, Explore helper type, unlisted project, bare name vs owner/repo, no key, endpoint failure, non-JSON stdin
  • bin/fm-project-mode.sh --list and bin/fm-project-mode.sh oldblog on a registry with a finished entry
  • bin/fm-intake-route.sh with the request on stdin: no key, routed request, local-only override, low confidence, off-list answer, empty request, 20001-byte request, endpoint failure
  • bin/fm-worker-health.sh &lt;task&gt; on a real fixture task (real bin/fm-crew-state.sh, private tmux server, bin/fm-busy-event.sh idle/busy): no key, each of the four health kinds, low confidence, unknown answer, endpoint failure, worktree gone, secondmate, bad task id
  • bin/fm-exists-search.sh &lt;project&gt; &#34;&lt;question&gt;&#34; [&lt;path&gt;], --change, and --enabled in a fixture git repo: no key, unlisted project, bare name vs owner/repo, matches, path limit, low confidence, endpoint failure, usage error
  • fm_brief_exists_search_step from bin/fm-dod-lib.sh for an opted-in and a not-opted-in project
  • claude -p ... --model opus in a scratch repo whose .claude/settings.local.json PreToolUse hook runs bin/fm-helper-model.sh --hook, then read the session and helper transcripts for the models used
  • File-hash snapshot of the fixture home before and after every run
🔧 **Document** - 1 issue found → auto-fixed ✅
  • ℹ️ .agents/skills/project-management/SKILL.md:22 - The finished bracket token for data/projects.md is documented only in docs/configuration.md "Intake routing" and bin/fm-project-mode.sh's header. The project-management skill, which is the agent-runtime owner firstmate reads when maintaining the registry, never tells it to add finished when a project's work is complete, so the list may not get updated in practice. I left it unedited because adding it changes firstmate's operating contract rather than fixing a stale fact; a one-line pointer from that skill to docs/configuration.md "Intake routing" would close it if wanted.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…reaches the captain

bin/fm-escalation-screen.sh sorts one question's text into a fixed kind
through the shared Jev caller and prints one advisory line: yours, captain's,
or by hand. Code decides a merge, approval, destructive or security question,
a review gate, and an over-long question without a request. Off without
TYPESAFE_API_KEY; every failure is by hand; it writes nothing.
…er hand-offs with Jev, opt-in

With TYPESAFE_API_KEY and the project listed in config/jev-code-projects, a
ship or scout Claude worker gets a PreToolUse hook on its helper-agent tool.
Code decides first (a named model or a non-inheriting helper type is left
alone); otherwise one fixed-choice question, mechanical or judgement, and a
confident mechanical runs that helper on sonnet. Every other outcome keeps
the model the worker asked for. The hook states no permission decision and
each asked hand-off is recorded in state/.helper-model.log.

Without the key or the listing the worker's settings file is unchanged.
…Jev, opt-in

bin/fm-intake-route.sh asks one fixed-choice request through the shared
caller which registered project a request or bug report is about and
which second mate's scope covers it. The choices are read at call time
from data/projects.md (bin/fm-project-mode.sh --list, which leaves out an
entry marked finished) and data/secondmates.md. Off without
TYPESAFE_API_KEY; advice only, printed for firstmate's own intake
resolution, and by hand on any failure or low confidence.
…tate read

bin/fm-worker-health.sh prints the bin/fm-crew-state.sh line unchanged and,
only with TYPESAFE_API_KEY present and only when that read comes from the
pane or status-log fallback for a local ship or scout, asks one fixed-choice
question through bin/fm-jev-lib.sh: working, stuck, waiting, or finished.
An answer at or above the shared confidence floor adds one advice-only
health line; every other outcome leaves the crew-state line alone.

It sends status lines, ages, and an unacknowledged-instruction count, never
terminal content, steering text, the brief, or project files. Firstmate runs
it at the start of stuck-crewmate-recovery's live-endpoint escalation;
nothing runs it automatically.
…tions

bin/fm-exists-search.sh asks one yes/no question of a project's functions
and prints a ranked file:line list, and with --change compares each function
a change adds with the existing ones closest to it. It is on only with
TYPESAFE_API_KEY and the project named in config/jev-code-projects, calls Jev
only through bin/fm-jev-lib.sh, and is advice only: every outcome exits 0.

An opted-in project's brief gains the search under "Before you write any
code"; every other brief is byte-identical. bin/fm-house-rules-check.sh's
base pick moves into house_rules_base so the search reuses it.
AGENTS.md's config/jev-code-projects entry lists every check keyed by a
firstmate project name; the helper model pick is one of them, as
docs/configuration.md "Jev code projects" already says.
…pick

The pick moves a helper to sonnet, which is a lowering only when the worker's
own model is a stronger one. A worker launched with no model named runs on the
account default, which firstmate cannot see and which may itself be sonnet or
haiku, so that launch now gets no hook and its settings file is the keyless
file. The pick can therefore never raise a helper's model.
@talktejas
talktejas merged commit cdc2e57 into main Oct 6, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant