kenaz 0.3.2 → 0.3.5

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 (71) hide show
  1. package/core/.claude/commands/Ansuz.md +36 -35
  2. package/core/.claude/hooks/__tests__/task-2395-project-registration.test.js +84 -0
  3. package/core/.claude/hooks/_ansuz-conversation.js +1 -0
  4. package/core/.claude/hooks/_ansuz-conversation.test.js +6 -0
  5. package/core/.claude/hooks/_run-user-prompt.js +0 -4
  6. package/core/.claude/hooks/_run-user-prompt.test.js +2 -3
  7. package/core/.claude/hooks/handoff-staleness.js +2 -2
  8. package/core/.claude/hooks/turn-context.js +50 -2
  9. package/core/.claude/settings.json +0 -37
  10. package/core/.collaboration/core/task-api.js +4 -12
  11. package/core/.formation/README.md +21 -146
  12. package/core/.formation/bifrost/codex-doctor.js +37 -2
  13. package/core/.formation/bifrost/codex-doctor.test.js +18 -0
  14. package/core/.formation/bifrost/launch.js +1 -1
  15. package/core/.formation/bifrost/openai/codex-hook-shim.js +6 -0
  16. package/core/.formation/bifrost/openai/codex-hook-shim.test.js +11 -1
  17. package/core/.formation/bifrost/setup-codex.js +3 -0
  18. package/core/.formation/spec/task_operations.parity.test.js +0 -1
  19. package/core/.formation/tools/dashboard-models-chain.test.js +0 -6
  20. package/core/.formation/tools/dispatch-detail/cross-project-worktree.md +1 -9
  21. package/core/.formation/tools/dispatch-detail/worktree-subagent-mode.md +2 -2
  22. package/core/.formation/tools/dispatch.js +28 -549
  23. package/core/.formation/tools/mcp-allowlist.test.js +0 -1
  24. package/core/.formation/tools/post-complete/task2345.test.js +0 -5
  25. package/core/.formation/tools/pre-dispatch-snapshot.test.js +0 -100
  26. package/core/.formation/tools/prompt.js +66 -1607
  27. package/core/.formation/tools/prompt.test.js +43 -582
  28. package/core/.kenaz/claude-hooks-manifest.json +16 -16
  29. package/core/.specialist/Ansuz/RULES.md +6 -10
  30. package/core/.specialist/tools/__tests__/freya-cli-contract.test.js +0 -1
  31. package/core/.specialist/tools/__tests__/task-2218-boot-session-id.test.js +17 -1
  32. package/core/.specialist/tools/__tests__/task-2222-session-identity-interleave.test.js +17 -1
  33. package/core/.specialist/tools/__tests__/task-2254-handoff-archive-contract.test.js +6 -5
  34. package/core/.specialist/tools/ansuz-boot-fast.js +44 -100
  35. package/core/.specialist/tools/ansuz-boot-fast.test.js +45 -1
  36. package/core/KENAZ_CORE_VERSION +1 -1
  37. package/core/dev/plans/ANSUZ_SLIM_PLAN.md +68 -0
  38. package/core/plugins/kenaz/commands/Amelia.md +1 -1
  39. package/core/plugins/kenaz/commands/Ansuz.md +36 -35
  40. package/core/plugins/kenaz/hooks/hooks.json +1 -5
  41. package/core/plugins/kenaz/hooks/kenaz-hook.js +0 -4
  42. package/core/plugins/kenaz/scripts/kenaz-mode.js +11 -1
  43. package/core/plugins/kenaz/scripts/kenaz.test.js +1 -1
  44. package/core/plugins/kenaz/scripts/sync.js +6 -2
  45. package/core/plugins/kenaz/scripts/ygg.js +13 -1
  46. package/core/plugins/kenaz/skills/product_manager/SKILL.md +12 -12
  47. package/lib/cli.js +47 -6
  48. package/lib/cli.test.js +24 -0
  49. package/lib/dashboard.js +10 -0
  50. package/package.json +11 -7
  51. package/vendor/install-kenaz-cli.js +18 -7
  52. package/core/.formation/tools/broadcast.js +0 -103
  53. package/core/.formation/tools/dispatch-dryrun.test.js +0 -54
  54. package/core/.formation/tools/dispatch-notifier.js +0 -84
  55. package/core/.formation/tools/formations.js +0 -1203
  56. package/core/.formation/tools/headless-branch-recovery.test.js +0 -64
  57. package/core/.formation/tools/headless-dryrun.test.js +0 -79
  58. package/core/.formation/tools/headless-v2.js +0 -659
  59. package/core/.formation/tools/headless.js +0 -1071
  60. package/core/.formation/tools/legacy-scheduler.js +0 -1994
  61. package/core/.formation/tools/logger.js +0 -273
  62. package/core/.formation/tools/memory-loader.js +0 -422
  63. package/core/.formation/tools/process-state.js +0 -33
  64. package/core/.formation/tools/remote-conflict.js +0 -98
  65. package/core/.formation/tools/runner.js +0 -1332
  66. package/core/.formation/tools/task-utils.js +0 -344
  67. package/core/.formation/tools/task2028-headless-worktree.test.js +0 -112
  68. package/core/.formation/tools/tests/headless-mcp-args-plumbing.test.js +0 -123
  69. package/core/.formation/tools/worktree-leak.test.js +0 -162
  70. package/core/.specialist/tools/__tests__/task-1863-boot-lease-reuse.test.js +0 -99
  71. package/core/.specialist/tools/__tests__/task-1977-banner-cap.test.js +0 -129
@@ -1,12 +1,9 @@
1
1
  /**
2
- * TASK_1978 — scaffold diet S2: dispatch prompt ceremony compression.
3
- *
4
- * Verifies the four compressed sub-agent/dispatch ceremony sections in
5
- * prompt.js stay within the ≤800-byte-per-section budget, that every hard
6
- * constraint that existed before the compression is still present (as a
7
- * substring), and that the corresponding dispatch-detail/*.md files (moved
8
- * explanatory prose) exist. Also checks the Shared Module: testing
9
- * injection (sourced from .formation/roles/_shared/modules/testing.yaml).
2
+ * TASK_1978 — scaffold diet S2: sub-agent dispatch prefixes stay within the
3
+ * byte budget and keep every hard constraint; the dispatch-detail files that
4
+ * hold the moved-out explanatory prose exist. Plus the live prompt.js pieces:
5
+ * promptProfile / extract* / splitPromptSections, the Rust check gate, and the
6
+ * TASK_2391 short subagent template.
10
7
  */
11
8
 
12
9
  const test = require('node:test');
@@ -18,24 +15,12 @@ const {
18
15
  buildPrompt,
19
16
  SUBAGENT_PREFIX,
20
17
  buildSubagentWorktreePrefix,
21
- appendCognitiveCheckpoint,
22
- buildTaskContextQuery,
23
- appendAlayaLessons,
24
- deriveTaskStack,
25
- deriveBijaStack,
26
- passesAlayaRelevanceGate,
27
- ALAYA_MIN_COSINE_DEFAULT,
28
- ALAYA_STACK_OVERRIDE_COSINE,
29
- ALAYA_CANDIDATE_POOL,
30
18
  promptProfile,
31
19
  extractRigor,
32
20
  extractComplexity,
33
21
  splitPromptSections,
34
- resolveModules,
35
- appendMemoryContext,
36
22
  hasRustScope,
37
23
  appendRustCheckReminder,
38
- appendWorkflow,
39
24
  applyRustCheckToPrefix,
40
25
  } = require('./prompt');
41
26
 
@@ -122,260 +107,12 @@ test('cross-project worktree prefix ends with a Details pointer to its detail fi
122
107
  );
123
108
  });
124
109
 
125
- // ── Shared Module: testing (sourced from _shared/modules/testing.yaml) ─
126
-
127
- function renderTestingModule() {
128
- const modPath = path.join(ROOT, '.formation', 'roles', '_shared', 'modules', 'testing.yaml');
129
- const raw = fs.readFileSync(modPath, 'utf-8');
130
- const nameMatch = raw.match(/^name:\s*(.+)$/m);
131
- const descMatch = raw.match(/^description:\s*(.+)$/m);
132
- const moduleName = nameMatch ? nameMatch[1].trim() : path.basename(modPath, '.yaml');
133
- const moduleDesc = descMatch ? descMatch[1].trim() : '';
134
- const lines = [`## Shared Module: ${moduleName}`];
135
- if (moduleDesc) { lines.push(moduleDesc); lines.push(''); }
136
- const contentMatch = raw.match(/^content:\n([\s\S]*)$/m);
137
- lines.push(contentMatch ? contentMatch[1].trimEnd() : raw.trimEnd());
138
- return lines.join('\n');
139
- }
140
-
141
- test('Shared Module: testing injection stays within the compressed-section byte budget', () => {
142
- const out = renderTestingModule();
143
- assert.ok(
144
- bytes(out) <= BYTE_BUDGET,
145
- `Shared Module: testing is ${bytes(out)} bytes, expected <= ${BYTE_BUDGET}`
146
- );
147
- });
148
-
149
- test('Shared Module: testing keeps coverage targets and defect-severity rules', () => {
150
- const out = renderTestingModule();
151
- const mustContain = ['>= 80%', '>= 70%', '100%', 'Critical=fix now', 'CI-integrated'];
152
- for (const needle of mustContain) {
153
- assert.ok(out.includes(needle), `Shared Module: testing missing: "${needle}"`);
154
- }
155
- });
156
-
157
- test('Shared Module: testing ends with a Details pointer to its detail file', () => {
158
- const out = renderTestingModule();
159
- assert.match(
160
- out,
161
- /Details: \.formation\/tools\/dispatch-detail\/testing-module\.md \(Read only if needed\)/
162
- );
163
- });
164
-
165
- // ── appendAlayaLessons (TASK_1992: Process lessons via alaya_recall;
166
- // TASK_2101: absolute relevance floor + stack match on top) ──
167
-
168
- const ALAYA_TASK = JSON.stringify({
169
- title: 'Fix appendCortexContext dedup bug',
170
- scope_paths: ['.formation/tools/prompt.js'],
171
- });
172
-
173
- function makeStubCaller(response) {
174
- // 2026-10-10: Alaya lessons are frozen by default; these tests keep covering the renderer.
175
- return Object.assign((toolName, toolArgs) => {
176
- assert.equal(toolName, 'alaya_recall');
177
- assert.equal(toolArgs.mode, 'hybrid');
178
- assert.deepEqual(toolArgs.seed_types, ['failure_seed', 'lesson_seed']);
179
- // TASK_2101: wider candidate pool (post-filtering needs real candidates
180
- // to keep) and the server-side cosine floor.
181
- assert.equal(toolArgs.limit, ALAYA_CANDIDATE_POOL);
182
- assert.equal(toolArgs.min_cosine, ALAYA_MIN_COSINE_DEFAULT);
183
- if (typeof response === 'function') return response(toolArgs);
184
- return response;
185
- }, { __forceAlaya: true });
186
- }
187
-
188
- // High enough to clear ALAYA_STACK_OVERRIDE_COSINE (0.8) — bypasses the
189
- // stack-match half of the gate so these fixtures only exercise the floor
190
- // and the pre-2101 rendering/dedup/cap behavior.
191
- const HIGH_COSINE = 0.95;
192
-
193
- test('appendAlayaLessons renders the section from a stubbed hybrid recall with 3 seeds', () => {
194
- const lines = [];
195
- const stub = makeStubCaller({
196
- mode_effective: 'hybrid',
197
- bijas: [
198
- { id: 'a1', seed_type: 'failure_seed', origin: 'local', content: 'Root cause one.', cosine: HIGH_COSINE },
199
- { id: 'a2', seed_type: 'lesson_seed', origin: 'remote', content: 'Lesson two.', cosine: HIGH_COSINE },
200
- { id: 'a3', seed_type: 'failure_seed', origin: 'remote', content: 'Failure three.', cosine: HIGH_COSINE },
201
- ],
202
- });
203
- appendAlayaLessons(lines, ALAYA_TASK, stub);
204
- const out = lines.join('\n');
205
- assert.ok(out.includes('## Process lessons (Alaya)'));
206
- assert.match(out, /Query: ".+" — 3 seeds \(mode=hybrid\)\./);
207
- assert.ok(out.includes('- **failure_seed** [local] Root cause one.'));
208
- assert.ok(out.includes('- **lesson_seed** [remote] Lesson two.'));
209
- assert.ok(out.includes('- **failure_seed** [remote] Failure three.'));
210
- });
211
-
212
- test('appendAlayaLessons omits the section when the recall is null, empty, or throws', () => {
213
- const nullCase = [];
214
- appendAlayaLessons(nullCase, ALAYA_TASK, makeStubCaller(null));
215
- assert.equal(nullCase.length, 0);
216
-
217
- const emptyCase = [];
218
- appendAlayaLessons(emptyCase, ALAYA_TASK, makeStubCaller({ mode_effective: 'hybrid', bijas: [] }));
219
- assert.equal(emptyCase.length, 0);
220
-
221
- const throwCase = [];
222
- appendAlayaLessons(throwCase, ALAYA_TASK, () => { throw new Error('MCP unavailable'); });
223
- assert.equal(throwCase.length, 0);
224
-
225
- const noQueryCase = [];
226
- appendAlayaLessons(noQueryCase, JSON.stringify({}), makeStubCaller({ bijas: [{ id: 'x', content: 'never called' }] }));
227
- assert.equal(noQueryCase.length, 0);
228
- });
229
-
230
- test('appendAlayaLessons caps the rendered section at 900 chars total', () => {
231
- const lines = [];
232
- // Per-line content is already capped at 220 chars inside appendAlayaLessons,
233
- // so 3 seeds alone stay well under 900 — push the total over budget via a
234
- // long `origin` field (rendered verbatim, uncapped) instead.
235
- const longOrigin = 'origin-'.repeat(60); // ~420 chars
236
- const longContent = 'y'.repeat(220);
237
- const stub = makeStubCaller({
238
- mode_effective: 'hybrid',
239
- bijas: [
240
- { id: 'b1', seed_type: 'failure_seed', origin: longOrigin, content: longContent, cosine: HIGH_COSINE },
241
- { id: 'b2', seed_type: 'lesson_seed', origin: longOrigin, content: longContent, cosine: HIGH_COSINE },
242
- { id: 'b3', seed_type: 'failure_seed', origin: longOrigin, content: longContent, cosine: HIGH_COSINE },
243
- ],
244
- });
245
- appendAlayaLessons(lines, ALAYA_TASK, stub);
246
- const start = lines.indexOf('## Process lessons (Alaya)');
247
- assert.ok(start >= 0, 'section header not found');
248
- const sectionLines = lines.slice(start, lines.length - 1); // drop trailing blank line
249
- const section = sectionLines.join('\n');
250
- assert.ok(section.length <= 900, `section is ${section.length} chars, expected <= 900`);
251
- assert.ok(section.endsWith('…'), 'over-budget section should end with an ellipsis');
252
- });
253
-
254
- test('appendAlayaLessons dedups seeds by id', () => {
255
- const lines = [];
256
- const stub = makeStubCaller({
257
- mode_effective: 'hybrid',
258
- bijas: [
259
- { id: 'dup', seed_type: 'failure_seed', origin: 'local', content: 'First copy.', cosine: HIGH_COSINE },
260
- { id: 'dup', seed_type: 'failure_seed', origin: 'local', content: 'Second copy (duplicate id).', cosine: HIGH_COSINE },
261
- ],
262
- });
263
- appendAlayaLessons(lines, ALAYA_TASK, stub);
264
- const out = lines.join('\n');
265
- assert.match(out, /1 seeds/);
266
- assert.ok(out.includes('First copy.'));
267
- assert.ok(!out.includes('Second copy'));
268
- });
269
-
270
- test('buildTaskContextQuery is the single query builder shared by Cortex and Alaya', () => {
271
- const q = buildTaskContextQuery(ALAYA_TASK);
272
- assert.ok(q && q.includes('prompt'));
273
- assert.equal(buildTaskContextQuery(JSON.stringify({})), null);
274
- });
275
-
276
- // ── TASK_2101: absolute relevance floor (cosine) + stack match ─────────
277
-
278
- test('deriveTaskStack extracts extensions and top path segments from scope_paths', () => {
279
- const stack = deriveTaskStack(['dashboard/src-tauri/src/features/cortex/bija_embedding.rs', '.formation/tools/prompt.js']);
280
- assert.ok(stack.has('rs'));
281
- assert.ok(stack.has('js'));
282
- assert.ok(stack.has('dashboard'));
283
- assert.ok(stack.has('dashboard/src-tauri'));
284
- assert.ok(stack.has('.formation'));
285
- assert.ok(stack.has('.formation/tools'));
286
- assert.equal(deriveTaskStack([]).size, 0);
287
- assert.equal(deriveTaskStack(undefined).size, 0);
288
- });
289
-
290
- test('deriveBijaStack reads tags and source_symbol (file_path:symbol:kind)', () => {
291
- const fromSourceSymbol = deriveBijaStack({
292
- tags: '', source_symbol: 'dashboard/src-tauri/src/features/cortex/bija_embedding.rs:recall_bijas_hybrid_with_vec:function',
293
- });
294
- assert.ok(fromSourceSymbol.has('rs'));
295
- assert.ok(fromSourceSymbol.has('dashboard'));
296
-
297
- const fromTags = deriveBijaStack({ tags: 'unity, csharp', source_symbol: '' });
298
- assert.ok(fromTags.has('unity'));
299
- assert.ok(fromTags.has('csharp'));
300
-
301
- // No signal at all (today's common case pre-hub-upgrade) -> empty, not an error.
302
- assert.equal(deriveBijaStack({}).size, 0);
303
- assert.equal(deriveBijaStack({ tags: '', source_symbol: '' }).size, 0);
304
- });
305
-
306
- test('passesAlayaRelevanceGate: cosine floor alone gates a null/low cosine out', () => {
307
- const taskStack = deriveTaskStack(['.formation/tools/prompt.js']);
308
- assert.equal(passesAlayaRelevanceGate({ cosine: null }, taskStack, ALAYA_MIN_COSINE_DEFAULT), false, 'null cosine (unscored) must fail the floor');
309
- assert.equal(passesAlayaRelevanceGate({ cosine: ALAYA_MIN_COSINE_DEFAULT - 0.01 }, taskStack, ALAYA_MIN_COSINE_DEFAULT), false, 'below-floor cosine must fail');
310
- assert.equal(
311
- passesAlayaRelevanceGate({ cosine: ALAYA_MIN_COSINE_DEFAULT, tags: 'js' }, taskStack, ALAYA_MIN_COSINE_DEFAULT),
312
- true,
313
- 'at-floor cosine with a stack match must pass'
314
- );
315
- });
316
-
317
- test('passesAlayaRelevanceGate: stack mismatch is dropped unless cosine clears the override', () => {
318
- const taskStack = deriveTaskStack(['.formation/tools/prompt.js']); // rs/js/.formation stack
319
- const csharpNoise = { cosine: 0.75, tags: '', source_symbol: 'Assets/Combat/CombatFeedback.cs:Play:method' };
320
- assert.equal(passesAlayaRelevanceGate(csharpNoise, taskStack, ALAYA_MIN_COSINE_DEFAULT), false, 'cross-stack .cs seed above the floor but below the override must be dropped');
321
-
322
- const csharpButVeryRelevant = { ...csharpNoise, cosine: ALAYA_STACK_OVERRIDE_COSINE };
323
- assert.equal(passesAlayaRelevanceGate(csharpButVeryRelevant, taskStack, ALAYA_MIN_COSINE_DEFAULT), true, 'cosine >= override clears a stack mismatch');
324
-
325
- const matchingStack = { cosine: 0.75, tags: 'js', source_symbol: '' };
326
- assert.equal(passesAlayaRelevanceGate(matchingStack, taskStack, ALAYA_MIN_COSINE_DEFAULT), true, 'a real stack overlap needs no override');
327
- });
328
-
329
- test('passesAlayaRelevanceGate: an empty task stack (no scope_paths) skips the stack half, floor still applies', () => {
330
- const emptyTaskStack = deriveTaskStack([]);
331
- assert.equal(passesAlayaRelevanceGate({ cosine: 0.75, tags: 'unrelated' }, emptyTaskStack, ALAYA_MIN_COSINE_DEFAULT), true);
332
- assert.equal(passesAlayaRelevanceGate({ cosine: 0.1, tags: 'unrelated' }, emptyTaskStack, ALAYA_MIN_COSINE_DEFAULT), false);
333
- });
334
-
335
- test('ALAYA_MIN_COSINE_DEFAULT rejects the measured live noise band (TASK_2101 R4: noise 0.656-0.697, on-topic >= 0.709)', () => {
336
- const emptyTaskStack = deriveTaskStack([]);
337
- assert.equal(passesAlayaRelevanceGate({ cosine: 0.697 }, emptyTaskStack, ALAYA_MIN_COSINE_DEFAULT), false, 'top of the measured noise band must fail');
338
- assert.equal(passesAlayaRelevanceGate({ cosine: 0.709 }, emptyTaskStack, ALAYA_MIN_COSINE_DEFAULT), true, 'bottom of the measured on-topic band must pass');
339
- });
340
-
341
- test('appendAlayaLessons drops candidates below the cosine floor or with a stack mismatch; renders nothing when none pass', () => {
342
- const lines = [];
343
- const stub = makeStubCaller({
344
- mode_effective: 'hybrid',
345
- bijas: [
346
- // Below the floor entirely (mirrors the RRF-rank-only noise this card fixes).
347
- { id: 'low_cosine', seed_type: 'failure_seed', origin: 'remote', content: 'TASK_396 combat feel seam: CombatFeedback.Play', cosine: 0.05 },
348
- // Cleared the floor but wrong stack (Unity C# seed on a Rust/JS card) at a cosine well under the override.
349
- { id: 'wrong_stack', seed_type: 'lesson_seed', origin: 'remote', content: 'dear-game-vm deployment is Docker-on-VM', cosine: 0.75, source_symbol: 'infra/game-vm/deploy.cs:Deploy:function' },
350
- // Never scored by the semantic arm at all (fts-only hit).
351
- { id: 'unscored', seed_type: 'failure_seed', origin: 'remote', content: 'Meta App Review requires Instagram OAuth', cosine: null },
352
- ],
353
- });
354
- appendAlayaLessons(lines, ALAYA_TASK, stub);
355
- assert.equal(lines.length, 0, 'zero passing candidates must render nothing, not an empty section');
356
- });
357
-
358
- test('appendAlayaLessons keeps a real stack match even at a moderate (non-override) cosine', () => {
359
- const lines = [];
360
- const stub = makeStubCaller({
361
- mode_effective: 'hybrid',
362
- bijas: [
363
- { id: 'relevant', seed_type: 'failure_seed', origin: 'remote', content: 'get_db_dir cross-boundary failure in synapse connection', cosine: 0.75, tags: 'js' },
364
- ],
365
- });
366
- appendAlayaLessons(lines, ALAYA_TASK, stub);
367
- const out = lines.join('\n');
368
- assert.ok(out.includes('## Process lessons (Alaya)'));
369
- assert.ok(out.includes('get_db_dir cross-boundary failure in synapse connection'));
370
- });
371
-
372
110
  // ── dispatch-detail files exist (the moved-out explanatory prose) ─────
373
111
 
374
112
  test('every dispatch-detail file referenced by a compressed section exists', () => {
375
113
  const files = [
376
114
  'worktree-subagent-mode.md',
377
115
  'cross-project-worktree.md',
378
- 'testing-module.md',
379
116
  ];
380
117
  for (const f of files) {
381
118
  assert.ok(fs.existsSync(detailPath(f)), `missing dispatch-detail file: ${f}`);
@@ -414,72 +151,22 @@ test('elixir.yaml no longer claims a 0-11 strict lifecycle', () => {
414
151
  assert.match(elixir, /dev\/archive\/ansuz\/BOOT_FULL\.md/);
415
152
  });
416
153
 
417
- // ── TASK_1998: two-axis prompt policy (2026-09-22 user decision) ───────
418
- // CONTEXT volume (Cross-Project Context caps, Related Files Preview, Known
419
- // Failures, Project Memory) is driven by `complexity`. PROCESS strictness
420
- // (Working contract vs full ritual) is driven by `rigor`. The two axes are
421
- // independent — a rigor:low/complexity:complex card gets the full Cortex
422
- // Context but only the compact Working contract, and vice versa.
423
-
424
- test('promptProfile: complexity gates Related Files Preview / Known Failures / Project Memory', () => {
425
- for (const rigor of ['low', 'medium', 'high']) {
426
- const p = promptProfile({ rigor, complexity: 'simple' });
427
- assert.equal(p.skipRelatedFilesPreview, true, `rigor=${rigor}`);
428
- assert.equal(p.skipKnownFailures, true, `rigor=${rigor}`);
429
- assert.equal(p.skipProjectMemory, true, `rigor=${rigor}`);
430
- }
154
+ // ── promptProfile: complexity gates the Related Files Preview (dispatch.js) ──
155
+
156
+ test('promptProfile: only a complex card gets the Related Files Preview', () => {
157
+ assert.equal(promptProfile({ complexity: 'simple' }).skipRelatedFilesPreview, true);
431
158
  for (const complexity of ['medium', null, undefined, 'bogus']) {
432
- const p = promptProfile({ rigor: 'medium', complexity });
159
+ const p = promptProfile({ complexity });
160
+ assert.equal(p.complexity, 'medium', `complexity=${complexity}`);
433
161
  assert.equal(p.skipRelatedFilesPreview, true, `complexity=${complexity}`);
434
- assert.equal(p.skipKnownFailures, false, `complexity=${complexity}`);
435
- assert.equal(p.skipProjectMemory, true, `complexity=${complexity}`);
436
- }
437
- const c = promptProfile({ rigor: 'medium', complexity: 'complex' });
438
- assert.equal(c.skipRelatedFilesPreview, false);
439
- assert.equal(c.skipKnownFailures, false);
440
- assert.equal(c.skipProjectMemory, false);
441
- });
442
-
443
- test('TASK_2371: promptProfile has no Cortex fields for any complexity', () => {
444
- for (const complexity of ['simple', 'medium', 'complex']) {
445
- const p = promptProfile({ rigor: 'medium', complexity });
446
- for (const k of Object.keys(p)) assert.ok(!/cortex|CrossProject/i.test(k), `unexpected profile key ${k}`);
447
- }
448
- });
449
-
450
- test('promptProfile: rigor=high uses the same compact ritual as medium (TASK_2371)', () => {
451
- for (const complexity of ['simple', 'medium', 'complex']) {
452
- const p = promptProfile({ rigor: 'high', complexity });
453
- assert.equal(p.rigor, 'high');
454
- assert.equal(p.compactRitual, true, `complexity=${complexity}`);
455
- assert.equal(p.ritualTier, 'medium', `complexity=${complexity}`);
456
162
  }
163
+ assert.equal(promptProfile({ complexity: 'complex' }).skipRelatedFilesPreview, false);
457
164
  });
458
165
 
459
- test('promptProfile: rigor=low/medium (incl. missing/unrecognized, default) get the compact ritual, with distinct tiers', () => {
460
- const low = promptProfile({ rigor: 'low', complexity: 'medium' });
461
- assert.equal(low.compactRitual, true);
462
- assert.equal(low.ritualTier, 'low');
463
-
464
- for (const rigor of ['medium', null, undefined, 'bogus']) {
465
- const p = promptProfile({ rigor, complexity: 'medium' });
466
- assert.equal(p.compactRitual, true, `rigor=${rigor}`);
467
- assert.equal(p.ritualTier, 'medium', `rigor=${rigor}`);
468
- }
469
- });
470
-
471
- test('promptProfile: backwards-compatible string call maps rigor to the given string and complexity to medium', () => {
472
- const viaString = promptProfile('high');
473
- const viaObject = promptProfile({ rigor: 'high', complexity: 'medium' });
474
- assert.deepEqual(viaString, viaObject);
475
- assert.equal(viaString.rigor, 'high');
476
- assert.equal(viaString.complexity, 'medium');
477
-
478
- for (const v of [null, undefined]) {
479
- const p = promptProfile(v);
480
- assert.equal(p.rigor, 'medium');
481
- assert.equal(p.complexity, 'medium');
482
- }
166
+ test('promptProfile: rigor is ignored; a bare string or missing arg is a complexity / the medium default', () => {
167
+ assert.deepEqual(promptProfile({ rigor: 'high', complexity: 'complex' }), promptProfile('complex'));
168
+ assert.deepEqual(promptProfile(), { complexity: 'medium', skipRelatedFilesPreview: true });
169
+ assert.deepEqual(Object.keys(promptProfile({ complexity: 'complex' })).sort(), ['complexity', 'skipRelatedFilesPreview']);
483
170
  });
484
171
 
485
172
  test('extractRigor reads taskData.rigor; null when missing, unparseable, or non-string', () => {
@@ -519,154 +206,6 @@ test('splitPromptSections splits on "## "/"# " headers only, not "### " sub-head
519
206
  assert.ok(sections[0].bytes > 0 && sections[1].bytes > 0);
520
207
  });
521
208
 
522
- // ── rigor tiers: Working contract / short progress note / full ritual ──
523
-
524
- test('rigor low: the six ritual sections collapse into "## Working contract"; Progress Note is dropped entirely', () => {
525
- const taskData = JSON.stringify({ id: 'TASK_TEST', rigor: 'low' });
526
-
527
- const ckLines = [];
528
- appendCognitiveCheckpoint(ckLines, taskData, {});
529
- const ck = ckLines.join('\n');
530
- assert.ok(ck.includes('## Working contract'));
531
- assert.ok(!ck.includes('synapse_inscribe'), 'memory system frozen 2026-10-10: no rune line');
532
- assert.ok(!ck.includes('## Progress Note (brief)'), 'rigor:low should not get the short progress note');
533
- const ritualNeedles = [
534
- 'Before You Start — Cognitive Recall',
535
- `Before You Finish — Don't Let Your Discovery Die`,
536
- '## Cognitive Checkpoint',
537
- ];
538
- for (const needle of ritualNeedles) {
539
- assert.ok(!ck.includes(needle), `rigor:low output should not contain "${needle}"`);
540
- }
541
-
542
- });
543
-
544
- test('rigor medium: "## Working contract" plus a short progress note, no rune-inscribe line (memory frozen)', () => {
545
- const taskData = JSON.stringify({ id: 'TASK_TEST', rigor: 'medium' });
546
-
547
- const ckLines = [];
548
- appendCognitiveCheckpoint(ckLines, taskData, {});
549
- const ck = ckLines.join('\n');
550
- assert.ok(ck.includes('## Working contract'));
551
- assert.ok(ck.includes('## Progress Note (brief)'), 'rigor:medium should get the short progress note');
552
- assert.ok(!ck.includes('synapse_inscribe'), 'memory system frozen 2026-10-10: no rune line');
553
- assert.ok(!ck.includes('## Progress Note (mandatory — write before finishing)'), 'rigor:medium should not get the full Progress Note');
554
- assert.ok(ck.includes('parallel tool calls in ONE turn'), 'rigor:medium should get the concrete parallel-reads batching rule');
555
- assert.ok(ck.includes('one bash command with separators'), 'rigor:medium should get the concrete shell-batching rule');
556
- assert.ok(ck.includes('one command over all N'), 'rigor:medium should get the concrete N-similar-items batching rule');
557
-
558
- });
559
-
560
- test('rigor high: same brief ritual as medium, no mandatory Before You Start / Before You Finish / Cognitive Checkpoint (TASK_2371)', () => {
561
- const taskData = JSON.stringify({ id: 'TASK_TEST', rigor: 'high' });
562
- const ckLines = [];
563
- appendCognitiveCheckpoint(ckLines, taskData, {});
564
- const ck = ckLines.join('\n');
565
- assert.ok(ck.includes('## Working contract'));
566
- assert.ok(ck.includes('## Progress Note (brief)'));
567
- assert.ok(!ck.includes('Cognitive Checkpoint'));
568
- assert.ok(!ck.includes('Before You Start'));
569
- assert.ok(!ck.includes('Before You Finish'));
570
- assert.ok(!ck.includes('Progress Note (mandatory'));
571
- });
572
-
573
- test('a rigor:low subagent prompt (SUBAGENT_PREFIX + Working contract) keeps the worktree hard rules', () => {
574
- const taskData = JSON.stringify({ id: 'TASK_TEST', rigor: 'low' });
575
- const lines = [];
576
- appendCognitiveCheckpoint(lines, taskData, {});
577
- const full = SUBAGENT_PREFIX + lines.join('\n');
578
- assert.ok(full.includes('禁 push'), 'worktree hard rule (no push) missing');
579
- assert.ok(full.includes('刪 worktree'), 'worktree hard rule (no delete worktree) missing');
580
- assert.ok(full.includes('## Working contract'));
581
- });
582
-
583
- // Functional check against the REAL .failure/.memory dirs (no mocking) —
584
- // confirms the section actually appears/disappears in an assembled prompt,
585
- // not just that the profile flag is set correctly.
586
- test('appendMemoryContext: complexity:medium prompt contains "## Known Failures", complexity:simple does not; neither carries "## Project Memory" except complexity:complex', () => {
587
- const mediumTaskData = JSON.stringify({ id: 'TASK_TEST', complexity: 'medium' });
588
- const mediumLines = [];
589
- appendMemoryContext(mediumLines, mediumTaskData, {});
590
- const mediumText = mediumLines.join('\n');
591
- assert.ok(mediumText.includes('## Known Failures'), 'complexity:medium prompt should contain "## Known Failures"');
592
- assert.ok(!mediumText.includes('## Project Memory'), 'complexity:medium prompt should NOT contain "## Project Memory"');
593
-
594
- const simpleTaskData = JSON.stringify({ id: 'TASK_TEST', complexity: 'simple' });
595
- const simpleLines = [];
596
- appendMemoryContext(simpleLines, simpleTaskData, {});
597
- const simpleText = simpleLines.join('\n');
598
- assert.ok(!simpleText.includes('## Known Failures'), 'complexity:simple prompt should NOT contain "## Known Failures"');
599
- assert.ok(!simpleText.includes('## Project Memory'), 'complexity:simple prompt should NOT contain "## Project Memory"');
600
- });
601
-
602
- // ── 3x3 matrix: simple/low, medium/medium, complex/high (task's minimum bar) ──
603
-
604
- test('two-axis matrix: simple/low has no Known Failures and has Working contract', () => {
605
- const taskData = JSON.stringify({ id: 'TASK_TEST', complexity: 'simple', rigor: 'low' });
606
- const memLines = [];
607
- appendMemoryContext(memLines, taskData, {});
608
- assert.ok(!memLines.join('\n').includes('## Known Failures'));
609
- const ckLines = [];
610
- appendCognitiveCheckpoint(ckLines, taskData, {});
611
- assert.ok(ckLines.join('\n').includes('## Working contract'));
612
- });
613
-
614
- test('two-axis matrix: medium/medium has Known Failures and the short progress note', () => {
615
- const taskData = JSON.stringify({ id: 'TASK_TEST', complexity: 'medium', rigor: 'medium' });
616
-
617
- const memLines = [];
618
- appendMemoryContext(memLines, taskData, {});
619
- assert.ok(memLines.join('\n').includes('## Known Failures'));
620
-
621
- const ckLines = [];
622
- appendCognitiveCheckpoint(ckLines, taskData, {});
623
- const ck = ckLines.join('\n');
624
- assert.ok(ck.includes('## Progress Note (brief)'), 'medium/medium should carry the short progress note');
625
- });
626
-
627
- test('two-axis matrix: complex/high has Related Files Preview enabled and the brief ritual', () => {
628
- const profile = promptProfile({ rigor: 'high', complexity: 'complex' });
629
- assert.equal(profile.skipRelatedFilesPreview, false);
630
- const ckLines = [];
631
- appendCognitiveCheckpoint(ckLines, JSON.stringify({ id: 'TASK_TEST', complexity: 'complex', rigor: 'high' }), {});
632
- const ck = ckLines.join('\n');
633
- assert.ok(ck.includes('## Progress Note (brief)'));
634
- assert.ok(!ck.includes('Cognitive Checkpoint'));
635
- });
636
-
637
- // ── TASK_2371: prompt diet ──
638
-
639
- test('TASK_2371: no Cortex / Episodic section can be produced (functions removed, builder output clean)', () => {
640
- const prompt = require('./prompt.js');
641
- assert.equal(prompt.appendCortexContext, undefined);
642
- assert.equal(prompt.appendEpisodicContext, undefined);
643
- const src = fs.readFileSync(path.join(__dirname, 'prompt.js'), 'utf-8');
644
- assert.ok(!src.includes('## Cross-Project Context'));
645
- assert.ok(!src.includes('## Episodic Memory'));
646
- });
647
-
648
- test('TASK_2371: Known Failures drop Heimdall REVERT / removed-system entries and cap at 3', () => {
649
- const lines = [];
650
- appendMemoryContext(lines, JSON.stringify({ id: 'TASK_TEST', complexity: 'complex' }), {});
651
- const text = lines.join('\n');
652
- assert.ok(!/Heimdall|\bREVERT\b|Kuzu|\bNorn\b|thalamus|\breflex\b|Formation squad/.test(text.split('## Project Memory')[0].split('## Known Failures')[1] || ''));
653
- const fail = (text.split('## Known Failures')[1] || '').split(/\n## /)[0];
654
- assert.ok((fail.match(/^- /gm) || []).length <= 3);
655
- });
656
-
657
- test('TASK_2371: loadFailures filter excludes Heimdall/REVERT entries', () => {
658
- const { loadFailures } = require('./memory-loader.js');
659
- const all = loadFailures({ tags: [] });
660
- for (const e of all) assert.ok(!/TASK_1413|TASK_1427|TASK_904/.test(e.id), e.id);
661
- });
662
-
663
- test('TASK_2371: Shared Modules inject only on explicit opt-in tags', () => {
664
- const noTag = resolveModules({ tags: ['auth', 'security', 'test', 'review'], scope_paths: ['src/auth/login.ts'] }, null, ROOT);
665
- assert.deepEqual(noTag, []);
666
- const optIn = resolveModules({ tags: ['security-review'] }, null, ROOT).map((p) => path.basename(p));
667
- assert.deepEqual(optIn, ['security.yaml']);
668
- });
669
-
670
209
  // ── TASK_2082: Rust check reminder for worktree subagents ──────────────
671
210
 
672
211
  test('hasRustScope: true for a .rs file, true for a dashboard/src-tauri/ path, false otherwise', () => {
@@ -727,57 +266,6 @@ test('appendRustCheckReminder: resolved core path is interpolated, no hard-coded
727
266
  assert.ok(!text.includes('C:\\'), 'template must not carry a hard-coded absolute path');
728
267
  });
729
268
 
730
- // Minimal do.yaml-shaped skillDef (real card structure per .formation/unit/do.yaml)
731
- // — enough to exercise appendWorkflow's commit-step branch without paying
732
- // for buildUnitPrompt's other live-project-dependent appends.
733
- const DO_LIKE_SKILL = {
734
- collaboration: {
735
- workflow: [
736
- {
737
- step: '提交變更',
738
- run: 'git add -A && (git commit --no-verify -m "[{task_id}] task completed" || true)',
739
- note: 'worktree 內顯式提交確保 post-complete 只需 merge 不需 recovery commit。',
740
- },
741
- ],
742
- },
743
- };
744
-
745
- test('appendWorkflow: a worktree-subagent card with Rust scope gets the Rust check section after the commit step', () => {
746
- const taskData = JSON.stringify({ id: 'TASK_TEST', scope_paths: ['dashboard/src-tauri/src/x.rs'] });
747
- const lines = [];
748
- appendWorkflow(lines, DO_LIKE_SKILL, { isSubagent: true, corePath: '/core', rustCoreCheck: () => true }, taskData);
749
- const text = lines.join('\n');
750
- assert.ok(text.includes('## Rust check (once, after commit)'));
751
- assert.ok(text.includes('node /core/dev/scripts/rust-verify-worktree.js run <your branch> <test filter>'));
752
- // Fixed by Ansuz review of eb7c3be5d: the section no longer tries to talk
753
- // over hard rule 4 with a "this overrides rule 4" note — that wording
754
- // fix now happens at the prefix+body composition point in dispatch.js
755
- // (applyRustCheckToPrefix), covered separately below.
756
- assert.ok(!text.includes('overrides'), 'section must not claim to override rule 4 by itself anymore');
757
- });
758
-
759
- test('appendWorkflow: a JS-only worktree-subagent card renders no Rust check section (byte-identical to before)', () => {
760
- const taskData = JSON.stringify({ id: 'TASK_TEST', scope_paths: ['dev/scripts/foo.js'] });
761
- const lines = [];
762
- appendWorkflow(lines, DO_LIKE_SKILL, { isSubagent: true, corePath: '/core' }, taskData);
763
- const text = lines.join('\n');
764
- assert.ok(!text.includes('## Rust check'), 'JS-only card must not gain the new section');
765
- // The commit-step block is unchanged: isolated staging note is still the last content.
766
- assert.ok(
767
- text.trimEnd().endsWith(
768
- 'Note: nothing to commit 時靜默跳過(不算失敗);git commit 失敗(如 index.lock 競爭)會自動退避重試兩次,仍失敗則以非零退出 — 此時大聲回報錯誤,不要吞掉。'
769
- )
770
- );
771
- });
772
-
773
- test('appendWorkflow: a shared-tree (non-subagent) Rust-scoped card renders no Rust check section', () => {
774
- const taskData = JSON.stringify({ id: 'TASK_TEST', scope_paths: ['dashboard/src-tauri/src/x.rs'] });
775
- const lines = [];
776
- appendWorkflow(lines, DO_LIKE_SKILL, { corePath: '/core' }, taskData);
777
- const text = lines.join('\n');
778
- assert.ok(!text.includes('## Rust check'), 'shared-tree mode has no isolated worktree to verify against');
779
- });
780
-
781
269
  // ── TASK_2082 defect fix (Ansuz review of eb7c3be5d): fix the contradictory
782
270
  // "Rust 交 Ansuz" hard rule 4 wording at the prefix+body COMPOSITION point
783
271
  // (dispatch.js) instead of relying on a later "overrides rule 4" note in
@@ -811,65 +299,38 @@ test('applyRustCheckToPrefix: also patches the cross-project worktree prefix var
811
299
  assert.ok(patched.includes('Rust 只准跑一次'));
812
300
  });
813
301
 
814
- // ── Role Expertise removal (TASK_2086) ──────────────────────────────────
815
- // appendRoleSkill/loadRoleSkill/ROLE_SKILL_MAP were removed: the extraction
816
- // regex was broken since it shipped (0a09fb2a5) and only ever injected a
817
- // one-line, wrong-stack (Python/FastAPI) role description + dangling
818
- // sub-heading. buildUnitPrompt (formation: unit) and buildLegacyPrompt
819
- // (formation: legacy) were the only two call sites.
820
-
821
- const MINIMAL_SKILL_DEF = { role: { identity: 'You are a developer.', priority: 'ship it' }, behavior: [] };
822
-
823
- function minimalTaskData(role) {
824
- return JSON.stringify({ id: 'TASK_9999', title: 'Test task', role, scope_paths: [] });
302
+ // ── TASK_2391: subagent dispatch prompt is a short template ─────────────
303
+ function loadDoSkill() {
304
+ return require('js-yaml').load(fs.readFileSync(path.join(__dirname, '..', 'unit', 'do.yaml'), 'utf8'));
825
305
  }
826
306
 
827
- test('buildPrompt (unit formation): role=backend produces no "## Role Expertise" section', () => {
828
- const out = buildPrompt({ ...MINIMAL_SKILL_DEF, formation: 'unit' }, 'do it', minimalTaskData('backend'), {});
829
- assert.ok(!out.includes('## Role Expertise'), 'Role Expertise section should be removed');
830
- assert.ok(!out.includes('Python/FastAPI') && !out.includes('FastAPI'), 'wrong-stack Python/FastAPI text should not be injected');
831
- });
832
-
833
- test('buildPrompt (unit formation): role=frontend produces no "## Role Expertise" section', () => {
834
- const out = buildPrompt({ ...MINIMAL_SKILL_DEF, formation: 'unit' }, 'do it', minimalTaskData('frontend'), {});
835
- assert.ok(!out.includes('## Role Expertise'), 'Role Expertise section should be removed');
307
+ test('TASK_2391: subagent prompt keeps card + commit rule + report, drops boilerplate sections', () => {
308
+ const card = {
309
+ id: 'TASK_9001', title: 'T', description: 'Desc', scope_paths: ['src/a.js'],
310
+ requirements: ['REQ-ONE'], acceptance_criteria: ['AC-ONE'], technical_notes: ['NOTE-ONE'],
311
+ complexity: 'complex', rigor: 'high',
312
+ };
313
+ const body = buildPrompt(loadDoSkill(), 'TASK_9001', JSON.stringify(card), { isSubagent: true, taskId: 'TASK_9001' });
314
+ for (const must of ['REQ-ONE', 'AC-ONE', 'NOTE-ONE', 'src/a.js', '--no-verify -m "[TASK_9001] task completed"', '<= 20 lines', 'commit sha', 'did not verify']) {
315
+ assert.ok(body.includes(must), `missing: ${must}`);
316
+ }
317
+ for (const gone of ['Code Minimalism', 'File Budget', 'Known Failures', 'Search Tool Selection', '## Behavior', '## Expected Output', 'Progress Note', 'Working contract', '## Rust check']) {
318
+ assert.ok(!body.includes(gone), `should be gone: ${gone}`);
319
+ }
320
+ assert.ok(body.length < 2500, `body too long: ${body.length}`);
836
321
  });
837
322
 
838
- test('buildPrompt (legacy formation): role=backend produces no "## Role Expertise" section', () => {
839
- const out = buildPrompt({ ...MINIMAL_SKILL_DEF, formation: 'legacy' }, 'do it', minimalTaskData('backend'), {});
840
- assert.ok(!out.includes('## Role Expertise'), 'Role Expertise section should be removed');
323
+ test('TASK_2391: subagent prompt gets the Rust check only for a Rust-scoped card', () => {
324
+ const rustCard = JSON.stringify({ id: 'TASK_9002', title: 'R', scope_paths: ['dashboard/src-tauri/src/x.rs'] });
325
+ const withRust = buildPrompt(loadDoSkill(), 'TASK_9002', rustCard, { isSubagent: true, taskId: 'TASK_9002', rustCoreCheck: () => true });
326
+ assert.ok(withRust.includes('## Rust check (once, after commit)'));
841
327
  });
842
328
 
843
- // TASK_2252 — appendWorkflow rigor gate must accept taskData as JSON string OR object
844
- // (bare JSON.parse(object) threw, was swallowed, and the rigor:high gate failed open).
845
- const RIGOR_SKILL = { collaboration: { workflow: [{ step: 'Finish', run: 'node .collaboration/core/task-api.js complete {task_id}' }] } };
846
- test('TASK_2252 appendWorkflow rigor gate: string/object/invalid taskData', () => {
847
- const cases = [
848
- ['string rigor:high', JSON.stringify({ id: 'T', rigor: 'high' }), true],
849
- ['object rigor:high', { id: 'T', rigor: 'high' }, true],
850
- ['string rigor:medium (control)', JSON.stringify({ id: 'T', rigor: 'medium' }), false],
851
- ['object rigor:medium (control)', { id: 'T', rigor: 'medium' }, false],
852
- ['raw YAML (unparseable)', 'id: T\nrigor: high\n', false],
853
- ['null', null, false],
854
- ];
855
- for (const [name, td, gated] of cases) {
856
- const lines = [];
857
- appendWorkflow(lines, RIGOR_SKILL, { taskId: 'TASK_X' }, td);
858
- const out = lines.join('\n');
859
- assert.equal(out.includes('rigor: high — 禁止執行 task-api.js complete'), gated, name);
860
- if (!gated) assert.ok(out.includes('Command: node .collaboration/core/task-api.js complete TASK_X'), `${name}: complete step kept`);
329
+ test('buildPrompt rejects squad/legion skills with a clear error instead of returning an empty prompt', () => {
330
+ for (const formation of ['squad', 'legion']) {
331
+ assert.throws(
332
+ () => buildPrompt({ formation, name: 'x' }, 'TASK_9003', null, { isSubagent: true }),
333
+ new RegExp(`Formation '${formation}'.*retired`)
334
+ );
861
335
  }
862
336
  });
863
- test('TASK_2252 appendWorkflow dispatch_overrides honoured for object taskData too', () => {
864
- const lines = [];
865
- appendWorkflow(lines, { collaboration: { workflow: [] } }, {}, { id: 'T', dispatch_overrides: 'DO NOT COMMIT' });
866
- assert.ok(lines.join('\n').includes('DO NOT COMMIT'));
867
- });
868
-
869
- test('memory frozen (2026-10-10): a caller without __forceAlaya renders no Alaya section and makes no MCP call', () => {
870
- const lines = [];
871
- let called = false;
872
- appendAlayaLessons(lines, ALAYA_TASK, () => { called = true; return { bijas: [{ id: 'x', content: 'y', cosine: 0.99 }] }; });
873
- assert.equal(called, false);
874
- assert.equal(lines.join('\n').includes('Process lessons (Alaya)'), false);
875
- });