@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,7 +17,13 @@ import { createHash } from 'crypto';
17
17
  import { createRequire } from 'module';
18
18
  import { importImpl } from '../../_resolve-impl.mjs';
19
19
 
20
+ const { parseRuleFrontmatter, inferRuleActivation } = await importImpl(import.meta.url, 'rules/activation.mjs');
21
+ const { resolveRulesInlineBudget, DEFAULT_RULES_INLINE_BUDGET_TOKENS } = await importImpl(import.meta.url, 'rules/inline-budget.mjs');
22
+ export { DEFAULT_RULES_INLINE_BUDGET_TOKENS, parseRuleFrontmatter as parseRuleActivation };
20
23
  const { boundedExecFileSync } = await importImpl(import.meta.url, 'process/bounded-child-process.mjs');
24
+ const { extractFrameworkSlug, sameSkillOwner, canDeploySkill, pruneRecordedSkillFiles, releaseSkillOwnership } =
25
+ await importImpl(import.meta.url, 'providers/skill-ownership.mjs');
26
+ export { extractFrameworkSlug, sameSkillOwner, pruneRecordedSkillFiles, releaseSkillOwnership };
21
27
 
22
28
  // Use graceful-fs to prevent EMFILE crashes on systems with low ulimit.
23
29
  // graceful-fs queues open() calls when FD pressure is detected and retries
@@ -243,8 +249,9 @@ export function updateSidecarManifest(dir, deployedEntries, opts) {
243
249
  const existing = readSidecarManifest(dir) || { managed: {} };
244
250
 
245
251
  for (const entry of deployedEntries) {
246
- const { filename, hash, frameworkSlug, kind } = entry;
252
+ const { filename, hash, frameworkSlug, kind, sourcePath } = entry;
247
253
  const sidecarEntry = { hash: `sha256:${hash}`, source, version };
254
+ if (sourcePath) sidecarEntry.sourcePath = sourcePath;
248
255
  if (frameworkSlug) sidecarEntry.frameworkSlug = frameworkSlug;
249
256
  // `kind` marks artifacts whose lifecycle is governed elsewhere — currently
250
257
  // only `skill-command` wrappers, which follow their source skill (#2507).
@@ -255,26 +262,6 @@ export function updateSidecarManifest(dir, deployedEntries, opts) {
255
262
  writeSidecarManifest(dir, existing, dryRun);
256
263
  }
257
264
 
258
- // ============================================================================
259
- // Cross-Framework Collision Detection (#1169)
260
- // ============================================================================
261
-
262
- /**
263
- * Extract the framework slug from a source file path.
264
- *
265
- * Recognizes paths under `agentic/code/frameworks/<slug>/...` and
266
- * `agentic/code/addons/<slug>/...`. Returns null for paths outside
267
- * those namespaces (operator-authored bundles, addons under
268
- * `.aiwg/addons/`, etc.) — those don't get collision-tracked.
269
- */
270
- export function extractFrameworkSlug(srcPath) {
271
- if (typeof srcPath !== 'string') return null;
272
- // Normalize separators for cross-platform matching
273
- const normalized = srcPath.replace(/\\/g, '/');
274
- const m = normalized.match(/agentic\/code\/(?:frameworks|addons)\/([^/]+)\//);
275
- return m ? m[1] : null;
276
- }
277
-
278
265
  // ============================================================================
279
266
  // Model Configuration
280
267
  // ============================================================================
@@ -762,7 +749,7 @@ export function deployFiles(files, destDir, opts, transformFn) {
762
749
 
763
750
  // Read and transform source content (needed for content-equality check
764
751
  // and the collision-vs-duplicate distinction)
765
- const srcContent = fs.readFileSync(f, 'utf8');
752
+ const srcContent = injectBoundRules(fs.readFileSync(f, 'utf8'), f, opts);
766
753
  let transformedContent = transformFn ? transformFn(f, srcContent, opts) : srcContent;
767
754
 
768
755
  // Rule triggers are index metadata, not something the reading agent needs.
@@ -901,7 +888,7 @@ export function deployFiles(files, destDir, opts, transformFn) {
901
888
  if (dryRun) console.log(`[dry-run] deploy ${a.src} -> ${a.dest} (${a.reason})`);
902
889
  else writeFile(a.dest, a.content, false);
903
890
  if (verbose) console.log(`deployed ${path.basename(a.src)} -> ${path.relative(process.cwd(), a.dest)} (${a.reason})`);
904
- deployedEntries.push({ filename: path.basename(a.dest), hash: a.hash, frameworkSlug: a.frameworkSlug });
891
+ deployedEntries.push({ filename: path.basename(a.dest), hash: a.hash, frameworkSlug: a.frameworkSlug, sourcePath: path.resolve(a.src) });
905
892
  } else if (a.type === 'skip') {
906
893
  if (verbose) console.log(`skip (${a.reason}): ${path.basename(a.dest)}`);
907
894
  // Preserve existing sidecar entries for skipped files
@@ -1113,7 +1100,7 @@ export function deploySkillsWithKernelRouting(
1113
1100
  // Standard-dir cleanup (#1217): when we're NOT copying standard
1114
1101
  // skills, anything AIWG-named under standardDestDir is a legacy
1115
1102
  // mirror from a rc.13-or-earlier deploy. Prune to clean up.
1116
- let prunedFromStandardDir = 0;
1103
+ let prunedFromStandardDir = pruneMissingStandardSkills(standardDestDir, opts);
1117
1104
  if (
1118
1105
  !copyStandardSkills &&
1119
1106
  standardDestDir &&
@@ -1550,6 +1537,32 @@ export function pruneStaleAiwgFiles(destDir, desiredStems, opts = {}) {
1550
1537
  return removed;
1551
1538
  }
1552
1539
 
1540
+ /** Source-scoped cleanup also works when a removed standard skill is absent from this deploy's input. */
1541
+ function pruneMissingStandardSkills(destDir, opts) {
1542
+ if (!destDir || !opts?.srcRoot || opts.dryRun || !fs.existsSync(destDir)) return 0;
1543
+ const sourceRoot = path.resolve(opts.srcRoot);
1544
+ let pruned = 0;
1545
+ for (const entry of fs.readdirSync(destDir, { withFileTypes: true })) {
1546
+ if (!entry.isDirectory()) continue;
1547
+ const skillDir = path.join(destDir, entry.name);
1548
+ const receipt = readSidecarManifest(skillDir);
1549
+ if (!receipt?.skillSource || !receipt.managed?.['SKILL.md']) continue;
1550
+ const relative = path.relative(sourceRoot, receipt.skillSource);
1551
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) continue;
1552
+ try {
1553
+ fs.statSync(path.join(receipt.skillSource, 'SKILL.md'));
1554
+ continue;
1555
+ } catch (error) {
1556
+ if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') continue;
1557
+ }
1558
+ if (releaseSkillOwnership(skillDir, {
1559
+ frameworkSlug: receipt.managed['SKILL.md'].frameworkSlug,
1560
+ skillSource: receipt.skillSource,
1561
+ }, opts)) pruned++;
1562
+ }
1563
+ return pruned;
1564
+ }
1565
+
1553
1566
  /**
1554
1567
  * Deploy a skill directory (copy recursively).
1555
1568
  *
@@ -1574,6 +1587,10 @@ export function deploySkillDir(skillDir, destDir, opts) {
1574
1587
  }
1575
1588
 
1576
1589
  const destSkillDir = path.join(destDir, skillName);
1590
+ const previous = readSidecarManifest(destSkillDir);
1591
+ const frameworkSlug = extractFrameworkSlug(skillMdPath);
1592
+ if (!canDeploySkill(previous, skillDir, opts)) return;
1593
+ const managed = {};
1577
1594
  if (!dryRun) ensureDir(destSkillDir);
1578
1595
 
1579
1596
  function copyRecursive(src, dest) {
@@ -1601,6 +1618,15 @@ export function deploySkillDir(skillDir, destDir, opts) {
1601
1618
  }
1602
1619
  }
1603
1620
 
1621
+ if (entry.name === 'SKILL.md') srcContent = injectBoundRules(srcContent, srcPath, opts);
1622
+
1623
+ const relative = path.relative(destSkillDir, destPath);
1624
+ managed[relative] = {
1625
+ hash: `sha256:${contentHash(srcContent)}`,
1626
+ source: opts.deploySource || 'bundled',
1627
+ version: opts.deployVersion || 'unknown',
1628
+ ...(frameworkSlug ? { frameworkSlug } : {}),
1629
+ };
1604
1630
  if (fs.existsSync(destPath)) {
1605
1631
  const destContent = fs.readFileSync(destPath, 'utf8');
1606
1632
  if (destContent === srcContent && !force) {
@@ -1620,6 +1646,8 @@ export function deploySkillDir(skillDir, destDir, opts) {
1620
1646
  }
1621
1647
 
1622
1648
  copyRecursive(skillDir, destSkillDir);
1649
+ pruneRecordedSkillFiles(destSkillDir, previous?.managed, new Set(Object.keys(managed)), opts);
1650
+ writeSidecarManifest(destSkillDir, { managed, skillSource: path.resolve(skillDir) }, dryRun);
1623
1651
 
1624
1652
  // Drop a `.aiwg-managed` marker so future cleanup runs can identify
1625
1653
  // AIWG-deployed skills regardless of frontmatter shape (some providers
@@ -2417,15 +2445,8 @@ export function ruleEnforcementLevel(content) {
2417
2445
  * deployment dispatchable; `AIWG_RULES_INLINE_BUDGET_TOKENS=0` disables the
2418
2446
  * budget, any other value overrides it.
2419
2447
  */
2420
- export const DEFAULT_RULES_INLINE_BUDGET_TOKENS = 64_000;
2421
- const RULE_CHARS_PER_TOKEN = 4;
2422
-
2423
2448
  export function resolveRulesInlineBudgetTokens(env = process.env) {
2424
- const raw = env.AIWG_RULES_INLINE_BUDGET_TOKENS;
2425
- if (raw === undefined || raw === '') return DEFAULT_RULES_INLINE_BUDGET_TOKENS;
2426
- const parsed = Number(raw);
2427
- if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_RULES_INLINE_BUDGET_TOKENS;
2428
- return parsed;
2449
+ return resolveRulesInlineBudget({}, env);
2429
2450
  }
2430
2451
 
2431
2452
  /** Sidecar recording which rules the inline budget moved on demand (#2562). */
@@ -2441,96 +2462,13 @@ export function readRuleBudgetSidecar(destDir) {
2441
2462
  }
2442
2463
  }
2443
2464
 
2444
- /**
2445
- * Keep a provider rule directory under the inline budget (#2562).
2446
- *
2447
- * Rules reach the directory over several deploy passes (the framework pass,
2448
- * then one per addon), so the budget is enforced against what is actually on
2449
- * disk after each pass rather than against any single pass's file list. When
2450
- * the inlined set exceeds the budget, the largest HIGH rules (then unlabelled
2451
- * ones; never CRITICAL or the indexes) are removed from the directory and
2452
- * recorded in the sidecar; the next pass skips re-deploying them and the
2453
- * on-demand index lists them as binding rules fetched via `aiwg show rule`.
2454
- * A changed budget invalidates the sidecar so the set is recomputed.
2455
- *
2456
- * Returns `{ demoted, removed }` — all demoted stems (sidecar state) and the
2457
- * stems removed by this call.
2458
- */
2459
- export function reconcileInlineRuleBudget(destDir, budgetTokens = resolveRulesInlineBudgetTokens(), opts = {}) {
2460
- const sidecarPath = path.join(destDir, RULE_BUDGET_SIDECAR);
2461
- if (!budgetTokens || budgetTokens <= 0) {
2462
- if (fs.existsSync(sidecarPath) && !opts.dryRun) fs.rmSync(sidecarPath, { force: true });
2463
- return { demoted: [], removed: [] };
2464
- }
2465
- const previous = readRuleBudgetSidecar(destDir);
2466
- const carried = previous && previous.budgetTokens === budgetTokens ? previous.demoted : [];
2467
- let files = [];
2468
- try {
2469
- files = fs.readdirSync(destDir, { withFileTypes: true })
2470
- .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
2471
- .map((entry) => path.join(destDir, entry.name))
2472
- .filter((file) => {
2473
- const base = path.basename(file);
2474
- if (base === 'RULES-INDEX.md' || base === 'RULES-ONDEMAND.md') return false;
2475
- try { return MANAGED_MARKER_RE.test(fs.readFileSync(file, 'utf8')); } catch { return false; }
2476
- });
2477
- } catch {
2478
- return { demoted: carried, removed: [] };
2479
- }
2480
- const { demoted } = applyInlineRuleBudget(files, budgetTokens);
2481
- const removed = demoted.map((file) => artifactStem(path.basename(file)));
2482
- if (!opts.dryRun) {
2483
- for (const file of demoted) fs.rmSync(file, { force: true });
2484
- }
2485
- const all = [...new Set([...carried, ...removed])].sort();
2486
- if (!opts.dryRun) {
2487
- if (all.length > 0) {
2488
- fs.writeFileSync(sidecarPath, `${JSON.stringify({ budgetTokens, demoted: all }, null, 2)}\n`, 'utf8');
2489
- } else if (fs.existsSync(sidecarPath)) {
2490
- fs.rmSync(sidecarPath, { force: true });
2491
- }
2492
- }
2493
- return { demoted: all, removed };
2465
+ // Compatibility exports: budgets never change selection or retain demotions.
2466
+ export function reconcileInlineRuleBudget(destDir, budgetTokens, opts = {}) {
2467
+ if (!opts.dryRun) fs.rmSync(path.join(destDir, RULE_BUDGET_SIDECAR), { force: true });
2468
+ return { demoted: [], removed: [] };
2494
2469
  }
2495
-
2496
- /**
2497
- * Fit an always-on rule set under the inline budget (#2562).
2498
- *
2499
- * CRITICAL rules and the generated indexes are never demoted. When the
2500
- * remaining set still exceeds the budget, HIGH rules are demoted largest-first
2501
- * (fewest rules leave the always-on set), then rules with no declared level.
2502
- * Demoted rules stay reachable via `aiwg show rule <name>` and are listed in
2503
- * RULES-ONDEMAND.md under their own heading. Returns `{ inline, demoted }`
2504
- * with the original order preserved for `inline`.
2505
- */
2506
- export function applyInlineRuleBudget(ruleFiles, budgetTokens = resolveRulesInlineBudgetTokens()) {
2507
- const files = [...(ruleFiles || [])];
2508
- if (!budgetTokens || budgetTokens <= 0) return { inline: files, demoted: [] };
2509
- const sized = files.map((file) => {
2510
- const base = path.basename(file);
2511
- let content = '';
2512
- try { content = fs.readFileSync(file, 'utf8'); } catch { /* unreadable → keep inline */ }
2513
- const level = base === 'RULES-INDEX.md' || base === 'RULES-ONDEMAND.md' ? 'critical' : ruleEnforcementLevel(content);
2514
- return { file, tokens: Math.ceil(Buffer.byteLength(content, 'utf8') / RULE_CHARS_PER_TOKEN), level };
2515
- });
2516
- let total = sized.reduce((sum, item) => sum + item.tokens, 0);
2517
- if (total <= budgetTokens) return { inline: files, demoted: [] };
2518
- const demotable = sized
2519
- .filter((item) => item.level !== 'critical')
2520
- .sort((a, b) => {
2521
- const rank = (level) => (level === 'high' ? 0 : level === null ? 1 : 2);
2522
- return rank(a.level) - rank(b.level) || b.tokens - a.tokens;
2523
- });
2524
- const demoted = new Set();
2525
- for (const item of demotable) {
2526
- if (total <= budgetTokens) break;
2527
- demoted.add(item.file);
2528
- total -= item.tokens;
2529
- }
2530
- return {
2531
- inline: files.filter((file) => !demoted.has(file)),
2532
- demoted: files.filter((file) => demoted.has(file)),
2533
- };
2470
+ export function applyInlineRuleBudget(ruleFiles) {
2471
+ return { inline: [...(ruleFiles || [])], demoted: [] };
2534
2472
  }
2535
2473
 
2536
2474
  /**
@@ -2544,10 +2482,14 @@ export function isAlwaysOnRule(filePath) {
2544
2482
  const base = path.basename(filePath);
2545
2483
  if (base === 'RULES-INDEX.md' || base === 'RULES-ONDEMAND.md') return true;
2546
2484
  try {
2547
- const lvl = ruleEnforcementLevel(fs.readFileSync(filePath, 'utf8'));
2548
- return lvl !== 'medium' && lvl !== 'low';
2549
- } catch {
2550
- return true; // unreadable → keep (safe default)
2485
+ const content = fs.readFileSync(filePath, 'utf8');
2486
+ const parsed = parseRuleFrontmatter(content);
2487
+ if (!parsed.valid) throw new Error(`${filePath}: ${parsed.diagnostics.map(d => d.message).join('; ')}`);
2488
+ const activation = parsed.rule.tagged ? parsed.rule.activation : inferRuleActivation(ruleEnforcementLevel(content));
2489
+ return activation === 'kernel' || activation === 'paths';
2490
+ } catch (error) {
2491
+ if (error.code === 'ENOENT') return true;
2492
+ throw error;
2551
2493
  }
2552
2494
  }
2553
2495
 
@@ -2565,7 +2507,9 @@ export function listOnDemandRuleFiles(srcRoot, excludeAddons = []) {
2565
2507
  for (const f of listMdFiles(dir)) {
2566
2508
  const b = path.basename(f);
2567
2509
  if (b === 'RULES-INDEX.md' || b === 'RULES-ONDEMAND.md') continue;
2568
- if (!isAlwaysOnRule(f)) out.push(f);
2510
+ const parsed = parseRuleFrontmatter(fs.readFileSync(f, 'utf8'));
2511
+ if (!parsed.valid) throw new Error(`${f}: invalid rule frontmatter`);
2512
+ if (!parsed.rule.tagged && !isAlwaysOnRule(f)) out.push(f);
2569
2513
  }
2570
2514
  };
2571
2515
  for (const addon of discoverAddons(aiwgRoot)) {
@@ -2678,19 +2622,23 @@ export function getAddonRuleFiles(srcRoot, excludeAddons = [], options = {}) {
2678
2622
  for (const addon of addons) {
2679
2623
  if (excludeAddons.includes(addon.name)) continue;
2680
2624
 
2681
- // Skip addons with consolidated rule indexes — their rules are
2682
- // included via assembleRulesIndex() instead of individual files
2683
- if (addon.manifest?.consolidation?.deployIndexOnly) continue;
2684
-
2685
2625
  const rulesDir = path.join(addon.path, 'rules');
2686
2626
  if (fs.existsSync(rulesDir)) {
2687
- files.push(...listMdFiles(rulesDir));
2627
+ for (const file of listMdFiles(rulesDir)) {
2628
+ // Consolidation remains a legacy inventory policy. Explicit activation
2629
+ // owns delivery, including the desired set used by the outer prune.
2630
+ if (addon.manifest?.consolidation?.deployIndexOnly) {
2631
+ const parsed = parseRuleFrontmatter(fs.readFileSync(file, 'utf8'));
2632
+ if (!parsed.valid) throw new Error(`${file}: invalid rule frontmatter`);
2633
+ if (!parsed.rule.tagged) continue;
2634
+ }
2635
+ files.push(file);
2636
+ }
2688
2637
  }
2689
2638
  }
2690
2639
 
2691
- // Tier gate (#1673): inline only always-on (CRITICAL/HIGH) rules. MEDIUM/LOW
2692
- // stay on-demand. Applied here so both the deploy enumeration and the prune
2693
- // desired-set (computeAllArtifactBasenames) exclude them for every provider.
2640
+ // Activation selects bodies; only untagged rules use the old severity tier.
2641
+ // The deploy enumeration and prune desired set must select the same bodies.
2694
2642
  return files.filter(isAlwaysOnRule);
2695
2643
  }
2696
2644
 
@@ -3382,3 +3330,25 @@ export const ProviderInterface = {
3382
3330
  // ── File Extension ────────────────────────────────────────────────
3383
3331
  getFileExtension(type) { return '.md'; }
3384
3332
  };
3333
+
3334
+ /** Replace only the generated binding block; keep authored bytes intact. */
3335
+ export function replaceBoundRulesBlock(content, rules = []) {
3336
+ const clean = content.replace(/\n?<!-- AIWG:bound-rules:start -->[\s\S]*?<!-- AIWG:bound-rules:end -->\n?/g, '');
3337
+ if (!rules.length) return clean;
3338
+ const unique = [...new Map(rules.map(rule => [rule.name, rule])).values()]
3339
+ .sort((a, b) => a.name.localeCompare(b.name));
3340
+ return `${clean}${clean.endsWith('\n') ? '' : '\n'}\n<!-- AIWG:bound-rules:start -->\n## Bound rules\n\nRequired reading on every invocation: fetch and follow every bound rule below before starting governed work. A summary or discovery pointer does not discharge this obligation.\n\n${unique.map(rule =>
3341
+ `- ${rule.name} — ${rule.summary}\n Fetch and follow before governed work: \`aiwg show rule ${rule.name}\`.`
3342
+ ).join('\n')}\n<!-- AIWG:bound-rules:end -->\n`;
3343
+ }
3344
+
3345
+ export function injectBoundRules(content, source, opts = {}) {
3346
+ if (!opts.ruleDelivery) return content;
3347
+ return replaceBoundRulesBlock(content, opts.ruleDelivery.bindings.get(path.resolve(source)) || []);
3348
+ }
3349
+
3350
+ /** All provider output is staged; the complete startup guard runs before promotion. */
3351
+ export async function withRuleDeployment(opts, provider, deploy) {
3352
+ const { deployWithRuleDelivery } = await import('./rule-delivery.mjs');
3353
+ return deployWithRuleDelivery(opts, provider, deploy);
3354
+ }
@@ -29,6 +29,7 @@ import path from 'path';
29
29
  import { tmpdir } from 'os';
30
30
  import { classifyModelRole, modelForRole } from './model-role.mjs';
31
31
  import {
32
+ withRuleDeployment,
32
33
  ensureDir,
33
34
  listMdFiles,
34
35
  listMdFilesRecursive,
@@ -49,9 +50,7 @@ import {
49
50
  getAddonRuleFiles,
50
51
  listOnDemandRuleFiles,
51
52
  writeOnDemandRuleIndex,
52
- readRuleBudgetSidecar,
53
53
  reconcileInlineRuleBudget,
54
- resolveRulesInlineBudgetTokens,
55
54
  assembleRulesIndex,
56
55
  normalizeDeploymentMode,
57
56
  collectFrameworkArtifacts,
@@ -521,6 +520,10 @@ export function getFileExtension(type) {
521
520
  * Orchestrates deployment of agents, commands, skills, and rules
522
521
  */
523
522
  export async function deploy(opts) {
523
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath }, deploySelected);
524
+ }
525
+
526
+ async function deploySelected(opts) {
524
527
  const {
525
528
  srcRoot,
526
529
  target,
@@ -619,7 +622,7 @@ export async function deploy(opts) {
619
622
  // Deploy based on flags — track counts for summary
620
623
  const counts = { agents: 0, commands: 0, skills: 0, rules: 0, souls: 0 };
621
624
 
622
- if (!commandsOnly && !skillsOnly) {
625
+ if (!commandsOnly && !skillsOnly && !rulesOnly) {
623
626
  // Apply filters if specified
624
627
  const filteredAgents = filterAgentFiles(agentFiles, opts);
625
628
  if (verbose && (opts.filter || opts.filterRole)) {
@@ -664,17 +667,7 @@ export async function deploy(opts) {
664
667
  pruneStaleAiwgSkills(kernelDestDir, computeAllKernelNames(srcRoot), opts);
665
668
  }
666
669
 
667
- const inlineBudgetTokens = resolveRulesInlineBudgetTokens();
668
- let demotedRuleNames = [];
669
670
  if (shouldDeployRules || rulesOnly) {
670
- // Inline budget (#2562): rules a previous pass already moved on demand are
671
- // not re-inlined; the directory is reconciled against the budget after
672
- // this pass deploys (see below).
673
- const sidecar = readRuleBudgetSidecar(path.join(target, paths.rules));
674
- if (sidecar && sidecar.budgetTokens === inlineBudgetTokens && sidecar.demoted.length > 0) {
675
- const skip = new Set(sidecar.demoted);
676
- ruleFiles = ruleFiles.filter((f) => !skip.has(path.basename(f).replace(/\.md$/, '')));
677
- }
678
671
  // Try assembled rules index (combines all component indexes)
679
672
  const assembled = assembleRulesIndex(srcRoot);
680
673
  if (assembled) {
@@ -707,17 +700,8 @@ export async function deploy(opts) {
707
700
  // On-demand index (#1673): list the MEDIUM/LOW rules that were tier-gated
708
701
  // out of the always-on set so agents can fetch them via `aiwg show rule`.
709
702
  const rulesDestDir = path.join(target, paths.rules);
710
- // Keep the always-on directory small enough that a subagent dispatch
711
- // still fits (#2562). Demoted HIGH rules remain binding and are listed
712
- // in RULES-ONDEMAND.md with their fetch hint.
713
- const budget = reconcileInlineRuleBudget(rulesDestDir, inlineBudgetTokens, opts);
714
- demotedRuleNames = budget.demoted;
715
- if (budget.removed.length > 0 && !opts.quiet) {
716
- console.log(` Inline rule budget: ${budget.removed.length} HIGH rule(s) moved on demand to fit ${inlineBudgetTokens.toLocaleString()} tokens: ${budget.removed.join(', ')} (see RULES-ONDEMAND.md)`);
717
- }
718
- const onDemandCount = writeOnDemandRuleIndex(rulesDestDir, listOnDemandRuleFiles(srcRoot), {
719
- ...opts, demotedNames: demotedRuleNames, inlineBudgetTokens,
720
- });
703
+ reconcileInlineRuleBudget(rulesDestDir, undefined, opts);
704
+ const onDemandCount = writeOnDemandRuleIndex(rulesDestDir, listOnDemandRuleFiles(srcRoot), opts);
721
705
  if (verbose && onDemandCount > 0) {
722
706
  console.log(` On-demand rules (not inlined): ${onDemandCount} → RULES-ONDEMAND.md`);
723
707
  }
@@ -38,6 +38,7 @@ import { spawn } from 'child_process';
38
38
  import { load as loadYaml } from 'js-yaml';
39
39
  import { classifyModelRole, modelForRole } from './model-role.mjs';
40
40
  import {
41
+ withRuleDeployment,
41
42
  ensureDir,
42
43
  listMdFiles,
43
44
  listMdFilesRecursive,
@@ -269,7 +270,8 @@ export async function deployCommands(targetDir, srcRoot, opts) {
269
270
 
270
271
  const child = spawn('node', [scriptPath, ...args], {
271
272
  stdio: 'inherit',
272
- cwd: helperRoot
273
+ cwd: helperRoot,
274
+ env: opts.deploymentHome ? { ...process.env, HOME: opts.deploymentHome } : process.env
273
275
  });
274
276
 
275
277
  child.on('close', (code) => {
@@ -318,7 +320,8 @@ export async function deploySkills(targetDir, srcRoot, opts) {
318
320
 
319
321
  const child = spawn('node', [scriptPath, ...args], {
320
322
  stdio: 'inherit',
321
- cwd: helperRoot
323
+ cwd: helperRoot,
324
+ env: opts.deploymentHome ? { ...process.env, HOME: opts.deploymentHome } : process.env
322
325
  });
323
326
 
324
327
  child.on('close', (code) => {
@@ -332,7 +335,7 @@ export async function deploySkills(targetDir, srcRoot, opts) {
332
335
  // Self-heal: prune AIWG-managed skill dirs left behind in the legacy
333
336
  // ~/.codex/skills/ home location by pre-fix versions, so codex-rs stops
334
337
  // listing each skill twice.
335
- pruneLegacyCodexSkills(opts);
338
+ pruneLegacyCodexSkills(opts, opts.legacyCodexSkills || legacyHomeSkillsDir);
336
339
  }
337
340
 
338
341
  /**
@@ -398,17 +401,16 @@ export function deployRules(ruleFiles, targetDir, opts) {
398
401
  * Aggregate agents to single AGENTS.md file
399
402
  */
400
403
  export function aggregateToAgentsMd(agentFiles, destPath, opts) {
401
- const blocks = [];
402
- for (const f of agentFiles) {
403
- let content = fs.readFileSync(f, 'utf8');
404
- content = transformAgent(f, content, opts);
405
- if (!content.endsWith('\n')) content += '\n';
406
- blocks.push(content);
404
+ const lines = ['# AIWG agent definitions', '', 'Read the selected definition before invoking an agent.', ''];
405
+ for (const file of agentFiles) {
406
+ const { metadata } = parseFrontmatter(fs.readFileSync(file, 'utf8'));
407
+ const name = metadata.name || path.basename(file, '.md');
408
+ lines.push(`- ${name}: .codex/agents/${path.basename(file, '.md')}.toml`);
407
409
  }
408
- const out = blocks.join('\n');
410
+ const out = `${lines.join('\n')}\n`;
409
411
  if (opts.dryRun) console.log(`[dry-run] write ${destPath}`);
410
412
  else fs.writeFileSync(destPath, out, 'utf8');
411
- console.log(`wrote ${path.relative(process.cwd(), destPath)} with ${agentFiles.length} agents`);
413
+
412
414
  }
413
415
 
414
416
  // ============================================================================
@@ -536,6 +538,10 @@ export function getFileExtension(type) {
536
538
  * Main deployment function for Codex provider
537
539
  */
538
540
  export async function deploy(opts) {
541
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath }, deploySelected);
542
+ }
543
+
544
+ async function deploySelected(opts) {
539
545
  const {
540
546
  srcRoot,
541
547
  target,
@@ -628,6 +634,7 @@ export async function deploy(opts) {
628
634
  // Aggregate to single AGENTS.md
629
635
  const destPath = path.join(target, 'AGENTS.md');
630
636
  console.log(`\nAggregating ${agentFiles.length} agents to AGENTS.md...`);
637
+ deployAgents(agentFiles, target, opts);
631
638
  aggregateToAgentsMd(agentFiles, destPath, opts);
632
639
  } else {
633
640
  console.log(`\nDeploying ${agentFiles.length} agents...`);
@@ -27,6 +27,8 @@ try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs;
27
27
  import path from 'path';
28
28
  import { spawn } from 'child_process';
29
29
  import {
30
+ withRuleDeployment,
31
+ parseRuleActivation,
30
32
  ensureDir,
31
33
  listMdFiles,
32
34
  listMdFilesRecursive,
@@ -117,25 +119,20 @@ export function transformCommand(srcPath, content, opts) {
117
119
  return content;
118
120
  }
119
121
 
120
- /**
121
- * Transform rule content for Cursor — inject MDC frontmatter with the
122
- * activation mode mapping per ADR-2 (rule activation mode schema).
123
- *
124
- * Reads the source rule's `activation:` field (or defaults to alwaysApply
125
- * per ADR-2 §2 default-preservation) and emits the corresponding MDC
126
- * frontmatter fields:
127
- * - alwaysApply (true): activation === 'alwaysApply' (default)
128
- * - alwaysApply (false), no globs: activation === 'auto'
129
- * - globs: activation === 'glob' + globs: '<pattern>'
130
- * - alwaysApply (false), description: activation === 'manual'
131
- *
132
- * Per ADR-2 §5: until the live-Cursor smoke test gate is in CI, all rules
133
- * deploy with alwaysApply: true regardless of source `activation` value
134
- * (with a deploy-time warning when the source declared a non-alwaysApply
135
- * mode). The deploy-time warning is emitted by the deployer, not this
136
- * transform.
137
- */
122
+ /** Compile the shared five-mode activation contract to native MDC. */
138
123
  export function transformRule(srcPath, content, opts) {
124
+ const parsed = parseRuleActivation(content);
125
+ if (!parsed.valid) throw new Error(`${srcPath}: ${parsed.diagnostics.map(d => d.message).join('; ')}`);
126
+ if (parsed.rule.tagged) {
127
+ const rule = parsed.rule;
128
+ // Bound/on-demand rules have no auto-selection description or glob hint.
129
+ const fields = rule.activation === 'paths'
130
+ ? `globs: ${JSON.stringify(rule.paths)}\nalwaysApply: false`
131
+ : rule.activation === 'summary'
132
+ ? `description: ${JSON.stringify(rule.summary)}\nalwaysApply: false`
133
+ : `alwaysApply: ${rule.activation === 'kernel'}`;
134
+ return `---\n${fields}\n---\n${parsed.body}`;
135
+ }
139
136
  // Detect proper YAML frontmatter — must close within the first 30 lines.
140
137
  // Files with `---` horizontal rules deeper in markdown content (like
141
138
  // RULES-INDEX.md) can have multiple `---` lines without being frontmatter;
@@ -451,6 +448,10 @@ export function getFileExtension(type) {
451
448
  * Orchestrates deployment of agents, commands, skills, and rules
452
449
  */
453
450
  export async function deploy(opts) {
451
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath, transformRule }, deploySelected);
452
+ }
453
+
454
+ async function deploySelected(opts) {
454
455
  const {
455
456
  srcRoot,
456
457
  target,
@@ -216,7 +216,9 @@ function regularFiles(dir, prefix = '') {
216
216
  function matchesSkillSource(source, target) {
217
217
  try {
218
218
  const expected = regularFiles(source);
219
- const actual = regularFiles(target).filter(name => name !== '.aiwg-managed');
219
+ // The marker and the per-skill ownership receipt are AIWG bookkeeping,
220
+ // not skill content, so neither makes a deployed skill look modified.
221
+ const actual = regularFiles(target).filter(name => name !== '.aiwg-managed' && name !== '.aiwg-manifest.json');
220
222
  if (JSON.stringify(expected) !== JSON.stringify(actual)) return false;
221
223
  return expected.every(relative => {
222
224
  if (relative.endsWith(path.sep)) return true;