klypix-mcp 1.86.3 → 1.87.0
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/bin/klypix-install.mjs +1 -1
- package/bin/klypix-worker.mjs +3 -3
- package/package.json +2 -2
- package/src/brain-doctor.mjs +24 -1
- package/src/enrichment.mjs +18 -2
- package/src/global-brain-hook.mjs +237 -74
- package/src/klypix-core.mjs +91 -3
- package/src/klypix-format.mjs +366 -21
- package/src/provenance.mjs +432 -0
package/bin/klypix-install.mjs
CHANGED
|
@@ -395,7 +395,7 @@ try {
|
|
|
395
395
|
// canvas-view-app.html is the canvas_view MCP App UI — staged raw (an HTML
|
|
396
396
|
// file must never get a JS-comment banner) beside the flat server, which
|
|
397
397
|
// resolves it via its ./canvas-view-app.html candidate path.
|
|
398
|
-
for (const f of ['global-brain-hook.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'brain-graveyard.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
398
|
+
for (const f of ['global-brain-hook.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'provenance.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'brain-graveyard.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
399
399
|
const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
|
|
400
400
|
}
|
|
401
401
|
for (const [src, dst] of [
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -621,7 +621,7 @@ server.registerTool('brain_lens', {
|
|
|
621
621
|
|
|
622
622
|
server.registerTool('brain_connect', {
|
|
623
623
|
title: 'Connect related-but-unlinked brain cards (densify the graph)',
|
|
624
|
-
description: 'Repairs orphaned decision/milestone cards first (scope:"orphans", the default) by proposing genuinely related unlinked pairs — semantic similarity at a conservative 0.55 threshold when the on-device model is installed, else shared tags + [[mentions]]. The dry run includes a before→projected orphan receipt; apply:true draws only additive, removable arrows and reports the measured after count. It NEVER archives or rewrites cards. Use scope:"all" for deliberate whole-graph densification. To DISMISS a brain_reconcile false-positive contradiction, pass pairs:[{fromId,toId}] with relationship:"not_contradiction".',
|
|
624
|
+
description: 'Repairs orphaned decision/milestone cards first (scope:"orphans", the default) by proposing genuinely related unlinked pairs — semantic similarity at a conservative 0.55 threshold when the on-device model is installed, else shared tags + [[mentions]]. The dry run includes a before→projected orphan receipt; apply:true draws only additive, removable arrows and reports the measured after count. It NEVER archives or rewrites cards. Use scope:"all" for deliberate whole-graph densification. To DISMISS a brain_reconcile false-positive contradiction, pass pairs:[{fromId,toId}] with relationship:"not_contradiction". Every applied not_fulfilled / not_contradiction dismissal is also recorded, with its surface and client, to the machine-local provenance sidecar (nothing extra is written into the brain file).',
|
|
625
625
|
inputSchema: {
|
|
626
626
|
canvas: z.string().optional().describe('Canvas filename/path. Defaults to the project brain ("brain").'),
|
|
627
627
|
apply: z.boolean().optional().describe('false (default) = suggest only; true = draw the connections.'),
|
|
@@ -631,11 +631,11 @@ server.registerTool('brain_connect', {
|
|
|
631
631
|
pairs: z.array(z.object({ fromId: z.string(), toId: z.string() })).optional().describe('Explicit card-id pairs to connect (bypasses auto-proposal). Use to dismiss a reconcile false-positive: pass the two card ids with relationship:"not_contradiction".'),
|
|
632
632
|
relationship: z.string().optional().describe('Relationship for explicit `pairs` (e.g. "not_contradiction" to permanently dismiss a contradiction candidate, or "relates_to", "depends_on", "supports").'),
|
|
633
633
|
},
|
|
634
|
-
}, async ({ canvas, apply, max, threshold, scope, pairs, relationship }) => toContent(await opBrainConnect({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), apply, max, threshold, scope, pairs, relationship, log })));
|
|
634
|
+
}, async ({ canvas, apply, max, threshold, scope, pairs, relationship }, extra) => toContent(await opBrainConnect({ vault: mcpPresence.vault, canvas: boundBrainCanvas(canvas), apply, max, threshold, scope, pairs, relationship, via: extra.klypixClientName, log })));
|
|
635
635
|
|
|
636
636
|
server.registerTool('brain_reconcile', {
|
|
637
637
|
title: 'Reconcile the brain — contradictions, unrecorded migrations, and what a release already closed',
|
|
638
|
-
description: 'Truth maintenance. (1) CONTRADICTIONS: finds same-subject live card pairs where one carries an explicit correction cue (uppercase "CORRECTION", "was WRONG", "OBSOLETE" — that side is the presumed truth, UNLESS the cue predates its counterpart: then the pair is marked "presumed superseded" and the newer card is presumed current — verify before retiring) or the two use opposite polarity words (deferred↔wired, broken↔fixed, dead↔live), i.e. stale facts whose correction never got linked — candidates only, YOU confirm each: retire the stale card via brain_note ✓. Dismiss a FALSE positive (either kind) by connecting the two ids with brain_connect pairs + relationship:"not_contradiction" — persisted, so it never resurfaces (and its cue stops overlaying recall/ask for that pair). (2) MIGRATIONS: lists committed migration files (Supabase / Rails / Prisma / Knex / generic) that NO brain card references, so an applied-but-unnarrated rollout can be recorded. (3) LEGACY: pre-v1.15 raw-bash ship cards to tidy. (4) RELEASE: which open cards look fulfilled by the commits a release ref already carries (subject+body coverage, the card\'s own #commit- receipt, or a hint edge whose milestone is in the ref). READ-ONLY by default and on every other mode. THE ONE EXCEPTION: on mode "claims" and mode "release" you may pass confirm/dismiss to actually close what you verified — confirm names exact card ids, so nothing is matched by prose; covering only part of a multi-item clause writes "✔ partial" and KEEPS the card open unless you pass whole:true; a call whose every entry is refused leaves the brain byte-identical. Never reads the database or the network. Run it periodically, when recall surfaces something you believe is stale, or right before cutting a release.',
|
|
638
|
+
description: 'Truth maintenance. (1) CONTRADICTIONS: finds same-subject live card pairs where one carries an explicit correction cue (uppercase "CORRECTION", "was WRONG", "OBSOLETE" — that side is the presumed truth, UNLESS the cue predates its counterpart: then the pair is marked "presumed superseded" and the newer card is presumed current — verify before retiring) or the two use opposite polarity words (deferred↔wired, broken↔fixed, dead↔live), i.e. stale facts whose correction never got linked — candidates only, YOU confirm each: retire the stale card via brain_note ✓. Dismiss a FALSE positive (either kind) by connecting the two ids with brain_connect pairs + relationship:"not_contradiction" — persisted, so it never resurfaces (and its cue stops overlaying recall/ask for that pair). (2) MIGRATIONS: lists committed migration files (Supabase / Rails / Prisma / Knex / generic) that NO brain card references, so an applied-but-unnarrated rollout can be recorded. (3) LEGACY: pre-v1.15 raw-bash ship cards to tidy. (4) RELEASE: which open cards look fulfilled by the commits a release ref already carries (subject+body coverage, the card\'s own #commit- receipt, or a hint edge whose milestone is in the ref). READ-ONLY by default and on every other mode. THE ONE EXCEPTION: on mode "claims" and mode "release" you may pass confirm/dismiss to actually close what you verified — confirm names exact card ids, so nothing is matched by prose; covering only part of a multi-item clause writes "✔ partial" and KEEPS the card open unless you pass whole:true; a call whose every entry is refused leaves the brain byte-identical. Never reads the database or the network. Every applied confirm/dismiss is also recorded to the machine-local provenance sidecar (actor, surface, exact card ids — nothing extra is written into the brain file), and a claims confirm settles the pair\'s dashed "likely closed by" hint into a solid confirmed edge. Run it periodically, when recall surfaces something you believe is stale, or right before cutting a release.',
|
|
639
639
|
inputSchema: {
|
|
640
640
|
canvas: z.string().optional().describe('Brain canvas filename/path. Defaults to the project brain ("brain").'),
|
|
641
641
|
root: z.string().optional().describe("Project root holding the migrations dir / git repo (default: the brain file's folder)."),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.87.0",
|
|
4
4
|
"mcpName": "io.github.dahshanlabs/klypix-mcp",
|
|
5
5
|
"description": "Active state management for multi-agent coding: a shared, versioned project brain over MCP.",
|
|
6
6
|
"type": "module",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"bench": "node bin/klypix-mcp.mjs bench",
|
|
85
85
|
"test:bench": "node test/bench.mjs",
|
|
86
86
|
"pretest": "node test/publish-workflow.mjs",
|
|
87
|
-
"test": "node test/publish-verdict.mjs && node test/npx-owned-names.mjs && node test/project-graph.mjs &&node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/capture-gap.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/partial-notes.mjs && node test/lifecycle-prefix.mjs && node test/close-link-safety.mjs && node test/resolve-ledger.mjs && node test/plan-fulfillment.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/evidence-anchors.mjs && node test/brain-evidence.mjs && node test/presence-visibility.mjs && node test/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/release-lease.mjs && node test/release-reconcile.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.mjs && node test/release-handshake.mjs && node test/completion-guard.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/one-command-setup.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/canvas-groups.mjs && node test/git-tools.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/status-shape.mjs && node test/status-hook.mjs",
|
|
87
|
+
"test": "node test/publish-verdict.mjs && node test/npx-owned-names.mjs && node test/project-graph.mjs &&node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/capture-gap.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/partial-notes.mjs && node test/lifecycle-prefix.mjs && node test/close-link-safety.mjs && node test/resolve-ledger.mjs && node test/plan-fulfillment.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/provenance.mjs && node test/confirm-trail.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/hook-unified-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/evidence-anchors.mjs && node test/brain-evidence.mjs && node test/presence-visibility.mjs && node test/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/release-lease.mjs && node test/release-reconcile.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.mjs && node test/release-handshake.mjs && node test/completion-guard.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/one-command-setup.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/canvas-groups.mjs && node test/git-tools.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/status-shape.mjs && node test/status-hook.mjs",
|
|
88
88
|
"test:memory": "node test/memory-runtime.mjs",
|
|
89
89
|
"test:memory:soak": "node --expose-gc test/memory-soak.mjs",
|
|
90
90
|
"runtime": "node bin/klypix-runtime.mjs",
|
package/src/brain-doctor.mjs
CHANGED
|
@@ -57,6 +57,11 @@ try { repoStateLib = await import('./repo-state.mjs'); } catch { repoStateLib =
|
|
|
57
57
|
// (the exact drift that once produced three different live-session counts).
|
|
58
58
|
let presenceLib = null;
|
|
59
59
|
try { presenceLib = await import('./agent-presence.mjs'); } catch { presenceLib = null; }
|
|
60
|
+
// provenance powers the informational JUDGMENTS line (confirm/dismiss verdict
|
|
61
|
+
// counts + rejected-prompt pool). Same failure-tolerant idiom; absence of the
|
|
62
|
+
// module OR of any records is a fact, never drift.
|
|
63
|
+
let provenanceLib = null;
|
|
64
|
+
try { provenanceLib = await import('./provenance.mjs'); } catch { provenanceLib = null; }
|
|
60
65
|
|
|
61
66
|
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
62
67
|
|
|
@@ -497,6 +502,13 @@ export function inspect(opts = {}) {
|
|
|
497
502
|
history = { available: true, count: points.length, newestAt: points[0]?.ts || null };
|
|
498
503
|
} catch { history = { available: true, count: 0, newestAt: null }; }
|
|
499
504
|
}
|
|
505
|
+
// Judgment provenance (2026-09-29). Informational only: a brain with zero
|
|
506
|
+
// recorded verdicts is NEW, not drifted (same doctrine as history above).
|
|
507
|
+
let provenance = null;
|
|
508
|
+
if (hasBrain && provenanceLib && typeof provenanceLib.provenanceCounts === 'function') {
|
|
509
|
+
try { provenance = { available: true, ...provenanceLib.provenanceCounts(brainPath, { home }) }; }
|
|
510
|
+
catch { provenance = null; }
|
|
511
|
+
}
|
|
500
512
|
const tools = inspectTools(brainDir, PKG_ROOT);
|
|
501
513
|
// ── CHECKOUT (release-state visibility, 2026-08-14 incident) ──────────────
|
|
502
514
|
// Advisory, never a verdict layer: when the PROJECT itself is a versioned
|
|
@@ -669,7 +681,7 @@ export function inspect(opts = {}) {
|
|
|
669
681
|
|
|
670
682
|
// `checkout` is additive (schema-stable): downstream renderers keep parsing
|
|
671
683
|
// every existing field; it never feeds layers/verdict/actions by design.
|
|
672
|
-
return { verdict, layers, drifted, readinessWarnings, version, running, supervisors, autoUpdate, hooks, codexSmart, codexHooks, gitCapture, history, tools, peers, sessions: peers, receipts: peers.receipts, receiptSessionId, harness, npm, decayGuard, checkout, project: { dir: projectDir, brainPath, hasBrain }, brainDir, actions };
|
|
684
|
+
return { verdict, layers, drifted, readinessWarnings, version, running, supervisors, autoUpdate, hooks, codexSmart, codexHooks, gitCapture, history, provenance, tools, peers, sessions: peers, receipts: peers.receipts, receiptSessionId, harness, npm, decayGuard, checkout, project: { dir: projectDir, brainPath, hasBrain }, brainDir, actions };
|
|
673
685
|
}
|
|
674
686
|
|
|
675
687
|
// One-line drift summary (empty when clean) — for a footer / status line.
|
|
@@ -840,6 +852,17 @@ export function render(r, opts = {}) {
|
|
|
840
852
|
const age = r.history.newestAt ? `${Math.max(0, Math.round((Date.now() - r.history.newestAt) / 60000))}m ago` : 'none yet';
|
|
841
853
|
L.push(`${ok} ${c.bold}HISTORY${c.rst} ${r.history.count} restore point(s) · newest ${age} ${c.dim}(npx klypix-mcp brain-history list)${c.rst}`);
|
|
842
854
|
}
|
|
855
|
+
// Judgment provenance — say what confirm/dismiss verdicts this machine has
|
|
856
|
+
// recorded, by verdict and surface, because "the trail exists" is only
|
|
857
|
+
// believable with the counts on the table. Zero records = a new sidecar,
|
|
858
|
+
// never a warning.
|
|
859
|
+
if (r.provenance?.available) {
|
|
860
|
+
const j = r.provenance.judgments || { total: 0, byVerdict: {}, bySource: {} };
|
|
861
|
+
const rej = r.provenance.rejected || { total: 0 };
|
|
862
|
+
const since = j.firstTs ? ` · since ${new Date(j.firstTs).toISOString().slice(0, 10)}` : '';
|
|
863
|
+
const srcBits = Object.entries(j.bySource || {}).map(([source, n]) => `${source} ${n}`).join(' · ');
|
|
864
|
+
L.push(`${ok} ${c.bold}JUDGMENTS${c.rst} ${j.total} recorded (${j.byVerdict?.yes || 0} yes / ${j.byVerdict?.no || 0} no)${since} · rejected-prompt pool ${rej.total}${srcBits ? ` ${c.dim}(${srcBits})${c.rst}` : ''}`);
|
|
865
|
+
}
|
|
843
866
|
|
|
844
867
|
// TOOLS
|
|
845
868
|
L.push(`${ok} ${c.bold}TOOLS${c.rst} ${r.tools.count} MCP verb(s)${r.tools.hash ? ` ${c.dim}[#${r.tools.hash}, ${r.tools.source}]${c.rst}` : ''}${r.tools.count ? `: ${c.dim}${r.tools.names.join(', ')}${c.rst}` : ''}`);
|
package/src/enrichment.mjs
CHANGED
|
@@ -120,18 +120,34 @@ export function enrichmentQuestionQuality(question) {
|
|
|
120
120
|
* pruned — enrichment is a rolling quality window, not an archive, and unlike
|
|
121
121
|
* the claims lane nothing downstream depends on any single entry existing.
|
|
122
122
|
* Returns { recorded, rejected } — `rejected` counts texts the quality gate
|
|
123
|
-
* refused (see enrichmentQuestionQuality); they
|
|
123
|
+
* refused (see enrichmentQuestionQuality); they never reach the enrichment
|
|
124
|
+
* sidecar or the embedder. Since 2026-09-29 each refusal is forwarded, with its
|
|
125
|
+
* reason, to the privacy-screened rejected-log in provenance.mjs (best-effort).
|
|
124
126
|
*/
|
|
125
127
|
export function recordEnrichment(brainPath, items, { home = os.homedir(), now = Date.now() } = {}) {
|
|
126
128
|
const list = [];
|
|
129
|
+
const discarded = [];
|
|
127
130
|
let rejected = 0;
|
|
128
131
|
for (const item of (Array.isArray(items) ? items : [])) {
|
|
129
132
|
const key = enrichmentKeyFor(item?.body);
|
|
130
133
|
if (key.length < 24) continue;
|
|
131
134
|
const quality = enrichmentQuestionQuality(item?.question);
|
|
132
|
-
if (!quality.ok) { rejected++; continue; }
|
|
135
|
+
if (!quality.ok) { rejected++; discarded.push({ text: quality.text, reason: quality.reason }); continue; }
|
|
133
136
|
list.push({ key, q: quality.text });
|
|
134
137
|
}
|
|
138
|
+
// A discard is still SIGNAL — the acknowledgement class is exactly the
|
|
139
|
+
// negative pool a judge study needs — so the text goes, WITH its refusal
|
|
140
|
+
// reason, to the bounded rejected-log sidecar (provenance.mjs applies the
|
|
141
|
+
// privacy screen: machine/console/pasted-doc classes store hash+length only).
|
|
142
|
+
// Lazy fire-and-forget import, never-throw: a stale deployment without
|
|
143
|
+
// provenance.mjs just skips, costing training data, never a capture.
|
|
144
|
+
if (discarded.length) {
|
|
145
|
+
try {
|
|
146
|
+
import('./provenance.mjs')
|
|
147
|
+
.then((prov) => { try { prov.recordRejectedEnrichment(brainPath, discarded, { home, now }); } catch { /* additive */ } })
|
|
148
|
+
.catch(() => { /* stale deployment — additive signal only */ });
|
|
149
|
+
} catch { /* dynamic import unavailable — additive signal only */ }
|
|
150
|
+
}
|
|
135
151
|
if (!list.length) return { recorded: 0, rejected };
|
|
136
152
|
const file = enrichmentFileFor(brainPath, home);
|
|
137
153
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -2743,13 +2743,19 @@ function draftRulesFromFixes(lib, cards, verified, sid) {
|
|
|
2743
2743
|
candidates.push({ area: c.area || '', text: seed, source: c.createdVia || 'marker' });
|
|
2744
2744
|
}
|
|
2745
2745
|
}
|
|
2746
|
-
if (!candidates.length && !skillCards.length) return { drafted: 0, approved: 0 };
|
|
2746
|
+
if (!candidates.length && !skillCards.length) return { drafted: 0, approved: 0, approvedDrafts: [] };
|
|
2747
2747
|
let added = 0, approved = 0;
|
|
2748
|
+
const approvedDrafts = []; // which draft each skill retired — the shown-then-promoted provenance signal
|
|
2748
2749
|
persistDrafts((drafts, now) => {
|
|
2749
2750
|
if (skillCards.length) { // approval: a captured 🛠️ skill retires the draft it fulfils
|
|
2750
|
-
const
|
|
2751
|
-
|
|
2752
|
-
|
|
2751
|
+
const kept = [];
|
|
2752
|
+
for (const d of drafts) {
|
|
2753
|
+
const sc = skillCards.find(sc => draftMatches(d, String(sc.text || '')));
|
|
2754
|
+
if (sc) approvedDrafts.push({ area: d.area || '', text: String(d.text || ''), skillText: String(sc.text || '') });
|
|
2755
|
+
else kept.push(d);
|
|
2756
|
+
}
|
|
2757
|
+
approved = drafts.length - kept.length;
|
|
2758
|
+
drafts = kept;
|
|
2753
2759
|
}
|
|
2754
2760
|
const handled = new Set();
|
|
2755
2761
|
for (const cand of candidates) {
|
|
@@ -2764,8 +2770,8 @@ function draftRulesFromFixes(lib, cards, verified, sid) {
|
|
|
2764
2770
|
}
|
|
2765
2771
|
return drafts;
|
|
2766
2772
|
});
|
|
2767
|
-
return { drafted: added, approved };
|
|
2768
|
-
} catch { return { drafted: 0, approved: 0 }; }
|
|
2773
|
+
return { drafted: added, approved, approvedDrafts };
|
|
2774
|
+
} catch { return { drafted: 0, approved: 0, approvedDrafts: [] }; }
|
|
2769
2775
|
}
|
|
2770
2776
|
// Pending drafts: TTL-fresh, not already covered by a live 🛠️ skill, and NOT decayed
|
|
2771
2777
|
// (a draft surfaced-and-ignored across ≥ RULE_DRAFT_MAX_SURFACINGS distinct sessions
|
|
@@ -3642,7 +3648,38 @@ async function capture(lib) {
|
|
|
3642
3648
|
{
|
|
3643
3649
|
const hadFixCommit = commitCards.some(cc => cc.createdVia === 'commit' && !/🏁/.test(String(cc.text || '')));
|
|
3644
3650
|
const verified = sessionVerified(shellCmds, errorIds, hadFixCommit);
|
|
3645
|
-
draftRulesFromFixes(lib, cards, verified, sid);
|
|
3651
|
+
const draftRes = draftRulesFromFixes(lib, cards, verified, sid);
|
|
3652
|
+
// Draft-rule provenance (2026-09-29): the shown-then-promoted (and
|
|
3653
|
+
// shown-then-ignored) loop is the one DETERMINISTIC yes/no signal the
|
|
3654
|
+
// hook owns — record both verdicts in the sidecar. Best-effort and
|
|
3655
|
+
// skew-safe: a bundle without provenance.mjs just skips. Repeat
|
|
3656
|
+
// observations of the same decayed draft dedup into count++ (jid).
|
|
3657
|
+
try {
|
|
3658
|
+
const judgments = [];
|
|
3659
|
+
for (const a of (draftRes.approvedDrafts || [])) {
|
|
3660
|
+
judgments.push({
|
|
3661
|
+
kind: 'rule', direction: 'rule-covers', verdict: 'yes', source: 'draft-promotion',
|
|
3662
|
+
actor: 'agent', session: sid, from: { text: a.text }, to: { text: a.skillText },
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
// A draft surfaced across RULE_DRAFT_MAX_SURFACINGS distinct
|
|
3666
|
+
// sessions and still unpromoted has EARNED its silence — that "no"
|
|
3667
|
+
// is a verdict too (the decay itself only stops the nagging).
|
|
3668
|
+
const nowTs = Date.now();
|
|
3669
|
+
for (const d of readRuleDrafts()) {
|
|
3670
|
+
if (!d || !d.id || !d.text) continue;
|
|
3671
|
+
if ((nowTs - (d.lastSeen || d.firstSeen || 0)) >= RULE_DRAFT_TTL_MS) continue;
|
|
3672
|
+
if ((Array.isArray(d.shownSessions) ? d.shownSessions.length : 0) < RULE_DRAFT_MAX_SURFACINGS) continue;
|
|
3673
|
+
judgments.push({
|
|
3674
|
+
kind: 'rule', direction: 'silence', verdict: 'no', source: 'draft-decay',
|
|
3675
|
+
actor: 'agent', session: sid, from: { text: String(d.text || '') },
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
if (judgments.length) {
|
|
3679
|
+
const prov = await import(new URL('./provenance.mjs', import.meta.url).href);
|
|
3680
|
+
prov.recordJudgments(BRAIN, judgments);
|
|
3681
|
+
}
|
|
3682
|
+
} catch { /* stale deployment or unwritable sidecar — additive signal only */ }
|
|
3646
3683
|
// Cross-lane FINDING routing rides the SAME verify gate: a finding this
|
|
3647
3684
|
// session verified about a file outside its own scope is drafted and
|
|
3648
3685
|
// routed to whoever declared that file. Nothing is sent. `recentPaths`
|
|
@@ -3888,7 +3925,13 @@ async function capture(lib) {
|
|
|
3888
3925
|
if (drainedLanded) clearDrained();
|
|
3889
3926
|
reportBatch();
|
|
3890
3927
|
try { await refreshAgentsBrief(lib, out); } catch { /* AGENTS.md refresh is best-effort */ }
|
|
3891
|
-
|
|
3928
|
+
// ownCounts marks the OWN/DRAINED boundary for the judgment writer
|
|
3929
|
+
// below: drained batches are APPENDED to the shared cards/resolutions
|
|
3930
|
+
// arrays (and land at the same tail indices), so "index < own length"
|
|
3931
|
+
// is exactly "this session's marker". Without it, scope 'all' stamped
|
|
3932
|
+
// THIS session's prompt hash and actor grade onto verdicts whose
|
|
3933
|
+
// markers came from ANOTHER session's queued batch (2026-09-29 review).
|
|
3934
|
+
return { ...res.stats, batchScope: scope, ownCounts: { cards: ownCards.length, resolutions: ownResolutions.length }, ...(batchError ? { batchError: String(batchError.message || batchError).slice(0, 160) } : {}), ...(quarantined.length ? { quarantined } : {}) };
|
|
3892
3935
|
};
|
|
3893
3936
|
// Canonical cross-process lock (heartbeat + token-checked release, shared
|
|
3894
3937
|
// with the MCP engine and the desktop app). Stale bundles missing the module
|
|
@@ -3942,6 +3985,82 @@ async function capture(lib) {
|
|
|
3942
3985
|
else if (outcome === 'fallback-milestone') d.action = 'resolve-unmatched';
|
|
3943
3986
|
}
|
|
3944
3987
|
}
|
|
3988
|
+
// Marker-verdict provenance (2026-09-29): what this Stop's markers ACTUALLY
|
|
3989
|
+
// retired — closes: fulfillments (stats.closedCards), archived ✓ resolves
|
|
3990
|
+
// (per-outcome cardIds), and the hint edges a full resolve settled — goes
|
|
3991
|
+
// to the machine-local provenance sidecar with an honest actor grade: a
|
|
3992
|
+
// terminal marker is agent-emittable, so the strongest attainable class is
|
|
3993
|
+
// 'agent-human-adjacent' (a machine-turn-guarded human prompt sat above
|
|
3994
|
+
// it), never 'human'. The prompt itself rides as a hash only.
|
|
3995
|
+
//
|
|
3996
|
+
// ATTRIBUTION BOUNDARY (2026-09-29 review): scope 'all' — the NORMAL
|
|
3997
|
+
// success path — means the landed arrays are this session's markers (the
|
|
3998
|
+
// prefix) PLUS queued batches from EARLIER sessions appended by the drain.
|
|
3999
|
+
// This session's prompt hash, session id and human-adjacency grade may
|
|
4000
|
+
// only ever stamp the own prefix; a drained batch's verdict is recorded
|
|
4001
|
+
// with the honest bare 'agent' grade and NO session/prompt — its human,
|
|
4002
|
+
// if one existed, sat above a different transcript this hook never saw.
|
|
4003
|
+
// Scope 'drained' (own half failed entirely) is skipped outright, as
|
|
4004
|
+
// before. Best-effort + skew-safe.
|
|
4005
|
+
if (stats.batchScope !== 'drained') {
|
|
4006
|
+
try {
|
|
4007
|
+
const ownCardCount = stats.ownCounts ? stats.ownCounts.cards : cards.length;
|
|
4008
|
+
const ownResolutionCount = stats.ownCounts ? stats.ownCounts.resolutions : resolutions.length;
|
|
4009
|
+
const hasDrained = cards.length > ownCardCount || resolutions.length > ownResolutionCount;
|
|
4010
|
+
const ownStamp = { actor: lastUserPrompt ? 'agent-human-adjacent' : 'agent', humanPromptText: lastUserPrompt, session: sid };
|
|
4011
|
+
const drainedStamp = { actor: 'agent' }; // another session's marker: agent-asserted, nothing stronger provable
|
|
4012
|
+
const judgments = [];
|
|
4013
|
+
for (const cc of (stats.closedCards || [])) {
|
|
4014
|
+
// The closing card is the captured card whose closes: named this
|
|
4015
|
+
// target (stats.closedCards carries the closed card, not the
|
|
4016
|
+
// closer). findIndex against the shared array places the closer
|
|
4017
|
+
// on the own/drained side of the boundary; an own closer with
|
|
4018
|
+
// the same closes text as a drained one wins the prefix scan,
|
|
4019
|
+
// which is correct — this session really did carry the marker.
|
|
4020
|
+
const closerIdx = cards.findIndex(c => c && c.closes && String(c.closes).trim().slice(0, 80) === cc.target);
|
|
4021
|
+
if (closerIdx === -1) continue; // unidentifiable closer (engine skew) — never guess attribution
|
|
4022
|
+
judgments.push({
|
|
4023
|
+
kind: 'pair', direction: 'closes', verdict: 'yes', source: 'marker-closes',
|
|
4024
|
+
...(closerIdx < ownCardCount ? ownStamp : drainedStamp),
|
|
4025
|
+
from: { id: cc.id, text: cc.title || '' },
|
|
4026
|
+
to: { text: cards[closerIdx].text },
|
|
4027
|
+
});
|
|
4028
|
+
}
|
|
4029
|
+
const seenResolved = new Set();
|
|
4030
|
+
for (const o of (stats.resolutionOutcomes || [])) {
|
|
4031
|
+
if (o.outcome !== 'archived') continue;
|
|
4032
|
+
const cardId = o.cardId || o.id;
|
|
4033
|
+
if (!cardId || seenResolved.has(cardId)) continue;
|
|
4034
|
+
seenResolved.add(cardId);
|
|
4035
|
+
const r = resolutions[o.i] || {};
|
|
4036
|
+
judgments.push({
|
|
4037
|
+
kind: 'pair', direction: 'closes', verdict: 'yes', source: 'marker-resolve',
|
|
4038
|
+
...(o.i < ownResolutionCount ? ownStamp : drainedStamp),
|
|
4039
|
+
from: { id: cardId },
|
|
4040
|
+
to: { text: r.text || '' },
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
for (const s of (stats.settledHints || [])) {
|
|
4044
|
+
if (s.via !== 'resolve') continue; // a byId settle is the reconcile-confirm's record, not the hook's
|
|
4045
|
+
// `ri` joins the settle to the resolution that caused it (the
|
|
4046
|
+
// engine mirrors resolutionOutcomes' indexing). Without the
|
|
4047
|
+
// join — a stale engine bundle — a merged capture cannot place
|
|
4048
|
+
// the settle on either side of the boundary, so it is skipped
|
|
4049
|
+
// rather than guessed.
|
|
4050
|
+
if (!Number.isInteger(s.ri) && hasDrained) continue;
|
|
4051
|
+
judgments.push({
|
|
4052
|
+
kind: 'pair', direction: 'closes', verdict: 'yes', source: 'hint-settle',
|
|
4053
|
+
...((Number.isInteger(s.ri) ? s.ri < ownResolutionCount : true) ? ownStamp : drainedStamp),
|
|
4054
|
+
from: { id: s.fromId, text: s.fromText || '' },
|
|
4055
|
+
to: { id: s.toId, text: s.toText || '' },
|
|
4056
|
+
});
|
|
4057
|
+
}
|
|
4058
|
+
if (judgments.length) {
|
|
4059
|
+
const prov = await import(new URL('./provenance.mjs', import.meta.url).href);
|
|
4060
|
+
prov.recordJudgments(BRAIN, judgments);
|
|
4061
|
+
}
|
|
4062
|
+
} catch { /* stale deployment or unwritable sidecar — additive signal only */ }
|
|
4063
|
+
}
|
|
3945
4064
|
// Same honesty for ~: a thin update was APPENDED to its card, not a
|
|
3946
4065
|
// replacement, and one the card already says changed nothing — a ledger
|
|
3947
4066
|
// that still read `update` would claim a rewrite that never happened.
|
|
@@ -4128,6 +4247,31 @@ async function cachedStruct(lib) {
|
|
|
4128
4247
|
// SessionStart already paid for. typeof-guarded: an older engine falls back
|
|
4129
4248
|
// to the uncached pure function, then to null (renderers compute their own).
|
|
4130
4249
|
const STATUS_CACHE = CACHE.replace(/\.json$/, '.status.json');
|
|
4250
|
+
// ── Unified-lane index cache (1.87) ──────────────────────────────────────────
|
|
4251
|
+
// rankHookWords / rankHookFused each build a per-struct index that the engine
|
|
4252
|
+
// caches in a WeakMap — useless to THIS one-shot process, which would rebuild
|
|
4253
|
+
// both on every prompt (measured on the 2,984-card brain: ~250-300ms of a
|
|
4254
|
+
// ~740ms end-to-end prompt; ~470ms with this cache — machine-dependent, the
|
|
4255
|
+
// review probe saw ~1s of builds). So the built indexes are persisted next to
|
|
4256
|
+
// the struct cache,
|
|
4257
|
+
// keyed on the brain file's mtimeMs AND size, and re-seeded into the engine's
|
|
4258
|
+
// WeakMaps before ranking. Behaviour-neutral: the doc holds only derived
|
|
4259
|
+
// stats re-linked to live cards by id, and ANY problem (stale key, unknown
|
|
4260
|
+
// id, parse error, version-skewed engine) falls back to an in-memory rebuild.
|
|
4261
|
+
// Named .brief-cache-…hookix.json so pruneCacheDir covers it too.
|
|
4262
|
+
const HOOK_IX_CACHE = CACHE.replace(/\.json$/, '.hookix.json');
|
|
4263
|
+
function seedHookIndexes(lib, struct) {
|
|
4264
|
+
if (typeof lib.seedHookIndexCache !== 'function' || typeof lib.hookIndexCacheDoc !== 'function') return;
|
|
4265
|
+
let st = null; try { st = fs.statSync(BRAIN); } catch { return; }
|
|
4266
|
+
try {
|
|
4267
|
+
const doc = JSON.parse(fs.readFileSync(HOOK_IX_CACHE, 'utf8'));
|
|
4268
|
+
if (doc && doc.mtimeMs === st.mtimeMs && doc.size === st.size && lib.seedHookIndexCache(struct, doc)) return;
|
|
4269
|
+
} catch { /* miss/corrupt → rebuild below */ }
|
|
4270
|
+
try {
|
|
4271
|
+
const doc = lib.hookIndexCacheDoc(struct); // builds both indexes once, warming the WeakMaps
|
|
4272
|
+
fs.writeFileSync(HOOK_IX_CACHE, JSON.stringify({ mtimeMs: st.mtimeMs, size: st.size, ...doc }));
|
|
4273
|
+
} catch { /* cache is best-effort — the rankers rebuild in memory */ }
|
|
4274
|
+
}
|
|
4131
4275
|
function cachedOpenStatusSummary(lib, struct, opts = {}) {
|
|
4132
4276
|
if (!struct) return null;
|
|
4133
4277
|
try {
|
|
@@ -4406,13 +4550,30 @@ async function promptRetrieve(lib) {
|
|
|
4406
4550
|
// nothing (a peer's presence/ship is itself the signal). Empty string when solo.
|
|
4407
4551
|
const peers = peerFooter(sid);
|
|
4408
4552
|
const messages = messageFooter(sid, input.transcript_path, lib); // 📨 durable notes: offer, acknowledge, replay until explicit consumption
|
|
4409
|
-
|
|
4410
|
-
|
|
4553
|
+
// ── Unified recall (1.87) — the bench-measured two-lane policy ──────────
|
|
4554
|
+
// Ported line-for-line from the 2026-09-28 "unified balanced" bench winner
|
|
4555
|
+
// (dev/held-out split of real founder prompts; parity-proven against the
|
|
4556
|
+
// reference policy). Routing: a STRONG status prompt keeps the computed
|
|
4557
|
+
// digest below; a prompt whose UNIQUE CONTENT tokens reach
|
|
4558
|
+
// HOOK_MODEL_MIN_TOKENS takes the MODEL lane (rankHookFused over the warm
|
|
4559
|
+
// vector cache) when the embedder answers in budget and any card vector
|
|
4560
|
+
// exists, else the WORDS lane (rankHookWords, BM25-idf rarity with an
|
|
4561
|
+
// evidence bar) decides. Git-diff fileToks still feed the WORDS lane for
|
|
4562
|
+
// terse prompts but never count toward the model gate — they are not the
|
|
4563
|
+
// asker's words. A repeat nudge suppresses the model lane (and its
|
|
4564
|
+
// embedder cost) exactly as it suppressed the old semantic fallback.
|
|
4565
|
+
let repeats = [], struct = null, freshHits = [];
|
|
4566
|
+
// lane + its deciding evidence are logged for every HUMAN decision — the
|
|
4567
|
+
// silent rows are precisely the data the bars get re-fitted from in the
|
|
4568
|
+
// field (the 2026-09-16 audit had only "hits: 5" ×165 to go on); machine
|
|
4569
|
+
// turns are skipped at the write below (no refit signal, and they would
|
|
4570
|
+
// rotate real rows out of the cap). Note the health log keeps its 500-row
|
|
4571
|
+
// cap, and per-prompt rows still fill it faster than the old sem-only
|
|
4572
|
+
// rows did — a field audit sees a shorter window per file, and the
|
|
4573
|
+
// vocabulary changed: `lane`/`evidence` succeed `top1` (`sem` still lands).
|
|
4574
|
+
let lane = statusStrong ? 'status-digest' : 'no-tokens', laneEvidence = null;
|
|
4575
|
+
if (!statusStrong && tokens.length) {
|
|
4411
4576
|
struct = await cachedStruct(lib);
|
|
4412
|
-
// The lexical bar is the engine's measured export (1.86: 4 — one title
|
|
4413
|
-
// word alone no longer injects; see HOOK_LEXICAL_MIN_SCORE). An older
|
|
4414
|
-
// engine keeps the pre-1.86 bar of 3.
|
|
4415
|
-
hits = lib.scoreCardsAgainstQuery(struct, tokens, { topK: 5, minScore: Number.isFinite(lib.HOOK_LEXICAL_MIN_SCORE) ? lib.HOOK_LEXICAL_MIN_SCORE : 3 });
|
|
4416
4577
|
// PRECISION-first repeat nudge ("you already did this in another session"):
|
|
4417
4578
|
// only on a do/build request, only completed-work cards, only high confidence.
|
|
4418
4579
|
// Matched on the PROMPT's stated intent (ptoks), not the git-diff fallback. A
|
|
@@ -4420,66 +4581,64 @@ async function promptRetrieve(lib) {
|
|
|
4420
4581
|
if (looksLikeWorkRequest(humanText || '') && typeof lib.detectRepeatWork === 'function') {
|
|
4421
4582
|
try { repeats = lib.detectRepeatWork(struct, ptoks, { topK: 2 }); } catch { /* best-effort */ }
|
|
4422
4583
|
}
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
else {
|
|
4451
|
-
// ONE production primitive for this lane (1.86). The ranking
|
|
4452
|
-
// used to live inline here, which meant the surface every
|
|
4453
|
-
// session actually receives was the one no harness could
|
|
4454
|
-
// measure — an eval must IMPORT the ranker, never re-implement
|
|
4455
|
-
// it. rankLexicalMissFallback in klypix-format.mjs is that
|
|
4456
|
-
// primitive (scripts/eval-hook-lane.mjs measures it); its
|
|
4457
|
-
// defaults are the measured ones. An older engine without the
|
|
4458
|
-
// export degrades to the pre-1.86 inline ranking.
|
|
4459
|
-
let ranked;
|
|
4460
|
-
if (typeof lib.rankLexicalMissFallback === 'function') {
|
|
4461
|
-
const r = lib.rankLexicalMissFallback(struct, sem, { topK: 5 });
|
|
4462
|
-
ranked = r.hits; semTop = r.topCos;
|
|
4463
|
-
} else {
|
|
4464
|
-
const fresh = Date.now() - 30 * 86_400_000;
|
|
4465
|
-
ranked = struct.cards
|
|
4466
|
-
.filter(c => c.type !== 'container' && (c.text || '').trim() && !/^archive$/i.test(c.area || ''))
|
|
4467
|
-
.map(c => { const v = sem.vecsMap.get(c.id); return { card: c, s: v ? sem.dot(sem.qv, v) : null }; })
|
|
4468
|
-
.filter(x => x.s != null && x.s >= 0.30)
|
|
4469
|
-
.map(x => { let score = x.s * 10; if ((x.card.createdAt || 0) >= fresh) score += 0.5; return { card: x.card, score }; })
|
|
4470
|
-
.sort((a, b) => b.score - a.score).slice(0, 5);
|
|
4584
|
+
const repeatIds = new Set(repeats.map(r => r.card.id));
|
|
4585
|
+
if (typeof lib.rankHookWords === 'function' && typeof lib.rankHookFused === 'function') {
|
|
4586
|
+
seedHookIndexes(lib, struct);
|
|
4587
|
+
let decided = false;
|
|
4588
|
+
// MODEL lane gate, in this order: repeat-nudge check, then the
|
|
4589
|
+
// unique-content-token bar — semanticVecs (and its embedder load)
|
|
4590
|
+
// is never paid for a terse prompt or a nudged repeat.
|
|
4591
|
+
if (!repeats.length && new Set(ptoks).size >= lib.HOOK_MODEL_MIN_TOKENS) {
|
|
4592
|
+
try {
|
|
4593
|
+
const semlib = await import(new URL('./brain-semantic.mjs', import.meta.url).href);
|
|
4594
|
+
if (typeof semlib.semanticVecs === 'function') {
|
|
4595
|
+
// Embed the DERIVED human text, never the raw prompt — a
|
|
4596
|
+
// mixed turn's stripped machine block must not
|
|
4597
|
+
// re-contaminate the semantic query. semanticVecs
|
|
4598
|
+
// lowercases, reads the warm card-vector cache read-only,
|
|
4599
|
+
// and returns null on ANY failure (not installed /
|
|
4600
|
+
// timeout / empty cache) → the words lane decides.
|
|
4601
|
+
const sem = await semlib.semanticVecs(BRAIN, struct, humanText || '', { timeoutMs: 1500 });
|
|
4602
|
+
if (sem) {
|
|
4603
|
+
const fused = lib.rankHookFused(struct, sem, tokens, { topK: 5 });
|
|
4604
|
+
if (fused.hits.length) { // pool > 0; an empty pool is "no vectors" → words lane
|
|
4605
|
+
decided = true;
|
|
4606
|
+
laneEvidence = fused.best;
|
|
4607
|
+
if (fused.best >= fused.bar) { lane = 'model-shown'; freshHits = fused.hits; }
|
|
4608
|
+
else lane = 'model-silent'; // below-bar is a DECISION, never rescued by words
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4471
4611
|
}
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4612
|
+
} catch { /* any model-lane failure degrades to the words lane */ }
|
|
4613
|
+
}
|
|
4614
|
+
if (!decided) {
|
|
4615
|
+
const w = lib.rankHookWords(struct, tokens, { topK: 5 });
|
|
4616
|
+
laneEvidence = w.evidence;
|
|
4617
|
+
if (w.hits.length && w.evidence >= w.bar) { lane = 'words-shown'; freshHits = w.hits; }
|
|
4618
|
+
else lane = 'words-silent';
|
|
4475
4619
|
}
|
|
4476
|
-
}
|
|
4620
|
+
} else {
|
|
4621
|
+
// Version-skew fallback (older klypix-format without the 1.87
|
|
4622
|
+
// exports): the pre-unified lexical lane at HOOK_LEXICAL_MIN_SCORE.
|
|
4623
|
+
freshHits = lib.scoreCardsAgainstQuery(struct, tokens, { topK: 5, minScore: Number.isFinite(lib.HOOK_LEXICAL_MIN_SCORE) ? lib.HOOK_LEXICAL_MIN_SCORE : 3 });
|
|
4624
|
+
lane = freshHits.length ? 'lexical' : 'lexical-silent';
|
|
4625
|
+
}
|
|
4626
|
+
// Repeat ids are filtered from the SHOWN hits — the nudge block renders
|
|
4627
|
+
// those cards itself, with their correction overlays.
|
|
4628
|
+
freshHits = freshHits.filter(h => !repeatIds.has(h.card.id));
|
|
4629
|
+
}
|
|
4630
|
+
// Header choice downstream: only a model-lane show is a "semantic match".
|
|
4631
|
+
const semMode = lane === 'model-shown' ? 'sem-hit' : 'lexical';
|
|
4632
|
+
// Health row: HUMAN decisions only. A machine turn (humanText === null —
|
|
4633
|
+
// harness-injected reminders, task notifications) always lands 'no-tokens'
|
|
4634
|
+
// and carries zero refit signal, but an agent-team session generates
|
|
4635
|
+
// hundreds of them a day — logged, they would rotate the evidence-bearing
|
|
4636
|
+
// words-silent/model-silent rows out of the 500-row cap (2026-09-29
|
|
4637
|
+
// review). `sem` is kept for grep continuity with the pre-1.87 rows; the
|
|
4638
|
+
// old `top1` field's successor is `evidence` (the deciding lane value).
|
|
4639
|
+
if (humanText !== null) {
|
|
4640
|
+
try { appendJsonl(HEALTH, { ts: nowIso(), project: path.basename(CWD), mode: 'prompt', sem: semMode, lane, hits: freshHits.length, mtok: new Set(ptoks).size, ...(Number.isFinite(laneEvidence) ? { evidence: Math.round(laneEvidence * 1000) / 1000 } : {}) }, 500); } catch { /* health log is best-effort */ }
|
|
4477
4641
|
}
|
|
4478
|
-
// The health row carries the best cosine the lane saw (`top1`) so the
|
|
4479
|
-
// admission rule can be re-fitted from the field later without a
|
|
4480
|
-
// transcript — the 2026-09-16 audit had only "hits: 5" ×165 to go on. A
|
|
4481
|
-
// skipped lane is logged too (`sem-skipped`, with the content-token count).
|
|
4482
|
-
if (semMode !== 'lexical') { try { appendJsonl(HEALTH, { ts: nowIso(), project: path.basename(CWD), mode: 'prompt', sem: semMode, hits: freshHits.length, ...(Number.isFinite(semTop) ? { top1: Math.round(semTop * 1000) / 1000 } : {}), ...(semMode === 'sem-skipped' ? { content: ptoks.length } : {}) }, 500); } catch { /* */ } }
|
|
4483
4642
|
// T8 STATUS-DIGEST INJECTION (2026-07-23): a status-shaped prompt gets the
|
|
4484
4643
|
// COMPUTED current-state digest INSTEAD of card hits — so an agent that
|
|
4485
4644
|
// never queried the brain still answers "what is remaining?" from state,
|
|
@@ -4958,11 +5117,15 @@ function staleOpenFooter(stale) {
|
|
|
4958
5117
|
const flat = (s) => String(s || '').replace(/\s+/g, ' ').trim();
|
|
4959
5118
|
const lines = [];
|
|
4960
5119
|
if (gaps && gaps.length) {
|
|
5120
|
+
// The id-addressed confirm is the PREFERRED channel (2026-09-29):
|
|
5121
|
+
// it names exact ids, settles the dashed hint to a solid 'closed
|
|
5122
|
+
// by', and records the confirmation trail — the bare ✓ marker
|
|
5123
|
+
// resolves by prose match and stays the human-typed fallback.
|
|
4961
5124
|
lines.push('', '---',
|
|
4962
5125
|
`## 🔧 Self-heal — ${total} open card(s) look DONE (a later milestone covers them)`,
|
|
4963
|
-
`These ❓/🎯 cards still read as open, but a shipped 🏁 milestone appears to fulfil them — so recall keeps surfacing already-done goals as "next".
|
|
4964
|
-
'· done →
|
|
4965
|
-
for (const g of gaps) lines.push(`- ⚠️ [${flat(g.open.area) || '?'}] ${flat(g.open.text).slice(0, 90)} · likely closed by → ${flat(g.by.text).slice(0, 70)}
|
|
5126
|
+
`These ❓/🎯 cards still read as open, but a shipped 🏁 milestone appears to fulfil them — so recall keeps surfacing already-done goals as "next". Verify, then confirm + close each:`,
|
|
5127
|
+
'· done → the per-card `brain_reconcile mode:"claims" confirm:[{ id, milestoneId }]` call below — stamps ✅, archives the open card, and settles its hint with a recorded confirmation. Human-typed fallback: `🧠 BRAIN [Area] ✓: <what it resolved to>` (or add `closes: <its title>` to the milestone marker).');
|
|
5128
|
+
for (const g of gaps) lines.push(`- ⚠️ [${flat(g.open.area) || '?'}] ${flat(g.open.text).slice(0, 90)} · likely closed by → ${flat(g.by.text).slice(0, 70)}\n confirm: \`brain_reconcile mode:"claims" confirm:[{ id:"${g.open.id}", milestoneId:"${g.by.id}" }]\``);
|
|
4966
5129
|
}
|
|
4967
5130
|
// Plan-shaped cards (2026-08-23): the same leak for proposals that never
|
|
4968
5131
|
// carried a ❓ — recall serves them as current intent ("only a proposal")
|