@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
@@ -5,12 +5,12 @@
5
5
  * - **Authority.** The tracker comes from `resolveTrackerAuthority` over the
6
6
  * project config and git remote URLs. A target whose forge is neither the
7
7
  * internal nor the customer tracker, or whose repository differs from that
8
- * tracker remote, is `unknown` / `tracker-blocked`. The API base is derived
9
- * from the selected remote URL; no host is assumed.
8
+ * tracker remote, is `unknown` with a specific precondition reason. The API
9
+ * base is derived from the selected remote URL; no host is assumed.
10
10
  * - **Access order.** `chooseTrackerAccess` over the probes a CLI process can
11
11
  * offer: the tracker HTTP API (with credentials from the existing tracker
12
12
  * token path, `resolveToken`), then the forge CLI (`gh`, GitHub only). MCP
13
- * is never offered. A blocker is `unknown` / `tracker-blocked`.
13
+ * is never offered. Blockers include a safe configuration or credential hint.
14
14
  * - **Matching.** A comment matches exactly when it carries the
15
15
  * `<!-- aiwg-effect: <id> -->` marker and was written by the pinned tracker
16
16
  * actor; a merged PR matches its merge commit's `Effect-Id:` trailer.
@@ -20,8 +20,8 @@
20
20
  * older than `minAbsentAgeMs`; a younger intent is `unknown` /
21
21
  * `consistency-lag`.
22
22
  *
23
- * Access material stays inside the transport: evidence carries IDs, SHAs and
24
- * digests only, and errors carry fixed messages.
23
+ * Access material stays inside the transport: evidence carries IDs, SHAs,
24
+ * digests and safe repair hints only, and errors carry fixed messages.
25
25
  *
26
26
  * @see docs/contracts/effect-ledger.v1.md "Tracker verifiers"
27
27
  */
@@ -92,7 +92,7 @@ function onPath(binary) {
92
92
  }
93
93
  }));
94
94
  }
95
- /** Resolve the tracker lane for a target: authority, remote, access path and reader. Throws `tracker-blocked`. */
95
+ /** Resolve the tracker lane, or return a classified precondition failure with a safe repair hint. */
96
96
  function resolveLane(options, target) {
97
97
  const parsed = TARGET.exec(target);
98
98
  if (!parsed)
@@ -103,16 +103,16 @@ function resolveLane(options, target) {
103
103
  const remotes = options.config?.remotes;
104
104
  let lane = null;
105
105
  if (authority.provider === provider) {
106
- lane = { remote: authority.issueTrackerRemote, url: authority.issueTrackerUrl, actor: remotes?.tracker_actor };
106
+ lane = { remote: authority.issueTrackerRemote, url: authority.issueTrackerUrl, actor: remotes?.tracker_actor, actorConfigKey: 'remotes.tracker_actor.login' };
107
107
  }
108
108
  else if (authority.customerIssueTrackerRemote && authority.customerProvider === provider) {
109
- lane = { remote: authority.customerIssueTrackerRemote, url: authority.customerIssueTrackerUrl, actor: remotes?.customer_tracker_actor };
109
+ lane = { remote: authority.customerIssueTrackerRemote, url: authority.customerIssueTrackerUrl, actor: remotes?.customer_tracker_actor, actorConfigKey: 'remotes.customer_tracker_actor.login' };
110
110
  }
111
111
  if (!lane)
112
- throw new EffectVerifierError('tracker-blocked');
112
+ return unknown('tracker-provider-unresolved', { hint: 'Set remotes.issue_provider (or remotes.customer_issue_provider for the customer tracker) to the target forge.' });
113
113
  const endpoint = forgeEndpointFromRemote(provider, lane.url);
114
114
  if (!endpoint || endpoint.repository.toLowerCase() !== parsed[2].toLowerCase())
115
- throw new EffectVerifierError('tracker-blocked');
115
+ return unknown('tracker-repository-mismatch', { hint: 'Match the target repository to the remote selected by remotes.issue_tracker (or remotes.customer_issue_tracker).' });
116
116
  const laneAuthority = { ...authority, provider, issueTrackerRemote: lane.remote, issueTrackerUrl: lane.url };
117
117
  const token = resolveToken(provider, options.env ?? process.env);
118
118
  const cli = options.cli === false ? null : options.cli ?? {};
@@ -125,13 +125,16 @@ function resolveLane(options, target) {
125
125
  const readerOptions = options.pageSize ? { pageSize: options.pageSize } : {};
126
126
  if (decision.kind === 'http-api' && token) {
127
127
  const transport = httpForgeTransport({ provider, apiBaseUrl: endpoint.apiBaseUrl, token, fetchImpl: options.fetchImpl, maxResponseBytes: options.maxResponseBytes });
128
- return { provider, repository: endpoint.repository, number, access: 'http-api', reader: createForgeReader(provider, transport, readerOptions), actor: lane.actor };
128
+ return { provider, repository: endpoint.repository, number, access: 'http-api', reader: createForgeReader(provider, transport, readerOptions), actor: lane.actor, actorConfigKey: lane.actorConfigKey };
129
129
  }
130
130
  if (decision.kind === 'cli' && cli) {
131
131
  const transport = cliForgeTransport({ provider, hostname: endpoint.host, runner: cli.runner, binary: cli.binary, maxResponseBytes: options.maxResponseBytes });
132
- return { provider, repository: endpoint.repository, number, access: 'cli', reader: createForgeReader(provider, transport, readerOptions), actor: lane.actor };
132
+ return { provider, repository: endpoint.repository, number, access: 'cli', reader: createForgeReader(provider, transport, readerOptions), actor: lane.actor, actorConfigKey: lane.actorConfigKey };
133
133
  }
134
- throw new EffectVerifierError('tracker-blocked');
134
+ // Gitea has no raw-read CLI fallback. GitHub may use gh when no token is set.
135
+ return provider === 'gitea'
136
+ ? unknown('tracker-credential-missing', { hint: 'Set AIWG_GITEA_TOKEN or GITEA_TOKEN for the tracker HTTP API.' })
137
+ : unknown('tracker-no-access-route', { hint: 'Set AIWG_GITHUB_TOKEN or GITHUB_TOKEN, or install and authenticate gh for the tracker host.' });
135
138
  }
136
139
  const unknown = (reason, evidence) => ({ result: 'unknown', reason, complete: false, ...(evidence ? { evidence } : {}) });
137
140
  /** `absent` once the intent is old enough, else `unknown` / `consistency-lag`. */
@@ -160,6 +163,8 @@ export function trackerCommentVerifier(options) {
160
163
  canReportAbsent: true,
161
164
  async verify(request) {
162
165
  const lane = resolveLane(options, request.target);
166
+ if ('result' in lane)
167
+ return lane;
163
168
  const expectedDigest = expectedString(request, 'digest');
164
169
  if (expectedDigest !== undefined && !DIGEST.test(expectedDigest))
165
170
  return unknown('malformed-response');
@@ -220,7 +225,10 @@ export function trackerCommentVerifier(options) {
220
225
  const summary = { ...base, pages, scanned };
221
226
  // A marker for this effect under another author is never ours; escalate instead of replaying.
222
227
  if (foreignMarker)
223
- return unknown('evidence-conflict', { ...summary, foreignMarker: true });
228
+ return unknown('evidence-conflict', {
229
+ ...summary, foreignMarker: true,
230
+ ...(!pinned ? { actorUnpinned: true, hint: `Set ${lane.actorConfigKey} to the trusted tracker author.` } : {}),
231
+ });
224
232
  if (digestMatch) {
225
233
  return { result: 'present', reason: 'digest-match', complete, evidence: { ...summary, commentId: digestMatch.id, bodyDigest: digestMatch.bodyDigest, confidence: 'exact' } };
226
234
  }
@@ -241,6 +249,8 @@ export function trackerIssueClosedVerifier(options) {
241
249
  canReportAbsent: true,
242
250
  async verify(request) {
243
251
  const lane = resolveLane(options, request.target);
252
+ if ('result' in lane)
253
+ return lane;
244
254
  const issue = await read(() => lane.reader.getIssue(lane.repository, lane.number, request.signal));
245
255
  const evidence = { ...laneEvidence(lane), state: issue.state, closedAt: issue.closedAt };
246
256
  if (issue.state === 'closed')
@@ -263,6 +273,8 @@ export function trackerPrMergedVerifier(options) {
263
273
  if (expectedSha !== undefined && !OBJECT_ID.test(expectedSha))
264
274
  return unknown('malformed-response');
265
275
  const lane = resolveLane(options, request.target);
276
+ if ('result' in lane)
277
+ return lane;
266
278
  const pr = await read(() => lane.reader.getPullState(lane.repository, lane.number, request.signal));
267
279
  const evidence = {
268
280
  ...laneEvidence(lane), state: pr.state, merged: pr.merged, mergedAt: pr.mergedAt,
@@ -200,6 +200,24 @@ export const decisionCommand = {
200
200
  allowedTools: ['Read', 'Write', 'Bash'],
201
201
  },
202
202
  };
203
+ export const rulesCommand = {
204
+ id: 'rules',
205
+ type: 'command',
206
+ name: 'Rule Pins',
207
+ description: 'Pin installed rules to every request and manage reusable capability groups',
208
+ version: '1.0.0',
209
+ capabilities: ['cli', 'rules', 'configuration', 'activation'],
210
+ keywords: ['rules', 'pin', 'unpin', 'pinned', 'tier', 'capability', 'group'],
211
+ category: 'config',
212
+ platforms: { claude: 'full', generic: 'full' },
213
+ deployment: { pathTemplate: '.{platform}/commands/{id}.md', core: true },
214
+ metadata: {
215
+ type: 'command',
216
+ template: 'utility',
217
+ argumentHint: '<pin|unpin|pinned|group> [rules...] [--tier tiers] [--capability ids] [--group name] [--all] [--dry-run] [--json]',
218
+ allowedTools: ['Read', 'Write', 'Bash'],
219
+ },
220
+ };
203
221
  export const gatesCommand = {
204
222
  id: 'gates',
205
223
  type: 'command',
@@ -3752,6 +3770,7 @@ export const commandDefinitions = [
3752
3770
  updateCommand,
3753
3771
  installationCommand,
3754
3772
  decisionCommand,
3773
+ rulesCommand,
3755
3774
  gatesCommand,
3756
3775
  refreshCommand,
3757
3776
  regenerateCommand,
@@ -12,8 +12,10 @@
12
12
  * @design @.aiwg/architecture/design-aiwg-remove-revert.md
13
13
  * @implements #1037
14
14
  */
15
- import { lstat, readdir, stat, unlink, rmdir } from 'fs/promises';
15
+ import { lstat, readdir, readFile, stat, unlink, rmdir } from 'fs/promises';
16
16
  import { resolve, join, relative, dirname, isAbsolute } from 'path';
17
+ import { deployedSkillRoots } from '../providers/deployed-skill-roots.js';
18
+ import { releaseSkillOwnership } from '../providers/skill-ownership.mjs';
17
19
  import { appendProjectLocalActivity } from './project-local-activity.js';
18
20
  import { sha256OfFileNormalized } from './managed-marker.js';
19
21
  import { getProviderArtifactPathStrings } from '../providers/provider-definitions.js';
@@ -257,7 +259,40 @@ export async function removeProjectLocalBundle(config, projectDir, bundleId, opt
257
259
  for (const provider of providers) {
258
260
  const artifactHashes = artifactHashesForProvider(installedEntry, provider);
259
261
  let providerHadSkip = false;
262
+ // Receipts cover the complete transformed skill and support tree, including
263
+ // retired skills no longer present in the source inventory.
264
+ const receiptSkills = new Set();
265
+ const sourceRoot = installedEntry.localPath ? resolve(projectDir, installedEntry.localPath) : undefined;
266
+ for (const root of deployedSkillRoots(projectDir, provider)) {
267
+ for (const name of await readdir(root).catch(() => [])) {
268
+ const skillDir = join(root, name);
269
+ try {
270
+ await readFile(join(skillDir, '.aiwg-manifest.json'), 'utf8');
271
+ }
272
+ catch {
273
+ continue;
274
+ }
275
+ receiptSkills.add(name);
276
+ const removed = releaseSkillOwnership(skillDir, {
277
+ frameworkSlug: bundleId,
278
+ skillSource: sourceRoot ? join(sourceRoot, 'skills', name) : undefined,
279
+ sourceRoot,
280
+ }, { dryRun });
281
+ if (removed === null) {
282
+ // Unrelated receipts are outside this bundle's removal inventory.
283
+ if (!Object.keys(artifactHashes).some(sourceRel => sourceRel.startsWith(`skills/${name}/`)))
284
+ continue;
285
+ providerHadSkip = true;
286
+ }
287
+ outcomes.push({ provider, artifactPath: `skills/${name}/SKILL.md`, deployedAbsPath: skillDir,
288
+ case: removed === null ? 'replaced' : 'pristine', reverted: removed !== null && !dryRun,
289
+ message: removed === null ? 'owned by another source — preserved' : dryRun
290
+ ? '[dry-run] would release skill ownership' : 'released skill ownership; operator files preserved' });
291
+ }
292
+ }
260
293
  for (const sourceRel of Object.keys(artifactHashes)) {
294
+ if (sourceRel.startsWith('skills/') && receiptSkills.has(sourceRel.split('/')[1]))
295
+ continue;
261
296
  const owner = resolveOwnership(config, bundleId, provider, sourceRel);
262
297
  if (owner) {
263
298
  providerHadSkip = true;
@@ -1,3 +1,4 @@
1
+ import { signalChild } from '../../process/signal-child.mjs';
1
2
  import { createHash, randomBytes } from 'node:crypto';
2
3
  import { spawn, spawnSync } from 'node:child_process';
3
4
  import { closeSync, lstatSync, openSync, readSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync, } from 'node:fs';
@@ -53,6 +54,10 @@ export const GATE_PROVIDER_MAX_TIMEOUT_MS = 30000;
53
54
  export const GATE_PROVIDER_DEFAULT_MAX_RECORDS = 50000;
54
55
  /** Largest accepted isolated-run stdout body (single JSON message). */
55
56
  export const GATE_PROVIDER_MAX_OUTPUT_BYTES = 1024 * 1024;
57
+ /** V8 old generation; RLIMIT_DATA also bounds off-heap ArrayBuffer allocations. */
58
+ export const GATE_PROVIDER_MAX_OLD_SPACE_MB = 128;
59
+ // Node reserves ~512 MiB for its code range before any provider runs.
60
+ export const GATE_PROVIDER_MAX_DATA_BYTES = 768 * 1024 * 1024;
56
61
  export class GateProviderError extends Error {
57
62
  code;
58
63
  constructor(message, code) {
@@ -317,11 +322,16 @@ function prepareIsolatedRun(record, args) {
317
322
  // OS sandbox (C1c/C1d): bubblewrap is the isolation boundary.
318
323
  // ---------------------------------------------------------------------------
319
324
  const BWRAP_CANDIDATES = ['/usr/bin/bwrap', '/bin/bwrap', '/usr/local/bin/bwrap'];
325
+ const PRLIMIT_CANDIDATES = ['/usr/bin/prlimit', '/bin/prlimit', '/usr/local/bin/prlimit'];
320
326
  const SANDBOX_NODE_PATH = '/aiwg/bin/node';
321
327
  const SANDBOX_CWD = '/tmp';
322
328
  const SANDBOX_SELF_TEST_TIMEOUT_MS = 15000;
323
329
  const SANDBOX_NAMESPACES = ['net', 'pid', 'ipc', 'uts', 'user', 'mnt'];
330
+ const MEMORY_NODE_ARGS = [
331
+ `--max-old-space-size=${GATE_PROVIDER_MAX_OLD_SPACE_MB}`, '--max-semi-space-size=16',
332
+ ];
324
333
  const DRIVER_NODE_ARGS = [
334
+ ...MEMORY_NODE_ARGS,
325
335
  '--experimental-vm-modules', '--permission', '--disallow-code-generation-from-strings',
326
336
  '--no-warnings', '--input-type=module', '-e', ISOLATED_CHILD_SOURCE,
327
337
  ];
@@ -330,8 +340,16 @@ const RESULT_FRAME = 'AIWG-RESULT ';
330
340
  const SANDBOX_SELF_TEST_SOURCE = [
331
341
  "import fs from 'node:fs';",
332
342
  "import net from 'node:net';",
343
+ "import v8 from 'node:v8';",
333
344
  "const input = JSON.parse(fs.readFileSync(0, 'utf8'));",
334
- 'const out = { env: {}, ns: {}, visible: [], connect: null };',
345
+ 'const out = { env: {}, ns: {}, visible: [], connect: null, memory: null };',
346
+ // Read the inherited kernel limits AND exercise the allocation path. Do
347
+ // not touch the allocation if the wrapper is broken: fail without committing it.
348
+ "const data = /^Max data size\\s+(\\S+)\\s+(\\S+)/m.exec(fs.readFileSync('/proc/self/limits', 'utf8'));",
349
+ 'let allocationBlocked = false;',
350
+ 'try { new Uint8Array(input.maxDataBytes); } catch (error) { allocationBlocked = error instanceof RangeError; }',
351
+ 'out.memory = { dataSoftBytes: Number(data?.[1]) || null, dataHardBytes: Number(data?.[2]) || null,',
352
+ ' heapLimitBytes: v8.getHeapStatistics().heap_size_limit, allocationBlocked };',
335
353
  'for (const key of Object.keys(process.env)) out.env[key] = String(process.env[key]);',
336
354
  "for (const name of input.namespaces) { try { out.ns[name] = fs.readlinkSync('/proc/self/ns/' + name); } catch { out.ns[name] = null; } }",
337
355
  'for (const path of input.paths) { try { fs.lstatSync(path); out.visible.push(path); } catch { /* absent */ } }',
@@ -349,7 +367,7 @@ let sandboxOverride = null;
349
367
  let sandboxVerdict;
350
368
  let runtimeBindCache;
351
369
  /**
352
- * Test-only hook: point the runner at another bwrap path (for example a
370
+ * Test-only hook: point the runner at another bwrap/prlimit path (for example a
353
371
  * missing binary, or one that fails the self-test) or reset with `null`.
354
372
  * It can only make the runner refuse: whatever path is configured must still
355
373
  * pass the capability self-test before any provider runs.
@@ -360,6 +378,13 @@ export function configureIsolationSandboxForTests(options) {
360
378
  }
361
379
  function resolveBwrapPath() {
362
380
  const candidates = sandboxOverride?.bwrapPath !== undefined ? [sandboxOverride.bwrapPath] : BWRAP_CANDIDATES;
381
+ return resolveExecutable(candidates);
382
+ }
383
+ function resolvePrlimitPath() {
384
+ const candidates = sandboxOverride?.prlimitPath !== undefined ? [sandboxOverride.prlimitPath] : PRLIMIT_CANDIDATES;
385
+ return resolveExecutable(candidates);
386
+ }
387
+ function resolveExecutable(candidates) {
363
388
  for (const candidate of candidates) {
364
389
  try {
365
390
  const status = statSync(candidate);
@@ -476,6 +501,10 @@ function sandboxCommand(nodeArgs) {
476
501
  if (bwrap === null) {
477
502
  throw new GateProviderError('bundle providers require bubblewrap (bwrap) for OS-level isolation and none was found: refusing to run', 'sandbox-unavailable');
478
503
  }
504
+ const prlimit = resolvePrlimitPath();
505
+ if (prlimit === null) {
506
+ throw new GateProviderError('bundle providers require prlimit for a kernel memory cap and none was found: refusing to run', 'sandbox-unavailable');
507
+ }
479
508
  const args = [
480
509
  '--unshare-all', '--unshare-user', '--disable-userns',
481
510
  '--die-with-parent', '--new-session', '--clearenv', '--cap-drop', 'ALL',
@@ -483,7 +512,13 @@ function sandboxCommand(nodeArgs) {
483
512
  for (const [source, target] of runtimeBinds())
484
513
  args.push('--ro-bind', source, target);
485
514
  args.push('--tmpfs', SANDBOX_CWD, '--proc', '/proc', '--dev', '/dev', '--chdir', SANDBOX_CWD, '--', SANDBOX_NODE_PATH, ...nodeArgs);
486
- return { command: bwrap, args, env: {} };
515
+ return {
516
+ command: prlimit,
517
+ // Both limits are hard as well as soft, inherited across exec into bwrap
518
+ // and node. Core dumps are disabled for bounded heap exhaustion failures.
519
+ args: [`--data=${GATE_PROVIDER_MAX_DATA_BYTES}:${GATE_PROVIDER_MAX_DATA_BYTES}`, '--core=0:0', '--', bwrap, ...args],
520
+ env: {},
521
+ };
487
522
  }
488
523
  /**
489
524
  * The exact sandboxed launch used for every provider run: bubblewrap with
@@ -491,7 +526,8 @@ function sandboxCommand(nodeArgs) {
491
526
  * cleared environment, new session, die-with-parent, read-only node runtime
492
527
  * binds, a private tmpfs, and node with `--permission` (no fs, no child
493
528
  * processes, no workers) plus `--disallow-code-generation-from-strings`.
494
- * The spawn environment itself is empty.
529
+ * prlimit applies a hard data cap (including off-heap memory); V8 has a
530
+ * separate heap cap. The spawn environment itself is empty.
495
531
  */
496
532
  export function isolationLaunchPlan() {
497
533
  return sandboxCommand(DRIVER_NODE_ARGS);
@@ -515,11 +551,11 @@ function hostProbePaths() {
515
551
  }
516
552
  function selfTestInput(port) {
517
553
  const paths = hostProbePaths();
518
- return { paths, input: JSON.stringify({ namespaces: SANDBOX_NAMESPACES, paths, port }) };
554
+ return { paths, input: JSON.stringify({ namespaces: SANDBOX_NAMESPACES, paths, port, maxDataBytes: GATE_PROVIDER_MAX_DATA_BYTES }) };
519
555
  }
520
556
  function judgeSelfTest(stdout, status, paths, port) {
521
557
  const report = {
522
- ok: false, reason: '', envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null,
558
+ ok: false, reason: '', envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null, memory: null,
523
559
  };
524
560
  let observed;
525
561
  try {
@@ -550,7 +586,15 @@ function judgeSelfTest(stdout, status, paths, port) {
550
586
  }
551
587
  report.hostPathsVisible = (observed.visible ?? []).filter(path => paths.includes(path));
552
588
  report.connect = observed.connect ?? null;
589
+ report.memory = observed.memory ?? null;
553
590
  const problems = [];
591
+ const memory = report.memory;
592
+ if (memory === null
593
+ || memory.dataSoftBytes !== GATE_PROVIDER_MAX_DATA_BYTES || memory.dataHardBytes !== GATE_PROVIDER_MAX_DATA_BYTES
594
+ || !(memory.heapLimitBytes > 0 && memory.heapLimitBytes <= (GATE_PROVIDER_MAX_OLD_SPACE_MB + 48) * 1024 * 1024)
595
+ || memory.allocationBlocked !== true) {
596
+ problems.push('memory cap not enforced');
597
+ }
554
598
  if (report.envKeys.length)
555
599
  problems.push(`environment leaks ${report.envKeys.join(',')}`);
556
600
  if (report.sharedNamespaces.length)
@@ -565,7 +609,7 @@ function judgeSelfTest(stdout, status, paths, port) {
565
609
  }
566
610
  /**
567
611
  * Runs the sandbox capability self-test asynchronously and returns the
568
- * observations (empty env, private namespaces, no host paths, optional
612
+ * observations (enforced memory caps, empty env, private namespaces, no host paths, optional
569
613
  * loopback connect to a parent listener on `connectPort`).
570
614
  */
571
615
  export function probeIsolationSandbox(options) {
@@ -573,20 +617,20 @@ export function probeIsolationSandbox(options) {
573
617
  let plan;
574
618
  let probe;
575
619
  try {
576
- plan = sandboxCommand(['--no-warnings', '--input-type=module', '-e', SANDBOX_SELF_TEST_SOURCE]);
620
+ plan = sandboxCommand([...MEMORY_NODE_ARGS, '--no-warnings', '--input-type=module', '-e', SANDBOX_SELF_TEST_SOURCE]);
577
621
  probe = selfTestInput(port);
578
622
  }
579
623
  catch (error) {
580
624
  return Promise.resolve({
581
625
  ok: false, reason: error instanceof Error ? error.message : 'sandbox unavailable',
582
- envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null,
626
+ envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null, memory: null,
583
627
  });
584
628
  }
585
629
  return new Promise((resolvePromise) => {
586
630
  const child = spawn(plan.command, plan.args, { env: plan.env, stdio: ['pipe', 'pipe', 'ignore'] });
587
631
  const chunks = [];
588
632
  const timer = setTimeout(() => { try {
589
- child.kill('SIGKILL');
633
+ signalChild(child, 'SIGKILL');
590
634
  }
591
635
  catch { /* exited */ } }, SANDBOX_SELF_TEST_TIMEOUT_MS);
592
636
  child.stdout?.on('data', (chunk) => { chunks.push(chunk); });
@@ -595,7 +639,7 @@ export function probeIsolationSandbox(options) {
595
639
  clearTimeout(timer);
596
640
  resolvePromise({
597
641
  ok: false, reason: `sandbox failed to start: ${error.message}`,
598
- envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null,
642
+ envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null, memory: null,
599
643
  });
600
644
  });
601
645
  child.on('close', (code) => {
@@ -607,15 +651,15 @@ export function probeIsolationSandbox(options) {
607
651
  }
608
652
  /**
609
653
  * Fail-closed gate before every provider run: bwrap must exist and pass the
610
- * capability self-test (cached per bwrap path for the process lifetime).
654
+ * capability self-test (cached per bwrap/prlimit path for the process lifetime).
611
655
  * There is no unsandboxed fallback.
612
656
  */
613
657
  function ensureIsolationSandbox() {
614
- const key = resolveBwrapPath() ?? '<missing>';
658
+ const key = JSON.stringify([resolveBwrapPath(), resolvePrlimitPath()]);
615
659
  if (sandboxVerdict === undefined || sandboxVerdict.key !== key) {
616
660
  let verdict;
617
661
  try {
618
- const plan = sandboxCommand(['--no-warnings', '--input-type=module', '-e', SANDBOX_SELF_TEST_SOURCE]);
662
+ const plan = sandboxCommand([...MEMORY_NODE_ARGS, '--no-warnings', '--input-type=module', '-e', SANDBOX_SELF_TEST_SOURCE]);
619
663
  const probe = selfTestInput(null);
620
664
  const completed = spawnSync(plan.command, plan.args, {
621
665
  env: plan.env, input: probe.input, encoding: 'utf8', timeout: SANDBOX_SELF_TEST_TIMEOUT_MS,
@@ -743,14 +787,14 @@ function runIsolatedRecord(record, args, limits) {
743
787
  }
744
788
  const timer = setTimeout(() => {
745
789
  try {
746
- child.kill('SIGKILL');
790
+ signalChild(child, 'SIGKILL');
747
791
  }
748
792
  catch { /* already exited */ }
749
793
  settle(() => rejectPromise(new GateProviderError(`provider ${record.id} timed out`, 'timeout')));
750
794
  }, limits.timeoutMs);
751
795
  const onAbort = () => {
752
796
  try {
753
- child.kill('SIGKILL');
797
+ signalChild(child, 'SIGKILL');
754
798
  }
755
799
  catch { /* already exited */ }
756
800
  settle(() => rejectPromise(new GateProviderError(`provider ${record.id} was cancelled`, 'cancelled')));
@@ -764,7 +808,7 @@ function runIsolatedRecord(record, args, limits) {
764
808
  if (bytes > limits.maxOutputBytes) {
765
809
  capped = true;
766
810
  try {
767
- child.kill('SIGKILL');
811
+ signalChild(child, 'SIGKILL');
768
812
  }
769
813
  catch { /* already exited */ }
770
814
  settle(() => rejectPromise(new GateProviderError(`provider ${record.id} exceeds its output cap`, 'rejected')));
@@ -0,0 +1,18 @@
1
+ export interface ManagerCommandOptions {
2
+ platform?: NodeJS.Platform;
3
+ env?: NodeJS.ProcessEnv;
4
+ execute?: (file: string, args: string[], options?: Record<string, unknown>) => unknown;
5
+ execOptions?: Record<string, unknown>;
6
+ }
7
+
8
+ export function resolveManagerCommand(
9
+ file: string,
10
+ args: string[],
11
+ options?: ManagerCommandOptions,
12
+ ): { file: string; args: string[] };
13
+
14
+ export function executeManagerCommand(
15
+ file: string,
16
+ args: string[],
17
+ options?: ManagerCommandOptions,
18
+ ): unknown;
@@ -1,7 +1,17 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
 
3
+ const cmdMetaChars = /([()\][%!^"`<>&|;, *?])/g;
4
+
3
5
  function quoteCmdArgument(value) {
4
- return `"${String(value).replace(/%/g, '%%').replace(/"/g, '""')}"`;
6
+ // Match cross-spawn's argument transport: first escape quotes and trailing
7
+ // backslashes for the target's argv parser, then protect cmd metacharacters
8
+ // outside quotes. Doubling % only works in batch source, not a /c operand.
9
+ const argument = String(value)
10
+ .replace(/(?=(\\+?)?)\1"/g, '$1$1\\"')
11
+ .replace(/(?=(\\+?)?)\1$/, '$1$1');
12
+ // Batch wrappers forward operands through another cmd parse (typically %*),
13
+ // so retain one layer of caret escaping for that second parse.
14
+ return `"${argument}"`.replace(cmdMetaChars, '^$1').replace(cmdMetaChars, '^$1');
5
15
  }
6
16
 
7
17
  /**
@@ -17,7 +27,9 @@ export function resolveManagerCommand(file, args, options = {}) {
17
27
 
18
28
  const env = options.env ?? process.env;
19
29
  const commandInterpreter = env.ComSpec || env.COMSPEC || 'cmd.exe';
20
- const command = `"${[file, ...args].map(quoteCmdArgument).join(' ')}"`;
30
+ // The command itself is consumed by the first parse; only its operands need
31
+ // the second escape layer. Escape spaces in the path as well as operators.
32
+ const command = `"${[String(file).replace(cmdMetaChars, '^$1'), ...args.map(quoteCmdArgument)].join(' ')}"`;
21
33
  return {
22
34
  file: commandInterpreter,
23
35
  args: ['/d', '/v:off', '/s', '/c', command],
@@ -0,0 +1,49 @@
1
+ export type InstallationMethod = 'npm' | 'web' | 'source';
2
+ export type InstallationRunMode = 'normal' | 'development';
3
+ export type InstallationChannel = 'stable' | 'next' | 'nightly' | 'edge';
4
+ export interface InstallationIdentity {
5
+ schemaVersion: 1;
6
+ runMode: InstallationRunMode;
7
+ method: InstallationMethod;
8
+ root: string;
9
+ updateStrategy: 'npm-global' | 'signed-web' | 'source-git';
10
+ managerExecutable: string | null;
11
+ channel: InstallationChannel;
12
+ edgePath: string | null;
13
+ checkOnStartup: boolean;
14
+ lastUpdateCheck: number | string | null;
15
+ updateCheckInterval: number;
16
+ recordedAt: string;
17
+ }
18
+ export interface InstallationStatus {
19
+ state: 'aligned' | 'mismatch' | 'stale' | 'unrecorded';
20
+ identity: InstallationIdentity | null;
21
+ canonicalRoot?: string;
22
+ actualRoot: string;
23
+ actualMethod: InstallationMethod;
24
+ /** Root AIWG reads its corpus from. Equals actualRoot unless an edge launcher redirect applies. */
25
+ frameworkRoot: string;
26
+ /** Edge/customize mode only: the running executable's own package root (#2505). */
27
+ launcher: { root: string; method: InstallationMethod } | null;
28
+ drift: string[];
29
+ managerProbe: { state: 'usable' | 'failed'; error?: string } | null;
30
+ }
31
+ export const INSTALLATION_IDENTITY_VERSION: 1;
32
+ export const INSTALLATION_FILE: string;
33
+ export function installationFile(options?: Record<string, unknown>): string;
34
+ export function inferInstallationMethod(root: string): InstallationMethod;
35
+ export function createInstallationIdentity(options: Record<string, any>): InstallationIdentity;
36
+ export function saveInstallationIdentity(identity: InstallationIdentity, options?: Record<string, any>): InstallationIdentity;
37
+ export function loadInstallationIdentity(options?: Record<string, any>): InstallationIdentity | null;
38
+ export function inspectInstallation(options: Record<string, any>): InstallationStatus;
39
+ export interface InstallationCopy {
40
+ root: string;
41
+ executables: string[];
42
+ onPath: boolean;
43
+ registered: boolean;
44
+ }
45
+ export function inspectInstallationCopies(options?: Record<string, any>): InstallationCopy[];
46
+ export function formatInstallationDiagnostic(status: InstallationStatus, options?: { readOnlyCommand?: string }): string;
47
+ export function assertCanonicalInstallation(options: Record<string, any>): InstallationStatus;
48
+ export function adoptInstallation(options: Record<string, any>): InstallationStatus;
49
+ export function switchInstallation(options: Record<string, any>): InstallationStatus;
@@ -5,12 +5,14 @@ import {
5
5
  existsSync,
6
6
  mkdirSync,
7
7
  readFileSync,
8
+ readdirSync,
8
9
  realpathSync,
9
10
  renameSync,
10
11
  statSync,
11
12
  writeFileSync,
12
13
  } from 'node:fs';
13
14
  import path from 'node:path';
15
+ import os from 'node:os';
14
16
  import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
15
17
  import { executeManagerCommand } from './manager-command.mjs';
16
18
 
@@ -269,13 +271,86 @@ export function inspectInstallation(options = {}) {
269
271
  };
270
272
  }
271
273
 
272
- export function formatInstallationDiagnostic(status) {
274
+ /** Inspect executable copies without launching them or changing installation identity. */
275
+ export function inspectInstallationCopies(options = {}) {
276
+ const env = options.env ?? process.env;
277
+ const home = options.homeDir ?? os.homedir();
278
+ const identity = options.identity ?? loadInstallationIdentity({ ...options, createIfMissing: false });
279
+ const copies = new Map();
280
+ const names = process.platform === 'win32' ? ['aiwg', 'aiwg.cmd', 'aiwg.exe'] : ['aiwg'];
281
+
282
+ function addDirectory(directory, onPath) {
283
+ for (const name of names) {
284
+ const executable = path.resolve(directory, name);
285
+ if (!executableIsUsable(executable)) continue;
286
+ let root = path.dirname(canonicalPath(executable));
287
+ while (!['aiwg', '@aiwg/cli'].includes(packageName(root))) {
288
+ const parent = path.dirname(root);
289
+ if (parent === root) { root = null; break; }
290
+ root = parent;
291
+ }
292
+ // npm's Windows command shims are files rather than symlinks.
293
+ if (!root && process.platform === 'win32') {
294
+ const candidate = path.join(directory, 'node_modules', 'aiwg');
295
+ if (packageName(candidate) === 'aiwg') root = canonicalPath(candidate);
296
+ }
297
+ if (!root) continue;
298
+ let copy = copies.get(root);
299
+ if (!copy) {
300
+ const status = identity ? inspectInstallation({ actualRoot: root, identity, createIfMissing: false }) : null;
301
+ copy = {
302
+ root,
303
+ executables: [],
304
+ onPath: false,
305
+ registered: Boolean(status && status.frameworkRoot === status.canonicalRoot),
306
+ };
307
+ copies.set(root, copy);
308
+ }
309
+ if (!copy.executables.includes(executable)) copy.executables.push(executable);
310
+ copy.onPath ||= onPath;
311
+ }
312
+ }
313
+
314
+ const pathDirectories = (env.PATH ?? '').split(path.delimiter);
315
+ for (const directory of new Set(pathDirectories)) addDirectory(directory, true);
316
+
317
+ const nvmRoots = new Set([path.join(home, '.nvm'), env.NVM_DIR].filter(Boolean));
318
+ // A running/registered nvm copy or NVM_BIN also identifies custom nvm homes.
319
+ for (const location of [options.actualRoot, identity?.root, env.NVM_BIN, ...pathDirectories]) {
320
+ if (!location) continue;
321
+ let directory = path.resolve(location);
322
+ while (path.dirname(directory) !== directory) {
323
+ if (path.basename(directory) === 'node' && path.basename(path.dirname(directory)) === 'versions') {
324
+ nvmRoots.add(path.dirname(path.dirname(directory)));
325
+ break;
326
+ }
327
+ directory = path.dirname(directory);
328
+ }
329
+ }
330
+ for (const nvmRoot of nvmRoots) {
331
+ const versions = path.join(nvmRoot, 'versions', 'node');
332
+ try {
333
+ for (const version of readdirSync(versions, { withFileTypes: true })) {
334
+ if (version.isDirectory()) addDirectory(path.join(versions, version.name, 'bin'), false);
335
+ }
336
+ } catch { /* nvm is optional and may no longer be installed. */ }
337
+ }
338
+ return [...copies.values()];
339
+ }
340
+
341
+ export function formatInstallationDiagnostic(status, options = {}) {
273
342
  if (status.state === 'aligned') return 'Canonical installation is aligned.';
274
343
  return [
275
- 'AIWG installation identity drift detected; update and refresh are blocked.',
344
+ options.readOnlyCommand
345
+ ? `installation identity drift detected; \`${options.readOnlyCommand}\` is read-only and continues, but update, refresh, and deployment stay blocked until the identity is repaired.`
346
+ : 'AIWG installation identity drift detected; update and refresh are blocked.',
347
+ `Running installation: ${status.actualMethod} at ${status.actualRoot}`,
348
+ `Registered installation: ${status.identity?.method ?? 'unrecorded'} at ${status.identity?.root ?? '(none)'}`,
276
349
  ...status.drift.map((item) => `- ${item}`),
350
+ 'Diagnose PATH and nvm copies: aiwg doctor',
277
351
  'Inspect: aiwg installation show',
278
- 'Adopt this installation: aiwg installation adopt',
352
+ `Register this running copy deliberately: aiwg installation adopt --method ${status.actualMethod} --yes`,
353
+ 'To keep the registered install, put its executable first on PATH; remove unwanted npm copies using npm from their Node prefix (npm uninstall -g aiwg).',
279
354
  'Switch deliberately: aiwg installation switch --root <path> --method <npm|web|source> [--manager <absolute-path>]',
280
355
  ].join('\n');
281
356
  }
@@ -310,9 +310,9 @@ async function handleClose(issues, rest, args) {
310
310
  }
311
311
  async function handleIndex(issues, rest, args) {
312
312
  if (rest[0] !== 'rebuild')
313
- throw new Error('Usage: aiwg issue index rebuild');
314
- const index = await issues.rebuildIssueIndex();
315
- printJsonOrText(args, index, `Rebuilt local issue index (${index.issues.length} issues)`);
313
+ throw new Error('Usage: aiwg issue index rebuild [--force]');
314
+ const index = await issues.rebuildIssueIndex({ force: args.flags.has('force') });
315
+ printJsonOrText(args, index, `Local issue index ready (${index.issues.length} issues)`);
316
316
  }
317
317
  function requiredStringFlag(args, flag) {
318
318
  const value = stringFlag(args, flag);
@@ -406,6 +406,6 @@ function printUsage() {
406
406
  aiwg issue sync map-comments <KEY> --map-file path [--json]
407
407
  aiwg issue comment <KEY> --body "..." [--author name]
408
408
  aiwg issue close <KEY> [--reason "..."]
409
- aiwg issue index rebuild`);
409
+ aiwg issue index rebuild [--force]`);
410
410
  }
411
411
  //# sourceMappingURL=cli.js.map