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,42 +1,14 @@
1
1
  /**
2
- * Formation Prompt Builder
3
- * 根據陣型和技能定義組裝 prompt
4
- *
5
- * 每個陣型有不同的 prompt 結構:
6
- * - unit: 角色 + 行為 + 任務 + 工作流程 + 輸出
7
- * - solo: 角色 + 視角列表 + 行為 + 任務 + 工作流程 + 輸出
8
- * - squad: 為每個成員分別組裝 prompt(含分工 scope)
9
- * - legion: 為 team lead + 每個成員分別組裝 prompt(含溝通規則)
2
+ * Subagent Prompt Builder
3
+ * Assembles the prompt for the one live dispatch path:
4
+ * node .formation/tools/dispatch.js --skill=do --task=X --dispatch-mode=subagent
5
+ * Task card + commit command + verify/report, behind a transport prefix
6
+ * (SUBAGENT_PREFIX / SUBAGENT_PREFIX_DIRECT / buildSubagentWorktreePrefix).
10
7
  */
11
8
 
12
- // ─────────────────────────────────────────
13
- // Memory Loader(記憶系統整合)
14
- // ─────────────────────────────────────────
15
- let memoryLoader = null;
16
- try {
17
- memoryLoader = require('./memory-loader');
18
- } catch { /* memory-loader not available — graceful degradation */ }
19
-
20
- // ─────────────────────────────────────────
21
- // Shared Principles Loader(共享原則注入)
22
- // ─────────────────────────────────────────
23
9
  const fs = require('fs');
24
10
  const path = require('path');
25
11
  const { buildWorktreeGitAddCommand } = require('./utils');
26
- // TASK_2104: Alaya relevance gate (cosine floor + stack match) extracted to
27
- // a dependency-free module so task-api.js's echoAlayaLessons can share it
28
- // without pulling in Formation-only deps. Re-exported below unchanged so
29
- // prompt.test.js's imports keep working.
30
- const {
31
- buildTaskContextQuery,
32
- deriveTaskStack,
33
- deriveBijaStack,
34
- passesAlayaRelevanceGate,
35
- dedupBijas,
36
- ALAYA_MIN_COSINE_DEFAULT,
37
- ALAYA_STACK_OVERRIDE_COSINE,
38
- ALAYA_CANDIDATE_POOL,
39
- } = require('./alaya-gate');
40
12
 
41
13
  // Shared core-path resolver (validates existsSync+isDirectory, falls back to KENAZ_SOURCE_DIR)
42
14
  let _resolveCoreDir = null;
@@ -44,53 +16,6 @@ try {
44
16
  _resolveCoreDir = require(path.join(__dirname, '..', '..', '.claude', 'hooks', 'core-resolve')).resolveCoreDir;
45
17
  } catch { /* graceful degradation — older installs without the export */ }
46
18
 
47
- let _sharedPrinciplesCache = null;
48
-
49
- // ─────────────────────────────────────────
50
- // Shared Module Tag Rules(模組自動注入規則)
51
- // ─────────────────────────────────────────
52
-
53
- // TASK_2371: opt-in only — a task must carry the explicit review tag. No keyword
54
- // match on generic tags (auth/test/...) or chart warnings; a 100-line OWASP
55
- // block on an unrelated auth card was pure noise.
56
- const MODULE_TAG_RULES = [
57
- { tags: ['security-review'], module: 'security.yaml' },
58
- { tags: ['testing-review'], module: 'testing.yaml' },
59
- { tags: ['code-review'], module: 'code_review.yaml' },
60
- ];
61
-
62
- /**
63
- * Resolve which shared modules to inject based on task tags and optional chart context.
64
- * @param {string|object} taskData - task JSON string or parsed object
65
- * @param {string|null} [chartPath] - optional path to chart.json for richer context
66
- * @param {string|null} [corePath] - KenazAI root path (defaults to two levels up from __dirname)
67
- * @returns {string[]} list of absolute module file paths to inject
68
- */
69
- function resolveModules(taskData, chartPath, corePath) {
70
- const modules = new Set();
71
- let taskTags = [];
72
-
73
- try {
74
- const parsed = typeof taskData === 'string' ? JSON.parse(taskData) : taskData;
75
- if (parsed) {
76
- taskTags = (parsed.tags || []).map(t => String(t).toLowerCase());
77
- }
78
- } catch { /* not JSON */ }
79
-
80
- // Tag-based matching
81
- for (const rule of MODULE_TAG_RULES) {
82
- if (rule.tags.some(t => taskTags.includes(t))) {
83
- modules.add(rule.module);
84
- }
85
- }
86
-
87
- if (modules.size === 0) return [];
88
-
89
- const root = corePath || path.resolve(__dirname, '..', '..');
90
- const modulesDir = path.join(root, '.formation', 'roles', '_shared', 'modules');
91
- return [...modules].map(m => path.join(modulesDir, m));
92
- }
93
-
94
19
  /**
95
20
  * Resolve the KenazAI core path from a sub-project's project.yaml.
96
21
  * Returns null when projectPath is absent or IS the core project.
@@ -122,35 +47,6 @@ function resolveCorePath(projectPath) {
122
47
  } catch { return null; }
123
48
  }
124
49
 
125
- function loadSharedPrinciples(corePath) {
126
- if (_sharedPrinciplesCache) return _sharedPrinciplesCache;
127
-
128
- const principlesPath = path.join(
129
- corePath || path.resolve(__dirname, '..', '..'),
130
- '.specialist', 'shared', 'principles.yaml'
131
- );
132
-
133
- try {
134
- if (!fs.existsSync(principlesPath)) return null;
135
- const content = fs.readFileSync(principlesPath, 'utf-8');
136
-
137
- // 提取 differential_over_intuition 區塊
138
- const ddxMatch = content.match(
139
- /differential_over_intuition:[\s\S]*?(?=\n# ═|$)/
140
- );
141
- if (!ddxMatch) return null;
142
-
143
- _sharedPrinciplesCache = ddxMatch[0].trim();
144
- return _sharedPrinciplesCache;
145
- } catch {
146
- return null;
147
- }
148
- }
149
-
150
- // ─────────────────────────────────────────
151
- // TASK_1998 R2 -> two-axis prompt policy (2026-09-22 user decision)
152
- // ─────────────────────────────────────────
153
-
154
50
  /**
155
51
  * Extract the card's `rigor` field from taskData (JSON string or object).
156
52
  * Returns null when taskData is absent, unparseable, or has no rigor field
@@ -183,78 +79,17 @@ function extractComplexity(taskData) {
183
79
  }
184
80
 
185
81
  /**
186
- * TASK_1998 two-axis prompt policy (2026-09-22 user decision, replaces the
187
- * single rigor-gated R2 diet switch):
188
- *
189
- * CONTEXT volume — driven by `complexity` (how much the task touches):
190
- * simple -> no Related Files Preview, no Known Failures, no Project Memory.
191
- * medium -> Known Failures on; no Preview; no Project Memory.
192
- * complex -> Related Files Preview on; Known Failures + Project Memory on.
193
- * (TASK_2371: Cross-Project Context (Cortex) and Episodic Memory are gone
194
- * for every complexity — stale / unverified outcomes.)
195
- *
196
- * PROCESS strictness — driven by `rigor`:
197
- * low -> the 8-line "## Working contract" only; rune inscription optional.
198
- * medium/high -> Working contract + "## Progress Note (brief)" + the
199
- * rune-inscribe line (TASK_2371: high no longer has a
200
- * mandatory recall/inscribe/Cognitive Checkpoint ritual).
201
- *
202
- * This is the ONLY place either axis branches into its behaviour; every call
203
- * site below reads flags off the returned profile instead of re-testing
204
- * rigor/complexity itself.
205
- *
206
- * Backwards compatible: a bare string (or null/undefined) is treated as the
207
- * legacy single-axis call — `rigor` — and `complexity` defaults to 'medium'.
208
- * @param {string|{rigor?: string|null, complexity?: string|null}|null|undefined} rigorOrOptions
82
+ * Context volume for the dispatch prompt, driven by the card's `complexity`:
83
+ * only a complex card gets the Related Files Preview (dispatch.js reads
84
+ * skipRelatedFilesPreview). Accepts {complexity} or a bare complexity string.
85
+ * @param {string|{complexity?: string|null}|null|undefined} complexityOrOptions
209
86
  */
210
- function promptProfile(rigorOrOptions) {
211
- let rawRigor;
212
- let rawComplexity;
213
- if (rigorOrOptions && typeof rigorOrOptions === 'object') {
214
- rawRigor = rigorOrOptions.rigor;
215
- rawComplexity = rigorOrOptions.complexity;
216
- } else {
217
- // Legacy string-rigor call — complexity defaults to 'medium'.
218
- rawRigor = rigorOrOptions;
219
- rawComplexity = 'medium';
220
- }
221
-
222
- const rigor = (rawRigor === 'low' || rawRigor === 'high') ? rawRigor : 'medium';
223
- const complexity = (rawComplexity === 'simple' || rawComplexity === 'complex') ? rawComplexity : 'medium';
224
-
225
- const CONTEXT_BY_COMPLEXITY = {
226
- simple: {
227
- skipRelatedFilesPreview: true,
228
- skipKnownFailures: true,
229
- skipProjectMemory: true,
230
- },
231
- medium: {
232
- skipRelatedFilesPreview: true,
233
- skipKnownFailures: false,
234
- skipProjectMemory: true,
235
- },
236
- complex: {
237
- skipRelatedFilesPreview: false,
238
- skipKnownFailures: false,
239
- skipProjectMemory: false,
240
- },
241
- };
242
-
243
- // ritualTier drives appendCognitiveCheckpoint:
244
- // 'low' -> Working contract only; 'medium' -> Working contract + short
245
- // progress note + mandatory inscribe line; 'high' -> full ritual (compactRitual=false).
246
- const PROCESS_BY_RIGOR = {
247
- low: { compactRitual: true, ritualTier: 'low' },
248
- medium: { compactRitual: true, ritualTier: 'medium' },
249
- high: { compactRitual: true, ritualTier: 'medium' }, // TASK_2371: high == medium ritual
250
- };
251
-
252
- return {
253
- rigor,
254
- complexity,
255
- ...CONTEXT_BY_COMPLEXITY[complexity],
256
- ...PROCESS_BY_RIGOR[rigor],
257
- };
87
+ function promptProfile(complexityOrOptions) {
88
+ const raw = complexityOrOptions && typeof complexityOrOptions === 'object'
89
+ ? complexityOrOptions.complexity
90
+ : complexityOrOptions;
91
+ const complexity = (raw === 'simple' || raw === 'complex') ? raw : 'medium';
92
+ return { complexity, skipRelatedFilesPreview: complexity !== 'complex' };
258
93
  }
259
94
 
260
95
  /**
@@ -287,17 +122,6 @@ function splitPromptSections(promptText) {
287
122
  return sections;
288
123
  }
289
124
 
290
- // ─────────────────────────────────────────
291
- // 主入口
292
- // ─────────────────────────────────────────
293
-
294
- /**
295
- * 根據陣型分發到對應的 prompt builder
296
- * @param {object} skillDef - parsed YAML
297
- * @param {string} taskInput - 任務描述或 ID
298
- * @param {string|null} taskData - 從 .collaboration 載入的任務 YAML
299
- * @returns {string|string[]} 單一 prompt(unit/solo)或 prompt 陣列(squad)
300
- */
301
125
  // TASK_2374: append-only usage counter (measure-first; cut decisions after 14 days of data).
302
126
  // Env KENAZ_USAGE_COUNTERS_FILE overrides the target (tests). Never throws.
303
127
  function recordSkillUsage(skillDef, options = {}) {
@@ -314,545 +138,54 @@ function recordSkillUsage(skillDef, options = {}) {
314
138
 
315
139
  function buildPrompt(skillDef, taskInput, taskData, options = {}) {
316
140
  const formation = skillDef.formation || 'unit';
317
- recordSkillUsage(skillDef, options);
318
-
319
- const builders = {
320
- unit: buildUnitPrompt,
321
- solo: buildSoloPrompt,
322
- squad: buildSquadPrompts,
323
- legion: buildLegionPrompts,
324
- legacy: buildLegacyPrompt,
325
- };
326
-
327
- const builder = builders[formation];
328
- if (!builder) {
329
- throw new Error(`Unknown formation: ${formation}. Expected: ${Object.keys(builders).join(', ')}`);
330
- }
331
-
332
- return builder(skillDef, taskInput, taskData, options);
333
- }
334
-
335
- // ─────────────────────────────────────────
336
- // Unit:單一角色,直接執行
337
- // ─────────────────────────────────────────
338
-
339
- function buildUnitPrompt(skillDef, taskInput, taskData, options = {}) {
340
- const lines = [];
341
-
342
- lines.push(`## Role`);
343
- lines.push(skillDef.role.identity);
344
- lines.push(`Priority: ${skillDef.role.priority}`);
345
- lines.push('');
346
-
347
- appendProjectContext(lines, options);
348
- appendFormationPreamble(lines, taskData, options);
349
- appendYggdrasilContext(lines, options.yggdrasilContext);
350
- appendMemoryContext(lines, taskData, options);
351
- appendAlayaLessons(lines, taskData);
352
- appendSharedModules(lines, taskData, options);
353
- appendBehavior(lines, skillDef);
354
- appendTask(lines, taskInput, taskData, options);
355
- appendCognitiveRecall(lines, taskData, options);
356
- appendWorkflow(lines, skillDef, options, taskData);
357
- appendOutput(lines, skillDef);
358
- appendCognitiveCheckpoint(lines, taskData, options);
359
-
360
- return lines.join('\n');
361
- }
362
-
363
- // ─────────────────────────────────────────
364
- // Solo:一個 agent 裝備多視角,自主切換
365
- // ─────────────────────────────────────────
366
-
367
- function buildSoloPrompt(skillDef, taskInput, taskData, options = {}) {
368
- const lines = [];
369
-
370
- lines.push(`## Role`);
371
- lines.push(skillDef.role.identity);
372
- lines.push(`Priority: ${skillDef.role.priority}`);
373
- lines.push('');
374
-
375
- appendProjectContext(lines, options);
376
- appendFormationPreamble(lines, taskData, options);
377
- appendYggdrasilContext(lines, options.yggdrasilContext);
378
- appendMemoryContext(lines, taskData, options);
379
- appendAlayaLessons(lines, taskData);
380
-
381
- // 分析原則(Solo strategy 專用)
382
- if (skillDef.analysis_principles && skillDef.analysis_principles.length > 0) {
383
- lines.push(`## Analysis Principles`);
384
- lines.push('');
385
- for (const p of skillDef.analysis_principles) {
386
- lines.push(`### ${p.name}`);
387
- lines.push(p.description.trim());
388
- lines.push('');
389
- }
390
- }
391
-
392
- // 可用視角
393
- if (skillDef.perspectives && skillDef.perspectives.length > 0) {
394
- lines.push(`## Available Perspectives`);
395
- lines.push('');
396
- lines.push('你隨時可以切換到以下任一視角。根據分析過程中的需要自主決定。');
397
- lines.push('');
398
-
399
- for (const p of skillDef.perspectives) {
400
- lines.push(`### ${p.name}`);
401
- lines.push(`- 關注:${p.lens}`);
402
- lines.push(`- 何時切換:${p.trigger}`);
403
- lines.push('');
404
- }
405
- }
406
-
407
- appendBehavior(lines, skillDef);
408
- appendSharedPrinciples(lines, options);
409
- appendTask(lines, taskInput, taskData, options);
410
- appendCognitiveRecall(lines, taskData, options);
411
- appendWorkflow(lines, skillDef, options, taskData);
412
- appendOutput(lines, skillDef);
413
- appendCognitiveCheckpoint(lines, taskData, options);
414
-
415
- return lines.join('\n');
416
- }
417
-
418
- // ─────────────────────────────────────────
419
- // Squad:為每個成員分別組裝 prompt
420
- // ─────────────────────────────────────────
421
-
422
- /**
423
- * Squad 回傳 prompt 陣列,每個成員一份
424
- * @returns {{ memberId: string, prompt: string, skill: string }[]}
425
- */
426
- function buildSquadPrompts(skillDef, taskInput, taskData, options = {}) {
427
- const members = skillDef.members || [];
428
- if (members.length === 0) {
429
- throw new Error('Squad YAML must define `members` array');
430
- }
431
-
432
- // 組裝共享上下文(注入到每個成員 prompt)
433
- const sharedBlock = buildSharedContextBlock(skillDef);
434
-
435
- return members.map(member => {
436
- const lines = [];
437
-
438
- // 成員角色
439
- lines.push(`## Role`);
440
- lines.push(typeof member.role === 'string' ? member.role.trim() : `你是 Squad 的一員:${member.name}`);
441
- lines.push('');
442
-
443
- // 分析模式(mode_behavior 注入)
444
- const analysisMode = options.analysisMode;
445
- if (analysisMode && member.mode_behavior && member.mode_behavior[analysisMode]) {
446
- const modeConfig = skillDef.analysis_mode?.modes?.[analysisMode];
447
- lines.push(`## Analysis Mode: ${analysisMode}`);
448
- if (modeConfig?.description) lines.push(modeConfig.description.trim());
449
- lines.push('');
450
- lines.push('**你在此模式下的立場調整**:');
451
- lines.push(member.mode_behavior[analysisMode].trim());
452
- lines.push('');
453
- }
454
-
455
- // 核心原則(principles 注入 — 所有成員共用的硬規則)
456
- if (skillDef.principles) {
457
- lines.push(`## Principles (絕對遵守)`);
458
- if (skillDef.principles.focus) {
459
- lines.push(skillDef.principles.focus.trim());
460
- lines.push('');
461
- }
462
- if (skillDef.principles.forbidden && skillDef.principles.forbidden.length > 0) {
463
- lines.push('**禁止 (違反即失敗)**:');
464
- for (const f of skillDef.principles.forbidden) lines.push(`- ❌ ${f}`);
465
- lines.push('');
466
- }
467
- if (skillDef.principles.required && skillDef.principles.required.length > 0) {
468
- lines.push('**必須遵守**:');
469
- for (const r of skillDef.principles.required) lines.push(`- ✅ ${r}`);
470
- lines.push('');
471
- }
472
- }
473
-
474
- // 成員身份
475
- lines.push(`## Identity`);
476
- lines.push(`你是 Squad 的成員 **${member.name}** (id: ${member.id || member.name})。`);
477
- lines.push(`你與其他成員並行工作,各自獨立,不能互相通訊。`);
478
- lines.push(`完成你負責的部分後回報結果,Lead 會負責整合。`);
479
- lines.push('');
480
-
481
- // 成員的 scope(負責範圍)— 支援 include/exclude 格式
482
- if (member.scope) {
483
- lines.push(`## Scope (你只能碰這些檔案)`);
484
- if (member.scope.include || member.scope.exclude) {
485
- if (member.scope.include) {
486
- lines.push(`Include:`);
487
- const includes = Array.isArray(member.scope.include) ? member.scope.include : [member.scope.include];
488
- for (const p of includes) lines.push(` - ${p}`);
489
- }
490
- if (member.scope.exclude) {
491
- lines.push(`Exclude:`);
492
- const excludes = Array.isArray(member.scope.exclude) ? member.scope.exclude : [member.scope.exclude];
493
- for (const p of excludes) lines.push(` - ${p}`);
494
- }
495
- } else if (Array.isArray(member.scope)) {
496
- for (const s of member.scope) lines.push(`- ${s}`);
497
- } else {
498
- lines.push(member.scope);
499
- }
500
- lines.push('');
501
- }
502
-
503
- // Yggdrasil network context
504
- appendYggdrasilContext(lines, options.yggdrasilContext);
505
- appendMemoryContext(lines, taskData, options);
506
- appendAlayaLessons(lines, taskData);
507
- appendFormationPreamble(lines, taskData, options);
508
-
509
- // 共享上下文
510
- if (sharedBlock) {
511
- lines.push(sharedBlock);
512
- lines.push('');
513
- }
514
-
515
- // 行為準則(squad 全局 + 成員級別)
516
- const behaviors = [
517
- ...(skillDef.behavior || []),
518
- ...(member.behavior || [])
519
- ];
520
- if (behaviors.length > 0) {
521
- lines.push(`## Behavior`);
522
- for (const b of behaviors) lines.push(`- ${b}`);
523
- lines.push('');
524
- }
525
-
526
- // 任務
527
- appendTask(lines, taskInput, taskData, options);
528
- appendCognitiveRecall(lines, taskData, options);
529
-
530
- // 成員專屬工作流程或全局工作流程
531
- if (member.workflow) {
532
- lines.push(`## Workflow`);
533
- for (const step of member.workflow) lines.push(`- ${step}`);
534
- lines.push('');
535
- appendDispatchOverrides(lines, taskData); // TASK_1820: same post-envelope channel as appendWorkflow
536
- } else {
537
- appendWorkflow(lines, skillDef, options, taskData);
538
- }
539
-
540
- // 輸出
541
- if (member.output) {
542
- lines.push(`## Expected Output`);
543
- lines.push(typeof member.output === 'string' ? member.output.trim() : JSON.stringify(member.output));
544
- } else {
545
- appendOutput(lines, skillDef);
546
- }
547
-
548
- appendCognitiveCheckpoint(lines, taskData, options);
549
-
550
- return {
551
- memberId: member.id || member.name,
552
- prompt: lines.join('\n'),
553
- skill: member.skill || 'do'
554
- };
555
- });
556
- }
557
-
558
- /**
559
- * 組裝 shared_context 區塊(Squad 成員共用的約定)
560
- */
561
- function buildSharedContextBlock(skillDef) {
562
- if (!skillDef.shared_context) return null;
563
-
564
- const lines = [];
565
- lines.push(`## Shared Context (所有成員共享的約定)`);
566
-
567
- if (skillDef.shared_context.description) {
568
- lines.push(skillDef.shared_context.description.trim());
569
- }
570
-
571
- if (skillDef.shared_context.includes) {
572
- lines.push('');
573
- const includes = skillDef.shared_context.includes;
574
- if (typeof includes === 'object' && !Array.isArray(includes)) {
575
- for (const [key, val] of Object.entries(includes)) {
576
- lines.push(`### ${key}`);
577
- lines.push(typeof val === 'string' ? val.trim() : JSON.stringify(val));
578
- lines.push('');
579
- }
580
- } else if (Array.isArray(includes)) {
581
- for (const item of includes) {
582
- if (typeof item === 'string') {
583
- lines.push(`- ${item}`);
584
- } else if (typeof item === 'object') {
585
- for (const [key, val] of Object.entries(item)) {
586
- lines.push(`### ${key}`);
587
- lines.push(typeof val === 'string' ? val.trim() : JSON.stringify(val));
588
- lines.push('');
589
- }
590
- }
591
- }
592
- }
141
+ if (formation === 'squad' || formation === 'legion') {
142
+ throw new Error(`Formation '${formation}' (skill '${skillDef.name || 'unknown'}') is retired: only unit/solo skills can be dispatched (--dispatch-mode=subagent).`);
593
143
  }
594
-
595
- return lines.join('\n');
596
- }
597
-
598
- // ─────────────────────────────────────────
599
- // Legion:為 team lead 和成員組裝 prompt
600
- // ─────────────────────────────────────────
601
-
602
- /**
603
- * Legion 回傳 team config + 每個成員的 prompt
604
- * @returns {{ teamName: string, teamDesc: string, lead: { prompt: string }, members: { name: string, prompt: string }[] }}
605
- */
606
- function buildLegionPrompts(skillDef, taskInput, taskData, options = {}) {
607
- const team = skillDef.team || {};
608
- // 支援 members: [...] 和 members: { roles: [...] } 兩種格式
609
- const rawMembers = skillDef.members || [];
610
- const members = Array.isArray(rawMembers) ? rawMembers : (rawMembers.roles || []);
611
-
612
- // 團隊名稱:優先用 team.name,fallback 到 skillDef.name
613
- const teamName = team.name || skillDef.name || 'Legion';
614
-
615
- // Team lead prompt
616
- const leadLines = [];
617
- leadLines.push(`## Role`);
618
- leadLines.push(`你是 ${teamName} 的 Team Lead。`);
619
- leadLines.push(skillDef.role?.identity || '');
620
- leadLines.push('');
621
- leadLines.push(`## Team Members`);
622
- for (const m of members) {
623
- leadLines.push(`- **${m.name}**: ${m.role || m.description || ''}`);
624
- }
625
- leadLines.push('');
626
-
627
- if (skillDef.communication) {
628
- leadLines.push(`## Communication Rules`);
629
- const comm = skillDef.communication;
630
- if (Array.isArray(comm)) {
631
- for (const rule of comm) leadLines.push(`- ${rule}`);
632
- } else if (typeof comm === 'object') {
633
- if (comm.principles) {
634
- leadLines.push(`### Principles`);
635
- for (const p of comm.principles) leadLines.push(`- ${p}`);
636
- leadLines.push('');
637
- }
638
- if (comm.patterns) {
639
- leadLines.push(`### Patterns`);
640
- for (const p of comm.patterns) {
641
- leadLines.push(`- **${p.name}**: ${p.who} — ${p.content || ''}`);
642
- }
643
- leadLines.push('');
644
- }
645
- if (comm.broadcast_policy) {
646
- leadLines.push(`### Broadcast Policy`);
647
- leadLines.push(comm.broadcast_policy.trim());
648
- leadLines.push('');
649
- }
650
- }
651
- leadLines.push('');
652
- }
653
-
654
- appendYggdrasilContext(leadLines, options.yggdrasilContext);
655
- appendMemoryContext(leadLines, taskData, options);
656
- appendAlayaLessons(leadLines, taskData);
657
- appendFormationPreamble(leadLines, taskData, options);
658
- appendTask(leadLines, taskInput, taskData, options);
659
- appendCognitiveRecall(leadLines, taskData);
660
-
661
- if (skillDef.lifecycle) {
662
- leadLines.push(`## Lifecycle`);
663
- // lifecycle 可能是 { phases: [...] } 或直接是陣列
664
- const phases = Array.isArray(skillDef.lifecycle)
665
- ? skillDef.lifecycle
666
- : (skillDef.lifecycle.phases || []);
667
- for (const step of phases) {
668
- if (typeof step === 'string') {
669
- leadLines.push(`- ${step}`);
670
- } else {
671
- leadLines.push(`### ${step.phase || step.step}`);
672
- if (step.description) leadLines.push(step.description.trim());
673
- if (step.lead_actions) {
674
- leadLines.push('Lead actions:');
675
- for (const a of step.lead_actions) leadLines.push(`- ${a}`);
676
- }
677
- if (step.exit_criteria) {
678
- leadLines.push('Exit criteria:');
679
- for (const c of step.exit_criteria) leadLines.push(`- ${c}`);
680
- }
681
- leadLines.push('');
682
- }
683
- }
684
- leadLines.push('');
685
- }
686
-
687
- appendOutput(leadLines, skillDef);
688
- appendCognitiveCheckpoint(leadLines, taskData, options);
689
-
690
- // Member prompts
691
- // 優先使用 YAML 的 member_prompts 模板(如 forge 的 designer/implementer/reviewer)
692
- const promptTemplates = skillDef.member_prompts || {};
693
-
694
- const memberPrompts = members.map(member => {
695
- const lines = [];
696
-
697
- // 如果有預寫好的 prompt 模板,直接用
698
- const template = promptTemplates[member.id];
699
- if (template) {
700
- lines.push(template.trim());
701
- lines.push('');
702
- } else {
703
- // 自動生成
704
- lines.push(`## Role`);
705
- lines.push(member.role || member.description?.trim() || `你是 ${teamName} 團隊的 ${member.name}。`);
706
- lines.push('');
707
-
708
- if (member.responsibilities) {
709
- lines.push(`## Responsibilities`);
710
- for (const r of member.responsibilities) lines.push(`- ${r}`);
711
- lines.push('');
712
- }
713
-
714
- if (member.skills) {
715
- lines.push(`## Skills`);
716
- for (const s of member.skills) lines.push(`- ${s}`);
717
- lines.push('');
718
- }
719
-
720
- if (member.scope) {
721
- lines.push(`## Scope`);
722
- if (Array.isArray(member.scope)) {
723
- for (const s of member.scope) lines.push(`- ${s}`);
724
- } else {
725
- lines.push(member.scope);
726
- }
727
- lines.push('');
728
- }
729
- }
730
-
731
- lines.push(`## Team Context`);
732
- lines.push(`你是 ${teamName} 團隊的成員。可以透過 SendMessage 和其他成員溝通。`);
733
- lines.push(`Team Lead 會分配任務給你,完成後回報。如果被阻塞,主動溝通。`);
734
- lines.push('');
735
-
736
- appendYggdrasilContext(lines, options.yggdrasilContext);
737
- appendFormationPreamble(lines, taskData, options);
738
- appendTask(lines, taskInput, taskData, options);
739
- appendCognitiveRecall(lines, taskData, options);
740
- appendCognitiveCheckpoint(lines, taskData, options);
741
-
742
- return {
743
- name: member.name,
744
- agentType: member.agent_type || 'general-purpose',
745
- prompt: lines.join('\n')
746
- };
747
- });
748
-
749
- return {
750
- teamName: teamName.toLowerCase().replace(/\s+/g, '-'),
751
- teamDesc: team.description || skillDef.description || '',
752
- lead: { prompt: leadLines.join('\n') },
753
- members: memberPrompts
754
- };
144
+ recordSkillUsage(skillDef, options);
145
+ return buildSubagentPrompt(skillDef, taskInput, taskData, options);
755
146
  }
756
147
 
757
- // ─────────────────────────────────────────
758
- // Legacy:排程器模式(不建 prompt,直接走 scheduler loop)
759
- // ─────────────────────────────────────────
760
-
761
148
  /**
762
- * Legacy 不需要組裝 subagent prompt。
763
- * 回傳 scheduler 配置摘要(供 dry-run 顯示用)。
149
+ * Short prompt for Agent-tool subagent dispatch (both harnesses): task card,
150
+ * commit command, verification expectation, report format. The worktree hard
151
+ * rules are prepended by dispatch.js (SUBAGENT_PREFIX et al.). No Code
152
+ * Minimalism / File Budget / Known Failures / Search Tool Selection / Behavior /
153
+ * Expected Output / Progress Note; Rust check only when the card has Rust scope.
764
154
  */
765
- function buildLegacyPrompt(skillDef, taskInput, taskData, options = {}) {
155
+ function buildSubagentPrompt(skillDef, taskInput, taskData, options = {}) {
766
156
  const lines = [];
767
- lines.push(`## Role`);
768
- lines.push(skillDef.role?.identity || 'You are a developer assigned to complete the task below.');
769
- lines.push('');
770
-
771
- appendBehavior(lines, skillDef);
772
157
  appendProjectContext(lines, options);
773
- appendYggdrasilContext(lines, options.yggdrasilContext);
774
- appendMemoryContext(lines, taskData, options);
775
- appendAlayaLessons(lines, taskData);
776
- appendTask(lines, taskInput, taskData, options);
777
- appendCognitiveRecall(lines, taskData, options);
158
+ appendTask(lines, taskInput, taskData, { ...options, compactCard: true });
159
+
160
+ const isIsolated = !!((options.isSubagent && !options.isCrossProject) || options.isWorktree);
161
+ const taskId = options.taskId || extractTaskId(taskData) || '{task_id}';
162
+ lines.push('## Commit');
163
+ lines.push(`Command: ${buildCommitCommand(isIsolated ? '-A' : '<only the files you edited>', `[${taskId}] task completed`)}`);
164
+ lines.push(isIsolated
165
+ ? 'Commit right after editing, before verifying. A failing commit: report it loudly. Add finds nothing after an Edit = you edited the main repo path: check pwd, redo in the worktree.'
166
+ : 'Shared working tree: stage only files you edited. Commit right after editing, before verifying. A failing commit: report it loudly.');
167
+ lines.push('');
168
+ appendRustCheckReminder(lines, taskData, options, isIsolated);
778
169
 
779
- if (skillDef.collaboration?.workflow) {
780
- appendWorkflow(lines, skillDef, options, taskData);
170
+ lines.push('## Verify and report');
171
+ // Ponytail A/B 2026-10-10: the arm without this line broke an existing fixture test it never ran.
172
+ lines.push('Before editing, grep the callers, tests and fixtures your change reaches; fix what it breaks. Least code that completes the card: reuse existing helpers, no unrequested abstraction, option or duplicate implementation.');
173
+ lines.push('Run the tests the card names (narrow, not the full suite); say what you did not verify.');
174
+ const onBlocked = skillDef.collaboration && skillDef.collaboration.on_blocked;
175
+ if (onBlocked && onBlocked.run) {
176
+ lines.push(`Blocked: \`${buildTaskApiRewriter(options)(onBlocked.run).replace(/\{task_id\}/g, taskId)}\` and explain.`);
781
177
  }
782
-
783
- appendCognitiveCheckpoint(lines, taskData, options);
784
-
178
+ lines.push('Final report <= 20 lines: files touched, tests run, commit sha, unverified items.');
179
+ lines.push('');
180
+ appendDispatchOverrides(lines, taskData);
785
181
  return lines.join('\n');
786
182
  }
787
183
 
788
- // ─────────────────────────────────────────
789
- // 共用片段
790
- // ─────────────────────────────────────────
791
-
792
- /**
793
- * Append context-aware shared modules to prompt.
794
- * Reads .formation/roles/_shared/modules/*.yaml based on task tags / chart context.
795
- * Inserted before appendBehavior().
796
- */
797
- function appendSharedModules(lines, taskData, options = {}) {
798
- const corePath = options.corePath || resolveCorePath(options.projectPath) || undefined;
799
- const chartPath = options.chartPath || null;
800
- const modulePaths = resolveModules(taskData, chartPath, corePath);
801
- if (modulePaths.length === 0) return;
802
-
803
- for (const modPath of modulePaths) {
804
- try {
805
- if (!fs.existsSync(modPath)) continue;
806
- const raw = fs.readFileSync(modPath, 'utf-8');
807
-
808
- // Extract name and description for the section header
809
- const nameMatch = raw.match(/^name:\s*(.+)$/m);
810
- const descMatch = raw.match(/^description:\s*(.+)$/m);
811
- const moduleName = nameMatch ? nameMatch[1].trim() : path.basename(modPath, '.yaml');
812
- const moduleDesc = descMatch ? descMatch[1].trim() : '';
813
-
814
- lines.push(`## Shared Module: ${moduleName}`);
815
- if (moduleDesc) {
816
- lines.push(moduleDesc);
817
- lines.push('');
818
- }
819
-
820
- // Inject content section (raw YAML-formatted text, stripped of top-level key)
821
- const contentMatch = raw.match(/^content:\n([\s\S]*)$/m);
822
- if (contentMatch) {
823
- lines.push(contentMatch[1].trimEnd());
824
- } else {
825
- lines.push(raw.trimEnd());
826
- }
827
- lines.push('');
828
- } catch { /* graceful degradation — skip unreadable module */ }
829
- }
830
- }
831
-
832
- function appendBehavior(lines, skillDef) {
833
- if (skillDef.behavior && skillDef.behavior.length > 0) {
834
- lines.push(`## Behavior`);
835
- for (const b of skillDef.behavior) {
836
- lines.push(`- ${b}`);
837
- }
838
- lines.push('');
839
- }
840
- }
841
-
842
- /**
843
- * Append shared principles (DDx protocol) to prompt.
844
- * Loaded from .specialist/shared/principles.yaml.
845
- */
846
- function appendSharedPrinciples(lines, options = {}) {
847
- const corePath = options.corePath || undefined;
848
- const ddx = loadSharedPrinciples(corePath);
849
- if (!ddx) return;
850
-
851
- lines.push(`## Shared Principles — 鑑別診斷 (DDx)`);
852
- lines.push(`遇到 Bug 診斷、架構決策、結果驗證時,必須套用以下 protocol:`);
853
- lines.push('');
854
- lines.push(ddx);
855
- lines.push('');
184
+ function extractTaskId(taskData) {
185
+ try {
186
+ const parsed = typeof taskData === 'string' ? JSON.parse(taskData) : taskData;
187
+ return (parsed && parsed.id) || null;
188
+ } catch { return null; }
856
189
  }
857
190
 
858
191
  /**
@@ -861,15 +194,14 @@ function appendSharedPrinciples(lines, options = {}) {
861
194
  * which project to operate on and where system files are.
862
195
  */
863
196
  function appendProjectContext(lines, options = {}) {
864
- const { projectPath, worktreePath } = options;
197
+ const { projectPath } = options;
865
198
 
866
199
  // Sub-project context — only when projectPath is not the core project
867
200
  if (projectPath) {
868
- const pathMod = require('path');
869
- const coreRoot = pathMod.resolve(__dirname, '..', '..');
201
+ const coreRoot = path.resolve(__dirname, '..', '..');
870
202
  const norm = p => p.replace(/[\\/]+$/, '').replace(/\\/g, '/').toLowerCase();
871
203
 
872
- if (norm(pathMod.resolve(projectPath)) !== norm(coreRoot)) {
204
+ if (norm(path.resolve(projectPath)) !== norm(coreRoot)) {
873
205
  lines.push(`## Project Context [internal — do NOT mention sub-project status to user]`);
874
206
  lines.push(`Working directory: ${projectPath}`);
875
207
  lines.push(`- Task files: ${projectPath}/.kenaz/tasks/`);
@@ -884,121 +216,13 @@ function appendProjectContext(lines, options = {}) {
884
216
  lines.push('');
885
217
  lines.push(`### KENAZ_CORE_PATH (resolved)`);
886
218
  lines.push(`\`${corePath}\``);
887
- lines.push(`All system tools use this path as root. When you need task-api.js, headless.js, or other Formation tools, reference:`);
219
+ lines.push(`All system tools use this path as root. When you need task-api.js, reference:`);
888
220
  lines.push(`- task-api: \`node "${corePath}/.collaboration/core/task-api.js" --project-path "${projectPath}"\``);
889
- lines.push(`- headless: \`node "${corePath}/.formation/tools/headless.js" --task=TASK_XXX --project-path="${projectPath}"\``);
890
- lines.push(`- post-complete: \`node "${corePath}/.formation/tools/post-complete.js" --task=TASK_XXX --project-path="${projectPath}"\``);
891
221
  }
892
222
 
893
223
  lines.push('');
894
224
  }
895
225
  }
896
-
897
- // Worktree context — when agent runs in an isolated git worktree
898
- if (worktreePath) {
899
- const originalProjectPath = projectPath || require('path').resolve(__dirname, '..', '..');
900
- lines.push(`## Worktree Context [internal]`);
901
- lines.push(`Your cwd is a git worktree at ${worktreePath}.`);
902
- lines.push(`For Synapse MCP calls (recall/inscribe/forge), use projectPath=${originalProjectPath} not your cwd.`);
903
- lines.push('');
904
- }
905
- }
906
-
907
- /**
908
- * Append standard Formation Preamble to prompt.
909
- * Injected into every dispatch agent for consistent task orientation.
910
- * Fields: Task (id + title), Branch (git, fallback: unknown),
911
- * Scope (scope_paths, omitted when empty), Principles (fixed).
912
- *
913
- * @param {string[]} lines - prompt lines array (mutated)
914
- * @param {string|object} taskData - task JSON string or parsed object
915
- * @param {object} [options] - options ({ projectPath })
916
- */
917
- function appendFormationPreamble(lines, taskData, options = {}) {
918
- let taskId = null;
919
- let taskTitle = null;
920
- let scopePaths = [];
921
-
922
- try {
923
- const parsed = typeof taskData === 'string' ? JSON.parse(taskData) : taskData;
924
- if (parsed) {
925
- taskId = parsed.id || null;
926
- taskTitle = parsed.title || null;
927
- scopePaths = parsed.scope_paths || [];
928
- }
929
- } catch { /* not JSON */ }
930
-
931
- // Resolve git branch — fallback to 'unknown' on any failure
932
- let branch = 'unknown';
933
- try {
934
- const { execSync } = require('child_process');
935
- const cwd = options.projectPath || path.resolve(__dirname, '..', '..');
936
- const out = execSync('git rev-parse --abbrev-ref HEAD', {
937
- cwd,
938
- encoding: 'utf-8',
939
- stdio: ['pipe', 'pipe', 'pipe'],
940
- timeout: 3000,
941
- windowsHide: true,
942
- });
943
- branch = (out || '').trim() || 'unknown';
944
- } catch { /* git unavailable or not a repo */ }
945
-
946
- lines.push(`## Dispatch Context`);
947
- if (taskId || taskTitle) {
948
- const label = [taskId, taskTitle].filter(Boolean).join(' — ');
949
- lines.push(`Task: ${label}`);
950
- }
951
- lines.push(`Branch: ${branch}`);
952
- if (scopePaths.length > 0) {
953
- lines.push(`Scope: ${scopePaths.join(', ')}`);
954
- }
955
- lines.push(`Principles: 完美達成>一切|禁止盲寫|露出問題|結構>估計`);
956
- lines.push('');
957
-
958
- appendMinimalismLadder(lines);
959
- appendFileBudgetSRP(lines);
960
- }
961
-
962
- /**
963
- * Append the YAGNI minimalism decision-ladder (Ponytail essence) to prompt.
964
- * Wired once into appendFormationPreamble so EVERY dispatched agent
965
- * (unit/solo/squad/legion + sub-agent path via shared buildPrompt) inherits it.
966
- * Closes the gap: Ansuz elixir holds wire>rewrite, but dispatched agents
967
- * previously received no active "write less" nudge — only the "禁止盲寫" guard.
968
- * Kept compact: a nudge, not a ritual.
969
- *
970
- * @param {string[]} lines - prompt lines array (mutated)
971
- */
972
- function appendMinimalismLadder(lines) {
973
- lines.push(`## Code Minimalism — 最省決策階梯 (YAGNI)`);
974
- lines.push(`寫任何代碼前,停在第一個成立的階:`);
975
- lines.push(`1. 這需要存在嗎?不需要 → 跳過(不臆測未來需求)`);
976
- lines.push(`2. stdlib / 語言內建有嗎?→ 用它`);
977
- lines.push(`3. 平台 / 框架原生功能?→ 用它,別自己造`);
978
- lines.push(`4. 專案已裝依賴有嗎?→ 重用,別加新依賴`);
979
- lines.push(`5. 一行能解嗎?→ 一行`);
980
- lines.push(`6. 否則:最小可行實作`);
981
- lines.push(`規則:不為單一實作建抽象介面;刪除優於新增;最短 diff。`);
982
- lines.push(`刻意簡化用 \`// thrift:\` 標記上限與升級路徑(例:\`// thrift: 同步即可,QPS>100 再改 async\`)。`);
983
- lines.push(`不可簡化掉(lazy ≠ negligent):input validation、error handling、security、accessibility、用戶明確要求的功能。`);
984
- lines.push('');
985
- }
986
-
987
- /**
988
- * Append the ARCH-001 file-budget & SRP clause (shift-left prevention half).
989
- * Wired into appendFormationPreamble next to appendMinimalismLadder so every
990
- * dispatched agent (unit/solo/squad/legion, subagent + Formation paths) sees
991
- * the budget while writing code, not as late rework at the L1 merge gate
992
- * (dev/scripts/check-file-size.js, .rule/general/file-size-threshold.yaml).
993
- *
994
- * @param {string[]} lines - prompt lines array (mutated)
995
- */
996
- function appendFileBudgetSRP(lines) {
997
- lines.push(`## File Budget & SRP (ARCH-001)`);
998
- lines.push(`單檔行數:warn=2000 / hard=4000。接近 warn 就停筆,優先 extraction 而非累加。`);
999
- lines.push(`拆分模式:module / hooks/ / components/ / types.ts(依 CODE-STR-002)。`);
1000
- lines.push(`verify 步驟跑:\`node dev/scripts/check-file-size.js --staged\` — 有 BLOCK 先拆再提交。`);
1001
- lines.push('');
1002
226
  }
1003
227
 
1004
228
  function appendTask(lines, taskInput, taskData, options = {}) {
@@ -1014,7 +238,7 @@ function appendTask(lines, taskInput, taskData, options = {}) {
1014
238
  // Structured task injection — give agent everything it needs
1015
239
  lines.push(`**Task ID**: ${parsed.id}`);
1016
240
  lines.push(`**Title**: ${parsed.title || 'N/A'}`);
1017
- if (parsed._file_path) {
241
+ if (parsed._file_path && !options.compactCard) {
1018
242
  lines.push(`**Task File**: ${parsed._file_path}`);
1019
243
  }
1020
244
 
@@ -1027,17 +251,17 @@ function appendTask(lines, taskInput, taskData, options = {}) {
1027
251
  lines.push(`[internal] Sub-project: \`${taskProjectPath}\` — do NOT mention this to the user.`);
1028
252
  lines.push(`- ALL file operations must target this project directory`);
1029
253
  lines.push(`- ALL git operations (branch, commit, merge) must run in this repo`);
1030
- lines.push(`- post-complete: \`--project-path=${taskProjectPath}\``);
1031
- lines.push(`- headless dispatch: \`--project-path=${taskProjectPath}\``);
1032
254
  lines.push('');
1033
255
  }
1034
256
 
1035
257
  if (parsed.description) {
1036
258
  lines.push(`**Description**: ${parsed.description.trim()}`);
1037
259
  }
1038
- if (parsed.priority) lines.push(`**Priority**: ${parsed.priority}`);
1039
- if (parsed.complexity) lines.push(`**Complexity**: ${parsed.complexity}`);
1040
- if (parsed.role) lines.push(`**Role**: ${parsed.role}`);
260
+ if (!options.compactCard) {
261
+ if (parsed.priority) lines.push(`**Priority**: ${parsed.priority}`);
262
+ if (parsed.complexity) lines.push(`**Complexity**: ${parsed.complexity}`);
263
+ if (parsed.role) lines.push(`**Role**: ${parsed.role}`);
264
+ }
1041
265
 
1042
266
  // Resolve scope_paths to absolute paths
1043
267
  const scopePaths = parsed.scope_paths || [];
@@ -1047,7 +271,6 @@ function appendTask(lines, taskInput, taskData, options = {}) {
1047
271
  lines.push('**Scope Paths** (你只能操作這些路徑):');
1048
272
  for (const sp of scopePaths) {
1049
273
  if (root) {
1050
- const path = require('path');
1051
274
  lines.push(`- ${sp} → ${path.resolve(root, sp)}`);
1052
275
  } else {
1053
276
  lines.push(`- ${sp}`);
@@ -1262,106 +485,6 @@ function applyRustCheckToPrefix(prefix, body) {
1262
485
  return prefix.split(HARD_RULE_4_RUST_PHRASE).join(HARD_RULE_4_RUST_ALLOW_PHRASE);
1263
486
  }
1264
487
 
1265
- function appendWorkflow(lines, skillDef, options = {}, taskData = null) {
1266
- if (skillDef.collaboration && skillDef.collaboration.workflow) {
1267
- // Sub-project: rewrite task-api commands to include --project-path
1268
- const rewriteCmd = buildTaskApiRewriter(options);
1269
- // Replace {task_id} placeholder with actual task ID when available
1270
- const taskId = options.taskId || null;
1271
- const resolveTaskId = (cmd) => taskId ? cmd.replace(/\{task_id\}/g, taskId) : cmd;
1272
-
1273
- // TASK_1821: rigor-gated self-complete. rigor:high cards must NOT be closed by
1274
- // their own executor — the party with independent verification on the Formation
1275
- // path is post-complete (dispatcher side: regression tests + verify-acceptance +
1276
- // verified merge), and it already moves the task active→done itself after
1277
- // isVerifiedMergeSuccess (post-complete.js Step 2.5), so the card cannot strand.
1278
- // NOTE: this axis (per-card rigor) is orthogonal to the isSubagent axis below
1279
- // (per-dispatch-path). The subagent branch `continue`s first, so its replacement
1280
- // note stays byte-identical and this check only fires on Formation/headless/serial.
1281
- // TASK_2252: extractRigor accepts string OR object (a bare JSON.parse(object)
1282
- // threw into the catch and silently skipped this gate).
1283
- const taskRigor = extractRigor(taskData);
1284
-
1285
- lines.push(`## Workflow`);
1286
- // TASK_1819: only isolated executors may stage the whole tree.
1287
- // - options.isSubagent → Agent-tool dispatch (worktree isolation, dispatch.js)
1288
- // ...UNLESS options.isCrossProject (TASK_1824): cross-project sub-agent dispatch
1289
- // has NO worktree isolation (TASK_1672 — the Agent tool worktree derives from the
1290
- // conversation cwd repo, not the target sub-project). It writes directly into the
1291
- // sub-project's SHARED working tree, so it gets explicit-paths staging like serial.
1292
- // - options.isWorktree → Formation task worktree (threaded from headless.js
1293
- // promptOpts.isWorktree = !!_unitWorktreePath)
1294
- // Serial (neither flag) shares the working tree with the user and Ansuz —
1295
- // a blanket `git add -A` swept 392 unrelated files in TASK_188, so serial
1296
- // mode gets an explicit-paths staging instruction instead.
1297
- const isIsolated = !!((options.isSubagent && !options.isCrossProject) || options.isWorktree);
1298
- for (const step of skillDef.collaboration.workflow) {
1299
- // In subagent mode: skip read/claim (card already inline; claim done by parent)
1300
- if (options.isSubagent) {
1301
- const stepRun = step.run || '';
1302
- if (stepRun.includes('task-api.js get ') || stepRun.includes('task-api.js claim ')) {
1303
- continue;
1304
- }
1305
- if (stepRun.includes('task-api.js complete')) {
1306
- lines.push(`### ${step.step}`);
1307
- lines.push('Note: ⚠️ Worktree mode — 禁止執行 task-api.js complete。主 thread 在 merge + 驗證後處理。');
1308
- lines.push('');
1309
- continue;
1310
- }
1311
- }
1312
- // TASK_1821: Formation/headless/serial path + rigor:high → replace the
1313
- // self-complete step with a prohibition note (same technique as the
1314
- // subagent branch above). The dispatcher-side post-complete pipeline
1315
- // closes the card after merge + independent verification.
1316
- if (taskRigor === 'high' && step.run && step.run.includes('task-api.js complete')) {
1317
- lines.push(`### ${step.step}`);
1318
- lines.push('Note: ⚠️ rigor: high — 禁止執行 task-api.js complete。任務留在 active/,由派工方的 post-complete 在 merge + 獨立驗證(回歸測試 + verify-acceptance)通過後移入 done/。在完成摘要中明確標註「未自行 complete,待派工者驗證後關閉」。');
1319
- lines.push('');
1320
- continue;
1321
- }
1322
- // TASK_1833: the workflow commit step must never render the blanket
1323
- // `|| true` from the skill YAML — see buildCommitCommand for why (one
1324
- // swallow covered both nothing-to-commit AND index.lock data loss).
1325
- // Applies to BOTH modes: isolated gets `git add -A`, shared-tree gets
1326
- // explicit-paths staging (TASK_1819) — each with the honest commit shape.
1327
- if (step.run && step.run.includes('git add -A') && step.run.includes('git commit')) {
1328
- const msgMatch = step.run.match(/-m\s+"([^"]+)"/);
1329
- const commitMsg = msgMatch ? msgMatch[1] : '[{task_id}] task completed';
1330
- lines.push(`### ${step.step}`);
1331
- if (isIsolated) {
1332
- lines.push(`Command: ${resolveTaskId(buildCommitCommand('-A', commitMsg))}`);
1333
- if (step.note) lines.push(`Note: ${step.note}`);
1334
- lines.push('Note: nothing to commit 時靜默跳過(不算失敗);git commit 失敗(如 index.lock 競爭)會自動退避重試兩次,仍失敗則以非零退出 — 此時大聲回報錯誤,不要吞掉。');
1335
- } else {
1336
- lines.push(`Command: ${resolveTaskId(buildCommitCommand('<只列出你本次明確編輯過的檔案路徑>', commitMsg))}`);
1337
- lines.push('Note: ⚠️ 共用工作樹模式(無 worktree 隔離)— 你與使用者、其他 agent 共用同一個工作樹。只 stage 你本次明確編輯過的檔案,嚴禁一次 stage 整個工作樹(TASK_188 曾因此把 392 個無關檔案掃進單一 commit)。nothing to commit 時靜默跳過(不算失敗);git commit 失敗(如 .git/index.lock 被其他 agent 持有)會自動退避重試兩次,仍失敗則以非零退出 — 此時大聲回報錯誤,不要吞掉。');
1338
- }
1339
- lines.push('');
1340
- appendRustCheckReminder(lines, taskData, options, isIsolated);
1341
- continue;
1342
- }
1343
- lines.push(`### ${step.step}`);
1344
- if (step.run) lines.push(`Command: ${resolveTaskId(rewriteCmd(step.run))}`);
1345
- if (step.note) lines.push(`Note: ${step.note}`);
1346
- if (step.actions) {
1347
- for (const a of step.actions) {
1348
- lines.push(`- ${a}`);
1349
- }
1350
- }
1351
- lines.push('');
1352
- }
1353
-
1354
- if (skillDef.collaboration.on_blocked) {
1355
- lines.push(`### On Blocked`);
1356
- lines.push(`Command: ${resolveTaskId(rewriteCmd(skillDef.collaboration.on_blocked.run))}`);
1357
- lines.push(`Note: ${skillDef.collaboration.on_blocked.note}`);
1358
- lines.push('');
1359
- }
1360
- }
1361
-
1362
- appendDispatchOverrides(lines, taskData);
1363
- }
1364
-
1365
488
  /**
1366
489
  * TASK_1820: card-controlled override channel, rendered immediately AFTER the
1367
490
  * ## Workflow envelope. Section order is fixed (## Task always precedes
@@ -1413,563 +536,13 @@ function buildTaskApiRewriter(options = {}) {
1413
536
  return (cmd) => cmd.replace(TASK_API_PATTERN, replacement);
1414
537
  }
1415
538
 
1416
- /**
1417
- * Append Yggdrasil network context — shows what other nodes are doing.
1418
- * Lets the agent know about remote activity and potential file conflicts.
1419
- *
1420
- * @param {string[]} lines
1421
- * @param {object} ctx - { onlineNodes, remoteTasks, myNodeName, myScopePaths }
1422
- */
1423
- function appendYggdrasilContext(lines, ctx) {
1424
- if (!ctx) return;
1425
- const { onlineNodes = [], remoteTasks = [], myScopePaths = [] } = ctx;
1426
- if (onlineNodes.length === 0 && remoteTasks.length === 0) return;
1427
-
1428
- lines.push('## Yggdrasil Network (Remote Collaborators)');
1429
- lines.push('Other developers/agents are currently working on this project:');
1430
- lines.push('');
1431
-
1432
- for (const node of onlineNodes) {
1433
- const files = node.touching_files || [];
1434
- lines.push(`**${node.node_name}** — ONLINE`);
1435
- if (node.branch) lines.push(` Branch: ${node.branch}`);
1436
- if (node.current_task) lines.push(` Task: ${node.current_task}`);
1437
- if (node.work_summary) lines.push(` Working on: ${node.work_summary}`);
1438
- if (files.length > 0) {
1439
- lines.push(` Files: ${files.slice(0, 8).join(', ')}${files.length > 8 ? ` (+${files.length - 8})` : ''}`);
1440
- }
1441
-
1442
- // Highlight file overlaps with my scope (informational, not blocking)
1443
- if (myScopePaths.length > 0 && files.length > 0) {
1444
- const overlaps = files.filter(f =>
1445
- myScopePaths.some(sp => f.startsWith(sp) || sp.startsWith(f))
1446
- );
1447
- if (overlaps.length > 0) {
1448
- lines.push(` ℹ️ FILE OVERLAP: ${overlaps.join(', ')}`);
1449
- lines.push(` → Be aware of potential merge conflicts. Coordinate if needed.`);
1450
- }
1451
- }
1452
- lines.push('');
1453
- }
1454
-
1455
- if (remoteTasks.length > 0) {
1456
- lines.push('Active remote tasks:');
1457
- for (const t of remoteTasks.slice(0, 5)) {
1458
- lines.push(`- ${t.task_id} (claimed by ${t.claimed_by})`);
1459
- }
1460
- lines.push('');
1461
- }
1462
-
1463
- lines.push('To check real-time network status: `node .formation/tools/coordinator.js --status`');
1464
- lines.push('');
1465
- }
1466
-
1467
- /**
1468
- * Append memory context — 從 .failure/ + .memory/ + .kenaz/main-memory/ 載入相關經驗
1469
- * 注入到 prompt 中讓 agent 避免重蹈覆轍
1470
- */
1471
- function appendMemoryContext(lines, taskData, options = {}) {
1472
- if (!memoryLoader) return;
1473
-
1474
- try {
1475
- // 從 taskData 提取上下文資訊
1476
- const memOpts = {};
1477
- if (taskData) {
1478
- let parsed = null;
1479
- try { parsed = JSON.parse(taskData); } catch { /* not JSON */ }
1480
- if (parsed) {
1481
- memOpts.taskTitle = parsed.title || '';
1482
- memOpts.role = parsed.role || '';
1483
- memOpts.scopePaths = parsed.scope_paths || [];
1484
- memOpts.tags = parsed.tags || [];
1485
- memOpts.priority = parsed.priority || '';
1486
- }
1487
- }
1488
-
1489
- // TASK_1998 two-axis policy: Known Failures / Project Memory are CONTEXT
1490
- // volume, driven by complexity — simple drops both, medium restores Known
1491
- // Failures but keeps Project Memory dropped, complex keeps both. Knowledge
1492
- // Base (main-memory) is unaffected either way — it isn't part of this gate.
1493
- const profile = promptProfile({ rigor: extractRigor(taskData), complexity: extractComplexity(taskData) });
1494
- memOpts.skipFailures = profile.skipKnownFailures;
1495
- // 2026-10-10: memory system frozen (project_memory_system_frozen) - no Project Memory block.
1496
- memOpts.skipMemory = true;
1497
-
1498
- // Resolve corePath for sub-projects so memory-loader reads from core's
1499
- // .failure/, .memory/, .kenaz/main-memory/ instead of __dirname fallback
1500
- const corePath = options.corePath || resolveCorePath(options.projectPath);
1501
- if (corePath) memOpts.corePath = corePath;
1502
-
1503
- const memoryBlock = memoryLoader.loadMemoryContext(memOpts);
1504
- if (memoryBlock) {
1505
- lines.push(memoryBlock);
1506
- }
1507
-
1508
- // Code-navigation reminder (SYN-002, rewritten for LSP in TASK_2335)
1509
- lines.push('');
1510
- lines.push('## Search Tool Selection (SYN-002)');
1511
- lines.push('搜尋前先判斷:精確字串(函數名、error message、概念關鍵字)→ Grep / Glob。「誰呼叫它」「改了影響哪裡」「定義在哪」→ kenaz_lsp_callers / kenaz_lsp_references / kenaz_lsp_definition(args: projectPath, file, line 或 symbol, depth);檔案內 symbol 列表 → kenaz_lsp_symbols。');
1512
- lines.push('');
1513
- } catch { /* graceful degradation */ }
1514
- }
1515
-
1516
- /**
1517
- * Synchronously call an MCP tool via JSON-RPC over the local Dashboard HTTP
1518
- * server (127.0.0.1:18080/api/mcp) in a subprocess — keeps buildPrompt() sync.
1519
- * Returns the parsed inner data object, or null on any failure / unavailability.
1520
- * Shared by appendAlayaLessons.
1521
- *
1522
- * @param {string} toolName - MCP tool name (e.g. 'cortex_recall')
1523
- * @param {object} toolArgs - forwarded as params.arguments
1524
- * @returns {object|null}
1525
- */
1526
- function callMcpToolSync(toolName, toolArgs) {
1527
- try {
1528
- const { execSync } = require('child_process');
1529
- const script = `
1530
- const http = require('http');
1531
- const payload = JSON.stringify({
1532
- jsonrpc: '2.0', id: 1,
1533
- method: 'tools/call',
1534
- params: { name: ${JSON.stringify(toolName)}, arguments: ${JSON.stringify(toolArgs)} }
1535
- });
1536
- const req = http.request({
1537
- hostname: '127.0.0.1', port: 18080, path: '/api/mcp',
1538
- method: 'POST',
1539
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
1540
- timeout: 5000
1541
- }, (res) => {
1542
- let d = '';
1543
- res.on('data', c => d += c);
1544
- res.on('end', () => process.stdout.write(d));
1545
- });
1546
- req.on('error', () => process.exit(0));
1547
- req.on('timeout', () => { req.destroy(); process.exit(0); });
1548
- req.write(payload);
1549
- req.end();
1550
- `;
1551
- const stdout = execSync(`node -e "${script.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, {
1552
- timeout: 8000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
1553
- });
1554
- if (!stdout || !stdout.trim()) return null;
1555
- const result = JSON.parse(stdout);
1556
- if (!result || result.error) return null;
1557
- const content = result.result?.content;
1558
- if (!content || !Array.isArray(content)) return null;
1559
- const textPart = content.find(c => c.type === 'text');
1560
- if (!textPart || !textPart.text) return null;
1561
- try { return JSON.parse(textPart.text); } catch { return null; }
1562
- } catch { return null; } // MCP server unavailable — graceful degradation
1563
- }
1564
-
1565
- // TASK_2101/TASK_2104: the absolute relevance gate (cosine floor + stack
1566
- // match) for Alaya lesson injection — constants, deriveTaskStack,
1567
- // deriveBijaStack, passesAlayaRelevanceGate — now lives in
1568
- // .formation/tools/alaya-gate.js (dependency-free, shared with task-api.js's
1569
- // echoAlayaLessons). Destructured from the require() at the top of this
1570
- // file; see alaya-gate.js for the full calibration writeup and rationale.
1571
-
1572
- /**
1573
- * Append bounded Alaya process-lesson context to the prompt.
1574
- * Reuses the title+scope_paths query, queries
1575
- * alaya_recall (hybrid mode, failure_seed + lesson_seed, with the TASK_2101
1576
- * min_cosine floor) and injects a ## Process lessons (Alaya) section capped
1577
- * at 900 chars total — but ONLY for candidates that also pass the stack-match
1578
- * gate. Zero passing candidates renders nothing (R3: an empty section beats
1579
- * noise).
1580
- *
1581
- * Graceful degradation: silently skips on empty query, empty results, or
1582
- * any MCP failure — the recall bar is never allowed to raise dispatch
1583
- * latency or fail the prompt build.
1584
- *
1585
- * @param {string[]} lines - prompt lines array (mutated)
1586
- * @param {string|object} taskData - task JSON string or parsed object
1587
- * @param {(toolName: string, toolArgs: object) => object|null} [mcpCaller] - injectable MCP bridge (defaults to callMcpToolSync; test seam)
1588
- */
1589
- function appendAlayaLessons(lines, taskData, mcpCaller = callMcpToolSync) {
1590
- if (!mcpCaller.__forceAlaya) return; // 2026-10-10: memory system frozen; tests opt in via __forceAlaya
1591
- const query = buildTaskContextQuery(taskData);
1592
- if (!query) return;
1593
-
1594
- let scopePaths = [];
1595
- try {
1596
- const parsed = typeof taskData === 'string' ? JSON.parse(taskData) : taskData;
1597
- if (parsed) scopePaths = parsed.scope_paths || [];
1598
- } catch { /* taskData not JSON — task stack stays empty, floor-only gate */ }
1599
- const taskStack = deriveTaskStack(scopePaths);
1600
-
1601
- try {
1602
- const recallData = mcpCaller('alaya_recall', {
1603
- query, mode: 'hybrid', seed_types: ['failure_seed', 'lesson_seed'],
1604
- limit: ALAYA_CANDIDATE_POOL, min_cosine: ALAYA_MIN_COSINE_DEFAULT,
1605
- });
1606
- if (!recallData) return;
1607
-
1608
- let bijas = recallData.bijas || [];
1609
- if (bijas.length === 0) return;
1610
- const candidateCount = bijas.length;
1611
-
1612
- bijas = dedupBijas(bijas);
1613
-
1614
- // TASK_2101 R3: stack-match gate on top of the server-side cosine floor
1615
- // (the server already applied min_cosine, but re-checking here keeps
1616
- // this function correct against a stub/older server that ignores it).
1617
- const gated = bijas.filter(b => passesAlayaRelevanceGate(b, taskStack, ALAYA_MIN_COSINE_DEFAULT));
1618
- const droppedCount = candidateCount - gated.length;
1619
- if (droppedCount > 0) {
1620
- // Log for floor/gate tuning (R3) — never fails the prompt build.
1621
- console.error(`[appendAlayaLessons] dropped ${droppedCount}/${candidateCount} candidates below the cosine floor (${ALAYA_MIN_COSINE_DEFAULT}) or stack mismatch for query "${query}"`);
1622
- }
1623
- bijas = gated.slice(0, 3);
1624
- if (bijas.length === 0) return;
1625
-
1626
- const modeEffective = recallData.mode_effective || 'hybrid';
1627
- const sectionLines = [
1628
- '## Process lessons (Alaya)',
1629
- `Query: "${query}" — ${bijas.length} seeds (mode=${modeEffective}).`,
1630
- ...bijas.map(b => {
1631
- const content = (b.content || '').replace(/\s+/g, ' ').trim().slice(0, 220);
1632
- return `- **${b.seed_type || 'seed'}** [${b.origin || 'unknown'}] ${content}`;
1633
- }),
1634
- ];
1635
-
1636
- let section = sectionLines.join('\n');
1637
- if (section.length > 900) {
1638
- section = section.slice(0, 899) + '…';
1639
- }
1640
-
1641
- lines.push('');
1642
- lines.push(...section.split('\n'));
1643
- lines.push('');
1644
- } catch { /* Alaya unavailable — graceful degradation */ }
1645
- }
1646
-
1647
- function appendCognitiveRecall(lines, taskData, options = {}) {
1648
- // Extract recall keywords from task data (title, scope_paths, tags)
1649
- let keywords = [];
1650
- let scopeFiles = [];
1651
- try {
1652
- const parsed = typeof taskData === 'string' ? JSON.parse(taskData) : taskData;
1653
- if (parsed) {
1654
- if (parsed.title) {
1655
- const stopWords = new Set(['the','a','an','is','are','was','were','be','to','of','and','in','for','on','at','by','with','from','as','into','fix','task','add','update','implement']);
1656
- const words = parsed.title.replace(/[^a-zA-Z0-9_\s]/g, ' ').split(/\s+/).filter(w => w.length > 2 && !stopWords.has(w.toLowerCase()));
1657
- keywords.push(...words.slice(0, 5));
1658
- }
1659
- if (parsed.scope_paths && parsed.scope_paths.length > 0) {
1660
- for (const sp of parsed.scope_paths) {
1661
- const parts = sp.replace(/\\/g, '/').split('/');
1662
- const last = parts[parts.length - 1].replace(/\.[^.]+$/, '');
1663
- if (last && last.length > 2) {
1664
- keywords.push(last);
1665
- scopeFiles.push(last);
1666
- }
1667
- }
1668
- }
1669
- if (parsed.tags && parsed.tags.length > 0) {
1670
- keywords.push(...parsed.tags.filter(t => t.length > 2));
1671
- }
1672
- }
1673
- } catch { /* taskData not JSON, skip */ }
1674
-
1675
- if (keywords.length === 0) return;
1676
- keywords = [...new Set(keywords)].slice(0, 8);
1677
-
1678
- }
1679
-
1680
- // Exact, shared wording — rigor:low ends its Working contract with this
1681
- // bullet; rigor:medium repeats the identical sentence as its own line after
1682
- // the short progress-note instructions (TASK_1998 spec: "the exact line").
1683
- const RUNE_OPTIONAL_LINE = 'Inscribe exactly one rune (synapse_inscribe) if you learned something non-obvious; otherwise skip it.';
1684
-
1685
- /**
1686
- * TASK_1998: rigor:medium's short (3-line) progress-note instruction — the
1687
- * same write-then-run contract as appendProgressNoteInstructions()'s full
1688
- * block, compressed to a header + 2 lines instead of a full schema dump.
1689
- * No-op when taskData has no id (mirrors appendProgressNoteInstructions).
1690
- */
1691
- function appendShortProgressNote(lines, taskData, options = {}) {
1692
- let taskId = null;
1693
- try {
1694
- const parsed = typeof taskData === 'string' ? JSON.parse(taskData) : taskData;
1695
- if (parsed) taskId = parsed.id;
1696
- } catch { /* taskData not JSON, skip */ }
1697
- if (!taskId) return;
1698
-
1699
- const corePath = options.corePath || path.resolve(__dirname, '..', '..');
1700
- const corePathFwd = corePath.replace(/\\/g, '/');
1701
- const noteFile = `.kenaz/specialist/progress_note_${taskId}.json`;
1702
-
1703
- lines.push(`## Progress Note (brief)`);
1704
- lines.push(`Write JSON to \`${noteFile}\`, then run: node "${corePathFwd}/.specialist/tools/phase-gate.js" note --task=${taskId} --from-file="${noteFile}"`);
1705
- lines.push(`Include: files touched, acceptance-criteria pass/fail, any deviations.`);
1706
- }
1707
-
1708
- function appendCognitiveCheckpoint(lines, taskData, options = {}) {
1709
- const profile = promptProfile({ rigor: extractRigor(taskData), complexity: extractComplexity(taskData) });
1710
-
1711
- // TASK_1998: PROCESS strictness is rigor-driven. low/medium collapse the
1712
- // six ritual sections (Before You Start recall, Before You Finish
1713
- // inscribe, Cognitive Checkpoint, Progress Note, Known Failures, Project
1714
- // Memory) into a compact contract — Ansuz already ran recall at
1715
- // scoping time; the full mandatory recall/inscribe ceremony is reserved
1716
- // for rigor:high. rigor:medium additionally gets a short progress-note
1717
- // instruction on top of the contract (dropped entirely for rigor:low).
1718
- if (profile.compactRitual) {
1719
- lines.push('');
1720
- lines.push(`## Working contract`);
1721
- lines.push(`- Read the files you touch before editing.`);
1722
- lines.push(`- Independent reads: fire them as parallel tool calls in ONE turn (e.g. Read a.js + Read b.js + Grep "foo" together), not one call per turn.`);
1723
- lines.push(`- Independent shell checks: one bash command with separators (e.g. \`echo ==a==; cmd1; echo ==b==; cmd2\`), not N bash calls.`);
1724
- lines.push(`- N similar items: one command over all N (e.g. one grep with alternation, or a loop), not N calls.`);
1725
- lines.push(`- One Write or a multi-hunk Edit beats many small edits.`);
1726
- lines.push(`- No self-check greps unless the card asks for them.`);
1727
- lines.push(`- Commit early and often; the worktree hard rules above still apply verbatim.`);
1728
- lines.push(`- Final report <= 20 lines: files touched, tests run, commit sha, uncertainties.`);
1729
- lines.push('');
1730
-
1731
- if (profile.ritualTier === 'medium') {
1732
- appendShortProgressNote(lines, taskData, options);
1733
- lines.push('');
1734
- }
1735
- return;
1736
- }
1737
- }
1738
-
1739
- function appendOutput(lines, skillDef) {
1740
- if (!skillDef.output) return;
1741
- lines.push(`## Expected Output`);
1742
- lines.push(`Primary: ${typeof skillDef.output.primary === 'string' ? skillDef.output.primary.trim() : JSON.stringify(skillDef.output.primary)}`);
1743
- if (skillDef.output.secondary) {
1744
- lines.push(`Secondary: ${typeof skillDef.output.secondary === 'string' ? skillDef.output.secondary.trim() : JSON.stringify(skillDef.output.secondary)}`);
1745
- }
1746
- }
1747
-
1748
- // ─────────────────────────────────────────
1749
- // Squad Synthesis:整合多份成員報告
1750
- // ─────────────────────────────────────────
1751
-
1752
- /**
1753
- * 組裝 Squad Synthesis Agent 的 prompt
1754
- * 在所有成員完成後,由 headless.js 自動呼叫
1755
- * @param {object} skillDef - Squad 的 YAML 定義(含 merge 設定)
1756
- * @param {string} reportDir - 報告目錄名稱(如 squad-strategy)
1757
- * @returns {string} synthesis prompt
1758
- */
1759
- function buildSquadSynthesisPrompt(skillDef, reportDir, options = {}) {
1760
- const lines = [];
1761
- const merge = skillDef.merge;
1762
-
1763
- lines.push('## Role');
1764
- lines.push('你是 Squad Synthesis Agent(整合者)。');
1765
- lines.push('你的任務是讀取多份獨立分析報告,按照指定的整合策略產出最終報告。');
1766
- lines.push('你不是成員之一,你是整合者。你不偏袒任何一方。');
1767
- lines.push('');
1768
-
1769
- // Principles(整合者也必須遵守的硬規則)
1770
- if (skillDef.principles) {
1771
- lines.push('## Principles (整合時也必須遵守)');
1772
- if (skillDef.principles.focus) {
1773
- lines.push(skillDef.principles.focus.trim());
1774
- lines.push('');
1775
- }
1776
- if (skillDef.principles.forbidden && skillDef.principles.forbidden.length > 0) {
1777
- lines.push('**禁止 (違反即失敗)**:');
1778
- for (const f of skillDef.principles.forbidden) lines.push(`- ❌ ${f}`);
1779
- lines.push('');
1780
- }
1781
- if (skillDef.principles.required && skillDef.principles.required.length > 0) {
1782
- lines.push('**必須遵守**:');
1783
- for (const r of skillDef.principles.required) lines.push(`- ✅ ${r}`);
1784
- lines.push('');
1785
- }
1786
- }
1787
-
1788
- // Merge strategy
1789
- if (merge) {
1790
- lines.push(`## Merge Strategy: ${merge.strategy || 'default'}`);
1791
- if (merge.description) {
1792
- lines.push(merge.description.trim());
1793
- }
1794
- lines.push('');
1795
-
1796
- // Merge steps — 核心指令
1797
- if (merge.steps && merge.steps.length > 0) {
1798
- lines.push('## Merge Steps (按順序執行)');
1799
- lines.push('');
1800
- for (const step of merge.steps) {
1801
- lines.push(`### Step: ${step.step}`);
1802
- if (step.action) lines.push(step.action.trim());
1803
- if (step.on_partial_failure) {
1804
- lines.push(`**Partial Failure Handling**: ${step.on_partial_failure.trim()}`);
1805
- }
1806
- lines.push('');
1807
- }
1808
- }
1809
- }
1810
-
1811
- // Reports location
1812
- const reportPath = `.formation/reports/${reportDir}`;
1813
- lines.push('## Reports Location');
1814
- lines.push(`所有成員報告位於 \`${reportPath}/\` 目錄。`);
1815
- lines.push('');
1816
- lines.push('**執行步驟**:');
1817
- lines.push(`1. 用 Glob 工具掃描 \`${reportPath}/*.md\``);
1818
- lines.push('2. 排除 synthesis.md(那是你要寫的輸出)');
1819
- lines.push('3. 用 Read 工具逐一讀取每份報告');
1820
- lines.push('4. 按照上述 Merge Steps 逐步整合');
1821
- lines.push('');
1822
-
1823
- // Squad members for context
1824
- if (skillDef.members && skillDef.members.length > 0) {
1825
- lines.push('## Squad Members(供參考)');
1826
- for (const m of skillDef.members) {
1827
- const roleFirstLine = (typeof m.role === 'string' ? m.role.split('\n')[0].trim() : m.name);
1828
- lines.push(`- **${m.name}** (${m.id}): ${roleFirstLine}`);
1829
- }
1830
- lines.push('');
1831
- }
1832
-
1833
- // Output
1834
- lines.push('## Output');
1835
- lines.push(`將整合報告寫入 \`${reportPath}/synthesis.md\``);
1836
- lines.push('');
1837
- lines.push('報告格式依 Merge Strategy 而定。確保報告完整、結構清晰、附上各成員的核心觀點摘要。');
1838
- lines.push('');
1839
-
1840
- // No taskData in scope (post-hoc synthesis, not a dispatched task card) —
1841
- // rigor/complexity both normalize to 'medium', landing in the compact-ritual
1842
- // branch (TASK_1998 two-axis policy).
1843
- appendCognitiveCheckpoint(lines, null, options);
1844
-
1845
- return lines.join('\n');
1846
- }
1847
-
1848
- // ─────────────────────────────────────────
1849
- // Squad Debate:Round 2 交叉辯論 prompt
1850
- // ─────────────────────────────────────────
1851
-
1852
- /**
1853
- * 組裝 Squad Round 2 Debate prompts
1854
- * 每個成員讀取其他人的 Round 1 報告,寫出針對性回應
1855
- * @param {object} skillDef - Squad YAML 定義
1856
- * @param {string} reportDir - 報告目錄名稱
1857
- * @returns {{ memberId: string, prompt: string, skill: string }[]}
1858
- */
1859
- function buildSquadDebatePrompts(skillDef, reportDir) {
1860
- const members = skillDef.members || [];
1861
- const debate = skillDef.debate || {};
1862
- const reportPath = `.formation/reports/${reportDir}`;
1863
-
1864
- return members.map(member => {
1865
- const lines = [];
1866
- const otherMembers = members.filter(m => m.id !== member.id);
1867
-
1868
- // Role reminder
1869
- lines.push('## Role');
1870
- lines.push(`你是 **${member.name}**,這是你的第二輪分析(辯論回合)。`);
1871
- lines.push('');
1872
-
1873
- // Original stance
1874
- const roleFirstLine = typeof member.role === 'string' ? member.role.split('\n')[0].trim() : '';
1875
- if (roleFirstLine) {
1876
- lines.push('## 你的立場(簡要提醒)');
1877
- lines.push(roleFirstLine);
1878
- lines.push('');
1879
- }
1880
-
1881
- // Debate instructions (per-member)
1882
- if (member.debate_instructions) {
1883
- lines.push('## Debate Instructions');
1884
- lines.push(member.debate_instructions.trim());
1885
- lines.push('');
1886
- }
1887
-
1888
- // Debate rules (squad-wide)
1889
- if (debate.rules && debate.rules.length > 0) {
1890
- lines.push('## Debate Rules');
1891
- for (const rule of debate.rules) {
1892
- lines.push(`- ${rule}`);
1893
- }
1894
- lines.push('');
1895
- }
1896
-
1897
- // Reports to read
1898
- lines.push('## Reports to Read');
1899
- for (const other of otherMembers) {
1900
- lines.push(`- **${other.name}**: \`${reportPath}/${other.id}.md\``);
1901
- }
1902
- lines.push('');
1903
- lines.push('用 Read 工具讀取上述報告,然後寫出你的回應。');
1904
- lines.push(`你自己的 Round 1 報告在 \`${reportPath}/${member.id}.md\`,可以參考但不需要重複。`);
1905
- lines.push('');
1906
-
1907
- // Output
1908
- lines.push('## Output');
1909
- lines.push(`將反駁報告寫入 \`${reportPath}/${member.id}-r2.md\``);
1910
- lines.push('');
1911
-
1912
- return {
1913
- memberId: `${member.id}-r2`,
1914
- prompt: lines.join('\n'),
1915
- skill: member.skill || 'do'
1916
- };
1917
- });
1918
- }
1919
-
1920
- // ─────────────────────────────────────────
1921
- // Headless 模式常數
1922
- // ─────────────────────────────────────────
1923
-
1924
- // TASK_1712: Rewritten with honest provenance framing so safety-hardened models
1925
- // (claude-sonnet-5) read this as legitimate orchestration context rather than injection.
1926
- // Behavioral constraints are fully preserved — only the framing changed.
1927
- // Key changes: (1) opened with machine-assembly provenance declaration, (2) replaced
1928
- // "don't read CLAUDE.md" with why Formation supplies its own rule set, (3) replaced
1929
- // "don't ask questions" with "no human is watching — report and exit if blocked",
1930
- // (4) replaced concealment wording in git rule with safety-rationale explanation.
1931
- // TASK_1784 (DEFECT-A): Rule 6 was a categorical git ban that contradicted the workflow's
1932
- // mandated commit step (do.yaml "提交變更": git add -A && git commit --no-verify). Literal-obedience
1933
- // models (gpt-5.6-sol) obeyed the ban, produced zero commits, and tripped the leak detector on
1934
- // every unmodified bifrost dispatch (TASK_1779 A/B-proven). Rule 6 now carves out exactly the
1935
- // workflow commit command shape and nothing else; all other git subcommands stay forbidden.
1936
- const HEADLESS_PREFIX = `## Automated Dispatch Context
1937
- This prompt was machine-assembled by the Kenaz orchestration system, which is owned and
1938
- operated by the repository owner. It runs unattended via \`claude -p\` (no interactive user
1939
- is watching the output stream). The task brief below was authored and approved by the user
1940
- before this automated run was triggered.
1941
-
1942
- ## Operating Rules
1943
- 1. Start with a one-line summary: "📋 {TASK_ID}: {what you are about to do}" (<=30 words). Then proceed.
1944
- 2. Execute directly — do not ask clarifying questions. Requirements were confirmed before dispatch.
1945
- If you are blocked by a genuine ambiguity, document the blocker in your output and exit; a human will read the log.
1946
- 3. Formation supplies its own rule set (see ## Role / ## Task below), so the project CLAUDE.md
1947
- is not the active instruction source for this run. Rely on the content in this prompt.
1948
- 4. Produce structured output when finished.
1949
- 5. On unrecoverable errors: describe what failed and exit. Do not attempt open-ended retries.
1950
- 6. **Git is restricted to the workflow's own commit step — nothing else.** Exactly two git commands
1951
- are permitted, and only when the ## Workflow section below explicitly lists them as a step:
1952
- the "git add ..." staging command and the "git commit --no-verify -m <message>" that follows it,
1953
- run EXACTLY as shown in that workflow step — stage only what that step specifies, never more —
1954
- on the branch this session started on, without switching branches.
1955
- When the workflow lists that commit step you MUST execute it — this rule does not cancel it.
1956
- Every other git subcommand is forbidden: do not run git checkout, git branch, git merge,
1957
- git push, git rebase, git stash, git reset, or anything else. Branch, merge, and push management
1958
- is handled exclusively by the Formation Lead after the task completes, to prevent
1959
- cross-repository contamination when multiple agents run in parallel against different project paths.
1960
- 7. **Handle exactly the task specified in ## Task below.** Do not discover or claim other tasks.
1961
- One agent — one task.
1962
- 8. Do not read the task directory directly (.collaboration/tasks/ or .kenaz/tasks/). Use task-api.js.
1963
- 9. Only complete the task assigned to you (see ## Task for the Task ID). Do not mark other tasks done.
1964
-
1965
- `;
1966
539
  // TASK_1833: rules 12-14 are shared VERBATIM across all sub-agent prefixes
1967
540
  // (worktree / direct-write / cross-project-worktree). The only variance is
1968
541
  // which rule number the commit instruction lives under. Kept as one template
1969
542
  // so the three prefixes cannot drift apart (coupling rule: no near-duplicate
1970
543
  // parallel impls).
1971
544
  const subagentAsyncRules = (commitRuleNo) => `12. **⚠️ 你送出訊息就等於結束本次執行。** 你的最終訊息**就是**回傳值 —— 沒有「稍後回來接手」這回事。不要規劃「先讓出控制權、等背景工作完成再繼續」,那會直接結束你的執行並留下無人收拾的背景行程。
1972
- 13. **禁止啟動預期超過數分鐘的建置或測試** —— cold Rust build(kuzu 的 CMake 可跑十幾分鐘)、完整測試套件都屬此類。原因是結構性的:Bash 前景呼叫上限 10 分鐘,超時會被砍;改用背景跑則你沒有同步工作可做,只能送出訊息(見規則 12),於是留下持有 target dir 鎖的孤兒行程,後續建置全部空等。
545
+ 13. **禁止啟動預期超過數分鐘的建置或測試** —— cold Rust build(可跑十幾分鐘)、完整測試套件都屬此類。原因是結構性的:Bash 前景呼叫上限 10 分鐘,超時會被砍;改用背景跑則你沒有同步工作可做,只能送出訊息(見規則 12),於是留下持有 target dir 鎖的孤兒行程,後續建置全部空等。
1973
546
  - **正確做法**:改完程式碼 → commit(規則 ${commitRuleNo})→ **直接回報結束**。在報告中寫明「未執行建置驗證」。
1974
547
  - **Rust 建置驗證一律由主線程(Ansuz)負責** —— 主線程有熱快取、能跨 turn 輪詢、能診斷並清理孤兒行程。這是分工,不是你偷懶。
1975
548
  - 這**不代表**你不必自我檢查。讀周邊程式碼、確認語法與型別、小範圍 grep 比對既有實作,這些輕量檢查仍然必須做。被禁止的只有「耗時數分鐘以上的建置與完整測試套件」。
@@ -1988,12 +561,11 @@ const subagentAsyncRules = (commitRuleNo) => `12. **⚠️ 你送出訊息就等
1988
561
  // separate from subagentAsyncRules() above: that one still backs
1989
562
  // SUBAGENT_PREFIX_DIRECT verbatim (untouched, out of TASK_1978 scope).
1990
563
  const WORKTREE_DISPATCH_HARD_RULES = `2. CLAUDE.md 生效;完成輸出結果;卡住回報錯誤並結束。
1991
- 3. 改完立刻 commit(## Workflow,已排除 telemetry)-不等驗證;禁 push/切 branch/刪 worktree/stash/reset --hard。
564
+ 3. 改完立刻 commit(## Commit,已排除 telemetry)-不等驗證;禁 push/切 branch/刪 worktree/stash/reset --hard。
1992
565
  4. 送出訊息=結束;禁長建置/全測試(commit 後報未驗證,Rust 交 Ansuz);禁 sleep 輪詢;禁用 image name 殺行程(只殺自己 PID)。
1993
566
  5. 只做指定任務;不讀任務目錄;只用 task-api.js;worktree 禁 complete。`;
1994
567
 
1995
- // Sub-agent dispatch preamble — used when task is dispatched via Agent tool (isolation=worktree)
1996
- // instead of claude -p (headless). Key differences from HEADLESS_PREFIX:
568
+ // Sub-agent dispatch preamble — used when a task is dispatched via the Agent tool (isolation=worktree):
1997
569
  // - Git IS allowed (worktree provides isolation; commits land on task branch, not user branch)
1998
570
  // - CLAUDE.md IS active (running inside an interactive Claude Code session)
1999
571
  // - Agent must commit changes to the worktree before returning
@@ -2049,135 +621,22 @@ function buildSubagentWorktreePrefix(worktreePath, branchName, projectPath) {
2049
621
  return `[CROSS-PROJECT WORKTREE SUBAGENT MODE] wt=${worktreePath} repo=${projectPath} branch=${branchName}
2050
622
  1. 先印摘要「📋 {TASK_ID}: {你準備做什麼}」,直接做,不問。
2051
623
  ${WORKTREE_DISPATCH_HARD_RULES}
2052
- 6. 工作目錄=上述 worktree;Edit/Write、cd/git -C 皆用它;禁編輯主樹;Synapse projectPath 用 ${projectPath}。
624
+ 6. 工作目錄=上述 worktree;Edit/Write、cd/git -C 皆用它;禁編輯主樹。
2053
625
  Details: .formation/tools/dispatch-detail/cross-project-worktree.md (Read only if needed)
2054
626
  `;
2055
627
  }
2056
628
 
2057
- const LEGION_SYSTEM_PREFIX = `You are the TEAM LEAD using Claude Code's OFFICIAL "agent teams" feature.
2058
-
2059
- YOUR ROLE: Team Lead
2060
- - You are RESPONSIBLE for this task's success or failure.
2061
- - You MUST coordinate teammates and ensure they complete their work.
2062
- - You CANNOT end the session until ALL teammates have reported back.
2063
- - If you end early, the task FAILS and it's YOUR fault.
2064
-
2065
- This is NOT the project's .formation/ directory system. Do NOT read .formation/ files.
2066
- To create teammates, use natural language like: "Create a teammate named X who handles Y"
2067
- Each teammate is a separate Claude instance running in parallel.
2068
-
2069
- `;
2070
-
2071
- const LEGION_AUTO_EXECUTE = `
2072
-
2073
- BEFORE YOU START - OUTPUT YOUR PLAN:
2074
- First, explain exactly what you will do step by step:
2075
- 1. What teammates will you create?
2076
- 2. How will you wait for their results?
2077
- 3. What tools will you use to poll for messages?
2078
- 4. When will you know all teammates are done?
2079
-
2080
- RULES:
2081
- 1. Do NOT ask questions. Execute immediately after stating your plan.
2082
- 2. Do NOT read CLAUDE.md - this is a separate system.
2083
- 3. Create teammates using natural language descriptions.
2084
- 4. IMPORTANT: Use "sonnet" model for ALL teammates to reduce cost.
2085
-
2086
- WAITING FOR TEAMMATES - CRITICAL:
2087
- 5. After spawning ALL teammates, you MUST enter a polling loop.
2088
- 6. In the loop: call "sleep 5" with Bash, then check if teammates sent messages.
2089
- 7. Teammates send results via SendMessage - you will see them as incoming user messages.
2090
- 8. Keep looping (sleep + check) until you have received messages from ALL teammates.
2091
- 9. Do NOT end until you have ACTUAL CONTENT from every single teammate.
2092
- 10. If a teammate is slow, keep waiting - do NOT give up or timeout early.
2093
- 11. Only after ALL teammates report back, synthesize their findings into final answer.`;
2094
-
2095
- // ─────────────────────────────────────────
2096
- // Legion Lead Prompt 組裝
2097
- // ─────────────────────────────────────────
2098
-
2099
- /**
2100
- * 組裝 Legion Lead 的完整 headless prompt
2101
- * 複製 Sunny teammate 的成功模式:
2102
- * SYSTEM_PREFIX + 成員列表(自然語言)+ 任務 + AUTO_EXECUTE
2103
- */
2104
- function buildLegionLeadPrompt(config) {
2105
- let prompt = LEGION_SYSTEM_PREFIX;
2106
-
2107
- prompt += `Create an agent team with ${config.members.length} teammates:\n`;
2108
- config.members.forEach((m, i) => {
2109
- const lines = m.prompt.split('\n');
2110
- const roleIdx = lines.findIndex(l => l.startsWith('## Role'));
2111
- let desc = m.name;
2112
- if (roleIdx >= 0) {
2113
- for (let j = roleIdx + 1; j < lines.length && j < roleIdx + 5; j++) {
2114
- const line = lines[j].trim();
2115
- if (line && !line.startsWith('#')) { desc = line; break; }
2116
- }
2117
- }
2118
- prompt += `${i + 1}. "${m.name}" - ${desc}\n`;
2119
- });
2120
- prompt += '\n';
2121
-
2122
- const taskMatch = config.lead.prompt.match(/## Task\n([\s\S]*?)(?=\n##|$)/);
2123
- if (taskMatch) {
2124
- prompt += `Task: ${taskMatch[1].trim()}\n`;
2125
- }
2126
-
2127
- prompt += LEGION_AUTO_EXECUTE;
2128
- return prompt;
2129
- }
2130
-
2131
629
  module.exports = {
2132
630
  recordSkillUsage,
2133
- buildPrompt, buildSquadSynthesisPrompt, buildSquadDebatePrompts,
2134
- buildLegionLeadPrompt,
2135
- resolveModules,
2136
- HEADLESS_PREFIX,
631
+ buildPrompt,
2137
632
  SUBAGENT_PREFIX,
2138
633
  SUBAGENT_PREFIX_DIRECT,
2139
634
  buildSubagentWorktreePrefix,
2140
- // TASK_1978: exported for direct unit-testing of the compressed ceremony
2141
- // sections (byte budget + hard-constraint presence), without exercising
2142
- // the full buildUnitPrompt pipeline (which touches memory/cortex/episodic
2143
- // context and needs a live project).
2144
- appendCognitiveCheckpoint,
2145
- // TASK_1992: exported for direct unit-testing of the Alaya process-lessons
2146
- // section with an injected mcpCaller stub (no live MCP dependency).
2147
- buildTaskContextQuery,
2148
- appendAlayaLessons,
2149
- // TASK_2101: exported for direct unit-testing of the absolute-relevance
2150
- // gate (cosine floor + stack match) that appendAlayaLessons applies before
2151
- // any bija is rendered into the dispatch prompt.
2152
- deriveTaskStack,
2153
- deriveBijaStack,
2154
- passesAlayaRelevanceGate,
2155
- ALAYA_MIN_COSINE_DEFAULT,
2156
- ALAYA_STACK_OVERRIDE_COSINE,
2157
- ALAYA_CANDIDATE_POOL,
2158
- // TASK_1998: exported for dispatch.js (Related Files Preview gating,
2159
- // prompt-byte-stats telemetry) and for direct unit-testing of the
2160
- // two-axis prompt policy (context volume by complexity, process
2161
- // strictness by rigor).
2162
635
  promptProfile,
2163
636
  extractRigor,
2164
637
  extractComplexity,
2165
638
  splitPromptSections,
2166
- // TASK_1998 R2 follow-up: exported for direct unit-testing of Known
2167
- // Failures/Project Memory rigor gating against the real .failure/.memory
2168
- // dirs (no live-project mocking needed).
2169
- appendMemoryContext,
2170
- // TASK_2082: exported for direct unit-testing of the Rust-check reminder
2171
- // gate (scope detection + core-crate existence + isolated-worktree gate)
2172
- // without needing a live dashboard/src-tauri checkout on the test machine.
2173
639
  hasRustScope,
2174
640
  appendRustCheckReminder,
2175
- // TASK_2082: exported so prompt.test.js can exercise the commit-step +
2176
- // Rust-check-reminder wiring directly, without paying for buildUnitPrompt's
2177
- // other live-project-dependent appends (memory/cortex/episodic context).
2178
- appendWorkflow,
2179
- // TASK_2082 defect fix: exported for dispatch.js to call at the prefix +
2180
- // body composition point (rewrites hard rule 4's wording in-place instead
2181
- // of relying on "later instructions win"), and for direct unit-testing.
2182
641
  applyRustCheckToPrefix,
2183
642
  };