mixdog 0.9.37 → 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/dispatch-persist-recovery-test.mjs +141 -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 +101 -8
- package/scripts/explore-prompt-policy-test.mjs +156 -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/notify-completion-mirror-test.mjs +73 -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/session-sweep.mjs +266 -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 +56 -48
- 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 -41
- 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/dispatch-persist.mjs +31 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
- 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/ask-session.mjs +18 -52
- 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 +339 -63
- package/src/runtime/agent/orchestrator/stall-policy.mjs +37 -0
- 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/provider-models.mjs +3 -3
- package/src/session-runtime/runtime-core.mjs +78 -10
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/session-runtime/warmup-schedulers.mjs +7 -1
- 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 +533 -143
- package/src/tui/engine/agent-job-feed.mjs +153 -16
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/queue-helpers.mjs +8 -0
- package/src/tui/engine/session-api.mjs +8 -2
- package/src/tui/engine/session-flow.mjs +34 -2
- 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 +83 -43
- 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
|
@@ -19,38 +19,80 @@ function assert(condition, message) {
|
|
|
19
19
|
function readSrc(...parts) {
|
|
20
20
|
return readFileSync(join(root, 'src', ...parts), 'utf8');
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
function normalize(text) {
|
|
23
|
+
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
24
|
+
}
|
|
25
|
+
function rawBlock(text, start, end, label) {
|
|
26
|
+
const source = String(text);
|
|
27
|
+
const from = source.indexOf(start);
|
|
28
|
+
if (from < 0) throw new Error(`${label}: missing start marker ${start}`);
|
|
29
|
+
const finish = source.indexOf(end, from + start.length);
|
|
30
|
+
if (finish < 0) throw new Error(`${label}: missing end marker ${end}`);
|
|
31
|
+
return source.slice(from, finish);
|
|
32
|
+
}
|
|
33
|
+
function block(text, start, end, label) {
|
|
34
|
+
return normalize(rawBlock(text, start, end, label));
|
|
35
|
+
}
|
|
36
|
+
function bodyAfterFrontmatter(text, label) {
|
|
37
|
+
const source = String(text);
|
|
38
|
+
const opening = source.match(/^---[ \t]*\r?\n/);
|
|
39
|
+
if (!opening) throw new Error(`${label}: missing frontmatter opening fence`);
|
|
40
|
+
const closing = source.slice(opening[0].length).match(/^---[ \t]*(?:\r?\n|$)/m);
|
|
41
|
+
if (!closing) throw new Error(`${label}: missing frontmatter closing fence`);
|
|
42
|
+
return source.slice(opening[0].length + closing.index + closing[0].length);
|
|
43
|
+
}
|
|
44
|
+
function roleBody(text, label) {
|
|
45
|
+
return normalize(bodyAfterFrontmatter(text, label));
|
|
46
|
+
}
|
|
47
|
+
function frontmatter(text, label) {
|
|
48
|
+
const source = String(text);
|
|
49
|
+
const opening = source.match(/^---[ \t]*\r?\n/);
|
|
50
|
+
if (!opening) throw new Error(`${label}: missing frontmatter opening fence`);
|
|
51
|
+
const closing = source.slice(opening[0].length).match(/^---[ \t]*(?:\r?\n|$)/m);
|
|
52
|
+
if (!closing) throw new Error(`${label}: missing frontmatter closing fence`);
|
|
53
|
+
return source.slice(opening[0].length, opening[0].length + closing.index);
|
|
25
54
|
}
|
|
26
55
|
|
|
27
|
-
// ---
|
|
56
|
+
// --- Compact rule contracts -------------------------------------------------
|
|
28
57
|
const workflow = readSrc('workflows', 'default', 'WORKFLOW.md');
|
|
29
58
|
const leadBrief = readSrc('rules', 'lead', 'lead-brief.md');
|
|
59
|
+
const solo = readSrc('workflows', 'solo', 'WORKFLOW.md');
|
|
60
|
+
const bench = readSrc('workflows', 'bench', 'WORKFLOW.md');
|
|
61
|
+
const general = readSrc('rules', 'lead', '01-general.md');
|
|
62
|
+
const leadTool = readSrc('rules', 'lead', 'lead-tool.md');
|
|
63
|
+
const core = readSrc('rules', 'agent', '00-core.md');
|
|
64
|
+
const common = readSrc('rules', 'agent', '00-common.md');
|
|
65
|
+
const skip = readSrc('rules', 'agent', '20-skip-protocol.md');
|
|
30
66
|
const BRIEF_FIELDS = ['Goal:', 'Anchors:', 'Allow/Forbid:', 'Deliver:', 'Verify:'];
|
|
31
|
-
|
|
32
|
-
|
|
67
|
+
const TOKEN_PRINCIPLE = /minimum (?:characters|chars), maximum (?:information|info)/i;
|
|
68
|
+
function requireAll(text, label, patterns) {
|
|
69
|
+
for (const pattern of patterns) assert(pattern.test(normalize(text).toLowerCase()), `${label}: missing ${pattern}`);
|
|
70
|
+
}
|
|
71
|
+
assert(TOKEN_PRINCIPLE.test(normalize(leadBrief)), 'lead-brief.md: brief must state min-char/max-info principle');
|
|
33
72
|
for (const field of BRIEF_FIELDS) assert(leadBrief.includes(field), `lead-brief.md: brief missing labeled field ${field}`);
|
|
34
73
|
assert(leadBrief.includes('Stop:'), 'lead-brief.md: brief must add Stop: for heavy-worker bound');
|
|
35
|
-
assert(/role-known|already (?:owns|knows)|wasted cost|wasted/i.test(
|
|
36
|
-
assert(/
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
assert(/role-known|already (?:owns|knows)|wasted cost|wasted/i.test(normalize(leadBrief)), 'lead-brief.md: brief must ban restating known rules/background as cost');
|
|
75
|
+
assert(/spec\/test beats its summary/i.test(normalize(leadBrief)), 'lead-brief.md: spec/test must beat summary');
|
|
76
|
+
requireAll(leadBrief, 'Lead brief lifecycle', [
|
|
77
|
+
/full brief only for fresh spawn\/`respawned: true`/, /live follow-ups are delta/,
|
|
78
|
+
/dead-tag send is cold: re-supply anchors/, /never `send` mid-run/,
|
|
79
|
+
/batch one follow-up after completion/, /interrupt only to cancel/,
|
|
80
|
+
/agent communication is english/,
|
|
81
|
+
]);
|
|
82
|
+
assert(/lead brief contract/i.test(normalize(workflow)), 'WORKFLOW.md: must defer to the lead brief contract');
|
|
39
83
|
assert(!BRIEF_FIELDS.every((field) => workflow.includes(field)), 'WORKFLOW.md: must not duplicate the full brief field list');
|
|
40
84
|
|
|
41
|
-
// --- Agent handoff contract (00-core.md) -----------------------------------
|
|
42
|
-
const core = readSrc('rules', 'agent', '00-core.md');
|
|
43
|
-
assert(/minimum characters, maximum information/i.test(flat(core)), '00-core: handoff must state token-optimized principle');
|
|
44
85
|
assert(/fragments/i.test(core), '00-core: handoff must require fragments');
|
|
45
86
|
assert(/file:line/i.test(core), '00-core: handoff must anchor evidence to file:line');
|
|
46
|
-
assert(/Banned as pure cost/i.test(flat(core)), '00-core: handoff must list banned cost items');
|
|
47
87
|
for (const banned of ['headings', 'tables', 'narration', 'raw logs', 'next-checks']) {
|
|
48
88
|
assert(core.toLowerCase().includes(banned), `00-core: banned list missing ${banned}`);
|
|
49
89
|
}
|
|
50
|
-
const common = readSrc('rules', 'agent', '00-common.md');
|
|
51
90
|
assert(/Public Agent Constraints/i.test(common), '00-common: must be titled public-only constraints');
|
|
52
|
-
assert(/git operations deferred to Lead/i.test(common), '00-common: must refuse git/Ship');
|
|
91
|
+
assert(/git operations deferred to Lead/i.test(normalize(common)), '00-common: must refuse git/Ship');
|
|
53
92
|
assert(/Overflow goes to a file/i.test(common), '00-common: must keep overflow-to-file rule');
|
|
93
|
+
requireAll(common, 'Public-agent shell', [
|
|
94
|
+
/shell only verifies own edits/, /no exploration, install, or state change beyond brief/,
|
|
95
|
+
]);
|
|
54
96
|
|
|
55
97
|
// --- Per-role output contracts --------------------------------------------
|
|
56
98
|
const roles = {
|
|
@@ -59,19 +101,125 @@ const roles = {
|
|
|
59
101
|
'reviewer/AGENT.md': readSrc('agents', 'reviewer', 'AGENT.md'),
|
|
60
102
|
'debugger/AGENT.md': readSrc('agents', 'debugger', 'AGENT.md'),
|
|
61
103
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
104
|
+
|
|
105
|
+
// Semantic contracts deliberately avoid prose snapshots.
|
|
106
|
+
function snapshot(actual, expected, label) {
|
|
107
|
+
assert(normalize(actual) === normalize(expected), `${label}: canonical snapshot changed`);
|
|
108
|
+
}
|
|
109
|
+
assert(/^agents:\s*$/m.test(frontmatter(solo, 'Solo workflow')), 'Solo: agents frontmatter must be empty');
|
|
110
|
+
requireAll(workflow, 'Default approval', [
|
|
111
|
+
/read-only investigation\/planning while consulting/, /later explicit user message after the latest plan/,
|
|
112
|
+
/initial\/additional\/changed requests reset planning/, /scope change needs a revised plan and fresh approval/,
|
|
113
|
+
/no edits, state mutation, or delegation/,
|
|
114
|
+
]);
|
|
115
|
+
requireAll(workflow, 'Default routing', [
|
|
116
|
+
/delegate by default/, /only coordinates, does git, or an obvious 1-edit\/ 1-check change/,
|
|
117
|
+
/implementation\/research\/debugging to its matching agent/, /indicators, not automatic categories/,
|
|
118
|
+
/worker: bounded established path, low local risk\/coupling\/verification, clear local check/,
|
|
119
|
+
/heavy worker: high risk\/coupling\/verification \(including any high-risk scope\), or coupled staged work needing coordinated verification/,
|
|
120
|
+
/reviewer verifies an implementation/, /debugger handles requested debugging or root cause after a failed fix/,
|
|
121
|
+
]);
|
|
122
|
+
requireAll(workflow, 'Default lifecycle', [
|
|
123
|
+
/draft before any implementation/, /parallel gain exceeds coordination\/merge cost/,
|
|
124
|
+
/all in one turn, with no count cap/, /real dependency, overlapping write, or inseparable coupling/,
|
|
125
|
+
/after async spawn, end the turn/, /every delegated implementation gets one reviewer/,
|
|
126
|
+
/lead integration\/cross-scope verification in parallel/, /high-risk scopes add distinct lenses/,
|
|
127
|
+
/original live session/, /loop fix -> re-verify \(same reviewer \+ lead re-check\) until clean/,
|
|
128
|
+
/bug surviving 2\+ fix cycles/, /agent reports relay.*as in-progress, never conclusions/,
|
|
129
|
+
/final \(not interim\) report/, /never forward raw agent output/,
|
|
130
|
+
/explicit user request after issue-free feedback/, /outcome\/direction change, pause and re-consult/,
|
|
131
|
+
]);
|
|
132
|
+
requireAll(solo, 'Solo lifecycle', [
|
|
133
|
+
/no edits or state mutation/, /never spawn, send, delegate, or ask agents to work/,
|
|
134
|
+
/read-only investigation\/planning while consulting/,
|
|
135
|
+
/later explicit user message after the latest plan/,
|
|
136
|
+
/initial\/additional\/changed requests reset planning/,
|
|
137
|
+
/scope change needs a revised plan and fresh approval/,
|
|
138
|
+
/checks\/fixes directly until clean or reports a blocker/, /final \(not interim\) report/,
|
|
139
|
+
/interim updates are in-progress, never conclusions/,
|
|
140
|
+
/issue-free user feedback/,
|
|
141
|
+
]);
|
|
142
|
+
requireAll(bench, 'Bench lifecycle', [
|
|
143
|
+
/never wait for approval or ask questions/, /verified complete or provably blocked/,
|
|
144
|
+
/maximum independent scopes/, /spawning every scope in one turn/,
|
|
145
|
+
/build\/test-green gate/, /no polling, guessing, or dependent work/,
|
|
146
|
+
/one reviewer per implementation scope/, /never deferred\/batched/,
|
|
147
|
+
/fact-check agent responses and cross-check implementation\/review yourself before acting/,
|
|
148
|
+
/return fixes to the original scope/, /loop verify -> fix -> re-verify until clean/,
|
|
149
|
+
/skip only simple, low-risk review/, /hard blocked/, /outcome and evidence/,
|
|
150
|
+
]);
|
|
151
|
+
requireAll(leadTool, 'Lead tools', [
|
|
152
|
+
/write-role agents self-verify/, /cross-scope verification.*benches.*all git/,
|
|
153
|
+
/workflow permits delegation/, /no-delegation workflow.*controls/,
|
|
154
|
+
]);
|
|
155
|
+
requireAll(general, 'General safety', [
|
|
156
|
+
/identify as mixdog\/current coding agent/, /destructive\/hard-to-reverse action needs explicit confirmation/,
|
|
157
|
+
/never push, build, deploy without explicit user request/, /implementation approval is not deploy approval/,
|
|
158
|
+
]);
|
|
159
|
+
requireAll(skip, 'Silent skip', [
|
|
160
|
+
/webhook-handler/, /scheduler-task/, /label-only, duplicate\/dedup, no action needed\/report/,
|
|
161
|
+
/whole response.*\[meta:silent\]/,
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
const roleSnapshots = {
|
|
165
|
+
'worker/AGENT.md': `# Worker
|
|
166
|
+
Scoped implementation agent.
|
|
167
|
+
|
|
168
|
+
Own only the bounded responsibility assigned in the brief. Trust its
|
|
169
|
+
\`file:line\` anchors; do only minimal targeted discovery, then make the
|
|
170
|
+
smallest coherent patch. No drive-by cleanup or scope expansion.
|
|
171
|
+
|
|
172
|
+
EDIT-FIRST DISCIPLINE. Patch promptly rather than repeating read-only turns;
|
|
173
|
+
stop and report blocked when the assigned scope cannot be completed.
|
|
174
|
+
|
|
175
|
+
Self-verify with a targeted check (for example, \`node --check\` or a focused
|
|
176
|
+
test), then report the changed \`file:line\` and stop.`,
|
|
177
|
+
'heavy-worker/AGENT.md': `# Heavy Worker
|
|
178
|
+
Own the assigned implementation slice through staged delivery.
|
|
179
|
+
|
|
180
|
+
Break work into bounded, dependency-aware slices and execute them in sequence.
|
|
181
|
+
At each checkpoint, run the narrowest relevant test or build before expanding
|
|
182
|
+
the slice. Keep the smallest coherent change; control blast radius rather than
|
|
183
|
+
rewriting adjacent systems.
|
|
184
|
+
|
|
185
|
+
EDIT-FIRST DISCIPLINE. Patch incrementally and stop at the first explicit
|
|
186
|
+
boundary: unclear ownership, a missing dependency, or growing blast radius.
|
|
187
|
+
Do not cross that boundary without a new bounded assignment; report blocked
|
|
188
|
+
work with the relevant file:line.
|
|
189
|
+
|
|
190
|
+
Self-verify each checkpoint and the final slice with shell (targeted test/build).`,
|
|
191
|
+
'reviewer/AGENT.md': `# Reviewer
|
|
192
|
+
Independent regression/risk review agent.
|
|
193
|
+
|
|
194
|
+
Review the approved intent, diff, and tests with independent judgment. Prioritize
|
|
195
|
+
actionable correctness, regression, security, and verification risks; inspect
|
|
196
|
+
affected boundaries. Do not reimplement the change or report non-risky nits.
|
|
197
|
+
Report findings first, severity-ordered, with one line per \`file:line\`. If clean,
|
|
198
|
+
say so in one line and include only material residual risk.`,
|
|
199
|
+
'debugger/AGENT.md': `# Debugger
|
|
200
|
+
Root-cause analysis agent.
|
|
201
|
+
|
|
202
|
+
Smallest confirmed cause chain before fixes. Return likely cause, evidence
|
|
203
|
+
(\`file:line\`), smallest next check/fix. Mark confirmed facts vs inferences;
|
|
204
|
+
avoid broad speculation.
|
|
205
|
+
|
|
206
|
+
Converge, don't sweep: when new evidence stops accruing, report the best
|
|
207
|
+
cause chain so far.`,
|
|
208
|
+
};
|
|
209
|
+
for (const [name, expected] of Object.entries(roleSnapshots)) {
|
|
210
|
+
const text = roles[name];
|
|
211
|
+
const permission = text.match(/^permission:\s*(.+)$/m)?.[1];
|
|
212
|
+
assert(permission === (name === 'worker/AGENT.md' || name === 'heavy-worker/AGENT.md' ? 'read-write' : 'read'), `${name}: permission contract changed`);
|
|
213
|
+
snapshot(roleBody(text, name), expected, name);
|
|
65
214
|
}
|
|
66
|
-
assert(/
|
|
67
|
-
assert(/
|
|
68
|
-
assert(/Stop:|how-to-verify|how to verify/i.test(flat(roles['heavy-worker/AGENT.md'])), 'heavy-worker: must bound scope / state how to verify');
|
|
215
|
+
assert(/confirmed facts vs inferences/i.test(normalize(roles['debugger/AGENT.md'])), 'debugger: must separate confirmed facts from inferences');
|
|
216
|
+
assert(/file:line/i.test(normalize(roles['debugger/AGENT.md'])), 'debugger: must anchor evidence to file:line');
|
|
69
217
|
|
|
70
218
|
// --- Injection: Lead rules actually carry the brief contract ---------------
|
|
71
219
|
const dataDir = mkdtempSync(join(tmpdir(), 'mixdog-internal-comms-smoke-'));
|
|
72
220
|
try {
|
|
73
221
|
const leadRules = rulesBuilder.buildInjectionContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
|
|
74
|
-
assert(
|
|
222
|
+
assert(TOKEN_PRINCIPLE.test(normalize(leadRules)), 'injected Lead rules must carry the brief token principle');
|
|
75
223
|
for (const field of BRIEF_FIELDS) assert(leadRules.includes(field), `injected Lead rules missing brief field ${field}`);
|
|
76
224
|
} finally {
|
|
77
225
|
rmSync(dataDir, { recursive: true, force: true });
|
|
@@ -35,8 +35,44 @@ function taskId(text) {
|
|
|
35
35
|
async function main() {
|
|
36
36
|
const leadToolRules = readFileSync('src/rules/lead/lead-tool.md', 'utf8');
|
|
37
37
|
const workflowRules = readFileSync('src/workflows/default/WORKFLOW.md', 'utf8');
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const soloRules = readFileSync('src/workflows/solo/WORKFLOW.md', 'utf8');
|
|
39
|
+
const compact = (text) => text.toLowerCase().replace(/\s+/g, ' ');
|
|
40
|
+
const hasAll = (text, ...terms) => terms.every((term) => text.includes(term));
|
|
41
|
+
const lead = compact(leadToolRules);
|
|
42
|
+
const workflow = compact(workflowRules);
|
|
43
|
+
const solo = compact(soloRules);
|
|
44
|
+
const reviewSkipViolation = (text) => compact(text)
|
|
45
|
+
.split(/[.!?]\s+|;|,\s+(?=(?:but|however|yet)\b)/)
|
|
46
|
+
.some((clause) => {
|
|
47
|
+
const hasReview = /\b(review|reviewer|verification)\b/.test(clause);
|
|
48
|
+
const hasSkip = /\b(skip|skipping|skipped|omit|omits|omitting|omitted)\b/.test(clause) ||
|
|
49
|
+
/\bwithout\s+(?:any\s+)?(?:a\s+)?(?:review|reviewer|verification)\b/.test(clause);
|
|
50
|
+
const negated = /\b(?:never|(?:must|shall|may)\s+not|can(?:not|'t)|do\s+not|don't|not)\b[^;]{0,40}\b(?:skip|skipping|skipped|omit|omitting|omitted)\b/.test(clause) ||
|
|
51
|
+
/\b(?:skip|skipping|skipped|omit|omitting|omitted)\b[^;]{0,40}\b(?:not allowed|forbidden|prohibited)\b/.test(clause) ||
|
|
52
|
+
/\b(?:never|(?:must|shall|may)\s+not|can(?:not|'t)|do\s+not|don't|not)\b[^;]{0,40}\bwithout\s+(?:any\s+)?(?:a\s+)?(?:review|reviewer|verification)\b/.test(clause);
|
|
53
|
+
const exception = /\b(?:unless|except(?:\s+when)?|only\s+if)\b/.test(clause);
|
|
54
|
+
return hasReview && hasSkip && (!negated || exception);
|
|
55
|
+
});
|
|
56
|
+
for (const [phrase, expected] of [
|
|
57
|
+
['Never skip Reviewer verification', false],
|
|
58
|
+
['Reviewer verification must not be skipped', false],
|
|
59
|
+
['Must not proceed without review', false],
|
|
60
|
+
['Never skip risky review, but may skip simple review', true],
|
|
61
|
+
['Never skip review unless low-risk', true],
|
|
62
|
+
['Never skip review except when low-risk', true],
|
|
63
|
+
['Never skip review only if low-risk', true],
|
|
64
|
+
['May omit review', true],
|
|
65
|
+
['Proceed without review', true],
|
|
66
|
+
]) assert(reviewSkipViolation(phrase) === expected, `review-skip detector case failed: ${phrase}`);
|
|
67
|
+
const skipsReview = reviewSkipViolation(workflow);
|
|
68
|
+
assert(hasAll(lead, 'workflow permits delegation', 'use `agent`', 'implementation', 'research', 'review', 'debugging'), 'lead rules must conditionally direct scoped work to agents');
|
|
69
|
+
assert(hasAll(workflow, 'after approval', 'delegate', 'by default'), 'default workflow must delegate after approval');
|
|
70
|
+
assert(hasAll(workflow, 'coordinates', 'git', '1-edit', '1-check'), 'default workflow must limit Lead direct work');
|
|
71
|
+
assert(hasAll(workflow, 'implementation/research/debugging', 'matching agent'), 'default workflow must route other work to matching agents');
|
|
72
|
+
assert(hasAll(workflow, 'parallel', 'independent', 'every delegated implementation'), 'workflow rules must keep independent work parallel');
|
|
73
|
+
assert(hasAll(workflow, 'every delegated implementation', 'reviewer', 'lead integration', 'fix', 're-verify'), 'default workflow must require review and the fix loop');
|
|
74
|
+
assert(!skipsReview, 'default workflow must not permit delegated-review skips');
|
|
75
|
+
assert(hasAll(solo, 'never spawn', 'send', 'delegate', 'ask agents'), 'Solo workflow must forbid delegation');
|
|
40
76
|
assert(/always start background tasks/i.test(AGENT_TOOL.description || '') && /distinct tags?/i.test(AGENT_TOOL.description || '') && /completion notification/i.test(AGENT_TOOL.description || ''), 'agent tool description must expose async parallel tags');
|
|
41
77
|
|
|
42
78
|
mkdirSync(dataDir, { recursive: true });
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { resolveMaintenancePreset } from '../src/runtime/shared/llm/index.mjs'
|
|
4
|
+
import { sonnetCascade } from '../src/runtime/memory/lib/memory-cycle2-gate.mjs'
|
|
5
|
+
import { runPhaseMerge } from '../src/runtime/memory/lib/memory-cycle2-mutations.mjs'
|
|
6
|
+
import { invokeCycle3Maintenance } from '../src/runtime/memory/lib/memory-cycle3.mjs'
|
|
7
|
+
|
|
8
|
+
test('cycle2 cascade uses the memory maintenance route', async () => {
|
|
9
|
+
const expected = resolveMaintenancePreset('memory')
|
|
10
|
+
let received
|
|
11
|
+
await sonnetCascade(
|
|
12
|
+
[{ id: 1, status: 'pending', verb: 'active', category: 'fact', element: 'x', summary: 'y' }],
|
|
13
|
+
'',
|
|
14
|
+
{
|
|
15
|
+
callLlm: (opts) => {
|
|
16
|
+
received = opts
|
|
17
|
+
return '1|keep'
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
assert.deepEqual(received.preset, expected)
|
|
22
|
+
assert.equal(received.mode, 'cycle2-cascade')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('cycle2 cascade preserves an explicit test override', async () => {
|
|
26
|
+
let received
|
|
27
|
+
await sonnetCascade(
|
|
28
|
+
[{ id: 1, status: 'pending', verb: 'active', category: 'fact', element: 'x', summary: 'y' }],
|
|
29
|
+
'',
|
|
30
|
+
{
|
|
31
|
+
cascadePreset: 'test-route',
|
|
32
|
+
callLlm: (opts) => {
|
|
33
|
+
received = opts
|
|
34
|
+
return '1|keep'
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
assert.equal(received.preset, 'test-route')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('cycle2 phase merge judge dispatches through the memory maintenance route', async () => {
|
|
42
|
+
const expected = resolveMaintenancePreset('memory')
|
|
43
|
+
let received
|
|
44
|
+
const db = {
|
|
45
|
+
query: async () => ({ rows: [] }),
|
|
46
|
+
}
|
|
47
|
+
db.query = async (sql) => {
|
|
48
|
+
if (sql.includes('WITH active AS')) {
|
|
49
|
+
return {
|
|
50
|
+
rows: [{
|
|
51
|
+
a_id: 1, a_category: 'fact', a_summary: 'a', a_score: 1, a_last_seen_at: 1, a_status: 'active',
|
|
52
|
+
b_id: 2, b_category: 'fact', b_summary: 'b', b_score: 1, b_last_seen_at: 1, b_status: 'active',
|
|
53
|
+
sim: 0.8,
|
|
54
|
+
}],
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { rows: [] }
|
|
58
|
+
}
|
|
59
|
+
const result = await runPhaseMerge(db, {
|
|
60
|
+
callLlm: (opts) => {
|
|
61
|
+
received = opts
|
|
62
|
+
return 'distinct'
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
assert.equal(result.llm_calls, 1)
|
|
66
|
+
assert.deepEqual(received.preset, expected)
|
|
67
|
+
assert.equal(received.mode, 'cycle2-phase_merge_judge')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('cycle2 phase merge preserves an explicit test override', async () => {
|
|
71
|
+
let received
|
|
72
|
+
const db = {
|
|
73
|
+
query: async (sql) => (sql.includes('WITH active AS')
|
|
74
|
+
? { rows: [{ a_id: 1, a_category: 'fact', a_summary: 'a', a_score: 1, a_last_seen_at: 1, a_status: 'active', b_id: 2, b_category: 'fact', b_summary: 'b', b_score: 1, b_last_seen_at: 1, b_status: 'active', sim: 0.8 }] }
|
|
75
|
+
: { rows: [] }),
|
|
76
|
+
}
|
|
77
|
+
await runPhaseMerge(db, {
|
|
78
|
+
preset: 'test-route',
|
|
79
|
+
callLlm: (opts) => {
|
|
80
|
+
received = opts
|
|
81
|
+
return 'distinct'
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
assert.equal(received.preset, 'test-route')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('cycle3 dispatches through the memory maintenance route', async () => {
|
|
88
|
+
const expected = resolveMaintenancePreset('memory')
|
|
89
|
+
let received
|
|
90
|
+
await invokeCycle3Maintenance('test prompt', {
|
|
91
|
+
callLlm: (opts) => {
|
|
92
|
+
received = opts
|
|
93
|
+
return '1|keep'
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
assert.deepEqual(received.preset, expected)
|
|
97
|
+
assert.equal(received.agent, 'cycle3-agent')
|
|
98
|
+
assert.equal(received.mode, 'cycle3-review')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('cycle3 preserves an explicit test override', async () => {
|
|
102
|
+
let received
|
|
103
|
+
await invokeCycle3Maintenance('test prompt', {
|
|
104
|
+
preset: 'test-route',
|
|
105
|
+
callLlm: (opts) => {
|
|
106
|
+
received = opts
|
|
107
|
+
return '1|keep'
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
assert.equal(received.preset, 'test-route')
|
|
111
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
|
|
5
|
+
const root = new URL('..', import.meta.url)
|
|
6
|
+
const rawRules = Object.fromEntries(await Promise.all([
|
|
7
|
+
['cycle1', 'src/rules/agent/40-cycle1-agent.md'],
|
|
8
|
+
['cycle2', 'src/rules/agent/41-cycle2-agent.md'],
|
|
9
|
+
['cycle3', 'src/rules/agent/42-cycle3-agent.md'],
|
|
10
|
+
].map(async ([name, path]) => [
|
|
11
|
+
name,
|
|
12
|
+
await readFile(new URL(path, root), 'utf8'),
|
|
13
|
+
])))
|
|
14
|
+
const rules = Object.fromEntries(Object.entries(rawRules)
|
|
15
|
+
.map(([name, text]) => [name, text.replace(/\s+/g, ' ').trim()]))
|
|
16
|
+
|
|
17
|
+
function includesAll(text, clauses) {
|
|
18
|
+
for (const clause of clauses) assert.ok(text.includes(clause), `missing: ${clause}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('cycle1 preserves chunk schema, taxonomy, coverage, and escaping', () => {
|
|
22
|
+
const text = rules.cycle1
|
|
23
|
+
includesAll(text, [
|
|
24
|
+
'<idx_csv>|<element>|<category>|<summary>', 'included input row numbers',
|
|
25
|
+
'comma-separated, without `@`', '5–10-word recall key',
|
|
26
|
+
'`rule` (standing policy)', '`constraint` (hard limit)',
|
|
27
|
+
'`decision` (agreed choice)', '`fact` (verified truth)', '`goal` (open target)',
|
|
28
|
+
'`preference` (style/taste)', '`task` (pending work)', '`issue` (broken state)',
|
|
29
|
+
'1–3 complete sentences', 'important names, paths, IDs, versions, numbers, errors, causes, and outcomes verbatim',
|
|
30
|
+
'match input language', 'Every input row appears exactly once',
|
|
31
|
+
'Group nearby same-topic rows, splitting only at real topic changes',
|
|
32
|
+
'retain clarifications with their topic', 'Never mix `[sess:XXX]` markers',
|
|
33
|
+
'Replace literal `|` with `/`', 'fields contain no newlines',
|
|
34
|
+
'No JSON, fences, prose, preamble, or tool calls',
|
|
35
|
+
])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('cycle2 preserves essential taxonomy, phases, rejects, formats, and fields', () => {
|
|
39
|
+
const text = rules.cycle2
|
|
40
|
+
includesAll(text, [
|
|
41
|
+
'`is_root` long-term memory', 'candidate `id`/`category`/`score`/`element`/`summary`',
|
|
42
|
+
'only when clearly exactly one essential concept',
|
|
43
|
+
'identity (stable non-derivable user fact)', 'preference (durable taste/style/interaction preference)',
|
|
44
|
+
'goal (long-running committed goal)', 'principle (cross-session behavior directive)',
|
|
45
|
+
'policy (standing team decision)', 'procedure (recurring trigger + steps + caveats)',
|
|
46
|
+
'event (rare foundational change not reconstructible from its rule)',
|
|
47
|
+
'system constant (durable path/schema/model/channel invariant needed later and absent from rules)',
|
|
48
|
+
'Anything unclear or outside these concepts is `archived`',
|
|
49
|
+
'`phase1_new_chunks` → `active` if clearly essential, otherwise `archived`',
|
|
50
|
+
'`phase2_reevaluate` → `active` to promote, otherwise `archived`',
|
|
51
|
+
'`phase3_active_review` requires every-row verdict: default `archived`, or `active`, `update`, `merge`',
|
|
52
|
+
'silence is not keep', 'work narratives', 'static facts without behavior/user value',
|
|
53
|
+
'rule-system meta', 'resolved bug/fix logs', 'rule-file duplicates',
|
|
54
|
+
'single-run measurements/counts/versions', 'session-scoped or in-progress decisions',
|
|
55
|
+
'<id>|<verb>', '<id>|update|<element>|<summary>',
|
|
56
|
+
'<id>|merge|<target_id>|<source_ids_csv>|<element>|<summary>',
|
|
57
|
+
'Use only input IDs; never invent IDs', 'fresh `element` and a 3-sentence `summary`',
|
|
58
|
+
'keeps `target_id`, absorbs `source_ids_csv`, and uses only one `project_id`',
|
|
59
|
+
'complete sentences in input language', 'preserve important specifics verbatim',
|
|
60
|
+
'omit actor/meta filler', '`rule > constraint > decision > fact > goal > preference > task > issue`',
|
|
61
|
+
'Replace literal `|` with `/`', 'fields contain no newlines',
|
|
62
|
+
'For phase 3, emit one verdict per input row', 'start with a digit',
|
|
63
|
+
])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('cycle3 preserves durable-event verdicts, formats, and exceptions', () => {
|
|
67
|
+
const text = rules.cycle3
|
|
68
|
+
includesAll(text, [
|
|
69
|
+
'one digit-starting pipe verdict line per input id', 'CORE is durable standing knowledge',
|
|
70
|
+
'rules, preferences, identity, goals, and current system/structure descriptions—not a log',
|
|
71
|
+
'Each entry is one short clause (≤120 chars)',
|
|
72
|
+
'Current rule/preference/live structure = durable',
|
|
73
|
+
'past event (shipped version, measured value, made fix) = not durable', 'When unsure, keep',
|
|
74
|
+
'`keep`: durable, already one short clause',
|
|
75
|
+
'`update`: durable but verbose/multi-sentence; compress to one ≤120-char clause',
|
|
76
|
+
'`merge`: duplicate; fold into its survivor in the same project pool',
|
|
77
|
+
'`delete`: past event, not a current rule or structure',
|
|
78
|
+
'Verbose durable is always `update`, never `keep`',
|
|
79
|
+
'<id>|keep', '<id>|update|<element>|<summary>',
|
|
80
|
+
'<id>|merge|<target_id>|<source_ids_csv>', '<id>|delete',
|
|
81
|
+
'IDs match input rows; never invent them', 'summary is one ≤120-char clause',
|
|
82
|
+
'`element` is short', 'retains `target_id`, absorbs sources, and stays within one `project_id`',
|
|
83
|
+
'Replace literal `|` with `/`', 'fields contain no newlines',
|
|
84
|
+
'Emit a verdict for every input row', 'start with a digit',
|
|
85
|
+
])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('compacted rules remain below their pre-compaction character counts', () => {
|
|
89
|
+
const preCompaction = { cycle1: 1214, cycle2: 2166, cycle3: 1530 }
|
|
90
|
+
for (const [name, before] of Object.entries(preCompaction)) {
|
|
91
|
+
assert.ok(rawRules[name].length < before, `${name} was not compacted`)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// notifyFnForSession must not double-inject a background-task completion. The
|
|
2
|
+
// mirror skip is gated on an EXPLICIT model-visible-delivery ack
|
|
3
|
+
// (modelVisibleDelivered), set ONLY by the TUI execution-ui path that injects
|
|
4
|
+
// the completion body into the active loop — never on a generic truthy
|
|
5
|
+
// onNotification return. So: delivered ack → mirror suppressed; headless /
|
|
6
|
+
// display-only / API listener (no ack) → mirror kept as the sole delivery.
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
|
|
10
|
+
import { shouldMirrorCompletionToPendingQueue } from '../src/session-runtime/runtime-core.mjs';
|
|
11
|
+
|
|
12
|
+
// A terminal agent completion with a Result body — accepted by
|
|
13
|
+
// shouldPersistModelVisibleToolCompletion.
|
|
14
|
+
const completionText = 'Async agent task task_1 completed finished.\n\nResult:\n> ok';
|
|
15
|
+
const completionMeta = { type: 'agent_task_result', execution_id: 'task_1', status: 'completed' };
|
|
16
|
+
|
|
17
|
+
test('delivered-live completion: no pending mirror (explicit model-visible ack)', () => {
|
|
18
|
+
const mirror = shouldMirrorCompletionToPendingQueue({
|
|
19
|
+
callerSessionId: 'sess_live',
|
|
20
|
+
modelVisibleDelivered: true,
|
|
21
|
+
hasEnqueue: true,
|
|
22
|
+
text: completionText,
|
|
23
|
+
meta: completionMeta,
|
|
24
|
+
});
|
|
25
|
+
assert.equal(mirror, false, 'live twin already injected → mirror suppressed');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('headless completion: pending mirror kept (no ack)', () => {
|
|
29
|
+
const mirror = shouldMirrorCompletionToPendingQueue({
|
|
30
|
+
callerSessionId: 'sess_headless',
|
|
31
|
+
modelVisibleDelivered: false,
|
|
32
|
+
hasEnqueue: true,
|
|
33
|
+
text: completionText,
|
|
34
|
+
meta: completionMeta,
|
|
35
|
+
});
|
|
36
|
+
assert.equal(mirror, true, 'no listener delivered → queue copy is the sole delivery');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('display-only listener returning true (no ack): pending mirror kept', () => {
|
|
40
|
+
// A generic display/API listener consumes the event (would return true) but
|
|
41
|
+
// never injects the model-visible body, so it sets NO modelVisibleDelivered
|
|
42
|
+
// ack. The mirror must stay, or the model never sees the completion body.
|
|
43
|
+
const mirror = shouldMirrorCompletionToPendingQueue({
|
|
44
|
+
callerSessionId: 'sess_display_only',
|
|
45
|
+
modelVisibleDelivered: false,
|
|
46
|
+
hasEnqueue: true,
|
|
47
|
+
text: completionText,
|
|
48
|
+
meta: completionMeta,
|
|
49
|
+
});
|
|
50
|
+
assert.equal(mirror, true, 'generic handled===true is NOT an ack → mirror kept');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('unhandled but non-terminal notification is never mirrored', () => {
|
|
54
|
+
const mirror = shouldMirrorCompletionToPendingQueue({
|
|
55
|
+
callerSessionId: 'sess_headless',
|
|
56
|
+
modelVisibleDelivered: false,
|
|
57
|
+
hasEnqueue: true,
|
|
58
|
+
text: 'still running...',
|
|
59
|
+
meta: { status: 'running' },
|
|
60
|
+
});
|
|
61
|
+
assert.equal(mirror, false, 'only persistable terminal completions mirror');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('missing session id or enqueue capability never mirrors', () => {
|
|
65
|
+
assert.equal(shouldMirrorCompletionToPendingQueue({
|
|
66
|
+
callerSessionId: '', modelVisibleDelivered: false, hasEnqueue: true,
|
|
67
|
+
text: completionText, meta: completionMeta,
|
|
68
|
+
}), false, 'no caller session → nothing to mirror into');
|
|
69
|
+
assert.equal(shouldMirrorCompletionToPendingQueue({
|
|
70
|
+
callerSessionId: 'sess_headless', modelVisibleDelivered: false, hasEnqueue: false,
|
|
71
|
+
text: completionText, meta: completionMeta,
|
|
72
|
+
}), false, 'no enqueue capability → cannot mirror');
|
|
73
|
+
});
|
|
@@ -13,7 +13,7 @@ function assert(condition, message) {
|
|
|
13
13
|
if (!condition) throw new Error(message);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const DEFAULT_REPORT_LABELS = new Set(['\uBC14\uB010 \uC810', '\uD655\uC778\uD55C \uAC83', '\uB0A8\uC740 \uB9AC\uC2A4\uD06C/\uB2E4\uC74C \uB2E8\uACC4', '\uB2E4\uC74C \uB2E8\uACC4']);
|
|
16
|
+
const DEFAULT_REPORT_LABELS = new Set(['Changes', 'Verification', 'Risks / next steps', '\uBC14\uB010 \uC810', '\uD655\uC778\uD55C \uAC83', '\uB0A8\uC740 \uB9AC\uC2A4\uD06C/\uB2E4\uC74C \uB2E8\uACC4', '\uB2E4\uC74C \uB2E8\uACC4']);
|
|
17
17
|
|
|
18
18
|
function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedLabels = new Set() } = {}) {
|
|
19
19
|
const text = String(value || '').trim();
|
|
@@ -52,7 +52,7 @@ for (const required of [
|
|
|
52
52
|
'Mixdog default — the most detailed style',
|
|
53
53
|
'State conclusions, not reasoning',
|
|
54
54
|
'Use labels such as',
|
|
55
|
-
'
|
|
55
|
+
'`Changes`, `Verification`,',
|
|
56
56
|
'Synthesize agent or retrieval results',
|
|
57
57
|
'Do not hide blockers',
|
|
58
58
|
]) {
|