Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e9d8cc2
Handle Lavish list-form feedback
BohnBawerick Sep 29, 2026
9147025
no-mistakes(review): Parse table rows beyond declared counts
BohnBawerick Sep 29, 2026
c1e01b4
no-mistakes(document): Document Lavish list-form feedback parsing
BohnBawerick Sep 29, 2026
e66a604
no-mistakes(ci): Updated the malformed-capture regression to require …
BohnBawerick Sep 29, 2026
2a37594
Preserve Lavish UTF-8 feedback
BohnBawerick Sep 29, 2026
8a87193
no-mistakes(document): Document Lavish result decoding guarantees
BohnBawerick Sep 29, 2026
c25c34e
Pass supported Codex max effort to workers
BohnBawerick Oct 2, 2026
4ea5d84
no-mistakes(review): Reject malformed Lavish lists and isolate Codex …
BohnBawerick Oct 2, 2026
10d8297
no-mistakes(document): Align Codex and Lavish documentation
BohnBawerick Oct 2, 2026
48b50ae
no-mistakes(document): Document catalog-based Codex max effort
BohnBawerick Oct 2, 2026
61985d8
fix(pi): support Pi 1.0 rendering contracts
BohnBawerick Oct 2, 2026
4299989
Validate Codex max effort from catalog
BohnBawerick Oct 2, 2026
25ab3ef
no-mistakes(review): Preserve Lavish metadata and harden Codex catalo…
BohnBawerick Oct 2, 2026
5298637
no-mistakes(review): Relay Codex max downgrade warnings through recovery
BohnBawerick Oct 2, 2026
112c121
no-mistakes(document): Document catalog validation and feedback metadata
BohnBawerick Oct 2, 2026
ca58502
no-mistakes(review): Relay Codex max warnings through secondmate rest…
BohnBawerick Oct 3, 2026
fce2fe2
no-mistakes(review): Reject truncated Lavish list items as incomplete
BohnBawerick Oct 3, 2026
5bf3d87
no-mistakes(document): Document Codex catalog helper
BohnBawerick Oct 3, 2026
68b48e7
no-mistakes(review): Validate Codex catalog schema before enabling max
BohnBawerick Oct 3, 2026
d393b4c
no-mistakes(document): Document Codex catalog validation and fallback
BohnBawerick Oct 3, 2026
72dbec0
no-mistakes(ci): Fixed both CI failures. Updated the Lavish text-rang…
BohnBawerick Oct 3, 2026
536aa1b
fix(bin): stop cap-cut forge reads recording observation unavailable
BohnBawerick Oct 3, 2026
a873559
no-mistakes(review): Harden pending-reply hash fallback under nounset
BohnBawerick Oct 3, 2026
1726136
no-mistakes(ci): Fixed all three CI failures. Updated the Calm export…
BohnBawerick Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Verified on 2026-06-11 with codex-cli 0.139.0 unless a fact gives a newer versio
| Skill invocation | `$<skill>`, for example `$no-mistakes`; `/<skill>` is Claude-only and Codex rejects it as "Unrecognized command". |
| Resume | `codex resume <session-id>`, using the id printed on quit. |
| Model flag | `--model <model>`. |
| Effort flag | `-c 'model_reasoning_effort="<low\|medium\|high\|xhigh\|max>"'`, verified on codex-cli 0.142.1 whose installed schema contains `model_reasoning_effort`, active config uses it, and bundled catalog advertised only the first four values while omitting `max`; current codex-cli 0.153.4 catalog data at `${CODEX_HOME:-~/.codex}/models_cache.json` advertises `max` for `gpt-5.6-luna`, which Firstmate passes for that model. |
| Effort flag | `-c 'model_reasoning_effort="<low\|medium\|high\|xhigh\|max>"'`; the [configuration guide](../../../../../docs/configuration.md#crew-dispatch-profiles-configcrew-dispatchjson) owns Firstmate's catalog-gated `max` validation and launch fallback. |
| Model discovery | Open the current interactive session's `/model` picker. |
| Marker | None; identity comes from ancestry, and `../../../bin/fm-harness.sh` is what keeps a retained foreign `CLAUDECODE` from renaming it. Verified on 2026-09-01 with codex-cli 0.152.0: the pane process is the `node` npm shim and the native `codex` binary runs as its foreground child, so a tool subprocess reaches the native name directly while the shim itself is identified from its script path. |

Expand Down
3 changes: 2 additions & 1 deletion .agents/skills/process-event-sources/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ Two rules the commands cannot enforce for you:
This call is atomically deduplicated by the exact source and sequence: it prints `handled: <id> <seq>` only the first time and `already-handled: <id> <seq>` on every repeat, so a paired effect gated on that distinction is never authorized twice. Reading the event line or the result file is not handling - only this call durably retires the wake, so call it every time, including on a repeat wake for a sequence you already acted on.
: Ask the adapter what the result means rather than parsing it yourself.
`bin/fm-procevent.sh classify <result-file>` routes through the immutable built-in or extension identity captured with that result; for Lavish, its existing direct command returns `feedback`, `ended`, `waiting`, `disconnected`, `missing`, or `unknown`.
Consume a Lavish capture with `bin/fm-procevent-lavish.sh read <result-file>` rather than grepping the raw file: that command reports declared and presented item counts plus a completeness verdict, enumerates every captured queued item while retaining supplied element identity, and surfaces a `tag=message` freeform message as its own field, labeling it as session-ending only when the session ended.
Consume a Lavish capture with `bin/fm-procevent-lavish.sh read <result-file>` rather than grepping the raw file: that command reports declared and presented item counts plus a completeness verdict, enumerates every captured queued item while retaining supplied element, target, and attachment metadata, and surfaces a `tag=message` freeform message as its own field, labeling it as session-ending only when the session ended.
A count mismatch or malformed item makes `read` report an incomplete result and exit nonzero, so leave that capture unacknowledged.
`answers` remains the keyed-choice extractor and never treats freeform prose as a decision key.
A `feedback` result can still be the last one a review ever produces, so never assume another wake is coming just because the state is not `ended`.
The crew-hosted recovery ordering and arm-and-acknowledge rule are owned by the [crew-hosted Lavish board contract](../../../docs/configuration.md#crew-hosted-lavish-review-boards); `bin/fm-brief.sh` emits its instruction at the point of use.
Expand Down
25 changes: 20 additions & 5 deletions .pi/extensions/fm-branch-supervision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2097,12 +2097,14 @@ ${context.command}
};

let stockOutcomesPreviewLines: number | null | undefined;
const getStockOutcomesPreviewLines = (): number | undefined => {
if (stockOutcomesPreviewLines !== undefined) return stockOutcomesPreviewLines ?? undefined;
let stockOutcomesCallShowsArgs: boolean | undefined;
const probeStockOutcomesRendering = (): void => {
if (stockOutcomesPreviewLines !== undefined && stockOutcomesCallShowsArgs !== undefined) return;
const probeTokens = Array.from(
{ length: 64 },
(_, index) => `FM_OUTCOMES_PREVIEW_PROBE_${String(index).padStart(2, "0")}`,
);
const callArgProbe = "FM_OUTCOMES_CALL_ARGS_PROBE";
try {
const probeDefinition: ToolDefinition = {
name: "fm_outcomes_preview_probe",
Expand All @@ -2114,7 +2116,7 @@ ${context.command}
const probe = new ToolExecutionComponent(
probeDefinition.name,
"fm-outcomes-preview-probe",
{},
{ probe: callArgProbe },
{ showImages: false },
probeDefinition,
{ requestRender() {} } as ConstructorParameters<typeof ToolExecutionComponent>[5],
Expand All @@ -2127,9 +2129,14 @@ ${context.command}
const rendered = probe.render(4096).join("\n");
const visibleLines = probeTokens.filter((token) => rendered.includes(token)).length;
stockOutcomesPreviewLines = visibleLines > 0 && visibleLines < probeTokens.length ? visibleLines : null;
stockOutcomesCallShowsArgs = rendered.includes(callArgProbe);
} catch {
stockOutcomesPreviewLines = null;
stockOutcomesCallShowsArgs = false;
}
};
const getStockOutcomesPreviewLines = (): number | undefined => {
probeStockOutcomesRendering();
return stockOutcomesPreviewLines ?? undefined;
};

Expand Down Expand Up @@ -2167,11 +2174,19 @@ ${context.command}
recent: Type.Optional(Type.Number({ description: "How many most-recent outcomes to read (default 20)" })),
}),
renderShell: "self",
renderCall: (_args, theme, context) => {
renderCall: (args, theme, context) => {
if (calmPresentation.stockExportRendering) throw new Error("Use Pi stock export rendering");
if (calmHides("assistant-tool-call")) return new Container();
const shellState = context.state as OutcomesToolShellState;
shellState.call = new Text(theme.fg("toolTitle", theme.bold("fm_branch_outcomes")), 0, 0);
probeStockOutcomesRendering();
let call = theme.fg("toolTitle", theme.bold("fm_branch_outcomes"));
const recent = (args as { recent?: unknown }).recent;
if (stockOutcomesCallShowsArgs && recent !== undefined) {
call += context.expanded
? `\n${theme.fg("muted", ` recent: ${JSON.stringify(recent)}`)}`
: ` ${theme.fg("muted", `recent=${JSON.stringify(recent)}`)}`;
}
shellState.call = new Text(call, 0, 0);
return refreshOutcomesToolShell(shellState, theme, context);
},
renderResult: (result, options, theme, context) => {
Expand Down
15 changes: 13 additions & 2 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"
. "$SCRIPT_DIR/fm-control-lib.sh"
# shellcheck source=bin/fm-env-lib.sh disable=SC1091
. "$SCRIPT_DIR/fm-env-lib.sh"
# shellcheck source=bin/fm-codex-catalog-lib.sh disable=SC1091
. "$SCRIPT_DIR/fm-codex-catalog-lib.sh"
# shellcheck source=bin/fm-tangle-lib.sh disable=SC1091
. "$SCRIPT_DIR/fm-tangle-lib.sh"
# shellcheck source=bin/fm-ff-lib.sh disable=SC1091
Expand Down Expand Up @@ -800,6 +802,7 @@ secondmate_liveness_one() { # <meta> <id>
dead|missing)
cause="remote endpoint $agent_state on its configured host"
if out=$(FM_SPAWN_NO_GUARD=1 "$FM_ROOT/bin/fm-spawn.sh" "$id" --secondmate 2>&1); then
fm_codex_catalog_relay_dropped_effort_warnings "$out"
secondmate_note_respawned "$id"
report_relaunch "$id" "$cause" "host=$remote_host"
else
Expand Down Expand Up @@ -837,6 +840,7 @@ secondmate_liveness_one() { # <meta> <id>
cause="recorded endpoint confidently missing"
fi
if out=$(FM_SPAWN_NO_GUARD=1 "$FM_ROOT/bin/fm-spawn.sh" "$id" --secondmate 2>&1); then
fm_codex_catalog_relay_dropped_effort_warnings "$out"
secondmate_note_respawned "$id"
report_relaunch "$id" "$cause" "backend=$backend"
else
Expand Down Expand Up @@ -1137,15 +1141,22 @@ crew_dispatch_validate() {
else
verified_harnesses='["claude","codex","opencode","pi","pi-signed","grok","kimi","cursor","agy","muse","rovo","omp"]'
fi
err=$(jq -r --argjson typed "$typed_active" --argjson verified_harnesses "$verified_harnesses" --arg provider_re "$FM_QUOTA_PROVIDER_ID_RE" '
codex_max_models='[]'
if codex_max_models=$(fm_codex_catalog_models_supporting_effort max | jq -Rsc 'split("\n") | map(select(length > 0))'); then
:
else
codex_max_models='[]'
fi
err=$(jq -r --argjson typed "$typed_active" --argjson verified_harnesses "$verified_harnesses" \
--argjson codex_max_models "$codex_max_models" --arg provider_re "$FM_QUOTA_PROVIDER_ID_RE" '
def verified($h): $verified_harnesses | index($h);
def provider_id($p): ($p | type) == "string" and ($p | test($provider_re));
def effort_ok($h; $m; $e):
if $e == null then true
elif ($e | type) != "string" then false
elif $e == "ultra" then (($h == "pi" or $h == "pi-signed") and (($m | type) == "string") and ($m | startswith("codex-native/")) and ($m | length) > 13)
elif $h == "claude" then (["low","medium","high","xhigh","max"] | index($e))
elif $h == "codex" then ((["low","medium","high","xhigh"] | index($e)) != null or ($e == "max" and $m == "gpt-5.6-luna"))
elif $h == "codex" then ((["low","medium","high","xhigh"] | index($e)) != null or ($e == "max" and ($codex_max_models | index($m)) != null))
elif $h == "grok" then (["low","medium","high"] | index($e))
elif $h == "agy" then (["low","medium","high"] | index($e))
elif $h == "pi" or $h == "pi-signed" or $h == "omp" then (["low","medium","high","xhigh","max"] | index($e))
Expand Down
68 changes: 68 additions & 0 deletions bin/fm-codex-catalog-lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash

fm_codex_catalog_path() {
printf '%s\n' "${CODEX_HOME:-$HOME/.codex}/models_cache.json"
}

fm_codex_catalog_read() {
local catalog
command -v jq >/dev/null 2>&1 || return 1
catalog=$(fm_codex_catalog_path)
jq -ces '
def valid_reasoning_level:
if type != "object" then false
else (.effort | type) == "string"
end;
def valid_model:
if type != "object" then false
elif (.supported_reasoning_levels | type) != "array" then false
else all(.supported_reasoning_levels[]; valid_reasoning_level)
end;
if length != 1 then error("expected one catalog object")
elif (.[0] | type) != "object" then error("catalog must be an object")
elif (.[0].models | type) != "array" then error("catalog models must be an array")
elif (.[0].models | all(.[]; valid_model) | not) then error("invalid catalog model")
else .[0]
end
' "$catalog" 2>/dev/null
}

fm_codex_catalog_supports_effort() {
local model=$1 effort=$2 catalog
[ -n "$model" ] && [ "$model" != default ] || return 1
catalog=$(fm_codex_catalog_read) || return 1
jq -e --arg model "$model" --arg effort "$effort" '
any(.models[];
(.slug? == $model)
and any(.supported_reasoning_levels[]; .effort == $effort)
)
' <<< "$catalog" >/dev/null 2>&1
}

fm_codex_catalog_models_supporting_effort() {
local effort=$1 catalog models
catalog=$(fm_codex_catalog_read) || return 1
models=$(jq -r --arg effort "$effort" '
.models[]
| select(any(.supported_reasoning_levels[]; .effort == $effort))
| .slug?
| select(type == "string" and length > 0)
' <<< "$catalog" 2>/dev/null) || return 1
[ -z "$models" ] || printf '%s\n' "$models"
}

fm_codex_catalog_warn_dropped_effort() {
printf 'warning: dropped codex effort %s for model %s; catalog does not advertise it\n' \
"$1" "${2:-default}" >&2
}

fm_codex_catalog_relay_dropped_effort_warnings() {
local output=$1 line
while IFS= read -r line; do
case "$line" in
warning:\ dropped\ codex\ effort\ max\ for\ model\ *\;\ catalog\ does\ not\ advertise\ it)
printf '%s\n' "$line" >&2
;;
esac
done <<< "$output"
}
34 changes: 26 additions & 8 deletions bin/fm-contributions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
#
# poll consumes fm-fleet-snapshot.sh --contribution-input, a local-only read,
# and spends at most FM_CONTRIBUTIONS_BUDGET seconds on forge reads (default 20,
# 1..25). Every read is capped at five seconds. A pull observation has three
# 1..25). Every read is capped at five seconds; a read a deadline cuts short -
# the budget's own or a read's cap - is unmeasured, never unavailable, because
# a slow forge is not a failed forge. A pull observation has three
# dependent waves: core, six independent reads, then the closing head read;
# an issue has two waves. Parallelizing each independent wave bounds either
# observation to 3 * 5 = 15 seconds. poll reserves min(the configured budget,
Expand All @@ -44,8 +46,9 @@
# before any forge read or record write; it is never read as an empty input.
# Each distinct URL is observed once per poll and applied to every owner. A
# final observation applies to every owner without another forge read. When
# the budget runs out mid-observation, the poll ends with that URL's records
# untouched; only a genuine forge failure or head change records an error.
# the budget or a per-read cap cuts an observation short, the poll ends with
# that URL's records untouched; only a genuine forge failure or head change
# records an error.
# API failure leaves error evidence; an expired or absent observation is not
# silence. FM_CONTRIBUTIONS_MAX_AGE (default 900 seconds) bounds freshness.
# A URL whose last good observation is merged is final: it is never re-read,
Expand Down Expand Up @@ -187,15 +190,17 @@ write_record() { # task record-json-file
}

forge() {
local remaining bounded=0 rc=0 forge_err=${FORGE_ERR:-$TMP/forge.err}
local remaining rc=0 forge_err=${FORGE_ERR:-$TMP/forge.err}
remaining=$((DEADLINE - $(date +%s)))
# The budget, not the forge, refused this read.
[ "$remaining" -gt 0 ] || { BUDGET_EXHAUSTED=1; : > "$TMP/budget-exhausted"; return 1; }
if [ "$remaining" -le 5 ]; then bounded=1; else remaining=5; fi
if [ "$remaining" -gt 5 ]; then remaining=5; fi
fm_run_timed "$remaining" env GH_PROMPT_DISABLED=1 GH_NO_UPDATE_NOTIFIER=1 \
gh "$@" 2> "$forge_err" || rc=$?
# A read killed at the budget's own deadline is budget exhaustion too.
if [ "$rc" -eq 124 ] && [ "$bounded" -eq 1 ]; then
# A read killed at any deadline - the budget's own or this read's five-second
# cap - was cut by a bound, not refused by the forge. A slow forge read is
# unmeasured, never unavailable.
if [ "$rc" -eq 124 ]; then
BUDGET_EXHAUSTED=1
: > "$TMP/budget-exhausted"
elif [ "$rc" -ne 0 ]; then
Expand Down Expand Up @@ -285,13 +290,26 @@ observe() { # canonical GitHub URL -> normalized JSON
| valid_record' >/dev/null
}

wake_key_hash() { # stdin -> sha256 digest line; shasum and sha256sum are both optional
if command -v shasum >/dev/null 2>&1; then
shasum -a 256
elif command -v sha256sum >/dev/null 2>&1; then
sha256sum
else
fail 'shasum or sha256sum is required for the contribution wake key'
fi
}

publish_pending() { # task canonical-url record-file
local task=$1 url=$2 record=$3 token key count emitted status
count=$(jq '.pending | length' "$record")
[ "$count" -gt 0 ] || return 0
while IFS= read -r token; do
[ -n "$token" ] || continue
key=$(printf '%s\n%s\n' "$url" "$token" | shasum -a 256 | awk '{print $1}')
key=$(printf '%s\n%s\n' "$url" "$token" | wake_key_hash | awk '{print $1}')
case "$key" in
*[!0-9a-f]*|'') fail 'could not hash the contribution wake key' ;;
esac
emitted=0
status=0
fm_lock_acquire_wait "$FM_WAKE_QUEUE_LOCK" || return 1
Expand Down
13 changes: 11 additions & 2 deletions bin/fm-dispatch-resolve.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"
. "$SCRIPT_DIR/fm-control-lib.sh"
# shellcheck source=bin/fm-env-lib.sh
. "$SCRIPT_DIR/fm-env-lib.sh"
# shellcheck source=bin/fm-codex-catalog-lib.sh
. "$SCRIPT_DIR/fm-codex-catalog-lib.sh"
# shellcheck source=bin/fm-timing-lib.sh
. "$SCRIPT_DIR/fm-timing-lib.sh"

Expand Down Expand Up @@ -122,18 +124,25 @@ trap 'rm -f "$RULES"' EXIT
cp "$RULES_PATH" "$RULES" || die "could not snapshot rules file: $RULES_PATH"
chmod 400 "$RULES" || die "could not protect rules snapshot"
VERIFIED_HARNESSES=$(fm_control_harnesses | jq -Rsc 'split("\n") | map(select(length > 0))')
CODEX_MAX_MODELS='[]'
if CODEX_MAX_MODELS=$(fm_codex_catalog_models_supporting_effort max | jq -Rsc 'split("\n") | map(select(length > 0))'); then
:
else
CODEX_MAX_MODELS='[]'
fi

# The fields this tool consumes must be well formed; bootstrap owns the wider
# schema diagnostic, but an intake never selects around a malformed file.
rules_err=$(jq -r --argjson verified_harnesses "$VERIFIED_HARNESSES" --arg provider_re "$FM_QUOTA_PROVIDER_ID_RE" '
rules_err=$(jq -r --argjson verified_harnesses "$VERIFIED_HARNESSES" \
--argjson codex_max_models "$CODEX_MAX_MODELS" --arg provider_re "$FM_QUOTA_PROVIDER_ID_RE" '
def verified($h): $verified_harnesses | index($h);
def provider_id($p): ($p | type) == "string" and ($p | test($provider_re));
def effort_ok($h; $m; $e):
if $e == null then true
elif ($e | type) != "string" then false
elif $e == "ultra" then (($h == "pi" or $h == "pi-signed") and (($m | type) == "string") and ($m | startswith("codex-native/")) and ($m | length) > 13)
elif $h == "claude" then (["low","medium","high","xhigh","max"] | index($e)) != null
elif $h == "codex" then ((["low","medium","high","xhigh"] | index($e)) != null or ($e == "max" and $m == "gpt-5.6-luna"))
elif $h == "codex" then ((["low","medium","high","xhigh"] | index($e)) != null or ($e == "max" and ($codex_max_models | index($m)) != null))
elif $h == "grok" or $h == "agy" then (["low","medium","high"] | index($e)) != null
elif $h == "pi" or $h == "pi-signed" or $h == "omp" or $h == "muse" then (["low","medium","high","xhigh","max"] | index($e)) != null
elif $h == "rovo" then (["low","medium","high","max"] | index($e)) != null
Expand Down
11 changes: 9 additions & 2 deletions bin/fm-pending-reply-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,20 @@ fm_pending_reply_path() { # <state-dir> <corr_id>

# Privacy-safe correlation id: 16 lowercase hex chars (64 bits of entropy).
fm_pending_reply_new_id() {
local raw hex
local raw='' hex=''
if command -v openssl >/dev/null 2>&1; then
raw=$(openssl rand -hex 8 2>/dev/null || true)
fi
if [ -z "$raw" ]; then
raw=$(printf '%s' "$$-$(date +%s%N 2>/dev/null || date +%s)-$RANDOM$RANDOM" | cksum 2>/dev/null | awk '{print $1}')
hex=$(printf '%s' "$raw$RANDOM$RANDOM" | shasum -a 256 2>/dev/null | awk '{print $1}')
if command -v shasum >/dev/null 2>&1; then
hex=$(printf '%s' "$raw$RANDOM$RANDOM" | shasum -a 256 2>/dev/null | awk '{print $1}')
elif command -v sha256sum >/dev/null 2>&1; then
hex=$(printf '%s' "$raw$RANDOM$RANDOM" | sha256sum 2>/dev/null | awk '{print $1}')
else
printf 'fm-pending-reply: no SHA-256 hasher available (need shasum or sha256sum)\n' >&2
return 1
fi
raw=${hex:0:16}
fi
printf '%s' "$(printf '%s' "$raw" | tr 'A-F' 'a-f' | tr -cd 'a-f0-9' | cut -c1-16)"
Expand Down
Loading
Loading