@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
@@ -17,6 +17,8 @@
17
17
  import { promises as fs } from 'node:fs';
18
18
  import * as path from 'node:path';
19
19
  import { projectControlPath } from '../../config/project-artifacts.js';
20
+ import { buildClaudeHookBlock, CLAUDE_HOOK_START, CLAUDE_HOOK_END } from './claude-hook.js';
21
+ import { dominantLineEnding, withLineEnding } from './line-endings.js';
20
22
  import { buildParallelismSection, replaceOrAppendParallelismBlock } from './parallelism-section.js';
21
23
  import { buildContextFinalizationBlock, replaceOrAppendFinalizationBlock } from './finalization.js';
22
24
  import { buildExternalLinksSection, replaceOrAppendExternalLinksBlock, } from './external-links-section.js';
@@ -55,6 +57,15 @@ export async function buildAiwgMdContent(projectPath, stagedClaudeMdContent) {
55
57
  if (claudeMdContent) {
56
58
  // Insert the AIWG signature comment as the second line.
57
59
  // Top line is typically `# AIWG` or `# Project` heading; preserve it.
60
+ // Regenerate may mirror CLAUDE.md before its hook updater runs. Normalize
61
+ // that managed block here too so legacy duplicate policy never reaches
62
+ // AIWG.md, even on the first refresh. Operator content stays untouched.
63
+ const start = claudeMdContent.indexOf(CLAUDE_HOOK_START);
64
+ const end = claudeMdContent.indexOf(CLAUDE_HOOK_END, start);
65
+ if (start >= 0 && end >= start) {
66
+ const hook = withLineEnding(buildClaudeHookBlock(), dominantLineEnding(claudeMdContent));
67
+ claudeMdContent = claudeMdContent.slice(0, start) + hook + claudeMdContent.slice(end + CLAUDE_HOOK_END.length);
68
+ }
58
69
  const lines = claudeMdContent.split('\n');
59
70
  // #1268: strip `@AIWG.md` self-include directives. CLAUDE.md uses
60
71
  // `@AIWG.md` to pull in the framework context; AIWG.md IS that framework
@@ -26,29 +26,14 @@ import { dominantLineEnding, withLineEnding } from './line-endings.js';
26
26
  import { assertSafeContextFile } from './context-file-safety.js';
27
27
  export const CLAUDE_HOOK_START = '<!-- AIWG:claude-md-hook:start -->';
28
28
  export const CLAUDE_HOOK_END = '<!-- AIWG:claude-md-hook:end -->';
29
- function buildClaudeArtifactOutputPolicy(policy = {}) {
30
- const providerNative = policy.provider_native ?? 'explicit-only';
31
- const design = policy.destinations?.['claude-code.design'];
32
- const designEnabled = design?.enabled !== false && design?.use_when !== 'disabled';
33
- return [
34
- '## AIWG artifact output policy',
35
- '',
36
- `- Canonical durable artifacts: AIWG artifact store (policy: ${policy.canonical ?? 'aiwg'}).`,
37
- `- Provider-native presentation/export: ${providerNative}.`,
38
- `- Claude Design: ${designEnabled ? 'available only when explicitly selected by the resolved policy or requested by the user' : 'disabled by project policy'}.`,
39
- '- Never substitute, relocate, or omit the canonical AIWG plan/review artifact because Claude adds or changes a provider default.',
40
- '- When both canonical and presentation outputs are selected, write the canonical artifact first; treat Design as a derived export and record provenance linking it to the canonical source of truth.',
41
- '- Unknown provider-native destinations fail safe with a diagnostic. Higher-authority project policy overrides user preferences; explicit task selection overrides provider defaults only within the project policy ceiling.',
42
- ];
43
- }
44
- export function buildClaudeHookBlock(policy = {}) {
29
+ export function buildClaudeHookBlock(_policy = {}) {
45
30
  return [
46
31
  CLAUDE_HOOK_START,
47
32
  '',
48
33
  buildProviderBootstrapBlock('claude'),
49
34
  '@.aiwg/aiwg.config',
50
35
  '',
51
- ...buildClaudeArtifactOutputPolicy(policy),
36
+ 'Artifact destination authority: `aiwg show rule artifact-output-policy` (kernel).',
52
37
  '',
53
38
  '<!--',
54
39
  ' This block is managed by `aiwg regenerate` and `aiwg use`.',
@@ -72,15 +57,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
72
57
  action: 'skipped',
73
58
  warnings: [],
74
59
  };
75
- let policy = {};
76
- try {
77
- const config = JSON.parse(await fs.readFile(path.join(projectPath, '.aiwg', 'aiwg.config'), 'utf8'));
78
- policy = config.artifact_outputs ?? {};
79
- }
80
- catch {
81
- // Missing/legacy/temporarily malformed config receives the safe default.
82
- }
83
- let block = buildClaudeHookBlock(policy);
60
+ let block = buildClaudeHookBlock();
84
61
  // Case 1: CLAUDE.md does not exist — create a minimal one with just the block.
85
62
  let existing;
86
63
  try {
@@ -2,7 +2,7 @@
2
2
  * Context-finalization pass for generated provider context files.
3
3
  *
4
4
  * Templates provide the stable structure; this pass stitches in current
5
- * workspace facts from `.aiwg/aiwg.config` and the discover-first protocol so
5
+ * workspace facts from `.aiwg/aiwg.config` and rule authority pointers so
6
6
  * provider context files do not remain template-only (#1365).
7
7
  */
8
8
  import { promises as fs } from 'node:fs';
@@ -10,6 +10,7 @@ import * as path from 'node:path';
10
10
  import { projectAiwgPath, projectControlPath, resolveProjectAiwgDir, } from '../../config/project-artifacts.js';
11
11
  import { renderTrackerProtocol, resolveTrackerAuthority } from '../../tracker/capability-protocol.js';
12
12
  import { readConfig, readGitRemoteUrls } from '../../tracker/project-inputs.js';
13
+ import { DECISION_COMPACT_ANCHOR, inspectDecisionOffloadActivation } from '../../decision/offload-activation.js';
13
14
  import { buildExternalLinksSection, replaceOrAppendExternalLinksBlock, } from './external-links-section.js';
14
15
  export const FINALIZATION_START = '<!-- aiwg-context-finalization:START -->';
15
16
  export const FINALIZATION_END = '<!-- aiwg-context-finalization:END -->';
@@ -52,6 +53,7 @@ export async function buildContextFinalizationBlock(projectPath, documentPath =
52
53
  }
53
54
  }
54
55
  const trackerAuthority = resolveTrackerAuthority(config, remoteUrls);
56
+ const decision = await inspectDecisionOffloadActivation({ cwd: projectPath });
55
57
  const lines = [
56
58
  FINALIZATION_START,
57
59
  '## Context Finalization',
@@ -65,11 +67,9 @@ export async function buildContextFinalizationBlock(projectPath, documentPath =
65
67
  `- Recorded deployments: ${formatList([...providerDeployments].sort())}`,
66
68
  `- Normalized project context: \`${normalizedAiwgMdPath}\``,
67
69
  '',
68
- '### Discover-First Protocol',
70
+ '### Capability Routing',
69
71
  '',
70
- 'Classify every user turn FIRST: is it a **new directive** or a continuation? When a message names or references an AIWG command/capability — even as pasted content like an `address-issues` tracker table, an issue list, or a `flow-*` name — treat it as a new directive and ACT: run `aiwg discover "<the need>"`, fetch with `aiwg show <type> <name>`, and invoke it. Do NOT ask "what would you like me to do with these?" when the action is implied — a pasted `address-issues #1234` table means run the address-issues workflow on those issues.',
71
- '',
72
- 'Also run `aiwg discover` before declining an AIWG request as out of scope or inventing a workflow from memory. The CLI ranks AIWG capabilities across the installed corpus and rebuilds the index from `$AIWG_ROOT` automatically, so a "no matches" for a command you know is deployed is a bug — not a signal it is absent. Commands AIWG deploys to your provider command directory (`.opencode/command/`, `.claude/commands/`, `~/.codex/prompts/`, …) ARE discoverable this way; fetch them with `aiwg show command <name>`. This prevents decline-without-search failures, ask-instead-of-act on new directives, and hallucinated skill or agent names. Full rule: `agentic/code/addons/aiwg-utils/rules/skill-discovery.md`.',
72
+ 'Routing authority: `aiwg show rule skill-discovery` (kernel); capability phrases: `aiwg show skill aiwg-utils-quickref`.',
73
73
  '',
74
74
  '### Engagement Verification',
75
75
  '',
@@ -84,6 +84,7 @@ export async function buildContextFinalizationBlock(projectPath, documentPath =
84
84
  `- ${normalizedAiwgMdLabel} is the normalized project-local context entry point.`,
85
85
  '- Root `AIWG.md` is the generated cross-provider companion loaded through `AGENTS.md` and provider twins.',
86
86
  `- \`AGENTS.md\`, \`WARP.md\`, \`.hermes.md\`, and \`.github/copilot-instructions.md\` are provider-facing bridges, not replacements for ${normalizedAiwgMdLabel}.`,
87
+ ...(decision.shouldAnchor ? ['', '## Compact Instructions', '', DECISION_COMPACT_ANCHOR] : []),
87
88
  FINALIZATION_END,
88
89
  '',
89
90
  ];
@@ -199,7 +199,24 @@ export async function isOverwriteSafe(filePath) {
199
199
  *
200
200
  * Per ADR-4 §4 atomic write requirement; on failure no partial state is persisted.
201
201
  */
202
- async function atomicWrite(filePath, content) {
202
+ async function atomicWrite(filePath, content, provider) {
203
+ // Rule delivery owns this block. Context regeneration runs after deployment
204
+ // and must retain the selected kernel even when rebuilding the adapter.
205
+ const existing = await fs.readFile(filePath, 'utf8').catch(() => '');
206
+ for (const owner of ['rule-startup', 'hermes-context', 'hermes-routing', 'warp-context']) {
207
+ if (content.includes(`<!-- AIWG:${owner}:start -->`))
208
+ continue;
209
+ const managed = existing.match(new RegExp(`<!-- AIWG:${owner}:start -->[\\s\\S]*?<!-- AIWG:${owner}:end -->`));
210
+ if (managed)
211
+ content = `${content.trimEnd()}\n\n${managed[0]}\n`;
212
+ }
213
+ const bytes = Buffer.byteLength(content, 'utf8');
214
+ if (provider === 'codex' && path.basename(filePath) === 'AGENTS.md' && bytes >= 32768) {
215
+ throw new Error('Codex AGENTS.md exceeds its 32 KB hard cap');
216
+ }
217
+ if (provider === 'hermes' && ['AGENTS.md', '.hermes.md'].includes(path.basename(filePath)) && content.length > 19000) {
218
+ throw new Error(`Hermes ${path.basename(filePath)} exceeds hard cap of 19000 characters`);
219
+ }
203
220
  const dir = path.dirname(filePath);
204
221
  const tmp = path.join(dir, `.${path.basename(filePath)}.tmp.${process.pid}`);
205
222
  await fs.mkdir(dir, { recursive: true });
@@ -212,6 +229,7 @@ async function atomicWrite(filePath, content) {
212
229
  await fs.unlink(tmp).catch(() => undefined);
213
230
  throw err;
214
231
  }
232
+ return bytes;
215
233
  }
216
234
  /**
217
235
  * Per-provider twin-file names for AGENTS.md emission per ADR-1 §4.
@@ -303,7 +321,7 @@ async function writeTwinFiles(provider, projectPath, agentsMdContent, opts, resu
303
321
  throw err;
304
322
  }
305
323
  }
306
- await atomicWrite(twinPath, twinContent);
324
+ await atomicWrite(twinPath, twinContent, provider);
307
325
  result.twinPaths.push(twinPath);
308
326
  }
309
327
  }
@@ -408,9 +426,8 @@ export async function generate(opts) {
408
426
  // write, and as the twin content downstream.
409
427
  const built = await buildAgentsMd(opts);
410
428
  if (canWrite) {
411
- await atomicWrite(agentsMdPath, built.content);
429
+ result.agentsMdBytes = await atomicWrite(agentsMdPath, built.content, opts.provider);
412
430
  result.agentsMdPath = agentsMdPath;
413
- result.agentsMdBytes = Buffer.byteLength(built.content, 'utf8');
414
431
  result.warnings.push(...built.warnings);
415
432
  if (built.splitOccurred) {
416
433
  await writeSpilloverBlock(opts.projectPath, built.spilloverContent, result);
@@ -232,26 +232,16 @@ export async function buildParallelismSection(projectPath, deps = {}) {
232
232
  lines.push(`*Rationale*: ${resolved.rationale}`);
233
233
  }
234
234
  lines.push('');
235
- lines.push('### Model-selected delegation rubric');
236
- lines.push('');
237
- lines.push('For each non-trivial task, assess whether it contains independent, bounded subtasks that can run concurrently. When delegation is supported, prefer the deployed model-pinned wrappers by task characteristics and consequence:');
238
- lines.push('');
239
- lines.push('- `aiwg-model-efficiency-worker`: discovery, inventory, focused edits, and other bounded low-cost work.');
240
- lines.push('- `aiwg-model-coding-worker`: implementation, tests, debugging, and routine technical delivery.');
241
- lines.push('- `aiwg-model-reasoning-worker`: architecture, synthesis, difficult analysis, and high-consequence review.');
242
- lines.push('');
243
- lines.push('Do not delegate trivial work, tightly coupled changes, serial dependencies, or tasks likely to collide in shared state; also keep work local when coordination costs exceed the expected benefit. Parallelize only independent work, and take the MIN of provider limits, `max_parallel_subagents`, `AIWG_CONTEXT_WINDOW` budget, framework-specific caps (including the RLM 7-agent hard cap for RLM dispatches), and natural task decomposition. Bump the project cap via `aiwg config set --project parallelism.max_parallel_subagents N`.');
244
- lines.push('');
245
- lines.push('The primary agent retains orchestration, final integration, conflict resolution, validation, and user-facing accountability.');
235
+ lines.push('Delegation policy and model-worker selection: `aiwg show rule subagent-scoping`.');
236
+ lines.push('After changing project parallelism with `aiwg config set/reset --project`, follow the emitted provider-specific regeneration commands and run `aiwg workspace-context doctor`.');
246
237
  lines.push('');
247
238
  lines.push(delegationSupport(primary));
248
239
  lines.push('');
249
240
  lines.push(workerDispatchStateLine(cfg.parallelism, cfg.setup));
250
241
  lines.push('');
251
242
  // Group dispatch disabled (worker_dispatch unset/false): the resolver reports
252
- // `disabled`, so no group block renders — the native-delegation rubric above
253
- // applies unchanged and the output stays byte-identical to the pre-dispatch
254
- // rendering.
243
+ // `disabled`, so no group block renders — the subagent-scoping pointer above
244
+ // remains available.
255
245
  if (cfg.parallelism?.worker_dispatch === true && healthUnresolved) {
256
246
  lines.push(PARALLELISM_HEALTH_UNRESOLVED_NOTE);
257
247
  lines.push('');
@@ -273,13 +263,15 @@ export async function buildParallelismSection(projectPath, deps = {}) {
273
263
  lines.push(`Configured groups: ${shown}${hidden > 0 ? ` (+${hidden} more)` : ''}`);
274
264
  }
275
265
  else {
276
- lines.push('Configured groups: none — group dispatch is enabled but no groups are configured; the native delegation rubric above applies unchanged.');
266
+ lines.push('Configured groups: none — group dispatch is enabled but no groups are configured; the subagent-scoping rule applies.');
277
267
  }
278
268
  lines.push('');
279
269
  lines.push('A group above shown as configured, with excluded members or an uncovered required role listed, is a static config read — not a live eligibility verdict. Eligibility is resolved per (group, member, effort) tuple against the current catalog and request; see `aiwg models resolve` below to check a specific role/task/group.');
280
270
  lines.push('');
281
271
  lines.push('Tier 0 direct handoff is the default: use your own shell or a qualified MCP worker tool. No Mission/MC session, scheduler, daemon, or activation marker is needed. Self-count active native + CLI + MCP workers, take the minimum of the project, group, CLI, model, and context caps, and launch each writing task once.');
282
272
  lines.push('');
273
+ lines.push('Before resolving, qualify the installed CLIs and configured model roles with `aiwg doctor --probe-workers --qualify` (`--yes` for headless confirmation).');
274
+ lines.push('Agents pass `--authorize-premium` to `aiwg models resolve` only after the human explicitly confirmed the premium escalation (escalation-discipline rule); authorization applies to that resolution only.');
283
275
  lines.push('Resolve each bounded brief with `aiwg models resolve --role <role> --task <class> --group <group> --brief <file> --workdir <tree> --final-output <file> --transport shell --format shell`. Copy the exact rendered command or the MCP server/tool/arguments byte-for-byte; never assemble vendor flags from memory. Re-resolve after any brief, profile, model, or workspace change. `--format shell` prints only the launch command: to see the resolved health windows for an attempt, use `--format json` (the default) and read `invocation.health` with its per-field `origins`.');
284
276
  lines.push('');
285
277
  lines.push(`Configured health windows (each followed by its origin — \`default\` or \`project config\`): check every ${check.value}s (${check.origin}); confirm startup within ${startup.value}s (${startup.origin}); quiet limit ${quiet.value}s (${quiet.origin}); stall limit ${stall.value}s (${stall.origin}); hard stop ${hard.value}s (${hard.origin}). Per-attempt windows scale quiet and stall by the single largest applicable role/effort/model factor, never their product (for example, reasoning at \`xhigh\` effort resolves 180s/600s from the 90s/300s defaults; an effective 1.25 scale gives 112.5s/375s). Only the hard deadline and its diagnostic/termination grace periods are capped by the remaining wall-clock and transport ceilings; startup, quiet, and stall keep their configured or scaled values, and \`truncatedWindows\` instead records when one of them would exceed that capped deadline (\`resolveWorkerEffectiveHealth()\` in \`src/serve/worker-invocation.ts\`). Resolved values win over these configured values.`);
@@ -161,7 +161,7 @@ function createManifest(serverId, serverName, analyzerResult, config) {
161
161
  resources: [],
162
162
  prompts: [],
163
163
  dependencies: {
164
- runtime: ['node >= 18.0.0'],
164
+ runtime: ['node >= 24'],
165
165
  external: [
166
166
  `${analyzerResult.metadata.sourceName}${analyzerResult.metadata.sourceVersion ? ' >= ' + analyzerResult.metadata.sourceVersion : ''}`
167
167
  ]
@@ -0,0 +1,10 @@
1
+ export class RuntimeDiscovery {
2
+ basePath: string;
3
+ constructor(basePath?: string | null, options?: { cwd?: string; quiet?: boolean });
4
+ discover(options?: { force?: boolean }): Promise<any>;
5
+ verify(): Promise<any>;
6
+ getCatalog(): Promise<any>;
7
+ generateDefinition(): Promise<any>;
8
+ checkTool(toolName: string): Promise<any>;
9
+ getSummary(): Promise<any>;
10
+ }
@@ -8,8 +8,9 @@
8
8
  */
9
9
 
10
10
  import { boundedExecFileSync, boundedExecSync } from '../../process/bounded-child-process.mjs';
11
- import { readFile, writeFile, mkdir, access } from 'fs/promises';
11
+ import { readFile, writeFile, mkdir, access, stat } from 'fs/promises';
12
12
  import { existsSync, constants } from 'fs';
13
+ import { createHash } from 'node:crypto';
13
14
  import { resolve, join, dirname } from 'path';
14
15
  import { platform, arch, homedir, tmpdir, cpus, totalmem, freemem } from 'os';
15
16
  import { fileURLToPath } from 'url';
@@ -22,26 +23,59 @@ const __dirname = dirname(__filename);
22
23
  * RuntimeDiscovery - Discovers and catalogs installed system tools
23
24
  */
24
25
  export class RuntimeDiscovery {
25
- constructor(basePath = null) {
26
- this.basePath = basePath || projectAiwgPath(process.cwd(), 'smiths', 'toolsmith');
26
+ constructor(basePath = null, { cwd = process.cwd(), quiet = false } = {}) {
27
+ this.cwd = cwd;
28
+ this.quiet = quiet;
29
+ this.basePath = basePath || projectAiwgPath(this.cwd, 'smiths', 'toolsmith');
27
30
  this.knownTools = this.#loadKnownTools();
28
31
  }
29
32
 
30
33
  /**
31
- * Perform full discovery scan
34
+ * Refresh discovery, skipping version probes when inputs are unchanged.
32
35
  * @returns {Promise<Object>} RuntimeCatalog
33
36
  */
34
- async discover() {
35
- console.log('Starting runtime discovery...');
37
+ async discover({ force = false } = {}) {
38
+ if (!this.quiet) console.log('Starting runtime discovery...');
39
+
40
+ const loaded = await this.#loadCatalog().catch(error => {
41
+ if (error instanceof SyntaxError) return null;
42
+ throw error;
43
+ });
44
+ const previous = Array.isArray(loaded?.tools) ? loaded : null;
45
+ const paths = new Map();
46
+ const inputs = [];
47
+ for (const name of this.#safeTools()) {
48
+ const toolPath = await this.#findToolPath(name);
49
+ paths.set(name, toolPath);
50
+ const metadata = toolPath ? await stat(toolPath).catch(() => null) : null;
51
+ inputs.push([name, toolPath, metadata && [metadata.mtimeMs, metadata.ctimeMs, metadata.size, metadata.mode]]);
52
+ }
53
+ const inputFingerprint = createHash('sha256').update(JSON.stringify([
54
+ 1, this.knownTools, inputs, platform(), arch(), homedir(), this.cwd,
55
+ process.env.PATH, process.env.PATHEXT, process.env.SHELL, process.env.ComSpec,
56
+ ])).digest('hex');
57
+ if (!force && previous?.inputFingerprint === inputFingerprint
58
+ && existsSync(join(this.basePath, 'runtime-info.md'))) {
59
+ if (!this.quiet) console.log(`Discovery skipped: unchanged (${previous.tools.length} tools)`);
60
+ return previous;
61
+ }
36
62
 
37
63
  const environment = await this.getEnvironment();
38
64
  const resources = await this.getResources();
39
- const tools = await this.#discoverTools();
65
+ const tools = await this.#discoverTools(paths);
66
+ const previousIds = new Set((previous?.tools || []).map(tool => tool.id));
67
+ const currentIds = new Set(tools.available.map(tool => tool.id));
68
+ const delta = {
69
+ added: [...currentIds].filter(id => !previousIds.has(id)),
70
+ removed: [...previousIds].filter(id => !currentIds.has(id)),
71
+ };
40
72
 
41
73
  const catalog = {
42
74
  $schema: 'https://aiwg.io/schemas/toolsmith/runtime-catalog-v1.json',
43
75
  version: '1.0',
44
76
  generated: new Date().toISOString(),
77
+ inputFingerprint,
78
+ delta,
45
79
  environment,
46
80
  resources,
47
81
  tools: tools.available,
@@ -52,7 +86,11 @@ export class RuntimeDiscovery {
52
86
  await this.#writeCatalog(catalog);
53
87
  await this.#writeRuntimeInfo(catalog);
54
88
 
55
- console.log(`Discovery complete: ${catalog.tools.length} tools found`);
89
+ if (!this.quiet) console.log(`Discovery complete: ${catalog.tools.length} tools found`);
90
+ if (!this.quiet) console.log(`Tool delta: +${delta.added.length} added, -${delta.removed.length} removed`);
91
+ if (delta.removed.length) {
92
+ if (!this.quiet) console.warn(`Previously cataloged tools no longer available: ${delta.removed.join(', ')}`);
93
+ }
56
94
  return catalog;
57
95
  }
58
96
 
@@ -146,7 +184,7 @@ export class RuntimeDiscovery {
146
184
  osVersion = boundedExecSync('ver', 'quick', { encoding: 'utf-8' }).trim();
147
185
  }
148
186
  } catch (error) {
149
- console.warn('Could not detect OS version:', error.message);
187
+ if (!this.quiet) console.warn('Could not detect OS version:', error.message);
150
188
  }
151
189
 
152
190
  return {
@@ -155,7 +193,7 @@ export class RuntimeDiscovery {
155
193
  arch: arch(),
156
194
  shell: process.env.SHELL || process.env.ComSpec || '/bin/sh',
157
195
  homeDir: homedir(),
158
- workingDir: process.cwd()
196
+ workingDir: this.cwd
159
197
  };
160
198
  }
161
199
 
@@ -184,7 +222,7 @@ export class RuntimeDiscovery {
184
222
  diskFreeGb = Math.round((totalFree / (1024 ** 3)) * 10) / 10;
185
223
  } else {
186
224
  // Unix: use df
187
- const output = boundedExecFileSync('df', ['-k', '.'], 'quick', { encoding: 'utf-8' });
225
+ const output = boundedExecFileSync('df', ['-k', '.'], 'quick', { encoding: 'utf-8', cwd: this.cwd });
188
226
  const lines = output.trim().split('\n');
189
227
  if (lines.length > 1) {
190
228
  const parts = lines[1].trim().split(/\s+/);
@@ -199,7 +237,7 @@ export class RuntimeDiscovery {
199
237
  memoryTotalGb = Math.round((totalMem / (1024 ** 3)) * 10) / 10;
200
238
  memoryAvailableGb = Math.round((freeMem / (1024 ** 3)) * 10) / 10;
201
239
  } catch (error) {
202
- console.warn('Could not get resource info:', error.message);
240
+ if (!this.quiet) console.warn('Could not get resource info:', error.message);
203
241
  }
204
242
 
205
243
  return {
@@ -298,9 +336,9 @@ export class RuntimeDiscovery {
298
336
  * @see https://git.integrolabs.net/roctinam/aiwg/issues/820
299
337
  * @private
300
338
  */
301
- async #discoverTools() {
339
+ #safeTools() {
302
340
  // Curated list: knownTools database + common dev tools safe to probe
303
- const safeToProbe = new Set([
341
+ return new Set([
304
342
  // From knownTools database
305
343
  ...Object.keys(this.knownTools),
306
344
  // Aliases from knownTools
@@ -328,12 +366,13 @@ export class RuntimeDiscovery {
328
366
  'yq', 'fx', 'miller',
329
367
  'htop', 'ncdu', 'dust', 'procs', 'tokei',
330
368
  ]);
369
+ }
331
370
 
371
+ async #discoverTools(paths) {
332
372
  const available = [];
333
373
  const unavailable = [];
334
374
 
335
- for (const name of safeToProbe) {
336
- const toolPath = await this.#findToolPath(name);
375
+ for (const [name, toolPath] of paths) {
337
376
  if (!toolPath) {
338
377
  // Not installed — skip silently (not an error)
339
378
  continue;
@@ -359,7 +398,7 @@ export class RuntimeDiscovery {
359
398
  }
360
399
 
361
400
  /**
362
- * Find tool path using which/where
401
+ * Search PATH first, then common user installation directories on Unix.
363
402
  * @private
364
403
  */
365
404
  async #findToolPath(toolName) {
@@ -370,8 +409,27 @@ export class RuntimeDiscovery {
370
409
  .split('\n')[0];
371
410
  return path;
372
411
  } catch {
373
- return null;
412
+ // Scheduled jobs often omit user installation directories from PATH.
413
+ }
414
+ if (platform() !== 'win32' && !toolName.includes('/') && !toolName.includes('\\')) {
415
+ const home = homedir();
416
+ const directories = new Set([
417
+ join(process.env.CARGO_HOME || join(home, '.cargo'), 'bin'),
418
+ join(home, '.local', 'bin'), join(home, 'bin'),
419
+ join(home, '.npm-global', 'bin'), join(home, 'go', 'bin'),
420
+ join(home, '.bun', 'bin'), join(home, '.deno', 'bin'),
421
+ ]);
422
+ for (const directory of directories) {
423
+ const candidate = join(directory, toolName);
424
+ try {
425
+ await access(candidate, constants.X_OK);
426
+ if ((await stat(candidate)).isFile()) return candidate;
427
+ } catch {
428
+ // Continue to the next user directory.
429
+ }
430
+ }
374
431
  }
432
+ return null;
375
433
  }
376
434
 
377
435
  /**
@@ -9,14 +9,17 @@
9
9
  * @issue #934
10
10
  * @issue #956
11
11
  */
12
- import { existsSync } from 'fs';
13
- import { appendFile, mkdir, readFile, readdir, rm, stat, writeFile } from 'fs/promises';
14
- import { dirname, join, relative, resolve, sep } from 'path';
12
+ import { resolve, sep } from 'path';
13
+ import { pinContainedRoot, readContainedFile, writeContainedFile, appendContainedFile, deleteContainedFile, listContainedFiles } from '../../utils/contained-filesystem.mjs';
15
14
  export class FilesystemAdapter {
16
15
  /** Absolute path where this subsystem's content lives. */
17
16
  root;
17
+ store;
18
18
  constructor(root) {
19
19
  this.root = resolve(root);
20
+ this.store = pinContainedRoot(this.root, undefined, {
21
+ createMissing: true, protectLocationPointer: false, rejectLinkedWrites: true,
22
+ });
20
23
  }
21
24
  /**
22
25
  * Path traversal guard. Rejects `..`, leading `/`, leading `~`, and
@@ -46,89 +49,28 @@ export class FilesystemAdapter {
46
49
  return abs;
47
50
  }
48
51
  async read(path) {
49
- const abs = this.resolveSafe(path);
50
- if (!existsSync(abs))
51
- return null;
52
- return readFile(abs, 'utf-8');
52
+ this.resolveSafe(path);
53
+ return readContainedFile(await this.store, path);
53
54
  }
54
55
  async write(path, content, _meta) {
55
- const abs = this.resolveSafe(path);
56
- await mkdir(dirname(abs), { recursive: true });
57
- await writeFile(abs, content, 'utf-8');
56
+ this.resolveSafe(path);
57
+ await writeContainedFile(await this.store, path, content);
58
58
  }
59
- /**
60
- * Atomic append. Uses fs.appendFile, which opens with O_APPEND so the
61
- * kernel guarantees atomicity for writes ≤ PIPE_BUF (4096 bytes on
62
- * Linux). Concurrent appenders interleave at line granularity rather
63
- * than racing read-then-write. See #976.
64
- */
59
+ /** Append through a verified O_APPEND handle, without read/replace races. */
65
60
  async append(path, content) {
66
- const abs = this.resolveSafe(path);
67
- await mkdir(dirname(abs), { recursive: true });
68
- await appendFile(abs, content, 'utf-8');
61
+ this.resolveSafe(path);
62
+ await appendContainedFile(await this.store, path, content);
69
63
  }
70
64
  async list(prefix) {
71
- // Empty prefix = list everything under root. A non-empty prefix
72
- // restricts to entries whose subsystem-relative path starts with it
73
- // (after path-normalizing the prefix itself).
74
- if (typeof prefix !== 'string') {
65
+ if (typeof prefix !== 'string')
75
66
  throw new Error('storage(fs): list prefix must be a string');
76
- }
77
- if (prefix.length > 0) {
78
- // resolveSafe also validates the prefix shape
67
+ if (prefix)
79
68
  this.resolveSafe(prefix);
80
- }
81
- if (!existsSync(this.root))
82
- return [];
83
- const out = [];
84
- await walkInto(this.root, this.root, prefix, out);
85
- return out;
69
+ return listContainedFiles(await this.store, prefix);
86
70
  }
87
71
  async delete(path) {
88
- const abs = this.resolveSafe(path);
89
- if (!existsSync(abs))
90
- return;
91
- await rm(abs, { force: true });
92
- }
93
- }
94
- async function walkInto(root, dir, prefix, out) {
95
- let entries;
96
- try {
97
- entries = await readdir(dir, { withFileTypes: true });
98
- }
99
- catch {
100
- return;
101
- }
102
- for (const e of entries) {
103
- const full = join(dir, e.name);
104
- const rel = relative(root, full).split(sep).join('/');
105
- if (e.isDirectory()) {
106
- // Descend if the dir could contain matches
107
- if (prefix === '' || rel.startsWith(prefix) || prefix.startsWith(rel + '/')) {
108
- await walkInto(root, full, prefix, out);
109
- }
110
- continue;
111
- }
112
- if (!e.isFile())
113
- continue;
114
- if (prefix !== '' && !rel.startsWith(prefix))
115
- continue;
116
- let size;
117
- let modifiedAt;
118
- try {
119
- const s = await stat(full);
120
- size = s.size;
121
- modifiedAt = s.mtime;
122
- }
123
- catch {
124
- // ignore stat failures
125
- }
126
- const entry = { path: rel };
127
- if (size !== undefined)
128
- entry.size = size;
129
- if (modifiedAt !== undefined)
130
- entry.modifiedAt = modifiedAt;
131
- out.push(entry);
72
+ this.resolveSafe(path);
73
+ await deleteContainedFile(await this.store, path);
132
74
  }
133
75
  }
134
76
  //# sourceMappingURL=fs.js.map
@@ -96,6 +96,7 @@ export class ObsidianAdapter {
96
96
  execSync('obsidian --version', {
97
97
  stdio: ['ignore', 'pipe', 'pipe'],
98
98
  timeout: 2000,
99
+ killSignal: 'SIGKILL',
99
100
  });
100
101
  }
101
102
  catch {
@@ -13,6 +13,7 @@
13
13
  * `code`. Access material stays inside the transport: it is never part of an
14
14
  * error, a result or a log line.
15
15
  */
16
+ import { signalChild } from '../process/signal-child.mjs';
16
17
  import { spawn } from 'node:child_process';
17
18
  export class ForgeReadError extends Error {
18
19
  code;
@@ -115,7 +116,7 @@ function spawnCliRunner(binary, maxBytes) {
115
116
  size += chunk.length;
116
117
  if (size > maxBytes + 65_536) {
117
118
  oversized = true;
118
- child.kill();
119
+ signalChild(child);
119
120
  }
120
121
  else
121
122
  chunks.push(chunk);
@@ -0,0 +1,2 @@
1
+ export function checkForUpdates(): Promise<void>;
2
+ export function forceUpdateCheck(): Promise<void>;
@@ -0,0 +1,22 @@
1
+ export type InstallMode = 'npm' | 'web' | 'source';
2
+
3
+ export interface UpdateResult {
4
+ mode: InstallMode;
5
+ packageRoot: string;
6
+ packageName: string | null;
7
+ channel: string;
8
+ status: 'updated' | 'current' | 'manual' | 'dry-run' | 'unsupported-offline';
9
+ changed: boolean;
10
+ version?: string;
11
+ command?: string;
12
+ message: string;
13
+ identity?: import('../installation/manager.mjs').InstallationIdentity;
14
+ identityPersistent?: boolean;
15
+ }
16
+
17
+ export function detectInstallMode(options?: Record<string, unknown>): Promise<{
18
+ mode: InstallMode;
19
+ packageRoot: string;
20
+ packageName: string | null;
21
+ }>;
22
+ export function updateInstallation(options?: Record<string, unknown>): Promise<UpdateResult>;
@@ -0,0 +1,37 @@
1
+ import type { BigIntStats } from 'node:fs';
2
+ import type { FileHandle } from 'node:fs/promises';
3
+
4
+ /** Opaque pinned filesystem boundary; configuration is captured only once. */
5
+ export interface ContainedRoot {
6
+ readonly configured: string;
7
+ readonly root: string;
8
+ readonly identity?: BigIntStats | null;
9
+ readonly error?: Error;
10
+ }
11
+ export interface ContainmentOptions {
12
+ /** Storage may create missing configured roots through a pinned ancestor. */
13
+ createMissing?: boolean;
14
+ /** Artifacts protect location pointers; ordinary storage metadata may opt out. */
15
+ protectLocationPointer?: boolean;
16
+ /** Storage refuses replacement of hardlinks; artifact atomic writes retain compatibility. */
17
+ rejectLinkedWrites?: boolean;
18
+ }
19
+ export interface ContainedFile {
20
+ handle: FileHandle;
21
+ fullPath: string;
22
+ close(): Promise<void>;
23
+ commit(): Promise<void>;
24
+ }
25
+ export interface ContainedEntry {
26
+ path: string;
27
+ size: number;
28
+ modifiedAt: Date;
29
+ }
30
+ export function validateContainedPath(relative: string): void;
31
+ export function pinContainedRoot(configured: string, projectRoot?: string, options?: ContainmentOptions): Promise<ContainedRoot>;
32
+ export function openContainedFile(store: ContainedRoot, relative: string, options?: { write?: boolean }): Promise<ContainedFile>;
33
+ export function readContainedFile(store: ContainedRoot, relative: string): Promise<string | null>;
34
+ export function writeContainedFile(store: ContainedRoot, relative: string, content: string): Promise<void>;
35
+ export function appendContainedFile(store: ContainedRoot, relative: string, content: string): Promise<void>;
36
+ export function deleteContainedFile(store: ContainedRoot, relative: string): Promise<void>;
37
+ export function listContainedFiles(store: ContainedRoot, prefix?: string): Promise<ContainedEntry[]>;