zelari-code 2.0.0-alpha.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/cli/headless.js +9 -0
  2. package/dist/cli/headless.js.map +1 -1
  3. package/dist/cli/headlessE2eSession.test.js +189 -0
  4. package/dist/cli/headlessE2eSession.test.js.map +1 -0
  5. package/dist/cli/headlessSessionEvent.test.js +83 -0
  6. package/dist/cli/headlessSessionEvent.test.js.map +1 -0
  7. package/dist/cli/headlessSpine.js +25 -0
  8. package/dist/cli/headlessSpine.js.map +1 -1
  9. package/dist/cli/hooks/chatState.js.map +1 -1
  10. package/dist/cli/hooks/eventsToMessages.js.map +1 -1
  11. package/dist/cli/hooks/useChatTurn.js +40 -7
  12. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  13. package/dist/cli/hooks/useSession.js +0 -2
  14. package/dist/cli/hooks/useSession.js.map +1 -1
  15. package/dist/cli/kraken/evidenceAnchoring.test.js +92 -0
  16. package/dist/cli/kraken/evidenceAnchoring.test.js.map +1 -0
  17. package/dist/cli/kraken/nativeVerification.js +113 -0
  18. package/dist/cli/kraken/nativeVerification.js.map +1 -0
  19. package/dist/cli/kraken/nativeVerification.test.js +207 -0
  20. package/dist/cli/kraken/nativeVerification.test.js.map +1 -0
  21. package/dist/cli/kraken/strictDefaults.test.js +149 -0
  22. package/dist/cli/kraken/strictDefaults.test.js.map +1 -0
  23. package/dist/cli/kraken/verificationBridge.js +121 -8
  24. package/dist/cli/kraken/verificationBridge.js.map +1 -1
  25. package/dist/cli/kraken/verificationBridge.session.test.js +104 -0
  26. package/dist/cli/kraken/verificationBridge.session.test.js.map +1 -0
  27. package/dist/cli/kraken/verificationBridge.test.js +63 -17
  28. package/dist/cli/kraken/verificationBridge.test.js.map +1 -1
  29. package/dist/cli/kraken/verifierAdvisoryLock.test.js +154 -0
  30. package/dist/cli/kraken/verifierAdvisoryLock.test.js.map +1 -0
  31. package/dist/cli/kraken/verifierResolution.js +28 -0
  32. package/dist/cli/kraken/verifierResolution.js.map +1 -0
  33. package/dist/cli/kraken/verifierRoundTrip.test.js +166 -0
  34. package/dist/cli/kraken/verifierRoundTrip.test.js.map +1 -0
  35. package/dist/cli/legacyContextIsolation.test.js +65 -0
  36. package/dist/cli/legacyContextIsolation.test.js.map +1 -0
  37. package/dist/cli/main.bundled.js +893 -317
  38. package/dist/cli/main.bundled.js.map +4 -4
  39. package/dist/cli/missionContinuation.test.js +117 -0
  40. package/dist/cli/missionContinuation.test.js.map +1 -0
  41. package/dist/cli/profileMatrix.test.js +130 -0
  42. package/dist/cli/profileMatrix.test.js.map +1 -0
  43. package/dist/cli/runHeadless.js +68 -8
  44. package/dist/cli/runHeadless.js.map +1 -1
  45. package/dist/cli/sessionManager.js +7 -0
  46. package/dist/cli/sessionManager.js.map +1 -1
  47. package/dist/cli/sessionSpine.js +43 -3
  48. package/dist/cli/sessionSpine.js.map +1 -1
  49. package/dist/cli/zelariMission.js +16 -0
  50. package/dist/cli/zelariMission.js.map +1 -1
  51. package/package.json +3 -3
@@ -0,0 +1,117 @@
1
+ /**
2
+ * F4 integration — the mission loop records advisory continuation without
3
+ * obeying it: advice on every implementation slice, deterministic stop rules
4
+ * untouched, spine round-trip of mission.progress state events.
5
+ */
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import { runZelariMission, } from './zelariMission.js';
8
+ import { SessionStore, readSessionLog, buildProjection, } from '@zelari/core/session';
9
+ import { deriveMissionState } from '@zelari/core/mission';
10
+ import * as path from 'node:path';
11
+ import * as os from 'node:os';
12
+ import { promises as fs } from 'node:fs';
13
+ const brief = {
14
+ goal: 'vertical slice',
15
+ deliverableThisMission: 'one module',
16
+ phases: [{ mode: 'implementation', focus: 'impl' }],
17
+ sliceMvp: { id: 'slice-1', summary: 'mvp', acceptance: [] },
18
+ };
19
+ function fakeMemory() {
20
+ return {
21
+ init: vi.fn(async () => { }),
22
+ add: vi.fn(async () => { }),
23
+ search: vi.fn(async () => []),
24
+ };
25
+ }
26
+ function deps(overrides = {}) {
27
+ return {
28
+ projectRoot: os.tmpdir(),
29
+ memory: fakeMemory(),
30
+ runSlice: vi.fn(async () => ({ completionOk: true, ran: true, writeCount: 2 })),
31
+ emit: vi.fn(),
32
+ env: { ZELARI_CHECKPOINT: '0', ZELARI_MISSION_MAX_ITER: '3' },
33
+ ...overrides,
34
+ };
35
+ }
36
+ describe('runZelariMission — advisory continuation (F4)', () => {
37
+ it('records advice on each implementation slice but never obeys it for done-by-score', async () => {
38
+ const onMissionProgress = vi.fn();
39
+ // Slice 1: claims completion but wrote 0 files → NOT done.
40
+ // Slice 2: real green → done. The loop must follow its own deterministic
41
+ // rules; the advice is only recorded.
42
+ const results = [
43
+ { completionOk: true, ran: true, writeCount: 0 },
44
+ { completionOk: true, ran: true, writeCount: 3 },
45
+ ];
46
+ let i = 0;
47
+ const d = deps({
48
+ runSlice: vi.fn(async () => results[i++]),
49
+ onMissionProgress,
50
+ });
51
+ const state = await runZelariMission('do it', brief, d);
52
+ expect(state.status).toBe('success');
53
+ expect(state.iteration).toBeGreaterThanOrEqual(2);
54
+ expect(onMissionProgress).toHaveBeenCalled();
55
+ const first = onMissionProgress.mock.calls[0][0];
56
+ expect(first.doneByScore).toBe(false);
57
+ expect(first.goalRewrite).toBe(false);
58
+ expect(['continue', 'wind-down', 'hold-for-user']).toContain(first.recommendation);
59
+ });
60
+ it('emits a wind-down advice only when the deterministic gate is green', async () => {
61
+ const seen = [];
62
+ const d = deps({
63
+ runSlice: vi.fn(async () => ({ completionOk: true, ran: true, writeCount: 1 })),
64
+ onMissionProgress: (advice) => seen.push(advice.recommendation),
65
+ });
66
+ const state = await runZelariMission('go', brief, d);
67
+ expect(state.status).toBe('success');
68
+ expect(seen[seen.length - 1]).toBe('wind-down');
69
+ });
70
+ it('budget-exhausted run holds for the user instead of claiming done', async () => {
71
+ const onMissionProgress = vi.fn();
72
+ const d = deps({
73
+ runSlice: vi.fn(async () => ({ completionOk: false, ran: true, writeCount: 1 })),
74
+ onMissionProgress,
75
+ });
76
+ const state = await runZelariMission('go', brief, d);
77
+ expect(state.status).toBe('stopped');
78
+ const last = onMissionProgress.mock.calls.at(-1)[0];
79
+ expect(['hold-for-user', 'continue']).toContain(last.recommendation);
80
+ });
81
+ });
82
+ describe('mission.progress spine round-trip', () => {
83
+ it('advice events replay as state (lastAdvice) and never join the model surface', async () => {
84
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'f4-'));
85
+ const store = new SessionStore(tmp);
86
+ const { sessionId, writer } = await store.create({ profile: 'mission/v1' });
87
+ await writer.append({
88
+ kind: 'mission.phase',
89
+ actor: { type: 'system' },
90
+ data: { phase: 'build' },
91
+ });
92
+ await writer.append({
93
+ kind: 'mission.progress',
94
+ actor: { type: 'system' },
95
+ data: {
96
+ recommendation: 'continue',
97
+ rationale: 'required criteria incomplete → no early-stop',
98
+ blockers: ['1/1 required criteria not passing'],
99
+ iteration: 2,
100
+ },
101
+ });
102
+ await writer.close();
103
+ const report = await readSessionLog(path.join(tmp, sessionId, 'events.jsonl'));
104
+ const events = report.events;
105
+ const projection = buildProjection(events, report.issues);
106
+ expect(projection.missionAdvice).toHaveLength(1);
107
+ expect(projection.missionAdvice[0].recommendation).toBe('continue');
108
+ const state = deriveMissionState(projection);
109
+ expect(state.phase).toBe('build');
110
+ expect(state.lastAdvice?.rationale).toContain('no early-stop');
111
+ const { isModelSurfaceEvent } = await import('@zelari/core/session');
112
+ const adviceEvent = events.find((e) => e.kind === 'mission.progress');
113
+ expect(adviceEvent).toBeDefined();
114
+ expect(isModelSurfaceEvent(adviceEvent)).toBe(false);
115
+ });
116
+ });
117
+ //# sourceMappingURL=missionContinuation.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missionContinuation.test.js","sourceRoot":"","sources":["../../src/cli/missionContinuation.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * profileMatrix — F7 (Exit-3.2, doc §10) profile × phase smoke matrix.
3
+ *
4
+ * Cells (§10 minimum): minimal+plan/build, kraken+plan/build, council+plan,
5
+ * mission+build. For each cell the smoke walks the three real wiring legs:
6
+ *
7
+ * 1. profile loader — mode/--profile resolves to the expected versioned
8
+ * profile (ADR-0022); explicit ids win over mode defaults
9
+ * 2. session metadata — the spine `session.started` header records profile +
10
+ * toolManifestHash of the declared profile, so same-task/same-profile
11
+ * runs stay comparable
12
+ * 3. capability gate — the REAL registry built for that phase: plan strips
13
+ * every PLAN_BLOCKED_TOOLS mutator while keeping the plan-domain tools
14
+ * (task_create/update/list + explore-only `task`); build registers the
15
+ * full mutator set; declared profile tools are a subset of the build
16
+ * registry (declarative 2.0 manifest ⊆ real 1.x registry)
17
+ *
18
+ * Wiring invariants that live inside runHeadless (mission slices never run in
19
+ * plan; kraken/council registries derive planMode from the phase) are locked
20
+ * as source assertions — same pattern as legacyContextIsolation.test.ts.
21
+ */
22
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
23
+ import { promises as fs } from 'node:fs';
24
+ import { readFileSync } from 'node:fs';
25
+ import path from 'node:path';
26
+ import os from 'node:os';
27
+ import { fileURLToPath } from 'node:url';
28
+ import { readSessionLog } from '@zelari/core/session';
29
+ import { MINIMAL_V1, KRAKEN_V1, MISSION_V1, toolManifestHash } from '@zelari/core/runtime';
30
+ import { openHeadlessSpine, resolveHeadlessProfileId } from './headlessSpine.js';
31
+ import { createBuiltinToolRegistry } from './toolRegistry.js';
32
+ import { PLAN_BLOCKED_TOOLS } from './phase.js';
33
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
34
+ function readCli(rel) {
35
+ return readFileSync(path.join(REPO_ROOT, 'src', 'cli', rel), 'utf8');
36
+ }
37
+ const MATRIX = [
38
+ { mode: 'kraken', profile: 'minimal/v1', explicitProfile: 'minimal/v1', phase: 'plan', declaredTools: MINIMAL_V1.tools },
39
+ { mode: 'kraken', profile: 'minimal/v1', explicitProfile: 'minimal/v1', phase: 'build', declaredTools: MINIMAL_V1.tools },
40
+ { mode: 'kraken', profile: 'kraken/v1', phase: 'plan', declaredTools: KRAKEN_V1.tools },
41
+ { mode: 'kraken', profile: 'kraken/v1', phase: 'build', declaredTools: KRAKEN_V1.tools },
42
+ { mode: 'council', profile: 'council/v1', phase: 'plan', declaredTools: [...MINIMAL_V1.tools, 'write_file'] },
43
+ // §10 + mission contract: mission runs BUILD only (slices implement on disk).
44
+ { mode: 'zelari', profile: 'mission/v1', phase: 'build', declaredTools: MISSION_V1.tools },
45
+ ];
46
+ let tmp;
47
+ beforeEach(async () => {
48
+ tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'zelari-profile-matrix-'));
49
+ });
50
+ afterEach(async () => {
51
+ await fs.rm(tmp, { recursive: true, force: true });
52
+ });
53
+ describe('F7 profile × phase smoke matrix (Exit-3.2)', () => {
54
+ it.each(MATRIX)('cell $mode/$phase → $profile: loader, session metadata and phase capability gate', async (cell) => {
55
+ // Leg 1 — profile loader: explicit id wins, otherwise the mode default.
56
+ expect(resolveHeadlessProfileId(cell.mode, cell.explicitProfile)).toBe(cell.profile);
57
+ // Leg 2 — session metadata on the spine header.
58
+ const sessionId = `mx-${cell.mode}-${cell.phase}`;
59
+ const handle = await openHeadlessSpine({ sessionId, mode: cell.mode, profile: cell.explicitProfile, baseDir: tmp, quiet: true });
60
+ expect(handle.profileId).toBe(cell.profile);
61
+ const report = await readSessionLog(path.join(tmp, sessionId, 'events.jsonl'));
62
+ expect(report.ok).toBe(true);
63
+ const started = report.events.find((e) => e.kind === 'session.started');
64
+ expect(started).toBeDefined();
65
+ const startedData = (started?.data ?? {});
66
+ expect(startedData.profile).toBe(cell.profile);
67
+ // The manifest hash is the DECLARED profile tool set (ADR-0022) — the
68
+ // comparability contract, distinct from the phase-gated live registry.
69
+ expect(startedData.toolManifestHash).toBe(toolManifestHash(cell.declaredTools));
70
+ // Leg 3 — real registry for the phase (no tool is invoked: zero side effects).
71
+ const { registry } = createBuiltinToolRegistry({
72
+ root: tmp,
73
+ sessionId,
74
+ planMode: cell.phase === 'plan',
75
+ lifecycleHooks: null,
76
+ lspProvider: null,
77
+ });
78
+ const names = new Set(registry.list());
79
+ // Observe builtins exist in BOTH phases.
80
+ for (const observe of ['read_file', 'grep_content', 'list_files']) {
81
+ expect(names.has(observe), `${observe} must exist in ${cell.phase}`).toBe(true);
82
+ }
83
+ if (cell.phase === 'plan') {
84
+ // No mutating builtin survives plan (no unauthorized side effects).
85
+ for (const blocked of PLAN_BLOCKED_TOOLS) {
86
+ expect(names.has(blocked), `${blocked} must be stripped in plan`).toBe(false);
87
+ }
88
+ // Plan-domain tools stay: durable plan tasks + explore-only delegation.
89
+ for (const planTool of ['task_create', 'task_update', 'task_list', 'task']) {
90
+ expect(names.has(planTool), `${planTool} must exist in plan`).toBe(true);
91
+ }
92
+ }
93
+ else {
94
+ // Build: the full mutator set is available.
95
+ for (const mutator of ['write_file', 'edit_file', 'apply_diff', 'bash', 'task']) {
96
+ expect(names.has(mutator), `${mutator} must exist in build`).toBe(true);
97
+ }
98
+ // Declared 2.0 profile tools are a subset of the real build registry.
99
+ for (const declared of cell.declaredTools) {
100
+ expect(names.has(declared), `declared tool ${declared} missing from build registry`).toBe(true);
101
+ }
102
+ }
103
+ }, 30_000);
104
+ });
105
+ describe('F7 wiring invariants (source-level, legacyContextIsolation pattern)', () => {
106
+ it('kraken and council registries derive planMode from the resolved phase', () => {
107
+ const src = readCli('runHeadless.ts');
108
+ // Kraken parent registry (headless hot path).
109
+ expect(src).toContain('planMode: planModeFromOpts(opts),');
110
+ // Council pipelines: explicit phase (design leg) and soft-gated variant.
111
+ expect(src).toContain('planModeFromOpts(opts) || softGated,');
112
+ expect(src).toContain('planModeFromOpts(opts),');
113
+ // planModeFromOpts is the single phase→planMode derivation.
114
+ expect(src).toContain("return (opts.phase ?? 'build') === 'plan';");
115
+ });
116
+ it('mission implementation slices are hard-wired to BUILD (never planMode)', () => {
117
+ const src = readCli('runHeadless.ts');
118
+ const lines = src.split(/\r?\n/);
119
+ const idx = lines.findIndex((l) => l.includes('const { registry: agentRegistry } = createBuiltinToolRegistry({'));
120
+ expect(idx).toBeGreaterThan(-1);
121
+ const window = lines.slice(idx, idx + 12).join('\n');
122
+ expect(window).toContain('planMode: false');
123
+ expect(window).not.toContain('planMode: true');
124
+ });
125
+ it('PLAN_BLOCKED_TOOLS stays exactly the mutating builtins', () => {
126
+ // If a new mutator lands without a plan gate, this reminds the author.
127
+ expect([...PLAN_BLOCKED_TOOLS].sort()).toEqual(['apply_diff', 'bash', 'edit_file', 'write_file']);
128
+ });
129
+ });
130
+ //# sourceMappingURL=profileMatrix.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profileMatrix.test.js","sourceRoot":"","sources":["../../src/cli/profileMatrix.test.ts"],"names":[],"mappings":"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"}
@@ -37,8 +37,8 @@ import { writeSessionTodos } from './sessionTodos.js';
37
37
  import { promises as fs } from 'node:fs';
38
38
  import path from 'node:path';
39
39
  import { randomUUID } from 'node:crypto';
40
- import { evaluateStrictBuildGate, strictGateEventPayload } from './kraken/verificationBridge.js';
41
- import { openHeadlessSpine, resolveHeadlessProfileId, seedHeadlessModelHistory, } from './headlessSpine.js';
40
+ import { evaluateStrictBuildGate, strictGateEventPayload, strictGateExitCode } from './kraken/verificationBridge.js';
41
+ import { openHeadlessSpine, resolveHeadlessProfileId, seedHeadlessModelHistory, sessionStartedEvent, } from './headlessSpine.js';
42
42
  export async function runHeadless(opts) {
43
43
  resetTaskSpawnCount();
44
44
  // Desktop multi-turn todo persistence: each message spawns a fresh process,
@@ -332,6 +332,8 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
332
332
  emitEvent({ type: 'message_delta', delta: finalAscii });
333
333
  emitEvent({ type: 'message_end' });
334
334
  emitEvent({ type: 'agent_end', reason: summary.converged ? 'completed' : 'error' });
335
+ // COMPAT MIRROR (ADR-0024): the session spine is the canonical context;
336
+ // history_snapshot is one-shot legacy-host replay only (F13 cleanup, doc §8).
335
337
  emitEvent({
336
338
  type: 'history_snapshot',
337
339
  messages: [
@@ -417,6 +419,9 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
417
419
  // are then derived from events. The 1.x rolling history no longer feeds
418
420
  // the harness messages directly (degraded spine falls back to it).
419
421
  const seededHistory = await seedHeadlessModelHistory(spine, opts.history);
422
+ // E1.4: advertise the spine session id so hosts (Desktop) resume the
423
+ // same event log next turn instead of replaying 1.x history JSON.
424
+ emitEvent(sessionStartedEvent(spine));
420
425
  if (opts.task)
421
426
  spine.userMessage(opts.task);
422
427
  // Fase 3 (ADR-0020): fresh per-run candidate registry (each headless run
@@ -785,7 +790,10 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
785
790
  emittedWrites: pass.emittedWrites + retry.emittedWrites,
786
791
  };
787
792
  }
788
- // Fase 8 (ADR-0020): completion gate a BUILD turn that used selection
793
+ // E2.2: when strict mode is on and the gate stays blocked after the repair
794
+ // pass, the run closes non-success (dedicated exit code + session status).
795
+ let strictExit = 0;
796
+ // Fase 8 (ADR-0020): completion gate — a BUILD turn that used selection // Fase 8 (ADR-0020): completion gate — a BUILD turn that used selection
789
797
  // cannot cleanly finish while required checks are unresolved (fail OR
790
798
  // unknown — a degraded observation is never proof). One automatic
791
799
  // repair pass (budget = 1, structural), reusing the same recovery
@@ -795,7 +803,7 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
795
803
  opts.mode === 'kraken' &&
796
804
  isKrakenSelectionEnabled() &&
797
805
  !planModeFromOpts(opts)) {
798
- const strictGate = evaluateStrictBuildGate('build');
806
+ const strictGate = await evaluateStrictBuildGate('build', { emit: (input) => spine.appendEvent(input) });
799
807
  const gate = strictGate.gate;
800
808
  const verificationPayload = strictGateEventPayload(strictGate);
801
809
  spine.verificationRun(verificationPayload);
@@ -829,7 +837,7 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
829
837
  successfulWrites: pass.successfulWrites + repair.successfulWrites,
830
838
  emittedWrites: pass.emittedWrites + repair.emittedWrites,
831
839
  };
832
- const after = evaluateStrictBuildGate('build');
840
+ const after = await evaluateStrictBuildGate('build', { emit: (input) => spine.appendEvent(input) });
833
841
  const afterPayload = strictGateEventPayload(after);
834
842
  spine.verificationRun(afterPayload);
835
843
  if (opts.output === 'json') {
@@ -837,6 +845,15 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
837
845
  }
838
846
  if (!after.blocked)
839
847
  markRepairSucceeded();
848
+ else {
849
+ strictExit = strictGateExitCode(after);
850
+ const gateMsg = `[headless] Kraken BUILD: strict completion gate still blocked after repair pass — ` +
851
+ `closing non-success (exit ${strictExit}): ${after.summary}`;
852
+ if (opts.output === 'json')
853
+ emitEvent({ type: 'log', message: gateMsg });
854
+ else
855
+ process.stderr.write(`[zelari-code --headless] ${gateMsg}\n`);
856
+ }
840
857
  }
841
858
  }
842
859
  progressRuntime.finish(pass.finalReason);
@@ -851,6 +868,7 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
851
868
  }
852
869
  process.stdout.write('');
853
870
  // v1.10.0: emit a history_snapshot so the desktop can replay this turn.
871
+ // COMPAT MIRROR (ADR-0024): session spine is canonical — see emit sites below.
854
872
  // Include the user turn + final assistant text (user/assistant only).
855
873
  if (pass.finalReason !== 'error' && opts.output === 'json') {
856
874
  try {
@@ -888,6 +906,8 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
888
906
  : []),
889
907
  ];
890
908
  if (snapshot.length > 0) {
909
+ // COMPAT MIRROR (ADR-0024): the session spine is the canonical context;
910
+ // history_snapshot is one-shot legacy-host replay only (F13 cleanup, doc §8).
891
911
  emitEvent({ type: 'history_snapshot', messages: snapshot });
892
912
  }
893
913
  }
@@ -896,7 +916,8 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
896
916
  }
897
917
  }
898
918
  try {
899
- await spine.close(pass.finalReason === 'error' ? 'error' : 'completed');
919
+ const closeStatus = pass.finalReason === 'error' ? 'error' : strictExit !== 0 ? 'stopped' : 'completed';
920
+ await spine.close(closeStatus);
900
921
  }
901
922
  catch { /* spine never fails the run */ }
902
923
  if (opts.exportSessionPath) {
@@ -915,6 +936,9 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
915
936
  }
916
937
  if (pass.finalReason === 'error')
917
938
  return 3;
939
+ // E2.2: strict done gate — a blocked verdict overrides a clean pass exit.
940
+ if (strictExit !== 0)
941
+ return strictExit;
918
942
  return pass.exitCode;
919
943
  }
920
944
  async function buildCouncilToolRegistry(planMode, opts) {
@@ -959,6 +983,9 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
959
983
  // are then derived from events. The 1.x rolling history no longer feeds
960
984
  // the harness messages directly (degraded spine falls back to it).
961
985
  const seededHistory = await seedHeadlessModelHistory(spine, opts.history);
986
+ // E1.4: advertise the spine session id so hosts (Desktop) resume the
987
+ // same event log next turn instead of replaying 1.x history JSON.
988
+ emitEvent(sessionStartedEvent(spine));
962
989
  if (opts.task)
963
990
  spine.userMessage(opts.task);
964
991
  // Experiment: free-form council+build soft-gated to design-phase unless
@@ -1078,6 +1105,8 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
1078
1105
  ? [{ role: 'assistant', content: lastAssistantText }]
1079
1106
  : []),
1080
1107
  ];
1108
+ // COMPAT MIRROR (ADR-0024): the session spine is the canonical context;
1109
+ // history_snapshot is one-shot legacy-host replay only (F13 cleanup, doc §8).
1081
1110
  emitEvent({ type: 'history_snapshot', messages: snapshot });
1082
1111
  }
1083
1112
  catch {
@@ -1121,6 +1150,9 @@ async function runHeadlessZelari(opts, provider, model, providerStream) {
1121
1150
  // are then derived from events. The 1.x rolling history no longer feeds
1122
1151
  // the harness messages directly (degraded spine falls back to it).
1123
1152
  const seededHistory = await seedHeadlessModelHistory(spine, opts.history);
1153
+ // E1.4: advertise the spine session id so hosts (Desktop) resume the
1154
+ // same event log next turn instead of replaying 1.x history JSON.
1155
+ emitEvent(sessionStartedEvent(spine));
1124
1156
  if (opts.task)
1125
1157
  spine.userMessage(opts.task);
1126
1158
  spine.missionPhase('design', 'mission-start');
@@ -1184,6 +1216,13 @@ async function runHeadlessZelari(opts, provider, model, providerStream) {
1184
1216
  emit,
1185
1217
  buildViaAgent,
1186
1218
  onMissionPhase: (phase, note) => spine.missionPhase(phase, note),
1219
+ onMissionProgress: (advice, iteration) => spine.missionProgress({
1220
+ recommendation: advice.recommendation,
1221
+ rationale: advice.rationale,
1222
+ blockers: advice.blockers,
1223
+ ...(advice.trend ? { trend: advice.trend } : {}),
1224
+ iteration,
1225
+ }),
1187
1226
  runSlice: async ({ userMessage: slicePrompt, runMode, ragContext, implementerRetry, }) => {
1188
1227
  const effectiveRunMode = planModeFromOpts(opts) ? 'design-phase' : runMode;
1189
1228
  // design-phase always council; implementation uses agent when policy ON
@@ -1397,8 +1436,27 @@ async function runHeadlessZelari(opts, provider, model, providerStream) {
1397
1436
  if (state.status === 'error')
1398
1437
  exitCode = exitCode || 3;
1399
1438
  else if (state.status === 'success') {
1400
- exitCode = 0;
1401
- spine.missionPhase('done', 'mission-success');
1439
+ // ADR-0025: missions close under the strict evidence gate by default
1440
+ // (opt-out: ZELARI_MISSION_STRICT=0). A blocked gate never exits 0 —
1441
+ // the mission "success" becomes the strict exit code and the spine
1442
+ // records mission-strict-blocked instead of mission-success.
1443
+ const missionGate = await evaluateStrictBuildGate('build', {
1444
+ emit: (input) => spine.appendEvent(input),
1445
+ surface: 'mission',
1446
+ });
1447
+ const missionVerificationPayload = strictGateEventPayload(missionGate);
1448
+ spine.verificationRun(missionVerificationPayload);
1449
+ if (opts.output === 'json') {
1450
+ emitEvent({ type: 'verification_run', ...missionVerificationPayload });
1451
+ }
1452
+ if (missionGate.blocked) {
1453
+ exitCode = strictGateExitCode(missionGate);
1454
+ spine.missionPhase('verification', 'mission-strict-blocked');
1455
+ }
1456
+ else {
1457
+ exitCode = 0;
1458
+ spine.missionPhase('done', 'mission-success');
1459
+ }
1402
1460
  }
1403
1461
  else if (state.status === 'stalled' || state.status === 'stopped') {
1404
1462
  exitCode = exitCode || 0;
@@ -1439,6 +1497,8 @@ async function runHeadlessZelari(opts, provider, model, providerStream) {
1439
1497
  }
1440
1498
  if (opts.output === 'json') {
1441
1499
  try {
1500
+ // COMPAT MIRROR (ADR-0024): the session spine is the canonical context;
1501
+ // history_snapshot is one-shot legacy-host replay only (F13 cleanup, doc §8).
1442
1502
  emitEvent({
1443
1503
  type: 'history_snapshot',
1444
1504
  messages: [