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
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
// D.4 runtime validation ??TASK_1214
|
|
3
|
-
* HEALTH_AND_GUARDIAN ??extracted from post-complete.js (D.4 refactor)
|
|
4
|
-
*
|
|
5
|
-
* Loop config reading, Synapse health measurement, health snapshot
|
|
6
|
-
* persistence (trend display only; TASK_2341: never a revert/gate), and
|
|
7
|
-
* Guardian periodic health check triggering.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const { spawnSync } = require('child_process');
|
|
13
|
-
const { ROOT, COLLABORATION_API } = require('../utils');
|
|
14
|
-
|
|
15
|
-
// Constants: natural home is this module (previously misplaced in cognitive.js)
|
|
16
|
-
const GUARDIAN_COUNTER_FILE = path.join(__dirname, '..', '..', '.synapse', 'guardian_counter.json');
|
|
17
|
-
const HEALTH_SNAPSHOTS_DIR_NAME = path.join('.kenaz', 'health_snapshots');
|
|
18
|
-
const DEFAULT_REVERT_THRESHOLD = -2;
|
|
19
|
-
// TASK_2341: tag for snapshots of the memory-only health score. Graph-era snapshots have no
|
|
20
|
-
// score_version, so they are never compared with these (see computeHealthVerdict).
|
|
21
|
-
const MEMORY_SCORE_VERSION = 'memory-v1';
|
|
22
|
-
|
|
23
|
-
// TASK_1726: Producer-side heartbeat observability.
|
|
24
|
-
const CHANNEL_HB_RELPATH = require('path').join('.kenaz', 'channel_heartbeats.jsonl');
|
|
25
|
-
const CHANNEL_HB_CAP = 5000; // lines; rotate to .bak when exceeded
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Append one heartbeat line to .kenaz/channel_heartbeats.jsonl.
|
|
29
|
-
* Bounded: rotates to .bak when CHANNEL_HB_CAP lines exceeded.
|
|
30
|
-
* Never throws -- heartbeat must never break the calling path.
|
|
31
|
-
* @param {string} root project root
|
|
32
|
-
* @param {string} channel logical channel name
|
|
33
|
-
* @param {boolean} ok true=write succeeded, false=write failed
|
|
34
|
-
* @param {string} [note] optional context
|
|
35
|
-
*/
|
|
36
|
-
function appendChannelHeartbeat(root, channel, ok, note) {
|
|
37
|
-
try {
|
|
38
|
-
const hbPath = require('path').join(root, CHANNEL_HB_RELPATH);
|
|
39
|
-
const dir = require('path').dirname(hbPath);
|
|
40
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
41
|
-
if (fs.existsSync(hbPath)) {
|
|
42
|
-
const count = fs.readFileSync(hbPath, 'utf8').split('\n').filter(l => l.trim()).length;
|
|
43
|
-
if (count >= CHANNEL_HB_CAP) fs.renameSync(hbPath, hbPath + '.bak');
|
|
44
|
-
}
|
|
45
|
-
const line = JSON.stringify({ channel, ts: new Date().toISOString(), ok, ...(note ? { note } : {}) });
|
|
46
|
-
fs.appendFileSync(hbPath, line + '\n', 'utf8');
|
|
47
|
-
} catch { /* never throw */ }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* TASK_2038: resolve the projectPath every MCP/HTTP payload sent by the
|
|
52
|
-
* post-complete module family must carry EXPLICITLY. Local, file-based reads
|
|
53
|
-
* in this same file already do `projectPath || ROOT` (e.g.
|
|
54
|
-
* readPreTaskHealthSnapshot, saveTaskHealthSnapshot) -- but every MCP/HTTP
|
|
55
|
-
* payload instead spread `...(projectPath ? { projectPath } : {})`, which
|
|
56
|
-
* OMITS the key entirely for a core KenazAI run (projectPath is undefined
|
|
57
|
-
* there -- see post-complete.js ~L4016, only set when --project-path is
|
|
58
|
-
* passed). synapse_health and friends then fall back server-side to whatever
|
|
59
|
-
* project the Dashboard currently has selected, not this run's actual
|
|
60
|
-
* project -- confirmed live: 4 of 5 recent KEEP/REVERT verdicts were scored
|
|
61
|
-
* against a different repo (rune_ec9ec6c1). One helper instead of repeating
|
|
62
|
-
* `|| ROOT` at each MCP call site (GEN-008: ROOT is post-complete's existing
|
|
63
|
-
* dynamically-resolved constant, not a hardcoded path).
|
|
64
|
-
* @param {string} [projectPath]
|
|
65
|
-
* @returns {string}
|
|
66
|
-
*/
|
|
67
|
-
function resolveMcpProjectPath(projectPath) {
|
|
68
|
-
return projectPath || ROOT;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* TASK_1861 introduced the KENAZ_DASHBOARD_PORT env override so tests can
|
|
73
|
-
* point measureHealthScoreSync at a stub server instead of the real
|
|
74
|
-
* Dashboard. TASK_2038 needs the identical override in
|
|
75
|
-
* triggerGuardianHealthCheck (which used to hardcode port 18080) so its own
|
|
76
|
-
* negative-control test can run against the same stub -- one resolver
|
|
77
|
-
* instead of two copies of the parse/validate logic drifting apart.
|
|
78
|
-
* @returns {number}
|
|
79
|
-
*/
|
|
80
|
-
function resolveMcpDashboardPort() {
|
|
81
|
-
const envPort = parseInt(process.env.KENAZ_DASHBOARD_PORT || '', 10);
|
|
82
|
-
return Number.isInteger(envPort) && envPort > 0 && envPort <= 65535 ? envPort : 18080;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Read loop configuration from .kenaz/loop/loop.json.
|
|
87
|
-
* Determines if KEEP/REVERT logic runs (auto mode only).
|
|
88
|
-
*
|
|
89
|
-
* TASK_1885: the file is written exclusively by loop_config.rs, whose
|
|
90
|
-
* LoopConfig struct carries #[serde(rename_all = "camelCase")] -- so the
|
|
91
|
-
* on-disk key is `revertThreshold`, not `revert_threshold`. There has never
|
|
92
|
-
* been a snake_case writer (loop.js's own readLoopConfig/default and the
|
|
93
|
-
* Rust struct both agree on camelCase), so this reads the single real key
|
|
94
|
-
* with no dual-format acceptance window -- a snake_case branch here would
|
|
95
|
-
* be dead code with no producer. The returned object keeps `revert_threshold`
|
|
96
|
-
* as the JS-internal field name; that shape is unrelated to the on-disk key
|
|
97
|
-
* and is what every consumer in this codebase already destructures.
|
|
98
|
-
* @param {string} [projectPath]
|
|
99
|
-
* @returns {{ mode: string, revert_threshold: number }}
|
|
100
|
-
*/
|
|
101
|
-
function readLoopConfig(projectPath) {
|
|
102
|
-
const root = projectPath || ROOT;
|
|
103
|
-
const loopFile = path.join(root, '.kenaz', 'loop', 'loop.json');
|
|
104
|
-
try {
|
|
105
|
-
if (fs.existsSync(loopFile)) {
|
|
106
|
-
const data = JSON.parse(fs.readFileSync(loopFile, 'utf8'));
|
|
107
|
-
return {
|
|
108
|
-
mode: data.mode || 'manual',
|
|
109
|
-
revert_threshold: typeof data.revertThreshold === 'number'
|
|
110
|
-
? data.revertThreshold
|
|
111
|
-
: DEFAULT_REVERT_THRESHOLD,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
} catch { /* corrupted ??use defaults */ }
|
|
115
|
-
return { mode: 'manual', revert_threshold: DEFAULT_REVERT_THRESHOLD };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Synchronously measure Synapse health score via MCP HTTP endpoint.
|
|
120
|
-
* Spawns a Node.js subprocess to work around Node's async-only HTTP.
|
|
121
|
-
*
|
|
122
|
-
* TASK_1893: the inline script also preserves the raw `metrics` object
|
|
123
|
-
* verbatim as `metrics_raw`, alongside (not replacing) the existing flat
|
|
124
|
-
* `sub_metrics: {name: number}` projection. This is the general fix for
|
|
125
|
-
* a producer/consumer disconnect that had already recurred twice on this
|
|
126
|
-
* exact line: TASK_1890 patched metric_trust + saturated_default through
|
|
127
|
-
* by name; three weeks and one field later (pain_path_count.metric_version,
|
|
128
|
-
* TASK_1889) the same projection dropped the new field again, because
|
|
129
|
-
* naming fields one at a time doesn't generalize. metrics_raw closes the
|
|
130
|
-
* hole for good -- any future per-metric field health.rs adds is preserved
|
|
131
|
-
* automatically, with no fourth card required. sub_metrics/
|
|
132
|
-
* sub_metrics_saturated are NOT widened to carry it, because their flat
|
|
133
|
-
* `{name: number}` / `{name: boolean}` shape is a load-bearing contract
|
|
134
|
-
* consumed in four places by `typeof === 'number'`/truthiness
|
|
135
|
-
* (computeSubMetricDeltas, pairedSubMetricDeltas, deriveSideTrust, and the
|
|
136
|
-
* 191 on-disk _after.json snapshots) -- widening it would break all of
|
|
137
|
-
* them silently.
|
|
138
|
-
* @param {string} [projectPath]
|
|
139
|
-
* @returns {{ score: number, grade: string, sub_metrics?: object, sub_metrics_saturated?: object, metric_trust?: object|null, metrics_raw?: object } | null} null if dashboard not running
|
|
140
|
-
*/
|
|
141
|
-
function measureHealthScoreSync(projectPath) {
|
|
142
|
-
// TASK_2038: resolve BEFORE building the inline script so `pp` is always a
|
|
143
|
-
// real path (never omitted) -- see resolveMcpProjectPath's JSDoc.
|
|
144
|
-
const ppArg = JSON.stringify(resolveMcpProjectPath(projectPath));
|
|
145
|
-
// TASK_1861: KENAZ_DASHBOARD_PORT env override (test injection / non-default port).
|
|
146
|
-
// Invalid or absent values fall back to the production default 18080.
|
|
147
|
-
const port = resolveMcpDashboardPort();
|
|
148
|
-
// Inline node script: makes async HTTP request, writes JSON result to stdout
|
|
149
|
-
const script = [
|
|
150
|
-
"const http = require('http');",
|
|
151
|
-
`const pp = ${ppArg};`,
|
|
152
|
-
"const payload = JSON.stringify({ jsonrpc: '2.0', id: 'hc-sync', method: 'tools/call',",
|
|
153
|
-
" params: { name: 'synapse_health',",
|
|
154
|
-
" arguments: { mode: 'score', save_snapshot: true, projectPath: pp } } });",
|
|
155
|
-
`const req = http.request({ hostname: '127.0.0.1', port: ${port}, path: '/api/mcp',`,
|
|
156
|
-
" method: 'POST',",
|
|
157
|
-
" headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },",
|
|
158
|
-
" timeout: 8000 }, (res) => {",
|
|
159
|
-
" let b = '';",
|
|
160
|
-
" res.on('data', c => b += c);",
|
|
161
|
-
" res.on('end', () => {",
|
|
162
|
-
" try {",
|
|
163
|
-
" const r = JSON.parse(b);",
|
|
164
|
-
" const t = r && r.result && r.result.content && r.result.content[0] && r.result.content[0].text;",
|
|
165
|
-
" if (t) { const d = JSON.parse(t);",
|
|
166
|
-
" const sm = d.metrics ? Object.fromEntries(Object.entries(d.metrics).map(([k,v]) => [k, typeof v === 'object' ? v.score : v])) : {};",
|
|
167
|
-
" const smSat = d.metrics ? Object.fromEntries(Object.entries(d.metrics).map(([k,v]) => [k, !!(v && typeof v === 'object' && v.saturated_default)])) : {};",
|
|
168
|
-
" const mt = (d.metric_trust && typeof d.metric_trust === 'object') ? d.metric_trust : null;",
|
|
169
|
-
// TASK_1893: preserve the whole metrics object verbatim, alongside (not
|
|
170
|
-
// instead of) the flat sm/smSat projections above -- see the JSDoc on
|
|
171
|
-
// measureHealthScoreSync for why this is the general fix rather than
|
|
172
|
-
// naming another field.
|
|
173
|
-
" const metricsRaw = (d.metrics && typeof d.metrics === 'object') ? d.metrics : {};",
|
|
174
|
-
" process.stdout.write(JSON.stringify({ score: d.overall_score, grade: d.grade, sub_metrics: sm, sub_metrics_saturated: smSat, metric_trust: mt, metrics_raw: metricsRaw }));",
|
|
175
|
-
" }",
|
|
176
|
-
" } catch(e) { process.stderr.write(e.message); }",
|
|
177
|
-
" });",
|
|
178
|
-
"});",
|
|
179
|
-
"req.on('error', () => {});",
|
|
180
|
-
"req.on('timeout', () => req.destroy());",
|
|
181
|
-
"req.write(payload);",
|
|
182
|
-
"req.end();",
|
|
183
|
-
].join('\n');
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const result = spawnSync(process.execPath, ['-e', script], {
|
|
187
|
-
encoding: 'utf8',
|
|
188
|
-
timeout: 12000,
|
|
189
|
-
windowsHide: true,
|
|
190
|
-
});
|
|
191
|
-
if (result.stdout && result.stdout.trim()) {
|
|
192
|
-
// TASK_2341: tag so memory-era scores are never compared with graph-era ones.
|
|
193
|
-
return { ...JSON.parse(result.stdout.trim()), score_version: MEMORY_SCORE_VERSION };
|
|
194
|
-
}
|
|
195
|
-
} catch { /* dashboard not running or timeout */ }
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Read pre-task health snapshot from disk.
|
|
201
|
-
* Written by headless.js at dispatch time, keyed by task_id.
|
|
202
|
-
* @param {string} taskId
|
|
203
|
-
* @param {string} [projectPath]
|
|
204
|
-
* @returns {{ score: number, grade: string, timestamp: string } | null}
|
|
205
|
-
*/
|
|
206
|
-
function readPreTaskHealthSnapshot(taskId, projectPath) {
|
|
207
|
-
const root = projectPath || ROOT;
|
|
208
|
-
const snapshotFile = path.join(root, HEALTH_SNAPSHOTS_DIR_NAME, `${taskId}_before.json`);
|
|
209
|
-
try {
|
|
210
|
-
if (fs.existsSync(snapshotFile)) {
|
|
211
|
-
return JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
|
|
212
|
-
}
|
|
213
|
-
} catch {}
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Save a health snapshot for a task (before or after).
|
|
219
|
-
* Includes sub_metrics if present in health object (TASK_1455).
|
|
220
|
-
* TASK_1890: also persists metric_trust (overall_trust, submetric_trust,
|
|
221
|
-
* risk_flags, sampling_info, index_staleness_hours) and per-metric
|
|
222
|
-
* saturated_default flags -- without this, a snapshot read back later (e.g.
|
|
223
|
-
* as a stand-in "before" reading, or for post-hoc audit) can never recover
|
|
224
|
-
* how trustworthy the graph was at measurement time, and any consumer is
|
|
225
|
-
* forced back onto the optimistic default this task exists to eliminate.
|
|
226
|
-
* TASK_1893: also persists metrics_raw (the unprojected metrics object,
|
|
227
|
-
* including per-metric fields like pain_path_count.metric_version) --
|
|
228
|
-
* without this a snapshot can never recover which FORMULA a metric was
|
|
229
|
-
* computed under, only its score, so a before/after pair spanning a formula
|
|
230
|
-
* change reads as an ordinary delta instead of an incomparable one.
|
|
231
|
-
* @param {string} taskId
|
|
232
|
-
* @param {'before'|'after'} type
|
|
233
|
-
* @param {{ score: number, grade: string, sub_metrics?: object, sub_metrics_saturated?: object, metric_trust?: object|null, metrics_raw?: object }} health
|
|
234
|
-
* @param {string} [projectPath]
|
|
235
|
-
* @returns {object|null} the exact payload written to disk (score, grade,
|
|
236
|
-
* sub_metrics, sub_metrics_saturated, metric_trust, metrics_raw, timestamp,
|
|
237
|
-
* and on 'after' also delta_from_previous/trend_5tasks), or null on write
|
|
238
|
-
* failure.
|
|
239
|
-
* TASK_1891: returning this lets callers thread delta_from_previous through
|
|
240
|
-
* to computeHealthVerdict for observability -- previously computed here and
|
|
241
|
-
* then silently discarded (rune_9d645791: "no verdict path reads it").
|
|
242
|
-
*/
|
|
243
|
-
function saveTaskHealthSnapshot(taskId, type, health, projectPath) {
|
|
244
|
-
const root = projectPath || ROOT;
|
|
245
|
-
const dir = path.join(root, HEALTH_SNAPSHOTS_DIR_NAME);
|
|
246
|
-
let writeOk = false;
|
|
247
|
-
let writeNote;
|
|
248
|
-
let writtenPayload = null;
|
|
249
|
-
try {
|
|
250
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
251
|
-
const payload = {
|
|
252
|
-
score: health.score,
|
|
253
|
-
grade: health.grade,
|
|
254
|
-
score_version: health.score_version || MEMORY_SCORE_VERSION,
|
|
255
|
-
sub_metrics: health.sub_metrics || {},
|
|
256
|
-
sub_metrics_saturated: health.sub_metrics_saturated || {},
|
|
257
|
-
metric_trust: health.metric_trust || null,
|
|
258
|
-
metrics_raw: health.metrics_raw || {},
|
|
259
|
-
timestamp: new Date().toISOString(),
|
|
260
|
-
};
|
|
261
|
-
// Enrich _after snapshots with delta_from_previous and trend_5tasks (TASK_1431)
|
|
262
|
-
// Priors are loaded BEFORE writing current file, so current task is naturally excluded.
|
|
263
|
-
if (type === 'after') {
|
|
264
|
-
try {
|
|
265
|
-
const priors = loadRecentAfterSnapshots(root, 5);
|
|
266
|
-
payload.delta_from_previous = priors.length >= 1 && typeof priors[0].score === 'number'
|
|
267
|
-
? parseFloat((health.score - priors[0].score).toFixed(2))
|
|
268
|
-
: null;
|
|
269
|
-
payload.trend_5tasks = priors.length >= 5 && typeof priors[4].score === 'number'
|
|
270
|
-
? parseFloat((health.score - priors[4].score).toFixed(2))
|
|
271
|
-
: null;
|
|
272
|
-
} catch { payload.delta_from_previous = null; payload.trend_5tasks = null; }
|
|
273
|
-
}
|
|
274
|
-
fs.writeFileSync(
|
|
275
|
-
path.join(dir, `${taskId}_${type}.json`),
|
|
276
|
-
JSON.stringify(payload, null, 2)
|
|
277
|
-
);
|
|
278
|
-
writeOk = true;
|
|
279
|
-
writtenPayload = payload;
|
|
280
|
-
} catch (e) {
|
|
281
|
-
// TASK_1726: warn-on-silent-skip
|
|
282
|
-
console.warn(`[channel:health_snapshot] write failed for ${taskId}_${type}: ${e && e.message || e}`);
|
|
283
|
-
writeNote = String(e && e.message || e);
|
|
284
|
-
}
|
|
285
|
-
// TASK_1726: heartbeat on every call (ok or not)
|
|
286
|
-
appendChannelHeartbeat(root, 'health_snapshot', writeOk, writeNote);
|
|
287
|
-
return writtenPayload;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Load the N most recent _after.json snapshots (TASK_1455).
|
|
292
|
-
* Used for sub-metric trend analysis in SUBMETRIC_REGRESSION verdict.
|
|
293
|
-
* @param {string} [projectPath]
|
|
294
|
-
* @param {number} [limit=10]
|
|
295
|
-
* @returns {Array<{ taskId: string, score: number, grade: string, sub_metrics: object, timestamp: string }>}
|
|
296
|
-
*/
|
|
297
|
-
function loadRecentAfterSnapshots(projectPath, limit = 10) {
|
|
298
|
-
const root = projectPath || ROOT;
|
|
299
|
-
const dir = path.join(root, HEALTH_SNAPSHOTS_DIR_NAME);
|
|
300
|
-
if (!fs.existsSync(dir)) return [];
|
|
301
|
-
try {
|
|
302
|
-
// Sort by recorded content timestamp (deterministic), falling back to file mtime
|
|
303
|
-
// only when the timestamp is missing/unparseable. mtime alone ties when many
|
|
304
|
-
// snapshots are written in the same millisecond, producing unstable ordering.
|
|
305
|
-
return fs.readdirSync(dir)
|
|
306
|
-
.filter(f => f.endsWith('_after.json'))
|
|
307
|
-
.map(f => {
|
|
308
|
-
try {
|
|
309
|
-
const data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
310
|
-
const ts = Date.parse(data.timestamp);
|
|
311
|
-
return {
|
|
312
|
-
taskId: f.replace('_after.json', ''),
|
|
313
|
-
score_version: data.score_version || null,
|
|
314
|
-
score: data.score ?? null,
|
|
315
|
-
grade: data.grade ?? null,
|
|
316
|
-
sub_metrics: data.sub_metrics || {},
|
|
317
|
-
timestamp: data.timestamp || null,
|
|
318
|
-
_sortKey: Number.isNaN(ts) ? fs.statSync(path.join(dir, f)).mtimeMs : ts,
|
|
319
|
-
};
|
|
320
|
-
} catch { return null; }
|
|
321
|
-
})
|
|
322
|
-
.filter(Boolean)
|
|
323
|
-
// TASK_2341: only same-version (memory-v1) snapshots form a comparable trend.
|
|
324
|
-
.filter(x => x.score_version === MEMORY_SCORE_VERSION)
|
|
325
|
-
.sort((a, b) => b._sortKey - a._sortKey) // newest first by recorded timestamp
|
|
326
|
-
.slice(0, limit)
|
|
327
|
-
.map(({ _sortKey, ...rest }) => rest);
|
|
328
|
-
} catch { return []; }
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* TASK_2341: memory-health trend record. NEVER a gate.
|
|
333
|
-
*
|
|
334
|
-
* synapse_health now scores MEMORY (rune_vitality, review_backlog, conflict_load,
|
|
335
|
-
* episode_backlog), not code. A code change cannot move that score, so no verdict
|
|
336
|
-
* derived from it may revert, block or penalise a task. This function therefore
|
|
337
|
-
* always returns verdict 'RECORDED'; `delta` is informational and is only a number
|
|
338
|
-
* when the before snapshot carries the same score_version (graph-era snapshots have
|
|
339
|
-
* none and are never compared with memory-era ones).
|
|
340
|
-
* @param {object|number} preSnapshotOrScore
|
|
341
|
-
* @param {{score:number, score_version?:string}} postHealth
|
|
342
|
-
* @param {number} [_threshold] ignored (kept for caller compatibility)
|
|
343
|
-
* @param {{trendDeltaFromPrevious?: number|null}} [opts]
|
|
344
|
-
* @returns {{delta: number|null, score: number, verdict: 'RECORDED', reason: string,
|
|
345
|
-
* subMetricRegressions: string[], trust: object, indeterminateReason: null,
|
|
346
|
-
* trendDeltaFromPrevious: number|null}}
|
|
347
|
-
*/
|
|
348
|
-
function computeHealthVerdict(preSnapshotOrScore, postHealth, _threshold, opts = {}) {
|
|
349
|
-
const preSn = typeof preSnapshotOrScore === 'number' ? { score: preSnapshotOrScore } : (preSnapshotOrScore || {});
|
|
350
|
-
const score = postHealth.score;
|
|
351
|
-
const version = postHealth.score_version || MEMORY_SCORE_VERSION;
|
|
352
|
-
const comparable = typeof preSn.score === 'number' && preSn.score_version === version;
|
|
353
|
-
const delta = comparable ? score - preSn.score : null;
|
|
354
|
-
const reason = comparable
|
|
355
|
-
? `Memory-health trend ${delta >= 0 ? '+' : ''}${Number(delta).toFixed(1)} pts (${version}) — recorded for display only, never gates`
|
|
356
|
-
: `Memory-health ${score}/100 (${version}) recorded; baseline is ${preSn.score_version || 'graph-era'} so not compared`;
|
|
357
|
-
return {
|
|
358
|
-
delta, score, verdict: 'RECORDED', reason, subMetricRegressions: [],
|
|
359
|
-
trust: { status: 'unknown', overall_trust: null, risk_flags: [] },
|
|
360
|
-
indeterminateReason: null,
|
|
361
|
-
trendDeltaFromPrevious: opts.trendDeltaFromPrevious ?? null,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Write the RECORDED health note to task YAML via task-api notes field.
|
|
368
|
-
* Field format: "post_complete_verdict: RECORDED | delta: N|n/a | reason: ..."
|
|
369
|
-
* @param {string} taskId
|
|
370
|
-
* @param {string} verdict
|
|
371
|
-
* @param {number|null} delta
|
|
372
|
-
* @param {string} reason
|
|
373
|
-
* @param {string} [projectPath]
|
|
374
|
-
*/
|
|
375
|
-
function writeVerdictToTask(taskId, verdict, delta, reason, projectPath) {
|
|
376
|
-
try {
|
|
377
|
-
const deltaText = typeof delta === 'number' ? delta.toFixed(2) : 'n/a';
|
|
378
|
-
const updateArgs = projectPath ? ['--project-path', projectPath] : [];
|
|
379
|
-
const verdictNote = `post_complete_verdict: ${verdict} | delta: ${deltaText} | reason: ${reason}`;
|
|
380
|
-
updateArgs.push('update', taskId, '--notes', verdictNote);
|
|
381
|
-
spawnSync('node', [COLLABORATION_API, ...updateArgs], {
|
|
382
|
-
encoding: 'utf8', timeout: 10000, cwd: ROOT, windowsHide: true,
|
|
383
|
-
});
|
|
384
|
-
} catch { /* best-effort */ }
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function readGuardianCounter() {
|
|
388
|
-
try {
|
|
389
|
-
if (fs.existsSync(GUARDIAN_COUNTER_FILE)) {
|
|
390
|
-
return JSON.parse(fs.readFileSync(GUARDIAN_COUNTER_FILE, 'utf8'));
|
|
391
|
-
}
|
|
392
|
-
} catch { /* corrupted ??reset */ }
|
|
393
|
-
return { completed_since_last_health: 0 };
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function writeGuardianCounter(data) {
|
|
397
|
-
try {
|
|
398
|
-
const dir = path.dirname(GUARDIAN_COUNTER_FILE);
|
|
399
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
400
|
-
fs.writeFileSync(GUARDIAN_COUNTER_FILE, JSON.stringify(data, null, 2));
|
|
401
|
-
} catch { /* best-effort */ }
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Fire-and-forget: call synapse_health via MCP HTTP endpoint.
|
|
406
|
-
* Saves a snapshot for trend tracking. Non-blocking, non-failing.
|
|
407
|
-
*/
|
|
408
|
-
function triggerGuardianHealthCheck(projectPath) {
|
|
409
|
-
const http = require('http');
|
|
410
|
-
// TASK_2038: explicit projectPath (never omitted) + same KENAZ_DASHBOARD_PORT
|
|
411
|
-
// override as measureHealthScoreSync -- see resolveMcpProjectPath/
|
|
412
|
-
// resolveMcpDashboardPort JSDoc.
|
|
413
|
-
const payload = JSON.stringify({
|
|
414
|
-
jsonrpc: '2.0', id: `guardian-${Date.now()}`, method: 'tools/call',
|
|
415
|
-
params: {
|
|
416
|
-
name: 'synapse_health',
|
|
417
|
-
arguments: { mode: 'score', save_snapshot: true, projectPath: resolveMcpProjectPath(projectPath) }
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
const req = http.request({
|
|
421
|
-
hostname: '127.0.0.1', port: resolveMcpDashboardPort(), path: '/api/mcp',
|
|
422
|
-
method: 'POST',
|
|
423
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
424
|
-
timeout: 10000
|
|
425
|
-
}, (res) => {
|
|
426
|
-
let body = '';
|
|
427
|
-
res.on('data', (chunk) => { body += chunk; });
|
|
428
|
-
res.on('end', () => {
|
|
429
|
-
try {
|
|
430
|
-
const result = JSON.parse(body);
|
|
431
|
-
const content = result?.result?.content?.[0]?.text;
|
|
432
|
-
if (content) {
|
|
433
|
-
const data = JSON.parse(content);
|
|
434
|
-
console.log(` ?儭? Guardian: health=${data.overall_score}/100 (${data.grade}) ??snapshot saved`);
|
|
435
|
-
}
|
|
436
|
-
} catch { console.log(' ?儭? Guardian: health snapshot saved'); }
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
req.on('error', () => { /* Dashboard not running ??skip silently */ });
|
|
440
|
-
req.on('timeout', () => { req.destroy(); });
|
|
441
|
-
req.write(payload);
|
|
442
|
-
req.end();
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
module.exports = {
|
|
446
|
-
resolveMcpProjectPath,
|
|
447
|
-
resolveMcpDashboardPort,
|
|
448
|
-
readLoopConfig,
|
|
449
|
-
measureHealthScoreSync,
|
|
450
|
-
readPreTaskHealthSnapshot,
|
|
451
|
-
saveTaskHealthSnapshot,
|
|
452
|
-
loadRecentAfterSnapshots,
|
|
453
|
-
computeHealthVerdict,
|
|
454
|
-
writeVerdictToTask,
|
|
455
|
-
readGuardianCounter,
|
|
456
|
-
writeGuardianCounter,
|
|
457
|
-
triggerGuardianHealthCheck,
|
|
458
|
-
appendChannelHeartbeat,
|
|
459
|
-
GUARDIAN_COUNTER_FILE,
|
|
460
|
-
HEALTH_SNAPSHOTS_DIR_NAME,
|
|
461
|
-
DEFAULT_REVERT_THRESHOLD,
|
|
462
|
-
MEMORY_SCORE_VERSION,
|
|
463
|
-
CHANNEL_HB_RELPATH,
|
|
464
|
-
};
|