@aiwg/cli 2026.10.3 → 2026.10.5

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 (279) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/model-capabilities.v1.json +96 -4
  3. package/bin/aiwg.mjs +50 -27
  4. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  5. package/dist/src/artifacts/browser-export.js +6 -0
  6. package/dist/src/artifacts/checksum-manifest.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +173 -92
  8. package/dist/src/artifacts/citation-table-schema.js +59 -0
  9. package/dist/src/artifacts/cli.js +69 -18
  10. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  11. package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
  12. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  13. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  14. package/dist/src/artifacts/discovery-eval.js +6 -4
  15. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  16. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  17. package/dist/src/artifacts/framework-capabilities.js +171 -0
  18. package/dist/src/artifacts/framework-index-paths.js +73 -0
  19. package/dist/src/artifacts/framework-source-digest.js +75 -0
  20. package/dist/src/artifacts/index-builder.js +286 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +130 -0
  23. package/dist/src/artifacts/index-refresh.js +591 -0
  24. package/dist/src/artifacts/index-worker.mjs +11 -0
  25. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  26. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  27. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  28. package/dist/src/artifacts/query-engine.js +410 -88
  29. package/dist/src/artifacts/relations-builder.js +168 -0
  30. package/dist/src/artifacts/relations.js +148 -0
  31. package/dist/src/artifacts/stats.js +5 -2
  32. package/dist/src/artifacts/types.js +30 -10
  33. package/dist/src/artifacts/watcher.js +1 -1
  34. package/dist/src/auth/credential-store.js +2 -1
  35. package/dist/src/catalog/cli.d.mts +5 -0
  36. package/dist/src/channel/manager.d.mts +20 -0
  37. package/dist/src/channel/manager.mjs +19 -2
  38. package/dist/src/cli/command-log.js +2 -3
  39. package/dist/src/cli/handlers/artifacts.js +3 -0
  40. package/dist/src/cli/handlers/auth.js +3 -0
  41. package/dist/src/cli/handlers/decision.js +19 -12
  42. package/dist/src/cli/handlers/diagnose.js +4 -3
  43. package/dist/src/cli/handlers/effect.js +4 -2
  44. package/dist/src/cli/handlers/evidence.js +3 -0
  45. package/dist/src/cli/handlers/help.js +1 -0
  46. package/dist/src/cli/handlers/index.js +3 -1
  47. package/dist/src/cli/handlers/init.js +4 -1
  48. package/dist/src/cli/handlers/lint.js +5 -0
  49. package/dist/src/cli/handlers/models.js +104 -8
  50. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  51. package/dist/src/cli/handlers/refresh.js +12 -0
  52. package/dist/src/cli/handlers/resource-versions.js +3 -0
  53. package/dist/src/cli/handlers/rules.js +195 -0
  54. package/dist/src/cli/handlers/run.js +4 -0
  55. package/dist/src/cli/handlers/runtime-info.js +1 -2
  56. package/dist/src/cli/handlers/script-runner.js +3 -2
  57. package/dist/src/cli/handlers/session.js +19 -9
  58. package/dist/src/cli/handlers/sessions.js +137 -43
  59. package/dist/src/cli/handlers/setup-manifest.js +39 -44
  60. package/dist/src/cli/handlers/setup.js +41 -0
  61. package/dist/src/cli/handlers/steward.js +11 -0
  62. package/dist/src/cli/handlers/subcommands.js +57 -2
  63. package/dist/src/cli/handlers/uhp.js +3 -0
  64. package/dist/src/cli/handlers/use.js +294 -55
  65. package/dist/src/cli/handlers/utilities.js +26 -10
  66. package/dist/src/cli/handlers/version.js +8 -0
  67. package/dist/src/cli/router.js +15 -1
  68. package/dist/src/cli/services/deployment-verification.js +19 -9
  69. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  70. package/dist/src/cli/skill-usage.js +2 -3
  71. package/dist/src/config/aiwg-config.js +87 -37
  72. package/dist/src/config/cli.js +111 -5
  73. package/dist/src/config/decision-config.js +106 -0
  74. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  75. package/dist/src/config/gitignore.js +50 -0
  76. package/dist/src/config/index-graph-overrides.js +36 -0
  77. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  78. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  79. package/dist/src/config/user-config-dir.d.mts +9 -0
  80. package/dist/src/config/user-config.js +2 -1
  81. package/dist/src/config/user-registry.js +23 -15
  82. package/dist/src/config/user-state.js +7 -0
  83. package/dist/src/decision/ask.js +203 -43
  84. package/dist/src/decision/context-live-qualification.js +2 -2
  85. package/dist/src/decision/driver.js +13 -8
  86. package/dist/src/decision/ensemble/aggregate.js +23 -10
  87. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  88. package/dist/src/decision/entry.js +2 -15
  89. package/dist/src/decision/graph-live-qualification.js +1 -1
  90. package/dist/src/decision/graph-run.js +9 -0
  91. package/dist/src/decision/heldout/collector.js +5 -13
  92. package/dist/src/decision/heldout/contract.js +30 -7
  93. package/dist/src/decision/jev-credentials.js +15 -1
  94. package/dist/src/decision/ledger.js +131 -28
  95. package/dist/src/decision/offload-activation.js +137 -0
  96. package/dist/src/decision/offload.js +550 -0
  97. package/dist/src/decision/qualification/load.js +4 -6
  98. package/dist/src/decision/savings.js +28 -0
  99. package/dist/src/effects/types.js +5 -1
  100. package/dist/src/effects/verifiers/git.js +2 -1
  101. package/dist/src/effects/verifiers/tracker.js +26 -14
  102. package/dist/src/extensions/commands/definitions.js +19 -0
  103. package/dist/src/extensions/project-local-remove.js +36 -1
  104. package/dist/src/gates/providers/loader.js +61 -17
  105. package/dist/src/installation/manager-command.d.mts +18 -0
  106. package/dist/src/installation/manager-command.mjs +14 -2
  107. package/dist/src/installation/manager.d.mts +49 -0
  108. package/dist/src/installation/manager.mjs +78 -3
  109. package/dist/src/issues/cli.js +4 -4
  110. package/dist/src/issues/local.js +24 -5
  111. package/dist/src/jobs/runner.js +1 -1
  112. package/dist/src/lint/cli.js +27 -1
  113. package/dist/src/lint/loader.js +2 -1
  114. package/dist/src/lint/reporters.js +25 -1
  115. package/dist/src/lint/rule-activation.js +43 -0
  116. package/dist/src/lint/runner.js +137 -62
  117. package/dist/src/mcp/adapters/codex-runtime.js +7 -1
  118. package/dist/src/mcp/claude-install.d.mts +4 -0
  119. package/dist/src/mcp/claude-install.mjs +41 -0
  120. package/dist/src/mcp/cli.d.mts +1 -0
  121. package/dist/src/mcp/cli.mjs +36 -11
  122. package/dist/src/mcp/config-file.d.mts +28 -0
  123. package/dist/src/mcp/config-layers.d.mts +36 -0
  124. package/dist/src/mcp/credentials.d.mts +51 -0
  125. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  126. package/dist/src/mcp/grok-build-config.mjs +1 -1
  127. package/dist/src/mcp/helpers.mjs +3 -2
  128. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  129. package/dist/src/mcp/muse-native-config.mjs +1 -0
  130. package/dist/src/mcp/omp-config.d.mts +4 -0
  131. package/dist/src/mcp/registry.js +3 -93
  132. package/dist/src/mcp/registry.mjs +3 -87
  133. package/dist/src/mcp/server-config.d.mts +3 -0
  134. package/dist/src/mcp/server-config.mjs +89 -0
  135. package/dist/src/mcp/server.mjs +37 -375
  136. package/dist/src/mcp/test-simple.mjs +2 -1
  137. package/dist/src/mcp/toml-editor.d.mts +4 -0
  138. package/dist/src/mcp/toml-string.d.mts +1 -0
  139. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  140. package/dist/src/mcp/tool-filters.d.mts +46 -0
  141. package/dist/src/mcp/tools/decision.mjs +12 -11
  142. package/dist/src/mcp/tools/discovery.mjs +62 -55
  143. package/dist/src/mcp/tools/orchestration.mjs +12 -2
  144. package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
  145. package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
  146. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
  147. package/dist/src/mission-protocol/codecs.js +8 -0
  148. package/dist/src/models/config-loader.js +7 -2
  149. package/dist/src/models/model-capabilities.v1.json +96 -4
  150. package/dist/src/models/model-discovery.js +76 -11
  151. package/dist/src/models/provider-policy.js +101 -7
  152. package/dist/src/models/qualification-digest.d.mts +2 -0
  153. package/dist/src/models/resolver.js +12 -10
  154. package/dist/src/models/worker-direct-readiness.js +65 -44
  155. package/dist/src/models/worker-dispatch-offer.js +3 -3
  156. package/dist/src/models/worker-health-readiness.js +16 -3
  157. package/dist/src/models/worker-installation.d.mts +39 -0
  158. package/dist/src/models/worker-installation.mjs +135 -28
  159. package/dist/src/process/bounded-child-process.d.mts +84 -0
  160. package/dist/src/process/bounded-child-process.mjs +43 -0
  161. package/dist/src/process/cli-command.d.mts +16 -0
  162. package/dist/src/process/signal-child.d.mts +6 -0
  163. package/dist/src/process/signal-child.mjs +7 -0
  164. package/dist/src/providers/deployed-skill-roots.js +20 -0
  165. package/dist/src/providers/grok-build-acp.js +3 -2
  166. package/dist/src/providers/grok-build-headless.js +12 -4
  167. package/dist/src/providers/grok-build-inspect.js +1 -0
  168. package/dist/src/providers/omp-agent.d.mts +8 -0
  169. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  170. package/dist/src/providers/omp-paths.d.mts +13 -0
  171. package/dist/src/providers/skill-ownership.d.mts +9 -0
  172. package/dist/src/providers/skill-ownership.mjs +88 -0
  173. package/dist/src/rules/activation.d.mts +21 -0
  174. package/dist/src/rules/activation.mjs +62 -0
  175. package/dist/src/rules/inline-budget.d.mts +26 -0
  176. package/dist/src/rules/inline-budget.mjs +66 -0
  177. package/dist/src/rules/inventory.js +129 -0
  178. package/dist/src/rules/pin-state.js +73 -0
  179. package/dist/src/rules/pins.d.mts +14 -0
  180. package/dist/src/rules/pins.mjs +112 -0
  181. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  182. package/dist/src/serve/mission-conductor.js +38 -12
  183. package/dist/src/serve/worker-invocation.js +30 -10
  184. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  185. package/dist/src/sessions/adapter-registry.js +23 -19
  186. package/dist/src/sessions/adapters/claude.js +39 -54
  187. package/dist/src/sessions/adapters/codex.js +95 -67
  188. package/dist/src/sessions/adapters/copilot.js +4 -4
  189. package/dist/src/sessions/adapters/cursor.js +40 -57
  190. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  191. package/dist/src/sessions/adapters/factory.js +31 -27
  192. package/dist/src/sessions/adapters/generic.js +10 -10
  193. package/dist/src/sessions/adapters/grok-build.js +11 -11
  194. package/dist/src/sessions/adapters/grokbot.js +24 -13
  195. package/dist/src/sessions/adapters/hermes.js +27 -29
  196. package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
  197. package/dist/src/sessions/adapters/muse.js +150 -93
  198. package/dist/src/sessions/adapters/omp.js +28 -14
  199. package/dist/src/sessions/adapters/openclaw.js +89 -79
  200. package/dist/src/sessions/adapters/opencode.js +27 -24
  201. package/dist/src/sessions/adapters/openhuman.js +18 -60
  202. package/dist/src/sessions/adapters/pi.js +29 -14
  203. package/dist/src/sessions/adapters/warp.js +3 -3
  204. package/dist/src/sessions/adapters/windsurf.js +19 -48
  205. package/dist/src/sessions/batch-contracts.js +16 -8
  206. package/dist/src/sessions/batch-import.js +252 -12
  207. package/dist/src/sessions/contracts.js +50 -3
  208. package/dist/src/sessions/coverage-preflight.js +27 -2
  209. package/dist/src/sessions/export-plan.js +4 -3
  210. package/dist/src/sessions/import-exclusions.js +72 -0
  211. package/dist/src/sessions/import-lease.js +34 -18
  212. package/dist/src/sessions/import-limits.js +37 -0
  213. package/dist/src/sessions/importer.js +60 -38
  214. package/dist/src/sessions/lifecycle.js +45 -0
  215. package/dist/src/sessions/policy.js +145 -26
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +105 -0
  218. package/dist/src/sessions/repository.js +151 -54
  219. package/dist/src/sessions/workspace-discovery.js +292 -60
  220. package/dist/src/sessions/workspace-matching.js +107 -0
  221. package/dist/src/skills/importer.js +1 -0
  222. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  223. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  224. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  225. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  226. package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
  227. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  228. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  229. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
  230. package/dist/src/storage/backends/fs.js +18 -76
  231. package/dist/src/storage/backends/obsidian.js +1 -0
  232. package/dist/src/tracker/forge-http.js +2 -1
  233. package/dist/src/update/checker.d.mts +2 -0
  234. package/dist/src/update/service.d.mts +22 -0
  235. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  236. package/dist/src/utils/contained-filesystem.mjs +683 -0
  237. package/dist/src/utils/temporary-directory.js +36 -0
  238. package/dist/src/writing/example-generator.js +1 -1
  239. package/package.json +15 -2
  240. package/schemas/models/model-catalog.v1.schema.json +1 -1
  241. package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
  242. package/tools/_resolve-impl.mjs +13 -15
  243. package/tools/agents/deploy-agents.mjs +49 -21
  244. package/tools/agents/providers/base.mjs +104 -134
  245. package/tools/agents/providers/claude.mjs +8 -24
  246. package/tools/agents/providers/codex.mjs +18 -11
  247. package/tools/agents/providers/cursor.mjs +19 -18
  248. package/tools/agents/providers/grok-build.mjs +3 -1
  249. package/tools/agents/providers/hermes.mjs +57 -47
  250. package/tools/agents/providers/openclaw.mjs +9 -4
  251. package/tools/agents/providers/pi.mjs +5 -0
  252. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  253. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  254. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  255. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  256. package/tools/agents/providers/warp.mjs +20 -26
  257. package/tools/cli/decision-offload-report.mjs +40 -0
  258. package/tools/cli/workspace-status.mjs +862 -0
  259. package/tools/plugin/package-plugins.mjs +186 -8
  260. package/tools/plugin/plugin-links.mjs +114 -0
  261. package/tools/providers/antigravity-transport.mjs +3 -1
  262. package/tools/providers/deepseek-harness-transport.mjs +6 -4
  263. package/tools/providers/omp-transport.mjs +4 -2
  264. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  265. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  266. package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
  267. package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
  268. package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
  269. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  270. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  271. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  272. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  273. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  274. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  275. package/tools/ralph-external/process-monitor.mjs +1017 -0
  276. package/tools/ralph-external/session-launcher.mjs +5057 -0
  277. package/tools/scripts/resource-preflight.mjs +541 -0
  278. package/tools/skills/deploy-skills-codex.mjs +2 -1
  279. package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
@@ -326,11 +326,13 @@ export function loadCorpus(root) {
326
326
  const venue = classifyFirst(`${venueRaw ?? ''} ${text.slice(0, 3500)}`.toLowerCase(), VENUE_PATTERNS) ||
327
327
  (venueRaw ? String(venueRaw).trim() : null);
328
328
  const { tier, paramsM } = extractParams(text);
329
+ const radar = extractRadar(root, refId);
329
330
  records.push({
330
331
  refId,
331
332
  sourceType: normalizeSourceType({ type: asStr(fm.type), sourceType: asStr(fm.source_type), venue }, srcTypeReg),
332
333
  referencedByProjects: extractReferencedByProjects(text),
333
334
  title: extractTitle(text, fm),
335
+ grade: radar?.gradeCurrent ?? asStr(fm.grade),
334
336
  path: filePath,
335
337
  year: extractYear(text, fm, citation),
336
338
  authors,
@@ -343,7 +345,7 @@ export function loadCorpus(root) {
343
345
  paramsM,
344
346
  incoming,
345
347
  outgoing,
346
- radar: extractRadar(root, refId),
348
+ radar,
347
349
  discovery: extractDiscovery(citation),
348
350
  funders: extractFunders(citation),
349
351
  });
@@ -242,9 +242,9 @@ function renderPipeline(ctx) {
242
242
  // Cadence → window math lives in corpus-config.ts (the one staleness helper, #1498/#1502).
243
243
  /** GRADE display order; unknown grades fall after, 'Ungraded' last. */
244
244
  const GRADE_ORDER = ['A', 'A-', 'B', 'B-', 'C', 'C-', 'D'];
245
- /** Papers grouped by current GRADE (radar `grade-current`); no-radar → Ungraded. (#1492) */
245
+ /** Papers grouped by current GRADE, falling back to reference frontmatter. */
246
246
  function renderByGrade(ctx) {
247
- const groups = groupBy(ctx.records, (r) => [r.radar?.gradeCurrent || 'Ungraded']);
247
+ const groups = groupBy(ctx.records, (r) => [r.grade || 'Ungraded']);
248
248
  const lines = header('Index: Papers by GRADE', ctx, ctx.records.length);
249
249
  const names = [...groups.keys()].sort((a, b) => {
250
250
  if (a === 'Ungraded')
@@ -284,7 +284,7 @@ function renderStaleQueue(ctx) {
284
284
  const lines = header('Index: Radar Stale-Refresh Queue', ctx, ctx.records.length);
285
285
  lines.push(`Overdue = days since last refresh minus the cadence window. ${staleCount} of ${rows.length} radar-tracked papers are overdue.`, '', '| REF | Title | GRADE | Cadence | Last Refreshed | Overdue (days) | Stale |', '|---|---|---|---|---|---:|---|');
286
286
  for (const { r, cadence, last, overdue } of sorted) {
287
- lines.push(`| ${r.refId} | ${r.title.slice(0, 70)} | ${r.radar.gradeCurrent ?? '—'} | ${cadence} | ${last} | ${overdue} | ${overdue > 0 ? 'yes' : 'no'} |`);
287
+ lines.push(`| ${r.refId} | ${r.title.slice(0, 70)} | ${r.grade ?? '—'} | ${cadence} | ${last} | ${overdue} | ${overdue > 0 ? 'yes' : 'no'} |`);
288
288
  }
289
289
  return lines.join('\n') + '\n';
290
290
  }
@@ -2,7 +2,8 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { performance } from 'node:perf_hooks';
5
- import { OPERATIONAL_DISCOVERY_TYPES } from './types.js';
5
+ import { getGraphIndexDir, OPERATIONAL_DISCOVERY_TYPES } from './types.js';
6
+ import { getFrameworkIndexBaseDir } from './framework-index-paths.js';
6
7
  import { loadGraphIndexFile } from './index-reader.js';
7
8
  import { loadFortemiCoreExport, loadFortemiCoreMetadataEntries, scoreStaticRecord } from './fortemi-core-query-adapter.js';
8
9
  import { discoverCapability } from './query-engine.js';
@@ -185,8 +186,9 @@ export function calculateDiscoveryMetrics(records, resultSets) {
185
186
  .some((item) => record.hard_negative_ids.some((id) => matches(item, id)))).length / records.length),
186
187
  };
187
188
  }
188
- function indexBytes(backend) {
189
- const root = path.join(process.env.XDG_DATA_HOME ?? path.join(os.homedir(), '.local', 'share'), 'aiwg', 'index', ...(backend === 'local' ? ['framework'] : ['fortemi-core', 'framework']));
189
+ function indexBytes(cwd, backend) {
190
+ const root = backend === 'local' ? getGraphIndexDir(cwd, 'framework')
191
+ : path.join(getFrameworkIndexBaseDir(cwd), 'fortemi-core', 'framework');
190
192
  let total = 0;
191
193
  const visit = (target) => {
192
194
  if (!fs.existsSync(target))
@@ -259,7 +261,7 @@ export async function evaluateDiscovery(options) {
259
261
  configuration: { backend: options.backend, strategy: options.strategy, limit, graph: 'framework' },
260
262
  hardware: { platform: os.platform(), arch: os.arch(), cpu_model: cpus[0]?.model ?? 'unknown', logical_cpus: cpus.length, total_memory_bytes: os.totalmem(), node: process.version },
261
263
  metrics,
262
- performance: { p50_latency_ms: round(percentile(latencies, 0.5), 3), p95_latency_ms: round(percentile(latencies, 0.95), 3), index_bytes: indexBytes(options.backend), peak_resident_memory_bytes: peakRss },
264
+ performance: { p50_latency_ms: round(percentile(latencies, 0.5), 3), p95_latency_ms: round(percentile(latencies, 0.95), 3), index_bytes: indexBytes(options.cwd, options.backend), peak_resident_memory_bytes: peakRss },
263
265
  ...(parity ? { parity } : {}),
264
266
  adoption_gate: {
265
267
  baseline: 'local:lexical', no_per_type_hit_at_3_regression: null, aggregate_mrr_improvement: null,
@@ -1,7 +1,11 @@
1
+ import { selectPrebuiltRelations } from './relations.js';
2
+ import { resolveFrameworkIndexRoot } from './framework-index-paths.js';
3
+ import { extractRuleBindings } from './index-builder.js';
1
4
  import fs from "node:fs";
2
5
  import { createHash } from "node:crypto";
3
6
  import { bm25Rank } from "./fulltext.js";
4
- import { getFortemiCorePrebuiltStatus, getFortemiCoreSyncStatus } from "./fortemi-core-sync.js";
7
+ import { consumeFortemiCoreExport, getFortemiCorePrebuiltStatus, getFortemiCoreSyncStatus } from "./fortemi-core-sync.js";
8
+ import { loadGraphIndexFile } from "./index-reader.js";
5
9
  import { operationalStateQueryProjection, } from "./operational-state.js";
6
10
  function tokenize(text) {
7
11
  return text
@@ -167,6 +171,14 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
167
171
  };
168
172
  }
169
173
  }
174
+ // A content-validated shipped corpus does not need the source metadata index.
175
+ // This also avoids preferring a much larger locally materialized text cache.
176
+ if (graph === 'framework' && selectPrebuiltRelations(cwd).reader) {
177
+ const prebuilt = getFortemiCorePrebuiltStatus(graph, resolveFrameworkIndexRoot(cwd));
178
+ if (prebuilt.built && !prebuilt.stale) {
179
+ return { exported: JSON.parse(fs.readFileSync(prebuilt.exportPath, 'utf8')) };
180
+ }
181
+ }
170
182
  let status = getFortemiCoreSyncStatus(cwd, graph);
171
183
  // The packaged framework index is a bootstrap fallback for an absent shared
172
184
  // cache. Once a shared cache exists, its stale state proves that the local
@@ -178,6 +190,20 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
178
190
  status = prebuilt;
179
191
  }
180
192
  }
193
+ // A packaged bootstrap cache can also predate a source checkout's index.
194
+ // Validate it against the same current data as the shared cache before use.
195
+ if (status.source === "prebuilt") {
196
+ const current = loadGraphIndexFile(cwd, "metadata.json", graph);
197
+ if (current &&
198
+ Date.parse(current.builtAt) > Date.parse(status.sourceIndexBuiltAt ?? status.generatedAt ?? "")) {
199
+ status = {
200
+ ...status,
201
+ stale: true,
202
+ sourceIndexNewer: true,
203
+ reason: "source index is newer than the Fortemi Core static cache",
204
+ };
205
+ }
206
+ }
181
207
  if (!status.optedIn) {
182
208
  return {
183
209
  reason: `Fortemi Core static index is not materialized for graph '${graph}'. Run 'aiwg index sync${graph === "project" ? "" : ` --graph ${graph}`}' first, or pass '--backend local' to use the legacy local index.`,
@@ -185,12 +211,13 @@ export function loadFortemiCoreExport(cwd, graph = "project", sourcePaths) {
185
211
  }
186
212
  if (!status.built || status.stale) {
187
213
  return {
214
+ ...(status.sourceIndexNewer ? { sourceIndexNewer: true } : {}),
188
215
  reason: `${status.reason ??
189
216
  `Fortemi Core static index for graph '${graph}' is stale or incomplete`}. Re-run 'aiwg index sync${graph === "project" ? "" : ` --graph ${graph}`}' or pass '--backend local' to use the legacy local index.`,
190
217
  };
191
218
  }
192
219
  try {
193
- const exported = JSON.parse(fs.readFileSync(status.exportPath, "utf-8"));
220
+ const exported = consumeFortemiCoreExport(status.exportPath, status.exportChecksum);
194
221
  if (exported.schema_version !== "aiwg.fortemi.index.export.v2") {
195
222
  return {
196
223
  reason: `Fortemi Core static index has schema '${exported.schema_version}', expected 'aiwg.fortemi.index.export.v2'. Re-run 'aiwg index sync'.`,
@@ -255,13 +282,19 @@ function entryFromRecord(record) {
255
282
  && typeof indexedScript.runtime === "string"
256
283
  ? indexedScript
257
284
  : undefined,
285
+ ...extractRuleBindings(indexedFrontmatter),
286
+ ...(Array.isArray(indexedFrontmatter.aiwg_governed_paths) ? { governedPaths: indexedFrontmatter.aiwg_governed_paths.filter((value) => typeof value === "string") } : {}),
258
287
  operationalState: record.operational_state,
259
288
  };
260
289
  }
261
290
  export function loadFortemiCoreMetadataEntries(cwd, graph = "project", sourcePaths) {
262
291
  const loaded = loadFortemiCoreExport(cwd, graph, sourcePaths);
263
292
  if (!loaded.exported)
264
- return { entries: [], reason: loaded.reason };
293
+ return {
294
+ entries: [],
295
+ reason: loaded.reason,
296
+ ...(loaded.sourceIndexNewer ? { sourceIndexNewer: true } : {}),
297
+ };
265
298
  return { entries: loaded.exported.items.map(entryFromRecord) };
266
299
  }
267
300
  export async function queryFortemiCoreAiwgDiscovery(cwd, options) {
@@ -1,15 +1,18 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { createHash } from "node:crypto";
3
+ import { createHash, randomUUID } from "node:crypto";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { parse as parseYaml } from "yaml";
6
6
  import { GRAPH_CONFIGS, getProjectIndexRoot, loadGlobalGraphConfigs } from "./types.js";
7
7
  import { buildAiwgFortemiIndexExport, } from "./browser-export.js";
8
8
  import { loadGraphIndexFile } from "./index-reader.js";
9
+ import { getFrameworkIndexBaseDir } from './framework-index-paths.js';
9
10
  function syncDir(cwd, graph) {
10
11
  loadGlobalGraphConfigs();
12
+ if (graph === 'framework')
13
+ return path.join(getFrameworkIndexBaseDir(cwd), 'fortemi-core', graph);
11
14
  const config = GRAPH_CONFIGS[graph];
12
- if (graph === "framework" || config?.shared) {
15
+ if (config?.shared) {
13
16
  const xdgData = process.env.XDG_DATA_HOME ?? path.join(process.env.HOME ?? cwd, ".local", "share");
14
17
  return path.join(xdgData, "aiwg", "index", "fortemi-core", graph);
15
18
  }
@@ -36,6 +39,47 @@ function prebuiltDir(graph) {
36
39
  function sha256(text) {
37
40
  return createHash("sha256").update(text).digest("hex");
38
41
  }
42
+ // Status checks and the query reader often inspect the same large export.
43
+ // Keep at most two private snapshots, and never retain an object exposed to a
44
+ // caller: query results have always been fresh, mutable objects.
45
+ const exportSnapshots = new Map();
46
+ function exportSignature(file) {
47
+ const stat = fs.statSync(file, { bigint: true });
48
+ return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
49
+ }
50
+ function readExportSnapshot(file, expectedChecksum) {
51
+ const signature = exportSignature(file);
52
+ const cached = exportSnapshots.get(file);
53
+ // The manifest checksum is part of the snapshot identity. Filesystems may
54
+ // preserve every stat field across a same-size rewrite within one timestamp
55
+ // tick, while the manifest already names the replacement's checksum.
56
+ if (cached?.signature === signature &&
57
+ cached.checksum === expectedChecksum)
58
+ return cached;
59
+ exportSnapshots.delete(file);
60
+ const text = fs.readFileSync(file, "utf-8");
61
+ const exported = JSON.parse(text);
62
+ const snapshot = { signature, exported, checksum: sha256(text) };
63
+ // A concurrent replacement cannot publish a snapshot under the wrong stamp.
64
+ if (exportSignature(file) === signature) {
65
+ exportSnapshots.set(file, snapshot);
66
+ while (exportSnapshots.size > 2)
67
+ exportSnapshots.delete(exportSnapshots.keys().next().value);
68
+ }
69
+ return snapshot;
70
+ }
71
+ /** Transfer a checked snapshot to the query reader without sharing mutable results. */
72
+ export function consumeFortemiCoreExport(file, expectedChecksum) {
73
+ try {
74
+ const snapshot = readExportSnapshot(file, expectedChecksum);
75
+ if (snapshot.checksum !== expectedChecksum)
76
+ throw new Error("export checksum does not match manifest");
77
+ return snapshot.exported;
78
+ }
79
+ finally {
80
+ exportSnapshots.delete(file);
81
+ }
82
+ }
39
83
  function readManifest(manifestPath) {
40
84
  return readManifestResult(manifestPath).manifest;
41
85
  }
@@ -59,11 +103,91 @@ function sourceIndexBuiltAt(cwd, graph) {
59
103
  const index = loadGraphIndexFile(cwd, "metadata.json", graph);
60
104
  return typeof index?.builtAt === "string" ? index.builtAt : null;
61
105
  }
106
+ /**
107
+ * Emit the v2 JSON document without stringifying a collection or a whole record.
108
+ * Strings are escaped in bounded slices too: a single large text field must not
109
+ * reintroduce V8's string limit. Preserve pretty-print bytes for stable checksums.
110
+ */
111
+ function* jsonTokens(value, maxChars, depth = 0, ancestors = new Set()) {
112
+ if (value && typeof value === "object" && "toJSON" in value && typeof value.toJSON === "function") {
113
+ value = value.toJSON();
114
+ }
115
+ if (typeof value === "string") {
116
+ yield '"';
117
+ const sliceChars = Math.floor(maxChars / 6); // A control character can expand to \uXXXX.
118
+ for (let start = 0; start < value.length;) {
119
+ let end = Math.min(start + sliceChars, value.length);
120
+ // Keep surrogate pairs together, including when UTF-8 encoding each write.
121
+ if (end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1]) && /[\uDC00-\uDFFF]/.test(value[end]))
122
+ end--;
123
+ yield JSON.stringify(value.slice(start, end)).slice(1, -1);
124
+ start = end;
125
+ }
126
+ yield '"';
127
+ }
128
+ else if (value && typeof value === "object") {
129
+ if (ancestors.has(value))
130
+ throw new TypeError("Converting circular structure to JSON");
131
+ ancestors.add(value);
132
+ const array = Array.isArray(value);
133
+ const entries = Array.isArray(value) ? value.map((item, index) => [String(index), item])
134
+ : Object.entries(value).filter(([, item]) => item !== undefined && typeof item !== "function" && typeof item !== "symbol");
135
+ yield array ? "[" : "{";
136
+ for (let i = 0; i < entries.length; i++) {
137
+ yield (i === 0 ? "\n" : ",\n") + " ".repeat(depth + 1);
138
+ const [key, item] = entries[i];
139
+ if (!array) {
140
+ yield* jsonTokens(key, maxChars, depth + 1, ancestors);
141
+ yield ": ";
142
+ }
143
+ yield* jsonTokens(item, maxChars, depth + 1, ancestors);
144
+ }
145
+ if (entries.length)
146
+ yield "\n" + " ".repeat(depth);
147
+ yield array ? "]" : "}";
148
+ ancestors.delete(value);
149
+ }
150
+ else {
151
+ yield JSON.stringify(value) ?? "null";
152
+ }
153
+ }
154
+ /** Write and hash the same bounded chunks; publish only complete exports. */
155
+ function writeExport(file, exported, maxChars) {
156
+ const hash = createHash("sha256");
157
+ const fd = fs.openSync(file, "w");
158
+ try {
159
+ const flush = (chunk) => {
160
+ const bytes = Buffer.from(chunk, "utf-8");
161
+ hash.update(bytes);
162
+ for (let offset = 0; offset < bytes.length;)
163
+ offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
164
+ };
165
+ let pending = "";
166
+ for (const token of jsonTokens(exported, maxChars)) {
167
+ if (pending.length + token.length > maxChars) {
168
+ flush(pending);
169
+ pending = "";
170
+ }
171
+ pending += token;
172
+ }
173
+ flush(pending);
174
+ flush("\n");
175
+ return hash.digest("hex");
176
+ }
177
+ finally {
178
+ fs.closeSync(fd);
179
+ }
180
+ }
62
181
  export function syncFortemiCoreIndex(cwd, options = {}) {
63
182
  const graph = options.graph ?? "project";
64
183
  const dir = syncDir(cwd, graph);
65
184
  const exportPath = path.join(dir, "aiwg-fortemi-index-v2.json");
66
185
  const manifestPath = path.join(dir, "manifest.json");
186
+ const maxChars = options.maxChunkChars ?? 64 * 1024;
187
+ if (!Number.isSafeInteger(maxChars) || maxChars < 64)
188
+ throw new Error("maxChunkChars must be an integer of at least 64");
189
+ fs.mkdirSync(dir, { recursive: true });
190
+ const temporaryPath = path.join(dir, `.export-${randomUUID()}.tmp`);
67
191
  const previous = readManifest(manifestPath);
68
192
  const sourceBuiltAt = sourceIndexBuiltAt(cwd, graph);
69
193
  const buildExport = (generatedAt) => {
@@ -74,12 +198,11 @@ export function syncFortemiCoreIndex(cwd, options = {}) {
74
198
  generatedAt,
75
199
  schemaVersion: "v2",
76
200
  });
77
- const json = JSON.stringify(exported, null, 2) + "\n";
201
+ const checksum = writeExport(temporaryPath, exported, maxChars);
78
202
  const recordsWithConcepts = exported.items.filter((item) => (item.skos_concepts?.length ?? 0) > 0).length;
79
203
  return {
80
204
  generatedAt,
81
- json,
82
- checksum: sha256(json),
205
+ checksum,
83
206
  itemCount: exported.items.length,
84
207
  skosCoverage: {
85
208
  records_with_concepts: recordsWithConcepts,
@@ -91,36 +214,40 @@ export function syncFortemiCoreIndex(cwd, options = {}) {
91
214
  },
92
215
  };
93
216
  };
94
- let built = buildExport(options.generatedAt ?? previous?.generated_at ?? new Date().toISOString());
95
- let status = "created";
96
- if (previous) {
97
- if (previous.export_checksum === built.checksum) {
98
- status = "unchanged";
99
- }
100
- else {
101
- status = "updated";
102
- if (options.generatedAt === undefined) {
103
- built = buildExport(new Date().toISOString());
217
+ try {
218
+ let built = buildExport(options.generatedAt ?? previous?.generated_at ?? new Date().toISOString());
219
+ let status = "created";
220
+ if (previous) {
221
+ if (previous.export_checksum === built.checksum) {
222
+ status = "unchanged";
223
+ }
224
+ else {
225
+ status = "updated";
226
+ if (options.generatedAt === undefined) {
227
+ built = buildExport(new Date().toISOString());
228
+ }
104
229
  }
105
230
  }
231
+ fs.renameSync(temporaryPath, exportPath);
232
+ const manifest = {
233
+ schema_version: "aiwg.fortemi.core-sync.v1",
234
+ backend: "fortemi-core",
235
+ graph,
236
+ generated_at: built.generatedAt,
237
+ export_path: path.relative(cwd, exportPath),
238
+ export_schema_version: "aiwg.fortemi.index.export.v2",
239
+ export_checksum: built.checksum,
240
+ item_count: built.itemCount,
241
+ skos_coverage: built.skosCoverage,
242
+ status,
243
+ source_index_built_at: sourceBuiltAt,
244
+ };
245
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
246
+ return manifest;
247
+ }
248
+ finally {
249
+ fs.rmSync(temporaryPath, { force: true });
106
250
  }
107
- fs.mkdirSync(dir, { recursive: true });
108
- fs.writeFileSync(exportPath, built.json, "utf-8");
109
- const manifest = {
110
- schema_version: "aiwg.fortemi.core-sync.v1",
111
- backend: "fortemi-core",
112
- graph,
113
- generated_at: built.generatedAt,
114
- export_path: path.relative(cwd, exportPath),
115
- export_schema_version: "aiwg.fortemi.index.export.v2",
116
- export_checksum: built.checksum,
117
- item_count: built.itemCount,
118
- skos_coverage: built.skosCoverage,
119
- status,
120
- source_index_built_at: sourceBuiltAt,
121
- };
122
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
123
- return manifest;
124
251
  }
125
252
  export function getFortemiCoreSyncStatus(cwd, graph = "project") {
126
253
  const dir = syncDir(cwd, graph);
@@ -149,9 +276,8 @@ export function getFortemiCoreSyncStatus(cwd, graph = "project") {
149
276
  let exportChecksumMatches = false;
150
277
  if (exportExists) {
151
278
  try {
152
- const exportText = fs.readFileSync(exportPath, "utf-8");
153
- const exported = JSON.parse(exportText);
154
- exportChecksumMatches = sha256(exportText) === manifest.export_checksum;
279
+ const { exported, checksum } = readExportSnapshot(exportPath, manifest.export_checksum);
280
+ exportChecksumMatches = checksum === manifest.export_checksum;
155
281
  if (exportChecksumMatches) {
156
282
  if (exported.schema_version !== manifest.export_schema_version) {
157
283
  exportReadReason = `export schema '${exported.schema_version}' does not match manifest '${manifest.export_schema_version}'`;
@@ -182,6 +308,7 @@ export function getFortemiCoreSyncStatus(cwd, graph = "project") {
182
308
  exportPath,
183
309
  built: exportExists,
184
310
  stale,
311
+ ...(sourceIsNewer ? { sourceIndexNewer: true } : {}),
185
312
  itemCount: manifest.item_count,
186
313
  exportChecksum: manifest.export_checksum,
187
314
  generatedAt: manifest.generated_at,
@@ -195,8 +322,8 @@ export function getFortemiCoreSyncStatus(cwd, graph = "project") {
195
322
  : null,
196
323
  };
197
324
  }
198
- export function getFortemiCorePrebuiltStatus(graph = "framework") {
199
- const dir = prebuiltDir(graph) ?? path.join(process.cwd(), "prebuilt", "fortemi-core", graph);
325
+ export function getFortemiCorePrebuiltStatus(graph = "framework", packageRoot) {
326
+ const dir = (packageRoot ? path.join(packageRoot, "prebuilt", "fortemi-core", graph) : prebuiltDir(graph)) ?? path.join(process.cwd(), "prebuilt", "fortemi-core", graph);
200
327
  const manifestPath = path.join(dir, "manifest.json");
201
328
  const exportPath = path.join(dir, "aiwg-fortemi-index-v2.json");
202
329
  const { manifest, reason: manifestReadReason } = readManifestResult(manifestPath);
@@ -222,8 +349,7 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
222
349
  let reason = null;
223
350
  if (exportExists) {
224
351
  try {
225
- const exportText = fs.readFileSync(exportPath, "utf-8");
226
- const exported = JSON.parse(exportText);
352
+ const { exported, checksum } = readExportSnapshot(exportPath, prebuiltManifest.export_checksum);
227
353
  if (prebuiltManifest.schema_version !== "aiwg.fortemi.prebuilt.v1" ||
228
354
  prebuiltManifest.backend !== "fortemi-core" ||
229
355
  prebuiltManifest.graph !== graph ||
@@ -231,7 +357,7 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
231
357
  prebuiltManifest.export_schema_version !== "aiwg.fortemi.index.export.v2") {
232
358
  reason = "prebuilt manifest is incompatible with the requested graph";
233
359
  }
234
- else if (sha256(exportText) !== prebuiltManifest.export_checksum) {
360
+ else if (checksum !== prebuiltManifest.export_checksum) {
235
361
  reason = "prebuilt export checksum does not match manifest";
236
362
  }
237
363
  else if (exported.schema_version !== prebuiltManifest.export_schema_version) {
@@ -0,0 +1,171 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { getGraphIndexDir } from './types.js';
5
+ import { frameworkSourceDigest } from './framework-source-digest.js';
6
+ import { acquireDirectoryLock, DirectoryLockContentionError } from './prebuilt-build-lock.js';
7
+ import { FRAMEWORK_CORPUS_KINDS as CORPUS_KINDS, resolveFrameworkCorpusRoot } from './framework-index-paths.js';
8
+ export { resolveFrameworkCorpusRoot } from './framework-index-paths.js';
9
+ function readText(file) {
10
+ try {
11
+ return fs.readFileSync(file, 'utf8');
12
+ }
13
+ catch {
14
+ return '';
15
+ }
16
+ }
17
+ /** Resolve HEAD without spawning git, including worktrees and packed refs. */
18
+ function gitHead(root) {
19
+ let gitDir = path.join(root, '.git');
20
+ const pointer = readText(gitDir).trim();
21
+ if (pointer.startsWith('gitdir: '))
22
+ gitDir = path.resolve(root, pointer.slice(8));
23
+ const head = readText(path.join(gitDir, 'HEAD')).trim();
24
+ if (!head.startsWith('ref: '))
25
+ return head;
26
+ const ref = head.slice(5);
27
+ const common = readText(path.join(gitDir, 'commondir')).trim();
28
+ const commonDir = common ? path.resolve(gitDir, common) : gitDir;
29
+ return readText(path.join(gitDir, ref)).trim()
30
+ || readText(path.join(commonDir, ref)).trim()
31
+ || readText(path.join(commonDir, 'packed-refs')).split('\n').find(line => line.endsWith(` ${ref}`))
32
+ || head;
33
+ }
34
+ /**
35
+ * Root/version identity plus the portable corpus digest. Its per-root stat
36
+ * receipt avoids rereading unchanged source contents and detects uncommitted
37
+ * edits as well as manifest/package/HEAD changes.
38
+ */
39
+ function sourceFingerprint(root) {
40
+ const hash = createHash('sha256').update('framework-capabilities-v2');
41
+ hash.update(path.resolve(root));
42
+ try {
43
+ hash.update(JSON.parse(readText(path.join(root, 'package.json'))).version ?? '');
44
+ }
45
+ catch { /* fixture root */ }
46
+ hash.update(gitHead(root));
47
+ hash.update(frameworkSourceDigest(root));
48
+ for (const kind of CORPUS_KINDS) {
49
+ const dir = path.join(root, 'agentic/code', kind);
50
+ let entries;
51
+ try {
52
+ entries = fs.readdirSync(dir, { withFileTypes: true });
53
+ }
54
+ catch {
55
+ continue;
56
+ }
57
+ hash.update(kind).update(readText(path.join(dir, 'manifest.json')));
58
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
59
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
60
+ continue;
61
+ hash.update(entry.name).update(readText(path.join(dir, entry.name, 'manifest.json')));
62
+ }
63
+ }
64
+ return hash.digest('hex');
65
+ }
66
+ /** Stat signatures also notice manual rebuilds, deleted caches, and replacement indexes. */
67
+ function fileSignature(file) {
68
+ try {
69
+ const stat = fs.statSync(file, { bigint: true });
70
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
71
+ }
72
+ catch {
73
+ return '';
74
+ }
75
+ }
76
+ function readReceipt(file) {
77
+ try {
78
+ return JSON.parse(readText(file));
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * Reconcile only when a cheap source/index receipt changes. Contenders serve an
86
+ * existing index after a short wait; cold starts wait for the first builder.
87
+ * The shared directory lock recovers owners proven dead on this host.
88
+ */
89
+ export async function refreshFrameworkCapabilities(root, syncFortemi = true, options = {}) {
90
+ const corpusRoot = resolveFrameworkCorpusRoot(root);
91
+ if (!corpusRoot)
92
+ return;
93
+ root = corpusRoot;
94
+ const dir = getGraphIndexDir(root, 'framework');
95
+ const receiptPath = path.join(dir, 'capability-sources.json');
96
+ const metadataPath = path.join(dir, 'metadata.json');
97
+ const fortemiDir = path.join(path.dirname(dir), 'fortemi-core', 'framework');
98
+ const fortemiSignature = () => [
99
+ 'manifest.json', 'aiwg-fortemi-index-v2.json',
100
+ ].map(file => fileSignature(path.join(fortemiDir, file))).join('|');
101
+ const source = sourceFingerprint(root);
102
+ const current = () => ({
103
+ source, index: fileSignature(metadataPath), relations: fileSignature(path.join(dir, 'relations.json')), ...(syncFortemi ? { fortemi: fortemiSignature() } : {}),
104
+ });
105
+ const matches = (receipt, probe) => Boolean(!options.force && receipt && probe.index && probe.relations && receipt.relations === probe.relations && receipt.source === probe.source
106
+ && receipt.index === probe.index && (!syncFortemi || receipt.fortemi === probe.fortemi));
107
+ if (matches(readReceipt(receiptPath), current()))
108
+ return;
109
+ let release;
110
+ try {
111
+ release = await acquireDirectoryLock(path.join(dir, 'capability-refresh.lock'), {
112
+ timeoutMs: fs.existsSync(metadataPath) ? 150 : 60_000,
113
+ pollMs: 25,
114
+ reclaimDeadOwner: true,
115
+ });
116
+ }
117
+ catch (error) {
118
+ if (error instanceof DirectoryLockContentionError && fs.existsSync(metadataPath))
119
+ return;
120
+ throw error;
121
+ }
122
+ try {
123
+ // Another process may have completed reconciliation while we waited.
124
+ const receipt = readReceipt(receiptPath);
125
+ const probe = current();
126
+ if (matches(receipt, probe))
127
+ return;
128
+ if (options.force || !receipt || !probe.index || receipt.source !== source || receipt.index !== probe.index || !probe.relations || receipt.relations !== probe.relations) {
129
+ const { buildIndex } = await import('./index-builder.js');
130
+ await buildIndex(root, { graph: 'framework', explicit: false, quiet: true });
131
+ }
132
+ // A forced source scan can be a no-op. The prior successful receipt still
133
+ // validates the exact index/export files, so avoid hashing the export again.
134
+ const updated = current();
135
+ const cacheUnchanged = receipt && updated.index && receipt.source === source
136
+ && receipt.index === updated.index && receipt.fortemi === updated.fortemi;
137
+ if (syncFortemi && !cacheUnchanged) {
138
+ const { getFortemiCoreSyncStatus, syncFortemiCoreIndex } = await import('./fortemi-core-sync.js');
139
+ const status = getFortemiCoreSyncStatus(root, 'framework');
140
+ const indexChanged = receipt?.index ? receipt.index !== updated.index : probe.index !== updated.index;
141
+ if (indexChanged || !status.built || status.stale)
142
+ syncFortemiCoreIndex(root, { graph: 'framework' });
143
+ }
144
+ // Publish only after success. A failed rebuild/sync is retried next call.
145
+ const temp = `${receiptPath}.${process.pid}.tmp`;
146
+ fs.writeFileSync(temp, JSON.stringify(current()));
147
+ fs.renameSync(temp, receiptPath);
148
+ }
149
+ finally {
150
+ await release();
151
+ }
152
+ }
153
+ /** Installation is recorded in project config, independently of index coverage. */
154
+ export async function isCorpusBundleInstalled(cwd, root, kind, id) {
155
+ if (!kind || !id)
156
+ return false;
157
+ const { readAiwgConfigCandidate } = await import('../config/aiwg-config.js');
158
+ const config = await readAiwgConfigCandidate(cwd);
159
+ if (!config)
160
+ return false;
161
+ if (config.installed[id] || (kind === 'framework' && config.installed.all))
162
+ return true;
163
+ try {
164
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, 'agentic/code', `${kind}s`, id, 'manifest.json'), 'utf8'));
165
+ return (manifest.modeAliases ?? []).some(alias => Boolean(config.installed[alias]));
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ //# sourceMappingURL=framework-capabilities.js.map