P0: main went red
Workflow: CI concluded failure on main.
Failing run: https://github.com/edobry/minsky/actions/runs/32184436800
HEAD SHA: 5163d9a5310e9fcf3d98fb20bae145e759ff571a
Head commit: feat(mt#4245): Reconcile the three agent tool-surface mechanisms into one registry (ADR-043)
Summary
Minsky had three mechanisms independently answering "what tools does this agent have," and an
Accepted RFC (390937f0, Piece C1) that already decided the question. This is the reconciliation
verdict, plus the one piece of it that ships without waiting for the registry.
The verdict (ADR-043, Proposed): build the capability registry as data plus a compiler, and make
the two shipped mechanisms its compilation targets. Do not build or adopt an MCP gateway.
cockpit.drivenSession.mcpServers → retained as separate (it answers "what does this
installation provide," which is not C1's question)
- agent-definition
tools: → retained as storage (harness-native; validated against, later
generated from, the registry)
- declared capability set (C1) → the source
The finding that changed the design
An advisory recorded in mt#4245 proposed enforcing tool masks with --disallowedTools (Stage 1) and
triggering escalation by harvesting permission_denials (Stage 2). Both premises were relayed and
unrun. Probing them falsified the composition:
| rule shape |
agent-visible signal when withheld |
| bare tool name |
none — tool is removed from context |
scoped rule (Bash(rm *)) |
permission_denials in the result JSON |
--disallowedTools and settings permissions.deny are equivalent; the axis is bare-vs-scoped. And
MCP rules have no scoped form — per code.claude.com/docs/en/permissions, an MCP rule is a
server name "optionally followed by the name of a tool from that server."
So permission_denials can never contain an MCP tool. Every motivating incident in RFC 390937f0 is
an MCP tool (reviewer_retrigger, deployment_status), which means the proposed harvest would have
covered none of them — silently, at exit 0, is_error: false. The escalation signal has to come
from mt#2588's tool-emitted CAPABILITY_REQUIRED instead, and third-party MCP tools can emit
neither signal. That gap is the honest trigger for a broker; ADR-043 records it as knowingly
uncovered rather than solved.
Key changes
docs/architecture/adr-043-agent-tool-surface-registry.md — the verdict, decision-first
scripts/lib/agent-tool-provisioning.ts — pure core: parse MCP server names, cross-reference
declarations against the provisioned set, format findings
scripts/check-agent-tool-provisioning.ts — the gate (exit 1 on mismatch)
package.json — check:agent-tool-provisioning, added to validate-all
The check answers: if this agent type ran inside a driven session, would its declared tools
resolve? A finding means "unavailable when driven," not "this tool does not exist" — a dispatched
subagent inherits its parent's MCP config, and .mcp.json is gitignored so it cannot be the
baseline.
Testing
Execution evidence:
$ bun test --preload ./tests/setup.ts --timeout=15000 scripts/lib/agent-tool-provisioning.test.ts
bun test v1.3.14 (0d9b296a)
10 pass
0 fail
15 expect() calls
Ran 10 tests across 1 file. [78.00ms]
AT1 — "for each of the three mechanisms it names one of {subsumed, retained-as-storage,
separate} and a reason; no mechanism is left unclassified." Discharged by ADR-043 §Decision:
separate / retained-as-storage / the source, each with its reason. Also recorded per-criterion in
the task spec's ## Outcome.
AT2 — "construct the incoherence deliberately: declare a tool whose server the substrate does not
provision, and confirm the chosen design surfaces it rather than failing at tool-call time." Run as
the negative control below.
Negative control — AT2, the gate must be able to FAIL:
$ MINSKY_COCKPIT_DRIVEN_SESSION_MCP_SERVERS=minsky bun run scripts/check-agent-tool-provisioning.ts
2 declared tool(s) name an MCP server a driven session does not provision.
Driven-session server set: minsky
server "github" is not provisioned:
auditor: mcp__github__get_file_contents
reviewer: mcp__github__get_file_contents
EXIT=1
$ bun run check:agent-tool-provisioning # shipped default set
All declared MCP tools resolve against the driven-session server set: minsky, github
EXIT=0
That reproduces the exact pre-mt#4239 incoherence, and confirms the config read is live rather than
silently falling back to defaults — a bug caught in this session by running the gate end-to-end
after its unit tests already passed.
SC5 — the mismatch is detectable: the two runs above. SC1/SC3/SC4 are verdict-shaped and
discharged in ADR-043 (§Decision, §"What the harness actually offers", §"The registry compiles to
three surfaces"). SC2 (retirement path) is not applicable under this verdict — C1 does not
subsume the cockpit config key, so nothing is retired; recorded rather than skipped.
validate_typecheck: 0 errors across 8 projects including tsconfig.scripts.json.
validate_lint: 0 errors, 0 warnings across 3763 files.
Round 1 (reviewer findings addressed)
The R1 review concluded REQUEST_CHANGES with an EMPTY structured-findings channel — its only
"finding" was the synthesized Reviewer concluded REQUEST_CHANGES but emitted no structured findings. The substance was in the conclusion prose, and both blocking items were legitimate:
- Crashed when
.minsky/agents/ was missing — reachable now that the check runs in
validate-all. Guarded with an early return that NAMES the missing path, so a corpus that was
never read cannot look like one that read clean.
import() of an absolute filesystem path — not a valid ESM specifier; resolution is
runtime-specific and a Windows drive letter reads as a protocol. Converted via pathToFileURL.
Both verified live, not by inspection:
$ mv .minsky/agents .minsky/agents-tmp && bun run scripts/check-agent-tool-provisioning.ts; \
mv .minsky/agents-tmp .minsky/agents
No agent definitions with a declared tool list under <repo>/.minsky/agents — nothing to check.
EXIT_WITH_DIR_MISSING=0
The file:// fix is proven by the gate importing all seven agent modules on the normal path.
Non-blocking, also applied: the always-provisioned server name now comes from the resolver's own
DRIVEN_SESSION_MCP_SERVER_NAME rather than a second literal, and ADR-043 records that
.minsky/agents/*/agent.ts is authoritative for tools: while .claude/agents/*.md is its
generated output. Declined, with the trade recorded in the code: replacing the module import
with a lighter text parse would mean regex-ing TypeScript, reintroducing exactly the source-of-truth
drift this gate exists to catch.
Deploy verification
package.json is the only deploy-surface file touched, and the change is two script entries —
check:agent-tool-provisioning plus its addition to validate-all. No dependency, engine, or start
command changed, and no Dockerfile executes either script (the sole reference under Dockerfile /
services/*/Dockerfile is a comment on line 160 mentioning check:bun-build-sync).
Committing to verify rather than claiming an exemption: after merge I will run
mcp__minsky__deployment_wait-for-latest with notBefore set to the merge timestamp and confirm
SUCCESS plus a health-body service identity, per /implement-task §10. A tool or auth flake there
is a blocker to resolve, not grounds to defer.
CI note
test-forced-tz failed once on this head and passed on a re-run of the same commit
(parseElapsedSeconds in src/cockpit/port-incumbent.test.ts:176 — Expected: < 60000, Received: 38109073018720000, 14197 pass / 1 fail). Same commit, same base, opposite result, and that check is
green on current main; this diff touches no cockpit code.
Already owned by mt#4260 ("Flaky: port-incumbent's live-process start-time test failed in CI with
a 1.2-million-year age, then passed on an unchanged re-run", IN-REVIEW) — that 1.2-million-year age
is this same value. Not re-filed; this run is a second occurrence of that task's defect.
Notes
Gate (h) caught a factual error in the spec during planning: it claimed the config key had "two call
sites," and it has five plus an env-var registration. Corrected in the spec; it changes nothing
here because nothing is being retired, but an implementer working from the old number would have
missed three spawn sites.
This PR also corrected two of my own earlier claims in the task record — the first probe pair varied
two axes at once and I attributed the difference to the wrong one. Both corrections are recorded in
the spec rather than edited away.
Out of scope, deliberately: building C1 itself (RFC Phase 3b — this task's output is its input), the
OAuth-integrated C2, and any gateway or broker adoption.
Co-Authored-By: minsky-ai[bot] <minsky-ai[bot]@users.noreply.github.com>
What this means
A push to main triggered CI and the workflow above did not conclude success. Per
CLAUDE.md user preference ("main must never be broken"), this is severity-1.
Diagnostic checklist
- Open the failing run URL above; identify which job/step failed.
- Check whether the offending PR was merged with a known-failing required check
(operator-API bypass via gh api PUT /merge despite enforce_admins).
- Confirm
enforce_admins is currently enabled:
gh api repos/edobry/minsky/branches/main/protection --jq .enforce_admins.enabled
Expected: true post-mt#1938. If false, that is itself a separate finding.
Recovery
- Open a hotfix branch off current
main.
- Apply the smallest fix that turns CI green (often a formatter pass or a config
flip).
- Land via the standard Minsky session flow:
tasks_create → session_start → session_commit → session_pr_create → /review-pr → session_pr_merge.
- Verify the post-merge
main build is green within ~5 minutes.
- Close this issue with a link to the hotfix PR.
Cross-references
P0: main went red
Workflow:
CIconcluded failure onmain.Failing run: https://github.com/edobry/minsky/actions/runs/32184436800
HEAD SHA:
5163d9a5310e9fcf3d98fb20bae145e759ff571aHead commit: feat(mt#4245): Reconcile the three agent tool-surface mechanisms into one registry (ADR-043)
Summary
Minsky had three mechanisms independently answering "what tools does this agent have," and an
Accepted RFC (390937f0, Piece C1) that already decided the question. This is the reconciliation
verdict, plus the one piece of it that ships without waiting for the registry.
The verdict (ADR-043, Proposed): build the capability registry as data plus a compiler, and make
the two shipped mechanisms its compilation targets. Do not build or adopt an MCP gateway.
cockpit.drivenSession.mcpServers→ retained as separate (it answers "what does thisinstallation provide," which is not C1's question)
tools:→ retained as storage (harness-native; validated against, latergenerated from, the registry)
The finding that changed the design
An advisory recorded in mt#4245 proposed enforcing tool masks with
--disallowedTools(Stage 1) andtriggering escalation by harvesting
permission_denials(Stage 2). Both premises were relayed andunrun. Probing them falsified the composition:
Bash(rm *))permission_denialsin the result JSON--disallowedToolsand settingspermissions.denyare equivalent; the axis is bare-vs-scoped. AndMCP rules have no scoped form — per
code.claude.com/docs/en/permissions, an MCP rule is aserver name "optionally followed by the name of a tool from that server."
So
permission_denialscan never contain an MCP tool. Every motivating incident in RFC 390937f0 isan MCP tool (
reviewer_retrigger,deployment_status), which means the proposed harvest would havecovered none of them — silently, at exit 0,
is_error: false. The escalation signal has to comefrom mt#2588's tool-emitted
CAPABILITY_REQUIREDinstead, and third-party MCP tools can emitneither signal. That gap is the honest trigger for a broker; ADR-043 records it as knowingly
uncovered rather than solved.
Key changes
docs/architecture/adr-043-agent-tool-surface-registry.md— the verdict, decision-firstscripts/lib/agent-tool-provisioning.ts— pure core: parse MCP server names, cross-referencedeclarations against the provisioned set, format findings
scripts/check-agent-tool-provisioning.ts— the gate (exit 1 on mismatch)package.json—check:agent-tool-provisioning, added tovalidate-allThe check answers: if this agent type ran inside a driven session, would its declared tools
resolve? A finding means "unavailable when driven," not "this tool does not exist" — a dispatched
subagent inherits its parent's MCP config, and
.mcp.jsonis gitignored so it cannot be thebaseline.
Testing
Execution evidence:
AT1 — "for each of the three mechanisms it names one of {subsumed, retained-as-storage,
separate} and a reason; no mechanism is left unclassified." Discharged by ADR-043 §Decision:
separate / retained-as-storage / the source, each with its reason. Also recorded per-criterion in
the task spec's
## Outcome.AT2 — "construct the incoherence deliberately: declare a tool whose server the substrate does not
provision, and confirm the chosen design surfaces it rather than failing at tool-call time." Run as
the negative control below.
Negative control — AT2, the gate must be able to FAIL:
That reproduces the exact pre-mt#4239 incoherence, and confirms the config read is live rather than
silently falling back to defaults — a bug caught in this session by running the gate end-to-end
after its unit tests already passed.
SC5 — the mismatch is detectable: the two runs above. SC1/SC3/SC4 are verdict-shaped and
discharged in ADR-043 (§Decision, §"What the harness actually offers", §"The registry compiles to
three surfaces"). SC2 (retirement path) is not applicable under this verdict — C1 does not
subsume the cockpit config key, so nothing is retired; recorded rather than skipped.
validate_typecheck: 0 errors across 8 projects includingtsconfig.scripts.json.validate_lint: 0 errors, 0 warnings across 3763 files.Round 1 (reviewer findings addressed)
The R1 review concluded REQUEST_CHANGES with an EMPTY structured-findings channel — its only
"finding" was the synthesized
Reviewer concluded REQUEST_CHANGES but emitted no structured findings. The substance was in the conclusion prose, and both blocking items were legitimate:.minsky/agents/was missing — reachable now that the check runs invalidate-all. Guarded with an early return that NAMES the missing path, so a corpus that wasnever read cannot look like one that read clean.
import()of an absolute filesystem path — not a valid ESM specifier; resolution isruntime-specific and a Windows drive letter reads as a protocol. Converted via
pathToFileURL.Both verified live, not by inspection:
The
file://fix is proven by the gate importing all seven agent modules on the normal path.Non-blocking, also applied: the always-provisioned server name now comes from the resolver's own
DRIVEN_SESSION_MCP_SERVER_NAMErather than a second literal, and ADR-043 records that.minsky/agents/*/agent.tsis authoritative fortools:while.claude/agents/*.mdis itsgenerated output. Declined, with the trade recorded in the code: replacing the module import
with a lighter text parse would mean regex-ing TypeScript, reintroducing exactly the source-of-truth
drift this gate exists to catch.
Deploy verification
package.jsonis the only deploy-surface file touched, and the change is two script entries —check:agent-tool-provisioningplus its addition tovalidate-all. No dependency, engine, or startcommand changed, and no Dockerfile executes either script (the sole reference under
Dockerfile/services/*/Dockerfileis a comment on line 160 mentioningcheck:bun-build-sync).Committing to verify rather than claiming an exemption: after merge I will run
mcp__minsky__deployment_wait-for-latestwithnotBeforeset to the merge timestamp and confirmSUCCESS plus a health-body service identity, per
/implement-task§10. A tool or auth flake thereis a blocker to resolve, not grounds to defer.
CI note
test-forced-tzfailed once on this head and passed on a re-run of the same commit(
parseElapsedSecondsinsrc/cockpit/port-incumbent.test.ts:176—Expected: < 60000, Received: 38109073018720000, 14197 pass / 1 fail). Same commit, same base, opposite result, and that check isgreen on current
main; this diff touches no cockpit code.Already owned by mt#4260 ("Flaky: port-incumbent's live-process start-time test failed in CI with
a 1.2-million-year age, then passed on an unchanged re-run", IN-REVIEW) — that 1.2-million-year age
is this same value. Not re-filed; this run is a second occurrence of that task's defect.
Notes
Gate (h) caught a factual error in the spec during planning: it claimed the config key had "two call
sites," and it has five plus an env-var registration. Corrected in the spec; it changes nothing
here because nothing is being retired, but an implementer working from the old number would have
missed three spawn sites.
This PR also corrected two of my own earlier claims in the task record — the first probe pair varied
two axes at once and I attributed the difference to the wrong one. Both corrections are recorded in
the spec rather than edited away.
Out of scope, deliberately: building C1 itself (RFC Phase 3b — this task's output is its input), the
OAuth-integrated C2, and any gateway or broker adoption.
Co-Authored-By: minsky-ai[bot] <minsky-ai[bot]@users.noreply.github.com>
What this means
A push to
maintriggered CI and the workflow above did not conclude success. PerCLAUDE.md user preference ("main must never be broken"), this is severity-1.
Diagnostic checklist
(operator-API bypass via
gh api PUT /mergedespiteenforce_admins).enforce_adminsis currently enabled:truepost-mt#1938. Iffalse, that is itself a separate finding.Recovery
main.flip).
tasks_create → session_start → session_commit → session_pr_create → /review-pr → session_pr_merge.mainbuild is green within ~5 minutes.Cross-references
.claude/hooks/SPEC.md§Layered enforcement model — three-layer model.github/workflows/main-watch.yml.