Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
7a8ac47
docs(roadmap): record PR #82 as merged and open the durability spine
cemililik Aug 11, 2026
921c097
docs(roadmap): fold the W1 evidence audit — correct seven findings be…
cemililik Aug 11, 2026
2e183d4
test(core): fail loud on an unscripted provider call in the agent-tur…
cemililik Aug 11, 2026
9ca4f2d
docs(decisions): add ADR-0078 — ordered durable append and the termin…
cemililik Aug 11, 2026
902696e
feat(core): add the append-audit harness — the instrument CR-10 is pr…
cemililik Aug 11, 2026
cc983b0
fix(core,docs): fold the Opus review — the harness could not see the …
cemililik Aug 11, 2026
cfc023a
fix(core,docs): fold the Sonnet review — the harness could poison its…
cemililik Aug 11, 2026
1223683
feat(core,db,shared): CR-10 — the durable append is ordered and guarded
cemililik Aug 11, 2026
4a822ab
fix(core,db,docs): fold the CR-10 Opus review — the harness had the g…
cemililik Aug 11, 2026
55e305e
fix(core): fold the CR-10 Sonnet review — two tests I credited did no…
cemililik Aug 11, 2026
483618c
feat(core,cli,shared): CR-92 — a terminal that cannot be made durable…
cemililik Aug 12, 2026
953e6f1
fix(cli,core): fold the CR-92 Opus review — the mechanism was not rea…
cemililik Aug 12, 2026
5eb0ab5
feat(cli,core,docs): CR-92 — the surface now says uncertain, and the …
cemililik Aug 12, 2026
8019b81
test(cli): certify CR-10 and CR-92 against the real history.db
cemililik Aug 12, 2026
0765d0b
fix(cli,core): fold the CR-92 Sonnet review — the retry path had no c…
cemililik Aug 12, 2026
a17cde4
docs(decisions): add ADR-0079 — cross-process run ownership via a lea…
cemililik Aug 12, 2026
67a75f2
docs(decisions): fold the ADR-0079 review — three clarity notes
cemililik Aug 12, 2026
28fc989
feat(db): CR-11 (a) — the run_leases table and its store operations
cemililik Aug 12, 2026
c999f4e
feat(db,shared): CR-11 (b) — the fence is checked on every durable write
cemililik Aug 12, 2026
dec917c
feat(core,db,cli): CR-11 (c) — the lease port, and a resume that acqu…
cemililik Aug 12, 2026
f7fe631
feat(core,cli): CR-11 (d) — the heartbeat, the fence classification, …
cemililik Aug 12, 2026
6ab36b9
feat(core,db,cli): CR-11 (e) — reconcile stops killing other people's…
cemililik Aug 12, 2026
80d2b9f
fix(core,cli,docs): CR-11 (f) — reconcile ownership at the write, not…
cemililik Aug 17, 2026
cf93e32
fix(core): CR-11 (g) — a claim that leads nowhere is never leaked, an…
cemililik Aug 17, 2026
636f0c3
fix(cli,docs): CR-11 (h) — a fenced run says so, instead of reporting…
cemililik Aug 17, 2026
1f5178c
fix(core,docs): CR-11 (i) — the heartbeat stops lying, and ADR-0079 s…
cemililik Aug 18, 2026
11026af
build(repo): CR-11 (j) — the two-process proof actually runs, and dis…
cemililik Aug 18, 2026
6e12e6b
fix(cli,db,docs): CR-11 (k) — the fenced-run classifier stops being f…
cemililik Aug 18, 2026
b7d2e79
test(core,cli): CR-11 (l) — pin the exit-6 chain, the wiring guard, a…
cemililik Aug 18, 2026
ffe66fa
docs(decisions): add ADR-0080 — a durable effect journal and a tiered…
cemililik Aug 18, 2026
de4e19c
fix(core,docs): CR-95 — a budget pause after a tool round fails close…
cemililik Aug 18, 2026
82cf882
feat(shared,cli): CR-12 (a) — the effect journal's five identities an…
cemililik Aug 18, 2026
6e2184f
feat(db): CR-12 (b) — the run_effects table and its journal store
cemililik Aug 18, 2026
6273f96
feat(core,shared,mcp): CR-12 (c) — the journal reaches the dispatch, …
cemililik Aug 18, 2026
9674e1b
feat(core,mcp): CR-12 (d) — which dispatches are journaled, declared …
cemililik Aug 18, 2026
7d119b0
feat(core,cli,db): CR-12 (e) — the prepare/settle bracket, and a slot…
cemililik Aug 18, 2026
55db8bf
fix(core,cli): CR-12 (f) — the guard now gates, the journal reaches e…
cemililik Aug 18, 2026
239c675
fix(core,db,docs): CR-12 (g) — the docs stop describing a gate that i…
cemililik Aug 18, 2026
e4df969
fix(core,db,cli): CR-12 (h) — a secret stops reaching the durable dig…
cemililik Aug 18, 2026
2817255
feat(core,db,cli): CR-12 (i) — the resume gate, so a crashed effect s…
cemililik Aug 18, 2026
b395cdc
fix(core,db,cli): CR-12 (j) — exit 7 was dead, a replay could answer …
cemililik Aug 18, 2026
32a7085
fix(core,db,cli): CR-12 (k) — the gate covers all three resumes, sess…
cemililik Aug 18, 2026
eff4614
docs(decisions): revise ADR-0081 against the maintainer's review — cl…
cemililik Aug 18, 2026
58d349a
fix(core,db): CR-12 (l) — the top-level secret leak my own test misse…
cemililik Aug 18, 2026
76b53b4
docs(decisions): accept ADR-0081, and record the supersession where i…
cemililik Aug 18, 2026
3c87fbb
feat(core): CR-13 — the compaction summary leaves `system`, and the t…
cemililik Aug 18, 2026
ad01a16
fix(core,repo): CR-13 (b) — close the fence's one-hop evasion, and pi…
cemililik Aug 18, 2026
4e03067
refactor(repo): CR-13 (c) — verify the prompt fence the way this repo…
cemililik Aug 18, 2026
b2e4376
fix(repo,docs): CR-13 (d) — close the type-predicate forgery, and cor…
cemililik Aug 18, 2026
78d66d8
docs(decisions): rewrite ADR-0082 against the maintainer's review — r…
cemililik Aug 18, 2026
eb2537a
fix(core,shared,docs): CR-13 (e) — make the user-first guarantee the …
cemililik Aug 18, 2026
6a1924c
docs(decisions): accept ADR-0082, and amend ADR-0011 with what the se…
cemililik Aug 18, 2026
510db7a
feat(llm,core): CR-14 (a) — rule 7's missing half, so a committed fai…
cemililik Aug 18, 2026
7fdcd8e
docs(standards,roadmap): land ADR-0082's obligations that are true today
cemililik Aug 18, 2026
2ff88f6
feat(llm): CR-14 (b) — the stream grammar verifier, with the terminal…
cemililik Aug 18, 2026
c304c74
fix(llm,core,docs): CR-14 (c) — commitment is a TURN fact, and a prov…
cemililik Aug 18, 2026
5fd9033
feat(llm): CR-21 (a) — the attempt deadline scope, hard-raced rather …
cemililik Aug 18, 2026
e7a974b
fix(llm): CR-21 (a) — drop two needless `async` markers the lint caught
cemililik Aug 18, 2026
237db43
fix(llm,docs): CR-14 (d) — the verifier stopped dropping a valid term…
cemililik Aug 18, 2026
ef73d0a
feat(llm,core): CR-14 + CR-21 (e) — the verifier and the deadline are…
cemililik Aug 18, 2026
c42ce80
feat(core,cli,docs): CR-21 (b) — the deadline reaches production, and…
cemililik Aug 18, 2026
69e6a86
fix(llm,cli,docs): CR-14 + CR-21 (f) — two blockers on the deadline p…
cemililik Aug 19, 2026
84718e8
docs(decisions): rewrite ADR-0083 against the maintainer's review — f…
cemililik Aug 19, 2026
1ee552c
docs(decisions): accept ADR-0083, and record its two parse-time amend…
cemililik Aug 19, 2026
06c4a87
feat(shared,core,docs): CR-15 (a) — the parse-time half, and three se…
cemililik Aug 19, 2026
1a10a8d
docs(decisions): narrow ADR-0083 §6's secret claim to the case it act…
cemililik Aug 19, 2026
dee068c
fix(cli,llm): CR-14 + CR-21 (g) — the unref guard was testing Node, n…
cemililik Aug 19, 2026
e9cde2b
fix(shared,core,docs): CR-15 (b) — the default validation I claimed a…
cemililik Aug 19, 2026
9ed8f16
feat(core): CR-15 (c) — the admission gate, so a declared default fin…
cemililik Aug 19, 2026
758522b
feat(core): CR-17 (a) — the checkpoint carries what the run was admit…
cemililik Aug 19, 2026
a77c968
fix(shared,core,cli,docs): CR-15 review fold — a format table with pr…
cemililik Aug 19, 2026
81211e4
feat(core): CR-17 (b1) — the resume stops taking the caller's word fo…
cemililik Aug 19, 2026
630c3b6
fix(cli,docs): CR-17 (b2a) — the durable snapshot froze a graph that …
cemililik Aug 19, 2026
d141f87
feat(core,db,docs): CR-17 (b2b) — the same slug is not the same graph
cemililik Aug 19, 2026
ffbb4cd
fix(core): CR-17 (b2b) — the mask is not a credential
cemililik Aug 19, 2026
bb5657b
fix(core,shared): CR-15 Sonnet fold — the LISTING is caller code too
cemililik Aug 19, 2026
3d4ba36
fix(core,shared,docs): CR-17 Opus fold — a legacy run that could neve…
cemililik Aug 19, 2026
06d8569
feat(cli,docs): CR-17 (c) — a secret-bearing run can be resumed at all
cemililik Aug 19, 2026
fda9ba9
docs(roadmap): close CR-15 and CR-17, and record what shipped against…
cemililik Aug 19, 2026
d748475
fix(cli,core,shared,db,docs): CR-17 second Opus fold — a red branch, …
cemililik Aug 19, 2026
9278f59
fix(cli,docs): CR-17 Sonnet fold — a bug I said I fixed, one layer ab…
cemililik Aug 19, 2026
92c07c3
docs(decisions): accept ADR-0084, and amend the two ADRs it refines
cemililik Aug 19, 2026
1dfff4a
feat(shared,db,cli): CR-16 (a) — one canonical form and one env denyl…
cemililik Aug 19, 2026
6ff1cee
feat(cli): CR-16 (b) — the fingerprint names a file, and the grant lo…
cemililik Aug 19, 2026
b5fe93c
style: run prettier over every file this branch touched
cemililik Aug 19, 2026
82e9e44
fix(shared,cli,docs): CR-16 (a) Opus fold — the third time I put an a…
cemililik Aug 20, 2026
84d1425
feat(cli): CR-16 (c) — the gate, before any spawn
cemililik Aug 20, 2026
cd54f6d
fix(cli,db,docs): CR-16 Sonnet fold — resolving a path is not verifyi…
cemililik Aug 20, 2026
3e1bcac
fix(cli): CR-16 (c) — the gate covered `run` and nothing else
cemililik Aug 20, 2026
45dba66
fix(cli,shared,db): CR-16 (c) Opus fold — the gate is required, and t…
cemililik Aug 20, 2026
15270a7
fix(cli): CR-16 (c) Sonnet fold — the prompt names the file again, an…
cemililik Aug 20, 2026
02f22e1
docs: ADR-0084 §11 — the consent contract gets a canonical home, and …
cemililik Aug 20, 2026
c22a45b
docs(roadmap): W1 closing register — and two items that were done and…
cemililik Aug 20, 2026
372c509
test(core): the ReDoS test measures GROWTH, not the speed of the mach…
cemililik Aug 20, 2026
099e3cf
fix(core,shared,cli,db,mcp): the Sonar findings that were real, and t…
cemililik Aug 20, 2026
dc54c77
perf(shared): the interpolation detector was quadratic on an artifact…
cemililik Aug 20, 2026
569ed40
refactor(shared,core,cli): split the seven functions Sonar called too…
cemililik Aug 20, 2026
a19fa6a
fix(db,core,llm,cli): a real open-race behind a "flaky" test, and a R…
cemililik Aug 20, 2026
4e7aa7b
refactor(core): the last three complexity findings — two split, two d…
cemililik Aug 20, 2026
149fa51
chore(tools): mark the lint fixture's intentional forms NOSONAR
cemililik Aug 20, 2026
cf23e63
refactor(db): a narrowed local for the opened connection, not an opti…
cemililik Aug 20, 2026
44b5e26
fix(cli): the event-loop test needs the strip-types flag on this proj…
cemililik Aug 20, 2026
42db117
fix(core,llm,shared,tools): the Sonar findings that survived the last…
cemililik Aug 24, 2026
131f299
refactor(core,shared): the last reducible complexity finding, and two…
cemililik Aug 24, 2026
836c8c4
refactor(shared,core): hoist the two declined regexes to named consta…
cemililik Aug 24, 2026
d5f75fa
refactor(shared): String.raw for the format-check classes, pinned by …
cemililik Aug 24, 2026
0382f74
fix(db,core,shared): a durable append must be AHEAD of the log, not m…
cemililik Aug 24, 2026
2fbdf1c
fix(core): a legitimately `undefined` tool result must replay as `und…
cemililik Aug 24, 2026
40d0b7c
fix(llm): a caller cancellation must not wait out the 120s provider d…
cemililik Aug 24, 2026
9adb343
fix(db,core,shared): a settle that moves no row must fail, not report…
cemililik Aug 24, 2026
a28c04a
fix(core,db,shared): a proven non-dispatch must not leave a permanent…
cemililik Aug 24, 2026
8cc2be5
fix(cli,docs): make the exit-5 recovery instruction true (PR83-06)
cemililik Aug 24, 2026
6df4bb1
docs(reference): the schema reference contradicted the shipped retent…
cemililik Aug 24, 2026
1f92cd9
refactor(core): split `dispatch`'s journal steps out, and pin what on…
cemililik Aug 24, 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
39 changes: 33 additions & 6 deletions apps/cli/src/chat/agent-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,39 @@ export function resolveChatAgent(
agentRef: string | undefined,
opts: ResolveChatAgentOptions,
): AgentDefinition {
return resolveChatAgentSource(agentRef, opts).agent;
}

/** The resolved agent together with the FILE it came from — `undefined` for the built-in default. */
export interface ResolvedChatAgent {
readonly agent: AgentDefinition;
/**
* The path the agent was read from, for the MCP consent prompt.
*
* Not folded into `AgentDefinition`: that shape is the parsed artifact, is persisted into a session
* snapshot, and is produced by the pure core parser, which has no business carrying a host path. It
* travels beside the agent instead —
* [ADR-0084](../../../../docs/decisions/0084-consent-before-a-local-mcp-spawn.md) §7 requires the prompt
* to name the declaring file, and `chat --agent ./downloaded.agent.yaml` is precisely the imported-artifact
* case the gate exists for, so "the user typed a word" is not a good enough answer there.
*/
readonly artifact: string | undefined;
}

/** {@link resolveChatAgent}, plus the source path the consent prompt needs. */
export function resolveChatAgentSource(
agentRef: string | undefined,
opts: ResolveChatAgentOptions,
): ResolvedChatAgent {
if (agentRef === undefined) {
return buildDefaultChatAgent(
opts.defaultModel ?? DEFAULT_CHAT_MODEL,
opts.reasoningEffort,
opts.defaultProvider,
);
return {
agent: buildDefaultChatAgent(
opts.defaultModel ?? DEFAULT_CHAT_MODEL,
opts.reasoningEffort,
opts.defaultProvider,
),
artifact: undefined,
};
}
const source = resolveYamlSource(agentRef, {
cwd: opts.cwd,
Expand All @@ -44,5 +71,5 @@ export function resolveChatAgent(
projectConfigDir: opts.projectConfigDir,
idSuffixes: ['.agent.yaml', '.relavium.yaml', '.yaml'],
});
return parseAgent(source.yaml, { source: source.path });
return { agent: parseAgent(source.yaml, { source: source.path }), artifact: source.path };
}
11 changes: 8 additions & 3 deletions apps/cli/src/chat/persister.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { reconstructSessionState } from '@relavium/core';
import { reconstructSessionState, unwrapUntrusted } from '@relavium/core';
import type { StreamChunk } from '@relavium/llm';
import {
createClient,
Expand Down Expand Up @@ -860,10 +860,15 @@ describe('createSessionPersister', () => {
// The full transcript is preserved (append-only — nothing deleted): 4 real rows + 1 marker.
expect(messages).toHaveLength(5);

// Resume honors the marker: only the kept exchange survives, with the summary as the preamble.
// Resume honors the marker: only the kept exchange survives, and the summary comes back RE-MARKED
// untrusted (ADR-0081 §2) — persistence stores a raw string, and a value does not become trustworthy by
// having been stored, so the reconstruction boundary is where the brand is reapplied.
const full = store.loadFull('sess-1');
const state = reconstructSessionState(full!.session, full!.messages);
expect(state.contextPreamble).toBe('the summary text');
expect(state.compactionSummary).toBeDefined();
expect(state.compactionSummary && unwrapUntrusted(state.compactionSummary)).toBe(
'the summary text',
);
expect(state.messages).toEqual([
{ role: 'user', content: [{ type: 'text', text: 'q2' }] },
{ role: 'assistant', content: [{ type: 'text', text: 'a2' }] },
Expand Down
48 changes: 46 additions & 2 deletions apps/cli/src/chat/session-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
toolUseTurn,
unresolvedResolver,
} from './test-support.js';
import { createInMemoryEffectJournal } from '@relavium/core';

/** A tool-call turn that carries JSON args (the `toolUseTurn` helper sends none) — for read_file/write_file. */
const callWithArgs = (id: string, name: string, args: unknown): StreamChunk[] => [
Expand Down Expand Up @@ -101,18 +102,28 @@ function deterministicIds() {
return { now: () => tick++, uuid: () => 'sess-test-1' };
}

function build(overrides: Partial<Parameters<typeof buildChatSession>[0]> = {}) {
async function build(overrides: Partial<Parameters<typeof buildChatSession>[0]> = {}) {
const { now, uuid } = deterministicIds();
return buildChatSession({
const built = await buildChatSession({
chat: EMPTY_CHAT,
agentRef: undefined,
cwd: '/workspace',
projectConfigDir: undefined,
now,
uuid,
providers: scriptedResolver([textTurn('hello there')]),
// `connectAgentMcp` REFUSES a stdio declaration when no gate was wired (ADR-0084 §1) — the optionality
// that left four of the five entry points ungated. These cases are about host wiring, not consent, so the
// default says so explicitly; a case about the gate itself overrides it.
consentGate: () => Promise.resolve(new Map()),
...overrides,
});
// In production the persister attaches this once `history.db` is open. Attaching a REAL in-memory journal
// here rather than leaving it unwired: MCP tools are tier 3 (ADR-0080), so the MCP tests below genuinely
// dispatch effects, and the unwired port correctly refuses those — which would test the refusal instead of
// the routing each test is about.
built.attachEffectJournal((correlation) => createInMemoryEffectJournal(correlation));
return built;
}

describe('buildChatSession', () => {
Expand Down Expand Up @@ -419,6 +430,34 @@ describe('buildChatSession + MCP host wiring (2.R)', () => {
expect(closed).toBe(1);
});

it('names the RESOLVED agent file at the consent gate (ADR-0084 §7)', async () => {
// The prompt's "declared in <file>" line is what turns a consent decision about an opaque program into
// one about an artifact the user can go read — and `chat --agent ./downloaded.agent.yaml` is exactly the
// imported-artifact case the gate exists for. It has now died silently TWICE, at two different layers,
// because every test called the gate directly and none went through the surface that computes the value.
const agentPath = writeMcpAgent();
let asked = 0;
let seen: string | undefined;
await build({
agentRef: agentPath,
consentGate: (_refs, _cwd, artifact) => {
asked += 1;
seen = artifact;
return Promise.resolve(new Map());
},
startMcpClient: () =>
Promise.resolve({
capability: { call: () => Promise.resolve({ content: [], isError: false }) },
toolDefs: [],
toolIdsByServer: new Map(),
skipped: [],
close: () => Promise.resolve(),
}),
});
expect(asked).toBe(1); // else `seen === undefined` would pass for a gate that never ran
expect(seen).toBe(agentPath);
});

it('MERGE-not-replace: a session with MCP keeps the fs arm too — read_file AND an MCP tool both dispatch', async () => {
// The keystone 2.5.A fix (ADR-0055): the inbound-MCP arm is MERGED onto the factory fs+process host, never
// REPLACING it. Proven end-to-end: in ONE session, read_file routes via host.fs (real file) AND mcp_fs_read
Expand Down Expand Up @@ -585,6 +624,7 @@ describe('buildResumedChatSession (2.N)', () => {
messages,
now: () => Date.parse(ISO),
providers: scriptedResolver([textTurn('continued')]),
consentGate: () => Promise.resolve(new Map()), // see `build` above (ADR-0084 §1)
});
}

Expand Down Expand Up @@ -827,7 +867,10 @@ describe('buildResumedChatSession (2.N)', () => {
now: () => Date.parse(ISO),
providers: scriptedResolver([toolUseTurn('c1', 'mcp_fs_read'), textTurn('done')]),
startMcpClient: () => realStartMcpClient([{ id: 'fs', open: () => Promise.resolve(conn) }]),
consentGate: () => Promise.resolve(new Map()), // see `build` above (ADR-0084 §1)
});
// A resumed session's MCP tools are tier 3 too (ADR-0080) — same reason as `build()` above.
built.attachEffectJournal((correlation) => createInMemoryEffectJournal(correlation));

// The RETURNED agent is the ORIGINAL snapshot — its grant is not baked with the dynamic id, and it still
// carries mcp_servers so a FUTURE resume re-discovers again (the persistence contract).
Expand Down Expand Up @@ -870,6 +913,7 @@ describe('buildResumedChatSession (2.N)', () => {
now: () => Date.parse(ISO),
providers: scriptedResolver([textTurn('unused')]),
startMcpClient: () => Promise.resolve(collidingClient),
consentGate: () => Promise.resolve(new Map()), // see `build` above (ADR-0084 §1)
});
await expect(building).rejects.toThrow(/duplicate tool id/);
expect(closed).toBe(1);
Expand Down
Loading
Loading