Skip to content

Commit 8e63265

Browse files
committed
fix(mcp): enforce persistence kernel boundary
Cause: The v0.24 kernel still discarded rejected semantic attempts, exposed misleading state-commit metadata, let retired workflow authority affect current artifacts, and allowed delegated review/debug agents to own source remediation. Scope: Persist bounded rejected-attempt audit history outside semantic digests, expose projection-only commit metadata, make the production Change facade single-mode, keep legacy workflow rows read-only, and constrain review/debug agents to evidence artifacts. Release as 0.24.2. Verification: npm run verify:release npm pack --dry-run --json git diff --cached --check Residual risk: The pre-v0.24 Change implementation remains packaged only as an internal compatibility layer for existing state and recovery paths; public tools cannot select it or create new legacy authority. Rollback: Revert this commit or reset to backup/v024-five-boundaries-20260731 before publication.
1 parent 9e8787f commit 8e63265

28 files changed

Lines changed: 3259 additions & 2925 deletions

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.24.2] — 2026-07-31
11+
12+
### Changed
13+
14+
- Made the production Change facade single-mode and Kernel-only. The pre-v0.24
15+
Change implementation is explicitly isolated for compatibility, while retired
16+
workflow and dialogue rows remain read-only history with no public artifact owner
17+
or invalidation writer.
18+
- Made review and debug agents evidence-only: delegated workers can write only their
19+
assigned report, while the primary host owns source remediation and final judgment.
20+
21+
### Fixed
22+
23+
- Preserved rejected semantic calls as bounded `ultra_kernel_attempt` audit history
24+
visible through `ultra.context`, without creating the rejected semantic authority.
25+
- Renamed misleading `_ultra.state_commit` response metadata to
26+
`_ultra.projection_commit`; projection processing no longer resembles a semantic
27+
acceptance receipt.
28+
- Removed current Change-packet decisions and artifact invalidation from retired
29+
workflow/dialogue tables while retaining safe migration and historical reads.
30+
1031
## [0.24.1] — 2026-07-31
1132

1233
### Changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ Retired fine-grained operation names are neither discoverable nor callable. Work
132132
prose stays in Skills, model judgment stays with the host, and SQLite records what
133133
happened rather than deciding what the model is allowed to think next.
134134

135+
Rejected semantic attempts remain visible as bounded `ultra_kernel_attempt` audit
136+
diagnostics in the next `ultra.context`; they do not create rejected authority.
137+
`_ultra.projection_commit` describes only generated-view processing, never semantic
138+
acceptance. The production Change API has no legacy/current mode switch.
139+
135140
The enforcement gradient is:
136141

137142
```text
@@ -404,6 +409,11 @@ recommends the next capability from current context and owner intent; SQLite doe
404409
encode the semantic route. Another public capability starts only after an explicit
405410
user command or skill invocation.
406411

412+
Review and debug agents are evidence-only workers. They may inspect the assigned
413+
checkout and write their declared report, but they never edit source or commit
414+
authority. The primary host evaluates the evidence, applies any repair, and records
415+
the verified outcome.
416+
407417
### Command interaction graph
408418

409419
Every solid handoff below means: the current capability returns context and checkpoint

adapters/tests/opencode.test.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const opencode = require('../opencode.js');
1212
const { parse: parseFm } = require('../_shared/frontmatter.cjs');
1313
const { CORE_PUBLIC_SKILLS, skillsForRuntime } = require('../_shared/runtime-assets.cjs');
1414
const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
15-
const changes = require('../../mcp-server/lib/change-workflow.cjs');
15+
const changes = require('../../mcp-server/lib/legacy-change-workflow.cjs');
1616
const { createTask } = require('../../mcp-server/lib/state-ops.cjs');
1717
const { seedReadyBaseline } = require('../../mcp-server/test-support/ready-baseline.cjs');
1818
const { completeChangeInput } = require('../../mcp-server/test-support/change-contract.cjs');

agents/code-reviewer.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: code-reviewer
33
description: Review an explicit diff or file set for consequential correctness, security, integration, test, and maintainability defects. Use for a bounded read-only review before commit, merge, or release.
4-
tools: Read, Grep, Glob, Bash, Write, Edit
4+
tools: Read, Grep, Glob, Bash, Write
55
model: opus
66
maxTurns: 30
77
skills:
@@ -34,10 +34,9 @@ evidence. Default to read-only analysis.
3434

3535
## Mutation boundary
3636

37-
Do not edit during an ordinary review. If the parent explicitly assigns review and
38-
fix, modify only findings within the accepted scope, preserve unrelated changes, and
39-
rerun the checks invalidated by each edit. Judgment-heavy or scope-expanding fixes
40-
return to the primary agent for a decision.
37+
Do not edit source. Write only the assigned evidence artifact. If remediation is
38+
authorized, return the smallest verified repair instructions to the primary host,
39+
which owns implementation and final judgment.
4140

4241
Use the current checkout and parent-supplied packet only. Do not call Ultra MCP write
4342
tools. Return a concise review, not raw tool output.

agents/debugger.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: debugger
3-
description: Find the earliest incorrect state behind an error, test failure, or unexpected behavior and return a minimal verified fix when implementation is authorized.
4-
tools: Read, Write, Edit, Bash, Grep, Glob
3+
description: Find the earliest incorrect state behind an error, test failure, or unexpected behavior and return bounded evidence plus a minimal verified remediation.
4+
tools: Read, Write, Bash, Grep, Glob
55
model: opus
66
maxTurns: 40
77
---
@@ -24,14 +24,14 @@ that violates the intended contract.
2424
distinguishes it from alternatives.
2525
5. Test the hypothesis without broad refactoring. Record evidence that accepts or
2626
rejects it.
27-
6. If implementation is authorized, write a regression test that fails for the
28-
observed defect, apply the minimum root-cause fix, and rerun focused and adjacent
29-
checks.
30-
7. Report the symptom, evidence trail, root cause, changed files when any, exact
27+
6. Describe the smallest regression test and root-cause repair that the primary host
28+
should apply. Do not edit source.
29+
7. Report the symptom, evidence trail, root cause, proposed files, exact diagnostic
3130
verification, and residual uncertainty.
3231

33-
Do not change code when the assignment is diagnosis-only. Do not use a passing test
34-
from another checkout as evidence.
32+
Write only the assigned evidence artifact. Do not change source, even when remediation
33+
is authorized; the primary host owns implementation and final judgment. Do not use a
34+
passing test from another checkout as evidence.
3535

3636
If three distinct fix attempts expose different underlying failures, stop patching and
3737
return the evidence as an architectural boundary problem. For an Ultra incident, also

docs/AGENT-CONTEXT.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Ultra owns:
1111
`ultra-change`, `ultra-plan`, `ultra-dev`, `ultra-test`, `ultra-review`,
1212
`ultra-deliver`, `ultra-status`, and `ultra-doctor`;
1313
- four internal worker-rule Skills;
14-
- bounded review/debug workers;
14+
- bounded evidence-only review/debug workers;
1515
- deterministic workflow Hooks;
1616
- exactly seven public MCP tools;
1717
- `.ultra` project authority, team sync, recovery, and the portable installer.
@@ -52,6 +52,12 @@ diagnostics and leaves the draft editable. Hard errors are limited to corruption
5252
unsafe paths, digest/CAS/lease conflicts, missing runtime prerequisites, permissions,
5353
and irreversible external effects.
5454

55+
A rejected semantic attempt is appended as `ultra_kernel_attempt` audit history and
56+
appears alongside the next `ultra.context` envelope without changing its semantic
57+
digest. It never creates the rejected semantic row or claims that authority was
58+
committed. `_ultra.projection_commit` reports only the post-call generated-view cycle;
59+
it is not a semantic acceptance receipt.
60+
5561
## Context Envelope
5662

5763
Every Skill begins by reading `ultra.context` for the relevant stage and scope. The
@@ -121,9 +127,10 @@ The packet binds:
121127
- exact output path;
122128
- `packet_digest`.
123129

124-
Workers may inspect and write only the declared output. They do not write SQLite,
125-
accept checkpoints, modify another worker's result, or decide final delivery. Output
126-
must echo `packet_digest`; the primary host verifies and records it.
130+
Workers may inspect the assigned checkout and write only the declared evidence output.
131+
They do not edit source, write SQLite, accept checkpoints, modify another worker's
132+
result, or decide final delivery. Output must echo `packet_digest`; the primary host
133+
verifies it, performs any source repair, and records the resulting evidence.
127134

128135
## Hook boundary
129136

@@ -163,3 +170,8 @@ state.
163170

164171
Every semantic file must have one writer, owner, consumer, digest, promotion gate, and
165172
archive rule as defined in `ARTIFACT-AUTHORITY.md`.
173+
174+
The production Change API has one Kernel behavior and exposes no legacy/current mode
175+
switch. Pre-v0.24 workflow and dialogue rows remain read-only migration/audit history:
176+
the public kernel cannot create workflow-owned artifacts or mutate those rows during
177+
current invalidation.

docs/ARCHITECTURE.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ There is no second semantic authority:
4343
- MCP owns structure validation, idempotency, digests, CAS, leases, safe paths,
4444
filesystem journals, recovery, and accepted checkpoints.
4545
- Hooks own deterministic observation and bounded injection only.
46-
- Agents receive one immutable Worker Packet and write only the declared output.
46+
- Agents receive one immutable Worker Packet and write only the declared evidence
47+
output; the primary host owns every source change and final judgment.
4748

4849
SQLite never chooses the next capability and never requires a fixed sequence of
4950
reasoning steps. Semantic gaps appear in `diagnostics`, `warnings`, or
@@ -66,9 +67,11 @@ Every host discovers exactly seven tools:
6667
| `ultra.doctor` | Mechanical diagnosis and backup-first repair |
6768

6869
Retired fine-grained tool names are not registered and return `UNKNOWN_TOOL`. The
69-
source repository retains migration readers and regression fixtures for old authority,
70-
but those modules are excluded from the npm distribution and have no live public
71-
consumer.
70+
source repository retains migration readers, an explicitly named internal Change
71+
compatibility implementation, and regression fixtures for old authority. The
72+
production Change facade exposes one Kernel behavior with no mode flag; retired
73+
semantic supervisors are excluded from the npm distribution and no public call can
74+
write their workflow/dialogue authority.
7275

7376
## Canonical Context Envelope
7477

@@ -130,6 +133,20 @@ Every delegated worker receives a digest-bound packet containing the exact Conte
130133
Envelope, accepted decisions, Git boundary, Task contract, acceptance, evidence
131134
references, output path, and output schema. The worker must echo `packet_digest`.
132135
Workers do not write SQLite or accept their own result.
136+
Review and debug workers do not edit source; their only mutation is the assigned
137+
evidence artifact, and the primary host applies any accepted remediation.
138+
139+
### Rejected attempts and projection metadata
140+
141+
Semantic diagnostics do not disappear when a caller corrects and retries. A rejected
142+
public call appends an `ultra_kernel_attempt` event containing the typed operation,
143+
scope, idempotency key, blockers, and diagnostics without storing raw prompts or
144+
creating the rejected semantic row. `ultra.context` exposes a bounded recent view of
145+
that audit history alongside, but outside the digest-bound semantic envelope.
146+
147+
Server metadata uses `_ultra.projection_commit` only for the deterministic
148+
post-mutation projection job. It must never be interpreted as proof that a semantic
149+
record, checkpoint, or archive was accepted.
133150

134151
## `.ultra` storage planes
135152

@@ -210,7 +227,9 @@ Contexts, and v0.22/v0.23 database and ledger authority.
210227
Migration is inspectable, exact-byte backup-first, transactional, and fail-closed on
211228
conflict. Old workflow and dialogue rows are retained as non-authoritative history;
212229
current Context, Decisions, Checkpoints, and the team ledger are rebuilt from verified
213-
facts. No user should edit SQLite or move a legacy file manually to recover.
230+
facts. Current artifact invalidation may traverse legacy graph edges to reach a live
231+
Task, but it never rewrites the legacy workflow row. No user should edit SQLite or move
232+
a legacy file manually to recover.
214233

215234
## External boundaries
216235

docs/STATE-DB-ACCESS-POLICY.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ It does not grant lifecycle authority: external writers use the published observ
4040
allowlist, while lifecycle event
4141
names are emitted only inside the mutation that owns the corresponding state change.
4242
Consumers must verify mutable rows rather than infer success from an event name.
43+
Rejected semantic calls are recorded as `ultra_kernel_attempt` observations and shown
44+
by `ultra.context`; they do not create the rejected row or authorize a transition.
4345

4446
## 2. Connection discipline
4547

@@ -80,7 +82,7 @@ resolve the mount/runtime constraint before retrying.
8082
| `decision_records` | MCP server through `ultra.record`; normalized accepted intent and applied references are stored, never UI receipts, prompts, or transcripts |
8183
| `stage_checkpoints` | MCP server through `ultra.checkpoint`; mutable drafts and immutable accepted/superseded revisions, never fixed reasoning steps |
8284
| `context_envelopes`, `worker_packets` | MCP server through `ultra.context`, `ultra.checkpoint`, and `ultra.session`; exact digest-bound handoff authority |
83-
| `decision_threads`, `decision_items`, `workflow_runs`, `workflow_steps` | legacy history retained for migration and audit; no current public writer or authorization role |
85+
| `decision_threads`, `decision_items`, `workflow_runs`, `workflow_steps` | read-only legacy history retained for migration and audit; no current public writer, invalidation mutation, or authorization role |
8486
| `artifacts`, `artifact_edges` | MCP server through `ultra.record` and checkpoint-owned registration |
8587
| `context_snapshots`, `spec_learning_candidates`, `trace_links` | MCP server through `ultra.record` and `ultra.checkpoint` |
8688
| `incidents`, `projection_jobs`, `event_consumers`, `circuit_breaker` | MCP server; backup-first doctor recovery may perform only documented mechanical transitions |
@@ -124,6 +126,11 @@ performs semantic import, publication, or conflict selection.
124126
than the orchestrator's boot-time WAL detector.
125127
- **No raw SQL in command md files.** Commands call MCP tools or
126128
`ultra-tools …` subcommands; the SQL lives in `mcp-server/lib`.
129+
- **No legacy authority mode switch.** Public Change callers use the single Kernel
130+
facade. New workflow-owned artifacts and workflow/dialogue writes are rejected.
131+
- **No semantic meaning in projection metadata.** `_ultra.projection_commit` means
132+
only that the generated-view job was durably enqueued and processed; acceptance is
133+
read from the typed tool result and current authority.
127134
- **No `vacuum` / `wal_checkpoint(TRUNCATE)` from inside a transaction.**
128135
Maintenance subcommands open their own connection.
129136

mcp-server/lib/artifact-registry.cjs

Lines changed: 7 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,13 @@ function invalidateConsumersFromEndpointInTx(db, start, {
473473
const key = endpointKey(endpoint);
474474
if (seen.has(key) || excluded.has(key) || key === endpointKey(start)) continue;
475475
seen.add(key);
476+
if (endpoint.type === 'workflow') {
477+
// Workflow rows are retained only as pre-v0.24 audit history. Preserve
478+
// graph traversal for downstream current authorities, but never rewrite
479+
// the historical workflow row or claim that it was invalidated.
480+
queue.push(...outgoingEdges(db, endpoint));
481+
continue;
482+
}
476483
invalidated.push({ type: endpoint.type, id: endpoint.id });
477484
if (endpoint.type === 'artifact') {
478485
const row = db.prepare('SELECT status FROM artifacts WHERE id = ?').get(endpoint.id);
@@ -503,62 +510,6 @@ function invalidateConsumersFromEndpointInTx(db, start, {
503510
});
504511
}
505512
}
506-
if (endpoint.type === 'workflow') {
507-
const row = db.prepare(
508-
'SELECT status, metadata_json, change_id, task_id FROM workflow_runs WHERE id = ?',
509-
).get(endpoint.id);
510-
if (row) {
511-
const metadata = parseJson(row.metadata_json, {});
512-
if (['active', 'blocked', 'ready'].includes(row.status)) {
513-
delete metadata.authority_invalidation;
514-
metadata.draft_dirty = true;
515-
db.prepare(
516-
`UPDATE workflow_runs
517-
SET status = CASE WHEN status = 'ready' THEN 'active' ELSE status END,
518-
metadata_json = ?,
519-
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
520-
WHERE id = ?`,
521-
).run(JSON.stringify(metadata), endpoint.id);
522-
ops.appendEventInTx(db, {
523-
type: 'workflow_draft_changed',
524-
change_id: row.change_id,
525-
task_id: row.task_id,
526-
payload: {
527-
workflow_id: endpoint.id,
528-
source_type: start.type,
529-
source_id: start.id,
530-
reason,
531-
},
532-
});
533-
queue.push(...outgoingEdges(db, endpoint));
534-
continue;
535-
}
536-
if (metadata.authority_invalidation?.invalidated !== true) {
537-
metadata.authority_invalidation = {
538-
invalidated: true,
539-
source_type: start.type,
540-
source_id: start.id,
541-
reason,
542-
invalidated_at: new Date().toISOString(),
543-
};
544-
db.prepare(
545-
`UPDATE workflow_runs SET metadata_json = ?,
546-
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?`,
547-
).run(JSON.stringify(metadata), endpoint.id);
548-
ops.appendEventInTx(db, {
549-
type: 'workflow_invalidated',
550-
change_id: row.change_id,
551-
task_id: row.task_id,
552-
payload: {
553-
workflow_id: endpoint.id,
554-
source_type: start.type,
555-
source_id: start.id,
556-
reason,
557-
},
558-
});
559-
}
560-
}
561-
}
562513
queue.push(...outgoingEdges(db, endpoint));
563514
}
564515
return invalidated;

mcp-server/lib/artifact-registry.test.cjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ test('artifact.record replaces edges transactionally and invalidates only exact
195195
}
196196
});
197197

198-
test('updating an active draft artifact reopens its workflow instead of self-invalidating it', () => {
198+
test('updating a legacy workflow-owned artifact never mutates retired workflow history', () => {
199199
const fx = fixture();
200200
try {
201201
fx.db.prepare(
@@ -216,7 +216,7 @@ test('updating an active draft artifact reopens its workflow instead of self-inv
216216
}, { rootDir: fx.rootDir });
217217

218218
write(fx.rootDir, '.ultra/changes/active/draft/plan.md', '# Plan v2\n');
219-
artifacts.recordArtifact(fx.db, {
219+
const second = artifacts.recordArtifact(fx.db, {
220220
id: 'draft-plan',
221221
owner_type: 'workflow',
222222
owner_id: 'draft-workflow',
@@ -232,9 +232,10 @@ test('updating an active draft artifact reopens its workflow instead of self-inv
232232
"SELECT status, metadata_json FROM workflow_runs WHERE id = 'draft-workflow'",
233233
).get();
234234
const metadata = JSON.parse(row.metadata_json);
235-
assert.equal(row.status, 'active');
236-
assert.equal(metadata.draft_dirty, true);
235+
assert.equal(row.status, 'ready');
236+
assert.equal(metadata.draft_dirty, undefined);
237237
assert.equal(metadata.authority_invalidation, undefined);
238+
assert.deepEqual(second.invalidated, []);
238239
} finally {
239240
cleanup(fx);
240241
}

0 commit comments

Comments
 (0)