@ijfw/memory-server 1.4.4 → 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.
Files changed (245) 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/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  23. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  24. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  26. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  27. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  28. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  29. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  47. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  48. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  49. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  50. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  52. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  53. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  54. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  68. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  69. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  71. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  72. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  73. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  74. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  92. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  93. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  94. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  95. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  96. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  97. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  98. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  99. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  115. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  116. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  117. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  118. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  119. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  120. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  121. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  122. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  123. package/package.json +6 -3
  124. package/src/active-extension-writer.js +144 -64
  125. package/src/api-client.js +43 -5
  126. package/src/audit-roster.js +80 -5
  127. package/src/blackboard.js +298 -6
  128. package/src/cli-run.js +33 -5
  129. package/src/codex-agents.js +96 -5
  130. package/src/cost/aggregator.js +39 -9
  131. package/src/cost/pricing.js +57 -0
  132. package/src/cost/readers/gemini.js +1 -1
  133. package/src/cross-audit-chunker.js +189 -0
  134. package/src/cross-dispatcher.js +124 -21
  135. package/src/cross-orchestrator-cli.js +754 -159
  136. package/src/cross-orchestrator.js +1065 -17
  137. package/src/cross-project-search.js +195 -9
  138. package/src/dashboard-client-waves.html +304 -0
  139. package/src/dashboard-client.html +5 -1
  140. package/src/dashboard-server.js +73 -0
  141. package/src/deploy-alerts.js +150 -0
  142. package/src/design/iframe-bridge.js +242 -0
  143. package/src/design-companion.js +144 -0
  144. package/src/dispatch/checkpoint-cli.js +97 -0
  145. package/src/dispatch/colon-syntax.js +81 -1
  146. package/src/dispatch/extension.js +26 -2
  147. package/src/dispatch/registry-cli.js +4 -1
  148. package/src/dispatch/wave-cli.js +201 -6
  149. package/src/dispatch/worktree-cli.js +40 -0
  150. package/src/dispatch-planner.js +97 -2
  151. package/src/dream/runner.mjs +47 -11
  152. package/src/dream/stage-runner.js +40 -0
  153. package/src/dream/state-file.js +102 -0
  154. package/src/extension-installer.js +70 -24
  155. package/src/extension-quota-tracker.js +4 -2
  156. package/src/extension-registry.js +289 -35
  157. package/src/feedback-detector.js +26 -0
  158. package/src/fs-lock.js +259 -7
  159. package/src/gate-result.js +95 -1
  160. package/src/hardware-signer.js +4 -2
  161. package/src/hero-line.js +86 -5
  162. package/src/intent-router.js +35 -0
  163. package/src/lib/a11y-contract.js +117 -0
  164. package/src/lib/atomic-io.js +29 -8
  165. package/src/lib/cache-keepalive.js +150 -0
  166. package/src/lib/jsonl-rotation.js +104 -0
  167. package/src/lib/lighthouse-pillar.js +121 -0
  168. package/src/lib/llm-call.js +121 -0
  169. package/src/lib/playwright-baseline.js +205 -0
  170. package/src/lib/rekor-bridge.js +221 -0
  171. package/src/lib/repo-map.js +392 -0
  172. package/src/lib/shasum-verify.js +164 -0
  173. package/src/lib/sketches-gc.js +132 -0
  174. package/src/lib/tmp-suffix.js +62 -0
  175. package/src/lib/ui-review-runner.js +595 -0
  176. package/src/lib/uispec-drift.js +301 -0
  177. package/src/lib/uispec-intake.js +381 -0
  178. package/src/lib/worktree-guards.js +118 -0
  179. package/src/lib/worktree-recovery.js +100 -0
  180. package/src/memory/auto-linker.js +267 -0
  181. package/src/memory/benchmark.js +498 -0
  182. package/src/memory/dedup.js +126 -0
  183. package/src/memory/embedding-cache.js +136 -0
  184. package/src/memory/fact-extractor.js +168 -0
  185. package/src/memory/fts5.js +65 -1
  186. package/src/memory/migration-runner.js +6 -1
  187. package/src/memory/migrations/004-bitemporal.js +91 -0
  188. package/src/memory/migrations/005-vector-cache.js +61 -0
  189. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  190. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  191. package/src/memory/migrations/008-write-provenance.js +41 -0
  192. package/src/memory/migrations/009-obsidian-backfill.js +50 -0
  193. package/src/memory/obsidian-parser.js +152 -0
  194. package/src/memory/query-dataview.js +86 -0
  195. package/src/memory/search.js +46 -15
  196. package/src/memory/temporal.js +529 -0
  197. package/src/memory/tokenize.js +10 -0
  198. package/src/memory-facts-handler.js +37 -0
  199. package/src/memory-feedback.js +260 -2
  200. package/src/model-refresh.js +292 -0
  201. package/src/observability/cost-anomaly.js +166 -0
  202. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  203. package/src/observability/trace-id.js +163 -0
  204. package/src/orchestrator/agents-md-blackboard.js +152 -0
  205. package/src/orchestrator/checkpoint-contract.md +140 -0
  206. package/src/orchestrator/debug-trident-trigger.js +374 -0
  207. package/src/orchestrator/debug-trident.js +570 -0
  208. package/src/orchestrator/merge-block-aware.js +350 -0
  209. package/src/orchestrator/plan-checker.js +475 -0
  210. package/src/orchestrator/post-done-runner.js +277 -0
  211. package/src/orchestrator/review.js +38 -3
  212. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  213. package/src/orchestrator/skill-telemetry.js +37 -0
  214. package/src/orchestrator/state-events.js +459 -0
  215. package/src/orchestrator/state-sdk.js +1932 -0
  216. package/src/orchestrator/status-protocol.js +84 -17
  217. package/src/orchestrator/subagent-telemetry.js +471 -0
  218. package/src/orchestrator/termination.js +160 -0
  219. package/src/orchestrator/verification-gate.js +200 -16
  220. package/src/orchestrator/wave-state.js +332 -23
  221. package/src/orchestrator/worktree-provision.js +77 -0
  222. package/src/override-resolver.js +5 -3
  223. package/src/override-use-registry.js +111 -5
  224. package/src/receipts.js +36 -4
  225. package/src/recovery/checkpoint.js +56 -3
  226. package/src/recovery/code-fixer.js +961 -0
  227. package/src/recovery/truncation.js +317 -0
  228. package/src/redactor.js +75 -6
  229. package/src/runtime-mediator.js +15 -1
  230. package/src/sanitizer.js +10 -0
  231. package/src/search-hybrid.js +139 -0
  232. package/src/server.js +795 -112
  233. package/src/swarm/worktree.js +27 -4
  234. package/src/swarm-config.js +102 -17
  235. package/src/team/domain-templates/book.json +51 -0
  236. package/src/team/domain-templates/business.json +44 -0
  237. package/src/team/domain-templates/content.json +50 -0
  238. package/src/team/domain-templates/design.json +44 -0
  239. package/src/team/domain-templates/research.json +44 -0
  240. package/src/team/domain-templates/software.json +40 -0
  241. package/src/team/generator.js +440 -3
  242. package/src/team/modify.js +203 -0
  243. package/src/team/schemas.js +48 -0
  244. package/src/update-apply.js +19 -3
  245. package/src/dashboard-charts.js +0 -239
@@ -10,15 +10,274 @@ import { fileURLToPath } from 'node:url';
10
10
  import { writeAtomic } from '../lib/atomic-io.js';
11
11
  import { syncCodexAgents } from '../codex-agents.js';
12
12
  import { detect } from '../project-type-detector.js';
13
- import { assertValidTeamBundle, validateTeamCharter, validateWorkflowManifest } from './schemas.js';
13
+ import {
14
+ DOMAIN_SPECIALIST_AGENT_IDS as CANONICAL_DOMAIN_SPECIALIST_AGENT_IDS,
15
+ SOFTWARE_CORE_AGENT_IDS as CANONICAL_SOFTWARE_CORE_AGENT_IDS,
16
+ assertValidTeamBundle,
17
+ validateTeamCharter,
18
+ validateWorkflowManifest,
19
+ } from './schemas.js';
14
20
 
15
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)));
16
28
  const SUPPORTED_ARCHETYPES = new Set(['software', 'design', 'content', 'book', 'research', 'business', 'mixed']);
17
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
+
36
+ // T24 / G7-core: the four universal software-core agents. Any software-
37
+ // domain roster MUST include all four. The ids resolve to static markdown
38
+ // files under `claude/agents/<id>.md`; the generator does not synthesise
39
+ // these — it references them by id and trusts the installer to deploy the
40
+ // markdown files into platform-native agent directories.
41
+ //
42
+ // Single source of truth lives in `./schemas.js` (so downstream validators
43
+ // can reference the canonical list without importing the generator). This
44
+ // re-export keeps the historic generator.js surface intact for callers
45
+ // already wired to `SOFTWARE_CORE_AGENT_IDS`.
46
+ //
47
+ // Static set (order is deterministic for snapshot stability):
48
+ // - ijfw-doc-verifier — factual-claim verification post-doc-gen
49
+ // - ijfw-integration-checker — cross-subagent E2E flow verification
50
+ // - ijfw-nyquist-auditor — coverage-gap closure + skeleton-test proposals
51
+ // - ijfw-code-fixer — atomic per-finding code fixes (G4 fixer)
52
+ export const SOFTWARE_CORE_AGENT_IDS = CANONICAL_SOFTWARE_CORE_AGENT_IDS;
53
+
54
+ // T25 / G7-gen: canonical per-domain specialist agent ids. Re-exported from
55
+ // schemas.js so callers wired to `generator.js` get a stable surface. See
56
+ // schemas.js for the per-archetype contract and the rationale for which
57
+ // archetypes are populated today.
58
+ export const DOMAIN_SPECIALIST_AGENT_IDS = CANONICAL_DOMAIN_SPECIALIST_AGENT_IDS;
59
+
60
+ // T24: archetypes that always include the software-core agent set.
61
+ // Currently only `software`; future domains (`mixed` with software files)
62
+ // may opt in via T25's domain-aware generator.
63
+ const SOFTWARE_CORE_ARCHETYPES = new Set(['software']);
64
+
65
+ // F-FUN-1: alias map -- detector returns language-flavoured labels and
66
+ // project-type-detector emits 'unknown' / unmapped domains. Canonicalize
67
+ // BEFORE the SUPPORTED_ARCHETYPES gate so detector outputs don't collapse
68
+ // to 'mixed' just because the literal string isn't in the supported set.
69
+ const ARCHETYPE_ALIASES = new Map([
70
+ // language-specific aliases that some detector paths surface
71
+ ['typescript', 'software'],
72
+ ['javascript', 'software'],
73
+ ['python', 'software'],
74
+ ['rust', 'software'],
75
+ ['go', 'software'],
76
+ ['java', 'software'],
77
+ ['ruby', 'software'],
78
+ ['php', 'software'],
79
+ ['cpp', 'software'],
80
+ ['c++', 'software'],
81
+ ['csharp', 'software'],
82
+ ['code', 'software'],
83
+ ['app', 'software'],
84
+ ['api', 'software'],
85
+ // domain synonyms surfaced by briefs and detector secondaries
86
+ ['marketing', 'content'],
87
+ ['campaign', 'content'],
88
+ ['launch', 'content'],
89
+ ['blog', 'content'],
90
+ ['copy', 'content'],
91
+ ['ui', 'design'],
92
+ ['ux', 'design'],
93
+ ['novel', 'book'],
94
+ ['story', 'book'],
95
+ ['manuscript', 'book'],
96
+ ['paper', 'research'],
97
+ ['study', 'research'],
98
+ ['thesis', 'research'],
99
+ ['strategy', 'business'],
100
+ ['ops', 'business'],
101
+ ['operations', 'business'],
102
+ ['education', 'mixed'],
103
+ ['unknown', 'mixed'],
104
+ ]);
105
+
106
+ // F-FUN-1: brief keyword maps. Each phrase scores +1 toward the listed
107
+ // archetype on whole-word hit. Word boundaries matter -- "research" matches
108
+ // in "research project" but not "researching" -- so we tokenize the brief
109
+ // before scoring. Mirrors the canonical domain list in team-templates.md.
110
+ const BRIEF_KEYWORDS = {
111
+ software: [
112
+ 'software', 'app', 'application', 'api', 'code', 'codebase', 'service',
113
+ 'webapp', 'backend', 'frontend', 'mobile', 'sdk', 'library', 'cli',
114
+ 'feature', 'endpoint', 'module', 'refactor', 'bugfix', 'plugin',
115
+ 'platform', 'integration', 'pipeline',
116
+ ],
117
+ book: [
118
+ 'book', 'novel', 'novella', 'story', 'chapter', 'chapters', 'manuscript',
119
+ 'memoir', 'fiction', 'nonfiction', 'prose', 'narrative', 'screenplay',
120
+ 'cookbook', 'anthology',
121
+ ],
122
+ content: [
123
+ 'campaign', 'launch', 'marketing', 'content', 'blog', 'article',
124
+ 'newsletter', 'copy', 'copywriting', 'landing', 'social', 'seo',
125
+ 'email', 'post', 'posts', 'announcement', 'press',
126
+ ],
127
+ design: [
128
+ 'design', 'ui', 'ux', 'wireframe', 'mockup', 'figma', 'prototype',
129
+ 'visual', 'brand', 'logo', 'illustration', 'typography', 'palette',
130
+ 'design-system',
131
+ ],
132
+ research: [
133
+ 'research', 'paper', 'study', 'thesis', 'methodology', 'experiment',
134
+ 'literature', 'corpus', 'survey', 'analysis', 'hypothesis', 'findings',
135
+ 'whitepaper',
136
+ ],
137
+ business: [
138
+ 'business', 'strategy', 'operations', 'ops', 'financial', 'finance',
139
+ 'budget', 'forecast', 'plan', 'pitch', 'investor', 'gtm', 'b2b', 'b2c',
140
+ 'revenue', 'roadmap', 'okrs',
141
+ ],
142
+ };
143
+
144
+ // F-FUN-1: brief signal threshold. A single keyword hit isn't enough to
145
+ // override filesystem signals (a software repo whose README says "we plan to
146
+ // research X" should stay software). Two distinct keyword hits OR a strong
147
+ // signal phrase ("write a book", "marketing campaign", "research paper")
148
+ // flips the result. Phrases score double because they're explicit.
149
+ const BRIEF_PHRASES = {
150
+ software: [/\b(build|ship|develop|implement)\s+(?:a|an|the)?\s*(app|application|api|service|feature|module|library)\b/i, /\bsource\s+code\b/i],
151
+ book: [/\bwrite\s+(?:a|an|the)?\s*(book|novel|memoir|story|chapter)\b/i, /\bbook\s+about\b/i],
152
+ content: [/\b(marketing|launch|content|seo|social\s+media)\s+(campaign|strategy|plan|push)\b/i, /\bblog\s+post\b/i],
153
+ design: [/\b(design|brand|ui|ux)\s+(system|kit|guide|language|review)\b/i, /\bwireframe(?:s|d)?\s+(?:the|for|of)\b/i],
154
+ research: [/\bresearch\s+(paper|project|study|report|brief)\b/i, /\bliterature\s+review\b/i],
155
+ business: [/\b(business|strategy|operations)\s+(plan|roadmap|memo)\b/i, /\binvestor\s+(deck|pitch|memo)\b/i],
156
+ };
157
+
158
+ const BRIEF_SCORE_FLIP_THRESHOLD = 2;
159
+
18
160
  export function detectTeamArchetype(projectRoot = process.cwd(), options = {}) {
19
161
  if (options.archetype) return normalizeArchetype(options.archetype);
162
+
163
+ // F-FUN-1: brief-first archetype routing. When the caller hands us a
164
+ // non-empty project brief, score it for explicit domain signals before
165
+ // falling back to filesystem-only detection. An explicit brief domain
166
+ // (e.g. "I'm writing a book about ...") MUST outweigh the filesystem
167
+ // signal -- otherwise book/research/campaign projects collapse to 'mixed'
168
+ // when run from a tmp directory or a freshly cloned repo with no files.
169
+ const brief = typeof options.brief === 'string' ? options.brief : '';
170
+ const briefScores = scoreBrief(brief);
171
+ const briefWinner = topBriefDomain(briefScores);
172
+
20
173
  const detected = detect(projectRoot, { maxFiles: options.maxFiles || 4000, c9Available: false });
21
- return normalizeArchetype(detected.primary_type || detected.type);
174
+ const detectedArchetype = normalizeArchetype(detected.primary_type || detected.type);
175
+
176
+ if (briefWinner) return briefWinner;
177
+ return detectedArchetype;
178
+ }
179
+
180
+ // F-FUN-1: brief scorer. Returns a map of {archetype: score}. Word-boundary
181
+ // match for single tokens, regex match for phrase signals (which count
182
+ // double). The caller picks the winner.
183
+ export function scoreBrief(brief) {
184
+ const scores = { software: 0, book: 0, content: 0, design: 0, research: 0, business: 0 };
185
+ if (!brief || typeof brief !== 'string') return scores;
186
+ const text = brief.toLowerCase();
187
+
188
+ for (const [domain, tokens] of Object.entries(BRIEF_KEYWORDS)) {
189
+ for (const token of tokens) {
190
+ const re = new RegExp(`\\b${escapeRegExp(token)}\\b`, 'i');
191
+ if (re.test(text)) scores[domain] += 1;
192
+ }
193
+ }
194
+
195
+ for (const [domain, patterns] of Object.entries(BRIEF_PHRASES)) {
196
+ for (const re of patterns) {
197
+ if (re.test(text)) scores[domain] += 2;
198
+ }
199
+ }
200
+
201
+ return scores;
202
+ }
203
+
204
+ function topBriefDomain(scores) {
205
+ let best = null;
206
+ let bestScore = 0;
207
+ for (const [domain, score] of Object.entries(scores)) {
208
+ if (score > bestScore) {
209
+ best = domain;
210
+ bestScore = score;
211
+ }
212
+ }
213
+ // Require at least the flip threshold AND a clear margin over runner-up.
214
+ // Without margin, "research the market for an app" ties software vs
215
+ // research and we should fall through to filesystem detection instead.
216
+ if (bestScore < BRIEF_SCORE_FLIP_THRESHOLD) return null;
217
+ const second = Object.entries(scores)
218
+ .filter(([d]) => d !== best)
219
+ .reduce((m, [, s]) => Math.max(m, s), 0);
220
+ if (second >= bestScore) return null;
221
+ return best;
222
+ }
223
+
224
+ function escapeRegExp(s) {
225
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
226
+ }
227
+
228
+ // T24 / G7-core: resolve the static software-core agent set for an
229
+ // archetype. Returns `[]` for non-software archetypes. The returned ids
230
+ // each resolve to `claude/agents/<id>.md` in the IJFW repo — callers can
231
+ // look the files up via the installer's deploy step (the markdown is the
232
+ // agent spec; the generator doesn't render its content, it references
233
+ // it). Deterministic order.
234
+ export function resolveSoftwareCoreAgentIds(archetype) {
235
+ const normalized = normalizeArchetype(archetype);
236
+ if (!SOFTWARE_CORE_ARCHETYPES.has(normalized)) return [];
237
+ // Return a fresh array so callers cannot mutate the frozen source set
238
+ // through the public surface.
239
+ return [...SOFTWARE_CORE_AGENT_IDS];
240
+ }
241
+
242
+ // T25 / G7-gen: resolve the per-domain specialist agent set. Returns the
243
+ // archetype's specialist ids per `DOMAIN_SPECIALIST_AGENT_IDS`, or `[]` if
244
+ // the archetype has no domain specialists yet (e.g. `research`, `business`,
245
+ // `mixed`). For `software` this returns `[]` — the software roster is
246
+ // covered by `resolveSoftwareCoreAgentIds`, not duplicated here.
247
+ //
248
+ // Deterministic order. Returns a fresh array per call.
249
+ export function resolveDomainSpecialistAgentIds(archetype) {
250
+ const normalized = normalizeArchetype(archetype);
251
+ const specialists = DOMAIN_SPECIALIST_AGENT_IDS[normalized];
252
+ if (!Array.isArray(specialists)) return [];
253
+ return [...specialists];
254
+ }
255
+
256
+ // T25 / G7-gen: resolve the FULL roster for an archetype — the union of
257
+ // software-core agents (when applicable) plus domain specialists. This is
258
+ // the single function downstream callers (installers, dashboard tiles,
259
+ // `roster.synthesize` consumers) should reach for when they want the
260
+ // complete set of agents the generator believes a domain needs.
261
+ //
262
+ // Contract:
263
+ // - software archetype → all 4 SOFTWARE_CORE_AGENT_IDS, no specialists
264
+ // - book/content/design → only the domain specialists for that archetype
265
+ // - other archetypes → []
266
+ // - order is deterministic: software-core first, then domain specialists
267
+ // - no duplicates: even if a domain ever overlaps with a core id (it
268
+ // should not, by convention — see schemas.js), the union dedupes.
269
+ //
270
+ // The returned array is fresh per call so callers cannot mutate the
271
+ // canonical sources via the public surface.
272
+ export function resolveRosterForDomain(archetype) {
273
+ const normalized = normalizeArchetype(archetype);
274
+ const core = resolveSoftwareCoreAgentIds(normalized);
275
+ const specialists = resolveDomainSpecialistAgentIds(normalized);
276
+ const merged = [...core];
277
+ for (const id of specialists) {
278
+ if (!merged.includes(id)) merged.push(id);
279
+ }
280
+ return merged;
22
281
  }
23
282
 
24
283
  export function loadTeamTemplate(archetype) {
@@ -26,11 +285,126 @@ export function loadTeamTemplate(archetype) {
26
285
  const path = join(FIXTURE_DIR, `${normalized}.json`);
27
286
  const bundle = JSON.parse(readFileSync(path, 'utf8'));
28
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
+
29
325
  return structuredClone(bundle);
30
326
  }
31
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
+
32
403
  export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
33
404
  const root = resolve(projectRoot);
405
+ // F-FUN-1: `options.brief` flows through to detectTeamArchetype so a
406
+ // CLI / MCP caller can hand in a project brief and get a correct
407
+ // archetype before any filesystem signals exist.
34
408
  const archetype = detectTeamArchetype(root, options);
35
409
  const bundle = loadTeamTemplate(archetype);
36
410
  const teamName = options.teamName || `${basename(root) || archetype}-team`;
@@ -56,13 +430,54 @@ export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
56
430
  writeAtomic(workflowPath, `${JSON.stringify(bundle.workflow, null, 2)}\n`, { mode: 0o600 });
57
431
 
58
432
  const agentFiles = [];
433
+ const writtenAgentNames = new Set();
59
434
  for (const role of bundle.charter.roles) {
60
435
  const agentPath = join(agentsDir, `${role.name}.md`);
61
436
  writeAtomic(agentPath, renderAgent(role, bundle), { mode: 0o600 });
62
437
  agentFiles.push(agentPath);
438
+ writtenAgentNames.add(role.name);
63
439
  }
64
440
  const codexAgents = syncCodexAgents(root, { bundle });
65
441
 
442
+ // T24 / G7-core: software-domain rosters always advertise the static
443
+ // software-core agent set. Non-software archetypes get an empty array
444
+ // (preserves the field on every return shape so callers don't need to
445
+ // null-check). The ids point to `claude/agents/<id>.md` in the IJFW
446
+ // install; the installer is responsible for placing the markdown.
447
+ const softwareCoreAgentIds = resolveSoftwareCoreAgentIds(archetype);
448
+
449
+ // T25 / G7-gen: domain-specific specialist agent ids. Same on-disk
450
+ // contract as the software-core ids — each id resolves to
451
+ // `claude/agents/<id>.md`. T25 returns the ids; T26 lands the matching
452
+ // markdown files. Until T26 ships, downstream installers should treat
453
+ // a missing file as "deploy stub" rather than fail-closed.
454
+ const domainSpecialistAgentIds = resolveDomainSpecialistAgentIds(archetype);
455
+ const rosterAgentIds = resolveRosterForDomain(archetype);
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
+
66
481
  return {
67
482
  ok: true,
68
483
  archetype,
@@ -73,6 +488,9 @@ export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
73
488
  workflowPath,
74
489
  agentFiles,
75
490
  codexAgents,
491
+ softwareCoreAgentIds,
492
+ domainSpecialistAgentIds,
493
+ rosterAgentIds,
76
494
  };
77
495
  }
78
496
 
@@ -105,7 +523,26 @@ export function readTeamAssembly(projectRoot = process.cwd()) {
105
523
 
106
524
  function normalizeArchetype(value) {
107
525
  const archetype = String(value || '').toLowerCase();
108
- return SUPPORTED_ARCHETYPES.has(archetype) ? archetype : 'mixed';
526
+ if (SUPPORTED_ARCHETYPES.has(archetype)) return archetype;
527
+ // F-FUN-1: canonicalize via the alias map BEFORE collapsing to 'mixed'.
528
+ // Without this, the detector's language-flavoured outputs (typescript,
529
+ // javascript, python, ...) and project-type-detector's 'unknown' all
530
+ // collapse to 'mixed', losing the strongest signal we have.
531
+ const aliased = ARCHETYPE_ALIASES.get(archetype);
532
+ if (aliased && SUPPORTED_ARCHETYPES.has(aliased)) return aliased;
533
+ return 'mixed';
534
+ }
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`;
109
546
  }
110
547
 
111
548
  function renderAgent(role, bundle) {