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.
Files changed (131) hide show
  1. package/core/.claude/skills/code_reviewer/SKILL.md +0 -1
  2. package/core/.claude/skills/code_reviewer/patterns.yaml +0 -8
  3. package/core/.claude/skills/dev_agent/SKILL.md +3 -6
  4. package/core/.claude/skills/heimdall/SKILL.md +5 -6
  5. package/core/.claude/skills/huginn/SKILL.md +11 -38
  6. package/core/.collaboration/core/task-2288-followups.test.js +2 -37
  7. package/core/.collaboration/core/task-2421-replica.test.js +229 -0
  8. package/core/.collaboration/core/task-api.js +223 -230
  9. package/core/.collaboration/core/task-api.test.js +1 -138
  10. package/core/.collaboration/core/task-replica.js +189 -0
  11. package/core/.formation/.merge-lock +1 -0
  12. package/core/.formation/bifrost/codex-doctor.js +12 -2
  13. package/core/.formation/bifrost/codex-doctor.test.js +15 -0
  14. package/core/.formation/bifrost/launch.js +2 -2
  15. package/core/.formation/bifrost/launch.test.js +2 -2
  16. package/core/.formation/bifrost/setup-codex.js +84 -16
  17. package/core/.formation/bifrost/shared/ansuz-boot-policy.js +3 -3
  18. package/core/.formation/bifrost/shared/codex-injection.js +3 -17
  19. package/core/.formation/bifrost/shared/consolidate.js +1 -51
  20. package/core/.formation/bifrost/sync.js +4 -5
  21. package/core/.formation/tools/__tests__/subagent-bifrost-guard.test.js +1 -1
  22. package/core/.formation/tools/dispatch.js +2 -2
  23. package/core/.formation/tools/kenaz-stats.js +1 -1
  24. package/core/.formation/tools/mcp-allowlist.test.js +5 -3
  25. package/core/.formation/tools/post-complete/bounded-http.js +1 -40
  26. package/core/.formation/tools/post-complete/bounded-http.test.js +2 -89
  27. package/core/.formation/tools/post-complete/cognitive.js +2 -119
  28. package/core/.formation/tools/post-complete/task2420.test.js +129 -0
  29. package/core/.formation/tools/post-complete/testing.js +3 -21
  30. package/core/.formation/tools/post-complete.js +131 -436
  31. package/core/.formation/tools/post-complete.test.js +0 -135
  32. package/core/.formation/tools/route-audit.test.js +8 -20
  33. package/core/.kenaz/specialist/memory_guard_fired.json +1 -1
  34. package/core/.kenaz/specialist/memory_guard_trigger_cache.json +1 -1
  35. package/core/.rule/INDEX.yaml +3 -3
  36. package/core/.rule/general/ddx-synapse-stack.yaml +11 -11
  37. package/core/.rule/general/specialist-identity-contract.yaml +5 -5
  38. package/core/.rule/general/synapse-code-intelligence.yaml +25 -111
  39. package/core/.rule/general/worktree-isolation.yaml +2 -2
  40. package/core/.rule/pipeline/task-execution.yaml +6 -9
  41. package/core/.specialist/Amelia/README.md +5 -5
  42. package/core/.specialist/Amelia/capability.yaml +10 -18
  43. package/core/.specialist/Amelia/knowledge.yaml +15 -66
  44. package/core/.specialist/Amelia/workflow.yaml +12 -21
  45. package/core/.specialist/Ansuz/capability.yaml +12 -87
  46. package/core/.specialist/Ansuz/concepts/autonomous_agenda.yaml +1 -1
  47. package/core/.specialist/Ansuz/concepts/autonomy_contract.yaml +2 -3
  48. package/core/.specialist/Ansuz/concepts/capability_pointers.yaml +1 -3
  49. package/core/.specialist/Ansuz/concepts/cognitive_fluency_contract.yaml +8 -21
  50. package/core/.specialist/Ansuz/concepts/cognitive_systems_overview.yaml +0 -4
  51. package/core/.specialist/Ansuz/concepts/cognitive_systems_turn_context.yaml +1 -1
  52. package/core/.specialist/Ansuz/concepts/completion_tracking.yaml +0 -1
  53. package/core/.specialist/Ansuz/concepts/deep_knowledge_pointers.yaml +0 -3
  54. package/core/.specialist/Ansuz/concepts/evolvable_guardrails.yaml +3 -3
  55. package/core/.specialist/Ansuz/concepts/fatigue_handoff.yaml +1 -7
  56. package/core/.specialist/Ansuz/concepts/guardrails_immutable.yaml +3 -4
  57. package/core/.specialist/Ansuz/concepts/identity.yaml +1 -2
  58. package/core/.specialist/Ansuz/concepts/phase9_verification.yaml +0 -1
  59. package/core/.specialist/Ansuz/concepts/phase_protocol.yaml +3 -4
  60. package/core/.specialist/Ansuz/core.yaml +3 -6
  61. package/core/.specialist/Ansuz/elixir.yaml +6 -6
  62. package/core/.specialist/Ansuz/exo_cortex_tools.yaml +5 -9
  63. package/core/.specialist/Ansuz/guards/blind_spot_guards.yaml +2 -2
  64. package/core/.specialist/Ansuz/guards/lesson_guards.yaml +6 -19
  65. package/core/.specialist/Ansuz/identity_contract.yaml +4 -4
  66. package/core/.specialist/Ansuz/knowledge.yaml +13 -40
  67. package/core/.specialist/Ansuz/modules/ddx.yaml +2 -2
  68. package/core/.specialist/Ansuz/modules/prophecy.yaml +19 -19
  69. package/core/.specialist/Ansuz/modules/pulse_empty.yaml +6 -17
  70. package/core/.specialist/Ansuz/tools/ansuz-handoff-snapshot.js +3 -102
  71. package/core/.specialist/Ansuz/tools/ansuz-sdk-runner.js +2 -44
  72. package/core/.specialist/Ansuz/workflow.yaml +32 -76
  73. package/core/.specialist/Forseti/core.yaml +2 -7
  74. package/core/.specialist/Forseti/rule.yaml +1 -1
  75. package/core/.specialist/Heimdall/SKILL.md +2 -3
  76. package/core/.specialist/Heimdall/capability.yaml +22 -45
  77. package/core/.specialist/Heimdall/core.yaml +24 -126
  78. package/core/.specialist/Heimdall/principles.yaml +8 -11
  79. package/core/.specialist/Heimdall/rule.yaml +2 -2
  80. package/core/.specialist/Huginn/capability.yaml +31 -52
  81. package/core/.specialist/Huginn/core.yaml +43 -123
  82. package/core/.specialist/Huginn/principles.yaml +5 -5
  83. package/core/.specialist/Huginn/rule.yaml +3 -9
  84. package/core/.specialist/REFLECTION_TO_CONTROL.md +3 -4
  85. package/core/.specialist/shared/guard_schema.json +1 -1
  86. package/core/.specialist/shared/principles.yaml +6 -6
  87. package/core/.specialist/tools/__tests__/rea-detect.test.js +10 -0
  88. package/core/.specialist/tools/phase-gate.js +10 -230
  89. package/core/.specialist/tools/phase-gate.test.js +2 -2
  90. package/core/.specialist/tools/prompt-files.test.js +1 -1
  91. package/core/.specialist/tools/rea-detect.js +6 -2
  92. package/core/.specialist/tools/session.js +2 -89
  93. package/core/.synapse/memory-shared.json.sync +1 -0
  94. package/core/.system/memory/README.md +3 -0
  95. package/core/CLAUDE.md +6 -7
  96. package/core/KENAZ_CORE_VERSION +1 -1
  97. package/core/dev/plans/CLI_FIRST_INSTALL_FLOW.md +75 -83
  98. package/core/dev/specs/peer/CODEX_MANAGED_DAEMON.md +54 -0
  99. package/core/dev/specs/peer/setup-codex-hooks.test.js +38 -0
  100. package/core/dev/spikes/task2145-evidence-20261004/backend-log-excerpt.txt +56 -0
  101. package/core/dev/spikes/task2145-evidence-20261004/graph.shadow +0 -0
  102. package/core/dev/spikes/task2145-evidence-20261004/graph.wal +0 -0
  103. package/core/dev/spikes/task2145-evidence-20261004/graph_corrupt_suspect.marker +1 -0
  104. package/core/plugins/kenaz/INVENTORY.md +1 -1
  105. package/core/plugins/kenaz/scripts/kenaz.test.js +70 -1
  106. package/core/plugins/kenaz/scripts/ygg.js +45 -4
  107. package/core/plugins/kenaz/skills/code_reviewer/SKILL.md +0 -1
  108. package/core/plugins/kenaz/skills/code_reviewer/patterns.yaml +0 -8
  109. package/core/plugins/kenaz/skills/dev_agent/SKILL.md +3 -6
  110. package/lib/cli.js +17 -9
  111. package/lib/cli.test.js +10 -0
  112. package/lib/dashboard.test.js +3 -3
  113. package/package.json +6 -6
  114. package/vendor/install-kenaz-cli.js +167 -16
  115. package/core/.claude/hooks/handoff-snapshot-on-decision.js +0 -188
  116. package/core/.claude/skills/synapse_maintain/SKILL.md +0 -84
  117. package/core/.collaboration/core/task-2264-single-after-capture.test.js +0 -61
  118. package/core/.formation/tools/alaya-gate.js +0 -198
  119. package/core/.formation/tools/post-complete/health.js +0 -464
  120. package/core/.formation/tools/post-complete/task2038.test.js +0 -246
  121. package/core/.formation/tools/post-complete/task2341.test.js +0 -74
  122. package/core/.rule/general/cortex-tools.yaml +0 -181
  123. package/core/.rule/general/memory-language.yaml +0 -67
  124. package/core/.rule/general/memory-source-priority.yaml +0 -207
  125. package/core/.rule/general/synapse-cognitive-fusion.yaml +0 -301
  126. package/core/.specialist/Ansuz/concepts/cognitive_systems_hippocampus.yaml +0 -14
  127. package/core/.specialist/Ansuz/concepts/write_verification.yaml +0 -26
  128. package/core/.specialist/Ansuz/modules/recall.yaml +0 -45
  129. package/core/.specialist/tools/MEMORY_RECALL_TRIGGERS.md +0 -156
  130. package/core/.specialist/tools/context-injection.js +0 -395
  131. package/core/dev/scripts/probe/hippocampus-concurrency.js +0 -247
@@ -17,11 +17,10 @@ const assert = require('assert');
17
17
  const fs = require('fs').promises;
18
18
  const path = require('path');
19
19
  const os = require('os');
20
- const { TaskAPI, CONFIG, parseYAML, toYAML, loadAlayaGate, echoAlayaLessons } = require('./task-api.js');
20
+ const { TaskAPI, CONFIG, parseYAML, toYAML } = require('./task-api.js');
21
21
  // TASK_2104: same shared gate task-api.js loads at runtime — used here only
22
22
  // to read its constants for assertions (query building + gate behavior
23
23
  // itself is covered by prompt.test.js against the one implementation).
24
- const alayaGate = require('../../.formation/tools/alaya-gate.js');
25
24
 
26
25
  // ============================================================================
27
26
  // 測試工具
@@ -1129,140 +1128,4 @@ runner.test('shared-mode: id_alloc stamp on task when local_fallback', async ()
1129
1128
  });
1130
1129
 
1131
1130
 
1132
- // ============================================================================
1133
- // 測試案例: TASK_1992 — Alaya process-lessons echo (create command)
1134
- // ============================================================================
1135
-
1136
- /**
1137
- * Captures console.log calls made during fn() and returns them as an array
1138
- * of joined-argument strings, restoring console.log afterward.
1139
- */
1140
- async function captureLogs(fn) {
1141
- const original = console.log;
1142
- const logs = [];
1143
- console.log = (...a) => logs.push(a.join(' '));
1144
- try {
1145
- await fn();
1146
- } finally {
1147
- console.log = original;
1148
- }
1149
- return logs;
1150
- }
1151
-
1152
- function mcpTextResponse(bodyObj) {
1153
- return {
1154
- ok: true,
1155
- json: async () => ({
1156
- jsonrpc: '2.0', id: 1,
1157
- result: { content: [{ type: 'text', text: JSON.stringify(bodyObj) }] },
1158
- }),
1159
- };
1160
- }
1161
-
1162
- const ALAYA_TASK_DATA = { title: 'Fix appendCortexContext dedup bug', scope_paths: ['.formation/tools/prompt.js'] };
1163
-
1164
- runner.test('alaya-gate buildTaskContextQuery (shared with prompt.js): builds a query from title + scope_paths, null when empty', () => {
1165
- const q = alayaGate.buildTaskContextQuery(ALAYA_TASK_DATA);
1166
- assert.ok(q && q.includes('prompt'));
1167
- assert.strictEqual(alayaGate.buildTaskContextQuery({}), null);
1168
- });
1169
-
1170
- runner.test('echoAlayaLessons: requests the candidate pool with min_cosine, prints up to 3 gated seeds from a mocked fetch', async () => {
1171
- let calledUrl = null;
1172
- let calledOpts = null;
1173
- const fetchMock = async (url, opts) => {
1174
- calledUrl = url;
1175
- calledOpts = opts;
1176
- return mcpTextResponse({
1177
- // js/.formation stack overlap with ALAYA_TASK_DATA's scope_paths, and
1178
- // cosine clears ALAYA_MIN_COSINE_DEFAULT — both must pass the gate.
1179
- bijas: [
1180
- { id: 'a1', seed_type: 'failure_seed', origin: 'local', content: 'Root cause one.', cosine: 0.75, tags: 'js' },
1181
- { id: 'a2', seed_type: 'lesson_seed', origin: 'remote', content: 'Lesson two.', cosine: 0.72, tags: 'js' },
1182
- ],
1183
- mode_effective: 'hybrid',
1184
- });
1185
- };
1186
-
1187
- const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
1188
-
1189
- assert.strictEqual(calledUrl, 'http://127.0.0.1:18080/api/mcp');
1190
- const body = JSON.parse(calledOpts.body);
1191
- assert.strictEqual(body.params.name, 'alaya_recall');
1192
- assert.strictEqual(body.params.arguments.mode, 'hybrid');
1193
- assert.deepStrictEqual(body.params.arguments.seed_types, ['failure_seed', 'lesson_seed']);
1194
- assert.strictEqual(body.params.arguments.limit, alayaGate.ALAYA_CANDIDATE_POOL);
1195
- assert.strictEqual(body.params.arguments.min_cosine, alayaGate.ALAYA_MIN_COSINE_DEFAULT);
1196
-
1197
- assert.ok(logs.some(l => l.includes('Alaya lessons for this card:')));
1198
- assert.ok(logs.some(l => l.includes('[failure_seed/local] Root cause one.')));
1199
- assert.ok(logs.some(l => l.includes('[lesson_seed/remote] Lesson two.')));
1200
- });
1201
-
1202
- runner.test('echoAlayaLessons: TASK_2104 gate drops a below-floor bija and never prints it', async () => {
1203
- const fetchMock = async () => mcpTextResponse({
1204
- bijas: [
1205
- // Below ALAYA_MIN_COSINE_DEFAULT (0.70) — must be dropped even though
1206
- // it stack-matches (tags: 'js') and would render fine otherwise.
1207
- { id: 'b1', seed_type: 'failure_seed', origin: 'local', content: 'Stale noise seed.', cosine: 0.5, tags: 'js' },
1208
- ],
1209
- mode_effective: 'hybrid',
1210
- });
1211
- const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
1212
- assert.strictEqual(logs.length, 0, 'a below-floor bija must print nothing, not fall back to ungated output');
1213
- });
1214
-
1215
- runner.test('echoAlayaLessons: TASK_2104 gate keeps an above-floor stack-matching bija', async () => {
1216
- const fetchMock = async () => mcpTextResponse({
1217
- bijas: [
1218
- { id: 'c1', seed_type: 'lesson_seed', origin: 'local', content: 'On-topic lesson.', cosine: 0.8, tags: 'js' },
1219
- ],
1220
- mode_effective: 'hybrid',
1221
- });
1222
- const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
1223
- assert.ok(logs.some(l => l.includes('[lesson_seed/local] On-topic lesson.')));
1224
- });
1225
-
1226
- runner.test('echoAlayaLessons: gate module unavailable (e.g. sub-project with no .formation/) silently skips, never falls back to ungated output', async () => {
1227
- let fetchCalled = false;
1228
- const fetchMock = async () => { fetchCalled = true; return mcpTextResponse({ bijas: [{ id: 'd1', seed_type: 'failure_seed', origin: 'local', content: 'never printed' }] }); };
1229
- const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock, null));
1230
- assert.strictEqual(logs.length, 0);
1231
- assert.strictEqual(fetchCalled, false, 'must return before ever calling fetch when the gate cannot be loaded');
1232
- });
1233
-
1234
- runner.test('echoAlayaLessons: silent when fetch rejects (network error / timeout)', async () => {
1235
- const fetchMock = async () => { throw new Error('AbortError: The operation was aborted'); };
1236
- const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
1237
- assert.strictEqual(logs.length, 0);
1238
- });
1239
-
1240
- runner.test('echoAlayaLessons: silent when fetch resolves non-ok', async () => {
1241
- const fetchMock = async () => ({ ok: false, status: 500 });
1242
- const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
1243
- assert.strictEqual(logs.length, 0);
1244
- });
1245
-
1246
- runner.test('echoAlayaLessons: silent when the MCP result has zero bijas', async () => {
1247
- const fetchMock = async () => mcpTextResponse({ bijas: [], mode_effective: 'hybrid' });
1248
- const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
1249
- assert.strictEqual(logs.length, 0);
1250
- });
1251
-
1252
- runner.test('echoAlayaLessons: KENAZ_NO_ALAYA_ECHO=1 suppresses the echo entirely (fetch never called)', async () => {
1253
- let fetchCalled = false;
1254
- const fetchMock = async () => { fetchCalled = true; return mcpTextResponse({ bijas: [{ id: 'x', seed_type: 'failure_seed', origin: 'local', content: 'never printed' }] }); };
1255
-
1256
- const prev = process.env.KENAZ_NO_ALAYA_ECHO;
1257
- process.env.KENAZ_NO_ALAYA_ECHO = '1';
1258
- try {
1259
- const logs = await captureLogs(() => echoAlayaLessons(ALAYA_TASK_DATA, fetchMock));
1260
- assert.strictEqual(logs.length, 0);
1261
- assert.strictEqual(fetchCalled, false);
1262
- } finally {
1263
- if (prev === undefined) delete process.env.KENAZ_NO_ALAYA_ECHO;
1264
- else process.env.KENAZ_NO_ALAYA_ECHO = prev;
1265
- }
1266
- });
1267
-
1268
1131
  runner.run();
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+ /**
3
+ * task-replica.js (TASK_2421)
4
+ *
5
+ * Shared-room card replicas. Task IDs are allocated room-wide via relay leases
6
+ * but card files live only on the creating node (.kenaz/tasks is gitignored), so
7
+ * other nodes got "任務不存在" for ids they saw in git/messages. When the project
8
+ * is in an active shared room, `task-api create` also publishes a READ-ONLY
9
+ * replica of the card to the relay tasks table, and `get/update/move/complete`
10
+ * on an unknown id look that replica up.
11
+ *
12
+ * Why status 'assigned' with NO assigned_to (no relay change needed):
13
+ * - GET /api/tasks defaults to status=pool, so the pool list never shows it
14
+ * - kenaz_pool_pull (inbox) filters assigned_to == this node, so the creator's
15
+ * own inbox does not fill with replicas either
16
+ * - 'assigned' is already a valid, non-pool relay status
17
+ * Residual: relay claim is not status-gated, so an explicit claim by exact id
18
+ * would still succeed; replicas are documented as non-claimable (full_yaml
19
+ * carries `replica: true`).
20
+ *
21
+ * Everything here is best-effort and never throws into a caller: a relay problem
22
+ * must never fail a local create/move.
23
+ */
24
+
25
+ const http = require('http');
26
+ const https = require('https');
27
+ const path = require('path');
28
+
29
+ const TIMEOUT_MS = 3000;
30
+ // local status dir -> relay status. Backlog/pending stays the non-pool 'assigned'.
31
+ const RELAY_STATUS = {
32
+ backlog: 'assigned', pending: 'assigned',
33
+ active: 'in_progress', blocked: 'blocked', done: 'done', cancel: 'cancelled',
34
+ };
35
+ const ALL_STATUSES = 'assigned,in_progress,blocked,done,cancelled,completed,claimed,pool';
36
+
37
+ /** Shared-room condition, identical to generateTaskID's. */
38
+ function isSharedRoom(yggConfig) {
39
+ return !!(yggConfig && yggConfig.enabled !== false &&
40
+ yggConfig.relay_url && String(yggConfig.relay_url).trim() !== '' &&
41
+ yggConfig.shared_task_ids !== false);
42
+ }
43
+
44
+ /** Replica publishing: shared room AND not opted out (`share_cards: false`). */
45
+ function shouldShareCards(yggConfig) {
46
+ return isSharedRoom(yggConfig) && yggConfig.share_cards !== false;
47
+ }
48
+
49
+ function projectOf(yggConfig, projectRoot) {
50
+ return (yggConfig.yggdrasil_project && String(yggConfig.yggdrasil_project).trim()) ||
51
+ path.basename(projectRoot) || 'default';
52
+ }
53
+
54
+ function nodeOf(yggConfig, gitUserName) {
55
+ return (yggConfig.node_name && String(yggConfig.node_name).trim()) || gitUserName || 'unknown';
56
+ }
57
+
58
+ function relayRequest(yggConfig, key, method, pathAndQuery, bodyObj) {
59
+ return new Promise((resolve, reject) => {
60
+ const base = String(yggConfig.relay_url).replace(/^wss:\/\//, 'https://').replace(/^ws:\/\//, 'http://');
61
+ let url;
62
+ try { url = new URL(pathAndQuery, base); } catch { reject(new Error('invalid relay_url')); return; }
63
+ const isHttps = url.protocol === 'https:';
64
+ const body = bodyObj !== undefined ? JSON.stringify(bodyObj) : null;
65
+ let settled = false;
66
+ const req = (isHttps ? https : http).request({
67
+ hostname: url.hostname, port: url.port || (isHttps ? 443 : 80),
68
+ path: url.pathname + url.search, method,
69
+ headers: {
70
+ ...(body ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } : {}),
71
+ ...(key ? { Authorization: `Bearer ${key}` } : {}),
72
+ },
73
+ }, (res) => {
74
+ let data = '';
75
+ res.on('data', (c) => { data += c; });
76
+ res.on('end', () => {
77
+ if (settled) return; settled = true;
78
+ if (res.statusCode < 200 || res.statusCode >= 300) { reject(new Error(`relay HTTP ${res.statusCode}`)); return; }
79
+ try { resolve(data ? JSON.parse(data) : null); } catch (e) { reject(new Error(`relay parse error: ${e.message}`)); }
80
+ });
81
+ });
82
+ req.on('error', (e) => { if (!settled) { settled = true; reject(e); } });
83
+ const timer = setTimeout(() => { if (!settled) { settled = true; req.destroy(); reject(new Error('relay timeout')); } }, TIMEOUT_MS);
84
+ req.on('close', () => clearTimeout(timer));
85
+ if (body) req.write(body);
86
+ req.end();
87
+ });
88
+ }
89
+
90
+ /** Plain-object copy of the card without internal `_` keys. */
91
+ function cleanCard(task) {
92
+ const out = {};
93
+ for (const [k, v] of Object.entries(task || {})) if (!k.startsWith('_')) out[k] = v;
94
+ return out;
95
+ }
96
+
97
+ /**
98
+ * Publish a replica. ctx = { yggConfig, key, projectRoot, node, toYAML }.
99
+ * Returns {ok:true} | {ok:false, error}.
100
+ */
101
+ async function publishReplica(ctx, task, localStatusDir) {
102
+ try {
103
+ const card = cleanCard(task);
104
+ const replicaCard = {
105
+ replica: true,
106
+ origin_node: ctx.node,
107
+ id: card.id, title: card.title, description: card.description,
108
+ priority: card.priority, status: card.status, alias: card.alias || null,
109
+ requirements: card.requirements || [], acceptance_criteria: card.acceptance_criteria || [],
110
+ created_by: card.created_by || null,
111
+ };
112
+ await relayRequest(ctx.yggConfig, ctx.key, 'POST', '/api/tasks', {
113
+ task_id: task.id,
114
+ project: projectOf(ctx.yggConfig, ctx.projectRoot),
115
+ status: RELAY_STATUS[localStatusDir || 'backlog'] || 'assigned',
116
+ priority: card.priority || 'P2',
117
+ title: card.title,
118
+ description: card.description,
119
+ created_by: ctx.node,
120
+ full_yaml: ctx.toYAML(replicaCard),
121
+ });
122
+ return { ok: true };
123
+ } catch (e) {
124
+ return { ok: false, error: e.message };
125
+ }
126
+ }
127
+
128
+ /** Push a status (and refreshed card) to an existing replica row. */
129
+ async function syncReplica(ctx, task, localStatusDir) {
130
+ try {
131
+ const card = cleanCard(task);
132
+ const body = {
133
+ project: projectOf(ctx.yggConfig, ctx.projectRoot),
134
+ title: card.title, description: card.description, priority: card.priority,
135
+ full_yaml: ctx.toYAML({
136
+ replica: true, origin_node: ctx.node, id: card.id, title: card.title,
137
+ description: card.description, priority: card.priority, status: card.status,
138
+ alias: card.alias || null, requirements: card.requirements || [],
139
+ acceptance_criteria: card.acceptance_criteria || [], created_by: card.created_by || null,
140
+ }),
141
+ };
142
+ if (localStatusDir && RELAY_STATUS[localStatusDir]) body.status = RELAY_STATUS[localStatusDir];
143
+ await relayRequest(ctx.yggConfig, ctx.key, 'PATCH',
144
+ `/api/tasks/${encodeURIComponent(task.id)}?project=${encodeURIComponent(body.project)}`, body);
145
+ return { ok: true };
146
+ } catch (e) {
147
+ return { ok: false, error: e.message };
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Look a card up on the relay by TASK id or alias. Returns
153
+ * { found:true, row, replica } | { found:false } | { found:false, error }.
154
+ * `replica` is the parsed full_yaml (or a minimal object from row columns).
155
+ */
156
+ async function fetchReplica(ctx, idOrAlias, parseYAML) {
157
+ try {
158
+ const project = projectOf(ctx.yggConfig, ctx.projectRoot);
159
+ const rows = await relayRequest(ctx.yggConfig, ctx.key, 'GET',
160
+ `/api/tasks?project=${encodeURIComponent(project)}&status=${ALL_STATUSES}&limit=1000&include_blocked=1`);
161
+ const list = Array.isArray(rows) ? rows : [];
162
+ const want = String(idOrAlias).toLowerCase();
163
+ for (const row of list) {
164
+ let card = null;
165
+ try { card = row.full_yaml ? parseYAML(row.full_yaml) : null; } catch { card = null; }
166
+ const alias = card && card.alias ? String(card.alias).toLowerCase() : null;
167
+ if (String(row.task_id).toLowerCase() === want || (alias && alias === want)) {
168
+ return { found: true, row, replica: card && Object.keys(card).length ? card : {
169
+ id: row.task_id, title: row.title, description: row.description, priority: row.priority,
170
+ } };
171
+ }
172
+ }
173
+ return { found: false };
174
+ } catch (e) {
175
+ return { found: false, error: e.message };
176
+ }
177
+ }
178
+
179
+ /** Human-readable "read-only replica" notice. */
180
+ function replicaNotice(taskId, row, replica) {
181
+ const node = (replica && replica.origin_node) || (row && row.created_by) || 'unknown';
182
+ return `read-only replica; card lives on node ${node}. ` +
183
+ `To track it locally: node .collaboration/core/task-api.js pull ${taskId}`;
184
+ }
185
+
186
+ module.exports = {
187
+ isSharedRoom, shouldShareCards, projectOf, nodeOf, relayRequest,
188
+ publishReplica, syncReplica, fetchReplica, replicaNotice, RELAY_STATUS,
189
+ };
@@ -0,0 +1 @@
1
+ {"pid":32452,"taskId":"TASK_2356","acquiredAt":"2026-10-09T10:19:19.033Z"}
@@ -127,10 +127,20 @@ function diagnose() {
127
127
  if (unreg.length) notes.push('Kenaz hooks not registered for: ' + unreg.join(', ') + '. Run: node .formation/bifrost/setup-codex.js');
128
128
  if (untrusted.length) notes.push('Kenaz hooks registered but NOT trusted (' + untrusted.join(', ') + '); codex does not run them. Open codex, type /hooks, and trust the kenaz entries (re-trust after any change to the hook command).');
129
129
  if (trust.UserPromptSubmit.registered && !trust.UserPromptSubmit.pull) notes.push('UserPromptSubmit hook lacks --pull, so independently opened codex sessions receive no peer mail. Remove the kenaz hook blocks from config.toml and re-run setup-codex.js.');
130
- return { trust, delivery_mode: deliveryMode(trust), ok: notes.length === 0, notes };
130
+ const managed = managedDaemon();
131
+ const mode = deliveryMode(trust);
132
+ if (managed && mode === 'pull-on-prompt') notes.push(MANAGED_NOTE);
133
+ return { trust, delivery_mode: mode, managed_daemon: managed, ok: notes.filter((n) => n !== MANAGED_NOTE).length === 0, notes };
131
134
  }
132
135
 
133
- module.exports = { promptText, skillText, installPrompts, installAgentsBlock, removeAgentsBlock, agentsBlock, hookTrust, deliveryMode, diagnose, SHIM_REL, EVENTS };
136
+ // TASK_2419: Codex desktop managed app-server daemon (unix control socket / pid file under CODEX_HOME).
137
+ function managedDaemon(home) {
138
+ const h = home || codexHome();
139
+ return fs.existsSync(path.join(h, 'app-server-control', 'app-server-control.sock')) || fs.existsSync(path.join(h, 'app-server-daemon', 'daemon.pid'));
140
+ }
141
+ const MANAGED_NOTE = 'Codex managed daemon detected: Kenaz has no push channel into it. Peer mail arrives on your next prompt via the UserPromptSubmit --pull hook (delivery_mode pull-on-prompt).';
142
+
143
+ module.exports = { managedDaemon, MANAGED_NOTE, promptText, skillText, installPrompts, installAgentsBlock, removeAgentsBlock, agentsBlock, hookTrust, deliveryMode, diagnose, SHIM_REL, EVENTS };
134
144
 
135
145
  if (require.main === module) {
136
146
  if (process.argv.includes('--install-prompts')) console.log(JSON.stringify(installPrompts()));
@@ -67,3 +67,18 @@ test('global AGENTS.md gets one managed Kenaz block (wish / field report guidanc
67
67
  assert.doesNotMatch(after, /kenaz:begin|kenaz_wish_create/);
68
68
  delete process.env.CODEX_HOME;
69
69
  });
70
+
71
+ test('TASK_2419: managed daemon detected from CODEX_HOME markers; diagnose reports it without failing ok', () => {
72
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cx-md-'));
73
+ assert.strictEqual(D.managedDaemon(home), false); // control: empty home
74
+ fs.mkdirSync(path.join(home, 'app-server-control'));
75
+ fs.writeFileSync(path.join(home, 'app-server-control', 'app-server-control.sock'), '');
76
+ assert.strictEqual(D.managedDaemon(home), true);
77
+ process.env.CODEX_HOME = home;
78
+ fs.writeFileSync(path.join(home, 'config.toml'), cfg(true, true));
79
+ const d = D.diagnose();
80
+ assert.strictEqual(d.managed_daemon, true);
81
+ assert.strictEqual(d.delivery_mode, 'pull-on-prompt');
82
+ assert.ok(d.notes.includes(D.MANAGED_NOTE)); assert.strictEqual(d.ok, d.notes.length === 1); // the managed note alone is informational
83
+ delete process.env.CODEX_HOME;
84
+ });
@@ -276,7 +276,7 @@ function renderMemoryStartupDoc() {
276
276
 
277
277
  function renderOverride(prompt, bootContext, { policy = BOOT_POLICY, projectPath = PROJECT_PATH, memoryStartupDoc = '' } = {}) {
278
278
  const projectInstruction = projectPath
279
- ? `You are working on sub-project: ${projectPath}\nAll task-api.js commands must include --project-path="${projectPath}".\nAll MCP tool calls (synapse_*, hippocampus_*, etc.) must include projectPath="${projectPath}".\n\n`
279
+ ? `You are working on sub-project: ${projectPath}\nAll task-api.js commands must include --project-path="${projectPath}".\nAll MCP tool calls (kenaz_*) must include projectPath="${projectPath}".\n\n`
280
280
  : '';
281
281
  const codexDdxInjection = renderCodexAntiDrift();
282
282
 
@@ -491,7 +491,7 @@ async function main() {
491
491
  ? runFastBoot()
492
492
  : await fetchBootContext({ specialist: SPECIALIST, projectRoot: ROOT, projectPath: PROJECT_PATH || ROOT, includeCanary: false });
493
493
  const offlineNote = probe.reachable ? ''
494
- : '# Boot context availability\nInitial Kenaz MCP probe failed; live recall availability is not established. Local fast boot context is not proof of a successful synapse_status check.\n\n';
494
+ : '# Boot context availability\nInitial Kenaz MCP probe failed; server availability is not established. Local fast boot context is not proof of a successful kenaz_ping check.\n\n';
495
495
  writeOverride(prompt, offlineNote + bootCtx);
496
496
  // ── SessionStart hook (TASK_1230) ─────────────────────────────────────────
497
497
  // Fires after prompt is written (AGENTS.override.md exists) but before Codex
@@ -23,7 +23,7 @@ function mockRequest(scenario = 'offline') {
23
23
  if (scenario === 'incomplete') { res.complete = false; res.emit('close'); return; }
24
24
  if (scenario === 'oversized') { res.emit('data', 'x'.repeat(1024 * 1024 + 1)); return; }
25
25
  const body = {
26
- ok: JSON.stringify({ jsonrpc: '2.0', id: 1, result: { tools: [{ name: 'synapse_status' }] } }),
26
+ ok: JSON.stringify({ jsonrpc: '2.0', id: 1, result: { tools: [{ name: 'kenaz_ping' }] } }),
27
27
  malformed: '{', empty: '{"result":{"tools":[]}}',
28
28
  'rpc-error': '{"error":{"code":-1}}', null: 'null',
29
29
  }[scenario] || '{}';
@@ -135,7 +135,7 @@ test('offline default Ansuz continues once with elixir, truthful status and pinn
135
135
  assert.deepEqual(Array.from(effects.promptCalls[0].args.slice(-2)), ['--project-path', project]);
136
136
  assert.ok(effects.promptCalls[0].args.includes('--external-boot-context'));
137
137
  const override = effects.writes.find(write => write.name.endsWith('AGENTS.override.md'));
138
- assert.match(override.content, /live recall availability is not established/);
138
+ assert.match(override.content, /server availability is not established/);
139
139
  assert.match(override.content, /Fixture fast context banner/);
140
140
  assert.equal((override.content.match(/## Codex Bootstrap Policy \(fast\)/g) || []).length, 1);
141
141
  assert.ok(override.content.includes(`projectPath="${project}"`));
@@ -225,7 +225,7 @@ function uninstall(options = {}) {
225
225
  for (let n = 0; n < segs.length; n++) {
226
226
  const [h, body] = segs[n], nx = segs[n + 1];
227
227
  if (h && /^\s*\[\[hooks\.\w+\]\]\s*$/.test(h) && nx && nx[0] && /^\s*\[\[hooks\.\w+\.hooks\]\]/.test(nx[0]) &&
228
- nx[1].join('\n').split('\\').join('/').includes(HOOK_SHIM_REL)) { n++; res.hooks = 'removed'; continue; }
228
+ isKenazHandler(nx)) { n++; res.hooks = 'removed'; continue; } // TASK_2418: any checkout's shim, same identity rule as missingHookEvents
229
229
  out.push(...body.filter((l) => !/^# kenaz-dashboard native hooks/.test(l)));
230
230
  }
231
231
  if (res.hooks === 'removed' && !dryRun) fs.writeFileSync(cfg, out.join('\n').replace(/\n{3,}/g, '\n\n'), 'utf8');
@@ -283,23 +283,82 @@ function getHookShimPath(_projectRoot) {
283
283
  return path.resolve(__dirname, '..', '..', HOOK_SHIM_REL);
284
284
  }
285
285
 
286
+ // TASK_2418: what counts as a kenaz hook, and what counts as a CURRENT one.
287
+ // A handler is kenaz's when its command runs codex-hook-shim.js (any checkout). It is current only when it
288
+ // runs exactly this core's shim (absolute path, slashes normalized, Windows case-insensitive) and the
289
+ // UserPromptSubmit one carries --pull (TASK_2379). Stale ones are replaced in place, never duplicated; handlers
290
+ // that do not run our shim are never touched.
291
+ const HOOK_SHIM_NAME = path.posix.basename(HOOK_SHIM_REL);
292
+ const HOOK_MARKER = /^# kenaz-dashboard native hooks/;
293
+ const HANDLER_HEADER = /^\s*\[\[hooks\.(\w+)\.hooks\]\]\s*$/;
294
+ const GROUP_HEADER = /^\s*\[\[hooks\.(\w+)\]\]\s*$/;
295
+ const slashes = (s) => String(s).replace(/\\/g, '/');
296
+ const foldCase = (s) => (process.platform === 'win32' ? s.toLowerCase() : s);
297
+
298
+ function parseSegments(text) { // [header line | null, body lines]
299
+ const segs = [];
300
+ for (const line of text.split(/\r?\n/)) {
301
+ if (/^\s*\[/.test(line) || !segs.length) segs.push([/^\s*\[/.test(line) ? line : null, [line]]);
302
+ else segs[segs.length - 1][1].push(line);
303
+ }
304
+ return segs;
305
+ }
306
+ const isKenazHandler = (seg) => slashes(seg[1].join('\n')).includes(HOOK_SHIM_NAME);
307
+ function isCurrentHandler(ev, seg, projectRoot) {
308
+ const m = slashes(seg[1].join('\n')).match(/^\s*command\s*=\s*(.*)$/m);
309
+ if (!m) return false;
310
+ const cmd = foldCase(m[1]);
311
+ const shim = foldCase(slashes(getHookShimPath(projectRoot)));
312
+ const at = cmd.indexOf('"' + shim + '"');
313
+ if (at < 0) return false;
314
+ return ev !== 'UserPromptSubmit' || /(^|\s)--pull(\s|['"]|$)/.test(cmd.slice(at + shim.length + 2));
315
+ }
316
+ /** Per event: segment indexes of kenaz handlers, split into current / stale. */
317
+ function scanKenazHooks(text, projectRoot) {
318
+ const segs = parseSegments(text), found = {};
319
+ segs.forEach((seg, i) => {
320
+ const m = seg[0] && seg[0].match(HANDLER_HEADER);
321
+ if (!m || !HOOK_EVENTS.includes(m[1]) || !isKenazHandler(seg)) return;
322
+ const f = found[m[1]] || (found[m[1]] = { current: [], stale: [] });
323
+ (isCurrentHandler(m[1], seg, projectRoot) ? f.current : f.stale).push(i);
324
+ });
325
+ return { segs, found };
326
+ }
327
+ /** config text without the kenaz handlers of `events` (and the empty group each one leaves behind). */
328
+ function stripKenazHooks(text, events, projectRoot) {
329
+ const { segs, found } = scanKenazHooks(text, projectRoot);
330
+ const drop = new Set();
331
+ for (const ev of events) {
332
+ for (const i of [...(found[ev] ? found[ev].current : []), ...(found[ev] ? found[ev].stale : [])]) {
333
+ drop.add(i);
334
+ const prev = segs[i - 1], next = segs[i + 1];
335
+ const g = prev && prev[0] && prev[0].match(GROUP_HEADER);
336
+ if (g && g[1] === ev && !(next && next[0] && HANDLER_HEADER.test(next[0]) && next[0].includes('.' + ev + '.'))
337
+ && prev[1].slice(1).every((l) => !l.trim())) drop.add(i - 1);
338
+ }
339
+ }
340
+ const out = [];
341
+ segs.forEach((seg, i) => {
342
+ if (!drop.has(i)) { out.push(...seg[1]); return; }
343
+ out.push(...seg[1].slice(1).filter((l) => /^\s*#/.test(l) && !HOOK_MARKER.test(l))); // keep neighbours' comments
344
+ });
345
+ return out.join('\n');
346
+ }
347
+
286
348
  function areHooksAlreadyRegistered(projectRoot) {
287
349
  const configPath = getCodexConfigPath();
288
350
  if (!fs.existsSync(configPath)) return false;
289
- const content = fs.readFileSync(configPath, 'utf8');
290
- const shimPath = getHookShimPath(projectRoot);
291
- // Registered if config contains our shim path in any hooks entry
292
- return content.includes(HOOK_SHIM_REL) || content.includes(shimPath.split('\\').join('/'));
351
+ // at least one CURRENT kenaz handler: a block left by an old checkout does not run this core's shim
352
+ const { found } = scanKenazHooks(fs.readFileSync(configPath, 'utf8'), projectRoot);
353
+ return Object.values(found).some((f) => f.current.length > 0);
293
354
  }
294
355
 
295
- // TASK_2299: HOOK_EVENTS that have no [[hooks.EV.hooks]] block pointing at our shim in config.toml.
296
- function missingHookEvents() {
356
+ // TASK_2299: HOOK_EVENTS without exactly one current kenaz handler. TASK_2418: stale (old checkout path,
357
+ // UserPromptSubmit without --pull) and duplicated ones count as missing, so registerHooks rewrites them.
358
+ function missingHookEvents(projectRoot) {
297
359
  const configPath = getCodexConfigPath();
298
- const content = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8').replace(/\\/g, '/') : '';
299
- return HOOK_EVENTS.filter((ev) => {
300
- const blocks = content.match(new RegExp('\\[\\[hooks\\.' + ev + '\\.hooks\\]\\][^\\[]*', 'g')) || [];
301
- return !blocks.some((blk) => blk.includes(HOOK_SHIM_REL));
302
- });
360
+ const { found } = scanKenazHooks(fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '', projectRoot);
361
+ return HOOK_EVENTS.filter((ev) => !found[ev] || found[ev].stale.length > 0 || found[ev].current.length !== 1);
303
362
  }
304
363
 
305
364
  // True when the codex CLI both supports hooks AND our shim is registered.
@@ -313,6 +372,7 @@ function nativeHooksActive(projectRoot) {
313
372
  function registerHooks(log, projectRoot, events) {
314
373
  const shimPath = getHookShimPath(projectRoot).split('\\').join('/');
315
374
  const configPath = getCodexConfigPath();
375
+ const evs = events || HOOK_EVENTS;
316
376
 
317
377
  // Ensure ~/.codex dir exists
318
378
  const codexDir = path.dirname(configPath);
@@ -322,8 +382,16 @@ function registerHooks(log, projectRoot, events) {
322
382
 
323
383
  // [[hooks.X]] array-of-tables entries are additive-safe TOML: appending is valid
324
384
  // even if the user already has their own [[hooks.X]] entries from other tools.
325
- const lines = ['', '# kenaz-dashboard native hooks (TASK_1733) -- auto-registered by setup-codex.js'];
326
- for (const ev of events || HOOK_EVENTS) {
385
+ // TASK_2418: kenaz handlers already present for these events (stale or not) are dropped first, so each
386
+ // event ends with exactly one current handler.
387
+ let kept = '';
388
+ try {
389
+ kept = fs.existsSync(configPath) ? stripKenazHooks(fs.readFileSync(configPath, 'utf8'), evs, projectRoot) : '';
390
+ if (!Object.keys(scanKenazHooks(kept, projectRoot).found).length) kept = kept.split('\n').filter((l) => !HOOK_MARKER.test(l)).join('\n');
391
+ } catch (err) { log('hook registration failed: ' + err.message); return { success: false, action: 'hooks_failed', error: err.message }; }
392
+ const lines = [''];
393
+ if (!kept.split('\n').some((l) => HOOK_MARKER.test(l))) lines.push('# kenaz-dashboard native hooks (TASK_1733) -- auto-registered by setup-codex.js');
394
+ for (const ev of evs) {
327
395
  lines.push(
328
396
  '[[hooks.' + ev + ']]',
329
397
  '',
@@ -337,8 +405,8 @@ function registerHooks(log, projectRoot, events) {
337
405
  const hookBlock = lines.join('\n');
338
406
 
339
407
  try {
340
- fs.appendFileSync(configPath, hookBlock, 'utf8');
341
- log('kenaz hooks appended to ' + configPath);
408
+ fs.writeFileSync(configPath, kept + (kept && !kept.endsWith('\n') ? '\n' : '') + hookBlock, 'utf8');
409
+ log('kenaz hooks written to ' + configPath);
342
410
  return { success: true, action: 'hooks_registered' };
343
411
  } catch (err) {
344
412
  log('hook registration failed: ' + err.message);
@@ -18,13 +18,13 @@ function resolveBootPolicy({ specialist = 'Ansuz', depth, bootMode, full = false
18
18
  function renderBootstrap({ policy, projectPath, port = '18080', contextual = false } = {}) {
19
19
  const fast = [
20
20
  'Ansuz fast boot (elixir) is the default. This bootstrap policy supersedes any older instruction to run Phase 0, including the Phase 0 sentence inside elixir.',
21
- 'Check synapse_status with the active projectPath when available. If unavailable, report that memory is unavailable and continue only read-only inspection and analysis using local evidence; do not claim recall succeeded.',
21
+ 'Check kenaz_ping with the active projectPath when available. If unavailable, report that the Kenaz server is unavailable and continue only read-only inspection and analysis using local evidence.',
22
22
  'Lightweight Phase 0 uses the supplied boot context and local project evidence; do not run phase-gate Phase 0 or its boot rituals in fast mode. This also supersedes legacy handoff instructions that promise Phase 0 archival. When offline, defer state-changing execution and memory-dependent work until the required service is restored.',
23
23
  'Existing authorization, scope, verification, task tracking, and merge restrictions remain in force.',
24
24
  ].join('\n');
25
25
  const command = `node .specialist/tools/phase-gate.js enter --phase=0 --specialist=${(policy?.specialist || 'Ansuz').toLowerCase()}${projectPath ? ` --project-path="${projectPath}"` : ''}`;
26
- const full = `Before running phase-gate enter, call synapse_status with the active projectPath. If it errors or times out, HALT and emit recovery: confirm Kenaz Dashboard on port ${port}, re-run setup-codex, and restart the Codex session.\nExecute Phase 0 initialization now: run \`${command}\` and follow its checklist.`;
27
- if (contextual) return `## Codex Bootstrap Policy\n\nFor Ansuz without an explicit full boot or legacy depth:\n${fast}\n\nFor explicit --full / --boot-mode full or kernel depth, and for other specialists: retain the full bootstrap guard; call synapse_status before Phase 0 and HALT with recovery instructions if it is unavailable. Use the active specialist and project-path for Phase 0.\n`;
26
+ const full = `Before running phase-gate enter, call kenaz_ping with the active projectPath. If it errors or times out, HALT and emit recovery: confirm Kenaz Dashboard on port ${port}, re-run setup-codex, and restart the Codex session.\nExecute Phase 0 initialization now: run \`${command}\` and follow its checklist.`;
27
+ if (contextual) return `## Codex Bootstrap Policy\n\nFor Ansuz without an explicit full boot or legacy depth:\n${fast}\n\nFor explicit --full / --boot-mode full or kernel depth, and for other specialists: retain the full bootstrap guard; call kenaz_ping before Phase 0 and HALT with recovery instructions if it is unavailable. Use the active specialist and project-path for Phase 0.\n`;
28
28
  return `## Codex Bootstrap Policy (${policy.mode})\n\n${policy.mode === 'fast' ? fast : full}\n`;
29
29
  }
30
30
 
@@ -31,7 +31,7 @@ Guards against failure modes seen in long Codex Ansuz runs (2026-04-09 audit). R
31
31
  ## Anti-Drift 1: DDx First (mechanism before action)
32
32
  Non-trivial problem on first contact:
33
33
  - You MUST enumerate 3 or more competing hypotheses before the first domain-specific tool call.
34
- - You MUST perform recall (synapse_recall / cortex_recall) before acting when memory tools are available.
34
+ - You MUST check the code first (kenaz_lsp_* / Grep) before acting.
35
35
  - You MUST NOT treat the first plausible idea as the plan.
36
36
 
37
37
  Direct attempt is only allowed when one of these is true:
@@ -100,22 +100,8 @@ Never accept an agent's self-report as verification. Phase 9 is NOT optional. Fo
100
100
 
101
101
  Only mark a task complete when EVERY requirement passes its grep/run check. If even one fails, create a FIX task and dispatch it. Half-done is not done.
102
102
 
103
- ## Anti-Drift 8: PROACTIVE LESSON CAPTURE (R-301 alignment)
104
- Proactive lesson capture is mandatory (R-301), so the next session does not rediscover every gotcha.
105
-
106
- When you observe a non-obvious technical fact during a session — bug root cause, surprising tool behavior, infrastructure quirk, workflow trap, performance gotcha — inscribe it as an ash rune IMMEDIATELY without waiting for user prompt.
107
-
108
- Inscription protocol:
109
- 1. Identify the closest STABLE existing code symbol as the rune anchor (file:function:kind format). Bare file paths are weak anchors and may fail FTS5 indexing silently.
110
- 2. \`synapse_inscribe\` with type=ash, confidence>=0.85, content in ENGLISH (MEM-LANG-001 — FTS5 cannot tokenize CJK reliably).
111
- 3. ALWAYS include trigger_phrases (3-5 natural language phrases) for semantic recall.
112
- 4. WRITE-VERIFY: after inscribe, run \`synapse_recall\` with a unique substring from the rune content. If result_count=0, re-inscribe with a different verified symbol_id, OR fall back to writing to .kenaz/specialist/lessons_pending_index.jsonl.
113
-
114
- Anti-patterns to avoid:
115
- - Inscribing on a non-existent symbol_id (silent index failure).
116
- - Skipping write-verify ("trust the success message" — discovered 2026-04-09 that inscribe API can return success while FTS5 indexing does not happen).
117
- - Chinese/CJK content in rune body (FTS5 returns 0 results for CJK queries).
118
- - Generic content ("we hit a bug, fixed it") — provides no recall value.
103
+ ## Anti-Drift 8: PROACTIVE LESSON CAPTURE
104
+ When you observe a non-obvious technical fact (bug root cause, surprising tool behavior, infrastructure quirk, workflow trap), record it immediately with \`kenaz learn add\` — do not wait for the user to ask. Keep it specific; a generic note ("we hit a bug, fixed it") has no recall value.
119
105
 
120
106
  ## Closing Note
121
107
  If a kernel/elixir rule and an Anti-Drift protocol conflict: kernel/elixir wins on identity and safety, Anti-Drift wins on execution and verification.