@aiwg/cli 2026.10.3 → 2026.10.4

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 +57 -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 +285 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +65 -0
  23. package/dist/src/artifacts/index-refresh.js +576 -0
  24. package/dist/src/artifacts/index-worker.mjs +9 -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 +98 -37
  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 +82 -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 +51 -11
  207. package/dist/src/sessions/contracts.js +40 -2
  208. package/dist/src/sessions/coverage-preflight.js +1 -1
  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 +133 -25
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +106 -0
  218. package/dist/src/sessions/repository.js +147 -52
  219. package/dist/src/sessions/workspace-discovery.js +221 -56
  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
@@ -1,5 +1,6 @@
1
1
  import { constants } from 'fs';
2
- import { mkdir, open, readFile, readdir, rename, unlink, writeFile } from 'fs/promises';
2
+ import { mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from 'fs/promises';
3
+ import { createHash } from 'node:crypto';
3
4
  import { basename, dirname, join, relative, resolve, sep } from 'path';
4
5
  import { hostname } from 'os';
5
6
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
@@ -328,15 +329,32 @@ export class LocalIssueProviderCore {
328
329
  const idx = events.findIndex((event) => event.event_id === since);
329
330
  return idx >= 0 ? events.slice(idx + 1) : events;
330
331
  }
331
- async rebuildIssueIndex() {
332
+ async rebuildIssueIndex(options = {}) {
332
333
  await this.ensureLayout();
333
334
  return this.lockManager.withIndexLock('rebuild issue index', async () => {
334
335
  const itemDir = this.path('items');
335
- const entries = await readdir(itemDir, { withFileTypes: true }).catch(() => []);
336
+ const entries = (await readdir(itemDir, { withFileTypes: true }))
337
+ .filter(entry => entry.isFile() && entry.name.endsWith('.md'))
338
+ .sort((a, b) => a.name.localeCompare(b.name));
339
+ const inputs = await Promise.all(entries.map(async (entry) => {
340
+ const metadata = await stat(join(itemDir, entry.name));
341
+ return [entry.name, metadata.mtimeMs, metadata.ctimeMs, metadata.size];
342
+ }));
343
+ const inputFingerprint = createHash('sha256').update(JSON.stringify([1, inputs])).digest('hex');
344
+ if (!options.force) {
345
+ try {
346
+ const cached = JSON.parse(await readFile(this.path('index/issues.index.json'), 'utf-8'));
347
+ if (cached?.provider === 'local' && Array.isArray(cached.issues) && cached.inputFingerprint === inputFingerprint) {
348
+ return cached;
349
+ }
350
+ }
351
+ catch (error) {
352
+ if (!(error instanceof SyntaxError) && !(isNodeError(error) && error.code === 'ENOENT'))
353
+ throw error;
354
+ }
355
+ }
336
356
  const issues = [];
337
357
  for (const entry of entries) {
338
- if (!entry.isFile() || !entry.name.endsWith('.md'))
339
- continue;
340
358
  const record = parseIssueMarkdown(await readFile(join(itemDir, entry.name), 'utf-8'));
341
359
  issues.push({
342
360
  id: record.fields.id,
@@ -355,6 +373,7 @@ export class LocalIssueProviderCore {
355
373
  const index = {
356
374
  provider: 'local',
357
375
  rebuilt_at: new Date().toISOString(),
376
+ inputFingerprint,
358
377
  issues,
359
378
  };
360
379
  await atomicWrite(this.path('index/issues.index.json'), `${JSON.stringify(index, null, 2)}\n`);
@@ -184,7 +184,7 @@ async function acquireLock(stateRoot, flow) {
184
184
  const lock = JSON.parse(await fs.readFile(file, 'utf8'));
185
185
  const tooOld = !lock.startedAt || Date.now() - Date.parse(lock.startedAt) > (flow.spec.workItem.claimTtlSeconds ?? 900) * 1000;
186
186
  let alive = false;
187
- if (typeof lock.pid === 'number') {
187
+ if (Number.isSafeInteger(lock.pid) && (lock.pid ?? 0) > 0) {
188
188
  try {
189
189
  process.kill(lock.pid, 0);
190
190
  alive = true;
@@ -13,6 +13,7 @@
13
13
  * @issue #810
14
14
  */
15
15
  import fs from 'fs';
16
+ import { readAiwgConfigCandidate } from '../config/aiwg-config.js';
16
17
  import path from 'path';
17
18
  import { fileURLToPath } from 'url';
18
19
  import { discoverRulesets } from './loader.js';
@@ -71,9 +72,15 @@ function parseArgs(args) {
71
72
  else if (arg === '--format' && i + 1 < args.length) {
72
73
  options.format = args[++i];
73
74
  }
75
+ else if (arg === '--startup-plan' && i + 1 < args.length) {
76
+ options.startupPlan = args[++i];
77
+ }
74
78
  else if (arg === '--ci') {
75
79
  options.ci = true;
76
80
  }
81
+ else if (arg === '--allow-empty') {
82
+ options.allowEmpty = true;
83
+ }
77
84
  else if (arg === '--fail-on' && i + 1 < args.length) {
78
85
  options.failOn = args[++i];
79
86
  }
@@ -102,6 +109,14 @@ function parseArgs(args) {
102
109
  export async function main(args) {
103
110
  const options = parseArgs(args);
104
111
  const cwd = process.cwd();
112
+ if (args.includes('--help') || args.includes('-h')) {
113
+ console.log('Usage: aiwg lint <target> [--ruleset <name>] [--format full|summary|json] [--ci] [--allow-empty] [--fail-on error|warn|info] [--no-gitignore]');
114
+ console.log(' --ci Fail on lint violations, zero files checked or zero rules applied');
115
+ console.log(' --allow-empty Allow zero files or zero applied rules under --ci');
116
+ console.log(' --startup-plan <json> Guard a complete projected startup set { provider, files: [{ path, bytes | content }] }');
117
+ console.log(' --list-rulesets | --list-rules <ruleset>');
118
+ return;
119
+ }
105
120
  // Discover available rulesets. `cwd` is where the user runs the lint (used
106
121
  // to find deployed `.aiwg/frameworks/*/lint/` overlays); `AIWG_ROOT` is the
107
122
  // AIWG install (used to find shipped `agentic/code/frameworks/*/lint/`).
@@ -126,7 +141,7 @@ export async function main(args) {
126
141
  }
127
142
  // Require a target for actual linting
128
143
  if (!options.target) {
129
- console.error('Usage: aiwg lint <target> [--ruleset <name>] [--format full|summary|json] [--ci] [--fail-on error|warn|info] [--no-gitignore]');
144
+ console.error('Usage: aiwg lint <target> [--ruleset <name>] [--format full|summary|json] [--ci] [--allow-empty] [--fail-on error|warn|info] [--no-gitignore]');
130
145
  console.error(' aiwg lint --list-rulesets');
131
146
  console.error(' aiwg lint --list-rules <ruleset>');
132
147
  process.exitCode = 1;
@@ -159,9 +174,20 @@ export async function main(args) {
159
174
  }
160
175
  return;
161
176
  }
177
+ let startupBudget;
178
+ if (options.startupPlan) {
179
+ const plan = JSON.parse(fs.readFileSync(path.resolve(cwd, options.startupPlan), 'utf8'));
180
+ if (!plan || typeof plan.provider !== 'string' || !Array.isArray(plan.files)) {
181
+ throw new Error('Startup plan must contain provider and files (the complete assembled startup set).');
182
+ }
183
+ startupBudget = { provider: plan.provider, files: plan.files, config: await readAiwgConfigCandidate(cwd) ?? {} };
184
+ }
162
185
  // Run lint
163
186
  const result = await runLint(targetDir, selectedRulesets, {
187
+ startupBudget,
164
188
  recursive: options.recursive,
189
+ ci: options.ci,
190
+ allowEmpty: options.allowEmpty,
165
191
  failOn: options.failOn,
166
192
  respectGitignore: options.respectGitignore ?? true,
167
193
  });
@@ -8,6 +8,7 @@
8
8
  * @issue #810
9
9
  */
10
10
  import fs from 'fs';
11
+ import { ruleActivationRuleset } from './rule-activation.js';
11
12
  import fsp from 'fs/promises';
12
13
  import path from 'path';
13
14
  import { projectAiwgPath } from '../config/project-artifacts.js';
@@ -178,7 +179,7 @@ export async function loadRuleset(lintDir, frameworkId) {
178
179
  * 2. Deployed frameworks: .aiwg/frameworks/{id}/lint/
179
180
  */
180
181
  export async function discoverRulesets(cwd, frameworkRoot) {
181
- const rulesets = [];
182
+ const rulesets = [ruleActivationRuleset];
182
183
  // Check framework source directories
183
184
  const frameworksDir = path.join(frameworkRoot, 'agentic', 'code', 'frameworks');
184
185
  if (fs.existsSync(frameworksDir)) {
@@ -31,14 +31,33 @@ function formatDiagnostic(d) {
31
31
  /**
32
32
  * Format lint results in full detail
33
33
  */
34
+ function formatBudget(result) {
35
+ const report = result.inlineBudget;
36
+ if (!report)
37
+ return [];
38
+ return [
39
+ `Startup budget (${report.provider}): limit ${report.limit} tokens${report.disabled ? ' (guard disabled)' : ''}; selected ${report.selected_tokens} estimated tokens (${report.selected_bytes} bytes; ${report.estimate})`,
40
+ ...report.contributors.map(file => ` ${file.path}: ${file.bytes} bytes (${file.estimated_tokens} estimated tokens)`),
41
+ ];
42
+ }
34
43
  function formatFull(result) {
35
44
  const lines = [];
36
45
  lines.push(`${BOLD}AIWG Lint${RESET}`);
37
46
  lines.push(`Target: ${result.target}`);
38
47
  lines.push(`Rulesets: ${result.rulesets.join(', ')}`);
39
48
  lines.push(`Files checked: ${result.summary.filesChecked}`);
49
+ lines.push(...formatBudget(result));
40
50
  lines.push(`Rules applied: ${result.summary.rulesApplied} of ${result.summary.rulesSelected}`);
41
51
  lines.push('');
52
+ if (result.summary.coverageMessage)
53
+ lines.push(`! ${result.summary.coverageMessage}`);
54
+ if (result.ruleCoverage) {
55
+ lines.push('Rule coverage (files by rule):');
56
+ for (const [ruleId, files] of Object.entries(result.ruleCoverage)) {
57
+ lines.push(` ${ruleId}: ${files.length ? files.join(', ') : '(no files)'}`);
58
+ }
59
+ lines.push('');
60
+ }
42
61
  // A target that excludes every rule produces the same clean output as a
43
62
  // target that passes them, which is how 34 findings stayed invisible across a
44
63
  // long session (#2555). Say so before the verdict.
@@ -92,9 +111,14 @@ function formatFull(result) {
92
111
  * Format lint results as a summary (counts only)
93
112
  */
94
113
  function formatSummary(result) {
95
- const lines = [];
114
+ const lines = [...formatBudget(result)];
96
115
  lines.push(`Lint: ${result.summary.filesChecked} files, ${result.summary.errors} errors, ${result.summary.warnings} warnings, ${result.summary.infos} info`);
97
116
  lines.push(`Rules applied: ${result.summary.rulesApplied} of ${result.summary.rulesSelected}`);
117
+ if (result.summary.coverageMessage)
118
+ lines.push(`! ${result.summary.coverageMessage}`);
119
+ if (result.summary.inapplicableRules.length > 0) {
120
+ lines.push(`Inapplicable rules: ${result.summary.inapplicableRules.join(', ')}`);
121
+ }
98
122
  if (result.summary.rulesSelected > 0 && result.summary.rulesApplied === 0) {
99
123
  lines.push(`! No rule applies to this target — this is not a clean result (#2555).`);
100
124
  }
@@ -0,0 +1,43 @@
1
+ import { loadLocalCapabilityEntries, resolveRuleBindingTarget } from '../artifacts/query-engine.js';
2
+ import { parseRuleFrontmatter, RULE_BINDING_TYPES } from '../rules/activation.mjs';
3
+ export const ruleActivationRuleset = {
4
+ id: 'rule-activation', name: 'Rule activation', framework: 'core', version: '1.0.0',
5
+ description: 'Validate rule and behavior activation contracts and exact indexed bindings',
6
+ rules: [{
7
+ id: 'rules/activation', name: 'Rule activation contract', severity: 'error',
8
+ description: 'Validate activation, summaries, paths, bindings, and migration fallback',
9
+ appliesTo: { glob: '**/{rules/**/*.md,behaviors/**/BEHAVIOR.md}' },
10
+ checks: [{ type: 'rule-activation' }],
11
+ }],
12
+ };
13
+ export function lintRuleActivation(content, file, projectRoot, entries) {
14
+ // Generated structural indexes are not authored rules.
15
+ if (/^(?:RULES-INDEX|RULES-ONDEMAND)\.md$/.test(file.split('/').pop()))
16
+ return [];
17
+ const result = parseRuleFrontmatter(content);
18
+ const diagnostics = result.diagnostics.map(item => ({
19
+ ruleId: 'rules/activation', ruleName: 'Rule activation contract',
20
+ file, severity: item.severity, message: item.message,
21
+ }));
22
+ if (!result.rule)
23
+ return diagnostics;
24
+ const bindings = result.rule.bound_to;
25
+ if (!Object.values(bindings).some(list => list.length > 0))
26
+ return diagnostics;
27
+ // Reuse existing artifact index ownership and show precedence. No title guessing,
28
+ // corpus directory resolver, or extra binding registry.
29
+ const indexed = entries ?? loadLocalCapabilityEntries(projectRoot);
30
+ for (const key of Object.keys(RULE_BINDING_TYPES)) {
31
+ for (const id of bindings[key]) {
32
+ try {
33
+ resolveRuleBindingTarget(indexed, RULE_BINDING_TYPES[key], id);
34
+ }
35
+ catch (error) {
36
+ diagnostics.push({ ruleId: 'rules/activation', ruleName: 'Rule activation contract',
37
+ file, severity: 'error', message: error instanceof Error ? error.message : String(error) });
38
+ }
39
+ }
40
+ }
41
+ return diagnostics;
42
+ }
43
+ //# sourceMappingURL=rule-activation.js.map
@@ -7,27 +7,54 @@
7
7
  * @issue #810
8
8
  */
9
9
  import fs from 'fs';
10
- import { boundedSpawnSync } from '../process/bounded-child-process.mjs';
10
+ import { lintRuleActivation } from './rule-activation.js';
11
+ import { guardRulesInlineBudget } from '../rules/inline-budget.mjs';
12
+ import { dropGitignored } from '../config/gitignore.js';
11
13
  import fsp from 'fs/promises';
12
14
  import path from 'path';
13
15
  import { glob } from 'glob';
14
16
  import { minimatch } from 'minimatch';
17
+ import { parse } from 'yaml';
15
18
  /**
16
19
  * Parse YAML-style frontmatter from a markdown file
17
20
  * Returns key-value pairs from the --- delimited block
18
21
  */
19
22
  function parseFrontmatter(content) {
20
- const match = content.match(/^---\n([\s\S]*?)\n---/);
23
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
21
24
  if (!match)
22
25
  return {};
23
- const result = {};
24
- for (const line of match[1].split('\n')) {
25
- const kv = line.match(/^(\w[\w-]*)\s*:\s*(.+)$/);
26
- if (kv) {
27
- result[kv[1]] = kv[2].trim().replace(/^["']|["']$/g, '');
26
+ try {
27
+ const parsed = parse(match[1]);
28
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
29
+ ? parsed
30
+ : {};
31
+ }
32
+ catch {
33
+ // Invalid YAML cannot supply required metadata.
34
+ return {};
35
+ }
36
+ }
37
+ function isPresent(value) {
38
+ if (value == null)
39
+ return false;
40
+ if (typeof value === 'string')
41
+ return value.trim().length > 0;
42
+ if (Array.isArray(value))
43
+ return value.length > 0;
44
+ return true;
45
+ }
46
+ /** Resolve project-relative metadata from the nearest repository/workspace marker. */
47
+ function findProjectRoot(targetDir) {
48
+ let dir = path.resolve(targetDir);
49
+ while (true) {
50
+ if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, '.aiwg'))) {
51
+ return dir;
28
52
  }
53
+ const parent = path.dirname(dir);
54
+ if (parent === dir)
55
+ return path.resolve(targetDir);
56
+ dir = parent;
29
57
  }
30
- return result;
31
58
  }
32
59
  const DEFAULT_REFERENCE_PATTERN = '\\bREF-\\d{3,}\\b';
33
60
  /**
@@ -215,8 +242,9 @@ function buildReferenceIndex(allFiles) {
215
242
  * Preserve support for references stored outside the lint target when a rule
216
243
  * explicitly supplies basePath. Accept both exact and slugged filenames.
217
244
  */
218
- function resolvesFromBasePath(refId, targetDir, basePath) {
245
+ function resolvesFromBasePath(refId, targetDir, basePath, projectRoot) {
219
246
  const candidateDirs = new Set([
247
+ path.resolve(projectRoot, basePath),
220
248
  path.resolve(targetDir, basePath),
221
249
  path.resolve(process.cwd(), basePath),
222
250
  ]);
@@ -237,14 +265,14 @@ function resolvesFromBasePath(refId, targetDir, basePath) {
237
265
  /**
238
266
  * Run a single check against a file's content and frontmatter
239
267
  */
240
- function runCheck(check, content, frontmatter, filePath, targetDir, allFiles, referenceIndex) {
268
+ function runCheck(check, content, frontmatter, filePath, targetDir, allFiles, referenceIndex, projectRoot) {
241
269
  const diagnostics = [];
242
270
  switch (check.type) {
243
271
  case 'frontmatter-required': {
244
272
  if (!check.fields)
245
273
  break;
246
274
  for (const field of check.fields) {
247
- if (!frontmatter[field] || frontmatter[field].trim() === '') {
275
+ if (!isPresent(frontmatter[field])) {
248
276
  diagnostics.push({
249
277
  ruleId: '',
250
278
  ruleName: '',
@@ -261,7 +289,7 @@ function runCheck(check, content, frontmatter, filePath, targetDir, allFiles, re
261
289
  if (!check.field || !check.pattern)
262
290
  break;
263
291
  const value = frontmatter[check.field];
264
- if (value && !new RegExp(check.pattern).test(value)) {
292
+ if (isPresent(value) && typeof value !== 'object' && !new RegExp(check.pattern).test(String(value))) {
265
293
  diagnostics.push({
266
294
  ruleId: '',
267
295
  ruleName: '',
@@ -282,7 +310,9 @@ function runCheck(check, content, frontmatter, filePath, targetDir, allFiles, re
282
310
  let match;
283
311
  while ((match = regex.exec(lines[i])) !== null) {
284
312
  const refId = match[0];
285
- const basePath = check.basePath || '.aiwg/research/findings';
313
+ const basePaths = check.basePath
314
+ ? [check.basePath]
315
+ : ['.aiwg/research/findings', 'findings', 'documentation/references', '.'];
286
316
  // A corpus documents its deliberate gaps ("REF-2464 remains
287
317
  // unallocated after deduplication"). Reading that sentence as a
288
318
  // dangling reference left only two ways to silence an error: delete
@@ -290,7 +320,7 @@ function runCheck(check, content, frontmatter, filePath, targetDir, allFiles, re
290
320
  // absent (#2555).
291
321
  if (absence.some((re) => re.test(lines[i])))
292
322
  continue;
293
- if (!referenceIndex.has(refId) && !resolvesFromBasePath(refId, targetDir, basePath)) {
323
+ if (!referenceIndex.has(refId) && !basePaths.some(basePath => resolvesFromBasePath(refId, targetDir, basePath, projectRoot))) {
294
324
  diagnostics.push({
295
325
  ruleId: '',
296
326
  ruleName: '',
@@ -376,9 +406,12 @@ function runCheck(check, content, frontmatter, filePath, targetDir, allFiles, re
376
406
  if (!check.field)
377
407
  break;
378
408
  const refPath = frontmatter[check.field];
379
- if (refPath) {
380
- const resolved = path.resolve(targetDir, refPath);
381
- if (!fs.existsSync(resolved)) {
409
+ if (isPresent(refPath)) {
410
+ // Project-root-relative is canonical (#3145); target-relative paths
411
+ // written for the legacy layout keep resolving.
412
+ const found = typeof refPath === 'string' &&
413
+ [projectRoot, targetDir].some(base => fs.existsSync(path.resolve(base, refPath)));
414
+ if (!found) {
382
415
  diagnostics.push({
383
416
  ruleId: '',
384
417
  ruleName: '',
@@ -398,7 +431,8 @@ function runCheck(check, content, frontmatter, filePath, targetDir, allFiles, re
398
431
  if (!check.pattern)
399
432
  break;
400
433
  const basename = path.basename(filePath, '.md');
401
- if (!new RegExp(check.pattern).test(basename)) {
434
+ const match = basename.match(new RegExp(check.pattern));
435
+ if (!match) {
402
436
  diagnostics.push({
403
437
  ruleId: '',
404
438
  ruleName: '',
@@ -408,6 +442,20 @@ function runCheck(check, content, frontmatter, filePath, targetDir, allFiles, re
408
442
  fix: `Rename to match the pattern ${check.pattern}`,
409
443
  });
410
444
  }
445
+ else if (check.field && isPresent(frontmatter[check.field])) {
446
+ const identifier = match[1] ?? match[0];
447
+ const value = frontmatter[check.field];
448
+ if (value !== identifier) {
449
+ diagnostics.push({
450
+ ruleId: '',
451
+ ruleName: '',
452
+ severity: 'warn',
453
+ file: filePath,
454
+ message: `File identifier '${identifier}' does not match frontmatter field '${check.field}': ${String(value)}`,
455
+ fix: `Make the filename identifier and '${check.field}' field agree`,
456
+ });
457
+ }
458
+ }
411
459
  break;
412
460
  }
413
461
  case 'cross-ref-bidirectional': {
@@ -473,15 +521,13 @@ export function ruleAppliesToFile(ruleGlob, file, targetDir) {
473
521
  /**
474
522
  * Run a single rule against all matching files in the target
475
523
  */
476
- async function runRule(rule, targetDir, allFiles, referenceIndex) {
524
+ async function runRule(rule, targetDir, allFiles, referenceIndex, projectRoot, artifactEntries) {
477
525
  const diagnostics = [];
478
- RULE_APPLIED.delete(rule.id);
479
526
  // Filter files matching the rule's glob
480
527
  const ruleGlob = rule.appliesTo.glob;
481
528
  const matchingFiles = allFiles.filter(f => ruleAppliesToFile(ruleGlob, f, targetDir));
482
529
  if (matchingFiles.length === 0)
483
- return diagnostics;
484
- RULE_APPLIED.add(rule.id);
530
+ return { diagnostics, files: matchingFiles };
485
531
  // Handle id-unique check at the ruleset level
486
532
  const uniqueCheck = rule.checks.find(c => c.type === 'id-unique');
487
533
  if (uniqueCheck) {
@@ -516,7 +562,11 @@ async function runRule(rule, targetDir, allFiles, referenceIndex) {
516
562
  for (const check of rule.checks) {
517
563
  if (check.type === 'id-unique')
518
564
  continue; // Already handled
519
- const checkDiagnostics = runCheck(check, content, frontmatter, file, targetDir, allFiles, referenceIndex);
565
+ if (check.type === 'rule-activation') {
566
+ diagnostics.push(...lintRuleActivation(content, file, projectRoot, artifactEntries));
567
+ continue;
568
+ }
569
+ const checkDiagnostics = runCheck(check, content, frontmatter, file, targetDir, allFiles, referenceIndex, projectRoot);
520
570
  for (const d of checkDiagnostics) {
521
571
  d.ruleId = rule.id;
522
572
  d.ruleName = rule.name;
@@ -525,10 +575,8 @@ async function runRule(rule, targetDir, allFiles, referenceIndex) {
525
575
  }
526
576
  }
527
577
  }
528
- return diagnostics;
578
+ return { diagnostics, files: matchingFiles };
529
579
  }
530
- /** Rule ids that selected at least one file in the current run (#2555). */
531
- const RULE_APPLIED = new Set();
532
580
  /**
533
581
  * Collect all files under a target directory
534
582
  */
@@ -547,37 +595,6 @@ async function collectFiles(targetDir, recursive, respectGitignore = true) {
547
595
  return [];
548
596
  }
549
597
  }
550
- /**
551
- * Drop files git ignores (#2555).
552
- *
553
- * Generated trees are regenerated, not authored: linting `indices/` reported an
554
- * error in text that documents a deliberate gap. `git check-ignore` is asked
555
- * once for the whole set so the semantics are git's own (negations, nested
556
- * `.gitignore`, `core.excludesFile`) rather than a re-implementation. Outside a
557
- * repository, or without git, every file is kept.
558
- */
559
- function dropGitignored(targetDir, files) {
560
- if (files.length === 0)
561
- return files;
562
- try {
563
- const result = boundedSpawnSync('git', ['check-ignore', '--stdin'], 'quick', {
564
- cwd: targetDir,
565
- input: `${files.join('\n')}\n`,
566
- encoding: 'utf8',
567
- });
568
- // 0 = some paths ignored, 1 = none ignored, anything else = not a repo or
569
- // git unavailable, in which case nothing is filtered.
570
- if (result.error || (result.status !== 0 && result.status !== 1))
571
- return files;
572
- const ignored = new Set((result.stdout ?? '').split('\n').map((line) => line.trim()).filter(Boolean));
573
- if (ignored.size === 0)
574
- return files;
575
- return files.filter((file) => !ignored.has(file));
576
- }
577
- catch {
578
- return files;
579
- }
580
- }
581
598
  /**
582
599
  * Auto-detect which rulesets apply to a target path
583
600
  *
@@ -602,27 +619,74 @@ function autoDetectRulesets(target, available) {
602
619
  }
603
620
  }
604
621
  // If no auto-detection, return all available
605
- return matches.length > 0 ? matches : available;
622
+ return matches.length > 0
623
+ ? available.filter(ruleset => ruleset.id === 'rule-activation' || matches.includes(ruleset))
624
+ : available;
606
625
  }
607
626
  /**
608
627
  * Run lint on a target path with specified or auto-detected rulesets
609
628
  */
610
629
  export async function runLint(targetDir, rulesets, options = {}) {
611
630
  const recursive = options.recursive ?? true;
612
- const allFiles = await collectFiles(targetDir, recursive, options.respectGitignore ?? true);
631
+ const targetStat = await fsp.stat(targetDir).catch(() => null);
632
+ const scanDir = targetStat?.isFile() ? path.dirname(targetDir) : targetDir;
633
+ const projectRoot = findProjectRoot(scanDir);
634
+ const respectGitignore = options.respectGitignore ?? true;
635
+ let allFiles;
636
+ let targetMessage = '';
637
+ if (!targetStat) {
638
+ allFiles = [];
639
+ targetMessage = `Target does not exist or is inaccessible: ${targetDir}.`;
640
+ }
641
+ else if (targetStat.isFile()) {
642
+ const file = path.basename(targetDir);
643
+ if (!/\.(md|yaml|yml|json)$/.test(file)) {
644
+ allFiles = [];
645
+ targetMessage = `Unsupported file type: ${file}. Expected .md, .yaml, .yml or .json.`;
646
+ }
647
+ else {
648
+ allFiles = respectGitignore ? dropGitignored(scanDir, [file]) : [file];
649
+ }
650
+ }
651
+ else {
652
+ allFiles = await collectFiles(scanDir, recursive, respectGitignore);
653
+ }
613
654
  const referenceIndex = buildReferenceIndex(allFiles);
614
655
  const allDiagnostics = [];
615
656
  const inapplicableRules = [];
657
+ const ruleCoverage = {};
616
658
  let rulesSelected = 0;
617
659
  for (const ruleset of rulesets) {
618
660
  for (const rule of ruleset.rules) {
619
661
  rulesSelected += 1;
620
- const diagnostics = await runRule(rule, targetDir, allFiles, referenceIndex);
621
- if (!RULE_APPLIED.has(rule.id))
662
+ const { diagnostics, files } = await runRule(rule, scanDir, allFiles, referenceIndex, projectRoot, options.artifactEntries);
663
+ ruleCoverage[rule.id] = files;
664
+ if (files.length === 0)
622
665
  inapplicableRules.push(rule.id);
623
666
  allDiagnostics.push(...diagnostics);
624
667
  }
625
668
  }
669
+ let budget;
670
+ if (options.startupBudget) {
671
+ try {
672
+ budget = guardRulesInlineBudget({
673
+ ...options.startupBudget, reportOnly: options.ci ? false : (options.startupBudget.reportOnly ?? true),
674
+ });
675
+ }
676
+ catch (error) {
677
+ allDiagnostics.push({
678
+ ruleId: 'rules/inline-budget', ruleName: 'Rules inline budget', file: '(assembled startup)',
679
+ severity: 'error', message: error instanceof Error ? error.message : String(error),
680
+ });
681
+ }
682
+ }
683
+ if (budget) {
684
+ for (const item of budget.diagnostics)
685
+ allDiagnostics.push({
686
+ ruleId: 'rules/inline-budget', ruleName: 'Rules inline budget', file: '(assembled startup)',
687
+ severity: item.severity, message: item.message,
688
+ });
689
+ }
626
690
  const errors = allDiagnostics.filter(d => d.severity === 'error').length;
627
691
  const warnings = allDiagnostics.filter(d => d.severity === 'warn').length;
628
692
  const infos = allDiagnostics.filter(d => d.severity === 'info').length;
@@ -634,10 +698,20 @@ export async function runLint(targetDir, rulesets, options = {}) {
634
698
  passed = false;
635
699
  if (failOn === 'info' && allDiagnostics.length > 0)
636
700
  passed = false;
701
+ const rulesApplied = rulesSelected - inapplicableRules.length;
702
+ const coverageMessage = allFiles.length === 0
703
+ ? `No files checked. ${targetMessage || 'The target is empty, contains no supported files, or all files are ignored.'}`
704
+ : rulesApplied === 0
705
+ ? 'No rules applied. Check the target path and selected ruleset globs.'
706
+ : undefined;
707
+ if (options.ci && !options.allowEmpty && coverageMessage)
708
+ passed = false;
637
709
  return {
638
710
  target: targetDir,
639
711
  rulesets: rulesets.map(r => r.id),
640
712
  diagnostics: allDiagnostics,
713
+ ...(budget ? { inlineBudget: budget.report } : {}),
714
+ ruleCoverage,
641
715
  summary: {
642
716
  filesChecked: allFiles.length,
643
717
  errors,
@@ -647,8 +721,9 @@ export async function runLint(targetDir, rulesets, options = {}) {
647
721
  // A run where no rule selected a file must never read the same as a clean
648
722
  // run (#2555). Reporters surface this; callers can gate on it.
649
723
  rulesSelected,
650
- rulesApplied: rulesSelected - inapplicableRules.length,
724
+ rulesApplied,
651
725
  inapplicableRules,
726
+ coverageMessage,
652
727
  },
653
728
  timestamp: new Date().toISOString(),
654
729
  };
@@ -14,6 +14,7 @@
14
14
  *
15
15
  * @implements #892
16
16
  */
17
+ import { signalChild } from '../../process/signal-child.mjs';
17
18
  import { readFile, mkdir, symlink, access, readdir, chmod, lstat, open } from 'fs/promises';
18
19
  import { join } from 'path';
19
20
  import { homedir } from 'os';
@@ -27,6 +28,7 @@ import { assertCredentialPolicy, resolveCredentialPolicy } from '../credentials.
27
28
  import { planToolFilters } from '../tool-filters.mjs';
28
29
  import { assertConfigDestination, writeConfigAtomic } from '../config-file.mjs';
29
30
  import { stripMcpServers } from '../toml-strip-mcp.mjs';
31
+ import { resolveUserConfigDir } from '../../config/user-config-dir.mjs';
30
32
  const DEFAULT_SHARED_STATE = {
31
33
  // Shared: operator history and session index are cross-profile
32
34
  shared: ['history.jsonl', 'sessions'],
@@ -174,6 +176,9 @@ export function launchWithProfile(profile, extraArgs = [], options = {}) {
174
176
  ...process.env,
175
177
  HOME: rtHome,
176
178
  CODEX_HOME: rtHome,
179
+ // Resolve before HOME changes so the child retains the operator's
180
+ // installation identity, channel and other provider-neutral configuration.
181
+ AIWG_CONFIG: resolveUserConfigDir(),
177
182
  };
178
183
  if (isNonInteractiveCodexInvocation(extraArgs)) {
179
184
  // Bounded (#3062): `codex exec -` never hands over the terminal; the
@@ -338,7 +343,7 @@ function spawnCodexLoginWithApiKey(apiKey, env) {
338
343
  child.removeListener('exit', onExit);
339
344
  if (result.error && child.exitCode === null && child.signalCode === null) {
340
345
  try {
341
- child.kill('SIGKILL');
346
+ signalChild(child, 'SIGKILL');
342
347
  }
343
348
  catch { /* already gone */ }
344
349
  }
@@ -411,6 +416,7 @@ export async function loginInProfile(profile, options = {}) {
411
416
  ...process.env,
412
417
  HOME: rtHome,
413
418
  CODEX_HOME: rtHome,
419
+ AIWG_CONFIG: resolveUserConfigDir(),
414
420
  };
415
421
  // `options.apiKey` (go/no-go review, blocker 1C) is the already-resolved
416
422
  // key from `--api-key-stdin`/`--api-key-file` — the caller (`src/mcp/
@@ -0,0 +1,4 @@
1
+ import type { ConfigWrite } from './config-file.mjs';
2
+ export function claudeDesktopConfigPath(platform?: string, env?: NodeJS.ProcessEnv): string;
3
+ export function resolveClaudeInstallTarget(target: string, scope: string, env?: NodeJS.ProcessEnv): string;
4
+ export function legacyClaudeMigration(projectDir: string): Promise<ConfigWrite[]>;