@ijfw/memory-server 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/fixtures/truncation-corpus/_generate-corpus.js +367 -0
  2. package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
  3. package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
  4. package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
  5. package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
  6. package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
  7. package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
  8. package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
  9. package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
  10. package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
  11. package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
  12. package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
  13. package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
  14. package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
  15. package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
  16. package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
  17. package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
  18. package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
  19. package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
  20. package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
  21. package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
  22. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
  23. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
  24. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
  25. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
  26. package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
  27. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
  28. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
  29. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
  30. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
  31. package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
  32. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
  33. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
  34. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
  35. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
  36. package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
  37. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
  38. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
  39. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
  40. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
  41. package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
  42. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
  43. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
  44. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
  45. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
  46. package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
  47. package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
  48. package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
  49. package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
  50. package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
  51. package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
  52. package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
  53. package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
  54. package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  55. package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
  56. package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
  57. package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
  58. package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
  59. package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
  60. package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
  61. package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
  62. package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  63. package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
  64. package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
  65. package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
  66. package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
  67. package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
  68. package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
  69. package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
  70. package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
  71. package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
  72. package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
  73. package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
  74. package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
  75. package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
  76. package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
  77. package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
  78. package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
  79. package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
  80. package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
  81. package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
  82. package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
  83. package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
  84. package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
  85. package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
  86. package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
  87. package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
  88. package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
  89. package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
  90. package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
  91. package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
  92. package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
  93. package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
  94. package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
  95. package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
  96. package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
  97. package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
  98. package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
  99. package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
  100. package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
  101. package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
  102. package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
  103. package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
  104. package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
  105. package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
  106. package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
  107. package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
  108. package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
  109. package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
  110. package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
  111. package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
  112. package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
  113. package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
  114. package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
  115. package/package.json +1 -1
  116. package/src/active-extension-writer.js +144 -64
  117. package/src/api-client.js +43 -5
  118. package/src/audit-roster.js +80 -5
  119. package/src/blackboard.js +298 -6
  120. package/src/cli-run.js +33 -5
  121. package/src/codex-agents.js +96 -5
  122. package/src/cost/aggregator.js +39 -9
  123. package/src/cost/pricing.js +57 -0
  124. package/src/cost/readers/gemini.js +1 -1
  125. package/src/cross-audit-chunker.js +189 -0
  126. package/src/cross-dispatcher.js +124 -21
  127. package/src/cross-orchestrator-cli.js +550 -14
  128. package/src/cross-orchestrator.js +1171 -10
  129. package/src/cross-project-search.js +195 -9
  130. package/src/dashboard-client-planning.html +273 -0
  131. package/src/dashboard-client-waves.html +304 -0
  132. package/src/dashboard-client.html +17 -2
  133. package/src/dashboard-server.js +152 -0
  134. package/src/deploy-alerts.js +150 -0
  135. package/src/design/iframe-bridge.js +242 -0
  136. package/src/design-companion.js +144 -0
  137. package/src/dispatch/checkpoint-cli.js +97 -0
  138. package/src/dispatch/colon-syntax.js +81 -1
  139. package/src/dispatch/extension.js +27 -1
  140. package/src/dispatch/registry-cli.js +4 -1
  141. package/src/dispatch/wave-cli.js +323 -0
  142. package/src/dispatch/worktree-cli.js +40 -0
  143. package/src/dispatch-planner.js +97 -2
  144. package/src/dream/runner.mjs +47 -11
  145. package/src/dream/stage-runner.js +40 -0
  146. package/src/dream/state-file.js +102 -0
  147. package/src/extension-installer.js +70 -24
  148. package/src/extension-quota-tracker.js +4 -2
  149. package/src/extension-registry.js +289 -35
  150. package/src/feedback-detector.js +26 -0
  151. package/src/fs-lock.js +259 -7
  152. package/src/gate-result.js +95 -1
  153. package/src/hero-line.js +86 -5
  154. package/src/intent-router.js +35 -0
  155. package/src/lib/a11y-contract.js +117 -0
  156. package/src/lib/atomic-io.js +29 -8
  157. package/src/lib/cache-keepalive.js +150 -0
  158. package/src/lib/jsonl-rotation.js +104 -0
  159. package/src/lib/lighthouse-pillar.js +121 -0
  160. package/src/lib/llm-call.js +121 -0
  161. package/src/lib/playwright-baseline.js +205 -0
  162. package/src/lib/rekor-bridge.js +221 -0
  163. package/src/lib/repo-map.js +392 -0
  164. package/src/lib/shasum-verify.js +164 -0
  165. package/src/lib/sketches-gc.js +132 -0
  166. package/src/lib/tmp-suffix.js +62 -0
  167. package/src/lib/ui-review-runner.js +554 -0
  168. package/src/lib/uispec-drift.js +301 -0
  169. package/src/lib/uispec-intake.js +381 -0
  170. package/src/lib/worktree-guards.js +118 -0
  171. package/src/lib/worktree-recovery.js +100 -0
  172. package/src/memory/auto-linker.js +152 -0
  173. package/src/memory/benchmark.js +498 -0
  174. package/src/memory/dedup.js +126 -0
  175. package/src/memory/embedding-cache.js +136 -0
  176. package/src/memory/fact-extractor.js +168 -0
  177. package/src/memory/fts5.js +65 -1
  178. package/src/memory/migrations/004-bitemporal.js +91 -0
  179. package/src/memory/migrations/005-vector-cache.js +61 -0
  180. package/src/memory/migrations/006-obsidian-graph.js +46 -0
  181. package/src/memory/migrations/007-skill-telemetry.js +24 -0
  182. package/src/memory/migrations/008-write-provenance.js +41 -0
  183. package/src/memory/obsidian-parser.js +91 -0
  184. package/src/memory/query-dataview.js +86 -0
  185. package/src/memory/search.js +10 -0
  186. package/src/memory/temporal.js +529 -0
  187. package/src/memory/tokenize.js +10 -0
  188. package/src/memory-facts-handler.js +37 -0
  189. package/src/memory-feedback.js +260 -2
  190. package/src/model-refresh.js +292 -0
  191. package/src/observability/cost-anomaly.js +166 -0
  192. package/src/observability/evaluator-checkpoint-contract.js +117 -0
  193. package/src/observability/trace-id.js +163 -0
  194. package/src/orchestrator/agents-md-blackboard.js +152 -0
  195. package/src/orchestrator/checkpoint-contract.md +140 -0
  196. package/src/orchestrator/debug-trident.js +570 -0
  197. package/src/orchestrator/merge-block-aware.js +350 -0
  198. package/src/orchestrator/plan-checker.js +475 -0
  199. package/src/orchestrator/post-done-runner.js +249 -0
  200. package/src/orchestrator/review.js +136 -0
  201. package/src/orchestrator/runtime-loop.js +430 -0
  202. package/src/orchestrator/skill-telemetry-sink.js +29 -0
  203. package/src/orchestrator/skill-telemetry.js +37 -0
  204. package/src/orchestrator/state-events.js +459 -0
  205. package/src/orchestrator/state-sdk.js +1764 -0
  206. package/src/orchestrator/status-protocol.js +235 -0
  207. package/src/orchestrator/subagent-telemetry.js +452 -0
  208. package/src/orchestrator/termination.js +160 -0
  209. package/src/orchestrator/verification-gate.js +281 -0
  210. package/src/orchestrator/wave-state.js +564 -0
  211. package/src/orchestrator/worktree-provision.js +77 -0
  212. package/src/override-use-registry.js +111 -5
  213. package/src/receipts.js +36 -4
  214. package/src/recovery/checkpoint.js +56 -3
  215. package/src/recovery/code-fixer.js +656 -0
  216. package/src/recovery/truncation.js +317 -0
  217. package/src/redactor.js +75 -6
  218. package/src/runtime-mediator.js +15 -0
  219. package/src/sanitizer.js +10 -0
  220. package/src/search-hybrid.js +139 -0
  221. package/src/server.js +603 -59
  222. package/src/swarm/worktree.js +27 -4
  223. package/src/swarm-config.js +113 -12
  224. package/src/team/domain-templates/book.json +51 -0
  225. package/src/team/domain-templates/business.json +41 -0
  226. package/src/team/domain-templates/content.json +50 -0
  227. package/src/team/domain-templates/design.json +44 -0
  228. package/src/team/domain-templates/research.json +41 -0
  229. package/src/team/domain-templates/software.json +40 -0
  230. package/src/team/generator.js +278 -3
  231. package/src/team/modify.js +203 -0
  232. package/src/team/schemas.js +48 -0
  233. package/src/update-apply.js +19 -3
@@ -10,15 +10,262 @@ 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)));
16
22
  const SUPPORTED_ARCHETYPES = new Set(['software', 'design', 'content', 'book', 'research', 'business', 'mixed']);
17
23
 
24
+ // T24 / G7-core: the four universal software-core agents. Any software-
25
+ // domain roster MUST include all four. The ids resolve to static markdown
26
+ // files under `claude/agents/<id>.md`; the generator does not synthesise
27
+ // these — it references them by id and trusts the installer to deploy the
28
+ // markdown files into platform-native agent directories.
29
+ //
30
+ // Single source of truth lives in `./schemas.js` (so downstream validators
31
+ // can reference the canonical list without importing the generator). This
32
+ // re-export keeps the historic generator.js surface intact for callers
33
+ // already wired to `SOFTWARE_CORE_AGENT_IDS`.
34
+ //
35
+ // Static set (order is deterministic for snapshot stability):
36
+ // - ijfw-doc-verifier — factual-claim verification post-doc-gen
37
+ // - ijfw-integration-checker — cross-subagent E2E flow verification
38
+ // - ijfw-nyquist-auditor — coverage-gap closure + skeleton-test proposals
39
+ // - ijfw-code-fixer — atomic per-finding code fixes (G4 fixer)
40
+ export const SOFTWARE_CORE_AGENT_IDS = CANONICAL_SOFTWARE_CORE_AGENT_IDS;
41
+
42
+ // T25 / G7-gen: canonical per-domain specialist agent ids. Re-exported from
43
+ // schemas.js so callers wired to `generator.js` get a stable surface. See
44
+ // schemas.js for the per-archetype contract and the rationale for which
45
+ // archetypes are populated today.
46
+ export const DOMAIN_SPECIALIST_AGENT_IDS = CANONICAL_DOMAIN_SPECIALIST_AGENT_IDS;
47
+
48
+ // T24: archetypes that always include the software-core agent set.
49
+ // Currently only `software`; future domains (`mixed` with software files)
50
+ // may opt in via T25's domain-aware generator.
51
+ const SOFTWARE_CORE_ARCHETYPES = new Set(['software']);
52
+
53
+ // F-FUN-1: alias map -- detector returns language-flavoured labels and
54
+ // project-type-detector emits 'unknown' / unmapped domains. Canonicalize
55
+ // BEFORE the SUPPORTED_ARCHETYPES gate so detector outputs don't collapse
56
+ // to 'mixed' just because the literal string isn't in the supported set.
57
+ const ARCHETYPE_ALIASES = new Map([
58
+ // language-specific aliases that some detector paths surface
59
+ ['typescript', 'software'],
60
+ ['javascript', 'software'],
61
+ ['python', 'software'],
62
+ ['rust', 'software'],
63
+ ['go', 'software'],
64
+ ['java', 'software'],
65
+ ['ruby', 'software'],
66
+ ['php', 'software'],
67
+ ['cpp', 'software'],
68
+ ['c++', 'software'],
69
+ ['csharp', 'software'],
70
+ ['code', 'software'],
71
+ ['app', 'software'],
72
+ ['api', 'software'],
73
+ // domain synonyms surfaced by briefs and detector secondaries
74
+ ['marketing', 'content'],
75
+ ['campaign', 'content'],
76
+ ['launch', 'content'],
77
+ ['blog', 'content'],
78
+ ['copy', 'content'],
79
+ ['ui', 'design'],
80
+ ['ux', 'design'],
81
+ ['novel', 'book'],
82
+ ['story', 'book'],
83
+ ['manuscript', 'book'],
84
+ ['paper', 'research'],
85
+ ['study', 'research'],
86
+ ['thesis', 'research'],
87
+ ['strategy', 'business'],
88
+ ['ops', 'business'],
89
+ ['operations', 'business'],
90
+ ['education', 'mixed'],
91
+ ['unknown', 'mixed'],
92
+ ]);
93
+
94
+ // F-FUN-1: brief keyword maps. Each phrase scores +1 toward the listed
95
+ // archetype on whole-word hit. Word boundaries matter -- "research" matches
96
+ // in "research project" but not "researching" -- so we tokenize the brief
97
+ // before scoring. Mirrors the canonical domain list in team-templates.md.
98
+ const BRIEF_KEYWORDS = {
99
+ software: [
100
+ 'software', 'app', 'application', 'api', 'code', 'codebase', 'service',
101
+ 'webapp', 'backend', 'frontend', 'mobile', 'sdk', 'library', 'cli',
102
+ 'feature', 'endpoint', 'module', 'refactor', 'bugfix', 'plugin',
103
+ 'platform', 'integration', 'pipeline',
104
+ ],
105
+ book: [
106
+ 'book', 'novel', 'novella', 'story', 'chapter', 'chapters', 'manuscript',
107
+ 'memoir', 'fiction', 'nonfiction', 'prose', 'narrative', 'screenplay',
108
+ 'cookbook', 'anthology',
109
+ ],
110
+ content: [
111
+ 'campaign', 'launch', 'marketing', 'content', 'blog', 'article',
112
+ 'newsletter', 'copy', 'copywriting', 'landing', 'social', 'seo',
113
+ 'email', 'post', 'posts', 'announcement', 'press',
114
+ ],
115
+ design: [
116
+ 'design', 'ui', 'ux', 'wireframe', 'mockup', 'figma', 'prototype',
117
+ 'visual', 'brand', 'logo', 'illustration', 'typography', 'palette',
118
+ 'design-system',
119
+ ],
120
+ research: [
121
+ 'research', 'paper', 'study', 'thesis', 'methodology', 'experiment',
122
+ 'literature', 'corpus', 'survey', 'analysis', 'hypothesis', 'findings',
123
+ 'whitepaper',
124
+ ],
125
+ business: [
126
+ 'business', 'strategy', 'operations', 'ops', 'financial', 'finance',
127
+ 'budget', 'forecast', 'plan', 'pitch', 'investor', 'gtm', 'b2b', 'b2c',
128
+ 'revenue', 'roadmap', 'okrs',
129
+ ],
130
+ };
131
+
132
+ // F-FUN-1: brief signal threshold. A single keyword hit isn't enough to
133
+ // override filesystem signals (a software repo whose README says "we plan to
134
+ // research X" should stay software). Two distinct keyword hits OR a strong
135
+ // signal phrase ("write a book", "marketing campaign", "research paper")
136
+ // flips the result. Phrases score double because they're explicit.
137
+ const BRIEF_PHRASES = {
138
+ 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],
139
+ book: [/\bwrite\s+(?:a|an|the)?\s*(book|novel|memoir|story|chapter)\b/i, /\bbook\s+about\b/i],
140
+ content: [/\b(marketing|launch|content|seo|social\s+media)\s+(campaign|strategy|plan|push)\b/i, /\bblog\s+post\b/i],
141
+ design: [/\b(design|brand|ui|ux)\s+(system|kit|guide|language|review)\b/i, /\bwireframe(?:s|d)?\s+(?:the|for|of)\b/i],
142
+ research: [/\bresearch\s+(paper|project|study|report|brief)\b/i, /\bliterature\s+review\b/i],
143
+ business: [/\b(business|strategy|operations)\s+(plan|roadmap|memo)\b/i, /\binvestor\s+(deck|pitch|memo)\b/i],
144
+ };
145
+
146
+ const BRIEF_SCORE_FLIP_THRESHOLD = 2;
147
+
18
148
  export function detectTeamArchetype(projectRoot = process.cwd(), options = {}) {
19
149
  if (options.archetype) return normalizeArchetype(options.archetype);
150
+
151
+ // F-FUN-1: brief-first archetype routing. When the caller hands us a
152
+ // non-empty project brief, score it for explicit domain signals before
153
+ // falling back to filesystem-only detection. An explicit brief domain
154
+ // (e.g. "I'm writing a book about ...") MUST outweigh the filesystem
155
+ // signal -- otherwise book/research/campaign projects collapse to 'mixed'
156
+ // when run from a tmp directory or a freshly cloned repo with no files.
157
+ const brief = typeof options.brief === 'string' ? options.brief : '';
158
+ const briefScores = scoreBrief(brief);
159
+ const briefWinner = topBriefDomain(briefScores);
160
+
20
161
  const detected = detect(projectRoot, { maxFiles: options.maxFiles || 4000, c9Available: false });
21
- return normalizeArchetype(detected.primary_type || detected.type);
162
+ const detectedArchetype = normalizeArchetype(detected.primary_type || detected.type);
163
+
164
+ if (briefWinner) return briefWinner;
165
+ return detectedArchetype;
166
+ }
167
+
168
+ // F-FUN-1: brief scorer. Returns a map of {archetype: score}. Word-boundary
169
+ // match for single tokens, regex match for phrase signals (which count
170
+ // double). The caller picks the winner.
171
+ export function scoreBrief(brief) {
172
+ const scores = { software: 0, book: 0, content: 0, design: 0, research: 0, business: 0 };
173
+ if (!brief || typeof brief !== 'string') return scores;
174
+ const text = brief.toLowerCase();
175
+
176
+ for (const [domain, tokens] of Object.entries(BRIEF_KEYWORDS)) {
177
+ for (const token of tokens) {
178
+ const re = new RegExp(`\\b${escapeRegExp(token)}\\b`, 'i');
179
+ if (re.test(text)) scores[domain] += 1;
180
+ }
181
+ }
182
+
183
+ for (const [domain, patterns] of Object.entries(BRIEF_PHRASES)) {
184
+ for (const re of patterns) {
185
+ if (re.test(text)) scores[domain] += 2;
186
+ }
187
+ }
188
+
189
+ return scores;
190
+ }
191
+
192
+ function topBriefDomain(scores) {
193
+ let best = null;
194
+ let bestScore = 0;
195
+ for (const [domain, score] of Object.entries(scores)) {
196
+ if (score > bestScore) {
197
+ best = domain;
198
+ bestScore = score;
199
+ }
200
+ }
201
+ // Require at least the flip threshold AND a clear margin over runner-up.
202
+ // Without margin, "research the market for an app" ties software vs
203
+ // research and we should fall through to filesystem detection instead.
204
+ if (bestScore < BRIEF_SCORE_FLIP_THRESHOLD) return null;
205
+ const second = Object.entries(scores)
206
+ .filter(([d]) => d !== best)
207
+ .reduce((m, [, s]) => Math.max(m, s), 0);
208
+ if (second >= bestScore) return null;
209
+ return best;
210
+ }
211
+
212
+ function escapeRegExp(s) {
213
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
214
+ }
215
+
216
+ // T24 / G7-core: resolve the static software-core agent set for an
217
+ // archetype. Returns `[]` for non-software archetypes. The returned ids
218
+ // each resolve to `claude/agents/<id>.md` in the IJFW repo — callers can
219
+ // look the files up via the installer's deploy step (the markdown is the
220
+ // agent spec; the generator doesn't render its content, it references
221
+ // it). Deterministic order.
222
+ export function resolveSoftwareCoreAgentIds(archetype) {
223
+ const normalized = normalizeArchetype(archetype);
224
+ if (!SOFTWARE_CORE_ARCHETYPES.has(normalized)) return [];
225
+ // Return a fresh array so callers cannot mutate the frozen source set
226
+ // through the public surface.
227
+ return [...SOFTWARE_CORE_AGENT_IDS];
228
+ }
229
+
230
+ // T25 / G7-gen: resolve the per-domain specialist agent set. Returns the
231
+ // archetype's specialist ids per `DOMAIN_SPECIALIST_AGENT_IDS`, or `[]` if
232
+ // the archetype has no domain specialists yet (e.g. `research`, `business`,
233
+ // `mixed`). For `software` this returns `[]` — the software roster is
234
+ // covered by `resolveSoftwareCoreAgentIds`, not duplicated here.
235
+ //
236
+ // Deterministic order. Returns a fresh array per call.
237
+ export function resolveDomainSpecialistAgentIds(archetype) {
238
+ const normalized = normalizeArchetype(archetype);
239
+ const specialists = DOMAIN_SPECIALIST_AGENT_IDS[normalized];
240
+ if (!Array.isArray(specialists)) return [];
241
+ return [...specialists];
242
+ }
243
+
244
+ // T25 / G7-gen: resolve the FULL roster for an archetype — the union of
245
+ // software-core agents (when applicable) plus domain specialists. This is
246
+ // the single function downstream callers (installers, dashboard tiles,
247
+ // `roster.synthesize` consumers) should reach for when they want the
248
+ // complete set of agents the generator believes a domain needs.
249
+ //
250
+ // Contract:
251
+ // - software archetype → all 4 SOFTWARE_CORE_AGENT_IDS, no specialists
252
+ // - book/content/design → only the domain specialists for that archetype
253
+ // - other archetypes → []
254
+ // - order is deterministic: software-core first, then domain specialists
255
+ // - no duplicates: even if a domain ever overlaps with a core id (it
256
+ // should not, by convention — see schemas.js), the union dedupes.
257
+ //
258
+ // The returned array is fresh per call so callers cannot mutate the
259
+ // canonical sources via the public surface.
260
+ export function resolveRosterForDomain(archetype) {
261
+ const normalized = normalizeArchetype(archetype);
262
+ const core = resolveSoftwareCoreAgentIds(normalized);
263
+ const specialists = resolveDomainSpecialistAgentIds(normalized);
264
+ const merged = [...core];
265
+ for (const id of specialists) {
266
+ if (!merged.includes(id)) merged.push(id);
267
+ }
268
+ return merged;
22
269
  }
23
270
 
24
271
  export function loadTeamTemplate(archetype) {
@@ -31,6 +278,9 @@ export function loadTeamTemplate(archetype) {
31
278
 
32
279
  export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
33
280
  const root = resolve(projectRoot);
281
+ // F-FUN-1: `options.brief` flows through to detectTeamArchetype so a
282
+ // CLI / MCP caller can hand in a project brief and get a correct
283
+ // archetype before any filesystem signals exist.
34
284
  const archetype = detectTeamArchetype(root, options);
35
285
  const bundle = loadTeamTemplate(archetype);
36
286
  const teamName = options.teamName || `${basename(root) || archetype}-team`;
@@ -63,6 +313,21 @@ export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
63
313
  }
64
314
  const codexAgents = syncCodexAgents(root, { bundle });
65
315
 
316
+ // T24 / G7-core: software-domain rosters always advertise the static
317
+ // software-core agent set. Non-software archetypes get an empty array
318
+ // (preserves the field on every return shape so callers don't need to
319
+ // null-check). The ids point to `claude/agents/<id>.md` in the IJFW
320
+ // install; the installer is responsible for placing the markdown.
321
+ const softwareCoreAgentIds = resolveSoftwareCoreAgentIds(archetype);
322
+
323
+ // T25 / G7-gen: domain-specific specialist agent ids. Same on-disk
324
+ // contract as the software-core ids — each id resolves to
325
+ // `claude/agents/<id>.md`. T25 returns the ids; T26 lands the matching
326
+ // markdown files. Until T26 ships, downstream installers should treat
327
+ // a missing file as "deploy stub" rather than fail-closed.
328
+ const domainSpecialistAgentIds = resolveDomainSpecialistAgentIds(archetype);
329
+ const rosterAgentIds = resolveRosterForDomain(archetype);
330
+
66
331
  return {
67
332
  ok: true,
68
333
  archetype,
@@ -73,6 +338,9 @@ export function createTeamAssembly(projectRoot = process.cwd(), options = {}) {
73
338
  workflowPath,
74
339
  agentFiles,
75
340
  codexAgents,
341
+ softwareCoreAgentIds,
342
+ domainSpecialistAgentIds,
343
+ rosterAgentIds,
76
344
  };
77
345
  }
78
346
 
@@ -105,7 +373,14 @@ export function readTeamAssembly(projectRoot = process.cwd()) {
105
373
 
106
374
  function normalizeArchetype(value) {
107
375
  const archetype = String(value || '').toLowerCase();
108
- return SUPPORTED_ARCHETYPES.has(archetype) ? archetype : 'mixed';
376
+ if (SUPPORTED_ARCHETYPES.has(archetype)) return archetype;
377
+ // F-FUN-1: canonicalize via the alias map BEFORE collapsing to 'mixed'.
378
+ // Without this, the detector's language-flavoured outputs (typescript,
379
+ // javascript, python, ...) and project-type-detector's 'unknown' all
380
+ // collapse to 'mixed', losing the strongest signal we have.
381
+ const aliased = ARCHETYPE_ALIASES.get(archetype);
382
+ if (aliased && SUPPORTED_ARCHETYPES.has(aliased)) return aliased;
383
+ return 'mixed';
109
384
  }
110
385
 
111
386
  function renderAgent(role, bundle) {
@@ -0,0 +1,203 @@
1
+ // F-FUN-4 + F-FUN-5 (audit-MED-teams-#7 + #13): team-charter mutation
2
+ // helpers. Honors the SKILL.md "Custom Agent Requests" promise -- users can
3
+ // add, remove, swap, list, and check roles on their custom team without
4
+ // hand-editing JSON.
5
+ //
6
+ // Every mutating helper:
7
+ // 1. Reads the current charter/workflow.
8
+ // 2. Applies the mutation in-memory.
9
+ // 3. Validates the resulting charter via team/schemas.js.
10
+ // 4. Writes the charter back atomically.
11
+ // 5. Re-runs syncCodexAgents so .codex/agents/ stays in lockstep.
12
+ //
13
+ // `checkTeamAssembly` is the standalone validator surfaced by
14
+ // `ijfw team check` -- it does not write anything and does not require
15
+ // `ijfw swarm plan` to surface error messages.
16
+
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { join, resolve } from 'node:path';
19
+ import { writeAtomic } from '../lib/atomic-io.js';
20
+ import { syncCodexAgents } from '../codex-agents.js';
21
+ import { readTeamAssembly } from './generator.js';
22
+ import { validateTeamCharter, validateWorkflowManifest } from './schemas.js';
23
+
24
+ function teamPaths(root) {
25
+ return {
26
+ charterPath: join(root, '.ijfw', 'team', 'charter.json'),
27
+ workflowPath: join(root, '.ijfw', 'team', 'workflow.json'),
28
+ };
29
+ }
30
+
31
+ function loadCharter(root) {
32
+ const { charterPath } = teamPaths(root);
33
+ if (!existsSync(charterPath)) return null;
34
+ return JSON.parse(readFileSync(charterPath, 'utf8'));
35
+ }
36
+
37
+ function loadWorkflow(root) {
38
+ const { workflowPath } = teamPaths(root);
39
+ if (!existsSync(workflowPath)) return null;
40
+ return JSON.parse(readFileSync(workflowPath, 'utf8'));
41
+ }
42
+
43
+ function writeCharter(root, charter) {
44
+ const { charterPath } = teamPaths(root);
45
+ writeAtomic(charterPath, `${JSON.stringify(charter, null, 2)}\n`, { mode: 0o600 });
46
+ return charterPath;
47
+ }
48
+
49
+ /**
50
+ * List the current roles + summary metadata for the team. Returns a
51
+ * structured payload so callers (CLI + future MCP tool) can render however
52
+ * they like.
53
+ */
54
+ export function listTeamRoles(projectRoot = process.cwd()) {
55
+ const root = resolve(projectRoot);
56
+ const charter = loadCharter(root);
57
+ if (!charter) return { ok: false, error: 'missing-charter' };
58
+ const roles = Array.isArray(charter.roles) ? charter.roles : [];
59
+ return {
60
+ ok: true,
61
+ team_name: charter.team_name || null,
62
+ project_archetypes: charter.project_archetypes || [],
63
+ roles: roles.map((r) => ({
64
+ name: r.name,
65
+ role_type: r.role_type,
66
+ model: r.model,
67
+ effort: r.effort || 'medium',
68
+ phase_scope: r.phase_scope || [],
69
+ })),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Add a role to the charter. The charter is supplied as a JSON path; we
75
+ * read+validate it before splicing in. Triggers a codex agent re-sync.
76
+ */
77
+ export function addTeamRole(projectRoot, { charterPath, role }) {
78
+ const root = resolve(projectRoot);
79
+ const charter = loadCharter(root);
80
+ if (!charter) return { ok: false, error: 'missing-charter' };
81
+
82
+ const incoming = role || readJsonFile(charterPath);
83
+ if (!incoming || typeof incoming !== 'object') return { ok: false, error: 'invalid-role-payload' };
84
+ if (!incoming.name) return { ok: false, error: 'role-name-required' };
85
+
86
+ const existing = (charter.roles || []).find((r) => r.name === incoming.name);
87
+ if (existing) return { ok: false, error: 'role-exists', name: incoming.name };
88
+
89
+ const next = { ...charter, roles: [...(charter.roles || []), incoming] };
90
+ const validation = validateTeamCharter(next);
91
+ if (!validation.ok) return { ok: false, error: 'invalid-charter', errors: validation.errors };
92
+
93
+ writeCharter(root, next);
94
+ const codex = syncCodexAgents(root);
95
+ return { ok: true, role: incoming, codex };
96
+ }
97
+
98
+ /**
99
+ * Remove a role by name. Refuses if removing the role would leave the
100
+ * charter invalid (e.g. last role).
101
+ */
102
+ export function removeTeamRole(projectRoot, { name }) {
103
+ const root = resolve(projectRoot);
104
+ const charter = loadCharter(root);
105
+ if (!charter) return { ok: false, error: 'missing-charter' };
106
+
107
+ const before = (charter.roles || []).length;
108
+ const roles = (charter.roles || []).filter((r) => r.name !== name);
109
+ if (roles.length === before) return { ok: false, error: 'role-not-found', name };
110
+
111
+ const next = { ...charter, roles };
112
+ const validation = validateTeamCharter(next);
113
+ if (!validation.ok) return { ok: false, error: 'invalid-charter', errors: validation.errors };
114
+
115
+ writeCharter(root, next);
116
+ const codex = syncCodexAgents(root);
117
+ return { ok: true, removed: name, codex };
118
+ }
119
+
120
+ /**
121
+ * Replace one role with another. `oldName` must exist; `replacement` is a
122
+ * full role object (or path to a JSON file describing one).
123
+ */
124
+ export function swapTeamRole(projectRoot, { oldName, charterPath, replacement }) {
125
+ const root = resolve(projectRoot);
126
+ const charter = loadCharter(root);
127
+ if (!charter) return { ok: false, error: 'missing-charter' };
128
+
129
+ const incoming = replacement || readJsonFile(charterPath);
130
+ if (!incoming || typeof incoming !== 'object') return { ok: false, error: 'invalid-role-payload' };
131
+ if (!incoming.name) return { ok: false, error: 'role-name-required' };
132
+
133
+ const roles = charter.roles || [];
134
+ const idx = roles.findIndex((r) => r.name === oldName);
135
+ if (idx === -1) return { ok: false, error: 'role-not-found', name: oldName };
136
+
137
+ // Name collision when the replacement keeps a *different* existing name.
138
+ if (incoming.name !== oldName && roles.some((r) => r.name === incoming.name)) {
139
+ return { ok: false, error: 'role-exists', name: incoming.name };
140
+ }
141
+
142
+ const nextRoles = roles.slice();
143
+ nextRoles[idx] = incoming;
144
+ const next = { ...charter, roles: nextRoles };
145
+ const validation = validateTeamCharter(next);
146
+ if (!validation.ok) return { ok: false, error: 'invalid-charter', errors: validation.errors };
147
+
148
+ writeCharter(root, next);
149
+ const codex = syncCodexAgents(root);
150
+ return { ok: true, swapped: { old: oldName, new: incoming.name }, codex };
151
+ }
152
+
153
+ /**
154
+ * F-FUN-5 (audit-MED-teams-#13): standalone team validation. Returns a
155
+ * structured pass/fail report so the CLI can render a human-readable list
156
+ * without forcing the caller into `ijfw swarm plan`.
157
+ */
158
+ export function checkTeamAssembly(projectRoot = process.cwd()) {
159
+ const root = resolve(projectRoot);
160
+ const charter = loadCharter(root);
161
+ const workflow = loadWorkflow(root);
162
+ const report = {
163
+ ok: true,
164
+ has_charter: Boolean(charter),
165
+ has_workflow: Boolean(workflow),
166
+ charter: { ok: true, errors: [] },
167
+ workflow: { ok: true, errors: [] },
168
+ role_count: 0,
169
+ artifact_count: 0,
170
+ };
171
+ if (!charter) {
172
+ report.ok = false;
173
+ report.charter = { ok: false, errors: ['charter.json is missing -- run: ijfw team init'] };
174
+ } else {
175
+ const c = validateTeamCharter(charter);
176
+ report.charter = c;
177
+ report.role_count = Array.isArray(charter.roles) ? charter.roles.length : 0;
178
+ if (!c.ok) report.ok = false;
179
+ }
180
+ if (!workflow) {
181
+ report.ok = false;
182
+ report.workflow = { ok: false, errors: ['workflow.json is missing -- run: ijfw team init'] };
183
+ } else {
184
+ const w = validateWorkflowManifest(workflow, charter);
185
+ report.workflow = w;
186
+ report.artifact_count = Array.isArray(workflow.artifacts) ? workflow.artifacts.length : 0;
187
+ if (!w.ok) report.ok = false;
188
+ }
189
+ return report;
190
+ }
191
+
192
+ function readJsonFile(path) {
193
+ if (!path) return null;
194
+ if (!existsSync(path)) return null;
195
+ try {
196
+ return JSON.parse(readFileSync(path, 'utf8'));
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ // Re-exports for the CLI surface so it can `import * as teamModify`.
203
+ export { readTeamAssembly };
@@ -10,6 +10,54 @@ const ARCHETYPES = new Set([
10
10
  'mixed',
11
11
  ]);
12
12
 
13
+ // T24 / G7-core: canonical software-core agent ids. Mirrored by
14
+ // `SOFTWARE_CORE_AGENT_IDS` in generator.js — this re-export gives
15
+ // schema-side consumers (validators, downstream tools, T25's domain-aware
16
+ // generator) a single source of truth without circular import on
17
+ // generator.js. Any change to the set MUST update both locations and the
18
+ // matching `claude/agents/<id>.md` files on disk.
19
+ export const SOFTWARE_CORE_AGENT_IDS = Object.freeze([
20
+ 'ijfw-doc-verifier',
21
+ 'ijfw-integration-checker',
22
+ 'ijfw-nyquist-auditor',
23
+ 'ijfw-code-fixer',
24
+ ]);
25
+
26
+ // T25 / G7-gen: canonical domain-specialist agent ids by archetype. The map
27
+ // is the single source of truth for which specialist ids the generator
28
+ // returns for each domain — T26 will author the matching
29
+ // `claude/agents/<id>.md` files so the installer can deploy them; T25 just
30
+ // makes the generator hand them back per archetype.
31
+ //
32
+ // Contract:
33
+ // - keys are normalized archetypes (post-alias-map). `book`, `content`,
34
+ // and `design` are populated today; other archetypes get `[]` until
35
+ // T26 expands the set.
36
+ // - values are deterministic arrays; insertion order is roster-display order.
37
+ // - software domain's specialists are the SOFTWARE_CORE_AGENT_IDS — this
38
+ // map intentionally does NOT duplicate them (`resolveDomainSpecialistAgentIds`
39
+ // in generator.js returns the union for software). Non-software domains
40
+ // own their own specialist lists exclusively.
41
+ //
42
+ // Choice of three+ domains (`book`, `content`, `design`) satisfies the
43
+ // "≥3 non-software domains" constraint and gives T26 concrete templates
44
+ // to flesh out. Other archetypes will gain rosters in later tasks.
45
+ export const DOMAIN_SPECIALIST_AGENT_IDS = Object.freeze({
46
+ book: Object.freeze([
47
+ 'ijfw-narrative-continuity-checker',
48
+ 'ijfw-line-editor',
49
+ 'ijfw-lore-keeper',
50
+ ]),
51
+ content: Object.freeze([
52
+ 'ijfw-campaign-strategist',
53
+ 'ijfw-copy-reviewer',
54
+ ]),
55
+ design: Object.freeze([
56
+ 'ijfw-design-critic',
57
+ 'ijfw-accessibility-reviewer',
58
+ ]),
59
+ });
60
+
13
61
  const ROLE_TYPES = new Set([
14
62
  'lead',
15
63
  'software',
@@ -1,5 +1,14 @@
1
1
  // MCP tool: ijfw_update_apply
2
2
  //
3
+ // @deprecated since v1.5.0; will be removed in v1.6.0 (F-FUN-3 / v1.5.0 audit-MED-M7).
4
+ // `ijfw_update_check` already issues a confirmation token whose instruction tells
5
+ // the user to type `ijfw update --confirm <token>` in their terminal directly.
6
+ // The intermediate `ijfw_update_apply` step writes a pending sentinel, but the
7
+ // terminal CLI does not require the sentinel to confirm — the token itself is
8
+ // authoritative. The tool is retained for v1.5.0 back-compat (older skills that
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).
11
+ //
3
12
  // Does NOT execute the update. Validates the token, writes (or overwrites)
4
13
  // the pending sentinel, returns instruction telling the user to run the
5
14
  // terminal-side confirm command. Idempotent against a matching sentinel
@@ -10,6 +19,11 @@
10
19
  import { validateToken, writePendingSentinel } from './lib/token.js';
11
20
  import { isVersionStringValid } from './lib/npm-view.js';
12
21
 
22
+ /**
23
+ * @deprecated since v1.5.0; scheduled for removal in v1.6.0. Callers should
24
+ * skip straight from `ijfw_update_check` to the terminal-side confirm command;
25
+ * the intermediate sentinel write is redundant given the token contract.
26
+ */
13
27
  export function ijfwUpdateApply(args = {}) {
14
28
  const { target_version, confirmation_token } = args || {};
15
29
  const sessionId = args.session_id || process.env.IJFW_SESSION_ID || 'default-session';
@@ -64,9 +78,11 @@ export function ijfwUpdateApply(args = {}) {
64
78
  export const TOOL_DEF = {
65
79
  name: 'ijfw_update_apply',
66
80
  description:
67
- 'Stage an IJFW update behind out-of-band terminal confirmation. Writes a pending sentinel; ' +
68
- "actual update only runs when the user types 'ijfw update --confirm <token>' in their terminal. " +
69
- 'This MCP tool NEVER executes the update directly.',
81
+ '[DEPRECATED v1.5.0; removal in v1.6.0] Stage an IJFW update behind out-of-band terminal ' +
82
+ 'confirmation. Writes a pending sentinel; actual update only runs when the user types ' +
83
+ "'ijfw update --confirm <token>' in their terminal. This MCP tool NEVER executes the " +
84
+ 'update directly. Prefer calling ijfw_update_check and forwarding the returned ' +
85
+ "'ijfw update --confirm <token>' instruction directly to the user.",
70
86
  inputSchema: {
71
87
  type: 'object',
72
88
  required: ['target_version', 'confirmation_token'],