@wjarka/cezarion 0.14.6 → 0.14.7

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 (123) hide show
  1. package/README.md +37 -2
  2. package/dist/agent-config/model-settings/shared.d.ts +2 -1
  3. package/dist/agent-config/model-settings/shared.js +1 -1
  4. package/dist/agent-config/model-settings/shared.js.map +1 -1
  5. package/dist/ci-wait/client.d.ts +4 -0
  6. package/dist/ci-wait/client.js +67 -0
  7. package/dist/ci-wait/client.js.map +1 -0
  8. package/dist/ci-wait/controller.d.ts +78 -0
  9. package/dist/ci-wait/controller.js +148 -0
  10. package/dist/ci-wait/controller.js.map +1 -0
  11. package/dist/ci-wait/github.d.ts +61 -0
  12. package/dist/ci-wait/github.js +209 -0
  13. package/dist/ci-wait/github.js.map +1 -0
  14. package/dist/ci-wait/injection.d.ts +3 -0
  15. package/dist/ci-wait/injection.js +26 -0
  16. package/dist/ci-wait/injection.js.map +1 -0
  17. package/dist/ci-wait/mcp.d.ts +97 -0
  18. package/dist/ci-wait/mcp.js +35 -0
  19. package/dist/ci-wait/mcp.js.map +1 -0
  20. package/dist/ci-wait/resources.d.ts +7 -0
  21. package/dist/ci-wait/resources.js +32 -0
  22. package/dist/ci-wait/resources.js.map +1 -0
  23. package/dist/ci-wait/supervisor.d.ts +21 -0
  24. package/dist/ci-wait/supervisor.js +163 -0
  25. package/dist/ci-wait/supervisor.js.map +1 -0
  26. package/dist/contract/ci-wait.d.ts +151 -0
  27. package/dist/contract/conversations.d.ts +7 -0
  28. package/dist/contract/delegation.d.ts +2 -0
  29. package/dist/contract/events.d.ts +3 -0
  30. package/dist/contract/index.d.ts +1 -0
  31. package/dist/contract/index.js +1016 -935
  32. package/dist/contract/runs.d.ts +461 -0
  33. package/dist/contract/skills.d.ts +92 -0
  34. package/dist/core/agent-env.js +2 -0
  35. package/dist/core/agent-env.js.map +1 -1
  36. package/dist/core/agent-runner.d.ts +6 -0
  37. package/dist/core/agent-runner.js +1 -0
  38. package/dist/core/agent-runner.js.map +1 -1
  39. package/dist/core/claude-cli-runner.js +10 -0
  40. package/dist/core/claude-cli-runner.js.map +1 -1
  41. package/dist/core/codex-app-server-runner.js +5 -1
  42. package/dist/core/codex-app-server-runner.js.map +1 -1
  43. package/dist/core/codex-app-server-transport.js +3 -1
  44. package/dist/core/codex-app-server-transport.js.map +1 -1
  45. package/dist/core/cursor-acp-runner.js +9 -1
  46. package/dist/core/cursor-acp-runner.js.map +1 -1
  47. package/dist/core/opencode-server-runner.js +6 -2
  48. package/dist/core/opencode-server-runner.js.map +1 -1
  49. package/dist/core/pi-runner.js +7 -1
  50. package/dist/core/pi-runner.js.map +1 -1
  51. package/dist/delegation/cli.js +8 -4
  52. package/dist/delegation/cli.js.map +1 -1
  53. package/dist/delegation/conversations.js +2 -1
  54. package/dist/delegation/conversations.js.map +1 -1
  55. package/dist/delegation/input.d.ts +6 -0
  56. package/dist/delegation/input.js +22 -0
  57. package/dist/delegation/input.js.map +1 -1
  58. package/dist/delegation/provision.js +3 -1
  59. package/dist/delegation/provision.js.map +1 -1
  60. package/dist/delegation/routes.d.ts +11 -0
  61. package/dist/delegation/service.js +31 -7
  62. package/dist/delegation/service.js.map +1 -1
  63. package/dist/delegation/transport.d.ts +11 -0
  64. package/dist/runs/delegation-state.d.ts +2 -0
  65. package/dist/runs/run-index.js +2 -3
  66. package/dist/runs/run-index.js.map +1 -1
  67. package/dist/runs/store.d.ts +514 -3
  68. package/dist/runs/store.js +85 -6
  69. package/dist/runs/store.js.map +1 -1
  70. package/dist/server/server.d.ts +1376 -12
  71. package/dist/workflows/run.d.ts +25 -1
  72. package/dist/workflows/run.js +470 -43
  73. package/dist/workflows/run.js.map +1 -1
  74. package/package.json +4 -3
  75. package/scripts/mock-ci-tool.mjs +53 -0
  76. package/scripts/mock-claude.mjs +8 -0
  77. package/scripts/mock-codex-app-server.mjs +212 -0
  78. package/scripts/mock-cursor-acp.mjs +6 -2
  79. package/scripts/mock-opencode-serve.mjs +392 -0
  80. package/scripts/mock-pi-rpc.mjs +7 -0
  81. package/scripts/pi-ci-wait.mjs +14 -0
  82. package/web/dist/assets/{agent-accounts-lP_xK83o.js → agent-accounts-BZm_2WAv.js} +1 -1
  83. package/web/dist/assets/{alert-dialog-CmDuefYh.js → alert-dialog-B422RI-k.js} +1 -1
  84. package/web/dist/assets/{collapsible-BqX8REmY.js → collapsible-Cy_Fmkrd.js} +1 -1
  85. package/web/dist/assets/{commit-list-BWvv1H7A.js → commit-list-CLxXMZvP.js} +1 -1
  86. package/web/dist/assets/{compare-variants-Bg7eLW3z.js → compare-variants-D2dRj6H_.js} +1 -1
  87. package/web/dist/assets/{diff-controls-HwWHHait.js → diff-controls-BGrXGi7O.js} +1 -1
  88. package/web/dist/assets/{diff-stat-mNkhAc5m.js → diff-stat-Cgnl_hYF.js} +1 -1
  89. package/web/dist/assets/{diff-0GBum40Q2.js → diff-tRE59UNb2.js} +2 -2
  90. package/web/dist/assets/{diff-view-DW64doDY.js → diff-view-DN1ort7Q.js} +1 -1
  91. package/web/dist/assets/{dropdown-menu-sZjYPcLh.js → dropdown-menu-dwDGT8MM.js} +1 -1
  92. package/web/dist/assets/{git-toolbar-DBW5EIcY.js → git-toolbar-UiHnGvE0.js} +1 -1
  93. package/web/dist/assets/{github-CkZ4hWI5.js → github-CaGjGQnW.js} +1 -1
  94. package/web/dist/assets/{image-preview-DZicGNkU.js → image-preview-C2JGcBlC.js} +1 -1
  95. package/web/dist/assets/index-CXZukB1O.css +1 -0
  96. package/web/dist/assets/{index-DWWI-B9n.js → index-D0RwD9-c.js} +3 -3
  97. package/web/dist/assets/{markdown-AY2K3kIV.js → markdown-C_IcKVQd.js} +1 -1
  98. package/web/dist/assets/{pill-CcJqA1cL.js → pill-BsbO6G7y.js} +1 -1
  99. package/web/dist/assets/{project-router-SwsXRKXs.js → project-router-DbLkll25.js} +1 -1
  100. package/web/dist/assets/{prompt-templates-iV2WD2jy.js → prompt-templates-BlTGJB5W.js} +1 -1
  101. package/web/dist/assets/{repo-git-BW3TOs3U.js → repo-git-CWD2dO8S.js} +1 -1
  102. package/web/dist/assets/{run-diff-DPUNj56q.js → run-diff-CyG0Nciv.js} +1 -1
  103. package/web/dist/assets/{run-header-CSaJIGXl.css → run-header-BJQfFL2l.css} +1 -1
  104. package/web/dist/assets/run-header-IFyIGCey.js +1 -0
  105. package/web/dist/assets/{skills-Dv8Vwv5D.js → skills-e7wHnuoc.js} +1 -1
  106. package/web/dist/assets/{tab-link-DSftvkZS.js → tab-link-BWNp3mLY.js} +1 -1
  107. package/web/dist/assets/{task-changes-CTCuVls1.js → task-changes-CpJImtq8.js} +1 -1
  108. package/web/dist/assets/{task-commits-weJQ5GSl.js → task-commits-hujrykPI.js} +1 -1
  109. package/web/dist/assets/{task-files-B4bUimY8.js → task-files-CjTiGtjt.js} +1 -1
  110. package/web/dist/assets/{task-thread-BuXrVB9K.js → task-thread-DDPFDS8C.js} +4 -4
  111. package/web/dist/assets/task-thread-Dc3wplva.css +1 -0
  112. package/web/dist/assets/{textarea-CxRdTWlq.js → textarea-FjaU370i.js} +1 -1
  113. package/web/dist/assets/thread-loading-77sJ8Yu_.js +1 -0
  114. package/web/dist/assets/{token-metrics-Z3UFYbuD.js → token-metrics-Clxh03bE.js} +1 -1
  115. package/web/dist/assets/utils-PYYcpCFZ.js +64 -0
  116. package/web/dist/assets/{workflows-vA27Yi8W.js → workflows-DGUQmWy7.js} +1 -1
  117. package/web/dist/assets/{zoomable-image-DUE1KFJ0.js → zoomable-image-Z3JDnm0b.js} +1 -1
  118. package/web/dist/index.html +14 -14
  119. package/web/dist/assets/index-DK0Mkv2m.css +0 -1
  120. package/web/dist/assets/run-header-DbDluWoV.js +0 -1
  121. package/web/dist/assets/task-thread-DMRD0IVt.css +0 -1
  122. package/web/dist/assets/thread-loading-JlP6dUpX.js +0 -1
  123. package/web/dist/assets/utils-CUnaAifW.js +0 -64
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wjarka/cezarion",
3
- "version": "0.14.6",
3
+ "version": "0.14.7",
4
4
  "description": "Local cockpit for running and tracking AI agent tasks in your repo. Uses your logged-in `claude` CLI and `gh` — zero config, zero database.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,6 +43,7 @@
43
43
  "dependencies": {
44
44
  "@clack/prompts": "^0.11.0",
45
45
  "@hono/node-server": "^1.13.0",
46
+ "@modelcontextprotocol/sdk": "^1.30.0",
46
47
  "hono": "^4.6.0",
47
48
  "smol-toml": "^1.7.0",
48
49
  "ws": "^8.21.1",
@@ -50,8 +51,8 @@
50
51
  "zod": "^4.4.3"
51
52
  },
52
53
  "devDependencies": {
53
- "@open-mercato/cezar-api-client": "^0.14.6",
54
- "@open-mercato/cezar-contract": "^0.14.6",
54
+ "@open-mercato/cezar-api-client": "^0.14.7",
55
+ "@open-mercato/cezar-contract": "^0.14.7",
55
56
  "@types/node": "^20.14.0",
56
57
  "@types/ws": "^8.18.1",
57
58
  "esbuild": "^0.28.1",
@@ -0,0 +1,53 @@
1
+ // Offline harness fixtures share the real bundled MCP protocol, never a fake receipt.
2
+ import { writeFileSync, existsSync } from 'node:fs';
3
+ import { basename } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
6
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
7
+
8
+ export async function probeCiTool(backend, wire, pr = process.env.CEZ_MOCK_CI_PR) {
9
+ if (!pr) return;
10
+ let result;
11
+ if (backend === 'pi') {
12
+ const extensions = wire.flatMap((arg, index) => arg === '--extension' ? [wire[index + 1]] : []);
13
+ const path = extensions.find(path => basename(path) === 'pi-ci-wait.mjs');
14
+ if (!path) throw new Error('CI extension absent');
15
+ // The real Pi loader compiles TS when running from source; installed script uses JS.
16
+ if (existsSync(new URL('../src/ci-wait/mcp.ts', import.meta.url))) { const { register } = await import('tsx/esm/api'); register(); }
17
+ const { default: extension } = await import(pathToFileURL(path));
18
+ const tools = [];
19
+ extension({ registerTool(tool) { tools.push(tool); } });
20
+ result = { names: tools.map(tool => tool.name), response: await tools[0].execute('ci-1', { pr }) };
21
+ } else {
22
+ let server; let env;
23
+ if (backend === 'claude') {
24
+ server = Object.values(JSON.parse(wire[wire.indexOf('--mcp-config') + 1]).mcpServers)[0];
25
+ env = Object.fromEntries(Object.entries(server.env ?? {}).map(([key, value]) => [key, value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? '')]));
26
+ } else if (backend === 'codex') {
27
+ server = Object.entries(wire.config ?? {}).find(([key]) => key.startsWith('mcp_servers.cezar_ci_'))?.[1];
28
+ env = Object.fromEntries((server?.env_vars ?? []).map(name => [name, process.env[name]]));
29
+ } else if (backend === 'cursor') {
30
+ server = wire.mcpServers.find(server => server.name.startsWith('cezar_ci_'));
31
+ env = Object.fromEntries(server?.env.map(({name,value}) => [name,value]) ?? []);
32
+ } else {
33
+ const local = Object.entries(wire.mcp ?? {}).find(([key]) => key.startsWith('cezar_ci_'))?.[1];
34
+ server = local && { command: local.command[0], args: local.command.slice(1) };
35
+ env = { ...process.env, ...local?.environment };
36
+ }
37
+ if (!server) throw new Error('CI server absent');
38
+ const client = new Client({ name: 'offline-harness', version: '1' });
39
+ try {
40
+ await client.connect(new StdioClientTransport({ command: server.command, args: server.args, env, stderr: 'pipe' }));
41
+ const list = await client.listTools();
42
+ result = { names: list.tools.map(tool => tool.name), response: await client.callTool({ name: 'cezar_wait_for_ci', arguments: { pr } }) };
43
+ } finally { await client.close(); }
44
+ }
45
+ if (process.env.CEZ_MOCK_CI_RESULT) writeFileSync(process.env.CEZ_MOCK_CI_RESULT, JSON.stringify(result));
46
+ return result;
47
+ }
48
+
49
+ export async function ciPrompt(backend, wire, text) {
50
+ const pr = /mock:ci-wait(?:\s+(https:\/\/[^\s\"\\]+))?/.exec(text)?.[1] ?? 'https://github.com/owner/repo/pull/1';
51
+ const result = await probeCiTool(backend, wire, pr);
52
+ return JSON.stringify(result.response);
53
+ }
@@ -22,6 +22,7 @@ if (process.env.CEZ_MOCK_ARGS_FILE) {
22
22
  }
23
23
  }
24
24
 
25
+ if (process.env.CEZ_MOCK_CI_PR) { const { probeCiTool } = await import('./mock-ci-tool.mjs'); await probeCiTool('claude', process.argv.slice(2)); }
25
26
  emit({ type: 'system', subtype: 'init' });
26
27
 
27
28
  let turn = 0;
@@ -75,6 +76,13 @@ function writeHandoffAndTodo() {
75
76
  }
76
77
 
77
78
  async function respond(userText, imageCount) {
79
+ if (userText.includes('mock:ci-wait')) {
80
+ const { ciPrompt } = await import('./mock-ci-tool.mjs');
81
+ const text = await ciPrompt('claude', process.argv.slice(2), userText);
82
+ emit({ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text }] } });
83
+ emit({ type: 'result', subtype: 'success', result: text, usage: { input_tokens: 20, output_tokens: 10 } });
84
+ return;
85
+ }
78
86
  turn += 1;
79
87
  await sleep(250);
80
88
  // `mock:done` anywhere in the message → the reply ends with the CEZ:DONE
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ // Bundled dry-run mock of `codex app-server` — speaks just enough JSON-RPC 2.0
3
+ // JSONL (§3 of agent-event-protocols.md) for the runner wiring test in
4
+ // `codex-ui-mapper.test.ts`: initialize/thread/turn handshake, one scripted
5
+ // turn with an agentMessage + a commandExecution (with live outputDelta),
6
+ // cumulative token usage, then exits on stdin EOF like the real server.
7
+ //
8
+ // `MOCK_CODEX_IGNORE_EOF=1` switches to the #703 teardown shape instead: the
9
+ // server stays deaf to stdin EOF (the CLI hang the EOF watchdog exists for)
10
+ // and handles SIGTERM itself, exiting 143 rather than dying from the signal.
11
+ import { createInterface } from 'node:readline';
12
+ import { appendFileSync } from 'node:fs';
13
+
14
+ const emit = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`);
15
+ const rl = createInterface({ input: process.stdin });
16
+ let echoSerial = 0;
17
+ let ciWire;
18
+
19
+ const ignoreEof = process.env.MOCK_CODEX_IGNORE_EOF === '1';
20
+ if (ignoreEof) {
21
+ process.on('SIGTERM', () => process.exit(143));
22
+ // Keep the event loop alive so EOF alone can never end the process.
23
+ setInterval(() => {}, 60_000);
24
+ }
25
+
26
+ rl.on('line', async (line) => {
27
+ let msg;
28
+ try {
29
+ msg = JSON.parse(line);
30
+ } catch {
31
+ return;
32
+ }
33
+ if (process.env.CEZ_MOCK_ARGS_FILE) appendFileSync(process.env.CEZ_MOCK_ARGS_FILE, `${JSON.stringify(msg)}\n`);
34
+ if (msg.id === 'ask-bad-1' && msg.error) {
35
+ // The runner rejected the malformed payload with -32602, as it must. A real
36
+ // app-server carries on from there, so the turn still completes — a mock
37
+ // that went silent here would make harness parity R4 pass by hanging
38
+ // instead of by ending the turn.
39
+ emit({ method: 'turn/completed', params: { turn: { id: 'turn_mock_1', status: 'completed' } } });
40
+ } else if (msg.id === 'ask-1' && msg.result) {
41
+ const answer = msg.result.answers?.library?.answers;
42
+ const freeText = msg.result.answers?.first?.answers;
43
+ emit((Array.isArray(answer) && answer[0] === 'Vitest') || (Array.isArray(freeText) && freeText[0] === 'Use sensible defaults')
44
+ ? { method: 'turn/completed', params: { turn: { id: 'turn_mock_1', status: 'completed' } } }
45
+ : { method: 'turn/failed', params: { turn: { id: 'turn_mock_1', status: 'failed' }, error: { message: 'bad answer' } } });
46
+ } else if (msg.method === 'initialize') {
47
+ emit({ id: msg.id, result: { userAgent: 'mock-codex/0.0.0' } });
48
+ } else if (msg.method === 'thread/start' || msg.method === 'thread/resume') {
49
+ if (process.env.CEZ_MOCK_CI_PR) { const { probeCiTool } = await import('./mock-ci-tool.mjs'); await probeCiTool('codex', msg.params); }
50
+ ciWire = msg.params;
51
+ const expectedSandbox = process.env.CEZ_CODEX_NETWORK === '0' ? 'workspace-write' : 'danger-full-access';
52
+ if (msg.params?.sandbox !== expectedSandbox || msg.params?.approvalPolicy !== undefined) {
53
+ emit({ id: msg.id, error: { code: -32602, message: `expected ${expectedSandbox} managed permissions` } });
54
+ return;
55
+ }
56
+ if (process.argv.includes('sandbox_workspace_write.network_access=true')) {
57
+ emit({ id: msg.id, error: { code: -32602, message: 'workspace-write override is obsolete in full-access mode' } });
58
+ return;
59
+ }
60
+ if (msg.method === 'thread/start') {
61
+ emit({ method: 'thread/started', params: { thread: { id: 'th_mock_1' } } });
62
+ emit({ id: msg.id, result: { thread: { id: 'th_mock_1' } } });
63
+ } else if (process.env.MOCK_CODEX_REJECT_RESUME === '1') {
64
+ emit({ id: msg.id, error: { code: -32603, message: `no rollout found for thread id ${msg.params?.threadId ?? ''}` } });
65
+ rl.close();
66
+ } else {
67
+ emit({ id: msg.id, result: { thread: { id: msg.params?.threadId } } });
68
+ }
69
+ } else if (msg.method === 'turn/start') {
70
+ emit({ id: msg.id, result: { turn: { id: 'turn_mock_1' } } });
71
+ emit({ method: 'turn/started', params: { turn: { id: 'turn_mock_1', status: 'inProgress', items: [] } } });
72
+ const turnText = msg.params?.input?.map?.((part) => part.text ?? '').join('\n') ?? '';
73
+ if (turnText.includes('mock:ci-wait')) {
74
+ const { ciPrompt } = await import('./mock-ci-tool.mjs');
75
+ const text = await ciPrompt('codex', ciWire, turnText);
76
+ emit({ method: 'item/completed', params: { item: { type: 'agentMessage', id: 'ci-result', text } } });
77
+ emit({ method: 'turn/completed', params: { turn: { id: 'turn_mock_1', status: 'completed' } } });
78
+ return;
79
+ }
80
+ if (turnText.includes('mock:agent-echo')) {
81
+ // Same documented agentMessage/turn completion frames as the baseline below.
82
+ emit({ method: 'item/completed', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: `item_echo_${++echoSerial}`, text: turnText } } });
83
+ emit({ method: 'turn/completed', params: { turn: { id: 'turn_mock_1', status: 'completed' } } });
84
+ return;
85
+ }
86
+ if (turnText.includes('mock:split-text')) {
87
+ // Deltas that split the marker itself, then the authoritative snapshot on
88
+ // `item/completed` — codex's real streaming shape. A runner emitting one
89
+ // v1 `text` per delta tears `CEZ:MONITORING` into `CEZ:` + `MONITORING`,
90
+ // which is #2 on codex's wire (harness parity S8).
91
+ const full = 'parity split text\n\nCEZ:MONITORING';
92
+ emit({ method: 'item/started', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: 'item_sp1', text: '' } } });
93
+ for (const delta of ['parity split text\n\n', 'CEZ:', 'MONITORING']) {
94
+ emit({ method: 'item/agentMessage/delta', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', itemId: 'item_sp1', delta } });
95
+ }
96
+ emit({ method: 'item/completed', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: 'item_sp1', text: full } } });
97
+ emit({ method: 'thread/tokenUsage/updated', params: { threadId: 'th_mock_1', tokenUsage: { total: { totalTokens: 30, inputTokens: 20, outputTokens: 10 }, last: { totalTokens: 30, inputTokens: 20, outputTokens: 10 } } } });
98
+ emit({ method: 'turn/completed', params: { turn: { id: 'turn_mock_1', status: 'completed' } } });
99
+ return;
100
+ }
101
+ if (turnText.includes('mock:ask-bad')) {
102
+ // `questions` is not an array of question objects, so `codexAskQuestions`
103
+ // must reject it: no card renders, and the turn still ends once the
104
+ // rejection lands above (harness parity R4).
105
+ emit({ id: 'ask-bad-1', method: 'item/tool/requestUserInput', params: {
106
+ threadId: 'th_mock_1', turnId: 'turn_mock_1', itemId: 'item_ask_bad', autoResolutionMs: null,
107
+ questions: 'not-an-array',
108
+ } });
109
+ return;
110
+ }
111
+ if (turnText.includes('mock:done')) {
112
+ // A turn that DECLARES the task complete, so the run reaches cezar's
113
+ // review gate instead of parking for the user. A markerless turn-end
114
+ // correctly parks as `waiting`, which is why harness parity R1 needs
115
+ // this scenario rather than the baseline one.
116
+ const full = 'parity done: the task is complete\n\nCEZ:DONE';
117
+ emit({ method: 'item/started', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: 'item_d1', text: '' } } });
118
+ emit({ method: 'item/agentMessage/delta', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', itemId: 'item_d1', delta: full } });
119
+ emit({ method: 'item/completed', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: 'item_d1', text: full } } });
120
+ emit({ method: 'thread/tokenUsage/updated', params: { threadId: 'th_mock_1', tokenUsage: { total: { totalTokens: 30, inputTokens: 20, outputTokens: 10 }, last: { totalTokens: 30, inputTokens: 20, outputTokens: 10 } } } });
121
+ emit({ method: 'turn/completed', params: { turn: { id: 'turn_mock_1', status: 'completed' } } });
122
+ return;
123
+ }
124
+ if (turnText.includes('mock:hold')) {
125
+ // The `turn/start` response and `turn/started` above are the ack. Holding
126
+ // the content AND `turn/completed` behind it is what makes harness parity
127
+ // S2 meaningful: a runner deriving turn-end from the ack reports it before
128
+ // this content ever arrives (the #4 failure mode on codex's wire).
129
+ setTimeout(() => {
130
+ const held = 'parity hold: content after the pause';
131
+ emit({ method: 'item/started', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: 'item_h1', text: '' } } });
132
+ emit({ method: 'item/agentMessage/delta', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', itemId: 'item_h1', delta: held } });
133
+ emit({ method: 'item/completed', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: 'item_h1', text: held } } });
134
+ emit({ method: 'thread/tokenUsage/updated', params: { threadId: 'th_mock_1', tokenUsage: { total: { totalTokens: 30, inputTokens: 20, outputTokens: 10 }, last: { totalTokens: 30, inputTokens: 20, outputTokens: 10 } } } });
135
+ emit({ method: 'turn/completed', params: { turn: { id: 'turn_mock_1', status: 'completed' } } });
136
+ }, 250);
137
+ return;
138
+ }
139
+ if (turnText.includes('mock:provider-error') || turnText.includes('mock:empty-success')) {
140
+ // Derived from the 0.147 rollout and upstream envelope; see the source
141
+ // core/__fixtures__/codex/provider-error.md and its independent golden NDJSON.
142
+ const completion = {"method": "turn/completed", "params": {"threadId": "th_mock_1", "turn": {"id": "turn_mock_1", "items": [], "itemsView": "notLoaded", "status": "failed", "error": {"message": "{\"type\":\"error\",\"status\":400,\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The 'gpt-6-astra' model requires a newer version of Codex. Please upgrade to the latest app or CLI and try again.\"}}", "codexErrorInfo": "other", "additionalDetails": null}, "startedAt": 1788632328, "completedAt": 1788632331, "durationMs": 3502}}};
143
+ if (turnText.includes('mock:empty-success')) {
144
+ completion.params.turn.status = 'completed';
145
+ completion.params.turn.error = null;
146
+ }
147
+ emit(completion);
148
+ return;
149
+ }
150
+ if (turnText.includes('mock:turn-failed')) {
151
+ emit({ method: 'turn/failed', params: {
152
+ turn: { id: 'turn_mock_1', status: 'failed' },
153
+ error: { message: turnText.includes('mock:legacy-interrupt') ? 'Turn interrupted by user' : 'model unavailable' },
154
+ } });
155
+ return;
156
+ }
157
+ if (turnText.includes('mock:subagent-activity')) {
158
+ emit({ method: 'item/started', params: { item: { type: 'subAgentActivity', id: 'activity_1', kind: 'started', agentThreadId: 'th_child', agentPath: '/root/scope_review' } } });
159
+ emit({ method: 'item/completed', params: { item: { type: 'subAgentActivity', id: 'activity_1', kind: 'started', agentThreadId: 'th_child', agentPath: '/root/scope_review' } } });
160
+ emit({ method: 'item/started', params: { item: { type: 'collabAgentToolCall', id: 'wait_1', tool: 'wait', status: 'inProgress', receiverThreadIds: [] } } });
161
+ emit({ method: 'item/completed', params: { item: { type: 'collabAgentToolCall', id: 'wait_1', tool: 'wait', status: 'completed', receiverThreadIds: [] } } });
162
+ emit({ method: 'turn/completed', params: { turn: { id: 'turn_mock_1', status: 'completed' } } });
163
+ return;
164
+ }
165
+ if (turnText.includes('mock:child-turn')) {
166
+ // A spawned sub-agent runs in its OWN child thread that emits a full turn
167
+ // lifecycle over the shared connection. Its turn/completed must not end the
168
+ // parent turn (#600): the parent is still working after the child finishes.
169
+ // Collaboration attribution from collab-agent-tool-call.ndjson; late text reproduces #149.
170
+ emit({ method: 'item/started', params: { threadId: 'th_mock_1', item: { type: 'collabAgentToolCall', id: 'item_spawn', tool: 'spawnAgent', status: 'inProgress', receiverThreadIds: ['th_child'] } } });
171
+ emit({ method: 'turn/started', params: { threadId: 'th_child', turn: { id: 'turn_child', status: 'inProgress', items: [] } } });
172
+ emit({ method: 'item/started', params: { threadId: 'th_child', turnId: 'turn_child', item: { type: 'commandExecution', id: 'item_child', command: ['rg', 'requestUserInput'], cwd: '/repo', status: 'inProgress' } } });
173
+ emit({ method: 'turn/completed', params: { threadId: 'th_child', turn: { id: 'turn_child', status: 'completed' } } });
174
+ // Parent keeps streaming after the child's turn ended.
175
+ emit({ method: 'item/started', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: 'item_p1', text: '' } } });
176
+ emit({ method: 'item/agentMessage/delta', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', itemId: 'item_p1', delta: 'Still working after the sub-agent.\nCEZ:MONITORING' } });
177
+ emit({ method: 'item/completed', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: 'item_p1', text: 'Still working after the sub-agent.\nCEZ:MONITORING' } } });
178
+ emit({ method: 'item/completed', params: { threadId: 'th_child', turnId: 'turn_child', item: { type: 'agentMessage', id: 'item_child_text', text: 'Child review finished.' } } });
179
+ // No completion for this second child item: parent turn-end must not flush it into v1.
180
+ emit({ method: 'item/agentMessage/delta', params: { threadId: 'th_child', turnId: 'turn_child', itemId: 'item_child_partial', delta: 'Child review still streaming.' } });
181
+ emit({ method: 'turn/completed', params: { threadId: 'th_mock_1', turn: { id: 'turn_mock_1', status: 'completed' } } });
182
+ return;
183
+ }
184
+ if (process.env.MOCK_CODEX_ASK === '1' || turnText.includes('mock:native-codex-ask')) {
185
+ const questions = turnText.includes('multi free text')
186
+ ? [{ id: 'first', header: 'First', question: 'First choice?', isOther: true, isSecret: false,
187
+ options: [{ label: 'A', description: 'Option A.' }, { label: 'B', description: 'Option B.' }] },
188
+ { id: 'second', header: 'Second', question: 'Second choice?', isOther: true, isSecret: false,
189
+ options: [{ label: 'C', description: 'Option C.' }, { label: 'D', description: 'Option D.' }] }]
190
+ : [{ id: 'library', header: 'Library', question: 'Which test library?', isOther: true,
191
+ isSecret: false, options: [{ label: 'Vitest', description: 'Use the existing test runner.' },
192
+ { label: 'Node test', description: 'Use node:test.' }] }];
193
+ emit({ id: 'ask-1', method: 'item/tool/requestUserInput', params: {
194
+ threadId: 'th_mock_1', turnId: 'turn_mock_1', itemId: 'item_ask_1', autoResolutionMs: null,
195
+ questions,
196
+ } });
197
+ return;
198
+ }
199
+ emit({ method: 'item/started', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: 'item_m1', text: '' } } });
200
+ emit({ method: 'item/agentMessage/delta', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', itemId: 'item_m1', delta: 'Checking the working tree.' } });
201
+ emit({ method: 'item/completed', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'agentMessage', id: 'item_m1', text: 'Checking the working tree.' } } });
202
+ emit({ method: 'item/started', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'commandExecution', id: 'item_c1', command: ['bash', '-lc', 'git status --short'], cwd: '/repo', status: 'inProgress' } } });
203
+ emit({ method: 'item/commandExecution/outputDelta', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', itemId: 'item_c1', delta: ' M src/example.ts\n' } });
204
+ emit({ method: 'item/completed', params: { threadId: 'th_mock_1', turnId: 'turn_mock_1', item: { type: 'commandExecution', id: 'item_c1', command: ['bash', '-lc', 'git status --short'], cwd: '/repo', status: 'completed', exitCode: 0 } } });
205
+ emit({ method: 'thread/tokenUsage/updated', params: { threadId: 'th_mock_1', tokenUsage: { total: { totalTokens: 1500, inputTokens: 1200, outputTokens: 300 }, last: { totalTokens: 1500, inputTokens: 1200, outputTokens: 300 } } } });
206
+ emit({ method: 'turn/completed', params: { turn: { id: 'turn_mock_1', status: 'completed' } } });
207
+ }
208
+ });
209
+
210
+ rl.on('close', () => {
211
+ if (!ignoreEof) process.exit(0);
212
+ });
@@ -2,7 +2,7 @@
2
2
  // Offline Cursor ACP wire. Shapes: cursor.com/docs/cli/acp; ACP v1 schema.
3
3
  import { createInterface } from 'node:readline';
4
4
  import { appendFileSync } from 'node:fs';
5
- const record = (file, value) => { if (file) appendFileSync(file, `${JSON.stringify(value)}\n`); };
5
+ const record = (file, value) => { if (file) appendFileSync(file, `${JSON.stringify(value, (key, value) => key === 'env' && Array.isArray(value) ? value.map(item => item.name?.startsWith('CEZ_TOOL_') ? { ...item, value: '[redacted]' } : item) : value)}\n`); };
6
6
  record(process.env.CEZ_MOCK_ARGS_FILE, process.argv.slice(2));
7
7
  const emit = value => process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...value })}\n`);
8
8
  const reply = (id, result) => emit({ id, result });
@@ -18,6 +18,7 @@ const configOptions = () => [
18
18
  ];
19
19
  const configuration = () => process.env.CEZ_MOCK_CURSOR_LEGACY === '1' ? {} : { configOptions: configOptions() };
20
20
  let pendingAsk;
21
+ let ciWire;
21
22
  let resumeDone = false;
22
23
  let prompts = 0;
23
24
  const update = (value, id = sessionId) => emit({ method: 'session/update', params: { sessionId: id, update: value } });
@@ -27,6 +28,7 @@ const question = { id: 'tests', prompt: 'Which test runner?', options: [{ id: 'v
27
28
  async function prompt(id, content) {
28
29
  const input = content.filter(b => b.type === 'text').map(b => b.text).join('\n');
29
30
  prompts += 1;
31
+ if (input.includes('mock:ci-wait')) { const { ciPrompt } = await import('./mock-ci-tool.mjs'); text(await ciPrompt('cursor', ciWire, input)); complete(id); return; }
30
32
  if (resumeDone) { resumeDone = false; text('Resumed work finished.\nCEZ:DONE'); complete(id); return; }
31
33
  if (input.includes('mock:hold')) await new Promise(r => setTimeout(r, 500));
32
34
  if (input.includes('mock:rpc-error')) { emit({ id, error: { code: -32603, message: 'Provider rejected request' } }); return; }
@@ -60,7 +62,7 @@ async function prompt(id, content) {
60
62
  text('Cursor inspected the workspace.');
61
63
  complete(id);
62
64
  }
63
- createInterface({ input: process.stdin }).on('line', line => {
65
+ createInterface({ input: process.stdin }).on('line', async line => {
64
66
  let msg; try { msg = JSON.parse(line); } catch { return; }
65
67
  record(process.env.CEZ_MOCK_ARGS_FILE, msg);
66
68
  record(process.env.CEZ_MOCK_STDIN_FILE, msg);
@@ -82,6 +84,8 @@ createInterface({ input: process.stdin }).on('line', line => {
82
84
  }
83
85
  return;
84
86
  }
87
+ if (process.env.CEZ_MOCK_CI_PR && ['session/new', 'session/load'].includes(msg.method)) { const { probeCiTool } = await import('./mock-ci-tool.mjs'); await probeCiTool('cursor', msg.params); }
88
+ if (['session/new', 'session/load'].includes(msg.method)) ciWire = msg.params;
85
89
  switch (msg.method) {
86
90
  case 'initialize': reply(msg.id, { protocolVersion: 1, agentCapabilities: { loadSession: true, promptCapabilities: { image: true } } }); break;
87
91
  case 'cursor/list_available_models': reply(msg.id, { models: [{ value: model, name: model, configOptions: configOptions().filter(option => option.id !== 'model') }] }); break;