mixdog 0.9.38 → 0.9.39
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.
- package/package.json +9 -4
- package/scripts/abort-recovery-test.mjs +43 -2
- package/scripts/agent-tag-reuse-smoke.mjs +146 -6
- package/scripts/agent-terminal-reap-test.mjs +127 -0
- package/scripts/agent-trace-io-test.mjs +69 -0
- package/scripts/code-graph-disk-hit-test.mjs +224 -0
- package/scripts/execution-completion-dedup-test.mjs +157 -0
- package/scripts/execution-pending-resume-kick-test.mjs +57 -2
- package/scripts/execution-resume-esc-integration-test.mjs +174 -0
- package/scripts/explore-bench.mjs +38 -2
- package/scripts/explore-prompt-policy-test.mjs +152 -11
- package/scripts/find-fuzzy-hidden-test.mjs +122 -0
- package/scripts/internal-comms-bench-test.mjs +226 -0
- package/scripts/internal-comms-bench.mjs +185 -58
- package/scripts/internal-comms-smoke.mjs +171 -23
- package/scripts/live-worker-smoke.mjs +38 -2
- package/scripts/memory-cycle-routing-test.mjs +111 -0
- package/scripts/memory-rule-contract-test.mjs +93 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/rg-runner-test.mjs +240 -0
- package/scripts/routing-corpus-test.mjs +349 -0
- package/scripts/routing-corpus.mjs +211 -32
- package/scripts/session-orphan-sweep-test.mjs +83 -0
- package/scripts/steering-drain-buckets-test.mjs +179 -0
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -0
- package/src/agents/heavy-worker/AGENT.md +10 -7
- package/src/agents/reviewer/AGENT.md +6 -4
- package/src/agents/worker/AGENT.md +7 -5
- package/src/rules/agent/00-common.md +4 -4
- package/src/rules/agent/00-core.md +11 -14
- package/src/rules/agent/20-skip-protocol.md +3 -3
- package/src/rules/agent/30-explorer.md +50 -60
- package/src/rules/agent/40-cycle1-agent.md +15 -24
- package/src/rules/agent/41-cycle2-agent.md +33 -57
- package/src/rules/agent/42-cycle3-agent.md +28 -42
- package/src/rules/lead/01-general.md +7 -10
- package/src/rules/lead/lead-brief.md +11 -14
- package/src/rules/lead/lead-tool.md +6 -5
- package/src/rules/shared/01-tool.md +44 -45
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +37 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +41 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +223 -42
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
- package/src/runtime/shared/tool-primitives.mjs +4 -1
- package/src/runtime/shared/tool-status.mjs +27 -0
- package/src/runtime/shared/tool-surface.mjs +6 -3
- package/src/session-runtime/config-helpers.mjs +14 -0
- package/src/session-runtime/context-status.mjs +1 -0
- package/src/session-runtime/effort.mjs +6 -2
- package/src/session-runtime/model-recency.mjs +5 -2
- package/src/session-runtime/runtime-core.mjs +35 -2
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +45 -69
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +31 -0
- package/src/tui/app/model-options.mjs +5 -3
- package/src/tui/app/model-picker.mjs +12 -24
- package/src/tui/app/transcript-window.mjs +1 -0
- package/src/tui/components/ToolExecution.jsx +11 -6
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
- package/src/tui/components/tool-execution/text-format.mjs +10 -19
- package/src/tui/dist/index.mjs +517 -142
- package/src/tui/engine/agent-job-feed.mjs +144 -17
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/session-api.mjs +8 -2
- package/src/tui/engine/session-flow.mjs +19 -1
- package/src/tui/engine/tool-card-results.mjs +54 -32
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +77 -42
- package/src/tui/engine.mjs +63 -2
- package/src/workflows/bench/WORKFLOW.md +25 -35
- package/src/workflows/default/WORKFLOW.md +38 -32
- package/src/workflows/solo/WORKFLOW.md +19 -22
- package/scripts/_jitter-fuzz.mjs +0 -44410
- package/scripts/_jitter-fuzz2.mjs +0 -44400
- package/scripts/_jitter-probe.mjs +0 -44397
- package/scripts/_jp2.mjs +0 -45614
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-orphan-sweep-test.mjs — proves the periodic sweep reclaims mature
|
|
3
|
+
* closed tombstones that exist ON DISK but are ABSENT from the summary index.
|
|
4
|
+
*
|
|
5
|
+
* Regression guard for the pre-fix bug where sweepStaleSessions iterated only
|
|
6
|
+
* listStoredSessionSummaries() rows, so on-disk session files missing from a
|
|
7
|
+
* stale index were never visited and their tombstones accumulated forever.
|
|
8
|
+
*/
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
|
|
15
|
+
test('mature closed on-disk orphan absent from summary index gets tombstone-swept', async () => {
|
|
16
|
+
const dataDir = mkdtempSync(join(tmpdir(), 'mixdog-orphan-sweep-'));
|
|
17
|
+
process.env.MIXDOG_DATA_DIR = dataDir;
|
|
18
|
+
try {
|
|
19
|
+
const store = await import('../src/runtime/agent/orchestrator/session/store.mjs');
|
|
20
|
+
const { sweepStaleSessions, _sessionSummary, _writeSummaryIndex } = store;
|
|
21
|
+
|
|
22
|
+
const sessionsDir = join(dataDir, 'sessions');
|
|
23
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const TWO_HOURS = 2 * 60 * 60 * 1000;
|
|
27
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
28
|
+
|
|
29
|
+
// (1) An INDEXED, fresh, open agent session — present in the summary
|
|
30
|
+
// index so the index is non-empty (blocking any rebuild) and must
|
|
31
|
+
// survive the sweep.
|
|
32
|
+
const indexed = {
|
|
33
|
+
id: 'sess_indexed_keep',
|
|
34
|
+
closed: false,
|
|
35
|
+
status: 'idle',
|
|
36
|
+
owner: 'agent:test',
|
|
37
|
+
updatedAt: now,
|
|
38
|
+
createdAt: now,
|
|
39
|
+
lastHeartbeatAt: now,
|
|
40
|
+
messages: [],
|
|
41
|
+
};
|
|
42
|
+
writeFileSync(join(sessionsDir, `${indexed.id}.json`), JSON.stringify(indexed));
|
|
43
|
+
|
|
44
|
+
// (2) An ORPHAN closed+mature tombstone — on disk, NOT in the index,
|
|
45
|
+
// closed >1h ago. Only reachable if the sweep reconciles the index
|
|
46
|
+
// candidate set with a direct directory scan.
|
|
47
|
+
const orphan = {
|
|
48
|
+
id: 'sess_orphan_tombstone',
|
|
49
|
+
closed: true,
|
|
50
|
+
status: 'closed',
|
|
51
|
+
owner: 'agent:test',
|
|
52
|
+
updatedAt: now - TWO_HOURS,
|
|
53
|
+
createdAt: now - TWO_HOURS,
|
|
54
|
+
messages: [],
|
|
55
|
+
};
|
|
56
|
+
writeFileSync(join(sessionsDir, `${orphan.id}.json`), JSON.stringify(orphan));
|
|
57
|
+
|
|
58
|
+
// Index contains ONLY the indexed session — the orphan is absent.
|
|
59
|
+
const rows = _writeSummaryIndex([_sessionSummary(indexed)]);
|
|
60
|
+
assert.equal(rows.length, 1, 'index seeded with exactly one row');
|
|
61
|
+
assert.ok(!rows.some((r) => r.id === orphan.id), 'orphan is absent from the summary index');
|
|
62
|
+
|
|
63
|
+
// Tombstone-only pass (no idle sweep) with a 1h maturity threshold.
|
|
64
|
+
const result = sweepStaleSessions({ sweepIdle: false, tombstoneMaxAgeMs: ONE_HOUR });
|
|
65
|
+
|
|
66
|
+
assert.equal(result.tombstonesCleaned, 1, 'the orphan tombstone was reclaimed');
|
|
67
|
+
assert.ok(
|
|
68
|
+
result.tombstoneDetails.some((d) => d.id === orphan.id),
|
|
69
|
+
'sweep reports the orphan id among tombstone deletions',
|
|
70
|
+
);
|
|
71
|
+
assert.ok(
|
|
72
|
+
!existsSync(join(sessionsDir, `${orphan.id}.json`)),
|
|
73
|
+
'orphan session file was unlinked from disk',
|
|
74
|
+
);
|
|
75
|
+
assert.ok(
|
|
76
|
+
existsSync(join(sessionsDir, `${indexed.id}.json`)),
|
|
77
|
+
'fresh open indexed session was preserved',
|
|
78
|
+
);
|
|
79
|
+
} finally {
|
|
80
|
+
try { rmSync(dataDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
81
|
+
delete process.env.MIXDOG_DATA_DIR;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { createSessionFlow } from '../src/tui/engine/session-flow.mjs';
|
|
4
|
+
import { createRunTurn } from '../src/tui/engine/turn.mjs';
|
|
4
5
|
|
|
5
6
|
// Minimal bag: drainPendingSteering only touches pending, the queue helpers,
|
|
6
7
|
// and commitSteeringQueueEntries (which no-ops on disk when runtime.id is not
|
|
@@ -118,3 +119,181 @@ test('post-turn drain does not send queued slash command to model', async () =>
|
|
|
118
119
|
assert.equal(bag.pending.length, 1, 'slash command remains for command dispatcher');
|
|
119
120
|
assert.equal(bag.pending[0].content, '/clear');
|
|
120
121
|
});
|
|
122
|
+
|
|
123
|
+
// Minimal store bag for createRunTurn: only the surface the streaming/steering
|
|
124
|
+
// finalize path touches. runtime.ask is a caller-supplied mock that drives the
|
|
125
|
+
// text-delta / steer-message callbacks.
|
|
126
|
+
function makeTurnBag(ask) {
|
|
127
|
+
let seq = 0;
|
|
128
|
+
const state = {
|
|
129
|
+
items: [],
|
|
130
|
+
stats: { turns: 0, inputTokens: 0, outputTokens: 0 },
|
|
131
|
+
busy: false,
|
|
132
|
+
spinner: null,
|
|
133
|
+
thinking: null,
|
|
134
|
+
};
|
|
135
|
+
const itemIndexById = new Map();
|
|
136
|
+
const findIndexById = (id) => state.items.findIndex((it) => it.id === id);
|
|
137
|
+
const bag = {
|
|
138
|
+
runtime: { id: null, toolMode: 'auto', ask, abort: () => {} },
|
|
139
|
+
nextId: () => `id_${++seq}`,
|
|
140
|
+
tuiDebug: () => {},
|
|
141
|
+
LEAD_TURN_TIMEOUT_MS: 300000,
|
|
142
|
+
flags: { leadTurnEpoch: 0 },
|
|
143
|
+
pending: [],
|
|
144
|
+
itemIndexById,
|
|
145
|
+
getState: () => state,
|
|
146
|
+
set: (patch) => Object.assign(state, patch),
|
|
147
|
+
pushItem: (spec) => {
|
|
148
|
+
state.items = [...state.items, spec];
|
|
149
|
+
if (spec?.id != null) itemIndexById.set(spec.id, state.items.length - 1);
|
|
150
|
+
},
|
|
151
|
+
patchItem: (id, patch) => {
|
|
152
|
+
const idx = findIndexById(id);
|
|
153
|
+
if (idx < 0) return;
|
|
154
|
+
const items = state.items.slice();
|
|
155
|
+
items[idx] = { ...items[idx], ...patch };
|
|
156
|
+
state.items = items;
|
|
157
|
+
},
|
|
158
|
+
pushNotice: () => {},
|
|
159
|
+
pushUserOrSyntheticItem: (text) => {
|
|
160
|
+
state.items = [...state.items, { kind: 'user', id: `u_${++seq}`, text }];
|
|
161
|
+
},
|
|
162
|
+
markToolCallActive: () => {},
|
|
163
|
+
markToolCallDone: () => {},
|
|
164
|
+
clearActiveToolSummary: () => {},
|
|
165
|
+
agentStatusState: () => ({}),
|
|
166
|
+
routeState: () => ({}),
|
|
167
|
+
syncContextStats: () => {},
|
|
168
|
+
denyAllToolApprovals: () => {},
|
|
169
|
+
requestToolApproval: async () => ({ approved: false }),
|
|
170
|
+
patchToolCardResult: () => {},
|
|
171
|
+
flushToolResults: () => {},
|
|
172
|
+
flushDeferredExecutionPendingResumeKick: () => {},
|
|
173
|
+
drain: async () => {},
|
|
174
|
+
drainPendingSteering: () => [],
|
|
175
|
+
};
|
|
176
|
+
return { bag, getState: () => state };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
test('onSteerMessage commits a streamed no-newline assistant tail into items', async () => {
|
|
180
|
+
// A terminal no-tool response streams a single line WITHOUT a trailing '\n',
|
|
181
|
+
// so no assistant row/currentAssistantId exists yet. A steering injection
|
|
182
|
+
// races finalization and must seal the pending tail instead of dropping it.
|
|
183
|
+
const TAIL = 'partial answer with no trailing newline';
|
|
184
|
+
const ask = async (_userText, opts) => {
|
|
185
|
+
opts.onTextDelta(TAIL);
|
|
186
|
+
opts.onSteerMessage('steer now');
|
|
187
|
+
return { result: { content: '' }, session: { messages: [] } };
|
|
188
|
+
};
|
|
189
|
+
const { bag, getState } = makeTurnBag(ask);
|
|
190
|
+
const runTurn = createRunTurn(bag);
|
|
191
|
+
|
|
192
|
+
await runTurn('do a thing');
|
|
193
|
+
|
|
194
|
+
const assistant = getState().items.find((it) => it.kind === 'assistant');
|
|
195
|
+
assert.ok(assistant, 'streamed no-newline tail must be committed as an assistant item');
|
|
196
|
+
assert.equal(assistant.text, TAIL);
|
|
197
|
+
assert.equal(assistant.streaming, false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('finalization does not duplicate a steer-committed tail when result.content repeats it', async () => {
|
|
201
|
+
// Same steer race, but the provider's final content equals the already-
|
|
202
|
+
// committed tail. Finalization must NOT re-emit it as a second item.
|
|
203
|
+
const TAIL = 'partial answer with no trailing newline';
|
|
204
|
+
const ask = async (_userText, opts) => {
|
|
205
|
+
opts.onTextDelta(TAIL);
|
|
206
|
+
opts.onSteerMessage('steer now');
|
|
207
|
+
return { result: { content: TAIL }, session: { messages: [] } };
|
|
208
|
+
};
|
|
209
|
+
const { bag, getState } = makeTurnBag(ask);
|
|
210
|
+
const runTurn = createRunTurn(bag);
|
|
211
|
+
|
|
212
|
+
await runTurn('do a thing');
|
|
213
|
+
|
|
214
|
+
const assistants = getState().items.filter((it) => it.kind === 'assistant');
|
|
215
|
+
assert.equal(assistants.length, 1, 'the committed tail must not be duplicated at finalize');
|
|
216
|
+
assert.equal(assistants[0].text, TAIL);
|
|
217
|
+
assert.equal(assistants[0].streaming, false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('finalization appends only the uncommitted remainder past a steer-committed tail', async () => {
|
|
221
|
+
// Provider content extends past the committed tail: only the new remainder
|
|
222
|
+
// becomes a fresh item, ordered after the committed segment (+ steering row).
|
|
223
|
+
const TAIL = 'partial answer with no trailing newline';
|
|
224
|
+
const REMAINDER = '\nmore text arriving after the steer';
|
|
225
|
+
const ask = async (_userText, opts) => {
|
|
226
|
+
opts.onTextDelta(TAIL);
|
|
227
|
+
opts.onSteerMessage('steer now');
|
|
228
|
+
return { result: { content: TAIL + REMAINDER }, session: { messages: [] } };
|
|
229
|
+
};
|
|
230
|
+
const { bag, getState } = makeTurnBag(ask);
|
|
231
|
+
const runTurn = createRunTurn(bag);
|
|
232
|
+
|
|
233
|
+
await runTurn('do a thing');
|
|
234
|
+
|
|
235
|
+
const assistants = getState().items.filter((it) => it.kind === 'assistant');
|
|
236
|
+
assert.equal(assistants.length, 2, 'committed tail + remainder are two distinct items');
|
|
237
|
+
assert.equal(assistants[0].text, TAIL, 'committed tail stays first, unchanged');
|
|
238
|
+
assert.equal(assistants[1].text, REMAINDER, 'only the uncommitted remainder is appended');
|
|
239
|
+
assert.equal(assistants[1].streaming, false);
|
|
240
|
+
const steerIdx = getState().items.findIndex((it) => it.kind === 'user' && it.text === 'steer now');
|
|
241
|
+
const remainderIdx = getState().items.findIndex((it) => it.kind === 'assistant' && it.text === REMAINDER);
|
|
242
|
+
assert.ok(steerIdx >= 0 && remainderIdx > steerIdx, 'remainder appends after the injected steering row');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Two committed segments this turn: a prior preamble P, then a no-newline TAIL
|
|
246
|
+
// sealed by the steer race. The provider's final content may OMIT P — the
|
|
247
|
+
// per-segment strip must still peel BOTH out (a single concatenated 'P+TAIL'
|
|
248
|
+
// prefix would fail to match and duplicate TAIL after the steering row).
|
|
249
|
+
const P = 'preamble sealed before the tail';
|
|
250
|
+
const TAIL2 = 'terminal tail with no trailing newline';
|
|
251
|
+
function makeTwoSegmentAsk(finalContent) {
|
|
252
|
+
return async (_userText, opts) => {
|
|
253
|
+
opts.onTextDelta(P);
|
|
254
|
+
opts.onSteerMessage('steer one'); // seals P as its own item
|
|
255
|
+
opts.onTextDelta(TAIL2);
|
|
256
|
+
opts.onSteerMessage('steer two'); // seals TAIL2 as its own item
|
|
257
|
+
return { result: { content: finalContent }, session: { messages: [] } };
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
test('per-segment strip: final content = TAIL only (P omitted) → no new item', async () => {
|
|
262
|
+
const { bag, getState } = makeTurnBag(makeTwoSegmentAsk(TAIL2));
|
|
263
|
+
await createRunTurn(bag)('do a thing');
|
|
264
|
+
const assistants = getState().items.filter((it) => it.kind === 'assistant');
|
|
265
|
+
assert.deepEqual(assistants.map((it) => it.text), [P, TAIL2], 'only the two committed segments exist');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('per-segment strip: final content = P + newline + TAIL → no new item', async () => {
|
|
269
|
+
const { bag, getState } = makeTurnBag(makeTwoSegmentAsk(`${P}\n${TAIL2}`));
|
|
270
|
+
await createRunTurn(bag)('do a thing');
|
|
271
|
+
const assistants = getState().items.filter((it) => it.kind === 'assistant');
|
|
272
|
+
assert.deepEqual(assistants.map((it) => it.text), [P, TAIL2], 'both segments stripped; nothing re-emitted');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('per-segment strip: final content extends past both committed segments → only remainder', async () => {
|
|
276
|
+
const { bag, getState } = makeTurnBag(makeTwoSegmentAsk(`${P}\n${TAIL2}\nmore`));
|
|
277
|
+
await createRunTurn(bag)('do a thing');
|
|
278
|
+
const assistants = getState().items.filter((it) => it.kind === 'assistant');
|
|
279
|
+
assert.equal(assistants.length, 3, 'committed P + TAIL + the new remainder');
|
|
280
|
+
assert.equal(assistants[2].text, '\nmore', 'only the uncommitted remainder is appended');
|
|
281
|
+
assert.equal(assistants[2].streaming, false);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('per-segment strip: segment sealed WITH a leading newline still peels', async () => {
|
|
285
|
+
// The committed segment carries its own leading '\n' ('\nTAIL'); the provider
|
|
286
|
+
// returns the trimmed 'TAIL'. Trimming the segment before the compare lets it
|
|
287
|
+
// peel so it is not duplicated after the steering row.
|
|
288
|
+
const LEAD_TAIL = '\nterminal tail sealed with a leading newline';
|
|
289
|
+
const ask = async (_userText, opts) => {
|
|
290
|
+
opts.onTextDelta(LEAD_TAIL);
|
|
291
|
+
opts.onSteerMessage('steer now');
|
|
292
|
+
return { result: { content: LEAD_TAIL.replace(/^\s+/, '') }, session: { messages: [] } };
|
|
293
|
+
};
|
|
294
|
+
const { bag, getState } = makeTurnBag(ask);
|
|
295
|
+
await createRunTurn(bag)('do a thing');
|
|
296
|
+
const assistants = getState().items.filter((it) => it.kind === 'assistant');
|
|
297
|
+
assert.equal(assistants.length, 1, 'leading-newline segment must not be duplicated at finalize');
|
|
298
|
+
assert.equal(assistants[0].text, LEAD_TAIL);
|
|
299
|
+
});
|
package/scripts/tool-smoke.mjs
CHANGED
|
@@ -609,6 +609,19 @@ const graphOut = await executeCodeGraphTool('code_graph', {
|
|
|
609
609
|
file: 'scripts/smoke.mjs',
|
|
610
610
|
}, root);
|
|
611
611
|
assertOk('code_graph', graphOut, /binding|spawnSync|symbol/i);
|
|
612
|
+
const graphStringSymbolOut = await executeCodeGraphTool('code_graph', {
|
|
613
|
+
mode: 'symbols',
|
|
614
|
+
symbols: 'executeBuiltinTool',
|
|
615
|
+
}, root);
|
|
616
|
+
assertOk('code_graph string symbols', graphStringSymbolOut, /executeBuiltinTool|symbol_search/i);
|
|
617
|
+
const graphRootAnchorOut = await executeCodeGraphTool('code_graph', {
|
|
618
|
+
mode: 'symbol_search',
|
|
619
|
+
symbol: 'executeBuiltinTool',
|
|
620
|
+
file: root,
|
|
621
|
+
}, root);
|
|
622
|
+
if (/file not found|outside cwd|arbitrary tree/i.test(String(graphRootAnchorOut))) {
|
|
623
|
+
throw new Error(`code_graph redundant root anchor was not normalized:\n${graphRootAnchorOut}`);
|
|
624
|
+
}
|
|
612
625
|
|
|
613
626
|
const graphSymbolBatchOut = await executeCodeGraphTool('code_graph', {
|
|
614
627
|
mode: 'symbol_search',
|
|
@@ -1242,16 +1255,11 @@ if (normalizedExplore.length !== 2 || normalizedExplore[0] !== 'where is model s
|
|
|
1242
1255
|
}
|
|
1243
1256
|
if (MAX_FANOUT_QUERIES !== 8) throw new Error(`explore fanout cap changed: ${MAX_FANOUT_QUERIES}`);
|
|
1244
1257
|
const explorerPrompt = buildExplorerPrompt('where is <agent> & status?');
|
|
1245
|
-
if (
|
|
1258
|
+
if (explorerPrompt !== '<query>where is <agent> & status?</query>') {
|
|
1246
1259
|
throw new Error(`explorer prompt contract failed: ${explorerPrompt}`);
|
|
1247
1260
|
}
|
|
1248
|
-
if (
|
|
1249
|
-
|
|
1250
|
-
|| !/Turns 2-3 exist SOLELY as zero-hit recovery/.test(explorerPrompt)
|
|
1251
|
-
|| !/HARD max 3 tool turns/.test(explorerPrompt)
|
|
1252
|
-
|| !/turn 1\/3/.test(explorerPrompt)
|
|
1253
|
-
) {
|
|
1254
|
-
throw new Error(`explorer prompt must force immediate answer on a specific-token anchor while preserving the 3-turn cap: ${explorerPrompt}`);
|
|
1261
|
+
if (/Reminder:|BUDGET|STOP and answer|verdicts|ratings|recommendations|grep|code_graph|find|glob/i.test(explorerPrompt)) {
|
|
1262
|
+
throw new Error(`explorer prompt must not duplicate the system routing/fan-out contract: ${explorerPrompt}`);
|
|
1255
1263
|
}
|
|
1256
1264
|
setInternalToolsProvider({
|
|
1257
1265
|
executor: async () => 'tool-smoke internal tool',
|
|
@@ -1358,7 +1366,7 @@ setInternalToolsProvider({
|
|
|
1358
1366
|
if (!/Read-only retrieval role/i.test(visible) || /# environment/i.test(visible) || /git operations deferred to Lead/i.test(visible)) {
|
|
1359
1367
|
throw new Error(`explorer hidden retrieval context should stay slim: ${visible.slice(0, 1200)}`);
|
|
1360
1368
|
}
|
|
1361
|
-
if (!/# Role: explorer/i.test(systemVisible) || /# Role: explorer/i.test(userReminderVisible) || !/
|
|
1369
|
+
if (!/# Role: explorer/i.test(systemVisible) || /# Role: explorer/i.test(userReminderVisible) || !/deliver WHERE/i.test(systemVisible)) {
|
|
1362
1370
|
throw new Error(`explorer role md must ride BP2 system, not BP3 user reminder: system=${systemVisible.slice(0, 600)} user=${userReminderVisible.slice(0, 600)}`);
|
|
1363
1371
|
}
|
|
1364
1372
|
// System layers (BP1 tool policy + BP2 role md) are shared/frozen and sized
|
|
@@ -1734,7 +1742,7 @@ const readArrayItemAnyOf = readArraySchema?.items?.anyOf || [];
|
|
|
1734
1742
|
if (!readArrayItemAnyOf.some((entry) => entry?.type === 'object' && entry?.properties?.offset && entry?.properties?.limit)) {
|
|
1735
1743
|
throw new Error('read schema must expose array-of-region objects for batched spans');
|
|
1736
1744
|
}
|
|
1737
|
-
if (/line\+context/i.test(readDescription) || !/verified file path/i.test(readDescription) || !/
|
|
1745
|
+
if (/line\+context/i.test(readDescription) || !/verified file path/i.test(readDescription) || !/guessed path.*find first/i.test(readDescription) || !/Batch paths\/regions as real arrays/i.test(readDescription)) {
|
|
1738
1746
|
throw new Error('read description must expose offset/limit as the single window form');
|
|
1739
1747
|
}
|
|
1740
1748
|
if (readProps.line || readProps.context) {
|
|
@@ -1834,7 +1842,7 @@ if (codeGraphSymbolSearchErr) {
|
|
|
1834
1842
|
if (!/code structure\/flow/i.test(codeGraphDescription) || !/symbols\/references\/calls\/deps/i.test(codeGraphDescription)) {
|
|
1835
1843
|
throw new Error('code_graph description must stay structure-oriented and name its symbol modes');
|
|
1836
1844
|
}
|
|
1837
|
-
if (!/
|
|
1845
|
+
if (!/files\[\] must be verified paths/i.test(codeGraphDescription) || !/Batch symbols\[\]\/files\[\]/i.test(codeGraphDescription)) {
|
|
1838
1846
|
throw new Error('code_graph description must route unknown file paths through locators first');
|
|
1839
1847
|
}
|
|
1840
1848
|
if (!/repo-local/i.test(codeGraphDescription) || !/NOT web search|not web/i.test(codeGraphDescription)) {
|
|
@@ -2150,7 +2158,7 @@ const grepHeadLimitDescription = grepTool?.inputSchema?.properties?.head_limit?.
|
|
|
2150
2158
|
if (!/Array = variants in one call/i.test(grepPatternDescription) || !/Verified file\/dir/i.test(grepPathDescription)) {
|
|
2151
2159
|
throw new Error('grep schema must keep compact pattern/path guidance');
|
|
2152
2160
|
}
|
|
2153
|
-
if (!/verified scope/i.test(grepTool?.description || '') || !/
|
|
2161
|
+
if (!/verified scope/i.test(grepTool?.description || '') || !/guessed path fragment.*find first/i.test(grepTool?.description || '')) {
|
|
2154
2162
|
throw new Error('grep description must require verified scopes and locator-first unknown paths');
|
|
2155
2163
|
}
|
|
2156
2164
|
if (!/Glob filter/i.test(grepGlobDescription) || !/no guessed src\/\*\*/i.test(grepGlobDescription)) {
|
|
@@ -2174,7 +2182,7 @@ if (!/exact glob from verified roots/i.test(globTool?.description || '')) {
|
|
|
2174
2182
|
if (!/Partial path\/name lookup/i.test(findTool?.description || '') || !/verify roots before grep\/glob/i.test(findTool?.description || '')) {
|
|
2175
2183
|
throw new Error('find description must advertise unverified path/name lookup and verified outputs');
|
|
2176
2184
|
}
|
|
2177
|
-
if (!/List verified directories/i.test(listTool?.description || '') || !/
|
|
2185
|
+
if (!/List verified directories/i.test(listTool?.description || '') || !/Guessed dir.*find first/i.test(listTool?.description || '') || !/Verified directory/i.test(listTool?.inputSchema?.properties?.path?.description || '')) {
|
|
2178
2186
|
throw new Error('list description must require verified directories and locator-first unknown dirs');
|
|
2179
2187
|
}
|
|
2180
2188
|
if (!/Repo-local/i.test(codeGraphProps.mode?.description || '') || !/one call/i.test(codeGraphProps.symbols?.description || '')) {
|
|
@@ -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
|
+
});
|
|
@@ -3,14 +3,17 @@ permission: read-write
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Heavy Worker
|
|
6
|
-
|
|
6
|
+
Own the assigned implementation slice through staged delivery.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
7
|
+
Independent regression/risk review agent.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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.
|
|
11
|
-
and
|
|
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
|
|
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
|
|
4
|
-
|
|
5
|
-
- Shell only verifies
|
|
6
|
-
|
|
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.
|