mixdog 0.9.38 → 0.9.40

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 (124) hide show
  1. package/package.json +9 -4
  2. package/scripts/abort-recovery-test.mjs +43 -2
  3. package/scripts/agent-tag-reuse-smoke.mjs +146 -6
  4. package/scripts/agent-terminal-reap-test.mjs +127 -0
  5. package/scripts/agent-trace-io-test.mjs +69 -0
  6. package/scripts/code-graph-disk-hit-test.mjs +224 -0
  7. package/scripts/execution-completion-dedup-test.mjs +157 -0
  8. package/scripts/execution-pending-resume-kick-test.mjs +57 -2
  9. package/scripts/execution-resume-esc-integration-test.mjs +176 -0
  10. package/scripts/explore-bench.mjs +38 -2
  11. package/scripts/explore-prompt-policy-test.mjs +152 -11
  12. package/scripts/find-fuzzy-hidden-test.mjs +122 -0
  13. package/scripts/internal-comms-bench-test.mjs +226 -0
  14. package/scripts/internal-comms-bench.mjs +185 -58
  15. package/scripts/internal-comms-smoke.mjs +171 -23
  16. package/scripts/live-worker-smoke.mjs +38 -2
  17. package/scripts/memory-cycle-routing-test.mjs +111 -0
  18. package/scripts/memory-rule-contract-test.mjs +93 -0
  19. package/scripts/output-style-smoke.mjs +2 -2
  20. package/scripts/rg-runner-test.mjs +240 -0
  21. package/scripts/routing-corpus-test.mjs +349 -0
  22. package/scripts/routing-corpus.mjs +211 -32
  23. package/scripts/session-orphan-sweep-test.mjs +83 -0
  24. package/scripts/steering-drain-buckets-test.mjs +316 -2
  25. package/scripts/tool-smoke.mjs +21 -13
  26. package/scripts/tool-tui-presentation-test.mjs +202 -0
  27. package/scripts/tui-transcript-perf-test.mjs +279 -0
  28. package/src/agents/heavy-worker/AGENT.md +10 -7
  29. package/src/agents/reviewer/AGENT.md +6 -4
  30. package/src/agents/worker/AGENT.md +7 -5
  31. package/src/rules/agent/00-common.md +4 -4
  32. package/src/rules/agent/00-core.md +11 -14
  33. package/src/rules/agent/20-skip-protocol.md +3 -3
  34. package/src/rules/agent/30-explorer.md +50 -60
  35. package/src/rules/agent/40-cycle1-agent.md +15 -24
  36. package/src/rules/agent/41-cycle2-agent.md +33 -57
  37. package/src/rules/agent/42-cycle3-agent.md +28 -42
  38. package/src/rules/lead/01-general.md +7 -10
  39. package/src/rules/lead/lead-brief.md +11 -14
  40. package/src/rules/lead/lead-tool.md +6 -5
  41. package/src/rules/shared/01-tool.md +44 -45
  42. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -1
  43. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
  44. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
  46. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
  47. package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
  48. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
  49. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -0
  50. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
  51. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
  52. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +67 -2
  53. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/store.mjs +232 -43
  56. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
  58. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +27 -13
  59. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
  60. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
  62. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
  63. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
  65. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
  66. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
  67. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
  68. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
  69. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
  70. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
  72. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
  73. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
  74. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
  75. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
  76. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
  77. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
  78. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  80. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
  81. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
  82. package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
  83. package/src/runtime/shared/buffered-appender.mjs +13 -2
  84. package/src/runtime/shared/tool-primitives.mjs +4 -1
  85. package/src/runtime/shared/tool-status.mjs +27 -0
  86. package/src/runtime/shared/tool-surface.mjs +6 -3
  87. package/src/session-runtime/config-helpers.mjs +14 -0
  88. package/src/session-runtime/context-status.mjs +1 -0
  89. package/src/session-runtime/effort.mjs +6 -2
  90. package/src/session-runtime/lifecycle-api.mjs +4 -0
  91. package/src/session-runtime/model-recency.mjs +5 -2
  92. package/src/session-runtime/runtime-core.mjs +36 -2
  93. package/src/session-runtime/session-turn-api.mjs +12 -0
  94. package/src/session-runtime/tool-catalog.mjs +34 -0
  95. package/src/standalone/agent-tool/notify.mjs +13 -0
  96. package/src/standalone/agent-tool.mjs +53 -70
  97. package/src/standalone/explore-tool.mjs +6 -7
  98. package/src/tui/App.jsx +33 -1
  99. package/src/tui/app/model-options.mjs +5 -3
  100. package/src/tui/app/model-picker.mjs +12 -24
  101. package/src/tui/app/transcript-window.mjs +10 -10
  102. package/src/tui/app/use-transcript-window.mjs +38 -56
  103. package/src/tui/components/ToolExecution.jsx +11 -6
  104. package/src/tui/components/TranscriptItem.jsx +1 -1
  105. package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
  106. package/src/tui/components/tool-execution/text-format.mjs +10 -19
  107. package/src/tui/dist/index.mjs +866 -300
  108. package/src/tui/engine/agent-job-feed.mjs +216 -19
  109. package/src/tui/engine/agent-response-tail.mjs +68 -0
  110. package/src/tui/engine/notification-plan.mjs +16 -0
  111. package/src/tui/engine/session-api.mjs +14 -2
  112. package/src/tui/engine/session-flow.mjs +22 -2
  113. package/src/tui/engine/tool-card-results.mjs +64 -37
  114. package/src/tui/engine/tool-result-status.mjs +75 -21
  115. package/src/tui/engine/turn.mjs +172 -77
  116. package/src/tui/engine.mjs +199 -39
  117. package/src/tui/index.jsx +2 -2
  118. package/src/workflows/bench/WORKFLOW.md +25 -35
  119. package/src/workflows/default/WORKFLOW.md +38 -32
  120. package/src/workflows/solo/WORKFLOW.md +19 -22
  121. package/scripts/_jitter-fuzz.mjs +0 -44410
  122. package/scripts/_jitter-fuzz2.mjs +0 -44400
  123. package/scripts/_jitter-probe.mjs +0 -44397
  124. package/scripts/_jp2.mjs +0 -45614
@@ -0,0 +1,202 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import {
5
+ aggregateToolCategoryEntry,
6
+ formatAggregateHeader,
7
+ summarizeToolArgs,
8
+ toolWorkUnit,
9
+ } from '../src/runtime/shared/tool-surface.mjs';
10
+ import {
11
+ normalizeToolTerminalStatus,
12
+ toolResultTerminalStatus,
13
+ } from '../src/runtime/shared/tool-status.mjs';
14
+ import {
15
+ aggregateBucketForCategory,
16
+ toolCallOutcome,
17
+ } from '../src/tui/engine/tool-result-status.mjs';
18
+ import { createToolCardResults } from '../src/tui/engine/tool-card-results.mjs';
19
+ import {
20
+ appendAgentResponseTail,
21
+ formatAgentResponseRaw,
22
+ } from '../src/tui/engine/agent-response-tail.mjs';
23
+
24
+ test('tool action copy keeps Add/Delete patch verbs and human read offsets', () => {
25
+ assert.equal(
26
+ toolWorkUnit('apply_patch', { patch: '*** Begin Patch\n*** Add File: new.mjs\n+x\n*** End Patch' }).done,
27
+ 'Created',
28
+ );
29
+ assert.equal(
30
+ toolWorkUnit('apply_patch', { patch: '*** Begin Patch\n*** Delete File: old.mjs\n*** End Patch' }).active,
31
+ 'Deleting',
32
+ );
33
+ assert.match(summarizeToolArgs('read', { path: 'a.mjs', offset: 0, limit: 10 }), /lines 1-10/);
34
+ });
35
+
36
+ test('Agent aggregation is batch-scoped and keeps category action wording', () => {
37
+ const entry = aggregateToolCategoryEntry('agent', { type: 'spawn' }, 'Agent');
38
+ assert.equal(formatAggregateHeader({ [entry.key]: { ...entry, count: 2 } }, { pending: false }), 'Called 2 agents');
39
+ assert.notEqual(
40
+ aggregateBucketForCategory('Agent', { agentBatch: 1 }),
41
+ aggregateBucketForCategory('Agent', { agentBatch: 2 }),
42
+ );
43
+ });
44
+
45
+ test('terminal result statuses stay separate from tool-call failure accounting', () => {
46
+ assert.equal(normalizeToolTerminalStatus('denied'), 'denied');
47
+ assert.equal(toolResultTerminalStatus('[status: cancelled]\n'), 'cancelled');
48
+
49
+ const state = {
50
+ items: [{
51
+ id: 'aggregate',
52
+ kind: 'tool',
53
+ result: null,
54
+ count: 1,
55
+ completedCount: 0,
56
+ }],
57
+ };
58
+ const aggregate = {
59
+ itemId: 'aggregate',
60
+ calls: new Map([['call_1', { name: 'web_fetch', args: {}, category: 'Web Research' }]]),
61
+ nextSummarySeq: 0,
62
+ ensureVisible: () => {},
63
+ };
64
+ const { patchToolCardResult } = createToolCardResults({
65
+ getState: () => state,
66
+ set: () => {},
67
+ patchItem: (id, patch) => {
68
+ const index = state.items.findIndex((item) => item.id === id);
69
+ state.items[index] = { ...state.items[index], ...patch };
70
+ return true;
71
+ },
72
+ markToolCallDone: () => {},
73
+ updateAgentJobCard: () => {},
74
+ buildAgentJobCardPatch: () => ({}),
75
+ agentStatusState: () => ({}),
76
+ });
77
+ const card = { itemId: 'aggregate', callId: 'call_1', done: false, aggregate };
78
+ assert.equal(patchToolCardResult(card, {
79
+ tool_call_id: 'call_1',
80
+ content: 'status: failed\nHTTP 404 Not Found',
81
+ }, new Map(), new Set()), true);
82
+ assert.equal(state.items[0].errorCount, 0);
83
+ assert.equal(state.items[0].callErrorCount, 0);
84
+ assert.equal(aggregate.calls.get('call_1').isError, false);
85
+ });
86
+
87
+ function patchAggregate(messages) {
88
+ const state = { items: [{ id: 'aggregate', kind: 'tool', result: null, count: messages.length, completedCount: 0 }] };
89
+ const aggregate = {
90
+ itemId: 'aggregate',
91
+ calls: new Map(messages.map((message) => [message.tool_call_id, {
92
+ name: 'shell',
93
+ args: {},
94
+ category: 'Shell',
95
+ }])),
96
+ nextSummarySeq: 0,
97
+ ensureVisible: () => {},
98
+ };
99
+ const { patchToolCardResult } = createToolCardResults({
100
+ getState: () => state,
101
+ set: () => {},
102
+ patchItem: (id, patch) => {
103
+ const index = state.items.findIndex((item) => item.id === id);
104
+ state.items[index] = { ...state.items[index], ...patch };
105
+ return true;
106
+ },
107
+ markToolCallDone: () => {},
108
+ updateAgentJobCard: () => {},
109
+ buildAgentJobCardPatch: () => ({}),
110
+ agentStatusState: () => ({}),
111
+ });
112
+ for (const message of messages) {
113
+ patchToolCardResult(
114
+ { itemId: 'aggregate', callId: message.tool_call_id, done: false, aggregate },
115
+ message,
116
+ new Map(),
117
+ new Set(),
118
+ );
119
+ }
120
+ return state.items[0];
121
+ }
122
+
123
+ test('exit detail stays successful and does not inflate Ok or Failed counts', () => {
124
+ const exit = 'Error: [shell-run-failed] [exit code: 1]\ncommand output';
125
+ const exitOnly = patchAggregate([{ tool_call_id: 'exit', content: exit, toolKind: 'error' }]);
126
+ assert.equal(exitOnly.errorCount, 0);
127
+ assert.equal(exitOnly.callErrorCount, 0);
128
+ assert.equal(exitOnly.exitErrorCount, 1);
129
+ assert.equal(exitOnly.result, 'Exit 1');
130
+
131
+ const mixed = patchAggregate([
132
+ { tool_call_id: 'failed', content: 'transport unavailable', isError: true },
133
+ { tool_call_id: 'exit', content: exit, isError: true },
134
+ ]);
135
+ assert.equal(mixed.errorCount, 1);
136
+ assert.equal(mixed.callErrorCount, 1);
137
+ assert.equal(mixed.exitErrorCount, 1);
138
+ assert.equal(mixed.result, '1 Failed · 1 Exit');
139
+ });
140
+
141
+ test('eager call outcome uses only envelope failure metadata', () => {
142
+ const exit = 'Error: [shell-run-failed] [exit code: 1]';
143
+ assert.equal(toolCallOutcome({ content: 'Error: domain failure' }, 'Error: domain failure').isCallError, false);
144
+ assert.equal(toolCallOutcome({ isError: true, content: 'ok-looking body' }, 'ok-looking body').isCallError, true);
145
+ assert.deepEqual(toolCallOutcome({ toolKind: 'error' }, exit), { isCallError: false, isExitError: true, exitCode: 1 });
146
+ assert.deepEqual(toolCallOutcome({ isError: true }, exit), { isCallError: false, isExitError: true, exitCode: 1 });
147
+ assert.equal(toolCallOutcome({ isError: true }, 'Error: [shell-run-failed] [timeout: 1s]').isCallError, true);
148
+ });
149
+
150
+ test('tail response aggregation is ordered, idempotent, and boundary-safe', () => {
151
+ const first = {
152
+ kind: 'tool',
153
+ agentDirection: 'inbound',
154
+ agentResponseKey: 'one',
155
+ agentResponseHasBody: true,
156
+ agentResponseEntries: [{ key: 'one', raw: 'body one', result: 'one', hasBody: true, isError: false }],
157
+ };
158
+ const secondPatch = appendAgentResponseTail(first, {
159
+ key: 'two', args: { type: 'result' }, rawResult: 'body two', result: 'two', hasBody: true,
160
+ }, 1);
161
+ const second = { ...first, ...secondPatch };
162
+ const thirdPatch = appendAgentResponseTail(second, {
163
+ key: 'three', args: { type: 'result' }, rawResult: 'body three', result: 'three', hasBody: true,
164
+ }, 2);
165
+ assert.equal(thirdPatch.count, 3);
166
+ assert.equal(
167
+ thirdPatch.rawResult,
168
+ '1. agent\nbody one\n\n2. agent\nbody two\n\n3. agent\nbody three',
169
+ );
170
+ const retryPatch = appendAgentResponseTail({ ...second, ...thirdPatch }, {
171
+ key: 'two', args: { type: 'result' }, rawResult: 'body two retry', result: 'two retry', hasBody: true,
172
+ }, 3);
173
+ assert.equal(retryPatch.count, 3);
174
+ assert.match(retryPatch.rawResult, /2\. agent\nbody two retry/);
175
+ assert.equal(appendAgentResponseTail({ kind: 'user' }, {
176
+ key: 'four', rawResult: 'body four', result: 'four', hasBody: true,
177
+ }), null);
178
+ assert.equal(formatAgentResponseRaw(retryPatch.agentResponseEntries).match(/\d+\. agent/g).length, 3);
179
+ });
180
+
181
+ test('failure preview upgrades its tail entry, but a boundary forces a new body card', () => {
182
+ const failurePreview = {
183
+ kind: 'tool',
184
+ agentDirection: 'inbound',
185
+ agentResponseKey: 'task_1',
186
+ agentResponseHasBody: false,
187
+ agentResponseEntries: [{
188
+ key: 'task_1', raw: 'failed preview', result: 'failed', hasBody: false, isError: true,
189
+ }],
190
+ };
191
+ const upgrade = appendAgentResponseTail(failurePreview, {
192
+ key: 'task_1', args: { type: 'result' }, rawResult: 'final body', result: 'final', hasBody: true, isError: false,
193
+ }, 1);
194
+ assert.equal(upgrade.count, 1);
195
+ assert.equal(upgrade.rawResult, '1. agent\nfinal body');
196
+ assert.equal(upgrade.isError, false);
197
+
198
+ const boundary = appendAgentResponseTail({ kind: 'assistant', text: 'intervening text' }, {
199
+ key: 'task_1', args: { type: 'result' }, rawResult: 'final body', result: 'final', hasBody: true,
200
+ });
201
+ assert.equal(boundary, null);
202
+ });
@@ -0,0 +1,279 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { createEngineItemMutators, replaceEngineItemsState } from '../src/tui/engine.mjs';
5
+ import { createEngineApiA } from '../src/tui/engine/session-api.mjs';
6
+ import { createRunTurn } from '../src/tui/engine/turn.mjs';
7
+ import { buildTranscriptRowIndexIncremental } from '../src/tui/app/transcript-window.mjs';
8
+
9
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10
+
11
+ function makeTurnHarness(ask) {
12
+ let seq = 0;
13
+ let state = {
14
+ items: [],
15
+ structureRevision: 0,
16
+ streamingTail: null,
17
+ stats: { turns: 0, inputTokens: 0, outputTokens: 0 },
18
+ busy: false,
19
+ spinner: null,
20
+ thinking: null,
21
+ };
22
+ const itemIndexById = new Map();
23
+ const set = (patch) => { state = { ...state, ...patch }; return true; };
24
+ const pushItem = (item) => {
25
+ const items = [...state.items, item];
26
+ if (item?.id != null) itemIndexById.set(item.id, items.length - 1);
27
+ set({ items, structureRevision: state.structureRevision + 1 });
28
+ };
29
+ const updateStreamingTail = (id, patch) => set({
30
+ streamingTail: {
31
+ ...(state.streamingTail?.id === id ? state.streamingTail : {}),
32
+ ...patch,
33
+ kind: 'assistant',
34
+ id,
35
+ streaming: true,
36
+ },
37
+ });
38
+ const { patchItem, settleStreamingTail } = createEngineItemMutators({
39
+ getState: () => state,
40
+ set,
41
+ itemIndexById,
42
+ });
43
+ const clearStreamingTail = (id = null) => {
44
+ if (id == null || state.streamingTail?.id === id) set({ streamingTail: null });
45
+ };
46
+ const bag = {
47
+ runtime: { id: null, toolMode: 'auto', ask, abort: () => true },
48
+ nextId: () => `id_${++seq}`,
49
+ tuiDebug: () => {},
50
+ LEAD_TURN_TIMEOUT_MS: 300_000,
51
+ flags: { leadTurnEpoch: 0, disposed: false },
52
+ pending: [],
53
+ itemIndexById,
54
+ getState: () => state,
55
+ set,
56
+ pushItem,
57
+ patchItem,
58
+ updateStreamingTail,
59
+ settleStreamingTail,
60
+ clearStreamingTail,
61
+ pushNotice: () => {},
62
+ pushUserOrSyntheticItem: () => {},
63
+ markToolCallActive: () => {},
64
+ markToolCallDone: () => {},
65
+ clearActiveToolSummary: () => {},
66
+ agentStatusState: () => ({}),
67
+ routeState: () => ({}),
68
+ syncContextStats: () => {},
69
+ denyAllToolApprovals: () => {},
70
+ requestToolApproval: async () => ({ approved: false }),
71
+ patchToolCardResult: () => {},
72
+ flushToolResults: () => {},
73
+ flushDeferredExecutionPendingResumeKick: () => {},
74
+ drain: async () => {},
75
+ drainPendingSteering: () => [],
76
+ };
77
+ return { runTurn: createRunTurn(bag), getState: () => state };
78
+ }
79
+
80
+ test('stream flush keeps settled items identity and finalize appends one assistant', async () => {
81
+ let identityStable = false;
82
+ const harness = makeTurnHarness(async (_text, options) => {
83
+ const before = harness.getState().items;
84
+ options.onTextDelta('settled line\n');
85
+ await wait(30);
86
+ identityStable = harness.getState().items === before;
87
+ assert.match(harness.getState().streamingTail?.text || '', /settled line/);
88
+ return { result: { content: 'settled line\n' }, session: { messages: [] } };
89
+ });
90
+
91
+ assert.equal(await harness.runTurn('go'), 'done');
92
+ assert.equal(identityStable, true, 'tail flush must not swap settled items');
93
+ assert.equal(harness.getState().streamingTail, null);
94
+ const assistants = harness.getState().items.filter((item) => item.kind === 'assistant');
95
+ assert.equal(assistants.length, 1);
96
+ assert.equal(assistants[0].streaming, false);
97
+ assert.equal(assistants[0].text, 'settled line\n');
98
+ });
99
+
100
+ test('abort after stream progress settles the tail and leaves no orphan', async () => {
101
+ const harness = makeTurnHarness(async (_text, options) => {
102
+ options.onTextDelta('partial line\n');
103
+ await wait(30);
104
+ const error = new Error('interrupted');
105
+ error.name = 'SessionClosedError';
106
+ throw error;
107
+ });
108
+
109
+ assert.equal(await harness.runTurn('go'), 'cancelled');
110
+ assert.equal(harness.getState().streamingTail, null);
111
+ const assistants = harness.getState().items.filter((item) => item.kind === 'assistant');
112
+ assert.equal(assistants.length, 1);
113
+ assert.equal(assistants[0].streaming, false);
114
+ assert.match(assistants[0].text, /partial line/);
115
+ });
116
+
117
+ // Codifies pre-existing parity: text had no item/currentAssistantId before its first completed line.
118
+ test('abort before a newline has no tail id and preserves prior text-drop parity', async () => {
119
+ const harness = makeTurnHarness(async (_text, options) => {
120
+ options.onTextDelta('partial without newline');
121
+ await wait(30);
122
+ const error = new Error('interrupted');
123
+ error.name = 'SessionClosedError';
124
+ throw error;
125
+ });
126
+
127
+ assert.equal(await harness.runTurn('go'), 'cancelled');
128
+ assert.equal(harness.getState().streamingTail, null);
129
+ assert.equal(
130
+ harness.getState().items.filter((item) => item.kind === 'assistant').length,
131
+ 0,
132
+ 'without a visible tail id, pre-newline text keeps the existing drop-on-abort behavior',
133
+ );
134
+ });
135
+
136
+ // Codifies pre-existing parity: the old in-items completed-line snapshot survived starved recovery.
137
+ test('starved Esc recovery settles a non-empty tail exactly once', async () => {
138
+ let state = {
139
+ items: [],
140
+ structureRevision: 0,
141
+ streamingTail: { id: 'tail_abort', kind: 'assistant', text: 'visible partial\n', streaming: true },
142
+ queued: [],
143
+ busy: true,
144
+ commandBusy: false,
145
+ spinner: { active: true },
146
+ thinking: null,
147
+ lastTurn: null,
148
+ };
149
+ const itemIndexById = new Map();
150
+ const set = (patch) => { state = { ...state, ...patch }; return true; };
151
+ const { patchItem, settleStreamingTail } = createEngineItemMutators({
152
+ getState: () => state,
153
+ set,
154
+ itemIndexById,
155
+ });
156
+ const bag = {
157
+ runtime: { abort: () => true },
158
+ nextId: () => 'notice',
159
+ flags: { leadTurnEpoch: 1, disposed: false, draining: false, manualAbortRecoveryMs: 10 },
160
+ pending: [],
161
+ listeners: new Set(),
162
+ getState: () => state,
163
+ set,
164
+ pushItem: () => {},
165
+ patchItem,
166
+ replaceItems: (items) => items,
167
+ settleStreamingTail,
168
+ clearStreamingTail: () => set({ streamingTail: null }),
169
+ pushNotice: () => {},
170
+ autoClearState: () => ({}),
171
+ agentStatusState: () => ({}),
172
+ routeState: () => ({}),
173
+ syncContextStats: () => {},
174
+ denyAllToolApprovals: () => {},
175
+ updateAgentJobCard: () => {},
176
+ requeueEntriesFront: () => {},
177
+ enqueue: () => {},
178
+ autoClearBeforeSubmit: async () => {},
179
+ restoreQueued: () => {},
180
+ resetStatsAndSyncContext: () => {},
181
+ drain: async () => {},
182
+ flushDeferredExecutionPendingResumeKick: () => {},
183
+ discardExecutionPendingResume: () => {},
184
+ };
185
+
186
+ createEngineApiA(bag).abort();
187
+ await wait(30);
188
+ assert.equal(state.streamingTail, null);
189
+ const assistants = state.items.filter((item) => item.kind === 'assistant');
190
+ assert.equal(assistants.length, 1);
191
+ assert.equal(assistants[0].id, 'tail_abort');
192
+ assert.equal(assistants[0].streaming, false);
193
+ });
194
+
195
+ test('removeNotice-style replacement preserves the live tail and later settles once', () => {
196
+ let state = {
197
+ items: [{ id: 'notice', kind: 'notice', text: 'temporary' }],
198
+ structureRevision: 4,
199
+ streamingTail: { id: 'tail_notice', kind: 'assistant', text: 'visible\n', streaming: true },
200
+ };
201
+ const itemIndexById = new Map([['notice', 0]]);
202
+ const set = (patch) => { state = { ...state, ...patch }; return true; };
203
+ const { settleStreamingTail } = createEngineItemMutators({
204
+ getState: () => state,
205
+ set,
206
+ itemIndexById,
207
+ });
208
+ state = replaceEngineItemsState({
209
+ state,
210
+ items: state.items.filter((item) => item.id !== 'notice'),
211
+ itemIndexById,
212
+ preserveStreamingTail: true,
213
+ });
214
+
215
+ assert.equal(state.streamingTail?.id, 'tail_notice');
216
+ assert.equal(settleStreamingTail('tail_notice', { text: 'complete final text' }), true);
217
+ assert.equal(state.items.length, 1);
218
+ assert.equal(state.items[0].text, 'complete final text');
219
+ assert.equal(state.items[0].streaming, false);
220
+ assert.equal(settleStreamingTail('tail_notice', { text: 'duplicate' }), false);
221
+ assert.equal(state.items.length, 1);
222
+ });
223
+
224
+ test('bulk replacement clears the tail and stale settle cannot append into reset transcript', () => {
225
+ let state = {
226
+ items: [{ id: 'old', kind: 'user', text: 'old prompt' }],
227
+ structureRevision: 8,
228
+ streamingTail: { id: 'stale_tail', kind: 'assistant', text: 'old response\n', streaming: true },
229
+ };
230
+ const itemIndexById = new Map([['old', 0]]);
231
+ const set = (patch) => { state = { ...state, ...patch }; return true; };
232
+ const { settleStreamingTail } = createEngineItemMutators({
233
+ getState: () => state,
234
+ set,
235
+ itemIndexById,
236
+ });
237
+ state = replaceEngineItemsState({ state, items: [], itemIndexById });
238
+
239
+ assert.equal(state.streamingTail, null);
240
+ assert.equal(settleStreamingTail('stale_tail', { text: 'must not reappear' }), false);
241
+ assert.deepEqual(state.items, []);
242
+ });
243
+
244
+ test('tool-card revision invalidates the incremental prefix cache', () => {
245
+ const cacheRef = { current: null };
246
+ const tail = { id: 'tail', kind: 'assistant', text: 'live\n', streaming: true };
247
+ let state = {
248
+ items: [{ id: 'tool', kind: 'tool', name: 'shell', text: 'ok', result: 'ok' }],
249
+ structureRevision: 1,
250
+ streamingTail: tail,
251
+ };
252
+ const itemIndexById = new Map([['tool', 0]]);
253
+ const set = (patch) => { state = { ...state, ...patch }; return true; };
254
+ const { patchItem } = createEngineItemMutators({
255
+ getState: () => state,
256
+ set,
257
+ itemIndexById,
258
+ });
259
+ const before = buildTranscriptRowIndexIncremental([...state.items, state.streamingTail], {
260
+ columns: 24,
261
+ cacheRef,
262
+ prefixRevision: state.structureRevision,
263
+ });
264
+ const cachedBeforePatch = cacheRef.current;
265
+ assert.equal(patchItem('tool', {
266
+ text: 'a much longer tool result '.repeat(20),
267
+ result: 'a much longer tool result '.repeat(20),
268
+ }), true);
269
+ assert.equal(state.structureRevision, 2, 'real engine patchItem bumps revision');
270
+ const after = buildTranscriptRowIndexIncremental([...state.items, state.streamingTail], {
271
+ columns: 24,
272
+ cacheRef,
273
+ prefixRevision: state.structureRevision,
274
+ });
275
+
276
+ assert.notEqual(cacheRef.current, cachedBeforePatch, 'revision bump rebuilds the prefix cache');
277
+ assert.equal(after.rows.length, before.rows.length);
278
+ assert.equal(cacheRef.current.prefixRevision, 2);
279
+ });
@@ -3,14 +3,17 @@ permission: read-write
3
3
  ---
4
4
 
5
5
  # Heavy Worker
6
- Broad implementation agent.
6
+ Own the assigned implementation slice through staged delivery.
7
7
 
8
- Bounded slices; smallest coherent change, not rewrite. Stop when scope is
9
- unclear or the blast radius grows.
8
+ Break work into bounded, dependency-aware slices and execute them in sequence.
9
+ At each checkpoint, run the narrowest relevant test or build before expanding
10
+ the slice. Keep the smallest coherent change; control blast radius rather than
11
+ rewriting adjacent systems.
10
12
 
11
- EDIT-FIRST DISCIPLINE. Patch the first bounded piece edit incrementally,
12
- don't read exhaustively. Repeated read-only turns without an edit = stalling;
13
- on a runtime reminder, patch the piece you understand or report blocked.
13
+ EDIT-FIRST DISCIPLINE. Patch incrementally and stop at the first explicit
14
+ boundary: unclear ownership, a missing dependency, or growing blast radius.
15
+ Do not cross that boundary without a new bounded assignment; report blocked
16
+ work with the relevant file:line.
14
17
 
15
- Self-verify edits with shell (targeted test/build).
18
+ Self-verify each checkpoint and the final slice with shell (targeted test/build).
16
19
 
@@ -4,8 +4,10 @@ permission: read
4
4
 
5
5
  # Reviewer
6
6
 
7
- Regression/risk review agent.
7
+ Independent regression/risk review agent.
8
8
 
9
- Find actionable correctness/regression/security/verification risks. Findings
10
- first, severity-ordered, one line with `file:line`; skip non-risky nits. If
11
- clean, one line + only material residual risk.
9
+ Review the approved intent, diff, and tests with independent judgment. Prioritize
10
+ actionable correctness, regression, security, and verification risks; inspect
11
+ affected boundaries. Do not reimplement the change or report non-risky nits.
12
+ Report findings first, severity-ordered, with one line per `file:line`. If clean,
13
+ say so in one line and include only material residual risk.
@@ -5,11 +5,13 @@ permission: read-write
5
5
  # Worker
6
6
  Scoped implementation agent.
7
7
 
8
- Smallest scoped change; no drive-by cleanup. Stop when done/blocked.
8
+ Own only the bounded responsibility assigned in the brief. Trust its
9
+ `file:line` anchors; do only minimal targeted discovery, then make the
10
+ smallest coherent patch. No drive-by cleanup or scope expansion.
9
11
 
10
- EDIT-FIRST DISCIPLINE. Brief anchors (`file:line`) are pre-verified — trust
11
- and patch. Repeated read-only turns without an edit = stalling; on a runtime
12
- reminder, patch now or return blocked. Threshold is "plausible", not "proven".
12
+ EDIT-FIRST DISCIPLINE. Patch promptly rather than repeating read-only turns;
13
+ stop and report blocked when the assigned scope cannot be completed.
13
14
 
14
- Self-verify edits with shell (node --check/test).
15
+ Self-verify with a targeted check (for example, `node --check` or a focused
16
+ test), then report the changed `file:line` and stop.
15
17
 
@@ -1,7 +1,7 @@
1
1
  # Public Agent Constraints
2
2
 
3
- - Do not touch git/Ship; refuse any `git add`/`commit`/`push`/`stash` with
4
- `git operations deferred to Lead`.
5
- - Shell only verifies your own edits (node --check, targeted tests, build/lint);
6
- no exploration, installs, or state changes beyond the brief.
3
+ - Do not touch git/Ship. Refuse `git add`/`commit`/`push`/`stash`: `git
4
+ operations deferred to Lead`.
5
+ - Shell only verifies own edits (node --check, targeted test, build/lint): no
6
+ exploration, install, or state change beyond brief.
7
7
  - Overflow goes to a file; hand off path + fragments.
@@ -1,16 +1,13 @@
1
1
  # Agent Constraints
2
2
 
3
- - Use English for agent task communication.
4
- - One turn = one batch; any read-only call that could have ridden the prior
5
- turn is wasted.
6
- - Never preamble/progress before tools; call tools immediately. Text only in
7
- the final handoff after tool work.
8
- - Final handoff: fragments only — outcome, key `file:line`, verification
9
- command+result, material risks/blockers. Follow stricter role contracts.
10
- - Do not repeat the brief, process, search path, or any fact. Report done/
11
- missing/blocker instead of retrieving to avoid reporting.
12
- - Handoff cap ~30 lines unless `Deliver:` raises it; this is a ceiling.
13
- - Ban headings, tables unless requested, prose narration, raw logs/tool traces,
14
- speculative next-checks, restated brief, articles/politeness.
15
- - Runtime wrap-up directives override this and may require done/remaining/
16
- blocking summary.
3
+ - Agent communication: English. One turn = one batch; a read-only call that
4
+ could join the prior turn is wasted.
5
+ - Call tools immediately: no preamble/progress; text only in final handoff.
6
+ - Final handoff is fragments: outcome, key `file:line`, verification
7
+ command+result, material risk/blocker; stricter role contracts win. Don't
8
+ repeat brief/process/search path/facts; report done/missing/blocker, don't
9
+ retrieve to report.
10
+ - Cap ~30 lines unless `Deliver:` raises it. No headings/tables unless asked,
11
+ prose narration, raw logs/tool traces, speculative next-checks, restated
12
+ brief, articles/politeness.
13
+ - Runtime wrap-up overrides; it may require done/remaining/blocking summary.
@@ -1,5 +1,5 @@
1
1
  # Skip Protocol
2
2
 
3
- For inbound-event reports (`webhook-handler`, `scheduler-task`) with nothing
4
- actionable to relay (label-only, duplicate/dedup, no action needed/report),
5
- prefix the whole response with `[meta:silent]`.
3
+ For non-actionable inbound-event reports (`webhook-handler`, `scheduler-task`:
4
+ label-only, duplicate/dedup, no action needed/report), prefix the whole response
5
+ with `[meta:silent]`.