kenaz 0.3.9 → 0.3.11
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 +12 -16
- package/core/.claude/hooks/memory-guard.js +13 -0
- 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-api.js +2 -218
- package/core/.collaboration/core/task-api.test.js +1 -138
- package/core/.formation/bifrost/launch.js +2 -2
- package/core/.formation/bifrost/launch.test.js +2 -2
- 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 +259 -0
- package/core/.formation/tools/kenaz-stats.test.js +180 -0
- 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/testing.js +3 -21
- package/core/.formation/tools/post-complete.js +2 -428
- package/core/.formation/tools/post-complete.test.js +0 -135
- package/core/.formation/tools/prompt.js +14 -0
- package/core/.formation/tools/route-audit.test.js +8 -20
- 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/RULES.md +6 -0
- 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__/task-2409-lessons.test.js +95 -0
- package/core/.specialist/tools/ansuz-boot-fast.js +1 -0
- package/core/.specialist/tools/lessons.js +234 -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/session.js +2 -89
- 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 +26 -0
- package/core/plugins/kenaz/INVENTORY.md +1 -1
- package/core/plugins/kenaz/commands/Ansuz.md +12 -16
- package/core/plugins/kenaz/scripts/kenaz.test.js +2 -1
- package/core/plugins/kenaz/scripts/ygg.js +2 -0
- 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 +13 -3
- package/package.json +6 -6
- package/core/.claude/hooks/handoff-snapshot-on-decision.js +0 -188
- package/core/.claude/skills/synapse_maintain/SKILL.md +0 -84
- 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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* CLI:
|
|
10
10
|
* node session.js list [--json]
|
|
11
11
|
* node session.js resolve <name>
|
|
12
|
-
* node session.js prompt <name> [--mode=assembled|reference] [--depth=capsule|elixir|kernel] [--exclude=a,b] [--task=...] [--context=...] [--source=plugin]
|
|
12
|
+
* node session.js prompt <name> [--mode=assembled|reference] [--depth=capsule|elixir|kernel] [--exclude=a,b] [--task=...] [--context=...] [--source=plugin]
|
|
13
13
|
* node session.js start <name> [--consumer=...] [--task=...] [--with-prompt] [--json]
|
|
14
14
|
* node session.js end <sessionId>
|
|
15
15
|
* node session.js active [--json]
|
|
@@ -76,8 +76,6 @@ const PROJECT_DIR = (() => {
|
|
|
76
76
|
return BASE_DIR;
|
|
77
77
|
})();
|
|
78
78
|
|
|
79
|
-
// Specialists whose `prompt` boot gets the most recent episode injected as prior-session context.
|
|
80
|
-
const EPISODE_SPECIALISTS = ['Ansuz', 'Amelia'];
|
|
81
79
|
const http = require('http');
|
|
82
80
|
const projectResolver = require('./lib/project-resolver');
|
|
83
81
|
// TASK_2163: chart files are keyed by project identity + id (sub-projects reusing
|
|
@@ -161,7 +159,7 @@ function buildAnsuzBootCapsule(spec, selfModel) {
|
|
|
161
159
|
lazy_load: [
|
|
162
160
|
'Load full kernel/capability files only when the current task needs their detailed rules.',
|
|
163
161
|
'Load full self_model only when updating identity, auditing drift, or resolving a blind spot.',
|
|
164
|
-
'Use
|
|
162
|
+
'Use kenaz_lsp_references / kenaz_lsp_callers before modifying code paths with broad blast radius.',
|
|
165
163
|
],
|
|
166
164
|
self_model: summarizeSelfModelForBootCapsule(selfModel),
|
|
167
165
|
};
|
|
@@ -1028,73 +1026,6 @@ function buildReferencePrompt(spec, { task, context, exclude }) {
|
|
|
1028
1026
|
return sections.join('\n');
|
|
1029
1027
|
}
|
|
1030
1028
|
|
|
1031
|
-
// ═══════════════════════════════════════
|
|
1032
|
-
// Hippocampus: Episodic Memory Recall
|
|
1033
|
-
// ═══════════════════════════════════════
|
|
1034
|
-
|
|
1035
|
-
/**
|
|
1036
|
-
* Recall the most recent episode from Hippocampus via MCP HTTP API.
|
|
1037
|
-
* Used in Ansuz prompt assembly to inject prior session context.
|
|
1038
|
-
* Graceful degradation: returns null if Dashboard/Hippocampus unavailable.
|
|
1039
|
-
* @param {number} limit - Max episodes to recall (default: 1)
|
|
1040
|
-
* @param {string|null} agentRole - Optional agent_role filter (e.g. 'ansuz', 'amelia').
|
|
1041
|
-
* When provided, only episodes with matching agent_role are returned.
|
|
1042
|
-
* When null, all episodes are returned (backwards compatible).
|
|
1043
|
-
* @returns {Promise<string|null>} Formatted episodic memory section, or null
|
|
1044
|
-
*/
|
|
1045
|
-
function recallRecentEpisode(limit = 1, agentRole = null) {
|
|
1046
|
-
return new Promise((resolve) => {
|
|
1047
|
-
const recallArgs = { limit, projectPath: PROJECT_DIR };
|
|
1048
|
-
if (agentRole) recallArgs.agent_role = agentRole;
|
|
1049
|
-
const payload = JSON.stringify({
|
|
1050
|
-
jsonrpc: '2.0', id: 1,
|
|
1051
|
-
method: 'tools/call',
|
|
1052
|
-
params: { name: 'hippocampus_episode_recall', arguments: recallArgs },
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
const req = http.request({
|
|
1056
|
-
hostname: '127.0.0.1', port: 18080, path: '/api/mcp',
|
|
1057
|
-
method: 'POST',
|
|
1058
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
1059
|
-
timeout: 5000,
|
|
1060
|
-
}, (res) => {
|
|
1061
|
-
let body = '';
|
|
1062
|
-
res.on('data', (c) => { body += c; });
|
|
1063
|
-
res.on('end', () => {
|
|
1064
|
-
try {
|
|
1065
|
-
const result = JSON.parse(body);
|
|
1066
|
-
const text = result?.result?.content?.find(c => c.type === 'text')?.text;
|
|
1067
|
-
if (!text) return resolve(null);
|
|
1068
|
-
|
|
1069
|
-
const data = JSON.parse(text);
|
|
1070
|
-
const episodes = data.episodes || [];
|
|
1071
|
-
if (episodes.length === 0) return resolve(null);
|
|
1072
|
-
|
|
1073
|
-
const lines = ['# ═══ Episodic Memory (from Hippocampus) ═══', ''];
|
|
1074
|
-
lines.push(`Last ${episodes.length} session(s):`);
|
|
1075
|
-
for (const ep of episodes) {
|
|
1076
|
-
const epData = ep.episode || ep;
|
|
1077
|
-
lines.push(`\n### ${epData.task_id || epData.id || 'unknown'}`);
|
|
1078
|
-
if (epData.summary) lines.push(epData.summary);
|
|
1079
|
-
if (epData.lesson) lines.push(`**Lesson**: ${epData.lesson}`);
|
|
1080
|
-
const entries = ep.entries || [];
|
|
1081
|
-
for (const entry of entries.slice(0, 5)) {
|
|
1082
|
-
lines.push(`- **${entry.phase || 'note'}**: ${entry.content || ''}`);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
resolve(lines.join('\n'));
|
|
1086
|
-
} catch {
|
|
1087
|
-
resolve(null);
|
|
1088
|
-
}
|
|
1089
|
-
});
|
|
1090
|
-
});
|
|
1091
|
-
req.on('error', () => resolve(null));
|
|
1092
|
-
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
1093
|
-
req.write(payload);
|
|
1094
|
-
req.end();
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
1029
|
/**
|
|
1099
1030
|
* Find the current Claude Code conversation JSONL file.
|
|
1100
1031
|
* Claude Code writes real-time conversation data to:
|
|
@@ -1685,23 +1616,6 @@ async function cli() {
|
|
|
1685
1616
|
let prompt = await buildPrompt(name, { mode, task: flags.task, context: flags.context, exclude, source, depth,
|
|
1686
1617
|
externalBootContext: flags['external-boot-context'] === true });
|
|
1687
1618
|
|
|
1688
|
-
// Inject the most recent episode as prior-session context for interactive boots.
|
|
1689
|
-
// --no-episode (legacy alias --no-muninn, still passed by k-cli): skip it for
|
|
1690
|
-
// headless/programmatic callers (e.g. kenaz_specialist_dispatch).
|
|
1691
|
-
if (EPISODE_SPECIALISTS.includes(name) && mode === 'assembled' && !flags['no-episode'] && !flags['no-muninn']) {
|
|
1692
|
-
// TASK_2218: the real project pin is written by .claude/hooks/ansuz-session-pin.js,
|
|
1693
|
-
// keyed by the hook-payload session_id -- nothing is pinned here.
|
|
1694
|
-
|
|
1695
|
-
// Prompt injection: skip in lean modes to keep prompt lean.
|
|
1696
|
-
if (depth !== 'kernel' && depth !== 'capsule' && depth !== 'elixir') {
|
|
1697
|
-
// Hippocampus: inject most recent episode as prior session context
|
|
1698
|
-
const episodicMemory = await recallRecentEpisode(1, name.toLowerCase());
|
|
1699
|
-
if (episodicMemory) {
|
|
1700
|
-
prompt += `\n\n${episodicMemory}\n`;
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
1619
|
console.log(prompt);
|
|
1706
1620
|
break;
|
|
1707
1621
|
}
|
|
@@ -1858,7 +1772,6 @@ if (require.main === module) {
|
|
|
1858
1772
|
cleanSessions,
|
|
1859
1773
|
cleanOrphanedClaude,
|
|
1860
1774
|
classifyOrphanCandidates, // exported for testing (TASK_1917) — same function production uses
|
|
1861
|
-
recallRecentEpisode,
|
|
1862
1775
|
findConversationLog,
|
|
1863
1776
|
// Identity verification (TASK_1080)
|
|
1864
1777
|
identityScore: (name, opts) => require('./identity-scorer').score(name, { projectPath: BASE_DIR, ...opts }),
|
package/core/CLAUDE.md
CHANGED
|
@@ -41,7 +41,7 @@ session 開始時先鎖定語言(對照用戶第一條訊息 + cwd/CLAUDE.md
|
|
|
41
41
|
- 修改非 trivial symbol 前:用 `kenaz_lsp_references` / `kenaz_lsp_callers` 看影響範圍(FAIL-009)。trivial 改動可跳過。
|
|
42
42
|
- 所有 `kenaz_lsp_*` 呼叫必須明確帶 `projectPath`。
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
**記憶系統已移除(2026-10-11,TASK_2411)**:Synapse / Hippocampus / Cortex recall / embeddings 與 Alaya seeds/bijas 已移除,`synapse_*`、`hippocampus_*`、`cortex_*`、`alaya_recall` 不再存在;教訓用 `kenaz learn add` 與 Claude 原生記憶檔;`~/.kenaz/cortex/*.db` 與 `.synapse/` 資料留在磁碟上、不再開啟。
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
@@ -59,11 +59,11 @@ harness 預設「每條記憶都在 MEMORY.md 加一行」;本專案改為三
|
|
|
59
59
|
## 專案概述
|
|
60
60
|
|
|
61
61
|
KenazAI 是一個 Claude Code 多 Agent 協作系統,包含:
|
|
62
|
-
- **
|
|
62
|
+
- **kenaz-server**:Rust 後端(`dashboard/src-tauri/`,npm `@kenazai/server-*`);Dashboard 桌面版已於 2026-10-11 退役(TASK_2396)
|
|
63
63
|
- **Specialist 系統**:AI Agent 調度引擎(`.specialist/`)
|
|
64
64
|
- **Formation 系統**:多 Agent 協作調度,支援 unit/solo/squad/legion 陣型(`.formation/`)
|
|
65
65
|
|
|
66
|
-
技術棧:Rust(
|
|
66
|
+
技術棧:Rust(kenaz-server)、Node.js(kenaz CLI、hooks、工具)、SQLite
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
@@ -132,8 +132,7 @@ node .collaboration/core/task-api.js move TASK_XXX active # 移動狀態
|
|
|
132
132
|
|------|------|------|
|
|
133
133
|
| 規則庫 | `.rule/` | 最高優先級規則,見 `INDEX.yaml` |
|
|
134
134
|
| 失敗學習 | `.failure/` | 已知失敗模式,Agent 執行前應檢查 |
|
|
135
|
-
|
|
|
136
|
-
| Rust 測試 | `npm run test:rust -- <filter>` | 主目錄直接可跑;用共用短路徑 target,不和 live tauri dev 搶 `target/` 鎖。勿直接 `cargo test` |
|
|
135
|
+
| Rust 測試 | `npm run test:rust -- <filter>` | 主目錄直接可跑;用共用短路徑 target,不和其他建置搶 `target/` 鎖。勿直接 `cargo test` |
|
|
137
136
|
| Formation | `.formation/` | 多 Agent 調度,見 `README.md` |
|
|
138
137
|
| Skills | `.claude/skills/` | Agent 行為定義(40+ skills) |
|
|
139
138
|
| 外部 Skills | `.agents/skills/` | `vercel-react-best-practices`、`supabase-postgres-best-practices` |
|
|
@@ -163,10 +162,10 @@ DB: postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
|
|
163
162
|
.collaboration/ # 任務系統
|
|
164
163
|
.formation/ # Formation 調度
|
|
165
164
|
.failure/ # 失敗學習系統
|
|
166
|
-
.memory/ #
|
|
165
|
+
.memory/ # (歷史)舊記憶資料,記憶系統已移除
|
|
167
166
|
.claude/ # Skills、Commands、Hooks
|
|
168
167
|
.agents/ # 外部 Skills
|
|
169
168
|
dev/scripts/ # CLI 工具
|
|
170
169
|
dev/PRODUCT_SPEC.md # 產品規格
|
|
171
|
-
dashboard/
|
|
170
|
+
dashboard/src-tauri/ # kenaz-server(Rust);dashboard/scripts/ 打包與安裝腳本
|
|
172
171
|
```
|
package/core/KENAZ_CORE_VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.11
|
|
@@ -126,3 +126,29 @@ Layout: `packages/kenaz` (CLI + staged kenaz-core), `packages/server-{win32-x64,
|
|
|
126
126
|
### doctor 為什麼檢查 plugin 路徑
|
|
127
127
|
|
|
128
128
|
Dashboard 執行時(`peer_mod.rs`)會重新登錄自己 kenaz-core 內的 `kenaz-peer`;自 TASK_2384 起登錄會取代舊副本,所以重新啟動 Dashboard 會把 `CLAUDE_CODE_PLUGIN_DIRS` 的 `kenaz-peer` 改回它的路徑。`kenaz doctor` 會列出:Dashboard 是否執行 / 已安裝、18080 的持有者、`CLAUDE_CODE_PLUGIN_DIRS` 是否指向 Dashboard 的 core。
|
|
129
|
+
|
|
130
|
+
### Dashboard 退役 (TASK_2396,2026-10-11)
|
|
131
|
+
|
|
132
|
+
Dashboard(Tauri 桌面 app + Next.js 前端)已從 repo 移除,`kenaz-server` 是唯一後端,`cargo build`(預設 feature `server`)直接產出它。
|
|
133
|
+
|
|
134
|
+
- 已安裝的 Dashboard 不會被動到:最後一版安裝檔與 Supabase `kenaz-release/latest.json` 都保留,既有安裝可繼續用,但不會再收到更新。
|
|
135
|
+
- 遷移步驟同上方「給使用者的步驟」:結束 Dashboard、關閉「開機時啟動」、`npx kenaz install`、`/kenaz init`、`npx kenaz doctor`。資料(專案、憑證、任務、記憶)都在 `~/.kenaz` 與 `<專案>/.kenaz`,不需搬移。
|
|
136
|
+
- 回到舊版 Dashboard 只能用當初的安裝檔;repo 內不再有它的建置腳本(`build:kenaz`、`publish-release`、`editions/`、`PUBLISH_UPDATE`)。
|
|
137
|
+
|
|
138
|
+
gui 行為去向(盤點自 `gui_app.rs`、`commands.rs`、`updater.rs`、24 個 `#[cfg(feature = "gui")]`):
|
|
139
|
+
|
|
140
|
+
| 行為 | 去向 |
|
|
141
|
+
|---|---|
|
|
142
|
+
| HTTP/MCP 啟動、port fallback、health watchdog、peer/presence/claude provision pollers | 已在 `server_boot::start_http_server`(TASK_2357/2369) |
|
|
143
|
+
| PluginManager + MCP servers、Yggdrasil remote poll、field report loops | 已在 `start_headless_services`(TASK_2369/2394) |
|
|
144
|
+
| KENAZ_SOURCE_DIR 解析、mcp.json 擁有者規則、env/tracing 初始化 | 已在 `server_boot`(`resolve_server_source_dir`、`should_rewrite_mcp`、`init_process`) |
|
|
145
|
+
| 自動更新 | 已由 `infrastructure/self_update.rs` 取代(ed25519 簽章 feed);Tauri updater 與 `/api/dashboard/*` 更新路由刪除 |
|
|
146
|
+
| 視窗、splash、webview 重啟守衛、WTS 解鎖監看 | 刻意移除(無 webview) |
|
|
147
|
+
| 系統匣、close-to-tray、開機自啟 | 刻意移除(服務由 `kenaz install` 的 service launcher 負責) |
|
|
148
|
+
| `kenaz://` deep link、single-instance、Agent Hub 核准卡 | 刻意移除(hub 的 sites/sessions 狀態與 HTTP 路由保留) |
|
|
149
|
+
| 語音 PTT、剪貼簿/檔案選取 tauri command、pty-server sidecar、claude-bridge、Next.js sidecar | 刻意移除(只有前端使用) |
|
|
150
|
+
| Tauri `#[tauri::command]` 包裝(scheduler、single_agent、script、config 等) | 刻意移除;對應 HTTP 路由原本就存在,函式本體保留 |
|
|
151
|
+
| `.env.local` 載入與 TUNNEL_SIGNING_SECRET 自動產生、tunnel token 寫回 | 未搬:kenaz-server 不載入 `.env.local`,tunnel 相關環境變數須自行以環境變數提供(見交接回報的未驗證項) |
|
|
152
|
+
| 啟動時 `auto_seed_project_config`(寫入 `~/.kenaz/projects.json` 的 core 項目) | 未搬:由 `kenaz install` / `/kenaz init` 登錄專案 |
|
|
153
|
+
| 啟動時 cortex seed merge(`merge_seed_if_needed`) | 刻意移除(記憶系統凍結,merge 核心 `merge_seed_at` 留待記憶卡處理) |
|
|
154
|
+
| 退出清理與 self-kill watchdog | 刻意移除;kenaz-server 在 ctrl-c 時關閉 LSP 並 shutdown HTTP |
|
|
@@ -37,7 +37,7 @@ Hooks only act in Kenaz projects (`.kenaz/project.yaml`, `_project-guard.js`): r
|
|
|
37
37
|
| Class | Skills |
|
|
38
38
|
|---|---|
|
|
39
39
|
| ship (34) | architect_agent, assessment_agent, backend_agent, backend_engineer, code_reviewer, dev_agent, designer, developer, feature_agent, frontend_agent, incident_response, product_manager, qa_engineer, security_agent, spec_optimizer, sre_agent, adapt, animate, audit, bolder, clarify, colorize, critique, delight, distill, extract, frontend-design, harden, normalize, onboard, optimize, polish, quieter, ui-ux-pro-max |
|
|
40
|
-
| core | heimdall, huginn,
|
|
40
|
+
| core | heimdall, huginn, sunny, optimization_agent |
|
|
41
41
|
| retire | legacy_dev |
|
|
42
42
|
| third-party / domain (not shipped; install separately from .agents/skills) | vercel-react-best-practices, supabase-postgres-best-practices, remotion-best-practices, pcb-review |
|
|
43
43
|
|
|
@@ -14,7 +14,7 @@ ARGUMENTS 含 `[專案: X]` 或 `--project-path X` → PROJECT_PATH = X,下方
|
|
|
14
14
|
- **自己做**:小而明確的修正(約 30 行內、單一檔案或少數檔案)→ 直接改,跑最近的測試,提交。
|
|
15
15
|
- **開卡派工**:中型、可平行、或高風險路徑的工作:
|
|
16
16
|
1. 用 `kenaz_task_create` 開卡(`projectPath` = 目前專案;寫清楚 requirements、acceptance criteria、scope paths)。查卡、改狀態用 `kenaz_task_list` / `kenaz_task_get` / `kenaz_task_update`。
|
|
17
|
-
2. 產生派工內容:`node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent
|
|
17
|
+
2. 產生派工內容:`node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .formation/tools/dispatch.js --skill=do --task=TASK_XXX --dispatch-mode=subagent`(有 PROJECT_PATH 加 `--project-path`)。把輸出 JSON 的 `promptText` 交給 subagent,`agentParams` 照用(模型不要自己換)。
|
|
18
18
|
3. subagent 在 worktree 裡改完並 commit;你驗證後合併。
|
|
19
19
|
- 開卡前先確認 scope 與驗收條件對準真正的問題,不要照著錯的假設派工。
|
|
20
20
|
|
|
@@ -23,24 +23,20 @@ ARGUMENTS 含 `[專案: X]` 或 `--project-path X` → PROJECT_PATH = X,下方
|
|
|
23
23
|
- 有**對照組**:證明測試在舊程式上會失敗,或修改前後結果不同。
|
|
24
24
|
- subagent 回報「未驗證」的項目,由你補驗或明講仍未驗證。
|
|
25
25
|
- 合併:`git merge` 後跑相關測試(或 `node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .specialist/Ansuz/tools/verify-and-land.js`),用這個專案自己的測試指令。
|
|
26
|
-
- 檔案長度:verify-and-land 合併前會跑 `check-file-size`(警告 1000 / 上限 2000 行,既有超長檔只擋「未配套拆分的增長」);被擋就按職責拆成 module / component / hook。
|
|
27
|
-
- 只在 Kenaz 核心 repo 本身:Rust 測試走 `npm run test:rust` / `dev/scripts/rust-verify-worktree.js`。
|
|
28
26
|
|
|
29
|
-
##
|
|
30
|
-
|
|
27
|
+
## 回報與提問
|
|
28
|
+
- 一律用用戶的語言(中文訊息 → 中文回覆,含提問選項)。
|
|
29
|
+
- 需要用戶決定時用 `AskUserQuestion`(2-4 個選項、建議的放第一個),不要在文字裡列選項等回覆;有證據能自己判斷的事不問。
|
|
30
|
+
- 回報說清楚:做了什麼、怎麼驗證的、還有什麼沒驗證或需要用戶決定。不說「應該可以」。
|
|
31
31
|
|
|
32
|
-
## Kenaz
|
|
33
|
-
Kenaz 的工具、hook、安裝、peer
|
|
34
|
-
- `projectPath`:目前專案路徑;`scope`:`kenaz`(Kenaz 本身的問題)或 `project`(只跟這個專案有關)
|
|
35
|
-
- `wish_type`:`bug`(壞了)/`feature`(缺能力,內容寫:需求、現在哪裡不夠、建議做法、還有誰受益)/`question`
|
|
36
|
-
- `content`:一行重現步驟 + 預期 vs 實際 + 版本(`npx kenaz doctor` 那幾行);附上 session id。不要貼金鑰或 token。
|
|
37
|
-
- 一個問題只報一次;報完照常用替代做法把工作完成,並在回報裡告訴用戶已送出 wish。
|
|
38
|
-
- 用戶直接說「回報這個問題」時也用同一個工具(`source`=`user`)。
|
|
32
|
+
## Kenaz 本身出問題時:先回報再繞過
|
|
33
|
+
Kenaz 的工具、hook、安裝、peer、派工擋住或拖慢工作時,**繞過之前**先 `kenaz_wish_create`(`projectPath`;`scope` kenaz|project;`wish_type` bug|feature|question;`content` = 一行重現 + 預期 vs 實際 + `npx kenaz doctor` 版本 + session id,不貼金鑰)。一個問題報一次,報完照常完成工作並告訴用戶;用戶說「回報這個問題」也用它(`source`=`user`)。
|
|
39
34
|
|
|
40
35
|
## 自主模式
|
|
41
|
-
|
|
36
|
+
用戶明確授權自主時:立刻 `node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .specialist/tools/autonomy.js grant --session <id> --scope "<範圍>"`,讀 RULES.md 的 Autonomy。之後結束回合會被 Stop hook 擋回:自己決定、一路做完,不在結尾問「要我…嗎?」;只有必要停點才 `pause`,做完才 `done`。
|
|
42
37
|
|
|
43
|
-
## 參考產品(REA
|
|
44
|
-
banner
|
|
38
|
+
## 參考產品(REA)與學習教訓
|
|
39
|
+
- banner `[rea]` 行:用戶提到參考產品時,有 REA 就提議分析,沒有就提示一次 `npx rea-agents setup`(範圍見 RULES.md 的 REA)。
|
|
40
|
+
- 用戶糾正/重複指示、推翻決定、或驗證抓到失敗時,自動寫教訓並在回報點名一行:`node "${CLAUDE_PLUGIN_ROOT}/hooks/core-resolve.js" .specialist/tools/lessons.js add --scope user|project --type pref|fact|pitfall|check --text "..."`(規則見 RULES.md 的 Lessons)。
|
|
45
41
|
|
|
46
|
-
|
|
42
|
+
需要更多規則時(授權自主、派工前、合併前、參考產品、寫教訓)讀 `${CLAUDE_PLUGIN_ROOT}/../../.specialist/Ansuz/RULES.md`。
|
|
@@ -59,7 +59,7 @@ test('ygg status fixture: masked key, room, peers, mail (TASK_2366)', () => {
|
|
|
59
59
|
const s = buildStatus(
|
|
60
60
|
{ enabled: true, effective_project_key: 'acme/app', node_name: 'laptop', publishable_key: key, key_source: 'project', collaboration_projects: ['acme/app'] },
|
|
61
61
|
{ connected: true, nodes: [{ node_name: 'laptop', is_online: true, updated_at: new Date().toISOString() }, { node_name: 'desk', is_online: true, updated_at: new Date().toISOString() }, { node_name: 'old', is_online: false }, { node_name: 'BA-Cheng', is_online: true, updated_at: '2026-06-30T00:00:00Z' }] },
|
|
62
|
-
{ pending_count: 2, undeliverable_count: 1 });
|
|
62
|
+
{ pending_count: 2, waiting_count: 2, undeliverable_count: 1 });
|
|
63
63
|
const out = renderStatus(s);
|
|
64
64
|
assert.ok(!out.includes('SECRETSECRET') && !JSON.stringify(s).includes('SECRETSECRET'));
|
|
65
65
|
assert.match(out, /ygk_\*{4}1234 \(source: project\)/);
|
|
@@ -67,6 +67,7 @@ test('ygg status fixture: masked key, room, peers, mail (TASK_2366)', () => {
|
|
|
67
67
|
assert.match(out, /Relay:\s+connected/);
|
|
68
68
|
assert.match(out, /Peers online:\s+desk$/m);
|
|
69
69
|
assert.match(out, /Pending mail:\s+2/);
|
|
70
|
+
assert.match(out, /Waiting for a session: 2/);
|
|
70
71
|
assert.match(out, /Undeliverable:\s+1/);
|
|
71
72
|
const down = renderStatus(buildStatus({ enabled: false }, { connected: false, reason: 'relay unreachable', nodes: [] }, null));
|
|
72
73
|
assert.match(down, /NOT connected \(relay unreachable\)/);
|
|
@@ -73,6 +73,7 @@ function buildStatus(cfg, relay, peer) {
|
|
|
73
73
|
? 'Room "' + cfg.effective_project_key + '" is a generic name; ' + online.join(', ') + ' may be an unrelated project. For a private room set yggdrasil_project: "' + cfg.effective_project_key + '-<unique-id>" in .kenaz/yggdrasil.yaml.'
|
|
74
74
|
: '',
|
|
75
75
|
pending_mail: peer && typeof peer.pending_count === 'number' ? peer.pending_count : null,
|
|
76
|
+
waiting_mail: peer && typeof peer.waiting_count === 'number' ? peer.waiting_count : null,
|
|
76
77
|
undeliverable_mail: peer && typeof peer.undeliverable_count === 'number' ? peer.undeliverable_count : null,
|
|
77
78
|
// TASK_2380: real delivery mode per live session (push | pull-on-prompt | none), never a bare 'enabled'.
|
|
78
79
|
delivery_mode: peer && peer.delivery_mode ? peer.delivery_mode : 'unknown',
|
|
@@ -92,6 +93,7 @@ function renderStatus(s) {
|
|
|
92
93
|
'Relay: ' + (s.relay_connected ? 'connected' : 'NOT connected' + (s.relay_note ? ' (' + s.relay_note + ')' : '')),
|
|
93
94
|
'Peers online: ' + (s.peers_online.length ? s.peers_online.join(', ') : 'none'),
|
|
94
95
|
'Pending mail: ' + (s.pending_mail == null ? 'unknown' : s.pending_mail),
|
|
96
|
+
'Waiting for a session: ' + (s.waiting_mail == null ? 'unknown' : s.waiting_mail) + ' (stored on this node, readable)',
|
|
95
97
|
'Undeliverable: ' + (s.undeliverable_mail == null ? 'unknown' : s.undeliverable_mail),
|
|
96
98
|
'Delivery: ' + s.delivery_mode + (s.sessions.length ? '' : ' (no live sessions)'),
|
|
97
99
|
];
|
|
@@ -232,7 +232,6 @@ description: 程式碼審查、三級問題分類(Blocker/Suggestion/Nit)、
|
|
|
232
232
|
|
|
233
233
|
| Mutation | Counterpart | 缺失嚴重度 |
|
|
234
234
|
|----------|-------------|-----------|
|
|
235
|
-
| `hippocampus_episode_start` | `hippocampus_episode_close` | leak(episode 永不歸檔) |
|
|
236
235
|
| `task-api.js claim` | `task-api.js complete` 或 `move blocked` | orphan(task 卡在 active/) |
|
|
237
236
|
| `addEventListener` | `removeEventListener` | leak(listener 記憶體洩漏) |
|
|
238
237
|
| `setInterval` | `clearInterval` | leak(定時器持續觸發) |
|
|
@@ -17,14 +17,6 @@ mutation_counterpart:
|
|
|
17
17
|
# orphan = logical state / lifecycle not terminated
|
|
18
18
|
|
|
19
19
|
pairs:
|
|
20
|
-
- pair: hippocampus_episode
|
|
21
|
-
mutation: 'hippocampus_episode_start'
|
|
22
|
-
counterpart: 'hippocampus_episode_close'
|
|
23
|
-
severity: leak
|
|
24
|
-
rationale: 'episode never closed -> consolidation never fires, unarchived memory accumulates'
|
|
25
|
-
grep_mutation: 'hippocampus_episode_start'
|
|
26
|
-
grep_counterpart: 'hippocampus_episode_close'
|
|
27
|
-
|
|
28
20
|
- pair: task_lifecycle
|
|
29
21
|
mutation: 'task-api.js claim'
|
|
30
22
|
counterpart: 'task-api.js complete | task-api.js move'
|
|
@@ -256,8 +256,6 @@ description: 開發協調、Pipeline管理、任務執行、進度追蹤、Agent
|
|
|
256
256
|
| `kenaz_lsp_references` | 所有引用 / 修改影響範圍 | 修改前評估 blast radius |
|
|
257
257
|
| `kenaz_lsp_definition` | 定義位置 | 跳到宣告處 |
|
|
258
258
|
| `kenaz_lsp_symbols` | 檔案內 symbol 列表 | 了解檔案結構 |
|
|
259
|
-
| `synapse_recall` | 歷史 rune / 已知風險 | 修改前召回 |
|
|
260
|
-
| `synapse_status` | 記憶系統狀態 | 任務開始前檢查 |
|
|
261
259
|
|
|
262
260
|
文字與概念搜尋用 Grep / Glob。
|
|
263
261
|
|
|
@@ -281,8 +279,7 @@ description: 開發協調、Pipeline管理、任務執行、進度追蹤、Agent
|
|
|
281
279
|
**在開始任何開發任務前,建議執行:**
|
|
282
280
|
|
|
283
281
|
1. **獲取專案 Context**:
|
|
284
|
-
-
|
|
285
|
-
- 使用 `synapse_recall` 召回相關已知風險與決策
|
|
282
|
+
- 讀取任務卡與 `kenaz learn list` 的相關教訓
|
|
286
283
|
|
|
287
284
|
2. **搜索相關代碼**:
|
|
288
285
|
- 使用 Grep / Glob 找到相關實現
|
|
@@ -335,10 +332,10 @@ description: 開發協調、Pipeline管理、任務執行、進度追蹤、Agent
|
|
|
335
332
|
2. 重新讀取 Skill 文件
|
|
336
333
|
3. 如果問題持續,直接應用該 Agent 的核心職責邏輯
|
|
337
334
|
|
|
338
|
-
### LSP
|
|
335
|
+
### LSP 查詢失敗時
|
|
339
336
|
1. 繼續執行任務(降級為 Grep/Glob)
|
|
340
337
|
2. 確認 Dashboard 已啟動(依賴 Rust HTTP server on port 18080)
|
|
341
|
-
3. 使用 `
|
|
338
|
+
3. 使用 `kenaz_ping` 檢查服務狀態
|
|
342
339
|
|
|
343
340
|
---
|
|
344
341
|
|
package/lib/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ const dash = require('./dashboard');
|
|
|
14
14
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
15
15
|
const PKG = require('../package.json');
|
|
16
16
|
const MIN_NODE = 18;
|
|
17
|
-
const USAGE = 'usage: kenaz <install|uninstall|status|doctor|init|ygg join <key>|ygg status|ygg leave|alaya status|alaya register|model [set <id>|unset] [--rigor r]
|
|
17
|
+
const USAGE = 'usage: kenaz <install|uninstall|status|doctor|init|ygg join <key>|ygg status|ygg leave|alaya status|alaya register|model [set <id>|unset] [--rigor r]|stats [--days N] [--json]|learn <add|list|rm>> [--dry-run] [--home <dir>] [--no-service] [--no-claude] [--no-codex] [--keep-dashboard] [--server-bin <path>]';
|
|
18
18
|
|
|
19
19
|
function parseArgs(argv) {
|
|
20
20
|
const o = { cmd: argv[0], flags: {} };
|
|
@@ -41,7 +41,9 @@ function coreModule(p, ...rel) {
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
const reaDetect = (p) => coreModule(p, '.specialist', 'tools', 'rea-detect.js');
|
|
44
|
+
const lessons = (p) => coreModule(p, '.specialist', 'tools', 'lessons.js');
|
|
44
45
|
const dispatchModel = (p) => coreModule(p, '.formation', 'tools', 'dispatch-model.js');
|
|
46
|
+
const kenazStats = (p) => coreModule(p, '.formation', 'tools', 'kenaz-stats.js');
|
|
45
47
|
|
|
46
48
|
function paths(home) {
|
|
47
49
|
const dir = path.join(home, '.kenaz');
|
|
@@ -232,8 +234,6 @@ async function doctor(p, flags, out, deps = {}) {
|
|
|
232
234
|
const r = await probe(process.env.KENAZ_HTTP_PORT || '18080');
|
|
233
235
|
let ver = ''; try { ver = JSON.parse(r.body).version; } catch (_) { /* health body is optional */ }
|
|
234
236
|
add('server reachable', r.ok ? 'ok' : 'FAIL', r.detail + (ver ? `, version ${ver}` : ''));
|
|
235
|
-
// TASK_2390: memory is frozen in the CLI build; 'disabled' is the expected state, never a failure.
|
|
236
|
-
if (r.ok) { let emb = ''; try { emb = JSON.parse(r.body).embedding || ''; } catch (_) { /* optional */ } add('memory', 'ok', emb === 'disabled' || !emb ? 'disabled (set KENAZ_MEMORY=on to enable)' : `on, embedding ${emb}`); }
|
|
237
237
|
|
|
238
238
|
const d = dash.detect(p.home, deps.dashboard);
|
|
239
239
|
if (!d.installed) add('dashboard', 'skip', 'no Kenaz Dashboard on this machine');
|
|
@@ -338,6 +338,16 @@ function run(argv, out = console.log, deps = {}) {
|
|
|
338
338
|
if (!dm) { out('kenaz-core not installed: run `kenaz install`'); return 1; }
|
|
339
339
|
return dm.main(argv.slice(1), out);
|
|
340
340
|
}
|
|
341
|
+
if (argv[0] === 'stats') {
|
|
342
|
+
const ks = kenazStats(paths(os.homedir()));
|
|
343
|
+
if (!ks) { out('kenaz-core not installed: run `kenaz install`'); return 1; }
|
|
344
|
+
return ks.main(argv.slice(1), out);
|
|
345
|
+
}
|
|
346
|
+
if (argv[0] === 'learn') {
|
|
347
|
+
const lm = lessons(paths(os.homedir()));
|
|
348
|
+
if (!lm) { out('kenaz-core not installed: run `kenaz install`'); return 1; }
|
|
349
|
+
return lm.main(argv.slice(1), out);
|
|
350
|
+
}
|
|
341
351
|
if (argv[0] === 'ygg' || argv[0] === 'init') return passthrough(argv, deps);
|
|
342
352
|
const { cmd, flags } = parseArgs(argv);
|
|
343
353
|
const fn = { install, uninstall, status, doctor }[cmd];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kenaz",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "Install Kenaz (plugin, hooks, kenaz-server) for Claude Code and Codex: npx kenaz install",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kenaz": "bin/kenaz.js"
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"test": "node --test lib/cli.test.js"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@kenazai/server-darwin-arm64": "0.3.
|
|
24
|
-
"@kenazai/server-darwin-x64": "0.3.
|
|
25
|
-
"@kenazai/server-linux-arm64": "0.3.
|
|
26
|
-
"@kenazai/server-linux-x64": "0.3.
|
|
27
|
-
"@kenazai/server-win32-x64": "0.3.
|
|
23
|
+
"@kenazai/server-darwin-arm64": "0.3.11",
|
|
24
|
+
"@kenazai/server-darwin-x64": "0.3.11",
|
|
25
|
+
"@kenazai/server-linux-arm64": "0.3.11",
|
|
26
|
+
"@kenazai/server-linux-x64": "0.3.11",
|
|
27
|
+
"@kenazai/server-win32-x64": "0.3.11"
|
|
28
28
|
},
|
|
29
29
|
"license": "UNLICENSED",
|
|
30
30
|
"repository": {
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* handoff-snapshot-on-decision.js — PostToolUse hook for synapse_inscribe (TASK_1400)
|
|
4
|
-
*
|
|
5
|
-
* Fires after mcp__kenaz-dashboard__synapse_inscribe when the inscribed rune type
|
|
6
|
-
* is one of: decision, risk, ash. Insight/pattern/fix/summary inscribes do NOT
|
|
7
|
-
* fire (too noisy). Includes rune_id in the nudge.
|
|
8
|
-
* 5-minute dedup keyed on rune_id (or normalized content hash if rune_id absent).
|
|
9
|
-
*
|
|
10
|
-
* Matcher: mcp__kenaz-dashboard__synapse_inscribe
|
|
11
|
-
*/
|
|
12
|
-
'use strict';
|
|
13
|
-
try { require('./_dedupe-guard').guard(typeof __filename === 'string' ? __filename : null); } catch (_) { /* never block a hook on guard load failure (e.g. vm sandbox without __filename) */ }
|
|
14
|
-
const kenazProjectRoot = require('./_project-guard').guard();
|
|
15
|
-
|
|
16
|
-
const fs = require('fs');
|
|
17
|
-
const path = require('path');
|
|
18
|
-
const { fenceSignal } = require('./_signal-fence');
|
|
19
|
-
|
|
20
|
-
const PROJECT_DIR = kenazProjectRoot;
|
|
21
|
-
const TELEMETRY_PATH = path.join(
|
|
22
|
-
PROJECT_DIR,
|
|
23
|
-
'.kenaz', 'specialist', 'handoff_nudges.jsonl'
|
|
24
|
-
);
|
|
25
|
-
const DEDUP_WINDOW_MS = 5 * 60 * 1000;
|
|
26
|
-
const DEDUP_KEY_PREFIX = 'inscribe:';
|
|
27
|
-
|
|
28
|
-
// Only these rune types trigger the nudge
|
|
29
|
-
const TRIGGER_TYPES = new Set(['decision', 'risk', 'ash']);
|
|
30
|
-
|
|
31
|
-
function readStdin() {
|
|
32
|
-
try {
|
|
33
|
-
return JSON.parse(fs.readFileSync(0, 'utf8').trim() || '{}');
|
|
34
|
-
} catch {
|
|
35
|
-
return {};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function extractInscribeType(payload) {
|
|
40
|
-
// Type can be in tool_input or inferred from response
|
|
41
|
-
return (
|
|
42
|
-
payload?.tool_input?.type ||
|
|
43
|
-
payload?.toolInput?.type ||
|
|
44
|
-
null
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function extractRuneId(payload) {
|
|
49
|
-
// rune_id appears in the tool_response from synapse_inscribe
|
|
50
|
-
try {
|
|
51
|
-
const resp =
|
|
52
|
-
payload?.tool_response ||
|
|
53
|
-
payload?.tool_result;
|
|
54
|
-
if (!resp) return null;
|
|
55
|
-
|
|
56
|
-
// Attempt to parse text content as JSON
|
|
57
|
-
let text = '';
|
|
58
|
-
if (typeof resp === 'string') {
|
|
59
|
-
text = resp;
|
|
60
|
-
} else if (resp?.content && Array.isArray(resp.content)) {
|
|
61
|
-
text = resp.content.map(c => c?.text || '').join('');
|
|
62
|
-
} else if (typeof resp === 'object') {
|
|
63
|
-
text = JSON.stringify(resp);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Try JSON parse first
|
|
67
|
-
try {
|
|
68
|
-
const parsed = JSON.parse(text);
|
|
69
|
-
if (parsed?.rune_id) return parsed.rune_id;
|
|
70
|
-
if (parsed?.id) return parsed.id;
|
|
71
|
-
} catch { /* not JSON */ }
|
|
72
|
-
|
|
73
|
-
// Fallback: regex extract
|
|
74
|
-
const m = text.match(/"rune_id"\s*:\s*"([^"]+)"/);
|
|
75
|
-
if (m) return m[1];
|
|
76
|
-
const m2 = text.match(/"id"\s*:\s*"([^"]+)"/);
|
|
77
|
-
if (m2) return m2[1];
|
|
78
|
-
return null;
|
|
79
|
-
} catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function normalizeKey(runeId, type, content) {
|
|
85
|
-
if (runeId) return runeId;
|
|
86
|
-
// Fallback: hash of type+content snippet
|
|
87
|
-
const snippet = `${type}:${(content || '').substring(0, 40)}`;
|
|
88
|
-
return snippet.replace(/\s+/g, '_');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function isDeduped(key) {
|
|
92
|
-
try {
|
|
93
|
-
if (!fs.existsSync(TELEMETRY_PATH)) return false;
|
|
94
|
-
const lines = fs.readFileSync(TELEMETRY_PATH, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
95
|
-
const cutoff = Date.now() - DEDUP_WINDOW_MS;
|
|
96
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
97
|
-
let entry;
|
|
98
|
-
try { entry = JSON.parse(lines[i]); } catch { continue; }
|
|
99
|
-
const ts = Number(entry.timestamp_ms || 0);
|
|
100
|
-
if (ts && ts < cutoff) break;
|
|
101
|
-
if (entry.dedup_key === key && !entry.suppressed) return true;
|
|
102
|
-
}
|
|
103
|
-
return false;
|
|
104
|
-
} catch {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function appendTelemetry(record) {
|
|
110
|
-
try {
|
|
111
|
-
fs.mkdirSync(path.dirname(TELEMETRY_PATH), { recursive: true });
|
|
112
|
-
fs.appendFileSync(TELEMETRY_PATH, JSON.stringify(record) + '\n', 'utf8');
|
|
113
|
-
} catch { /* best-effort */ }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Auto-invoke the snapshot CLI as a detached process so the hook returns fast
|
|
117
|
-
// and the snapshot write does not depend on the agent remembering. Errors are
|
|
118
|
-
// swallowed — never block the user turn.
|
|
119
|
-
function invokeHandoffSnapshot(trigger, milestone, sessionId, transcriptPath) {
|
|
120
|
-
try {
|
|
121
|
-
const { spawn } = require('child_process');
|
|
122
|
-
const cli = path.join(PROJECT_DIR, '.specialist', 'Ansuz', 'tools', 'ansuz-handoff-snapshot.js');
|
|
123
|
-
if (!fs.existsSync(cli)) return;
|
|
124
|
-
const args = [cli, `--trigger=${trigger}`];
|
|
125
|
-
if (milestone) args.push(`--last-milestone=${milestone}`);
|
|
126
|
-
if (sessionId) args.push(`--session-id=${sessionId}`);
|
|
127
|
-
// Scopes pending/unfinished to tasks this conversation touched.
|
|
128
|
-
if (transcriptPath) args.push(`--transcript-path=${transcriptPath}`);
|
|
129
|
-
const child = spawn('node', args, {
|
|
130
|
-
detached: true,
|
|
131
|
-
stdio: 'ignore',
|
|
132
|
-
cwd: PROJECT_DIR,
|
|
133
|
-
windowsHide: true,
|
|
134
|
-
});
|
|
135
|
-
child.unref();
|
|
136
|
-
} catch { /* best-effort */ }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function main() {
|
|
140
|
-
const payload = readStdin();
|
|
141
|
-
const inscribeType = extractInscribeType(payload);
|
|
142
|
-
|
|
143
|
-
// Only fire for decision/risk/ash types
|
|
144
|
-
if (!inscribeType || !TRIGGER_TYPES.has(inscribeType)) {
|
|
145
|
-
process.exit(0);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Ansuz-only: see handoff-snapshot-on-commit.js.
|
|
149
|
-
if (!require('./_ansuz-conversation').isAnsuzConversation(payload.transcript_path, PROJECT_DIR)) {
|
|
150
|
-
process.exit(0);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const runeId = extractRuneId(payload);
|
|
154
|
-
const content = payload?.tool_input?.content || payload?.toolInput?.content || '';
|
|
155
|
-
const dedupKey = DEDUP_KEY_PREFIX + normalizeKey(runeId, inscribeType, content);
|
|
156
|
-
const suppressed = isDeduped(dedupKey);
|
|
157
|
-
|
|
158
|
-
appendTelemetry({
|
|
159
|
-
timestamp: new Date().toISOString(),
|
|
160
|
-
timestamp_ms: Date.now(),
|
|
161
|
-
hook: 'handoff-snapshot-on-decision',
|
|
162
|
-
dedup_key: dedupKey,
|
|
163
|
-
inscribe_type: inscribeType,
|
|
164
|
-
rune_id: runeId,
|
|
165
|
-
suppressed,
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
if (suppressed) {
|
|
169
|
-
process.exit(0);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const runeRef = runeId ? ` (rune_id: ${runeId})` : '';
|
|
173
|
-
|
|
174
|
-
// Auto-write the handoff (system-level enforcement, instance-independent).
|
|
175
|
-
// TASK_1837: pass NO --last-milestone. The old value ('<type> rune inscribed')
|
|
176
|
-
// carried zero information and became the slot's current_topic, clobbering the
|
|
177
|
-
// real work topic (measured 2026-07-31: 5 of 8 orphan slots had it as topic —
|
|
178
|
-
// garbage inherited by the next session). With no milestone the snapshot CLI
|
|
179
|
-
// derives the topic from the latest commit subject: actual work content. The
|
|
180
|
-
// rune reference stays in the stdout nudge below, where it is useful.
|
|
181
|
-
invokeHandoffSnapshot('live_decision_point', null, payload.session_id, payload.transcript_path);
|
|
182
|
-
|
|
183
|
-
process.stdout.write(
|
|
184
|
-
'\n' + fenceSignal('handoff', `🔁 ${inscribeType} rune inscribed${runeRef}. Handoff auto-snapshot dispatched (live_decision_point).`) + '\n'
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
main();
|