Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions bin/fm-brief-heading-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
# This file is the single owner of how a brief's sections are read: the
# `# Task` subsections bin/fm-brief.sh scaffolds feed the no-mistakes
# `--intent` contract in bin/fm-dod-lib.sh, spawn and promotion validation,
# and the task text bin/fm-dispatch-resolve.sh sends to the router, so every
# consumer sees the same section bodies.
# and the task text bin/fm-dispatch-resolve.sh sends to the router.
# Every consumer shares parsing semantics; resolver-only privacy preprocessing
# is owned by docs/configuration.md "Never-send list".

# Parse an exact ATX heading outside fenced blocks. Body mode prints through
# the next unfenced heading at the same or a higher level; present mode reports
Expand Down
10 changes: 10 additions & 0 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@
# charters still use a single `{TASK}` charter fill. Firstmate may adjust other
# sections when the task genuinely deviates (e.g. working an existing external
# PR instead of shipping a new one).
# Jev privacy markers: to keep project- or customer-sensitive brief text out of
# bin/fm-dispatch-resolve.sh requests, wrap it in these exact standalone lines:
# <!-- dispatch-never-send:start -->
# <!-- dispatch-never-send:end -->
# They need the exact opt-in line `# dispatch-never-send marked-sections` in
# config/dispatch-never-send. Missing opt-in or malformed markers with the
# reserved `dispatch-never-send` prefix stop the request; arbitrary spelling
# mistakes are not detected. The local brief and the worker's instructions
# keep the marked text; docs/configuration.md "Never-send list" owns the full
# contract.
# Usage: fm-brief.sh <task-id> <repo-name> --mode <no-mistakes|direct-PR|local-only> [--branch-prefix <prefix>] [--forge <none|gerrit> [--shape squash]] [--herdr-lab]
# fm-brief.sh <task-id> <repo-name> --scout [--herdr-lab]
# fm-brief.sh <task-id> --secondmate {<project>...|--no-projects}
Expand Down
5 changes: 3 additions & 2 deletions bin/fm-config-inherit-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
# set of LOCAL (gitignored) config items down into each secondmate home's
# config/, so a secondmate's OWN crewmates inherit the primary's settings
# (e.g. primary config/crew-dispatch.json makes a secondmate use the same dispatch
# profile rules and primary config/dispatch-never-send keeps the same values
# out of its dispatch resolver requests, primary config/crew-harness=codex makes a secondmate's crewmates
# profile rules and primary config/dispatch-never-send applies the same
# never-send values and marked-section opt-in to its dispatch resolver
# requests, primary config/crew-harness=codex makes a secondmate's crewmates
# spawn on codex too, primary config/backlog-backend=manual makes that home
# hand-edit backlog files too, primary config/backend pins that home's local
# runtime-backend default for future spawns, primary config/startup-memory-budget
Expand Down
97 changes: 75 additions & 22 deletions bin/fm-dispatch-resolve.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,13 @@
# docs/configuration.md "Crew dispatch profiles" owns the declared fields and
# "Typed dispatch resolution" owns this tool's operator contract.
#
# Never-send check: when the optional $FM_HOME/config/dispatch-never-send list
# exists, every string value of the built request is checked against it
# before the POST. Each non-blank, non-# line is a literal matched
# case-insensitively, with surrounding whitespace trimmed and every run of
# whitespace, on both sides, treated as one space. A match, or a list that
# is not a readable regular file, prints one
# "dispatch-resolve: off (...; nothing sent)" line on stderr naming at most
# the list line number, never its value, prints nothing on stdout, and exits
# 0 with no network or quota call, exactly like the absent-key off path.
# Never-send: docs/configuration.md owns the opt-in list and section markers
# in $FM_HOME/config/dispatch-never-send. Enabled marked regions are removed
# from the brief before task extraction; markers without that opt-in stop the
# whole request. Then every request string is checked for literals.
# A match, unreadable list, or invalid privacy directive/marker prints one
# "dispatch-resolve: off (...; nothing sent)" line on stderr without private
# text, prints nothing on stdout, and exits 0 with no network or quota call.
#
# Output (stdout, TOON-style block):
# dispatch-resolve:
Expand Down Expand Up @@ -301,19 +299,72 @@ never_send_off() {
exit 0
}

# Checks every string the request carries, so no text reaches the network
# unchecked. grep's own stderr is discarded because it can echo the pattern.
never_send_check() {
local list value n=0 rc
# Snapshot and validate privacy policy before reading any outgoing brief text.
NEVER_SEND_LIST='' MARKED_SECTIONS=0
never_send_load() {
local value n=0
[ -e "$NEVER_SEND_PATH" ] || [ -L "$NEVER_SEND_PATH" ] || return 0
{ [ -f "$NEVER_SEND_PATH" ] && [ -r "$NEVER_SEND_PATH" ]; } \
|| never_send_off "$NEVER_SEND_PATH is not a readable regular file"
# Collapse whitespace runs on both sides so a value the brief wraps across
# lines or spaces differently still matches
NEVER_SEND_LIST=$(jq -Rr 'gsub("\\s+"; " ")' "$NEVER_SEND_PATH" 2>/dev/null) \
|| never_send_off "could not read $NEVER_SEND_PATH"
while IFS= read -r value; do
n=$((n + 1))
value=${value# }
value=${value% }
case "$value" in
'# dispatch-never-send marked-sections') MARKED_SECTIONS=1 ;;
'#'*)
case "$(printf '%s' "${value#'#'}" | tr '[:upper:]' '[:lower:]')" in
dispatch-never-send*|' dispatch-never-send'*)
never_send_off "invalid privacy directive in $NEVER_SEND_PATH line $n" ;;
esac
;;
esac
done <<<"$NEVER_SEND_LIST"
}

# Markers are interpreted on the original brief, even inside Markdown fences,
# so protected headings cannot change extraction or cause a whole-brief fallback.
never_send_brief() {
local rc=0
grep -qiE -e '<!--[[:space:]]*dispatch-never-send' "$BRIEF" 2>/dev/null || rc=$?
case "$rc" in
0) [ "$MARKED_SECTIONS" -eq 1 ] \
|| never_send_off "never-send markers need the marked-sections directive" ;;
1) cp "$BRIEF" "$SEND_TEXT" || never_send_off "could not read the brief"
return ;;
*) never_send_off "could not read the brief" ;;
esac
awk '
{
marker = $0
sub(/^[[:space:]]+/, "", marker)
sub(/[[:space:]]+$/, "", marker)
if (marker == "<!-- dispatch-never-send:start -->") {
if (hidden) exit 1
hidden = 1
next
}
if (marker == "<!-- dispatch-never-send:end -->") {
if (!hidden) exit 1
hidden = 0
next
}
if (tolower($0) ~ /<!--[[:space:]]*dispatch-never-send/) exit 1
if (!hidden) print
}
END { if (hidden) exit 1 }
' "$BRIEF" > "$SEND_TEXT" 2>/dev/null \
|| never_send_off "invalid never-send markers or unreadable brief"
}

# Checks every string the request carries. grep stderr can echo the pattern.
never_send_check() {
local value n=0 rc
[ -n "$NEVER_SEND_LIST" ] || return 0
jq -r '.. | strings | gsub("\\s+"; " ")' <<<"$REQUEST" > "$SEND_TEXT" 2>/dev/null \
|| never_send_off "could not extract the request text to check"
list=$(jq -Rr 'gsub("\\s+"; " ")' "$NEVER_SEND_PATH" 2>/dev/null) \
|| never_send_off "could not read $NEVER_SEND_PATH"
while IFS= read -r value; do
n=$((n + 1))
value=${value# }
Expand All @@ -327,7 +378,7 @@ never_send_check() {
1) ;;
*) never_send_off "could not check the request text against $NEVER_SEND_PATH line $n" ;;
esac
done <<<"$list"
done <<<"$NEVER_SEND_LIST"
}

# Send Jev only the task-specific sections bin/fm-brief.sh scaffolds, plus a
Expand All @@ -336,22 +387,24 @@ never_send_check() {
# A brief with neither section goes whole. Ship delivery mode is deliberately
# not sent: live runs showed it pushing routine ship briefs to the top tier.
brief_kind() {
if grep -qxF 'This is a SCOUT task: the deliverable is a written report, not a PR.' "$BRIEF"; then
if grep -qxF 'This is a SCOUT task: the deliverable is a written report, not a PR.' "$SEND_TEXT"; then
printf 'Brief kind: scout (report only)\n\n'
fi
}
task_sections() {
local heading
for heading in "## Captain's intent" "## Firstmate spec"; do
fm_brief_task_heading_present "$BRIEF" "$heading" || continue
printf '%s\n%s\n\n' "$heading" "$(fm_brief_task_heading_body "$BRIEF" "$heading")"
fm_brief_task_heading_present "$SEND_TEXT" "$heading" || continue
printf '%s\n%s\n\n' "$heading" "$(fm_brief_task_heading_body "$SEND_TEXT" "$heading")"
done
}
never_send_load
never_send_brief
SECTIONS=$(task_sections)
if [ -n "$SECTIONS" ]; then
{ brief_kind; printf '%s\n' "$SECTIONS"; } > "$TASK_TEXT" || die "could not read brief: $BRIEF"
else
cp "$BRIEF" "$TASK_TEXT" || die "could not read brief: $BRIEF"
cp "$SEND_TEXT" "$TASK_TEXT" || die "could not read brief: $BRIEF"
fi
command -v curl >/dev/null 2>&1 || emit_error "curl not installed"
REQUEST=$(jq -n --rawfile brief "$TASK_TEXT" --arg project "$PROJECT" --arg model "$TS_MODEL" \
Expand Down
45 changes: 37 additions & 8 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,7 @@ Firstmate invokes the resolve path directly after writing the brief, without a p

When on and at least one rule exists, the tool sends the project name and the brief's task-specific text as state and asks one Choice question whose options are every rule's `when` plus the fixed neutral option for no matching rule; the model never sees quota, catalogs, `why`, `use`, approvals, or confidence floors.
The task-specific text is the brief's `## Captain's intent` and `## Firstmate spec` sections under `# Task` that `bin/fm-brief.sh` scaffolds, read by the same parser that feeds `fm-spawn.sh` validation and the no-mistakes `--intent` contract; a brief with neither section is sent whole.
Section extraction and the whole-brief fallback both read the brief after any enabled never-send marked regions are removed (see the never-send list below).

When the sections are sent from a scout brief, the line `Brief kind: scout (report only)` comes first, taken from the scaffold's scout contract line; ship briefs and briefs sent whole get no kind line.
A ship brief's delivery mode is deliberately not sent, because in live runs naming it pushed a routine ship brief toward the hardest tier (see [the verification record](verification/dispatch-resolve.md)).
Expand All @@ -1175,22 +1176,50 @@ The scaffold's standard setup, rules, and definition-of-done text is the same in

**Never-send list (config/dispatch-never-send)**

The optional local, gitignored `config/dispatch-never-send` keeps values you name from ever leaving the machine in a resolver request.
It has no default entries, and an absent file changes nothing.
Like `config/crew-dispatch.json`, it is inherited into secondmate homes, so a secondmate's resolver withholds the same values.
The optional local, gitignored `config/dispatch-never-send` keeps named values and marked brief regions out of Jev resolver requests.
It has no default entries, and an absent file sends unmarked briefs exactly as before.
Like `config/crew-dispatch.json`, it is inherited into secondmate homes, so a secondmate's resolver applies the same privacy policy.

Each non-blank line not beginning with `#` is one literal value, matched case-insensitively.
Each non-blank line not beginning with `#` remains one literal value, matched case-insensitively.
Every entry is trimmed of surrounding whitespace, and any run of whitespace, in the entry or in the checked text, counts as one space, so a value the brief wraps across lines still matches.
Ordinary `#` comments remain ignored.
The one supported directive is `# dispatch-never-send marked-sections`, compared after trimming surrounding whitespace and collapsing whitespace runs to one space.
Its spelling and case must match exactly, and the space after `#` is required.
Any other comment that starts with `dispatch-never-send` after the `#` and optional whitespace, compared case-insensitively (for example `#dispatch-never-send marked-sections` or `# Dispatch-Never-Send marked-sections`), is an invalid directive and stops every request.

```text
# Client names
# Literal values still stop the whole request
Example Client Ltd
# Remove explicitly marked regions from briefs
# dispatch-never-send marked-sections
```

Before the request is sent, every string in it is checked: the project name, the task text, each rule's `when`, and the fixed question text.
A match stops the request: the resolver behaves exactly as when it is off, printing one `dispatch-resolve: off (...; nothing sent)` line on stderr and nothing on stdout, making no network or quota call, and exiting 0, so firstmate dispatches through its existing intake.
A list that is present but not a readable regular file also stops the request the same way rather than sending unchecked text.
Brief authors wrap project- or customer-sensitive text in these exact standalone marker lines:

```markdown
<!-- dispatch-never-send:start -->
### Private context
Synthetic private details kept only in the local brief.
<!-- dispatch-never-send:end -->
```

With `# dispatch-never-send marked-sections` present, the resolver removes both marker lines and everything between them from the original brief, before task-section extraction or whole-brief fallback.
Place the opening marker before the heading when the heading itself is sensitive.
Surrounding whitespace on marker lines is allowed, markers apply even inside Markdown code fences, and multiple disjoint regions are supported.
The local brief and other brief consumers remain unchanged.

Markers never send silently.
A brief containing `<!--` followed by optional whitespace and the reserved `dispatch-never-send` prefix, compared case-insensitively on the same line, stops the entire request when the directive is absent, whether the list file is missing or only holds literals.
With the directive present, every such line must be exactly one of the two canonical markers above after trimming surrounding whitespace: nested, unmatched, inline, malformed suffixes (`<!-- dispatch-never-send:star -->`), unspaced (`<!--dispatch-never-send:start-->`), or differently cased (`<!-- Dispatch-Never-Send:start -->`) markers stop the entire request rather than being corrected.
A brief without such text is sent as before.
This option protects only the marked occurrences in the brief, not copies elsewhere or dispatch-rule text; use literals when those must also be withheld.
Do not send real Vernant/customer text until authorized: TypeSafe's public terms have not established the required `standard_confidential/v1` processor protections of deletion within 30 days and no training.

Before the request is sent, every remaining string in it is checked for literal matches: the project name, the sanitized task text, each rule's `when`, and the fixed question text.
A literal match stops the request: the resolver behaves exactly as when it is off, printing one `dispatch-resolve: off (...; nothing sent)` line on stderr and nothing on stdout, making no network or quota call, and exiting 0, so firstmate dispatches through its existing intake.
A list that is present but not a readable regular file, an invalid directive, or a marker problem also stops the request the same way rather than sending unchecked text.
That one diagnostic names the list line number at most and never prints the listed value or the matching text.
The offline behavior coverage in `tests/fm-dispatch-resolve.test.sh` captures outgoing request bodies using only synthetic data.

**Missing or invalid rules**

Expand Down
3 changes: 2 additions & 1 deletion docs/verification/dispatch-resolve.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ It proves the absent key (environment and `.env`) prints one stderr line, nothin
It proves absent, default-only, and empty-rules files return `no rules to match` without a model or quota request, while a broken rules-file symlink exits 2 as unreadable.
It proves the documented starter configuration resolves its Pi default through the declared Claude provider, a `.env` key turns the tool on, and the environment wins over it.
It proves the key is absent from child environments, never appears on `curl` argv, and arrives only as the bearer header on the descriptor.
It proves the request uses the fixed endpoint and model, carries only the project, the brief's task sections read by the shared brief-heading parser with a scout line only for a scout brief and never a ship brief's delivery mode (or the whole brief when it has neither section), and rule Choice with one option per rule plus the fixed neutral none option, and never carries `why`, `use`, or quota.
It proves the request uses the fixed endpoint and model, carries only the project, the privacy-filtered brief's task sections read by the shared brief-heading parser with a scout line only for a scout brief and never a ship brief's delivery mode (or the whole privacy-filtered brief when it has neither section), and rule Choice with one option per rule plus the fixed neutral none option, and never carries `why`, `use`, or quota.
Synthetic fixtures cover the [never-send contract](../configuration.md) ("Never-send list") through outgoing-body capture and checks that withheld requests invoke neither `curl` nor `quota-axi`.
It proves a declared `min_confidence` is checked against the rule's own probability both as the pick and as a runner-up, a picked rule below it falls to the most probable runner-up that clears its floor, is `ambiguous` when none does or two tie, and that a file without declared floors keeps the global 0.6 floor on confidence unchanged.
It proves the clear, fixed-floor ambiguous with candidate evidence, escalate (approval with candidate evidence, unverifiable rule floor, tie, nothing rankable), known rule-floor fall-through, known and unverifiable profile-floor evidence, explicit-provider and provider-ID enforcement, authoritative Agy and explicit-provider Gemini routing, partial providers, eligible unranked candidates and their clear-result note, concrete quota vetoes and profile-floor shortfalls taking precedence over uncertainty, account-wide quota veto, limiting-bound ranking, schema-6 account-row binding with schema-5 compatibility, missing-curl and quota-axi failures, HTTP 429 and 500, transport failure, malformed usage, zero-mass or malformed probabilities or confidence, malformed or duplicate profile, invalid selector, removed-option rejection, and out-of-range rule ID paths behave as the contract states, with configuration errors exiting 2 before any network call.
It proves runway is judged against the task horizon: an `established` projection shorter than the default 240-minute or a declared `task_horizon_minutes` horizon on any applicable bound of the highest-ranked candidate (including a non-limiting exact-model bound) escalates with no profile, while `through_reset`, an established projection covering the horizon (including exactly at it), an `early` or absent-confidence projection, and `unknown` runway clear with any non-passing bound named in a candidate warning; `exhausted_now` vetoes every candidate outside the `omp` Codex pool; a short winner is never replaced by a lower-ranked candidate in its rule, another rule, or the default array; and the horizon is never sent to the model.
Expand Down
3 changes: 3 additions & 0 deletions tests/fm-brief.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ test_help_includes_entire_header() {
local help
help=$("$ROOT/bin/fm-brief.sh" --help)
assert_contains "$help" "Refuses to overwrite an existing brief." "fm-brief.sh --help omitted its header terminator"
assert_contains "$help" "<!-- dispatch-never-send:start -->" "fm-brief.sh --help omitted the never-send start marker"
assert_contains "$help" "<!-- dispatch-never-send:end -->" "fm-brief.sh --help omitted the never-send end marker"
assert_contains "$help" "# dispatch-never-send marked-sections" "fm-brief.sh --help omitted the never-send opt-in"
pass "fm-brief.sh: --help renders the complete header"
}

Expand Down
Loading
Loading