gh stub exiting 2 on the close/api path -> wrapper exit 1 with the
normalized stderr message; mirrors the issue-assign 6b arm. All
touched suites green; brain mirror updated.
- issue-assign + milestone-create: set -o pipefail; the remove-assignee
lookup pipeline now fails loud on provider failure instead of reading
an empty result as a silent no-assignees skip (codex blocker). A
successful lookup with zero assignees still skips the edit.
- test-issue-assign-usage-contract: 6b arm proves provider-exit
normalization end to end (gh stub exiting 2 on issue edit -> wrapper
exit 1 with the normalized stderr message; codex should-fix).
- Battery: 23/23 suites green.
- Option-like value = any token starting with '-' followed by an
alphanumeric (-h, -ab, --help); multi-character short clusters were
still accepted (codex blocker). Bare '-' is reserved (future stdin).
- usage_error now prints the usage text to stderr as well (codex
should-fix): usage output belonged to stdout only on the help path.
- 16/16 usage-contract suites green.
- Value guards now reject option-like values: --anything always, and
single-dash flag shapes (-h, -i). Previously -b --help consumed
--help as the body and performed the write (codex example:
issue-close -i --help proceeding to 'Closed GitHub issue #--help').
Multi-char dash-leading text (-start of a list) stays a legal value.
- Every remaining direct provider exec (gh/tea/CMD arrays across
issue-create/edit/assign/list/view, milestone-*, pr-create/edit,
pr-close, issue-close/reopen) wrapped with rc capture and normalized
to exit 1 with a stderr message — provider exit 2 no longer collides
with the reserved usage-error status.
- All 16 usage-contract suites gained option-like and short-flag arms
(16/16 green). Existing suites re-verified; test-pr-edit and
test-issue-create-interactive-auth fail identically with these
changes stashed (environment-coupled, not regressions; documented).
request-changes and comment both validate their required body
immediately after argument parsing (rc 2, stderr, zero provider
contact). The repo-host-override suite's action-message expectation
updated to the new wording; suite green.
An unsupported --action previously reached platform detection (and
could touch the provider) before failing with a provider-class status;
comment-without-body exited 1 mid-switch. Both now fail fast with
usage_error (rc 2, stderr) immediately after argument parsing. Test
arms added including the zero-provider-contact assertion for
invalid-action runs.
- issue-list + milestone-create: usage() default exit 2, value guards,
missing-title via usage_error.
- lane-brief: getopts replaced with a while/case parser carrying R2
long-flag aliases (--repo --milestone --label/--labels --login
--limit) beside the short forms; login-resolution failure reclassified
from exit 2 to exit 1 (credential class, not an invocation defect).
- Three suites enrolled (population 88); lane-brief pr-linkage suite
still ALL PASS; green in both trees; mirrored to the brain tree.
With this, every agent-facing wrapper in tools/git carries the R1/R4
usage-error contract (22/22 non-guard wrappers; guards keep their own
contracts).
usage_error contract (stderr + exit 2) with value guards on every
value-taking option and the missing-required checks (milestone-close
-t, issue-view -i). Three suites enrolled (population 85); green in
both trees; mirrored to the brain tree.
usage() default exit 2 (usage-error contract); value guards on all
seven value-taking options; -d/--draft stays boolean. Existing
fallback-default-base suite green (29/29). Usage-contract suite
enrolled (population 81); mirrored to the brain tree.
usage() default exit 2; semantic invocation checks (mutual exclusion,
integer/format validation, missing edit option, --login-only-for-Gitea)
exit 2; value guards on all value-taking options; existing pr-edit
suite still green; usage-contract suite enrolled (population 80);
mirrored to the brain tree.
- issue-create: usage() default exit is now 2 (usage-error contract);
unknown option, missing title, and value-less -t/-b/-l/-m land there
with stderr messages; exit-code line added to help. Interactive
-i/--interactive unchanged (boolean, no value arm). Existing
body-safety and interactive-auth suites still green.
- Template bug fixed across ALL seven usage-contract suites:
expect_stderr referenced an unbound $desc (should be $2); latent
until issue-create's capitalized 'Unknown option' missed the
case-sensitive grep and fired it. All seven re-verified green.
- Suite enrolled (population 79); mirrored to the brain tree.
issue-edit already used long-flag-first parsing; this adds the rc=2
usage-error contract (unknown option, missing -i, value-less flags
across all five options), the exit-code line in help, and the enrolled
usage-contract suite (population 78). Mirrored to the brain tree.
-b/--body canonical review comment flag with -c/--comment alias; usage
errors stderr + exit 2 (unknown option, missing -n/-a, value-less
flags including -a/-l/-r/-H, and the semantic check: request-changes
without a comment). Existing pr-review suites still green.
Suite enrolled in framework-shell CI (enumeration guard OK); mirrored
to the brain tree.
Same contract as issue-close (7a38eed1): -b/--body canonical comment
flag with -c/--comment alias; usage errors stderr + exit 2 (unknown
option, missing required, value-less flags); GitHub-path provider
failures normalized to exit 1.
Both new suites enrolled in framework-shell CI (enumeration guard OK,
population 76). pr-close's suite uses FAILING provider stubs (exit 99):
its API fallback treats a successful curl as a closed PR, so exit-0
stubs let sandbox arms succeed (measured); post-sandbox contact
assertions are correspondingly scoped, parser arms remain zero-contact.
Mirrored to the brain tree; all usage-contract suites green in both.
Same contract as issue-comment (PR #1462): -b/--body canonical comment
flag with -c/--comment alias; usage errors stderr + exit 2 (unknown
option, missing -i, value-less flags); provider failures normalized to
exit 1 on the GitHub path (gh's own usage errors exit 2 and would
collide with the reserved status).
New suite test-issue-close-usage-contract.sh, enrolled in
framework-shell CI (enumeration guard OK, population 74). Probe
assertions split: parser arms must contact zero providers; sandbox arms
may issue detection reads only (tea login list), never gh/curl.
Existing fail-closed suite still green. Mirrored to the brain tree.
Codex review of a27b7dc9 (blocker): check-test-enumeration.sh rejects
unenumerated test files.
- test-issue-comment-usage-contract.sh enrolled in test:framework-shell
(hermetic by construction: PATH stubs, sandboxed runner, no network).
- test-issue-comment-readback.sh enrolled and its stale exclusion removed.
The exclusion's own note anticipated this: 'joins CI after the
wrapper-half hermeticity fix' - that fix (neutralizing seat-exported
MOSAIC_GIT_IDENTITY / MOSAIC_BRAIN_HOME that escape the sandboxed HOME)
landed in a27b7dc9. Suite green in brain and worktree trees tonight.
Enumeration guard: OK (population 73, enumerated 60, excluded 14).
Upstream of brain commits 08a00149 + 971586ef + the arm-6 sandbox fix
(brain 5th commit of 2026-08-28 series):
- R1: -b/--body is the canonical comment flag (matches issue-create,
issue-edit, pr-create, pr-edit); -c/--comment stays a compatible alias.
- R4: usage errors print to stderr and exit 2, distinct from provider,
credential, and verification failures (exit 1). Value-less flags fail
loudly (previously -c with no value died silently at rc=1 via set -e on
the failed shift 2). gh failures on the GitHub path normalize to exit 1
(gh's own usage errors exit 2 and would collide with the reserved code).
- Tests: new usage-contract suite (help rc, unknown/missing/value-less rc=2
on stderr, alias parse acceptance under a sandboxed runner, GitHub-path
exit normalization with a stubbed gh, zero provider contact on parser
failure); readback suite gains case 11 (full verified write via -b) and
neutralizes seat-exported MOSAIC_GIT_IDENTITY / MOSAIC_BRAIN_HOME that
escape the sandboxed HOME (documented HTTP 401 / fail-loud shapes).
Driver: a fleet seat full-stopped on an issue-comment usage error because
usage failures were indistinguishable from provider failures and the stop
gate treated every wrapper failure as blocking.
Per ops-ci-01's procedure (comments 23386/23394, brain D27): pipeline config
travels with the commit under test, so the controlled rerun requires the pinned
anchor in the PR head itself. This merge absorbs everything next took since the
original merge parent (1bdeed62): the ci-base pin (cb9a0d1, lock-9cb7ffcd8828),
the legacy-credential removal (#1325), and the ci-queue-wait statuses:null fix
(#1129 — the branch from the divergence analysis, now landed).
One conflict, same file as round 1: test:framework-shell union — our 54-entry
chain plus next's new test-ci-queue-wait-no-status.sh entry at its position
(55 entries, every target existence-verified). Enumeration guard green:
population 61, enumerated 46, excluded (signed) 16.
Phase D2 of plan 2026-08-19: single package, single install, single command.
The framework installer already treats skills/** as a shipped, manifest-owned
framework subtree, so the folded skills now install into
$MOSAIC_HOME/skills with the rest of the framework — no second repository,
no separate sync step:
- mosaic-sync-skills (bash + powershell): the fetch machinery is gone (clone,
pull, dirty-state migration, rsync from sources/agent-skills). The script
now only links installed skills into runtime homes. --link-only is a compat
no-op; --no-link exits having nothing to do.
- catalog.ts: the sources/agent-skills fallback is dead and removed.
- install.sh, launch.ts, defaults/README.md, README.md, skills/README.md:
references to the second repo rewritten to describe the shipped path.
Verified: clean install into a fresh MOSAIC_HOME produces 102 skills with no
sources/ directory; the linker then links the selected skills into the four
runtime homes with no git involvement.
Both defects found in review by rev-code-01 on #1311.
F3 — the JSONL record interpolated every field with a bare %s. An identity comes
from git config or the environment and a cwd is whatever directory git ran in, so
either can contain a quote or a backslash. One such refusal turned the day's spool
into unparseable JSONL, and the operator would only discover it while reading the
record that explains an outage. Fields are now JSON-escaped.
F2 — the diagnostic printed "record: <spool>/<date>.jsonl" unconditionally, but
the record is only written inside the branch where mkdir -p succeeded. When the
spool cannot be created the helper named a file that does not exist, on exactly
the hosts where the escalation was lost. It now reports the real path or says
NOT WRITTEN.
Also: prettier on README.md, which was the format-step failure on pipeline 2508.
It reflowed only the two tables this branch added.
Tests: cases 14 and 15 cover both. Verified discriminating — against the previous
helper with these same tests, case 14 fails with the unparseable record printed
and case 15 fails on both assertions; against this one both pass.
The first draft of case 14 used `ls "$spool"/*.jsonl | head -1`, which under
`set -o pipefail` exits 2 on a missed glob and killed the suite with zero output
— the same silent-nonzero failure rev-code-01 hit from a partial tools/ extraction
and the reason this file exists. Replaced with a glob loop and a comment.
Two changes to one rule: a credential is resolved from exactly one place,
and an identity that cannot be resolved is refused rather than substituted.
FAIL CLOSED. Both readers ended in an unconditional fall-through to the
shared Gitea account whenever an identity did not resolve. Every seat in a
fleet therefore pushed, opened PRs and filed reviews under one account, and
a record made that way cannot be traced to the agent that made it
afterwards. The fallback now applies only where there is no attribution to
lose: a host with no fleet. Where seats exist, an unresolvable request emits
nothing, exits nonzero, explains itself on stderr, and — in the git helper —
appends a record naming the identity, host, reason and cwd, and no token
value, to ${MOSAIC_CREDENTIAL_SPOOL:-~/.local/state/mosaic-credential-escalations}.
A host runs a fleet when <brain>/fleet/agents exists, which is the signal
packages/mosaic/src/fleet/brain-home.ts already uses to decide a brain is
active, resolved the same way (MOSAIC_BRAIN_HOME, else ~/.mosaic). This is
what keeps the change a no-op for an operator who has not provisioned
per-slot tokens: no fleet directory, shared account, unchanged. It is also
why there is no environment variable to restore the old behavior — one would
reintroduce the substitution being removed.
STORE SELECTION. Both readers hardcoded ~/.config/mosaic/secrets/gitea-tokens,
so a seat's own secrets/ slot was invisible to the framework: a seat could
hold a valid credential and still be served the shared account. The store is
now chosen by what the identity is. An identity with a directory under
<brain>/fleet/agents/ is a seat and is read only from
<brain>/fleet/agents/<id>/secrets/; any other identity is a service identity
and is read from the framework store. There is no precedence between them
and no fallback from one to the other, so a seat with an empty slot is
refused even when a same-named token sits in the framework store. Two copies
of one credential are drift rather than redundancy, and drift surfaces as
the stale copy returning 401, which reads as a revoked token and sends
whoever debugs it somewhere else.
detect-platform.sh is in scope alongside git-credential-mosaic because they
are the two readers of these tokens. Patching only the git helper would make
"one credential, one location" true for push and fetch and false for
pr-create.sh, issue-create.sh and pr-review.sh, which is the harder failure
to notice.
TESTS. The three assertions that pinned the shared-account fall-through are
now fail-closed assertions, and a refusal is checked four independent ways:
nonzero exit, empty stdout, a stderr diagnostic naming identity and host,
and no shared token value anywhere in the output. The exit code alone would
pass against a helper that emitted the credential and then failed. Added:
seat-slot resolution, the no-cross-store-fallback case with a control
proving the framework-store file it declines to read is readable, no-identity
on a fleet host, the fleet gate firing on the default ~/.mosaic and not only
on an injected MOSAIC_BRAIN_HOME, and a cross-host leak check. Both suites
were run against the pre-change code as a control and fail there on exactly
the shared-token emission.
shellcheck is not installed on the authoring host, so the rewritten helper
is unlinted locally and CI is the first lint of it.
The first PR for this change was filed under the retired mos-dt-0 principal
(pr-create.sh has no --login flag and find_tea_login_for_host returns the first
host match) and was closed and refiled as #1270. That left in-tree references
pointing at a closed duplicate PR rather than at the burn-down issue, which is
the wrong target for them anyway: the open design question belongs on #1271.
The `test` step has failed on every `next` pipeline since #1017 on exactly one
assertion, and it is the same one on unrelated PRs:
FAIL: host provides 'pi' in the system path; missing-binary cases are not
measurable here (framework/tools/fleet/test-start-agent-session.sh:103)
Measured 2026-08-16 across pipelines 2444 (#1256), 2438 (#1240) and 2441
(#1017-quality): exactly one FAIL line in each full log, identical, this line.
Control `zzz-not-present-zzz` -> 0 on all three.
Cause. #1241 (5c35a250) added the guard: the suite shims fake mosaic/pi/npm into
$FAKE_BIN, but the constructed PANE_PATH always ends in the real system path, so
on a host that installs those binaries the missing-binary cases cannot be
measured and a green run would mean nothing. The guard says so instead of
passing. Its own pipeline 2430 was green only because the suite was CI-excluded
at the time, so the guard had never run in CI. #1017 (c56483eb) then enumerated
it and dropped the exclusion. The CI image installs
@earendil-works/[email protected].1 on purpose, so the precondition is
unsatisfiable there. Both commits are mine.
The guard is correct and is not being softened. A check that cannot measure its
property and reports success is the failure mode this repo has been cataloguing
all week; the error was wiring the suite into an image that violates its
precondition, so the wiring is what gets reverted.
Second effect, which is the reason this cost a day rather than an hour:
test:framework-shell is one && chain and this sat at position 44 of 48, so
glpi/test-list-http-status.sh, orchestrator/test-board-roll.sh,
woodpecker/test-ci-wait-exit-matrix.sh and _scripts/test-fleet-transport-check.sh
have not run at all since the merge. The pipeline reported one failure, never
"one failure plus four unrun". All four are green when run directly on
sb-it-1-dt, so the mask hid nothing broken -- but that is a local result on one
host, not a CI-image result.
Verification, with controls:
- enumeration guard OK (population 52, enumerated 36, signed-excluded 16).
- control A, exclusion line removed while unwired -> FAIL UNENUMERATED.
- control B, exclusion line kept while rewired -> FAIL CONTRADICTORY EXCLUSION.
The gate discriminates in both directions, so its OK is load-bearing.
- the four formerly-masked suites: rc=0 each, run directly.
- the full chain cannot be run to completion on sb-it-1-dt: it stops earlier, at
the lease-broker Invariant R test, because this host carries the quarantined
operator-global pi 0.84.2 against a measured 0.84.1. That is host-specific and
out of scope here -- CI pins 0.84.1, and the single FAIL line in those three
pipelines proves positions 1-43 passed there.
Burn-down is to control the tail of PANE_PATH inside the test, not to remove pi
from the image. Recorded in the exclusion reason and in #1269.
Resolves conflicts from #1229 (tools/install.sh node provisioning) and #1252
(package.json test:framework-shell). tools/install.sh resolved to the reviewed
composite blob 5d28f773; package.json resolved as a union so both #1252's four
suites and #1245's transport-check suite run (48 links).