kenaz 0.3.10 → 0.3.12
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/skills/code_reviewer/SKILL.md +0 -1
- package/core/.claude/skills/code_reviewer/patterns.yaml +0 -8
- package/core/.claude/skills/dev_agent/SKILL.md +3 -6
- package/core/.claude/skills/heimdall/SKILL.md +5 -6
- package/core/.claude/skills/huginn/SKILL.md +11 -38
- package/core/.collaboration/core/task-2288-followups.test.js +2 -37
- package/core/.collaboration/core/task-2421-replica.test.js +229 -0
- package/core/.collaboration/core/task-api.js +223 -230
- package/core/.collaboration/core/task-api.test.js +1 -138
- package/core/.collaboration/core/task-replica.js +189 -0
- package/core/.formation/.merge-lock +1 -0
- package/core/.formation/bifrost/codex-doctor.js +12 -2
- package/core/.formation/bifrost/codex-doctor.test.js +15 -0
- package/core/.formation/bifrost/launch.js +2 -2
- package/core/.formation/bifrost/launch.test.js +2 -2
- package/core/.formation/bifrost/setup-codex.js +84 -16
- package/core/.formation/bifrost/shared/ansuz-boot-policy.js +3 -3
- package/core/.formation/bifrost/shared/codex-injection.js +3 -17
- package/core/.formation/bifrost/shared/consolidate.js +1 -51
- package/core/.formation/bifrost/sync.js +4 -5
- package/core/.formation/tools/__tests__/subagent-bifrost-guard.test.js +1 -1
- package/core/.formation/tools/dispatch.js +2 -2
- package/core/.formation/tools/kenaz-stats.js +1 -1
- package/core/.formation/tools/mcp-allowlist.test.js +5 -3
- package/core/.formation/tools/post-complete/bounded-http.js +1 -40
- package/core/.formation/tools/post-complete/bounded-http.test.js +2 -89
- package/core/.formation/tools/post-complete/cognitive.js +2 -119
- package/core/.formation/tools/post-complete/task2420.test.js +129 -0
- package/core/.formation/tools/post-complete/testing.js +3 -21
- package/core/.formation/tools/post-complete.js +131 -436
- package/core/.formation/tools/post-complete.test.js +0 -135
- package/core/.formation/tools/route-audit.test.js +8 -20
- package/core/.kenaz/specialist/memory_guard_fired.json +1 -1
- package/core/.kenaz/specialist/memory_guard_trigger_cache.json +1 -1
- package/core/.rule/INDEX.yaml +3 -3
- package/core/.rule/general/ddx-synapse-stack.yaml +11 -11
- package/core/.rule/general/specialist-identity-contract.yaml +5 -5
- package/core/.rule/general/synapse-code-intelligence.yaml +25 -111
- package/core/.rule/general/worktree-isolation.yaml +2 -2
- package/core/.rule/pipeline/task-execution.yaml +6 -9
- package/core/.specialist/Amelia/README.md +5 -5
- package/core/.specialist/Amelia/capability.yaml +10 -18
- package/core/.specialist/Amelia/knowledge.yaml +15 -66
- package/core/.specialist/Amelia/workflow.yaml +12 -21
- package/core/.specialist/Ansuz/capability.yaml +12 -87
- package/core/.specialist/Ansuz/concepts/autonomous_agenda.yaml +1 -1
- package/core/.specialist/Ansuz/concepts/autonomy_contract.yaml +2 -3
- package/core/.specialist/Ansuz/concepts/capability_pointers.yaml +1 -3
- package/core/.specialist/Ansuz/concepts/cognitive_fluency_contract.yaml +8 -21
- package/core/.specialist/Ansuz/concepts/cognitive_systems_overview.yaml +0 -4
- package/core/.specialist/Ansuz/concepts/cognitive_systems_turn_context.yaml +1 -1
- package/core/.specialist/Ansuz/concepts/completion_tracking.yaml +0 -1
- package/core/.specialist/Ansuz/concepts/deep_knowledge_pointers.yaml +0 -3
- package/core/.specialist/Ansuz/concepts/evolvable_guardrails.yaml +3 -3
- package/core/.specialist/Ansuz/concepts/fatigue_handoff.yaml +1 -7
- package/core/.specialist/Ansuz/concepts/guardrails_immutable.yaml +3 -4
- package/core/.specialist/Ansuz/concepts/identity.yaml +1 -2
- package/core/.specialist/Ansuz/concepts/phase9_verification.yaml +0 -1
- package/core/.specialist/Ansuz/concepts/phase_protocol.yaml +3 -4
- package/core/.specialist/Ansuz/core.yaml +3 -6
- package/core/.specialist/Ansuz/elixir.yaml +6 -6
- package/core/.specialist/Ansuz/exo_cortex_tools.yaml +5 -9
- package/core/.specialist/Ansuz/guards/blind_spot_guards.yaml +2 -2
- package/core/.specialist/Ansuz/guards/lesson_guards.yaml +6 -19
- package/core/.specialist/Ansuz/identity_contract.yaml +4 -4
- package/core/.specialist/Ansuz/knowledge.yaml +13 -40
- package/core/.specialist/Ansuz/modules/ddx.yaml +2 -2
- package/core/.specialist/Ansuz/modules/prophecy.yaml +19 -19
- package/core/.specialist/Ansuz/modules/pulse_empty.yaml +6 -17
- package/core/.specialist/Ansuz/tools/ansuz-handoff-snapshot.js +3 -102
- package/core/.specialist/Ansuz/tools/ansuz-sdk-runner.js +2 -44
- package/core/.specialist/Ansuz/workflow.yaml +32 -76
- package/core/.specialist/Forseti/core.yaml +2 -7
- package/core/.specialist/Forseti/rule.yaml +1 -1
- package/core/.specialist/Heimdall/SKILL.md +2 -3
- package/core/.specialist/Heimdall/capability.yaml +22 -45
- package/core/.specialist/Heimdall/core.yaml +24 -126
- package/core/.specialist/Heimdall/principles.yaml +8 -11
- package/core/.specialist/Heimdall/rule.yaml +2 -2
- package/core/.specialist/Huginn/capability.yaml +31 -52
- package/core/.specialist/Huginn/core.yaml +43 -123
- package/core/.specialist/Huginn/principles.yaml +5 -5
- package/core/.specialist/Huginn/rule.yaml +3 -9
- package/core/.specialist/REFLECTION_TO_CONTROL.md +3 -4
- package/core/.specialist/shared/guard_schema.json +1 -1
- package/core/.specialist/shared/principles.yaml +6 -6
- package/core/.specialist/tools/__tests__/rea-detect.test.js +10 -0
- package/core/.specialist/tools/phase-gate.js +10 -230
- package/core/.specialist/tools/phase-gate.test.js +2 -2
- package/core/.specialist/tools/prompt-files.test.js +1 -1
- package/core/.specialist/tools/rea-detect.js +6 -2
- package/core/.specialist/tools/session.js +2 -89
- package/core/.synapse/memory-shared.json.sync +1 -0
- package/core/.system/memory/README.md +3 -0
- package/core/CLAUDE.md +6 -7
- package/core/KENAZ_CORE_VERSION +1 -1
- package/core/dev/plans/CLI_FIRST_INSTALL_FLOW.md +75 -83
- package/core/dev/specs/peer/CODEX_MANAGED_DAEMON.md +54 -0
- package/core/dev/specs/peer/setup-codex-hooks.test.js +38 -0
- package/core/dev/spikes/task2145-evidence-20261004/backend-log-excerpt.txt +56 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph.shadow +0 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph.wal +0 -0
- package/core/dev/spikes/task2145-evidence-20261004/graph_corrupt_suspect.marker +1 -0
- package/core/plugins/kenaz/INVENTORY.md +1 -1
- package/core/plugins/kenaz/scripts/kenaz.test.js +70 -1
- package/core/plugins/kenaz/scripts/ygg.js +45 -4
- package/core/plugins/kenaz/skills/code_reviewer/SKILL.md +0 -1
- package/core/plugins/kenaz/skills/code_reviewer/patterns.yaml +0 -8
- package/core/plugins/kenaz/skills/dev_agent/SKILL.md +3 -6
- package/lib/cli.js +17 -9
- package/lib/cli.test.js +10 -0
- package/lib/dashboard.test.js +3 -3
- package/package.json +6 -6
- package/vendor/install-kenaz-cli.js +167 -16
- package/core/.claude/hooks/handoff-snapshot-on-decision.js +0 -188
- package/core/.claude/skills/synapse_maintain/SKILL.md +0 -84
- package/core/.collaboration/core/task-2264-single-after-capture.test.js +0 -61
- package/core/.formation/tools/alaya-gate.js +0 -198
- package/core/.formation/tools/post-complete/health.js +0 -464
- package/core/.formation/tools/post-complete/task2038.test.js +0 -246
- package/core/.formation/tools/post-complete/task2341.test.js +0 -74
- package/core/.rule/general/cortex-tools.yaml +0 -181
- package/core/.rule/general/memory-language.yaml +0 -67
- package/core/.rule/general/memory-source-priority.yaml +0 -207
- package/core/.rule/general/synapse-cognitive-fusion.yaml +0 -301
- package/core/.specialist/Ansuz/concepts/cognitive_systems_hippocampus.yaml +0 -14
- package/core/.specialist/Ansuz/concepts/write_verification.yaml +0 -26
- package/core/.specialist/Ansuz/modules/recall.yaml +0 -45
- package/core/.specialist/tools/MEMORY_RECALL_TRIGGERS.md +0 -156
- package/core/.specialist/tools/context-injection.js +0 -395
- package/core/dev/scripts/probe/hippocampus-concurrency.js +0 -247
|
@@ -131,51 +131,6 @@ function extractEpisodeFromTranscript(filePath){
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
// archiveEpisodeToHippocampus: create+append+close hippocampus episode via MCP HTTP.
|
|
135
|
-
// fail-open: any failure returns false without throwing.
|
|
136
|
-
// hippocampus_episode_append REQUIRES phase (entry_type is not a valid param — server
|
|
137
|
-
// rejects with isError and the episode closes empty). projectPath must be explicit:
|
|
138
|
-
// the MCP HTTP layer is stateless and defaults to the dashboard's current project.
|
|
139
|
-
async function archiveEpisodeToHippocampus(port,opts,extracted){
|
|
140
|
-
try{
|
|
141
|
-
var root=resolveRoot(opts);
|
|
142
|
-
var scope='Bifrost Codex session'+(opts.sessionId?' '+opts.sessionId:'');
|
|
143
|
-
var tags=['bifrost','codex','session_memory'];
|
|
144
|
-
var startRes=await mcpCall(port,'hippocampus_episode_start',{
|
|
145
|
-
scope:scope,
|
|
146
|
-
tags:tags,
|
|
147
|
-
source:'bifrost_codex',
|
|
148
|
-
projectPath:root
|
|
149
|
-
});
|
|
150
|
-
var startParsed=mcpToolJson(startRes);
|
|
151
|
-
if(!startParsed)return false;
|
|
152
|
-
var epId=startParsed.episode_id||startParsed.id;
|
|
153
|
-
if(!epId)return false;
|
|
154
|
-
var content=extracted?extracted.summary:'Codex session ended (no transcript).';
|
|
155
|
-
var ap1=await mcpCall(port,'hippocampus_episode_append',{
|
|
156
|
-
episode_id:epId,
|
|
157
|
-
content:content,
|
|
158
|
-
phase:'summary',
|
|
159
|
-
projectPath:root
|
|
160
|
-
});
|
|
161
|
-
if(!mcpToolJson(ap1))process.stderr.write('[bifrost:consolidate] episode append (summary) failed\n');
|
|
162
|
-
if(extracted&&extracted.runeHints&&extracted.runeHints.length){
|
|
163
|
-
var ap2=await mcpCall(port,'hippocampus_episode_append',{
|
|
164
|
-
episode_id:epId,
|
|
165
|
-
content:'Rune candidates: '+extracted.runeHints.join('; '),
|
|
166
|
-
phase:'observation',
|
|
167
|
-
projectPath:root
|
|
168
|
-
});
|
|
169
|
-
if(!mcpToolJson(ap2))process.stderr.write('[bifrost:consolidate] episode append (observation) failed\n');
|
|
170
|
-
}
|
|
171
|
-
await mcpCall(port,'hippocampus_episode_close',{episode_id:epId,projectPath:root});
|
|
172
|
-
return true;
|
|
173
|
-
}catch(e){
|
|
174
|
-
process.stderr.write('[bifrost:consolidate] hippocampus archive error: '+e.message+'\n');
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
134
|
// writeHandoff: write ISBAR handoff JSON to the per-session slot (TASK_1829).
|
|
180
135
|
// Last-writer-wins within a session's own slot only; other sessions' slots are
|
|
181
136
|
// untouched (per-session isolation replaced the old shared singular slot).
|
|
@@ -205,7 +160,7 @@ function writeHandoff(root,opts,extracted){
|
|
|
205
160
|
status:'session_ended'
|
|
206
161
|
},
|
|
207
162
|
background:{
|
|
208
|
-
context:'Codex session ended via Stop hook. Transcript tee
|
|
163
|
+
context:'Codex session ended via Stop hook. Transcript tee archived.',
|
|
209
164
|
turn_count:extracted?extracted.turnCount:0
|
|
210
165
|
},
|
|
211
166
|
assessment:{
|
|
@@ -259,10 +214,6 @@ async function runStopConsolidation(opts){
|
|
|
259
214
|
else if(tee&&typeof tee.close==='function')tee.close();
|
|
260
215
|
var extracted=null;
|
|
261
216
|
try{extracted=extractEpisodeFromTranscript(transcriptPath);}catch(ee){}
|
|
262
|
-
// Episode archive (fail-open)
|
|
263
|
-
var epOk=false;
|
|
264
|
-
try{epOk=await archiveEpisodeToHippocampus(port,opts||{},extracted);}catch(ae){}
|
|
265
|
-
if(!epOk){process.stderr.write('[bifrost:consolidate] hippocampus unavailable or error -- skipping episode.\n');}
|
|
266
217
|
// Handoff (fail-open)
|
|
267
218
|
try{writeHandoff(root,opts||{},extracted);}catch(he){}
|
|
268
219
|
// Heartbeat (fail-open)
|
|
@@ -279,7 +230,6 @@ module.exports={
|
|
|
279
230
|
mcpCall:mcpCall,
|
|
280
231
|
mcpToolJson:mcpToolJson,
|
|
281
232
|
extractEpisodeFromTranscript:extractEpisodeFromTranscript,
|
|
282
|
-
archiveEpisodeToHippocampus:archiveEpisodeToHippocampus,
|
|
283
233
|
writeHandoff:writeHandoff,
|
|
284
234
|
appendHeartbeat:appendHeartbeat,
|
|
285
235
|
runStopConsolidation:runStopConsolidation
|
|
@@ -57,15 +57,15 @@ function translate(content) {
|
|
|
57
57
|
// 4. Replace ToolSearch references (Codex doesn't have this)
|
|
58
58
|
result = result.replace(
|
|
59
59
|
/用 `ToolSearch "synapse"` 偵測/g,
|
|
60
|
-
'嘗試呼叫
|
|
60
|
+
'嘗試呼叫 kenaz_ping 偵測'
|
|
61
61
|
);
|
|
62
62
|
result = result.replace(
|
|
63
63
|
/ToolSearch 未找到 → 該 session 全程降級用 Grep\/Glob/g,
|
|
64
|
-
'
|
|
64
|
+
'kenaz_ping 失敗 → 該 session 全程降級用 shell 命令搜尋'
|
|
65
65
|
);
|
|
66
66
|
|
|
67
67
|
// 5. Replace Claude Code MCP tool name prefix
|
|
68
|
-
// mcp__kenaz-
|
|
68
|
+
// mcp__kenaz-dashboard__kenaz_task_list → kenaz_task_list (Codex uses short names)
|
|
69
69
|
result = result.replace(/mcp__kenaz-dashboard__/g, '');
|
|
70
70
|
|
|
71
71
|
// 6. Replace /Ansuz, /Amelia etc. slash command references
|
|
@@ -85,7 +85,6 @@ ${renderBootstrap({ contextual: true })}
|
|
|
85
85
|
## Project-Path 自動解析
|
|
86
86
|
|
|
87
87
|
當前工作目錄(cwd)即為專案根目錄。所有 MCP 工具呼叫的 \`projectPath\` 參數使用 cwd:
|
|
88
|
-
- \`synapse_recall({ query: "...", projectPath: "<cwd>" })\`
|
|
89
88
|
- \`kenaz_task_list({ projectPath: "<cwd>" })\`
|
|
90
89
|
- phase-gate.js 指令加上 \`--project-path=<cwd>\`
|
|
91
90
|
|
|
@@ -97,7 +96,7 @@ ${renderBootstrap({ contextual: true })}
|
|
|
97
96
|
|
|
98
97
|
本檔案由 Bifröst 從 CLAUDE.md 自動翻譯。以下是 Codex 特有的差異:
|
|
99
98
|
|
|
100
|
-
1. **MCP 工具**:透過 \`codex mcp add kenaz-dashboard\` 連接 Dashboard(由 setup-codex.js 自動完成)。所有
|
|
99
|
+
1. **MCP 工具**:透過 \`codex mcp add kenaz-dashboard\` 連接 Dashboard(由 setup-codex.js 自動完成)。所有 kenaz_* 工具可直接使用,不需要 \`mcp__kenaz-dashboard__\` 前綴。
|
|
101
100
|
2. **檔案操作**:Codex 使用 shell 命令進行檔案操作,而非 Claude Code 的 Read/Edit/Write 工具。
|
|
102
101
|
3. **互動式問題**:Codex 沒有 AskUserQuestion 工具,直接用文字詢問用戶即可。
|
|
103
102
|
4. **Specialist 系統**:若需啟動 Ansuz 等 Specialist,使用 \`node .formation/bifrost/launch.js --specialist Ansuz\`(自動注入 MCP + AGENTS.override.md)。
|
|
@@ -139,7 +139,7 @@ setModels({ dispatchModel: 'claude-fable-5', dispatchFamily: 'claude', dispatchM
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
// ── [7] TASK_1815 model mapping: every emitted model is an enum member ──────
|
|
142
|
-
// Covers the NATIVE ids from
|
|
142
|
+
// Covers the NATIVE ids from the retired Dashboard model list (models.ts) plus the missing-field
|
|
143
143
|
// fallback ('claude-sonnet-4-6', mirrors DEFAULT_DISPATCH_MODEL) and a haiku id
|
|
144
144
|
// (enum member with no dashboard source entry today — must still map).
|
|
145
145
|
const MAP_CASES = [
|
|
@@ -49,7 +49,7 @@ const DEFAULT_DISPATCH_MODEL = 'claude-sonnet-5-5';
|
|
|
49
49
|
* cannot express — the enum has FAMILY names only. So this translation is
|
|
50
50
|
* structurally LOSSY: claude-opus-5[1m] and claude-opus-4-8[1m] both become
|
|
51
51
|
* 'opus'. Callers must surface that loss, never treat it as equivalence.
|
|
52
|
-
* NOTE:
|
|
52
|
+
* NOTE: the retired Dashboard model list (models.ts) `family` ('claude'|'bifrost') is the
|
|
53
53
|
* ADAPTER family and is unrelated to this Claude sub-family mapping.
|
|
54
54
|
* @param {string} rawModel - dashboard model id, possibly with [1m]-style suffix
|
|
55
55
|
* @returns {{ model: string|null, lossless: boolean }} model=null → unmappable
|
|
@@ -62,7 +62,7 @@ function translateModelForAgentTool(rawModel) {
|
|
|
62
62
|
return { model: stripped, lossless: stripped === String(rawModel).trim() };
|
|
63
63
|
}
|
|
64
64
|
// claude-<family>[-version...] → <family>. Covers every NATIVE id in
|
|
65
|
-
//
|
|
65
|
+
// the retired Dashboard model list (models.ts) (claude-fable-5-1[1m], claude-opus-5-5[1m],
|
|
66
66
|
// claude-fable-5[1m], claude-opus-4-8[1m], claude-sonnet-5,
|
|
67
67
|
// claude-sonnet-4-6) plus future versions of the same families. 'haiku' has
|
|
68
68
|
// no dashboard source entry today but maps fine if one appears.
|
|
@@ -21,7 +21,7 @@ function openDb(file) {
|
|
|
21
21
|
try {
|
|
22
22
|
const { DatabaseSync } = require('node:sqlite');
|
|
23
23
|
return { db: new DatabaseSync(file, { readOnly: true }) };
|
|
24
|
-
} catch (e) { return { err: `${NO_DATA} (cannot read ${file}: ${e.code
|
|
24
|
+
} catch (e) { return { err: `${NO_DATA} (cannot read ${file}: ${['MODULE_NOT_FOUND', 'ERR_UNKNOWN_BUILTIN_MODULE'].includes(e.code) ? 'needs Node >= 22.5 for node:sqlite' : e.message})` }; }
|
|
25
25
|
}
|
|
26
26
|
function rows(db, sql, ...args) { try { return db.prepare(sql).all(...args); } catch (_) { return null; } }
|
|
27
27
|
|
|
@@ -11,7 +11,9 @@ const MCP_RS = path.join(ROOT, 'dashboard', 'src-tauri', 'src', 'infrastructure'
|
|
|
11
11
|
const ALLOWLIST_FILES = [
|
|
12
12
|
'.specialist/Ansuz/tools/ansuz-sdk-runner.js',
|
|
13
13
|
];
|
|
14
|
-
const REMOVED = ['synapse_associate', 'synapse_audit', 'synapse_apply_usage_boosts', 'synapse_pulse', 'synapse_thought_push', 'synapse_verify', 'cortex_search'
|
|
14
|
+
const REMOVED = ['synapse_associate', 'synapse_audit', 'synapse_apply_usage_boosts', 'synapse_pulse', 'synapse_thought_push', 'synapse_verify', 'cortex_search',
|
|
15
|
+
// TASK_2411: the memory system
|
|
16
|
+
'synapse_recall', 'synapse_inscribe', 'synapse_forge', 'synapse_status', 'hippocampus_episode_start', 'cortex_recall', 'alaya_recall'];
|
|
15
17
|
|
|
16
18
|
function dispatchedTools() {
|
|
17
19
|
const src = fs.readFileSync(MCP_RS, 'utf8');
|
|
@@ -22,9 +24,9 @@ function dispatchedTools() {
|
|
|
22
24
|
return new Set([...src.slice(start, end).matchAll(/^\s*"([a-z0-9_]+)"\s*=>/gm)].map(m => m[1]));
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
test('dispatcher tool list is non-trivial and contains the
|
|
27
|
+
test('dispatcher tool list is non-trivial and contains the LSP tools and none of the removed memory tools', () => {
|
|
26
28
|
const tools = dispatchedTools();
|
|
27
|
-
for (const t of ['
|
|
29
|
+
for (const t of ['kenaz_lsp_callers', 'kenaz_ping', 'alaya_reports_list']) assert.ok(tools.has(t), t);
|
|
28
30
|
for (const t of REMOVED) assert.ok(!tools.has(t), `${t} should not be dispatched`);
|
|
29
31
|
});
|
|
30
32
|
|
|
@@ -60,43 +60,4 @@ function getJson(endpoint, opts) {
|
|
|
60
60
|
return httpJson('GET', endpoint, undefined, opts);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
if (!response.ok) return { status: 'failed', reason: response.reason };
|
|
65
|
-
const rpc = response.data;
|
|
66
|
-
if (!rpc || rpc.error || rpc.id !== id || rpc.jsonrpc !== '2.0') return { status: 'failed', reason: 'rpc_error' };
|
|
67
|
-
const result = rpc.result;
|
|
68
|
-
if (!result || (result.isError != null && result.isError !== false) || !Array.isArray(result.content)) return { status: 'failed', reason: 'mcp_error' };
|
|
69
|
-
try {
|
|
70
|
-
const texts = result.content.filter(c => c && c.type === 'text');
|
|
71
|
-
if (texts.length !== 1) throw new Error('shape');
|
|
72
|
-
const data = JSON.parse(texts[0].text);
|
|
73
|
-
if (!data || data.error || data.success === false || data.gate_fired || data.symbol_id !== args.symbol_id) throw new Error('result');
|
|
74
|
-
if (data.outcome === 'SPOOLED' && typeof data.spool_id === 'string' && data.spool_id &&
|
|
75
|
-
typeof data.spool_file === 'string' && data.spool_file) return { status: 'spooled' };
|
|
76
|
-
if (!data.outcome && typeof data.rune_id === 'string' && data.rune_id && data.type === args.type) return { status: 'successful' };
|
|
77
|
-
} catch { /* No confirmed acknowledgement. */ }
|
|
78
|
-
return { status: 'failed', reason: 'unconfirmed_result' };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function inscribeBatch(items, { timeoutMs = 10000, request = postJson, endpoint = {} } = {}) {
|
|
82
|
-
const counts = { successful: 0, spooled: 0, failed: 0, skipped: 0, reason: null };
|
|
83
|
-
const deadline = Date.now() + timeoutMs;
|
|
84
|
-
for (let i = 0; i < items.length; i++) {
|
|
85
|
-
const remaining = deadline - Date.now();
|
|
86
|
-
if (remaining <= 0) { counts.reason = 'batch_deadline'; break; }
|
|
87
|
-
const id = `rune-extract-${deadline}-${i}`;
|
|
88
|
-
let verdict;
|
|
89
|
-
try {
|
|
90
|
-
const response = await request({ ...endpoint, path: '/api/mcp' }, {
|
|
91
|
-
jsonrpc: '2.0', id, method: 'tools/call', params: { name: 'synapse_inscribe', arguments: items[i] },
|
|
92
|
-
}, { timeoutMs: remaining });
|
|
93
|
-
verdict = classifyInscribe(response, id, items[i]);
|
|
94
|
-
} catch { verdict = { status: 'failed', reason: 'request_error' }; }
|
|
95
|
-
counts[verdict.status]++;
|
|
96
|
-
if (verdict.status === 'failed') { counts.reason = verdict.reason; break; }
|
|
97
|
-
}
|
|
98
|
-
counts.skipped = items.length - counts.successful - counts.spooled - counts.failed;
|
|
99
|
-
return counts;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
module.exports = { postJson, getJson, classifyInscribe, inscribeBatch };
|
|
63
|
+
module.exports = { postJson, getJson };
|
|
@@ -2,33 +2,7 @@
|
|
|
2
2
|
const { test } = require('node:test');
|
|
3
3
|
const assert = require('node:assert/strict');
|
|
4
4
|
const http = require('node:http');
|
|
5
|
-
const { postJson
|
|
6
|
-
|
|
7
|
-
test('passive extraction caps the batch at 20 and emits one truthful failure summary', async () => {
|
|
8
|
-
const fs = require('node:fs');
|
|
9
|
-
const vm = require('node:vm');
|
|
10
|
-
const sourcePath = require.resolve('./cognitive');
|
|
11
|
-
const localRequire = require('node:module').createRequire(sourcePath);
|
|
12
|
-
const mod = { exports: {} }, logs = [];
|
|
13
|
-
let batches = 0, requests = 0;
|
|
14
|
-
const ctx = { module: mod, console: { log: s => logs.push(s), warn: s => logs.push(s) },
|
|
15
|
-
require: name => name === './bounded-http' ? { inscribeBatch: async values => {
|
|
16
|
-
batches++;
|
|
17
|
-
assert.equal(values.length, 20);
|
|
18
|
-
assert.ok(values.every(v => v.projectPath === 'fixture-project'));
|
|
19
|
-
return inscribeBatch(values, { request: async () => { requests++; throw new Error('fixture'); } });
|
|
20
|
-
} } : localRequire(name),
|
|
21
|
-
};
|
|
22
|
-
vm.runInNewContext(fs.readFileSync(sourcePath, 'utf8'), ctx);
|
|
23
|
-
mod.exports.initCognitive({ getPostCompleteChangedFiles: () => Array.from({ length: 25 }, (_, i) => `file${i}.js`), stripCJK: s => s });
|
|
24
|
-
const result = await mod.exports.triggerPassiveRuneExtraction('fix: fixture', 'fixture-root', 'fixture-project');
|
|
25
|
-
assert.equal(batches, 1);
|
|
26
|
-
assert.equal(requests, 1);
|
|
27
|
-
assert.equal(result.failed, 1);
|
|
28
|
-
assert.equal(result.skipped, 19);
|
|
29
|
-
assert.equal(logs.length, 1);
|
|
30
|
-
assert.match(logs[0], /successful=0, spooled=0, failed=1, skipped=19/);
|
|
31
|
-
});
|
|
5
|
+
const { postJson } = require('./bounded-http');
|
|
32
6
|
|
|
33
7
|
async function server(t, handler) {
|
|
34
8
|
const s = http.createServer(handler);
|
|
@@ -36,54 +10,12 @@ async function server(t, handler) {
|
|
|
36
10
|
t.after(() => new Promise(resolve => { s.closeAllConnections(); s.close(resolve); }));
|
|
37
11
|
return { hostname: '127.0.0.1', port: s.address().port };
|
|
38
12
|
}
|
|
39
|
-
const items = () => [0, 1, 2].map(i => ({ symbol_id: `file${i}.js`, type: 'summary', projectPath: 'fixture-project', content: 'fixture' }));
|
|
40
|
-
function rpc(payload, data, isError = false) {
|
|
41
|
-
return { ok: true, data: { jsonrpc: '2.0', id: payload.id,
|
|
42
|
-
result: { content: [{ type: 'text', text: JSON.stringify(data) }], isError } } };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
test('written and server-spooled acknowledgements are separate; project routing survives', async () => {
|
|
46
|
-
let calls = 0;
|
|
47
|
-
const result = await inscribeBatch(items(), { request: async (_endpoint, payload) => {
|
|
48
|
-
const args = payload.params.arguments;
|
|
49
|
-
assert.equal(args.projectPath, 'fixture-project');
|
|
50
|
-
calls++;
|
|
51
|
-
if (calls === 1) return rpc(payload, { rune_id: 'rune_one', symbol_id: args.symbol_id, type: args.type });
|
|
52
|
-
if (calls === 2) return rpc(payload, { outcome: 'SPOOLED', spool_id: 'spool_one', spool_file: 'fixture-spool.json', symbol_id: args.symbol_id });
|
|
53
|
-
return rpc(payload, { error: 'rejected' }, true);
|
|
54
|
-
} });
|
|
55
|
-
assert.deepEqual(result, { successful: 1, spooled: 1, failed: 1, skipped: 0, reason: 'mcp_error' });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
for (const fault of ['transport', 'rpc', 'mcp', 'text_json', 'unknown', 'gate', 'spool_without_receipt', 'wrong_id']) {
|
|
59
|
-
test('first ' + fault + ' failure stops batch without marking unsent writes successful', async () => {
|
|
60
|
-
let calls = 0;
|
|
61
|
-
const result = await inscribeBatch(items(), { request: async (_endpoint, payload) => {
|
|
62
|
-
calls++;
|
|
63
|
-
if (fault === 'transport') throw new Error('fixture transport');
|
|
64
|
-
if (fault === 'rpc') return { ok: true, data: { error: { code: -32000 } } };
|
|
65
|
-
if (fault === 'mcp') return rpc(payload, {}, true);
|
|
66
|
-
if (fault === 'text_json') { const r = rpc(payload, {}); r.data.result.content[0].text = 'bad'; return r; }
|
|
67
|
-
if (fault === 'gate') return rpc(payload, { gate_fired: true, rune_id: 'existing', symbol_id: 'file0.js', type: 'summary' });
|
|
68
|
-
if (fault === 'spool_without_receipt') return rpc(payload, { outcome: 'SPOOLED', symbol_id: 'file0.js' });
|
|
69
|
-
if (fault === 'wrong_id') { const r = rpc(payload, { rune_id: 'one', symbol_id: 'file0.js', type: 'summary' }); r.data.id = 'other'; return r; }
|
|
70
|
-
return rpc(payload, { success: true });
|
|
71
|
-
} });
|
|
72
|
-
assert.equal(calls, 1);
|
|
73
|
-
assert.equal(result.successful, 0);
|
|
74
|
-
assert.equal(result.spooled, 0);
|
|
75
|
-
assert.equal(result.failed, 1);
|
|
76
|
-
assert.equal(result.skipped, 2);
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
13
|
test('HTTP failure and invalid JSON fail closed', async t => {
|
|
81
14
|
let calls = 0;
|
|
82
15
|
const endpoint = await server(t, (_req, res) => { calls++; res.writeHead(503); res.end('{}'); });
|
|
83
|
-
const result = await
|
|
16
|
+
const result = await postJson(endpoint, {});
|
|
84
17
|
assert.equal(calls, 1);
|
|
85
18
|
assert.equal(result.reason, 'http_503');
|
|
86
|
-
assert.equal(result.skipped, 2);
|
|
87
19
|
const malformed = await server(t, (_req, res) => res.end('not-json'));
|
|
88
20
|
assert.deepEqual(await postJson(malformed, {}), { ok: false, reason: 'invalid_json' });
|
|
89
21
|
});
|
|
@@ -109,22 +41,3 @@ test('body size is bounded and disconnected response fails once', async t => {
|
|
|
109
41
|
assert.equal(result.ok, false);
|
|
110
42
|
assert.match(result.reason, /transport_error|response_aborted|response_error/);
|
|
111
43
|
});
|
|
112
|
-
|
|
113
|
-
test('batch uses one deadline and serial requests; timeout skips remaining items', async t => {
|
|
114
|
-
let calls = 0;
|
|
115
|
-
const endpoint = await server(t, (req, res) => {
|
|
116
|
-
calls++;
|
|
117
|
-
let body = '';
|
|
118
|
-
req.on('data', b => { body += b; });
|
|
119
|
-
req.on('end', () => {
|
|
120
|
-
if (calls !== 1) return;
|
|
121
|
-
const payload = JSON.parse(body);
|
|
122
|
-
setTimeout(() => res.end(JSON.stringify(rpc(payload, { rune_id: 'one', symbol_id: 'file0.js', type: 'summary' }).data)), 20);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
const start = Date.now();
|
|
126
|
-
const result = await inscribeBatch(items(), { endpoint, timeoutMs: 100 });
|
|
127
|
-
assert.deepEqual(result, { successful: 1, spooled: 0, failed: 1, skipped: 1, reason: 'timeout' });
|
|
128
|
-
assert.equal(calls, 2);
|
|
129
|
-
assert.ok(Date.now() - start < 1000);
|
|
130
|
-
});
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* COGNITIVE — extracted from post-complete.js (D.4 refactor)
|
|
3
3
|
*
|
|
4
|
-
* Cognitive checkpoint detection
|
|
5
|
-
*
|
|
6
|
-
* and cognitive competence map.
|
|
4
|
+
* Cognitive checkpoint detection and thinking tags. (Passive rune extraction and the
|
|
5
|
+
* Hippocampus MCP sync were removed with the memory system in TASK_2411.)
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
const path = require('path');
|
|
10
9
|
const fs = require('fs');
|
|
11
|
-
const { execSync, spawnSync } = require('child_process');
|
|
12
10
|
const { ROOT, COLLABORATION_API, LOG_DIR, getLogDir } = require('../utils');
|
|
13
11
|
// TASK_2275: kenaz-core ships without node_modules; fall back to the vendored copy.
|
|
14
12
|
const yaml = (() => { try { return require('js-yaml'); } catch { return require('../../../.vendor/js-yaml.cjs'); } })();
|
|
@@ -147,124 +145,9 @@ function collectThinkingTags(taskId, projectRoot) {
|
|
|
147
145
|
return { tags: ordered.slice(0, 3), truncated, totalRaw };
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
/**
|
|
151
|
-
* Passive Rune Extraction: mine knowledge from git commit messages.
|
|
152
|
-
* Background bounded batch: callers may await its result; no retries.
|
|
153
|
-
* Called after a successful commit (gitResult.committed === true).
|
|
154
|
-
*
|
|
155
|
-
* Behavior:
|
|
156
|
-
* 1. Parse commit type from message prefix (fix/feat/refactor)
|
|
157
|
-
* 2. Get changed file list via git diff HEAD~1 HEAD
|
|
158
|
-
* 3. Call synapse_inscribe for each file (max 20) via MCP HTTP
|
|
159
|
-
*
|
|
160
|
-
* @param {string} commitMsg - full commit message
|
|
161
|
-
* @param {string} projectRoot - absolute path to git repo root
|
|
162
|
-
* @param {string|null} projectPath - passed through to synapse_inscribe
|
|
163
|
-
*/
|
|
164
|
-
function triggerPassiveRuneExtraction(commitMsg, projectRoot, projectPath) {
|
|
165
|
-
// 1. Parse commit type
|
|
166
|
-
const typeMatch = commitMsg.match(/^(fix!?|feat!?|refactor)/i);
|
|
167
|
-
let runeType, contentPrefix;
|
|
168
|
-
if (typeMatch) {
|
|
169
|
-
const t = typeMatch[1].toLowerCase();
|
|
170
|
-
if (t.startsWith('fix')) {
|
|
171
|
-
runeType = 'risk';
|
|
172
|
-
contentPrefix = 'Fixed';
|
|
173
|
-
} else if (t.startsWith('feat')) {
|
|
174
|
-
runeType = 'summary';
|
|
175
|
-
contentPrefix = 'Added';
|
|
176
|
-
} else {
|
|
177
|
-
runeType = 'decision';
|
|
178
|
-
contentPrefix = 'Refactored';
|
|
179
|
-
}
|
|
180
|
-
} else {
|
|
181
|
-
runeType = 'summary';
|
|
182
|
-
contentPrefix = 'Added';
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// 2. Get changed files (post-commit diff)
|
|
186
|
-
const changedFiles = getPostCompleteChangedFiles(projectRoot, null).slice(0, 20);
|
|
187
|
-
|
|
188
|
-
if (changedFiles.length === 0) return;
|
|
189
|
-
|
|
190
|
-
// One bounded batch; completion counts are acknowledgements, never requests fired.
|
|
191
|
-
const { inscribeBatch } = require('./bounded-http');
|
|
192
|
-
const runeContent = `${contentPrefix}: ${stripCJK(commitMsg)}`;
|
|
193
|
-
const items = changedFiles.map(filePath => ({
|
|
194
|
-
symbol_id: filePath, type: runeType, content: runeContent, confidence: 0.65,
|
|
195
|
-
// TASK_2025: synapse_inscribe is a write tool -- must not omit projectPath
|
|
196
|
-
// (mcp.rs resolve_project_for_write no longer falls back to the
|
|
197
|
-
// Dashboard's currently-selected project). Fall back to ROOT (this
|
|
198
|
-
// script's own core project) rather than dropping the param.
|
|
199
|
-
projectPath: projectPath || ROOT,
|
|
200
|
-
}));
|
|
201
|
-
return inscribeBatch(items).then(counts => {
|
|
202
|
-
console.log(` [Rune extraction] successful=${counts.successful}, spooled=${counts.spooled}, failed=${counts.failed}, skipped=${counts.skipped} (${runeType}${counts.reason ? '; ' + counts.reason : ''}; failed writes are unconfirmed)`);
|
|
203
|
-
return counts;
|
|
204
|
-
}).catch(() => {
|
|
205
|
-
console.warn(' [Rune extraction] batch failed; completion not confirmed');
|
|
206
|
-
return null;
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// TASK_2341: Cognitive Attribution (POST /api/synapse/link-cognitive -> COGNITIVE_APPLIES_TO graph
|
|
211
|
-
// edges) was removed with the Kuzu graph; the route no longer exists.
|
|
212
|
-
|
|
213
|
-
// ─────────────────────────────────────────
|
|
214
|
-
// Hippocampus Episode Integration
|
|
215
|
-
// ─────────────────────────────────────────
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Call a hippocampus MCP tool synchronously via the Dashboard HTTP endpoint.
|
|
219
|
-
* Uses the same spawnSync pattern as measureHealthScoreSync.
|
|
220
|
-
* Returns parsed JSON result, or null if the dashboard is not running / timed out.
|
|
221
|
-
* @param {string} toolName - e.g. 'hippocampus_episode_start' | 'hippocampus_episode_close'
|
|
222
|
-
* @param {object} toolArgs - arguments forwarded to the tool
|
|
223
|
-
* @returns {object|null}
|
|
224
|
-
*/
|
|
225
|
-
function callHippocampusMcpSync(toolName, toolArgs) {
|
|
226
|
-
const script = [
|
|
227
|
-
"const http = require('http');",
|
|
228
|
-
`const payload = JSON.stringify({ jsonrpc: '2.0', id: 'hipp-sync-' + Date.now(), method: 'tools/call',`,
|
|
229
|
-
` params: { name: ${JSON.stringify(toolName)}, arguments: ${JSON.stringify(toolArgs)} } });`,
|
|
230
|
-
"const req = http.request({ hostname: '127.0.0.1', port: 18080, path: '/api/mcp',",
|
|
231
|
-
" method: 'POST',",
|
|
232
|
-
" headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },",
|
|
233
|
-
" timeout: 8000 }, (res) => {",
|
|
234
|
-
" let b = '';",
|
|
235
|
-
" res.on('data', c => b += c);",
|
|
236
|
-
" res.on('end', () => {",
|
|
237
|
-
" try {",
|
|
238
|
-
" const r = JSON.parse(b);",
|
|
239
|
-
" const t = r && r.result && r.result.content && r.result.content[0] && r.result.content[0].text;",
|
|
240
|
-
" if (t) { process.stdout.write(t); }",
|
|
241
|
-
" } catch(e) { process.stderr.write(e.message); }",
|
|
242
|
-
" });",
|
|
243
|
-
"});",
|
|
244
|
-
"req.on('error', () => {});",
|
|
245
|
-
"req.on('timeout', () => req.destroy());",
|
|
246
|
-
"req.write(payload);",
|
|
247
|
-
"req.end();",
|
|
248
|
-
].join('\n');
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
const res = spawnSync(process.execPath, ['-e', script], {
|
|
252
|
-
encoding: 'utf8',
|
|
253
|
-
timeout: 12000,
|
|
254
|
-
windowsHide: true,
|
|
255
|
-
});
|
|
256
|
-
if (res.stdout && res.stdout.trim()) {
|
|
257
|
-
try { return JSON.parse(res.stdout.trim()); } catch { return { raw: res.stdout.trim() }; }
|
|
258
|
-
}
|
|
259
|
-
} catch { /* dashboard not running or timeout — graceful degradation */ }
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
148
|
module.exports = {
|
|
264
149
|
initCognitive,
|
|
265
150
|
parseCognitiveCheckpoint,
|
|
266
151
|
detectCognitiveCheckpoint,
|
|
267
152
|
collectThinkingTags,
|
|
268
|
-
triggerPassiveRuneExtraction,
|
|
269
|
-
callHippocampusMcpSync,
|
|
270
153
|
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TASK_2420 — runtime/config-only completion and truthful outcome reporting.
|
|
3
|
+
*
|
|
4
|
+
* Drives the REAL task-api.js CLI (`complete`) against a temp git repo with a temp
|
|
5
|
+
* .kenaz/tasks store. Never touches ~/.kenaz, ~/.claude or ~/.codex.
|
|
6
|
+
*/
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const test = require('node:test');
|
|
10
|
+
const assert = require('node:assert/strict');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync, spawnSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
17
|
+
const TASK_API = path.join(ROOT, '.collaboration', 'core', 'task-api.js');
|
|
18
|
+
const { summarizePostComplete, spawnFailureCause } = require('../post-complete');
|
|
19
|
+
|
|
20
|
+
function git(cmd, cwd) {
|
|
21
|
+
return execSync(cmd, { cwd, encoding: 'utf8', timeout: 20000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function setup(taskId, scopePaths = []) {
|
|
25
|
+
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tn2420-')));
|
|
26
|
+
const repo = path.join(tmp, 'repo');
|
|
27
|
+
fs.mkdirSync(repo);
|
|
28
|
+
git('git init -b userwork', repo);
|
|
29
|
+
git('git config user.email t@t.com', repo);
|
|
30
|
+
git('git config user.name Test', repo);
|
|
31
|
+
git('git config commit.gpgsign false', repo);
|
|
32
|
+
fs.writeFileSync(path.join(repo, 'README.md'), 'init\n');
|
|
33
|
+
fs.writeFileSync(path.join(repo, '.gitignore'), '.kenaz/\n');
|
|
34
|
+
git('git add README.md .gitignore', repo);
|
|
35
|
+
git('git commit --no-verify -m init', repo);
|
|
36
|
+
const dir = path.join(repo, '.kenaz', 'tasks', 'active');
|
|
37
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
38
|
+
fs.writeFileSync(path.join(dir, `${taskId}.yaml`), [
|
|
39
|
+
`id: "${taskId}"`, 'title: "runtime repair"', 'status: in_progress', 'priority: P2',
|
|
40
|
+
scopePaths.length ? `scope_paths:\n${scopePaths.map(p => ` - "${p}"`).join('\n')}` : 'scope_paths: []', '',
|
|
41
|
+
].join('\n'));
|
|
42
|
+
return { tmp, repo };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function complete(repo, taskId, extra = []) {
|
|
46
|
+
return spawnSync('node', [TASK_API, '--project-path', repo, 'complete', taskId, '--skip-archive', ...extra], {
|
|
47
|
+
cwd: ROOT, encoding: 'utf8', timeout: 120000, windowsHide: true,
|
|
48
|
+
env: { ...process.env, KENAZ_INTEGRATION_QUEUE: '' },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const where = (repo, taskId) => ['done', 'active', 'blocked']
|
|
53
|
+
.filter(d => fs.existsSync(path.join(repo, '.kenaz', 'tasks', d, `${taskId}.yaml`)));
|
|
54
|
+
|
|
55
|
+
test('TASK_2420: no-branch task with --no-source completes; git merge/verify reported as skipped', () => {
|
|
56
|
+
const taskId = 'TASK_24201';
|
|
57
|
+
const { tmp, repo } = setup(taskId);
|
|
58
|
+
try {
|
|
59
|
+
const r = complete(repo, taskId, ['--no-source']);
|
|
60
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
61
|
+
assert.equal(r.status, 0, out);
|
|
62
|
+
assert.match(r.stdout, /任務已完成/);
|
|
63
|
+
assert.match(out, /no branch: nothing to integrate/);
|
|
64
|
+
assert.doesNotMatch(out, /Git merge failed/);
|
|
65
|
+
assert.deepEqual(where(repo, taskId), ['done']);
|
|
66
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('TASK_2420: --no-source is refused when the task branch has unmerged commits (work is not silently dropped)', () => {
|
|
70
|
+
const taskId = 'TASK_24202';
|
|
71
|
+
const { tmp, repo } = setup(taskId);
|
|
72
|
+
try {
|
|
73
|
+
git(`git checkout -b task/${taskId.toLowerCase()}`, repo);
|
|
74
|
+
fs.writeFileSync(path.join(repo, 'a.txt'), 'real work\n');
|
|
75
|
+
git('git add a.txt', repo);
|
|
76
|
+
git('git commit --no-verify -m work', repo);
|
|
77
|
+
git('git checkout userwork', repo);
|
|
78
|
+
const r = complete(repo, taskId, ['--no-source']);
|
|
79
|
+
assert.notEqual(r.status, 0, 'must not report success');
|
|
80
|
+
assert.doesNotMatch(r.stdout, /✅ 任務已完成/);
|
|
81
|
+
assert.match(r.stderr, /任務未完成/);
|
|
82
|
+
assert.match(r.stderr, /no_source_but_work_exists/);
|
|
83
|
+
assert.match(r.stderr, /exists with commits not on userwork/);
|
|
84
|
+
assert.ok(!where(repo, taskId).includes('done'), `card must not stay in done/: ${where(repo, taskId)}`);
|
|
85
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('TASK_2420: branch task whose merge fails is NOT reported as plain success and the cause is shown', () => {
|
|
89
|
+
const taskId = 'TASK_24203';
|
|
90
|
+
const { tmp, repo } = setup(taskId);
|
|
91
|
+
try {
|
|
92
|
+
// Conflicting edits: branch and userwork both change a.txt differently.
|
|
93
|
+
fs.writeFileSync(path.join(repo, 'a.txt'), 'base\n');
|
|
94
|
+
git('git add a.txt', repo); git('git commit --no-verify -m base', repo);
|
|
95
|
+
git(`git checkout -b task/${taskId.toLowerCase()}`, repo);
|
|
96
|
+
fs.writeFileSync(path.join(repo, 'a.txt'), 'branch side\n');
|
|
97
|
+
git('git commit --no-verify -am branch', repo);
|
|
98
|
+
git('git checkout userwork', repo);
|
|
99
|
+
fs.writeFileSync(path.join(repo, 'a.txt'), 'user side\n');
|
|
100
|
+
git('git commit --no-verify -am user', repo);
|
|
101
|
+
const r = complete(repo, taskId);
|
|
102
|
+
assert.notEqual(r.status, 0, `${r.stdout}\n${r.stderr}`);
|
|
103
|
+
assert.doesNotMatch(r.stdout, /✅ 任務已完成/);
|
|
104
|
+
assert.match(r.stderr, /任務未完成/);
|
|
105
|
+
assert.match(r.stderr, /git: failed — \S+/);
|
|
106
|
+
assert.ok(!where(repo, taskId).includes('done'), `card must not stay in done/: ${where(repo, taskId)}`);
|
|
107
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('TASK_2420: summarize — move/reconcile failures carry their real cause and card is flagged unreconciled', () => {
|
|
111
|
+
const s = summarizePostComplete({
|
|
112
|
+
git: { merged: false, taskBranch: 'task/x', failure_reason: 'merge_conflict', mergeErrorText: 'CONFLICT in a.txt', blockError: 'EBUSY: resource busy' },
|
|
113
|
+
reconcileError: 'ETIMEDOUT: spawnSync node ETIMEDOUT',
|
|
114
|
+
});
|
|
115
|
+
assert.equal(s.outcome, 'not_completed');
|
|
116
|
+
assert.equal(s.cardState, 'done_unreconciled');
|
|
117
|
+
const txt = JSON.stringify(s.steps);
|
|
118
|
+
assert.match(txt, /EBUSY: resource busy/);
|
|
119
|
+
assert.match(txt, /ETIMEDOUT/);
|
|
120
|
+
assert.match(txt, /CONFLICT in a\.txt/);
|
|
121
|
+
assert.doesNotMatch(txt, /unknown error/i);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('TASK_2420: spawnFailureCause never returns "unknown error"', () => {
|
|
125
|
+
assert.match(spawnFailureCause({ status: null, stderr: '', stdout: '', error: Object.assign(new Error('spawnSync node ETIMEDOUT'), { code: 'ETIMEDOUT' }) }), /ETIMEDOUT/);
|
|
126
|
+
assert.match(spawnFailureCause({ status: null, signal: 'SIGTERM', stderr: '', stdout: '' }), /SIGTERM/);
|
|
127
|
+
assert.match(spawnFailureCause({ status: 3, stderr: '', stdout: '' }), /code 3/);
|
|
128
|
+
assert.equal(spawnFailureCause({ status: 1, stderr: 'boom\nmore' }), 'boom');
|
|
129
|
+
});
|
|
@@ -178,19 +178,7 @@ function detectAllTestCommands(projectRoot, scopePaths = []) {
|
|
|
178
178
|
const normScopes = (scopePaths || []).map(p => p.replace(/\\/g, '/'));
|
|
179
179
|
const scopeTouches = (prefix) => normScopes.some(s => s === prefix || s.startsWith(prefix + '/') || prefix.startsWith(s + '/'));
|
|
180
180
|
|
|
181
|
-
// 1.
|
|
182
|
-
const dashPkg = path.join(projectRoot, 'dashboard', 'package.json');
|
|
183
|
-
if (fs.existsSync(dashPkg)) {
|
|
184
|
-
try {
|
|
185
|
-
const pkg = JSON.parse(fs.readFileSync(dashPkg, 'utf8'));
|
|
186
|
-
const testScript = pkg.scripts?.test;
|
|
187
|
-
if (testScript && !testScript.includes('echo "Error')) {
|
|
188
|
-
commands.push({ cmd: 'npm test -- --passWithNoTests', cwd: path.join(projectRoot, 'dashboard'), label: 'dashboard (npm test)' });
|
|
189
|
-
}
|
|
190
|
-
} catch { /* skip */ }
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// 2. Root package.json
|
|
181
|
+
// 1. Root package.json
|
|
194
182
|
const rootPkg = path.join(projectRoot, 'package.json');
|
|
195
183
|
if (fs.existsSync(rootPkg)) {
|
|
196
184
|
try {
|
|
@@ -202,7 +190,7 @@ function detectAllTestCommands(projectRoot, scopePaths = []) {
|
|
|
202
190
|
} catch { /* skip */ }
|
|
203
191
|
}
|
|
204
192
|
|
|
205
|
-
//
|
|
193
|
+
// 2. Rust: dashboard/src-tauri/ (kenaz-server lib) — only when scope explicitly touches it.
|
|
206
194
|
// The cargo build pulls kuzu via cmake which is the dominant source of
|
|
207
195
|
// false-positive regression failures observed in this project (~every
|
|
208
196
|
// dispatch reported "src-tauri tests FAILED" with cmake exit 2). Empty
|
|
@@ -210,13 +198,7 @@ function detectAllTestCommands(projectRoot, scopePaths = []) {
|
|
|
210
198
|
// Agents that explicitly need Rust coverage can pass --test-cmd.
|
|
211
199
|
const tauriCargo = path.join(projectRoot, 'dashboard', 'src-tauri', 'Cargo.toml');
|
|
212
200
|
if (fs.existsSync(tauriCargo) && scopeTouches('dashboard/src-tauri')) {
|
|
213
|
-
commands.push({ cmd: 'cargo test --
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// 4. Rust: dashboard/pty-server/ — same selective rule
|
|
217
|
-
const ptyCargo = path.join(projectRoot, 'dashboard', 'pty-server', 'Cargo.toml');
|
|
218
|
-
if (fs.existsSync(ptyCargo) && scopeTouches('dashboard/pty-server')) {
|
|
219
|
-
commands.push({ cmd: 'cargo test', cwd: path.join(projectRoot, 'dashboard', 'pty-server'), label: 'pty-server (cargo test)' });
|
|
201
|
+
commands.push({ cmd: 'cargo test --lib', cwd: path.join(projectRoot, 'dashboard', 'src-tauri'), label: 'src-tauri (cargo test)' });
|
|
220
202
|
}
|
|
221
203
|
|
|
222
204
|
// 5. Python (pytest) — root level
|