@ijfw/memory-server 1.5.0 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/bin/ijfw-memorize +14 -7
  2. package/fixtures/team/book.json +6 -6
  3. package/fixtures/team/business.json +146 -20
  4. package/fixtures/team/content.json +6 -6
  5. package/fixtures/team/design.json +148 -20
  6. package/fixtures/team/mixed.json +206 -27
  7. package/fixtures/team/research.json +146 -20
  8. package/fixtures/team/software.json +148 -20
  9. package/package.json +8 -4
  10. package/src/brain/budget-guard.js +86 -0
  11. package/src/brain/citation-resolver.js +41 -0
  12. package/src/brain/context-injection.js +69 -0
  13. package/src/brain/discovery.js +83 -0
  14. package/src/brain/dream-pipeline.js +324 -0
  15. package/src/brain/dump-ingest.js +88 -0
  16. package/src/brain/entity-collapse.js +28 -0
  17. package/src/brain/export.js +112 -0
  18. package/src/brain/extractors/index.js +24 -0
  19. package/src/brain/extractors/markdown.js +27 -0
  20. package/src/brain/extractors/pdf.js +31 -0
  21. package/src/brain/extractors/transcript.js +38 -0
  22. package/src/brain/first-run-scan.js +61 -0
  23. package/src/brain/index.js +1 -0
  24. package/src/brain/layout-sentinel.js +29 -0
  25. package/src/brain/migrate-facts-internal-once.js +87 -0
  26. package/src/brain/path-guard.js +103 -0
  27. package/src/brain/paths.js +26 -0
  28. package/src/brain/promotion-suggester.js +41 -0
  29. package/src/brain/stub-detector.js +33 -0
  30. package/src/brain/tiered-llm.js +83 -0
  31. package/src/brain/wiki-compiler.js +144 -0
  32. package/src/brain/wiki-sentinels.js +45 -0
  33. package/src/brain/wiki-templates.js +94 -0
  34. package/src/cross-orchestrator-cli.js +336 -150
  35. package/src/cross-orchestrator.js +52 -3
  36. package/src/dashboard-server.js +1 -1
  37. package/src/dispatch/extension.js +1 -1
  38. package/src/dream/runner.mjs +21 -0
  39. package/src/extension-registry.js +2 -2
  40. package/src/handlers/brain-handler.js +319 -0
  41. package/src/hardware-signer.js +4 -2
  42. package/src/lib/ui-review-runner.js +48 -7
  43. package/src/memory/auto-linker.js +121 -2
  44. package/src/memory/benchmark.js +4 -3
  45. package/src/memory/layout-migrations/001-visible-layer.js +131 -0
  46. package/src/memory/layout-migrations/index.js +50 -0
  47. package/src/memory/migration-runner.js +37 -3
  48. package/src/memory/migrations/009-obsidian-backfill.js +50 -0
  49. package/src/memory/obsidian-parser.js +65 -2
  50. package/src/memory/reader.js +2 -1
  51. package/src/memory/search.js +190 -41
  52. package/src/memory/temporal.js +40 -1
  53. package/src/orchestrator/agents-md-blackboard.js +114 -1
  54. package/src/orchestrator/debug-trident-trigger.js +374 -0
  55. package/src/orchestrator/discipline-selector.js +276 -0
  56. package/src/orchestrator/merge-block-aware.js +15 -5
  57. package/src/orchestrator/post-done-runner.js +36 -8
  58. package/src/orchestrator/state-sdk.js +216 -10
  59. package/src/orchestrator/subagent-telemetry.js +19 -0
  60. package/src/orchestrator/wave-state.js +38 -0
  61. package/src/override-resolver.js +5 -3
  62. package/src/recovery/code-fixer.js +311 -6
  63. package/src/runtime-mediator.js +0 -1
  64. package/src/server.js +486 -132
  65. package/src/swarm-config.js +30 -22
  66. package/src/team/domain-templates/business.json +4 -1
  67. package/src/team/domain-templates/research.json +4 -1
  68. package/src/team/generator.js +162 -0
  69. package/src/update-apply.js +1 -1
  70. package/src/dashboard-charts.js +0 -239
  71. package/src/orchestrator/runtime-loop.js +0 -430
@@ -53,20 +53,24 @@ const V150_SPECIALISTS = [
53
53
  RELEASE_ENG, DOC_WRITER, ACCESSIBILITY_ENG,
54
54
  ];
55
55
 
56
- // F-FUN-3 (audit-MED-teams-#6): non-software domain specialists. Mirrors
57
- // the fixture role definitions so we don't duplicate prompts; the IDs here
58
- // are the *bench* names the swarm config tracks. Selecting a non-software
59
- // archetype yields a bench tailored to the domain (book gets story-architect,
60
- // not accessibility-eng). Software remains the default to preserve back-compat.
61
- const STORY_ARCHITECT = { id: 'story-architect', role: 'Plot + structure architecture', agent_type: 'ijfw-story-architect', since: '1.5.0' };
62
- const CONTINUITY_EDITOR = { id: 'continuity-editor', role: 'Timeline + voice continuity', agent_type: 'ijfw-continuity-editor', since: '1.5.0' };
63
- const PROSE_STYLIST = { id: 'prose-stylist', role: 'Sentence-level voice + pacing', agent_type: 'ijfw-prose-stylist', since: '1.5.0' };
64
-
65
- const CAMPAIGN_STRATEGIST = { id: 'campaign-strategist', role: 'Audience + funnel strategy', agent_type: 'ijfw-campaign-strategist', since: '1.5.0' };
66
- const COPY_EDITOR = { id: 'copy-editor', role: 'Channel-aware copy editing', agent_type: 'ijfw-copy-editor', since: '1.5.0' };
67
-
68
- const RESEARCH_LEAD = { id: 'research-lead', role: 'Methodology + literature review', agent_type: 'ijfw-research-lead', since: '1.5.0' };
69
- const DATA_ANALYST = { id: 'data-analyst', role: 'Quantitative analysis', agent_type: 'ijfw-data-analyst', since: '1.5.0' };
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 = [STORY_ARCHITECT, CONTINUITY_EDITOR, PROSE_STYLIST, DOC_VERIFIER, NYQUIST_AUDITOR];
80
- const CONTENT_BENCH = [CAMPAIGN_STRATEGIST, COPY_EDITOR, DOC_VERIFIER, NYQUIST_AUDITOR];
81
- const RESEARCH_BENCH = [RESEARCH_LEAD, DATA_ANALYST, DOC_VERIFIER, NYQUIST_AUDITOR];
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) -- selected
93
- // when the caller hands in a project archetype from the team detector.
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": [
@@ -19,8 +19,20 @@ import {
19
19
  } from './schemas.js';
20
20
 
21
21
  const FIXTURE_DIR = resolve(fileURLToPath(new URL('../../fixtures/team/', import.meta.url)));
22
+ // W1.5.B (ADR W1.5 Option C "merge"): domain-templates is the canonical
23
+ // agent-id spec; fixtures remain canonical for the executable team bundle.
24
+ // `loadTeamTemplate` cross-validates the two sources at load time so any
25
+ // future drift surfaces as a load-time signal rather than a silent
26
+ // runtime mismatch. See `.planning/1.5.1/decisions/W1.5-canonical-source.md`.
27
+ const DOMAIN_TEMPLATES_DIR = resolve(fileURLToPath(new URL('./domain-templates/', import.meta.url)));
22
28
  const SUPPORTED_ARCHETYPES = new Set(['software', 'design', 'content', 'book', 'research', 'business', 'mixed']);
23
29
 
30
+ // W1.5.B: one-time warning suppression per archetype so drift between
31
+ // fixtures and domain-templates emits a single, clearly-attributed
32
+ // warning rather than spamming stderr on every loadTeamTemplate call
33
+ // (tests + the CLI may both call into the loader inside the same process).
34
+ const _crossValidationWarned = new Set();
35
+
24
36
  // T24 / G7-core: the four universal software-core agents. Any software-
25
37
  // domain roster MUST include all four. The ids resolve to static markdown
26
38
  // files under `claude/agents/<id>.md`; the generator does not synthesise
@@ -273,9 +285,121 @@ export function loadTeamTemplate(archetype) {
273
285
  const path = join(FIXTURE_DIR, `${normalized}.json`);
274
286
  const bundle = JSON.parse(readFileSync(path, 'utf8'));
275
287
  assertValidTeamBundle(bundle);
288
+
289
+ // W1.5.B cross-validation gate. Read the matching T26 domain-template
290
+ // (if any) and cross-check that the fixture's `charter.roles[].name`
291
+ // set agrees with the template's `agent_ids`. Two sources of truth
292
+ // that SHOULD agree by construction (ADR W1.5 Option C "merge"); this
293
+ // gate makes future drift self-detect at load time rather than as a
294
+ // silent runtime miss far from the cause.
295
+ //
296
+ // Behaviour:
297
+ // - template file missing
298
+ // → e.g. `mixed` (deliberately template-free per ADR W1.5).
299
+ // Fixture is the sole source of truth; gate is a no-op.
300
+ // - template present but `agent_ids` empty
301
+ // → fall back to fixture as ground truth; emit a one-time
302
+ // warning so the unpopulated template is visible. W1.5.C
303
+ // populated research + business; if any other archetype ever
304
+ // lands empty in future, operators see it immediately.
305
+ // - template populated AND agrees with fixture role names
306
+ // (every fixture role name appears in template `agent_ids`)
307
+ // → silent pass; the canonical contract holds.
308
+ // - template populated AND disagrees
309
+ // → emit a one-time warning naming the drifting role(s) and the
310
+ // template ids they failed to match. Fail-loud at load time
311
+ // via `process.emitWarning` so CI logs and operators see the
312
+ // drift, but DON'T throw during the v1.5.1 transitional
313
+ // window where W1.5.E (fixture rename) and W1.5.B (this gate)
314
+ // ship in separate commits. Once W1.5.E lands and shipped
315
+ // fixtures are realigned, this warning never fires in the
316
+ // normal codepath; downstream may graduate the warning to a
317
+ // thrown error once the steady state is confirmed.
318
+ //
319
+ // The contract for callers: a non-throwing return ALWAYS means the
320
+ // fixture bundle is structurally valid (assertValidTeamBundle above);
321
+ // drift between fixture role names and the domain-template agent_ids
322
+ // surfaces as a one-time `IjfwTeamFixtureDrift` warning on stderr.
323
+ crossValidateAgainstDomainTemplate(normalized, bundle);
324
+
276
325
  return structuredClone(bundle);
277
326
  }
278
327
 
328
+ // W1.5.B: read the T26 domain-template for an archetype (if any) and
329
+ // compare it against the fixture bundle's role names. See
330
+ // loadTeamTemplate above for the contract and ADR W1.5 for the rationale.
331
+ function crossValidateAgainstDomainTemplate(archetype, bundle) {
332
+ const templatePath = join(DOMAIN_TEMPLATES_DIR, `${archetype}.json`);
333
+ if (!existsSync(templatePath)) {
334
+ // No T26 template for this archetype (e.g. `mixed` — deliberately
335
+ // template-free per ADR W1.5). Fixture is the sole source of truth.
336
+ return;
337
+ }
338
+
339
+ let template;
340
+ try {
341
+ template = JSON.parse(readFileSync(templatePath, 'utf8'));
342
+ } catch (err) {
343
+ // Unparseable template should not silently bypass the gate, but a
344
+ // parse error in the template is a separate failure mode from
345
+ // drift; emit a warning so the broken file is visible without
346
+ // breaking the generator for users whose fixture is structurally fine.
347
+ warnOnce(
348
+ `team-generator: domain-template "${archetype}.json" is unreadable (${err.message}); ` +
349
+ 'falling back to fixture as ground truth.',
350
+ `parse:${archetype}`,
351
+ );
352
+ return;
353
+ }
354
+
355
+ const templateIds = Array.isArray(template.agent_ids) ? template.agent_ids : [];
356
+ if (templateIds.length === 0) {
357
+ // ADR W1.5 step 5: T26 empty. Fall back to fixture as ground truth
358
+ // and emit a one-time warning so the gap is visible to operators.
359
+ warnOnce(
360
+ `team-generator: domain-template "${archetype}" has empty agent_ids — ` +
361
+ 'falling back to fixture as ground truth.',
362
+ `empty:${archetype}`,
363
+ );
364
+ return;
365
+ }
366
+
367
+ const fixtureRoleNames = (bundle.charter && Array.isArray(bundle.charter.roles))
368
+ ? bundle.charter.roles.map((role) => role && role.name).filter(Boolean)
369
+ : [];
370
+
371
+ // The contract (ADR W1.5 Option C): every shipped-fixture role.name
372
+ // MUST appear in the template's agent_ids set. Drift is a load-time
373
+ // observability signal — emitted as a one-time warning so CI/operators
374
+ // see it, but NOT thrown during the v1.5.1 transitional window where
375
+ // W1.5.E (fixture rename) and W1.5.B (this gate) ship in separate
376
+ // commits. Steady state post-W1.5.E: this warning never fires; the
377
+ // gate becomes effectively-throw because mismatch can no longer exist.
378
+ const templateSet = new Set(templateIds);
379
+ const missing = fixtureRoleNames.filter((name) => !templateSet.has(name));
380
+ if (missing.length === 0) return;
381
+
382
+ warnOnce(
383
+ `team-generator: fixtures/team/${archetype}.json drifted from ` +
384
+ `domain-templates/${archetype}.json — role name(s) ${missing.map((m) => `"${m}"`).join(', ')} ` +
385
+ `not present in template agent_ids [${templateIds.join(', ')}]. ` +
386
+ 'See ADR .planning/1.5.1/decisions/W1.5-canonical-source.md (Option C "merge"): ' +
387
+ 'fixture roles MUST be a subset of the domain-template agent_ids. ' +
388
+ 'W1.5.E (fixture rename) will close any remaining drift; until then ' +
389
+ 'the fixture is treated as ground truth for the executable team bundle.',
390
+ `drift:${archetype}`,
391
+ );
392
+ }
393
+
394
+ function warnOnce(message, key) {
395
+ if (_crossValidationWarned.has(key)) return;
396
+ _crossValidationWarned.add(key);
397
+ // process.emitWarning is the Node-canonical channel for non-fatal
398
+ // load-time signals — visible to CI logs, suppressible via
399
+ // --no-warnings, and includes a stack so the source is traceable.
400
+ process.emitWarning(message, { type: 'IjfwTeamFixtureDrift', code: 'IJFW_W1_5_B_DRIFT' });
401
+ }
402
+
279
403
  export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
280
404
  const root = resolve(projectRoot);
281
405
  // F-FUN-1: `options.brief` flows through to detectTeamArchetype so a
@@ -306,10 +430,12 @@ export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
306
430
  writeAtomic(workflowPath, `${JSON.stringify(bundle.workflow, null, 2)}\n`, { mode: 0o600 });
307
431
 
308
432
  const agentFiles = [];
433
+ const writtenAgentNames = new Set();
309
434
  for (const role of bundle.charter.roles) {
310
435
  const agentPath = join(agentsDir, `${role.name}.md`);
311
436
  writeAtomic(agentPath, renderAgent(role, bundle), { mode: 0o600 });
312
437
  agentFiles.push(agentPath);
438
+ writtenAgentNames.add(role.name);
313
439
  }
314
440
  const codexAgents = syncCodexAgents(root, { bundle });
315
441
 
@@ -328,6 +454,30 @@ export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
328
454
  const domainSpecialistAgentIds = resolveDomainSpecialistAgentIds(archetype);
329
455
  const rosterAgentIds = resolveRosterForDomain(archetype);
330
456
 
457
+ // W1.5.B: wire DOMAIN_SPECIALIST_AGENT_IDS through to file creation.
458
+ // Previously the canonical specialist ids were surfaced in the return
459
+ // value (`domainSpecialistAgentIds`, `rosterAgentIds`) but no matching
460
+ // `.md` files were written into `.ijfw/agents/` — so a swarm
461
+ // dispatcher resolving "the book domain specialists" by id got a list
462
+ // pointing at non-existent local files. This loop closes that gap by
463
+ // emitting a stub agent .md for every canonical specialist id that
464
+ // the fixture role-write loop above did NOT already cover. When
465
+ // fixture role names already match the canonical ids (the steady
466
+ // state post-W1.5.E), this loop is a no-op because `writtenAgentNames`
467
+ // already contains them.
468
+ //
469
+ // The stub references the canonical `claude/agents/<id>.md` so a
470
+ // swarm dispatcher resolving by id still gets a discoverable local
471
+ // entry; the installer is responsible for materialising the full
472
+ // agent spec.
473
+ for (const specialistId of domainSpecialistAgentIds) {
474
+ if (writtenAgentNames.has(specialistId)) continue;
475
+ const agentPath = join(agentsDir, `${specialistId}.md`);
476
+ writeAtomic(agentPath, renderSpecialistStub(specialistId, archetype, bundle), { mode: 0o600 });
477
+ agentFiles.push(agentPath);
478
+ writtenAgentNames.add(specialistId);
479
+ }
480
+
331
481
  return {
332
482
  ok: true,
333
483
  archetype,
@@ -383,6 +533,18 @@ function normalizeArchetype(value) {
383
533
  return 'mixed';
384
534
  }
385
535
 
536
+ // W1.5.B: stub renderer for canonical domain-specialist ids that the
537
+ // shipped fixture doesn't yet name as a `charter.roles[].name`. Keeps
538
+ // the `.ijfw/agents/<id>.md` directory in agreement with
539
+ // `domainSpecialistAgentIds` so downstream swarm dispatchers resolving
540
+ // by id always find a local file. The stub is intentionally minimal —
541
+ // the full agent spec lives in `claude/agents/<id>.md` and is deployed
542
+ // by the installer; this is a discovery breadcrumb, not a duplicated spec.
543
+ function renderSpecialistStub(specialistId, archetype, bundle) {
544
+ const archetypes = bundle.charter.project_archetypes.join(', ');
545
+ return `---\nname: ${specialistId}\nmodel: sonnet\neffort: medium\ndescription: ${specialistId} — canonical ${archetype} domain specialist (T26 domain-template).\nallowed-tools: Read, Write, Edit, Bash\n---\n\n# ${specialistId}\n\nCanonical domain specialist for ${archetypes} projects.\n\nFull agent specification: claude/agents/${specialistId}.md (deployed by the IJFW installer).\n\nThis stub exists so swarm dispatchers resolving by id find a local entry; the canonical spec is the source of truth.\n\nRecord claims, findings, blockers, and decisions in .ijfw/blackboard/ when swarm execution is active.\n`;
546
+ }
547
+
386
548
  function renderAgent(role, bundle) {
387
549
  const owned = role.owns.map((item) => `- ${item.artifact_type}: ${(item.paths || item.refs || []).join(', ')}`).join('\n');
388
550
  const reviews = role.reviews.map((item) => `- ${item.artifact_type}: ${item.criteria.join(', ')}`).join('\n') || '- None';
@@ -7,7 +7,7 @@
7
7
  // terminal CLI does not require the sentinel to confirm — the token itself is
8
8
  // authoritative. The tool is retained for v1.5.0 back-compat (older skills that
9
9
  // still call it work unchanged) and slated for retirement in v1.6.0 to free the
10
- // MCP-tool slot (see CLAUDE.md "MCP server: ≤12 tools" cap).
10
+ // MCP-tool slot (see CLAUDE.md "MCP server: ≤14 tools" cap).
11
11
  //
12
12
  // Does NOT execute the update. Validates the token, writes (or overwrites)
13
13
  // the pending sentinel, returns instruction telling the user to run the
@@ -1,239 +0,0 @@
1
- /**
2
- * dashboard-charts.js — IJFW v1.4.3 W9-C (B19)
3
- *
4
- * Pure-JS canvas/DOM chart helpers for the dashboard. No external libs.
5
- * Theme-aware: reads CSS custom properties `--ijfw-chart-fg`,
6
- * `--ijfw-chart-bg`, `--ijfw-chart-warning` from the element's computed
7
- * style. Falls back to sane defaults if the host page hasn't set them.
8
- *
9
- * All helpers are defensive against malformed input so a bad payload from
10
- * the API can never throw inside the dashboard render loop.
11
- */
12
-
13
- const DEFAULTS = {
14
- fg: '#9ad2ff',
15
- bg: 'rgba(154,210,255,0.18)',
16
- warning: '#ff9b3a',
17
- text: '#cfd6dd',
18
- };
19
-
20
- function _readTheme(el) {
21
- // Both `canvas` and plain `div` go through `getComputedStyle`. In the test
22
- // environment the element is a hand-rolled mock that doesn't reach the DOM
23
- // — guard so we don't blow up if `getComputedStyle` is unavailable.
24
- let cs = null;
25
- try {
26
- if (el && typeof globalThis.getComputedStyle === 'function') {
27
- cs = globalThis.getComputedStyle(el);
28
- }
29
- } catch { cs = null; }
30
- const read = (prop, fallback) => {
31
- if (!cs || typeof cs.getPropertyValue !== 'function') return fallback;
32
- const v = cs.getPropertyValue(prop);
33
- return (v && v.trim()) ? v.trim() : fallback;
34
- };
35
- return {
36
- fg: read('--ijfw-chart-fg', DEFAULTS.fg),
37
- bg: read('--ijfw-chart-bg', DEFAULTS.bg),
38
- warning: read('--ijfw-chart-warning', DEFAULTS.warning),
39
- text: read('--ijfw-chart-text', DEFAULTS.text),
40
- };
41
- }
42
-
43
- function _safeNum(v, def = 0) {
44
- return (typeof v === 'number' && Number.isFinite(v)) ? v : def;
45
- }
46
-
47
- /**
48
- * lineChart(canvas, points, opts)
49
- * points: [{ x: number, y: number }, ...] OR [number, ...]
50
- * opts: { xMin?, xMax?, yMax?, color?, fill? }
51
- *
52
- * Empty data renders nothing (clears the canvas) without throwing.
53
- */
54
- export function lineChart(canvas, points, opts = {}) {
55
- if (!canvas || typeof canvas.getContext !== 'function') return;
56
- const ctx = canvas.getContext('2d');
57
- if (!ctx) return;
58
- const W = _safeNum(canvas.width, 200);
59
- const H = _safeNum(canvas.height, 100);
60
- const theme = _readTheme(canvas);
61
- const color = opts.color || theme.fg;
62
- const fill = opts.fill === false ? null : theme.bg;
63
-
64
- try { ctx.clearRect(0, 0, W, H); } catch {}
65
-
66
- const pts = Array.isArray(points) ? points : [];
67
- const norm = pts.map((p, i) => {
68
- if (typeof p === 'number') return { x: i, y: _safeNum(p, 0) };
69
- return { x: _safeNum(p && p.x, i), y: _safeNum(p && p.y, 0) };
70
- }).filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y));
71
-
72
- if (norm.length === 0) return;
73
-
74
- let xMin = _safeNum(opts.xMin, norm[0].x);
75
- let xMax = _safeNum(opts.xMax, norm[norm.length - 1].x);
76
- if (xMax === xMin) xMax = xMin + 1;
77
- const yMax = _safeNum(opts.yMax, Math.max(1, ...norm.map((p) => p.y)));
78
- const yScale = yMax > 0 ? yMax : 1;
79
-
80
- const pad = 4;
81
- const innerW = Math.max(1, W - pad * 2);
82
- const innerH = Math.max(1, H - pad * 2);
83
-
84
- const px = (x) => pad + ((x - xMin) / (xMax - xMin)) * innerW;
85
- const py = (y) => pad + (1 - (Math.max(0, y) / yScale)) * innerH;
86
-
87
- // Filled area
88
- if (fill) {
89
- try {
90
- ctx.beginPath();
91
- ctx.moveTo(px(norm[0].x), H - pad);
92
- for (const p of norm) ctx.lineTo(px(p.x), py(p.y));
93
- ctx.lineTo(px(norm[norm.length - 1].x), H - pad);
94
- ctx.closePath();
95
- ctx.fillStyle = fill;
96
- ctx.fill();
97
- } catch {}
98
- }
99
-
100
- // Line stroke
101
- try {
102
- ctx.beginPath();
103
- ctx.moveTo(px(norm[0].x), py(norm[0].y));
104
- for (let i = 1; i < norm.length; i++) ctx.lineTo(px(norm[i].x), py(norm[i].y));
105
- ctx.strokeStyle = color;
106
- ctx.lineWidth = 1.5;
107
- ctx.stroke();
108
- } catch {}
109
- }
110
-
111
- /**
112
- * barChart(canvas, bars, opts)
113
- * bars: [{ label: string, value: number, color?: string }, ...]
114
- * opts: { horizontal?: boolean, color?: string, maxValue?: number }
115
- *
116
- * Zero-value bars render as empty rails. Negative values are clamped to 0.
117
- */
118
- export function barChart(canvas, bars, opts = {}) {
119
- if (!canvas || typeof canvas.getContext !== 'function') return;
120
- const ctx = canvas.getContext('2d');
121
- if (!ctx) return;
122
- const W = _safeNum(canvas.width, 200);
123
- const H = _safeNum(canvas.height, 100);
124
- const theme = _readTheme(canvas);
125
- const defaultColor = opts.color || theme.fg;
126
- const horizontal = Boolean(opts.horizontal);
127
-
128
- try { ctx.clearRect(0, 0, W, H); } catch {}
129
-
130
- const rows = Array.isArray(bars) ? bars.filter((b) => b && typeof b === 'object') : [];
131
- if (rows.length === 0) return;
132
-
133
- const values = rows.map((b) => Math.max(0, _safeNum(b.value, 0)));
134
- const maxVal = _safeNum(opts.maxValue, Math.max(1, ...values));
135
- const scale = maxVal > 0 ? maxVal : 1;
136
-
137
- const pad = 4;
138
- const labelGutter = horizontal ? 80 : 14;
139
- const innerW = Math.max(1, W - pad * 2 - (horizontal ? labelGutter : 0));
140
- const innerH = Math.max(1, H - pad * 2 - (horizontal ? 0 : labelGutter));
141
- const slot = (horizontal ? innerH : innerW) / rows.length;
142
- const barW = Math.max(1, slot * 0.7);
143
-
144
- for (let i = 0; i < rows.length; i++) {
145
- const b = rows[i];
146
- const v = values[i];
147
- const color = b.color || defaultColor;
148
- if (horizontal) {
149
- const y = pad + i * slot + (slot - barW) / 2;
150
- const len = (v / scale) * innerW;
151
- try {
152
- ctx.fillStyle = theme.bg;
153
- ctx.fillRect(pad + labelGutter, y, innerW, barW);
154
- ctx.fillStyle = color;
155
- ctx.fillRect(pad + labelGutter, y, len, barW);
156
- ctx.fillStyle = theme.text;
157
- ctx.fillText(String(b.label || ''), pad, y + barW * 0.75);
158
- } catch {}
159
- } else {
160
- const x = pad + i * slot + (slot - barW) / 2;
161
- const len = (v / scale) * innerH;
162
- try {
163
- ctx.fillStyle = theme.bg;
164
- ctx.fillRect(x, pad, barW, innerH);
165
- ctx.fillStyle = color;
166
- ctx.fillRect(x, pad + (innerH - len), barW, len);
167
- ctx.fillStyle = theme.text;
168
- ctx.fillText(String(b.label || '').slice(0, 8), x, H - pad);
169
- } catch {}
170
- }
171
- }
172
- }
173
-
174
- /**
175
- * progressBar(div, data)
176
- * data: { current: number, limit: number | null, label?: string, warning?: boolean }
177
- *
178
- * Mutates `div` in place. Renders an "unlimited" placeholder when limit is
179
- * null. Applies a warning class when `warning` is truthy.
180
- */
181
- export function progressBar(div, data = {}) {
182
- if (!div) return;
183
- const theme = _readTheme(div);
184
- const cur = Math.max(0, _safeNum(data.current, 0));
185
- const lim = (data.limit === null || data.limit === undefined) ? null : _safeNum(data.limit, null);
186
- const label = typeof data.label === 'string' ? data.label : '';
187
- const warn = Boolean(data.warning);
188
-
189
- // Clear children.
190
- try {
191
- while (div.firstChild) div.removeChild(div.firstChild);
192
- } catch {
193
- // Some test mocks omit firstChild/removeChild. Skip cleanup.
194
- }
195
-
196
- const setClass = (extra) => {
197
- try {
198
- div.className = ['ijfw-progress', extra, warn ? 'ijfw-progress--warn' : '']
199
- .filter(Boolean).join(' ');
200
- } catch {}
201
- };
202
-
203
- // Helper to create child elements safely.
204
- function appendEl(tag, opts) {
205
- try {
206
- if (typeof div.ownerDocument === 'object' && div.ownerDocument && typeof div.ownerDocument.createElement === 'function') {
207
- const el = div.ownerDocument.createElement(tag);
208
- if (opts && opts.text) el.textContent = opts.text;
209
- if (opts && opts.style) el.setAttribute('style', opts.style);
210
- if (opts && opts.cls) el.className = opts.cls;
211
- if (typeof div.appendChild === 'function') div.appendChild(el);
212
- return el;
213
- }
214
- } catch {}
215
- return null;
216
- }
217
-
218
- if (lim === null) {
219
- setClass('ijfw-progress--unlimited');
220
- appendEl('span', { cls: 'ijfw-progress-label', text: label });
221
- appendEl('span', { cls: 'ijfw-progress-val', text: cur + ' / unlimited' });
222
- return;
223
- }
224
-
225
- setClass('');
226
- const denom = lim > 0 ? lim : 1;
227
- const pct = Math.max(0, Math.min(100, (cur / denom) * 100));
228
- appendEl('span', { cls: 'ijfw-progress-label', text: label });
229
- const rail = appendEl('span', { cls: 'ijfw-progress-rail', style: 'background:' + theme.bg + ';display:inline-block;height:6px;width:120px;border-radius:3px;overflow:hidden;vertical-align:middle;margin:0 6px' });
230
- if (rail) {
231
- try {
232
- const fill = (rail.ownerDocument || div.ownerDocument).createElement('span');
233
- fill.className = 'ijfw-progress-fill';
234
- fill.setAttribute('style', 'display:block;height:100%;width:' + pct.toFixed(1) + '%;background:' + (warn ? theme.warning : theme.fg));
235
- rail.appendChild(fill);
236
- } catch {}
237
- }
238
- appendEl('span', { cls: 'ijfw-progress-val', text: cur + ' / ' + lim });
239
- }