@ijfw/memory-server 1.5.0 → 1.5.1
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/ijfw-memorize +14 -7
- package/fixtures/team/book.json +6 -6
- package/fixtures/team/business.json +146 -20
- package/fixtures/team/content.json +6 -6
- package/fixtures/team/design.json +148 -20
- package/fixtures/team/mixed.json +206 -27
- package/fixtures/team/research.json +146 -20
- package/fixtures/team/software.json +148 -20
- package/package.json +6 -3
- package/src/cross-orchestrator-cli.js +204 -145
- package/src/cross-orchestrator.js +50 -1
- package/src/dispatch/extension.js +1 -1
- package/src/hardware-signer.js +4 -2
- package/src/lib/ui-review-runner.js +48 -7
- package/src/memory/auto-linker.js +116 -1
- package/src/memory/migration-runner.js +6 -1
- package/src/memory/migrations/009-obsidian-backfill.js +50 -0
- package/src/memory/obsidian-parser.js +62 -1
- package/src/memory/search.js +46 -25
- package/src/orchestrator/debug-trident-trigger.js +374 -0
- package/src/orchestrator/post-done-runner.js +36 -8
- package/src/orchestrator/state-sdk.js +174 -6
- package/src/orchestrator/subagent-telemetry.js +19 -0
- package/src/override-resolver.js +5 -3
- package/src/recovery/code-fixer.js +310 -5
- package/src/runtime-mediator.js +0 -1
- package/src/server.js +198 -59
- package/src/swarm-config.js +30 -22
- package/src/team/domain-templates/business.json +4 -1
- package/src/team/domain-templates/research.json +4 -1
- package/src/team/generator.js +162 -0
- package/src/update-apply.js +1 -1
- package/src/dashboard-charts.js +0 -239
- package/src/orchestrator/runtime-loop.js +0 -430
package/src/server.js
CHANGED
|
@@ -41,6 +41,11 @@ import { maybeRerankWithVectors } from './search-hybrid.js';
|
|
|
41
41
|
import { crossProjectSearch } from './cross-project-search.js';
|
|
42
42
|
// R2-E -- single source of truth for markdown/HTML/control-char defanger.
|
|
43
43
|
import { sanitizeContent } from './sanitizer.js';
|
|
44
|
+
// v1.5.1 R4-H3 — secret redaction on the direct ijfw_store write path.
|
|
45
|
+
// The redactor is already wired into FTS5 ingest + auto-memorize; the
|
|
46
|
+
// direct MCP store was the one bypass, so a secret pasted into an
|
|
47
|
+
// ijfw_store call could land in .ijfw/memory/*.md cleartext.
|
|
48
|
+
import { redactSecrets } from './redactor.js';
|
|
44
49
|
// H5.5 / H5.6 — ingest-time fact extraction + semantic dedup. Closes
|
|
45
50
|
// memory-engine.md competitor gaps (mem0/Zep extract facts; Graphiti dedups).
|
|
46
51
|
// Both are pure-JS, zero-LLM, deterministic.
|
|
@@ -591,23 +596,6 @@ function readGlobalKnowledge() {
|
|
|
591
596
|
).join('\n\n');
|
|
592
597
|
}
|
|
593
598
|
|
|
594
|
-
function getSessionCount() {
|
|
595
|
-
try {
|
|
596
|
-
if (!existsSync(SESSIONS_DIR)) return 0;
|
|
597
|
-
return readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.md')).length;
|
|
598
|
-
} catch {
|
|
599
|
-
return 0;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function getDecisionCount() {
|
|
604
|
-
const journal = readOr(join(MEMORY_DIR, 'project-journal.md'));
|
|
605
|
-
if (!journal) return 0;
|
|
606
|
-
// Match only journal entry lines (we now prefix with - [timestamp]) -- not
|
|
607
|
-
// arbitrary list bullets that might appear in seeded content.
|
|
608
|
-
return (journal.match(/^- \[\d{4}-\d{2}-\d{2}T/gm) || []).length;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
599
|
function getRecentJournalEntries(count = 5) {
|
|
612
600
|
const journal = readOr(join(MEMORY_DIR, 'project-journal.md'));
|
|
613
601
|
if (!journal) return '';
|
|
@@ -991,7 +979,7 @@ const TOOLS = [
|
|
|
991
979
|
},
|
|
992
980
|
{
|
|
993
981
|
name: 'ijfw_memory_search',
|
|
994
|
-
description: 'Keyword search across memory sources. Up to 20 results. Scope defaults to current project; pass scope:"all" to search across every IJFW project ever opened on this machine (results tagged [project:<name>]). Pass scope:"sandbox" to retrieve sandboxed ijfw_run output -- include label to get the full output of a specific run, or omit label to list all available sandbox entries.',
|
|
982
|
+
description: 'Keyword search across memory sources. Up to 20 results. Scope defaults to current project; pass scope:"all" to search across every IJFW project ever opened on this machine (results tagged [project:<name>]). Pass scope:"sandbox" to retrieve sandboxed ijfw_run output -- include label to get the full output of a specific run, or omit label to list all available sandbox entries. The query field also accepts colon-namespaced commands: "compute:<query>" hits the per-project FTS5 index, "graph:<query>" routes through the knowledge-graph search.',
|
|
995
983
|
inputSchema: {
|
|
996
984
|
type: 'object',
|
|
997
985
|
properties: {
|
|
@@ -1050,8 +1038,8 @@ const TOOLS = [
|
|
|
1050
1038
|
inputSchema: {
|
|
1051
1039
|
type: 'object',
|
|
1052
1040
|
properties: {
|
|
1053
|
-
period: { type: 'string', enum: ['today', '7d', '30d', 'all'], description: 'Time window (default 7d).' },
|
|
1054
|
-
metric: { type: 'string', enum: ['tokens', 'cost', 'sessions', 'routing'], description: 'Which metric to render (default tokens).' }
|
|
1041
|
+
period: { type: 'string', enum: ['today', '7d', '30d', 'all'], default: '7d', description: 'Time window (default 7d).' },
|
|
1042
|
+
metric: { type: 'string', enum: ['tokens', 'cost', 'sessions', 'routing'], default: 'tokens', description: 'Which metric to render (default tokens).' }
|
|
1055
1043
|
},
|
|
1056
1044
|
required: []
|
|
1057
1045
|
}
|
|
@@ -1072,7 +1060,7 @@ const TOOLS = [
|
|
|
1072
1060
|
UPDATE_APPLY_TOOL,
|
|
1073
1061
|
{
|
|
1074
1062
|
name: 'ijfw_run',
|
|
1075
|
-
description: 'Run a shell command. For commands likely to produce large output (builds, test suites, grep -r, log tails), use this instead of Bash -- full output is sandboxed to disk and a smart summary is returned to context. For git/nav/quick ops, use Bash directly.',
|
|
1063
|
+
description: 'Run a shell command. For commands likely to produce large output (builds, test suites, grep -r, log tails), use this instead of Bash -- full output is sandboxed to disk and a smart summary is returned to context. For git/nav/quick ops, use Bash directly. Also accepts colon-namespaced commands instead of a shell line: "compute:python", "compute:js", "index:<source>", "detect:project_type".',
|
|
1076
1064
|
inputSchema: {
|
|
1077
1065
|
type: 'object',
|
|
1078
1066
|
properties: {
|
|
@@ -1088,10 +1076,10 @@ const TOOLS = [
|
|
|
1088
1076
|
// Absorbs the retired ijfw_subagent_post_done tool (post-done IS a state
|
|
1089
1077
|
// transition → reachable as the `subagent.post-done` verb). All 20 frozen
|
|
1090
1078
|
// verbs from STATE-SDK-CONTRACT §7 are reachable through this one tool,
|
|
1091
|
-
// keeping the MCP cap at
|
|
1079
|
+
// keeping the MCP cap at 13/13. The same `query(verb, payload, ctx)` core
|
|
1092
1080
|
// is also exposed as a JS import and a CLI colon-namespace (`ijfw state:<verb>`).
|
|
1093
1081
|
name: 'ijfw_state',
|
|
1094
|
-
description: 'State-SDK verb facade — invoke any of the 20 frozen verbs
|
|
1082
|
+
description: 'State-SDK verb facade — invoke any of the 20 frozen verbs over the canonical physical state files. The 20 verbs: workflow.get, workflow.set-phase, wave.get, wave.advance, wave.record-task, phase.plan-check, phase.complete, subagent.dispatch, subagent.checkpoint, subagent.post-done, event.emit, telemetry.record, roster.synthesize, roster.record, extension.set-active, decision.add, blocker.add, blocker.resolve, state.replay, state.validate. Single MCP face for the state-SDK; subagent.post-done is the verb that absorbed the retired ijfw_subagent_post_done tool. Returns the verb result with `ok` + `verbId` + verb-specific fields (see STATE-SDK-CONTRACT §7).',
|
|
1095
1083
|
inputSchema: {
|
|
1096
1084
|
type: 'object',
|
|
1097
1085
|
properties: {
|
|
@@ -1110,15 +1098,16 @@ const TOOLS = [
|
|
|
1110
1098
|
// (codex/gemini/claude by default) in parallel; if verdicts diverge,
|
|
1111
1099
|
// re-runs with a CYCLE_SUMMARY of the disagreement until consensus or
|
|
1112
1100
|
// maxIterations (default 3). Stall breaker halts on byte-identical
|
|
1113
|
-
// iterations.
|
|
1101
|
+
// iterations. Slot 12 of the 13/13 tool cap.
|
|
1114
1102
|
name: 'ijfw_cross_audit_converge',
|
|
1115
1103
|
description: 'Multi-lens Trident audit with consensus convergence loop. Dispatches codex/gemini/claude in parallel against a commit range, detects verdict divergence, and re-runs with a cycle summary until consensus or maxIterations. Returns {verdict, iterations, findings, divergence?, stalled?}. Verdict: PASS / CONDITIONAL / FAIL / consensus_failed / UNREACHABLE.',
|
|
1116
1104
|
inputSchema: {
|
|
1117
1105
|
type: 'object',
|
|
1118
1106
|
properties: {
|
|
1119
1107
|
commitRange: { type: 'string', description: 'Git commit range to audit (e.g. "HEAD~1..HEAD", "main..feature/x"). Required.' },
|
|
1120
|
-
maxIterations: { type: 'number', description: 'Max convergence iterations (default 3). 1 → single-shot (fallback mode).' },
|
|
1108
|
+
maxIterations: { type: 'number', minimum: 1, maximum: 10, description: 'Max convergence iterations (default 3, capped at 10). 1 → single-shot (fallback mode).' },
|
|
1121
1109
|
lenses: { type: 'array', items: { type: 'string' }, description: 'Lens ids to dispatch (default ["codex","gemini","claude"]).' },
|
|
1110
|
+
autoFix: { type: 'boolean', description: 'v1.5.1 (T27) — opt-in consensus auto-fix. When true, after a non-PASS convergence the consensus code-fixer AUTOMATICALLY MODIFIES CODE: it runs an atomic per-finding fix loop (one revertable git commit per fix) over HIGH findings that 2+ lenses agreed on. SAFETY BOUNDS: the fixer can only write files inside the audited project root (path-containment guard refuses out-of-root paths) and touches at most 10 distinct files per run (change cap — beyond it it stops and reports rather than mass-rewriting). Logic bugs are deferred to humans, never auto-patched. Results surface on result.autoFix without changing the verdict. Default false — the audit is read-only unless you explicitly opt in.' },
|
|
1122
1111
|
},
|
|
1123
1112
|
required: ['commitRange'],
|
|
1124
1113
|
},
|
|
@@ -1272,6 +1261,69 @@ function handleRecall({ context_hint, detail_level = 'standard', from_project })
|
|
|
1272
1261
|
return { text: results.map(r => `[${r.source}] ${r.content}`).join('\n') };
|
|
1273
1262
|
}
|
|
1274
1263
|
|
|
1264
|
+
// v1.5.1 R4-H2 — wire the v1.5.0 memory-moat to the real write path.
|
|
1265
|
+
//
|
|
1266
|
+
// M1 (Obsidian wikilink/tag/meta indexing -> memory_links/_tags/_meta) and
|
|
1267
|
+
// M2 (A-Mem auto-linking) only fire inside memory/fts5.js#indexEntry. But the
|
|
1268
|
+
// production memory writers never called indexEntry: handleStore wrote the
|
|
1269
|
+
// markdown journal only, and search.js#autoIndex did a raw INSERT. So the
|
|
1270
|
+
// memory-moat's flagship — "memory that learns about you" — ran ONLY in the
|
|
1271
|
+
// benchmark harness. This helper routes a real ijfw_store through indexEntry,
|
|
1272
|
+
// which in one atomic INSERT also fires M1 (synchronous, idempotent) + M2
|
|
1273
|
+
// (fire-and-forget, env-gated via IJFW_AUTOLINK_OFF, budget-capped).
|
|
1274
|
+
//
|
|
1275
|
+
// Best-effort + fire-and-forget: handleStore stays synchronous and a missing
|
|
1276
|
+
// driver / unmigrated schema / DB error never breaks the markdown-or-JSONL
|
|
1277
|
+
// store path. The journal markdown remains the source of truth (hot tier);
|
|
1278
|
+
// the FTS5 row is the warm-tier mirror. Dedup safety: handleStore previously
|
|
1279
|
+
// did NO DB INSERT at all, so this is a NEW row, not a duplicate of an
|
|
1280
|
+
// existing write. search.js#autoIndex only batch-rebuilds when the FTS table
|
|
1281
|
+
// is empty (rowCount === 0) — it will skip an already-populated table — so a
|
|
1282
|
+
// store followed by a search cannot double-index the same entry.
|
|
1283
|
+
async function indexStoredEntryToFts5({ body, source, sessionId }) {
|
|
1284
|
+
if (typeof body !== 'string' || body.length === 0) return null;
|
|
1285
|
+
const fts5Mod = await import('./memory/fts5.js');
|
|
1286
|
+
const root = process.env.IJFW_PROJECT_DIR || PROJECT_DIR;
|
|
1287
|
+
const db = await fts5Mod.openDb(root);
|
|
1288
|
+
try {
|
|
1289
|
+
// indexEntry runs the ingest scrub gate + M1 indexObsidianRelations +
|
|
1290
|
+
// M2 autoLink internally. body is already sanitised + redacted by the
|
|
1291
|
+
// handleStore caller; the scrub gate re-running over already-clean text
|
|
1292
|
+
// is idempotent.
|
|
1293
|
+
const inserted = fts5Mod.indexEntry(db, {
|
|
1294
|
+
body,
|
|
1295
|
+
source: source || 'memory_store',
|
|
1296
|
+
session_id: sessionId || null,
|
|
1297
|
+
});
|
|
1298
|
+
// indexEntry dispatches M2 autoLink + the D2 graph auto-index as
|
|
1299
|
+
// fire-and-forget promises that still hold the db handle. We own the
|
|
1300
|
+
// handle here, so we MUST let those settle before closing the db —
|
|
1301
|
+
// otherwise autoLink races into a "database connection is not open"
|
|
1302
|
+
// error. Capture the promise references SYNCHRONOUSLY right after
|
|
1303
|
+
// indexEntry returns (no await in between) so an interleaved store
|
|
1304
|
+
// can't overwrite the module-level statics before we read them. Both
|
|
1305
|
+
// promises swallow their own failures, so awaiting them never rejects.
|
|
1306
|
+
// This keeps M2 wired on the real store path without changing
|
|
1307
|
+
// handleStore's fire-and-forget contract (the caller already treats
|
|
1308
|
+
// this whole function as fire-and-forget).
|
|
1309
|
+
const autoLinkP = fts5Mod.indexEntry.__lastAutoLinkPromise;
|
|
1310
|
+
const autoIndexP = typeof fts5Mod.__getLastAutoIndexPromise === 'function'
|
|
1311
|
+
? fts5Mod.__getLastAutoIndexPromise()
|
|
1312
|
+
: null;
|
|
1313
|
+
try { await autoLinkP; } catch { /* swallowed by indexEntry */ }
|
|
1314
|
+
try { await autoIndexP; } catch { /* swallowed by indexEntry */ }
|
|
1315
|
+
return inserted;
|
|
1316
|
+
} finally {
|
|
1317
|
+
try { fts5Mod.closeDb(db); } catch { /* best-effort */ }
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Diagnostic hook for tests — holds the most recent FTS5/M1/M2 indexing
|
|
1322
|
+
// promise fired by handleStore so end-to-end tests can await deterministic
|
|
1323
|
+
// completion before asserting on memory_links / memory_tags. Production
|
|
1324
|
+
// callers do not read this.
|
|
1325
|
+
handleStore.__lastIndexPromise = null;
|
|
1326
|
+
|
|
1275
1327
|
function handleStore({ content, type, tags = [], summary, why, how_to_apply }) {
|
|
1276
1328
|
// --- Input Validation ---
|
|
1277
1329
|
if (!content || typeof content !== 'string') {
|
|
@@ -1304,13 +1356,22 @@ function handleStore({ content, type, tags = [], summary, why, how_to_apply }) {
|
|
|
1304
1356
|
|
|
1305
1357
|
// Sanitize ALL text fields -- never store raw user/agent text in markdown
|
|
1306
1358
|
// that gets re-injected into a future LLM context.
|
|
1307
|
-
|
|
1359
|
+
//
|
|
1360
|
+
// v1.5.1 R4-H3 — secret redaction. sanitizeContent strips prompt-injection
|
|
1361
|
+
// control chars but does NOT scrub secret-shaped tokens (API keys, OAuth
|
|
1362
|
+
// secrets). Without this, a secret pasted into a direct ijfw_store call
|
|
1363
|
+
// lands in .ijfw/memory/*.md cleartext and re-injects into every future
|
|
1364
|
+
// recall. The redactor is already wired into the FTS5 ingest path
|
|
1365
|
+
// (memory/fts5.js#indexEntry) and the auto-memorize path; this closes the
|
|
1366
|
+
// direct MCP store as the one remaining bypass. Redact AFTER sanitize so
|
|
1367
|
+
// the redaction labels ([REDACTED:*]) are never themselves scrubbed.
|
|
1368
|
+
const safeContent = redactSecrets(sanitizeContent(content));
|
|
1308
1369
|
if (!safeContent) {
|
|
1309
1370
|
return { text: 'content was empty after sanitisation (only control/format chars).', isError: true };
|
|
1310
1371
|
}
|
|
1311
|
-
const safeSummary = summary ? sanitizeContent(summary).substring(0, 120) : '';
|
|
1312
|
-
const safeWhy = why ? sanitizeContent(why) : '';
|
|
1313
|
-
const safeHow = how_to_apply ? sanitizeContent(how_to_apply) : '';
|
|
1372
|
+
const safeSummary = summary ? redactSecrets(sanitizeContent(summary)).substring(0, 120) : '';
|
|
1373
|
+
const safeWhy = why ? redactSecrets(sanitizeContent(why)) : '';
|
|
1374
|
+
const safeHow = how_to_apply ? redactSecrets(sanitizeContent(how_to_apply)) : '';
|
|
1314
1375
|
|
|
1315
1376
|
const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
|
|
1316
1377
|
const journalEntry = `**${type}**${tagStr}: ${safeSummary || safeContent.substring(0, 200)}`;
|
|
@@ -1345,6 +1406,27 @@ function handleStore({ content, type, tags = [], summary, why, how_to_apply }) {
|
|
|
1345
1406
|
return { text: `Memory journal is not writable (${journalResult.code}) -- check .ijfw/ directory permissions and retry.`, isError: true };
|
|
1346
1407
|
}
|
|
1347
1408
|
|
|
1409
|
+
// v1.5.1 R4-H2 — mirror the stored entry into the FTS5 warm tier, which
|
|
1410
|
+
// also fires M1 (Obsidian indexing) + M2 (A-Mem auto-linking). Index the
|
|
1411
|
+
// full content (already sanitised + redacted above) so [[wikilinks]],
|
|
1412
|
+
// #tags and [key:: value] metadata land in memory_links/_tags/_meta and
|
|
1413
|
+
// the auto-linker sees the real body. Fire-and-forget: handleStore stays
|
|
1414
|
+
// synchronous and a DB failure never breaks the markdown store. The
|
|
1415
|
+
// promise is exposed for tests that need deterministic completion.
|
|
1416
|
+
try {
|
|
1417
|
+
handleStore.__lastIndexPromise = indexStoredEntryToFts5({
|
|
1418
|
+
body: safeContent,
|
|
1419
|
+
source: `memory_store:${type}`,
|
|
1420
|
+
sessionId: null,
|
|
1421
|
+
}).catch((e) => {
|
|
1422
|
+
try { console.error('[ijfw memory] FTS5/M1/M2 index failed:', e?.message || e); } catch { /* never throw */ }
|
|
1423
|
+
return null;
|
|
1424
|
+
});
|
|
1425
|
+
} catch (e) {
|
|
1426
|
+
handleStore.__lastIndexPromise = null;
|
|
1427
|
+
try { console.error('[ijfw memory] FTS5 index dispatch failed:', e?.message || e); } catch { /* never throw */ }
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1348
1430
|
// H5.5 — Fact extraction AFTER successful append. Best-effort: a failure
|
|
1349
1431
|
// here is logged in the return text but does NOT poison the store result.
|
|
1350
1432
|
// Memory-id ties facts.jsonl rows back to their journal entry.
|
|
@@ -1848,32 +1930,6 @@ function handleMetrics({ period = '7d', metric = 'tokens' } = {}) {
|
|
|
1848
1930
|
return { text: out.join('\n') };
|
|
1849
1931
|
}
|
|
1850
1932
|
|
|
1851
|
-
function handleStatus() {
|
|
1852
|
-
const sessionCount = getSessionCount();
|
|
1853
|
-
const decisionCount = getDecisionCount();
|
|
1854
|
-
const hasKnowledge = existsSync(join(MEMORY_DIR, 'knowledge.md'));
|
|
1855
|
-
const hasHandoff = existsSync(join(MEMORY_DIR, 'handoff.md'));
|
|
1856
|
-
const hasGlobal = readGlobalKnowledge().trim().length > 0;
|
|
1857
|
-
|
|
1858
|
-
const parts = [];
|
|
1859
|
-
if (hasKnowledge) {
|
|
1860
|
-
const kb = readKnowledgeBase();
|
|
1861
|
-
const kbLines = kb.split('\n').filter(l => l.trim().startsWith('**')).length;
|
|
1862
|
-
parts.push(`Knowledge: ${kbLines} entries`);
|
|
1863
|
-
}
|
|
1864
|
-
if (sessionCount > 0 || decisionCount > 0) {
|
|
1865
|
-
parts.push(`History: ${sessionCount} sessions, ${decisionCount} decisions`);
|
|
1866
|
-
}
|
|
1867
|
-
if (hasHandoff) {
|
|
1868
|
-
const handoff = readHandoff();
|
|
1869
|
-
const statusLine = handoff.split('\n').find(l => l.trim().length > 0 && !l.startsWith('<!--') && !l.startsWith('#'));
|
|
1870
|
-
if (statusLine) parts.push(`Last: ${statusLine.trim().substring(0, 150)}`);
|
|
1871
|
-
}
|
|
1872
|
-
if (hasGlobal) parts.push('Project preferences loaded');
|
|
1873
|
-
|
|
1874
|
-
return { text: parts.join('\n') || 'Fresh project -- no memory yet.' };
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
1933
|
// --- MCP Protocol Handler (JSON-RPC 2.0 over stdio) ---
|
|
1878
1934
|
|
|
1879
1935
|
function createResponse(id, result) {
|
|
@@ -1999,9 +2055,74 @@ function handleMessage(msg) {
|
|
|
1999
2055
|
lenses: Array.isArray(a.lenses) && a.lenses.length > 0 ? a.lenses : undefined,
|
|
2000
2056
|
dispatch: defaultConvergeDispatch,
|
|
2001
2057
|
projectRoot: process.cwd(),
|
|
2058
|
+
// v1.5.1 R4-H4 — opt-in consensus auto-fix (T27). Threaded
|
|
2059
|
+
// from the tool schema so the code-fixer can genuinely fire;
|
|
2060
|
+
// default false so the audit stays non-mutating unless the
|
|
2061
|
+
// caller explicitly asks for it.
|
|
2062
|
+
autoFix: a.autoFix === true,
|
|
2002
2063
|
});
|
|
2003
2064
|
const isErr = r.verdict === 'consensus_failed' || r.verdict === 'FAIL' || r.verdict === 'UNREACHABLE';
|
|
2004
|
-
|
|
2065
|
+
// v1.5.1 W2.D — emit a canonical gate-result block through the
|
|
2066
|
+
// gate-result-formatter so this Trident-as-a-service surface is
|
|
2067
|
+
// consistent with the Trident gate (dispatch.js) and preflight
|
|
2068
|
+
// gates. appendGateResult guarantees the fenced block is the
|
|
2069
|
+
// LAST content emitted and is idempotent. Failure to format the
|
|
2070
|
+
// block must NOT clobber the verdict payload — observability,
|
|
2071
|
+
// not correctness.
|
|
2072
|
+
let convergeText = JSON.stringify(r, null, 2);
|
|
2073
|
+
try {
|
|
2074
|
+
const { emitGateResult } = await import('./gate-result.js');
|
|
2075
|
+
const { appendGateResult } = await import('./gate-result-formatter.js');
|
|
2076
|
+
// Map the converge verdict onto a schema-valid gate status.
|
|
2077
|
+
const VERDICT_TO_STATUS = {
|
|
2078
|
+
PASS: 'PASS',
|
|
2079
|
+
CONDITIONAL: 'CONDITIONAL',
|
|
2080
|
+
WARN: 'WARN',
|
|
2081
|
+
FLAG: 'FLAG',
|
|
2082
|
+
FAIL: 'FAIL',
|
|
2083
|
+
consensus_failed: 'FAIL',
|
|
2084
|
+
UNREACHABLE: 'FAIL',
|
|
2085
|
+
INCONCLUSIVE: 'FLAG',
|
|
2086
|
+
};
|
|
2087
|
+
const gateStatus = VERDICT_TO_STATUS[r.verdict] || 'FLAG';
|
|
2088
|
+
const block = await emitGateResult(
|
|
2089
|
+
{
|
|
2090
|
+
gate: 'cross-audit',
|
|
2091
|
+
status: gateStatus,
|
|
2092
|
+
lenses: [],
|
|
2093
|
+
affected_artifacts: [],
|
|
2094
|
+
accounting: {
|
|
2095
|
+
duration_ms:
|
|
2096
|
+
typeof r.duration_ms === 'number' ? r.duration_ms : 0,
|
|
2097
|
+
lenses_invoked: Array.isArray(a.lenses)
|
|
2098
|
+
? a.lenses.length
|
|
2099
|
+
: 0,
|
|
2100
|
+
cost_usd: null,
|
|
2101
|
+
},
|
|
2102
|
+
remediation: [],
|
|
2103
|
+
},
|
|
2104
|
+
{ projectRoot: process.cwd() },
|
|
2105
|
+
);
|
|
2106
|
+
// emitGateResult returns the fenced block as a string; the
|
|
2107
|
+
// formatter validates it back into an object before append.
|
|
2108
|
+
const parsed = JSON.parse(
|
|
2109
|
+
block.replace(/^```gate-result\n/, '').replace(/\n```$/, ''),
|
|
2110
|
+
);
|
|
2111
|
+
convergeText = appendGateResult(convergeText, parsed);
|
|
2112
|
+
} catch (gateErr) {
|
|
2113
|
+
try {
|
|
2114
|
+
const msg =
|
|
2115
|
+
gateErr && gateErr.message
|
|
2116
|
+
? gateErr.message
|
|
2117
|
+
: String(gateErr);
|
|
2118
|
+
process.stderr.write(
|
|
2119
|
+
`ijfw: cross_audit_converge gate-result emit failed: ${msg}\n`,
|
|
2120
|
+
);
|
|
2121
|
+
} catch {
|
|
2122
|
+
/* never crash the tool on a logging-channel failure */
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
result = { text: convergeText, isError: isErr };
|
|
2005
2126
|
} catch (err) {
|
|
2006
2127
|
result = { text: JSON.stringify({ error: err && err.message ? err.message : String(err) }), isError: true };
|
|
2007
2128
|
}
|
|
@@ -2038,9 +2159,6 @@ function handleMessage(msg) {
|
|
|
2038
2159
|
result = await handleSearch(searchArgs);
|
|
2039
2160
|
break;
|
|
2040
2161
|
}
|
|
2041
|
-
case 'ijfw_memory_status':
|
|
2042
|
-
result = handleStatus();
|
|
2043
|
-
break;
|
|
2044
2162
|
case 'ijfw_memory_prelude':
|
|
2045
2163
|
result = await handlePrelude(args || {});
|
|
2046
2164
|
break;
|
|
@@ -2153,6 +2271,27 @@ function handleMessage(msg) {
|
|
|
2153
2271
|
}
|
|
2154
2272
|
}
|
|
2155
2273
|
|
|
2274
|
+
// --- B17 WebSocket revocation client (dynamic-import gate) ---
|
|
2275
|
+
// extension-registry-ws.js is dormant by default. Its docstring contract:
|
|
2276
|
+
// "Imported via `await import(...)` ONLY when `process.env.IJFW_REGISTRY_WS_URL`
|
|
2277
|
+
// is set at startup." Firing the gate here at MCP startup keeps the module out
|
|
2278
|
+
// of the import graph entirely unless the operator opts in via the env var, so
|
|
2279
|
+
// MCP startup never opens a socket for the common (unset) case. Best-effort:
|
|
2280
|
+
// a failed WS bind must never block the stdio transport.
|
|
2281
|
+
if (process.env.IJFW_REGISTRY_WS_URL || process.env.IJFW_REGISTRY_WS_SOURCE) {
|
|
2282
|
+
(async () => {
|
|
2283
|
+
try {
|
|
2284
|
+
const { initWsClient } = await import('./extension-registry-ws.js');
|
|
2285
|
+
const res = await initWsClient();
|
|
2286
|
+
if (!res.ok) {
|
|
2287
|
+
process.stderr.write(`IJFW: WS revocation client not started: ${res.error}\n`);
|
|
2288
|
+
}
|
|
2289
|
+
} catch (err) {
|
|
2290
|
+
process.stderr.write(`IJFW: WS revocation client init failed: ${err && err.message ? err.message : err}\n`);
|
|
2291
|
+
}
|
|
2292
|
+
})();
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2156
2295
|
// --- stdio Transport ---
|
|
2157
2296
|
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
2158
2297
|
|
package/src/swarm-config.js
CHANGED
|
@@ -53,20 +53,24 @@ const V150_SPECIALISTS = [
|
|
|
53
53
|
RELEASE_ENG, DOC_WRITER, ACCESSIBILITY_ENG,
|
|
54
54
|
];
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
// the
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
56
|
+
// v1.5.1 W1.5.D — non-software domain specialists, aligned with T26
|
|
57
|
+
// domain-templates (the canonical agent-id spec per ADR
|
|
58
|
+
// .planning/1.5.1/decisions/W1.5-canonical-source.md). Every `agent_type`
|
|
59
|
+
// here MUST resolve to a real `claude/agents/<id>.md` file on disk; the 5
|
|
60
|
+
// phantom constants that previously lived here (STORY_ARCHITECT,
|
|
61
|
+
// CONTINUITY_EDITOR, PROSE_STYLIST, COPY_EDITOR, DATA_ANALYST) were deleted
|
|
62
|
+
// because no markdown shipped for them.
|
|
63
|
+
const NARRATIVE_CONTINUITY_CHECKER = { id: 'narrative-continuity-checker', role: 'Timeline + voice continuity', agent_type: 'ijfw-narrative-continuity-checker', since: '1.5.0' };
|
|
64
|
+
const LINE_EDITOR = { id: 'line-editor', role: 'Sentence-level voice + pacing', agent_type: 'ijfw-line-editor', since: '1.5.0' };
|
|
65
|
+
const LORE_KEEPER = { id: 'lore-keeper', role: 'World/canon consistency', agent_type: 'ijfw-lore-keeper', since: '1.5.0' };
|
|
66
|
+
const CAMPAIGN_STRATEGIST = { id: 'campaign-strategist', role: 'Audience + funnel strategy', agent_type: 'ijfw-campaign-strategist', since: '1.5.0' };
|
|
67
|
+
const COPY_REVIEWER = { id: 'copy-reviewer', role: 'Channel-aware copy editing', agent_type: 'ijfw-copy-reviewer', since: '1.5.0' };
|
|
68
|
+
const DESIGN_CRITIC = { id: 'design-critic', role: 'Usability + design heuristics', agent_type: 'ijfw-design-critic', since: '1.5.0' };
|
|
69
|
+
const ACCESSIBILITY_REVIEWER = { id: 'accessibility-reviewer', role: 'WCAG + a11y audit', agent_type: 'ijfw-accessibility-reviewer', since: '1.5.0' };
|
|
70
|
+
const RESEARCH_LEAD = { id: 'research-lead', role: 'Methodology + literature review', agent_type: 'ijfw-research-lead', since: '1.5.1' };
|
|
71
|
+
const METHOD_REVIEWER = { id: 'method-reviewer', role: 'Method + bias audit', agent_type: 'ijfw-method-reviewer', since: '1.5.1' };
|
|
72
|
+
const STRATEGY_LEAD = { id: 'strategy-lead', role: 'Strategy + decision quality', agent_type: 'ijfw-strategy-lead', since: '1.5.1' };
|
|
73
|
+
const RISK_REVIEWER = { id: 'risk-reviewer', role: 'Feasibility + downside audit', agent_type: 'ijfw-risk-reviewer', since: '1.5.1' };
|
|
70
74
|
|
|
71
75
|
// Per-archetype bench definitions. Keys here track the project_archetypes
|
|
72
76
|
// vocabulary used by team/generator.js so a brief-detected archetype maps
|
|
@@ -76,9 +80,12 @@ const TYPED_BENCH = [...BASE, TESTS_SPECIALIST, TYPES_SPECIALIST, DOC_VERIFIE
|
|
|
76
80
|
const GO_RUST_BENCH = [...BASE, DOC_VERIFIER, PATTERN_MAPPER, SECURITY_AUDITOR, INTEGRATION_CHECKER, NYQUIST_AUDITOR, ...V150_SPECIALISTS];
|
|
77
81
|
const OTHER_BENCH = [...BASE, DOC_VERIFIER, PATTERN_MAPPER, SECURITY_AUDITOR, INTEGRATION_CHECKER, NYQUIST_AUDITOR, ...V150_SPECIALISTS];
|
|
78
82
|
|
|
79
|
-
const BOOK_BENCH
|
|
80
|
-
const CONTENT_BENCH
|
|
81
|
-
const
|
|
83
|
+
const BOOK_BENCH = [NARRATIVE_CONTINUITY_CHECKER, LINE_EDITOR, LORE_KEEPER, DOC_VERIFIER, NYQUIST_AUDITOR];
|
|
84
|
+
const CONTENT_BENCH = [CAMPAIGN_STRATEGIST, COPY_REVIEWER, DOC_VERIFIER, NYQUIST_AUDITOR];
|
|
85
|
+
const DESIGN_BENCH = [DESIGN_CRITIC, ACCESSIBILITY_REVIEWER, DOC_VERIFIER, NYQUIST_AUDITOR];
|
|
86
|
+
const RESEARCH_BENCH = [RESEARCH_LEAD, METHOD_REVIEWER, DOC_VERIFIER, NYQUIST_AUDITOR];
|
|
87
|
+
const BUSINESS_BENCH = [STRATEGY_LEAD, RISK_REVIEWER, DOC_VERIFIER, NYQUIST_AUDITOR];
|
|
88
|
+
const MIXED_BENCH = [...BASE, DOC_VERIFIER, DESIGN_CRITIC, CAMPAIGN_STRATEGIST, NYQUIST_AUDITOR];
|
|
82
89
|
|
|
83
90
|
export const DEFAULT_SPECIALISTS = {
|
|
84
91
|
// Language-keyed defaults (preserved for back-compat with v1.4.x callers
|
|
@@ -89,16 +96,17 @@ export const DEFAULT_SPECIALISTS = {
|
|
|
89
96
|
go: GO_RUST_BENCH,
|
|
90
97
|
rust: GO_RUST_BENCH,
|
|
91
98
|
other: OTHER_BENCH,
|
|
92
|
-
// Archetype-keyed defaults (new in v1.5.0 audit-MED-teams-#6
|
|
93
|
-
//
|
|
99
|
+
// Archetype-keyed defaults (new in v1.5.0 audit-MED-teams-#6; bench
|
|
100
|
+
// mis-mappings fixed in v1.5.1 W1.5.D so every agent_type resolves to a
|
|
101
|
+
// real claude/agents/<id>.md file on disk).
|
|
94
102
|
software: SOFTWARE_BENCH,
|
|
95
103
|
book: BOOK_BENCH,
|
|
96
104
|
content: CONTENT_BENCH,
|
|
97
105
|
marketing: CONTENT_BENCH,
|
|
98
106
|
research: RESEARCH_BENCH,
|
|
99
|
-
design: CONTENT_BENCH
|
|
100
|
-
business: SOFTWARE_BENCH
|
|
101
|
-
mixed: SOFTWARE_BENCH
|
|
107
|
+
design: DESIGN_BENCH, // v1.5.1 W1.5.D: was CONTENT_BENCH (mis-mapped)
|
|
108
|
+
business: BUSINESS_BENCH, // v1.5.1 W1.5.D: was SOFTWARE_BENCH (mis-mapped)
|
|
109
|
+
mixed: MIXED_BENCH, // v1.5.1 W1.5.D: was SOFTWARE_BENCH (mis-mapped)
|
|
102
110
|
};
|
|
103
111
|
|
|
104
112
|
// F-FUN-3: helper -- pick the right bench for a (language, archetype) pair.
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
"domain": "business",
|
|
4
4
|
"display_name": "Business / Strategy",
|
|
5
5
|
"description": "Business plans, strategy memos, GTM plans, investor decks, OKRs, and operational roadmaps.",
|
|
6
|
-
"agent_ids": [
|
|
6
|
+
"agent_ids": [
|
|
7
|
+
"ijfw-strategy-lead",
|
|
8
|
+
"ijfw-risk-reviewer"
|
|
9
|
+
],
|
|
7
10
|
"agent_id_source": "domain-specialist",
|
|
8
11
|
"workflow_phases": ["diagnose", "plan", "decide", "review"],
|
|
9
12
|
"brief_fields": [
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
"domain": "research",
|
|
4
4
|
"display_name": "Research / Analysis",
|
|
5
5
|
"description": "Academic papers, whitepapers, literature reviews, studies, and investigative analysis.",
|
|
6
|
-
"agent_ids": [
|
|
6
|
+
"agent_ids": [
|
|
7
|
+
"ijfw-research-lead",
|
|
8
|
+
"ijfw-method-reviewer"
|
|
9
|
+
],
|
|
7
10
|
"agent_id_source": "domain-specialist",
|
|
8
11
|
"workflow_phases": ["question", "collect", "synthesize", "review"],
|
|
9
12
|
"brief_fields": [
|