Skip to content

feat(bin): advisory risk level beside a recorded pull request, opt-in - #42

Closed
talktejas wants to merge 5 commits into
mainfrom
fm/fm-jev-pr-risk-level
Closed

talktejas wants to merge 5 commits into
mainfrom
fm/fm-jev-pr-risk-level

Conversation

@talktejas

Copy link
Copy Markdown
Owner

Intent

A research report on using Jev (TypeSafe's decision-only model) in the coding workflow proposed ten integrations in one table. The answer on row 4: "impement"

Row 4 as it was shown:

  • What it does: Risk level on every merge request. Code collects the facts (database migrations, login and permissions, payments, deletions, size). Jev adds three yes/no answers: behaviour changed with no test, description does not match the change, something hard to undo. Result: low, medium or high beside the merge request.
  • Where it plugs in: bin/fm-pr-check.sh, the step that already records a pull request when a worker reports it ready
  • What it saves: Shows which merges need a real look. Projects without the no-mistakes review pipeline get a risk level too; today only that pipeline gives one.
  • Who else does it: hush (2★) flags pull requests as risky, untested or undescribed; its issue labels were 78% right with 19% left untouched on 800 issues. jev-pr-risk-evaluator (0★, early): "No model performs arithmetic, assigns reviewers, or chooses the final route."
  • Build size: Small to medium. Most of the level comes from file paths, which is plain code.
  • If Jev is unsure or down: Shows "risk not rated". Never lowers a level.

Stated with the table: Jev only picks from a fixed list; in every row, if Jev is unsure or down, work carries on exactly as today.

What Changed

  • bin/fm-pr-check.sh now prints one advisory risk: low|medium|high or risk: not rated line, with reasons, after it has recorded a pull request and armed its merge poll. The line is printed only when TYPESAFE_API_KEY is available, and a rating failure never changes the registration or its exit status. bin/fm-pr-merge.sh re-registers with the new --no-risk flag, so a merge rates nothing.
  • New bin/fm-pr-risk-lib.sh sets the level in code from the diff bin/fm-review-diff.sh reads: migration, login/permissions or payments paths, or 1500+ changed lines are high; a deleted file, 400+ lines or 20+ files are medium. It then asks Jev up to three yes/no questions (behaviour changed with no test, description does not match the change, something hard to undo). A confident yes can only raise the level, and any unanswered question yields risk: not rated instead of risk: low.
  • bin/fm-jev-lib.sh gains fm_jev_code_allowed, which reads a per-home config/jev-code-projects list. A project's diff and pull request text go to Jev only when the project is listed; unlisted projects are rated from the path and size facts alone. The branch also adds tests/fm-pr-risk.test.sh (wired into bin/fm-test-run.sh), a --no-risk case in tests/fm-pr-merge.test.sh, and docs in docs/configuration.md, AGENTS.md and docs/verification/jev-pr-risk.md.

Risk Assessment

✅ Low: The rating is advisory, runs only after the registration and armed: line, is off without the key, sends nothing for projects not listed in config/jev-code-projects, and the round-3 --no-risk fix makes the merge-time re-registration skip it entirely; I found no remaining reachable defect.

Testing

Drove the real bin/fm-pr-check.sh 34 times against a fixture git project and task worktree in a throwaway home, with real git, the real bin/fm-review-diff.sh, and real curl over HTTP to a local stand-in for the Jev endpoint (gh and glab were shims because no real pull request exists). Covered: key absent (output, stderr and task record identical to the base commit's script), each code fact, each Jev yes raising the level, Jev unsure/erroring/hanging/unreachable giving risk: not rated or leaving a fact-set level untouched, the per-project opt-in sending nothing for an unlisted project, --no-risk at merge time, a secondmate home's ready line, and a GitLab merge request. Also ran tests/fm-pr-risk.test.sh (all passed) and tests/fm-pr-merge.test.sh (zero failures, including the merge-time no-rating case); these are mocked tests, not live runs. Not exercised live: a real bin/fm-pr-merge.sh merge (no mergeable pull request on a real forge), and the real api.typesafe.ai model, since no TYPESAFE_API_KEY was provided to this phase. No UI surface, so evidence is a CLI transcript rather than screenshots. Worktree left clean.

  • Live validation: ✅ go - 17 of 19 scenarios driven live against the product
Scenario Result Live Evidence
Home with no TYPESAFE_API_KEY registers a PR: no risk line, and output, stderr and task record match the base commit exactly ✅ pass live transcript.txt runs 1-2: COMPARE: stdout, stderr and task meta identical to base commit; 0 requests, 0 curl starts
Key present and project listed, ordinary code change, Jev answers no three times: risk: low printed after the armed: line ✅ pass live transcript.txt run 3; three separate yes/no questions received (untested, mismatch, irreversible) carrying title, description, facts, file list and diff start. Jev was a local stand-in server.
A Jev yes only raises: untested or mismatch gives medium, irreversible gives high ✅ pass live transcript.txt runs 4-6 (stand-in Jev answering yes at 0.9)
Code facts set the level without Jev: migration, login/permissions and payments paths and 1500 lines are high; a deleted file and 400 lines are medium ✅ pass live transcript.txt runs 8-13
Awkward paths are still classified: a non-ASCII migration path that git quotes, and a .sql path containing a space, both read high ✅ pass live transcript.txt runs 14-15
A changed test file settles untested in code, so only two questions are asked ✅ pass live transcript.txt run 7: requests received 2 [mismatch irreversible]
Jev unsure (yes at 0.59, or no at 0.4, below the 0.6 floor): risk: not rated, never low ✅ pass live transcript.txt runs 16-17: unanswered: ... (Jev unsure)
Jev answers outside the fixed yes/no list: the answer is not counted ✅ pass live transcript.txt run 18: reported as Jev error, not rated, remaining questions skipped
Jev down (HTTP 500, non-JSON reply, hang past the 5 s timeout, connection refused): PR still recorded and poll armed, risk: not rated, only one call attempted ✅ pass live transcript.txt runs 19-22: exit 0, poll_armed=yes, 1 curl start each; the hang cost 6.7 s wall, i.e. one timeout not three
Never lowers: on a migration change the level stays high when Jev is unreachable and when Jev confidently answers no to everything ✅ pass live transcript.txt runs 23-24
Forge description unreadable: mismatch is not asked, the other two are, and the line is not rated ✅ pass live transcript.txt run 25: requests 2 [untested irreversible]
Key present but project not opted in (list file absent, empty, only a # alpha comment, similar names, wildcards, trailing space): nothing is sent and the forge description is not read ✅ pass live transcript.txt runs 26-29: 0 requests, 0 curl starts, 0 forge description reads, unanswered: ... (project not listed)
Unlisted project with a migration: high from facts alone with the three questions named unanswered; listing the project beside comment lines turns the full rating on ✅ pass live transcript.txt runs 30-31
Merge-time re-registration (--no-risk) with key present and project listed: no risk line, no change read, no forge description read, no Jev call ✅ pass live transcript.txt run 32: stdout is only armed: state/task-a.check.sh, 0 requests, wall 0.9 s
A bin/fm-pr-merge.sh merge with the key set prints no risk line ⏸️ untested no The prior payload did not establish a live result: this was only covered by bash tests/fm-pr-merge.test.sh (ok - fm-pr-merge re-registers without rating the pull request), which runs the real fm-p…
Secondmate home: the parent-channel ready line carries no risk token and the risk line comes after armed: ✅ pass live transcript.txt run 33: parent channel line is done [key=child-pr-task-a]: child task-a PR ready: https://github.com/o/r/pull/8
GitLab merge request on a self-hosted host gets a risk line, with the description read through glab ✅ pass live transcript.txt run 34: risk: high - database migration; glab called as mr view 5 -R https://gitlab.example.com/grp/sub/proj -F json (glab was a shim)
The key never reaches curl's argv or any child's environment; it arrives only as the Authorization header ✅ pass live transcript.txt run 3: key on curl argv: 0, curl env key vars: 0, stand-in received Bearer <the key>
The real Jev model at api.typesafe.ai gives sensible yes/no answers to the three shipped questions ⏸️ untested no No TYPESAFE_API_KEY was provided to this phase, and the endpoint is fixed in bin/fm-jev-lib.sh, so all Jev answers here came from a local stand-in. docs/verification/jev-pr-risk.md records an earlier…
Evidence: Live CLI transcript of all 34 registrations

Source: Live CLI transcript of all 34 registrations

############ A. Key absent: work carries on exactly as today

=== 1. key absent, project listed, migration change -> no risk line at all
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.2s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 0 []
  curl started: 0   forge description reads: 0

=== 2. same registration with the BASE commit's script (f6a10db)
$ /tmp/fmrisk.OTq8et/base/bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.2s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 0 []
  curl started: 0   forge description reads: 0
  COMPARE: stdout, stderr and task meta identical to base commit

############ B. Key present, project listed: full rating

=== 3. ordinary code change, Jev answers no x3 -> low
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: low - no risk fact found and all three questions answered no
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=2.2s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1
  --- state Jev was sent (first request, diff_start trimmed):
  {"title":"Tidy the parser","description":"Splits one long function.","facts":{"changed_lines":12,"files":1,"deleted_files":0,"test_files_changed":0,"code_files_changed":1,"touches_database_migration":false,"touches_login_or_permissions":false,"touches_payments":false},"files":[{"path":"src/parser.js","added":12,"removed":0,"deleted":false}],"diff_start":"diff --git a/src/parser.js b/src/parser.js\nnew file mode 100...","diff_cut_short":false}
  --- options offered per question: ["no","yes"]  auth header received: Bearer <the key>
  --- key on curl argv: 0   curl env key vars: 0

=== 4. Jev: behaviour changed with no test -> medium
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: medium - behaviour changed with no test
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=2.2s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1

=== 5. Jev: description does not match -> medium
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: medium - description does not match the change
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=2.3s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1

=== 6. Jev: something hard to undo -> high
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: high - something hard to undo
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=2.2s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1

=== 7. test file changed -> 'untested' settled in code, only 2 questions asked
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: low - no risk fact found and all three questions answered no
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.9s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 2 [mismatch irreversible]
  curl started: 2   forge description reads: 1

=== 8. fact: migration path (Jev answers no x3)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: high - database migration
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.8s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1

=== 9. fact: login/permissions path (Jev answers no x3)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: high - login and permissions
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.8s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1

=== 10. fact: payments path (Jev answers no x3)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: high - payments
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=2.3s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1

=== 11. fact: deleted file (Jev answers no x3)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: medium - 1 deleted file(s)
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=2.1s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1

=== 12. fact: 400 lines (Jev answers no x3)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  s

... [8329 bytes truncated] ...

ently says no x3 on a migration -> stays high (never lowers)
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: high - database migration
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=2.1s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1

=== 25. forge description unreadable -> mismatch unasked, not rated
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: not rated - no risk fact found; unanswered: mismatch (description unreadable)
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.8s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 2 [untested irreversible]
  curl started: 2   forge description reads: 1

############ D. Per-project opt-in: the key alone sends nothing

=== 26. no config/jev-code-projects -> nothing sent, no forge read, not rated
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: not rated - no risk fact found; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.5s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 0 []
  curl started: 0   forge description reads: 0

=== 27. empty list file -> nothing sent
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: not rated - no risk fact found; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.6s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 0 []
  curl started: 0   forge description reads: 0

=== 28. only '# alpha' comment and similar names -> nothing sent
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: not rated - no risk fact found; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.8s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 0 []
  curl started: 0   forge description reads: 0

=== 29. wildcards / trailing-space name do not match -> nothing sent
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: not rated - no risk fact found; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.7s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 0 []
  curl started: 0   forge description reads: 0

=== 30. unlisted project with a migration -> high from facts, questions named unanswered
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: high - database migration; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed)
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=2.0s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 0 []
  curl started: 0   forge description reads: 0

=== 31. listed beside comment lines -> asked and rated
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: high - database migration
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=2.2s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1

############ E. Merge-time re-registration never rates

=== 32. listed project, key present, --no-risk -> registration only
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 --no-risk
  stdout| armed: state/task-a.check.sh
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.2s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 0 []
  curl started: 0   forge description reads: 0

############ F. Secondmate home: ready line unchanged, risk printed after armed

=== 33. secondmate home, migration change
$ bin/fm-pr-check.sh task-a https://github.com/o/r/pull/8 
  stdout| armed: state/task-a.check.sh
  stdout| risk: high - database migration
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=2.3s  recorded=pr=https://github.com/o/r/pull/8  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1
  parent channel line: done [key=child-pr-task-a]: child task-a PR ready: https://github.com/o/r/pull/8

############ G. GitLab merge request

=== 34. GitLab MR on a self-hosted host, description read through glab
$ bin/fm-pr-check.sh task-a https://gitlab.example.com/grp/sub/proj/-/merge_requests/5 
  stdout| armed: state/task-a.check.sh
  stdout| risk: high - database migration
  stderr| WARNING: watcher still down (same stale episode; last beat: never, grace 999999s) - full banner already printed this episode.
  stderr| fm-contributions: data directory unavailable
  stderr| contributions: observation not armed; coverage is unconfirmed
  exit=0  wall=1.9s  recorded=pr=https://gitlab.example.com/grp/sub/proj/-/merge_requests/5  poll_armed=yes
  requests received by Jev stand-in: 3 [untested mismatch irreversible]
  curl started: 3   forge description reads: 1
  glab mr view 5 -R https://gitlab.example.com/grp/sub/proj -F json
Evidence: Driver script that produced the transcript

Source: Driver script that produced the transcript

#!/usr/bin/env bash
# Drives the real bin/fm-pr-check.sh in an isolated home against a fixture
# project/worktree. Real git, real bin/fm-review-diff.sh, real curl over HTTP to
# a local stand-in for api.typesafe.ai (jev-standin.py); gh/glab are shims
# because no real pull request exists.
set -u
ROOT=$(pwd); E=$(cd "$(dirname "$0")" && pwd)
T=$(mktemp -d /tmp/fmrisk.XXXXXX); H=$T/home; S=$T/shim; J=$T/jev
mkdir -p "$H/state" "$H/config" "$S" "$J"
KEY='drive-key-51aa-not-on-argv'
URL='https://github.com/o/r/pull/8'
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@example.invalid
unset TYPESAFE_API_KEY FM_TASK_ID

echo '{}' > "$J/mode.json"; : > "$J/requests.jsonl"
python3 "$E/jev-standin.py" "$J" & SRV=$!
trap 'kill $SRV 2>/dev/null; rm -rf "$T"' EXIT
for _ in $(seq 50); do [ -s "$J/port" ] && break; sleep 0.1; done
PORT=$(cat "$J/port")

cat > "$S/curl" <<EOF
#!/usr/bin/env bash
{ printf 'curl argv: %s\n' "\$*"; env | grep -c '^TYPESAFE_API_KEY' | sed 's/^/curl env key vars: /'; } >> "$T/children.log"
args=(); for a; do args+=("\${a/https:\/\/api.typesafe.ai/http://127.0.0.1:\$(cat "$J/port.use")}"); done
exec /usr/bin/curl "\${args[@]}"
EOF
cat > "$S/gh" <<EOF
#!/usr/bin/env bash
printf 'gh %s\n' "\$*" >> "$T/children.log"
case "\$*" in *title,body*) [ -s "$T/pr.json" ] && cat "$T/pr.json" || exit 1 ;; *) exit 1 ;; esac
EOF
cat > "$S/glab" <<EOF
#!/usr/bin/env bash
printf 'glab %s\n' "\$*" >> "$T/children.log"
case "\$*" in *"mr view"*) [ -s "$T/mr.json" ] && cat "$T/mr.json" || exit 1 ;; *) exit 1 ;; esac
EOF
chmod +x "$S"/*
export PATH="$S:$PATH" FM_HOME="$H" FM_STATE_OVERRIDE="$H/state" FM_GUARD_GRACE=999999

P=$T/alpha; WT=$T/wt
git init -q -b main "$P"; mkdir -p "$P/src"; echo '# alpha' > "$P/README.md"; seq 1 4 > "$P/src/old.js"
git -C "$P" add -A; git -C "$P" commit -qm initial
git -C "$P" worktree add -q -b fm/task-a "$WT"

change() { # <path>:<lines>|<path>:deleted ...
  git -C "$WT" reset -q --hard main
  for spec; do p=${spec%:*} n=${spec##*:}
    if [ "$n" = deleted ]; then git -C "$WT" rm -q -- "$p"; else mkdir -p "$WT/$(dirname "$p")"; seq 1 "$n" > "$WT/$p"; git -C "$WT" add -- "$p"; fi
  done; git -C "$WT" commit -qm change
}
mode() { printf '%s' "$1" > "$J/mode.json"; }
up() { echo "$PORT" > "$J/port.use"; }
down() { echo 1 > "$J/port.use"; }   # port 1: connection refused
key() { if [ "$1" = on ]; then printf 'TYPESAFE_API_KEY=%s\n' "$KEY" > "$H/.env"; else rm -f "$H/.env"; fi; }
list() { if [ "$1" = none ]; then rm -f "$H/config/jev-code-projects"; else printf "$1" > "$H/config/jev-code-projects"; fi; }
SC=0
run() { # <title> [script] [url] [extra args...]
  local title=$1 script=${2:-$ROOT/bin/fm-pr-check.sh} url=${3:-$URL}; shift; shift 2>/dev/null; shift 2>/dev/null
  SC=$((SC+1))
  printf 'window=fm-task-a\nkind=ship\nworktree=%s\nproject=%s\n' "$WT" "$P" > "$H/state/task-a.meta"
  rm -f "$H/state/task-a.pr-poll" "$H/state/task-a.check.sh"; : > "$J/requests.jsonl"; : > "$T/children.log"
  local t0=$(date +%s.%N) rc=0
  OUT=$("$script" task-a "$url" "$@" 2>"$T/err") || rc=$?
  local dt=$(echo "$(date +%s.%N) - $t0" | bc)
  printf '\n=== %d. %s\n' "$SC" "$title"
  printf '$ %s task-a %s %s\n' "${script#$ROOT/}" "$url" "$*"
  printf '%s\n' "$OUT" | sed 's/^/  stdout| /'
  [ ! -s "$T/err" ] || sed 's/^/  stderr| /' "$T/err"
  printf '  exit=%s  wall=%.1fs  recorded=%s  poll_armed=%s\n' "$rc" "$dt" \
    "$(grep '^pr=' "$H/state/task-a.meta" || echo none)" "$([ -e "$H/state/task-a.pr-poll" ] && echo yes || echo no)"
  printf '  requests received by Jev stand-in: %s [%s]\n' "$(wc -l < "$J/requests.jsonl")" "$(jq -r .question "$J/requests.jsonl" | paste -sd' ')"
  printf '  curl started: %s   forge description reads: %s\n' "$(grep -c '^curl argv' "$T/children.log")" "$(grep -c 'title,body\|mr view' "$T/children.log")"
}

echo '{"title":"Tidy the parser","body":"Splits one long function."}' > "$T/pr.json"
up; mode '{}'

echo "############ A. Key absent: work carries on exactly as today"
key off; list 'alpha\n'; change db/migrations/0042_drop.sql:3 src/parser.js:12
# warm-up so the once-per-episode watcher banner is not what differs between the two runs
printf 'window=fm-task-a\nkind=ship\nworktree=%s\nproject=%s\n' "$WT" "$P" > "$H/state/task-a.meta"; "$ROOT/bin/fm-pr-check.sh" task-a "$URL" >/dev/null 2>&1
run "key absent, project listed, migration change -> no risk line at all"
A_OUT=$OUT; A_ERR=$(cat "$T/err"); A_META=$(cat "$H/state/task-a.meta")
mkdir -p "$T/base"; git -C "$ROOT" archive f6a10db bin | tar -x -C "$T/base"
run "same registration with the BASE commit's script (f6a10db)" "$T/base/bin/fm-pr-check.sh"
[ "$A_OUT|$A_ERR|$A_META" = "$OUT|$(cat "$T/err")|$(cat "$H/state/task-a.meta")" ] \
  && echo "  COMPARE: stdout, stderr and task meta identical to base commit" || echo "  COMPARE: DIFFERS from base commit"

echo; echo "############ B. Key present, project listed: full rating"
key on
change src/parser.js:12
run "ordinary code change, Jev answers no x3 -> low"
echo "  --- state Jev was sent (first request, diff_start trimmed):"
jq -c 'select(.question=="untested") | .body.state.change | .diff_start |= (.[0:60] + "...")' "$J/requests.jsonl" | sed 's/^/  /'
echo "  --- options offered per question: $(jq -c '.body.questions[] | (.criteria | keys)' "$J/requests.jsonl" | sort -u | paste -sd' ')  auth header received: $(jq -r .auth "$J/requests.jsonl" | sort -u | sed "s/$KEY/<the key>/")"
echo "  --- key on curl argv: $(grep -c "$KEY" "$T/children.log")   $(grep 'env key vars' "$T/children.log" | sort -u)"
mode '{"untested":["yes",0.9]}';     run "Jev: behaviour changed with no test -> medium"
mode '{"mismatch":["yes",0.9]}';     run "Jev: description does not match -> medium"
mode '{"irreversible":["yes",0.9]}'; run "Jev: something hard to undo -> high"
mode '{}'
change src/parser.js:12 tests/parser.test.js:5; run "test file changed -> 'untested' settled in code, only 2 questions asked"
for c in 'db/migrations/0042_drop.sql:3|migration path' 'app/AuthController.php:3|login/permissions path' 'src/billing/invoice.ts:3|payments path' 'src/old.js:deleted|deleted file' 'src/big.js:400|400 lines' 'src/big.js:1500|1500 lines' 'db/migrations/café.sql:3|non-ASCII (git-quoted) migration path' 'my schema.sql:3|path with a space'; do
  change "${c%%|*}"; run "fact: ${c##*|} (Jev answers no x3)"
done

echo; echo "############ C. Jev unsure or down: 'not rated', never lowers"
change src/parser.js:12
mode '{"irreversible":["yes",0.59]}'; run "yes below the 0.6 floor -> not counted, not rated (never low)"
mode '{"mismatch":["no",0.4]}';       run "no below the floor -> not rated (never low)"
mode '{"untested":["maybe",0.9]}';    run "answer outside the fixed yes/no list -> not counted"
mode '{"default":"http500"}';         run "Jev returns HTTP 500 -> not rated, registration intact, one call only"
mode '{"default":"garbage"}';         run "Jev returns non-JSON -> not rated"
mode '{"default":"hang"}';            run "Jev hangs past the 5s timeout -> not rated after ONE timeout"
mode '{}'; down;                      run "Jev unreachable (connection refused) -> not rated"
change db/migrations/0042_drop.sql:3; run "Jev unreachable on a migration -> stays high"
up; mode '{"default":["no",0.99]}';   run "Jev confidently says no x3 on a migration -> stays high (never lowers)"
mode '{}'; change src/parser.js:12; : > "$T/pr.json"; run "forge description unreadable -> mismatch unasked, not rated"
echo '{"title":"Tidy the parser","body":"Splits one long function."}' > "$T/pr.json"

echo; echo "############ D. Per-project opt-in: the key alone sends nothing"
mode '{"default":["yes",0.99]}'
list none;                       run "no config/jev-code-projects -> nothing sent, no forge read, not rated"
list '';                         run "empty list file -> nothing sent"
list '# alpha\nbeta\nalpha-two\nalph\n'; run "only '# alpha' comment and similar names -> nothing sent"
list '*\n.*\nalpha \n';          run "wildcards / trailing-space name do not match -> nothing sent"
change db/migrations/0042_drop.sql:3; run "unlisted project with a migration -> high from facts, questions named unanswered"
list '# may be sent\nbeta\n\nalpha\n'; mode '{}'; run "listed beside comment lines -> asked and rated"

echo; echo "############ E. Merge-time re-registration never rates"
run "listed project, key present, --no-risk -> registration only" "$ROOT/bin/fm-pr-check.sh" "$URL" --no-risk

echo; echo "############ F. Secondmate home: ready line unchanged, risk printed after armed"
printf 'mate-x\n' > "$H/.fm-secondmate-home"; printf 'schema=fm-secondmate-parent.v1\nroute=remote\n' > "$H/.fm-secondmate-parent"
rm -f "$H/state/parent-replies.status"
run "secondmate home, migration change"
echo "  parent channel line: $(cat "$H/state/parent-replies.status" 2>&1)"
rm -f "$H/.fm-secondmate-home" "$H/.fm-secondmate-parent"

echo; echo "############ G. GitLab merge request"
echo '{"title":"Drop legacy column","description":"Removes users.legacy via migration."}' > "$T/mr.json"
run "GitLab MR on a self-hosted host, description read through glab" "$ROOT/bin/fm-pr-check.sh" "https://gitlab.example.com/grp/sub/proj/-/merge_requests/5"
grep '^glab' "$T/children.log" | sed 's/^/  /'
Evidence: Local stand-in for the Jev endpoint

Source: Local stand-in for the Jev endpoint

#!/usr/bin/env python3
"""Local stand-in for POST /v1/systemone. Answers from a mode file; logs every request."""
import json, sys, time
from http.server import BaseHTTPRequestHandler, HTTPServer
D = sys.argv[1]
class H(BaseHTTPRequestHandler):
    def log_message(self, *a): pass
    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
        key = list(body['questions'])[0]
        with open(D + '/requests.jsonl', 'a') as f:
            f.write(json.dumps({'path': self.path, 'auth': self.headers.get('Authorization'), 'question': key, 'body': body}) + '\n')
        mode = json.load(open(D + '/mode.json'))
        m = mode.get(key, mode.get('default', ['no', 0.95]))
        if m == 'hang': time.sleep(8); return
        if m == 'http500': self.send_response(500); self.end_headers(); self.wfile.write(b'boom'); return
        if m == 'garbage': out = b'<html>not json'
        else:
            choice, conf = m
            other = 'no' if choice == 'yes' else 'yes'
            probs = {choice: conf, other: round(1 - conf, 4)} if choice in ('yes', 'no') else {choice: conf, 'yes': round(1 - conf, 4)}
            out = json.dumps({'model': 'jev-standin', 'answers': {key: {'choice': choice, 'confidence': conf, 'probabilities': probs}}}).encode()
        self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers(); self.wfile.write(out)
s = HTTPServer(('127.0.0.1', 0), H)
open(D + '/port', 'w').write(str(s.server_port))
s.serve_forever()
Evidence: Representative lines printed beside the registration
key absent: armed: state/task-a.check.sh (no risk line; identical to base commit)
listed, 3x no: risk: low - no risk fact found and all three questions answered no
Jev yes (untested): risk: medium - behaviour changed with no test
Jev yes (irreversible): risk: high - something hard to undo
migration path: risk: high - database migration
Jev unsure: risk: not rated - no risk fact found; unanswered: irreversible (Jev unsure)
Jev down: risk: not rated - no risk fact found; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
Jev down + migration: risk: high - database migration; unanswered: untested (Jev error), mismatch (Jev error), irreversible (Jev error)
project not listed: risk: not rated - no risk fact found; unanswered: untested (project not listed), mismatch (project not listed), irreversible (project not listed) [0 requests sent]
--no-risk (merge): armed: state/task-a.check.sh (no risk line; 0 requests)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 6 issues found → auto-fixed (3) ✅
  • ⚠️ bin/fm-pr-check.sh:171 - Unrequired component with two side effects: the risk=&lt;level&gt; token appended to a secondmate's parent-channel ready line (lines 171, 177-181). The intent asks only for the level "beside the merge request" in bin/fm-pr-check.sh; nothing requires carrying it to the parent. To support it, the rating now runs BEFORE fm_parent_channel_report and before armed: is printed, so (1) the ready line and the armed: confirmation wait behind up to 60s diff read + 20s forge read + 3x5s Jev calls, and a kill in that window leaves an armed poll with no ready line published, which is not "work carries on exactly as today"; (2) fm_parent_channel_append_once dedupes on the exact line, so a re-registration whose rating differs (first run Jev timed out -> no token, second run -> risk=high; or the PR changed between runs) appends a second done [key=child-pr-&lt;id&gt;] line, breaking the once-only invariant tests/fm-teardown.test.sh asserts ("re-registration duplicated the ready line"). Smallest honest remedy: remove the token and call fm_pr_risk after the armed: line is printed.
  • ⚠️ tests/fm-teardown.test.sh:2032 - The main-pr-ready case still runs the real fm-pr-check.sh with FM_ROOT_OVERRIDE=&#34;$ROOT&#34; and no FM_HOME, so FM_HOME defaults to the developer's checkout and fm_jev_key_load reads $ROOT/.env. The change fixed the three identical sites at 1209/1217/1246 but missed this one. On a checkout that is a live home with TYPESAFE_API_KEY in its gitignored .env, this test reads the fixture diff and makes real curl calls to api.typesafe.ai with the real key (no fake curl in that fakebin), adding network dependence and latency. Add FM_HOME=&#34;$case_dir/home&#34; as at the other sites.
  • ⚠️ bin/fm-pr-risk-lib.sh:172 - Policy decision needed: with this change any home that already holds TYPESAFE_API_KEY (set for typed dispatch resolution or wake triage) starts sending each registered PR's title, description, file list and the first 30000 bytes of its source diff to api.typesafe.ai, for every project, with no separate switch. The existing callers under the same key send only routing text/status lines (docs: "sends no brief, pane content, or quota"). The intent implies Jev must see the change, but neither it nor repository instructions say that the existing key is consent to upload project source (including private client repos and anything secret-looking in a diff). Confirm that the shared key is the intended opt-in, or name the gate you want.
  • ℹ️ bin/fm-pr-risk-lib.sh:230 - Intent conformance check. Criterion: "If Jev is unsure or down: Shows &test: wait on each stall checkpoint's own effect in wake-queue tests #34;risk not rated&test: wait on each stall checkpoint's own effect in wake-queue tests #34;. Never lowers a level." The change prints risk: not rated only when no fact raised the level; otherwise it prints the code-set level with an ; unanswered: ... suffix (lines 230-236), e.g. risk: medium - 1 deleted file(s); unanswered: irreversible (Jev error) where an answered irreversible could have made it high. This reads as a reasonable interpretation of "never lowers a level", but it is not the literal criterion, so confirm it. Related: with the key absent no line is printed at all, although the intent says projects without the no-mistakes pipeline "get a risk level too" and the fact-based level needs no model.
  • ℹ️ bin/fm-pr-risk-lib.sh:84 - _fm_pr_risk_facts only recognises headers matching ^diff --git a/. Git quotes paths containing non-ASCII bytes, quotes or backslashes by default (diff --git &#34;a/db/migrations/caf\303\251.sql&#34; ...), so such a file is never classified (a migration, auth or deleted file goes unseen), its ---/+++ headers and hunk lines are counted against the previous file, and a change made only of such files reads as the change could not be read. The level is silently too low for the mixed case. Accept an optional leading quote in the header/+++ patterns (or have the diff read use -c core.quotepath=false).
  • ⚠️ bin/fm-pr-risk-lib.sh:57 - Unrequired component: the FM_REVIEW_DIFF_BIN environment override. No intent requirement needs a configurable diff reader; it exists as a test seam (and for the synthetic live run in docs/verification/jev-pr-risk.md) yet ships as a production knob that lets the environment substitute the executable whose output sets the level. Recommend removing it and having the tests drive the real bin/fm-review-diff.sh against a fixture worktree, as tests/fm-teardown.test.sh already does.

🔧 Fix applied.
1 warning still open:

  • ⚠️ bin/fm-pr-risk-lib.sh:95 - For a path containing a space, git ends the +++ b/&lt;path&gt; line with a tab, and _fm_pr_risk_facts keeps that tab in path, so every $-anchored pattern misses. Confirmed by running the parser on a real git diff: my schema.sql gives migration=0, and src/my parser.test.js gives tests=0 code=2. Effect: a root-level .sql or schema.rb/schema.prisma file with a space in its name is not rated high, and a test file with a space is counted as code, so untested is asked (or left unanswered) instead of being settled in code. The level is silently too low, and the tab-suffixed path is also what is sent to Jev. Fix: strip the trailing tab when taking the path from the +++ line, e.g. add sub(/\t$/, &#34;&#34;, path) before the closing-quote strip.

🔧 Fix applied.
3 issues (1 warning, 2 infos) still open:

  • ⚠️ bin/fm-pr-check.sh:184 - bin/fm-pr-merge.sh calls fm-pr-check.sh on every merge (record_pr_metadata, fm-pr-merge.sh:862, invoked at :1131) while it holds the task's .control-&lt;id&gt;.lock and before the forge merge command. Because the rating now runs unconditionally at the end of fm-pr-check.sh, a home with TYPESAFE_API_KEY re-rates the pull request inside every merge: a second fm-review-diff.sh run (fetches, up to 60 s), and for a listed project a second forge read (20 s) and up to three more Jev calls that re-send the title, description, file list and diff excerpt. The merge is delayed by that much with the control lock held, and a risk: line is printed into the merge output after the merge has already been authorized. The intent places the level at "the step that already records a pull request when a worker reports it ready"; nothing requires a rating at merge time, and the docs' cost statement ("adds at most one change read ... to a registration") does not mention it. Decide whether the merge-time re-registration should skip the rating; the smallest remedy needs a way for the merge caller to say so (or moving the fm_pr_risk call out of the path fm-pr-merge.sh uses), which is a behaviour choice rather than a mechanical fix.
  • ℹ️ bin/fm-pr-check.sh:184 - The opt-in is matched on the last path component of the task's project= (${PR_PROJECT##*/}), so two projects in different parent directories that share a directory name are opted in together by one line in config/jev-code-projects. This is documented ("the directory name of the task's project= metadata") and matches the supervisor's instruction to list project names, so no change is requested; noted only because the consequence of a collision is an unlisted repository's diff being sent.
  • ℹ️ bin/fm-pr-risk-lib.sh:90 - A file renamed with no content change has no +++ b/ line, so _fm_pr_risk_facts keeps the old path taken from the diff --git a/... header. Moving an unchanged file into e.g. db/migrations/ or auth/ is classified by where it came from, not where it lands. Rare and fail-soft (the other facts and questions still apply); reading the rename to line would close it.

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

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 17 of 19 scenarios driven live against the product
Scenario Result Live Evidence
Home with no TYPESAFE_API_KEY registers a PR: no risk line, and output, stderr and task record match the base commit exactly ✅ pass live transcript.txt runs 1-2: COMPARE: stdout, stderr and task meta identical to base commit; 0 requests, 0 curl starts
Key present and project listed, ordinary code change, Jev answers no three times: risk: low printed after the armed: line ✅ pass live transcript.txt run 3; three separate yes/no questions received (untested, mismatch, irreversible) carrying title, description, facts, file list and diff start. Jev was a local stand-in server.
A Jev yes only raises: untested or mismatch gives medium, irreversible gives high ✅ pass live transcript.txt runs 4-6 (stand-in Jev answering yes at 0.9)
Code facts set the level without Jev: migration, login/permissions and payments paths and 1500 lines are high; a deleted file and 400 lines are medium ✅ pass live transcript.txt runs 8-13
Awkward paths are still classified: a non-ASCII migration path that git quotes, and a .sql path containing a space, both read high ✅ pass live transcript.txt runs 14-15
A changed test file settles untested in code, so only two questions are asked ✅ pass live transcript.txt run 7: requests received 2 [mismatch irreversible]
Jev unsure (yes at 0.59, or no at 0.4, below the 0.6 floor): risk: not rated, never low ✅ pass live transcript.txt runs 16-17: unanswered: ... (Jev unsure)
Jev answers outside the fixed yes/no list: the answer is not counted ✅ pass live transcript.txt run 18: reported as Jev error, not rated, remaining questions skipped
Jev down (HTTP 500, non-JSON reply, hang past the 5 s timeout, connection refused): PR still recorded and poll armed, risk: not rated, only one call attempted ✅ pass live transcript.txt runs 19-22: exit 0, poll_armed=yes, 1 curl start each; the hang cost 6.7 s wall, i.e. one timeout not three
Never lowers: on a migration change the level stays high when Jev is unreachable and when Jev confidently answers no to everything ✅ pass live transcript.txt runs 23-24
Forge description unreadable: mismatch is not asked, the other two are, and the line is not rated ✅ pass live transcript.txt run 25: requests 2 [untested irreversible]
Key present but project not opted in (list file absent, empty, only a # alpha comment, similar names, wildcards, trailing space): nothing is sent and the forge description is not read ✅ pass live transcript.txt runs 26-29: 0 requests, 0 curl starts, 0 forge description reads, unanswered: ... (project not listed)
Unlisted project with a migration: high from facts alone with the three questions named unanswered; listing the project beside comment lines turns the full rating on ✅ pass live transcript.txt runs 30-31
Merge-time re-registration (--no-risk) with key present and project listed: no risk line, no change read, no forge description read, no Jev call ✅ pass live transcript.txt run 32: stdout is only armed: state/task-a.check.sh, 0 requests, wall 0.9 s
A bin/fm-pr-merge.sh merge with the key set prints no risk line ⏸️ untested no The prior payload did not establish a live result: this was only covered by bash tests/fm-pr-merge.test.sh (ok - fm-pr-merge re-registers without rating the pull request), which runs the real fm-p…
Secondmate home: the parent-channel ready line carries no risk token and the risk line comes after armed: ✅ pass live transcript.txt run 33: parent channel line is done [key=child-pr-task-a]: child task-a PR ready: https://github.com/o/r/pull/8
GitLab merge request on a self-hosted host gets a risk line, with the description read through glab ✅ pass live transcript.txt run 34: risk: high - database migration; glab called as mr view 5 -R https://gitlab.example.com/grp/sub/proj -F json (glab was a shim)
The key never reaches curl's argv or any child's environment; it arrives only as the Authorization header ✅ pass live transcript.txt run 3: key on curl argv: 0, curl env key vars: 0, stand-in received Bearer &lt;the key&gt;
The real Jev model at api.typesafe.ai gives sensible yes/no answers to the three shipped questions ⏸️ untested no No TYPESAFE_API_KEY was provided to this phase, and the endpoint is fixed in bin/fm-jev-lib.sh, so all Jev answers here came from a local stand-in. docs/verification/jev-pr-risk.md records an earlier…
  • bash ~/.no-mistakes/evidence/01M46JQAD2T0BB7FNC9WPS14PQ/drive.sh — 34 live runs of bin/fm-pr-check.sh task-a &lt;url&gt; in an isolated FM_HOME, output in transcript.txt
  • Base comparison: git archive f6a10db bin extracted to a temp dir and its fm-pr-check.sh run on the same keyless registration; stdout, stderr and task meta compared identical
  • bin/fm-pr-check.sh task-a &lt;url&gt; --no-risk with key present and project listed: zero requests, zero curl starts, zero forge description reads
  • bash tests/fm-pr-risk.test.sh — all four groups ok, # all fm-pr-risk tests passed
  • bash tests/fm-pr-merge.test.sh — 0 not ok lines; ok - fm-pr-merge re-registers without rating the pull request (mocked forge, not live)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

bin/fm-pr-check.sh prints one risk line after arming the merge poll when
TYPESAFE_API_KEY is present. Code sets the level from paths, deletions and
size; Jev answers up to three yes/no questions through bin/fm-jev-lib.sh and
may only raise it. An unanswered question never yields low.
@talktejas

Copy link
Copy Markdown
Owner Author

Superseded by #45, which carries the same work on a fresh branch cut from current main.

@talktejas talktejas closed this Oct 5, 2026
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