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.
- package/core/.claude/commands/Ansuz.md +36 -35
- package/core/.claude/hooks/__tests__/task-2395-project-registration.test.js +84 -0
- package/core/.claude/hooks/_ansuz-conversation.js +1 -0
- package/core/.claude/hooks/_ansuz-conversation.test.js +6 -0
- package/core/.claude/hooks/_run-user-prompt.js +0 -4
- package/core/.claude/hooks/_run-user-prompt.test.js +2 -3
- package/core/.claude/hooks/handoff-staleness.js +2 -2
- package/core/.claude/hooks/turn-context.js +50 -2
- package/core/.claude/settings.json +0 -37
- package/core/.collaboration/core/task-api.js +4 -12
- package/core/.formation/README.md +21 -146
- package/core/.formation/bifrost/codex-doctor.js +37 -2
- package/core/.formation/bifrost/codex-doctor.test.js +18 -0
- package/core/.formation/bifrost/launch.js +1 -1
- package/core/.formation/bifrost/openai/codex-hook-shim.js +6 -0
- package/core/.formation/bifrost/openai/codex-hook-shim.test.js +11 -1
- package/core/.formation/bifrost/setup-codex.js +3 -0
- package/core/.formation/spec/task_operations.parity.test.js +0 -1
- package/core/.formation/tools/dashboard-models-chain.test.js +0 -6
- package/core/.formation/tools/dispatch-detail/cross-project-worktree.md +1 -9
- package/core/.formation/tools/dispatch-detail/worktree-subagent-mode.md +2 -2
- package/core/.formation/tools/dispatch.js +28 -549
- package/core/.formation/tools/mcp-allowlist.test.js +0 -1
- package/core/.formation/tools/post-complete/task2345.test.js +0 -5
- package/core/.formation/tools/pre-dispatch-snapshot.test.js +0 -100
- package/core/.formation/tools/prompt.js +66 -1607
- package/core/.formation/tools/prompt.test.js +43 -582
- package/core/.kenaz/claude-hooks-manifest.json +16 -16
- package/core/.specialist/Ansuz/RULES.md +6 -10
- package/core/.specialist/tools/__tests__/freya-cli-contract.test.js +0 -1
- package/core/.specialist/tools/__tests__/task-2218-boot-session-id.test.js +17 -1
- package/core/.specialist/tools/__tests__/task-2222-session-identity-interleave.test.js +17 -1
- package/core/.specialist/tools/__tests__/task-2254-handoff-archive-contract.test.js +6 -5
- package/core/.specialist/tools/ansuz-boot-fast.js +44 -100
- package/core/.specialist/tools/ansuz-boot-fast.test.js +45 -1
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dev/plans/ANSUZ_SLIM_PLAN.md +68 -0
- package/core/plugins/kenaz/commands/Amelia.md +1 -1
- package/core/plugins/kenaz/commands/Ansuz.md +36 -35
- package/core/plugins/kenaz/hooks/hooks.json +1 -5
- package/core/plugins/kenaz/hooks/kenaz-hook.js +0 -4
- package/core/plugins/kenaz/scripts/kenaz-mode.js +11 -1
- package/core/plugins/kenaz/scripts/kenaz.test.js +1 -1
- package/core/plugins/kenaz/scripts/sync.js +6 -2
- package/core/plugins/kenaz/scripts/ygg.js +13 -1
- package/core/plugins/kenaz/skills/product_manager/SKILL.md +12 -12
- package/lib/cli.js +47 -6
- package/lib/cli.test.js +24 -0
- package/lib/dashboard.js +10 -0
- package/package.json +11 -7
- package/vendor/install-kenaz-cli.js +18 -7
- package/core/.formation/tools/broadcast.js +0 -103
- package/core/.formation/tools/dispatch-dryrun.test.js +0 -54
- package/core/.formation/tools/dispatch-notifier.js +0 -84
- package/core/.formation/tools/formations.js +0 -1203
- package/core/.formation/tools/headless-branch-recovery.test.js +0 -64
- package/core/.formation/tools/headless-dryrun.test.js +0 -79
- package/core/.formation/tools/headless-v2.js +0 -659
- package/core/.formation/tools/headless.js +0 -1071
- package/core/.formation/tools/legacy-scheduler.js +0 -1994
- package/core/.formation/tools/logger.js +0 -273
- package/core/.formation/tools/memory-loader.js +0 -422
- package/core/.formation/tools/process-state.js +0 -33
- package/core/.formation/tools/remote-conflict.js +0 -98
- package/core/.formation/tools/runner.js +0 -1332
- package/core/.formation/tools/task-utils.js +0 -344
- package/core/.formation/tools/task2028-headless-worktree.test.js +0 -112
- package/core/.formation/tools/tests/headless-mcp-args-plumbing.test.js +0 -123
- package/core/.formation/tools/worktree-leak.test.js +0 -162
- package/core/.specialist/tools/__tests__/task-1863-boot-lease-reuse.test.js +0 -99
- package/core/.specialist/tools/__tests__/task-1977-banner-cap.test.js +0 -129
|
@@ -1,42 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
763
|
-
*
|
|
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
|
|
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
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
780
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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 (
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
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(##
|
|
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
|
|
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,
|
|
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
|
};
|