@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,4 +1,6 @@
1
+ import { signalChild } from '../../process/signal-child.mjs';
1
2
  import { execFile } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
2
4
  import { promisify } from 'node:util';
3
5
  import { setTimeout as delay } from 'node:timers/promises';
4
6
  import path from 'node:path';
@@ -29,7 +31,7 @@ async function execCanonicalSystemctl(exec, args, options) {
29
31
 
30
32
  /** Parse only explicit stop proof. Missing/unknown properties are observation
31
33
  * failures, never evidence of termination. A collected unit has no exit result. */
32
- export function parseShellUnitStatus(stdout) {
34
+ export function parseShellUnitStatus(stdout, { stopAccepted = false } = {}) {
33
35
  const fields = Object.fromEntries(String(stdout).split('\n').filter(line => line.includes('='))
34
36
  .map(line => { const at = line.indexOf('='); return [line.slice(0, at), line.slice(at + 1)]; }));
35
37
  if (fields.LoadState === 'not-found' && fields.ActiveState === 'inactive' && fields.MainPID === '0') {
@@ -42,17 +44,31 @@ export function parseShellUnitStatus(stdout) {
42
44
  throw new Error('worker status unavailable: missing or invalid systemd properties');
43
45
  }
44
46
  if (!['inactive', 'failed'].includes(fields.ActiveState) || fields.MainPID !== '0') return uncertain();
47
+ // An inactive unit may still have a queued start. Only an accepted stop
48
+ // followed by this fresh observation can prove a never-executed unit ended.
49
+ if (fields.ExecMainCode === '0' && stopAccepted) {
50
+ return { termination: 'confirmed', exitCode: null, neverExecuted: true };
51
+ }
52
+ if (!['1', '2', '3'].includes(fields.ExecMainCode)) return uncertain();
45
53
  const signaled = ['2', '3'].includes(fields.ExecMainCode);
46
- return { termination: 'confirmed', exitCode: fields.Result === 'success' && !signaled
54
+ return { termination: 'confirmed', nativeResult: fields.Result, exitCode: fields.Result === 'success' && !signaled
47
55
  ? Number(fields.ExecMainStatus) : Number(fields.ExecMainStatus) || 1,
48
56
  ...(signaled ? { signalCode: Number(fields.ExecMainStatus) } : {}) };
49
57
  }
50
58
 
59
+ function boundedDeadline(deadlineMs, deadlineAtMs, now) {
60
+ if (deadlineAtMs !== undefined && (!Number.isFinite(deadlineAtMs)
61
+ || (deadlineAtMs >= 1e12) !== (now >= 1e12))) {
62
+ throw new RangeError('Worker deadlineAtMs must use the same clock as nowMs (or clock); supply the matching clock');
63
+ }
64
+ return Math.min(deadlineAtMs ?? Infinity, now + deadlineMs);
65
+ }
66
+
51
67
  export async function waitForUnitTermination(unit, deadlineMs, { signal, pollIntervalMs = 2000,
52
68
  exec = execFileAsync, sleep = (ms, abortSignal) => delay(ms, undefined, { signal: abortSignal }),
53
- clock = Date.now } = {}) {
69
+ clock = () => performance.now(), deadlineAtMs, stopAccepted = false } = {}) {
54
70
  if (typeof unit !== 'string' || !/^[A-Za-z0-9:_.@-]+$/.test(unit)) throw new Error('Worker isolation requires a unit-safe unit name');
55
- const deadline = clock() + deadlineMs;
71
+ const deadline = boundedDeadline(deadlineMs, deadlineAtMs, clock());
56
72
  while (!signal?.aborted && clock() < deadline) {
57
73
  try {
58
74
  const { stdout } = await execCanonicalSystemctl(exec,
@@ -61,7 +77,7 @@ export async function waitForUnitTermination(unit, deadlineMs, { signal, pollInt
61
77
  // overall grace/deadline even when a probe is still in flight.
62
78
  { timeout: Math.max(1000, Math.min(10000, deadline - clock())), signal });
63
79
  if (signal?.aborted) return uncertain();
64
- const outcome = parseShellUnitStatus(stdout);
80
+ const outcome = parseShellUnitStatus(stdout, { stopAccepted });
65
81
  if (outcome.termination === 'confirmed') return outcome;
66
82
  } catch (error) {
67
83
  // systemctl may return nonzero for a collected unit while still showing
@@ -86,7 +102,8 @@ export async function waitForUnitTermination(unit, deadlineMs, { signal, pollInt
86
102
  * after leader reap to limit (not eliminate) the PGID reuse race. */
87
103
  export function buildShellStopControl(launch, { waitForUnit = waitForUnitTermination,
88
104
  stopUnit = defaultStopShellUnit, killUnit = defaultKillShellUnit, graceMs,
89
- onConfirmed, platform = process.platform, recheckMaxAttempts = 6, recheckDelayMs = 250 }) {
105
+ onConfirmed, platform = process.platform, recheckMaxAttempts = 6, recheckDelayMs = 250,
106
+ nowMs = () => performance.now() }) {
90
107
  let closed = false;
91
108
  let exitCode = null;
92
109
  let signalCode = null;
@@ -94,6 +111,7 @@ export function buildShellStopControl(launch, { waitForUnit = waitForUnitTermina
94
111
  let stopTask;
95
112
  let delivery;
96
113
  let stopped = false;
114
+ let stopAccepted = false;
97
115
  let recheckTimer;
98
116
  let rechecks = 0;
99
117
  let disposed = false;
@@ -101,11 +119,21 @@ export function buildShellStopControl(launch, { waitForUnit = waitForUnitTermina
101
119
  let resolveClose;
102
120
  const probes = new Set();
103
121
  const child = launch.child;
104
- const ownedGroup = platform !== 'win32' && launch.ownedGroup && child?.pid;
122
+ const ownedGroup = platform !== 'win32' && launch.ownedGroup;
105
123
  const closeProof = new Promise(resolve => { resolveClose = resolve; });
106
124
 
125
+ function safeGroupTarget() {
126
+ if (!Number.isSafeInteger(child?.pid) || child.pid <= 1 || child.pid === process.pid) return false;
127
+ if (platform !== 'linux') return true;
128
+ try {
129
+ const stat = readFileSync('/proc/self/stat', 'utf8');
130
+ const pgid = Number(stat.slice(stat.lastIndexOf(')') + 2).split(/\s+/)[2]);
131
+ return Number.isSafeInteger(pgid) && child.pid !== pgid;
132
+ } catch { return false; }
133
+ }
107
134
  function groupGone() {
108
135
  if (!ownedGroup) return true;
136
+ if (!safeGroupTarget()) return false;
109
137
  try { process.kill(-child.pid, 0); return false; } catch (error) { return error.code === 'ESRCH'; }
110
138
  }
111
139
  function confirm(outcome) {
@@ -128,10 +156,10 @@ export function buildShellStopControl(launch, { waitForUnit = waitForUnitTermina
128
156
  if (groupGone()) confirm({ termination: 'confirmed', exitCode, signalCode });
129
157
  else void control.stop(); // descendants outlived their leader
130
158
  }
131
- function onError() {
159
+ function onError(error) {
132
160
  // A pid-bearing child error is an observation failure, not a deadline.
133
161
  // ENOENT has no pid and cannot have started a worker.
134
- if (!child?.pid && launch.mode !== 'systemd-unit') confirm({ termination: 'confirmed', exitCode: null });
162
+ if (!child?.pid && launch.mode !== 'systemd-unit') confirm({ termination: 'confirmed', exitCode: null, errorCode: error?.code ?? 'SPAWN_ERROR' });
135
163
  }
136
164
  child?.once('close', onClose);
137
165
  child?.on('error', onError);
@@ -155,10 +183,11 @@ export function buildShellStopControl(launch, { waitForUnit = waitForUnitTermina
155
183
  abort.abort(); // cancel a losing poll or execFile, including its sleep
156
184
  }
157
185
  }
158
- async function waitStopped(ms) {
186
+ async function waitStopped(ms, deadline) {
159
187
  if (confirmed) return confirmed;
160
188
  if (launch.mode === 'systemd-unit') {
161
- const outcome = await bounded(signal => waitForUnit(launch.unit, ms, { signal }), ms);
189
+ const outcome = await bounded(signal => waitForUnit(launch.unit, ms, { signal, stopAccepted,
190
+ ...(deadline ? { deadlineAtMs: deadline.at, clock: deadline.now } : {}) }), ms);
162
191
  return outcome.termination === 'confirmed' ? confirm(outcome) : outcome;
163
192
  }
164
193
  if (closed && groupGone()) return confirm({ termination: 'confirmed', exitCode, signalCode });
@@ -181,15 +210,17 @@ export function buildShellStopControl(launch, { waitForUnit = waitForUnitTermina
181
210
  if (launch.mode === 'systemd-unit') {
182
211
  const result = await bounded(abortSignal => (signalName === 'SIGTERM' ? stopUnit : killUnit)(launch.unit,
183
212
  { signal: abortSignal, timeout: Math.max(1, budgetMs) }), budgetMs);
213
+ if (signalName === 'SIGTERM' && result.ok) stopAccepted = true;
184
214
  return !!result.ok;
185
215
  }
186
216
  if (ownedGroup) {
217
+ if (!safeGroupTarget()) return false;
187
218
  if ((closed || child.exitCode != null || child.signalCode != null) && groupSwept) return false;
188
219
  if (signalName === 'SIGKILL') groupSwept = true;
189
220
  process.kill(-child.pid, signalName);
190
221
  return true;
191
222
  }
192
- return !closed && !!child?.kill(signalName);
223
+ return !closed && !!signalChild(child, signalName);
193
224
  } catch { return false; }
194
225
  }
195
226
  const control = {
@@ -205,15 +236,15 @@ export function buildShellStopControl(launch, { waitForUnit = waitForUnitTermina
205
236
  if (stopTask) return delivery;
206
237
  stopped = true;
207
238
  const phaseMs = launch.mode === 'systemd-unit' ? Math.max(1000, graceMs) : graceMs;
208
- let phaseEnd = Date.now() + phaseMs;
239
+ let phaseEnd = nowMs() + phaseMs;
209
240
  delivery = signal('SIGTERM', phaseMs);
210
241
  stopTask = (async () => {
211
242
  await delivery;
212
- let outcome = await waitStopped(Math.max(0, phaseEnd - Date.now()));
243
+ let outcome = await waitStopped(Math.max(0, phaseEnd - nowMs()));
213
244
  if (outcome.termination !== 'confirmed') {
214
- phaseEnd = Date.now() + phaseMs;
245
+ phaseEnd = nowMs() + phaseMs;
215
246
  await signal('SIGKILL', phaseMs);
216
- outcome = await waitStopped(Math.max(0, phaseEnd - Date.now()));
247
+ outcome = await waitStopped(Math.max(0, phaseEnd - nowMs()));
217
248
  }
218
249
  if (outcome.termination !== 'confirmed') recheck();
219
250
  return outcome;
@@ -224,16 +255,16 @@ export function buildShellStopControl(launch, { waitForUnit = waitForUnitTermina
224
255
  await control.stop();
225
256
  return confirmed ?? (stopTask ? await stopTask : uncertain());
226
257
  },
227
- wait: async deadlineMs => {
228
- const deadline = Date.now() + deadlineMs;
258
+ wait: async (deadlineMs, { deadlineAtMs, nowMs: waitNow = nowMs } = {}) => {
259
+ const deadline = boundedDeadline(deadlineMs, deadlineAtMs, waitNow());
229
260
  let outcome;
230
261
  do {
231
- outcome = await waitStopped(Math.max(0, deadline - Date.now()));
262
+ outcome = await waitStopped(Math.max(0, deadline - waitNow()), { at: deadline, now: waitNow });
232
263
  if (confirmed) return confirmed;
233
264
  // Observation failure alone never mandates a stop. Avoid a hot loop
234
265
  // when an injected/failed observer returns immediately.
235
- if (!outcome.timedOut) await Promise.race([closeProof, delay(Math.min(50, Math.max(0, deadline - Date.now())))]);
236
- } while (!disposed && !outcome.timedOut && Date.now() < deadline);
266
+ if (!outcome.timedOut) await Promise.race([closeProof, delay(Math.min(50, Math.max(0, deadline - waitNow())))]);
267
+ } while (!disposed && !outcome.timedOut && waitNow() < deadline);
237
268
  if (disposed) return uncertain();
238
269
  return { ...await control.stopAndConfirm(), timedOut: true };
239
270
  },
@@ -10,7 +10,7 @@ export async function sweepWorkerStdin(root = tmpdir(), { staleMs = 86400000, no
10
10
  try {
11
11
  for (const name of await readdir(root)) {
12
12
  const match = /^aiwg-worker-stdin-(\d+)-[A-Za-z0-9]+$/.exec(name);
13
- if (!match || isAlive(Number(match[1]))) continue;
13
+ if (!match || !Number.isSafeInteger(Number(match[1])) || Number(match[1]) <= 0 || isAlive(Number(match[1]))) continue;
14
14
  const directory = path.join(root, name);
15
15
  try {
16
16
  const stat = await lstat(directory);
@@ -40,7 +40,7 @@ export async function prepareWorkerStdin(bytes, root = tmpdir()) {
40
40
  }
41
41
 
42
42
  export async function launchPreparedShellWorker(invocation, { allowDegraded, config, name, live, readers, thresholdOverlay,
43
- memoryMaxGb, driver, servedRoot, isolation, spawn, spawnAsync, stdinRuntimeRoot },
43
+ memoryMaxGb, driver, servedRoot, isolation, spawn, spawnAsync, stdinRuntimeRoot, admission, invocationId, dispatchGuard },
44
44
  { loadRalphExternal, readBoundedFile, importImpl, describeGateFailures }) {
45
45
  let attempt;
46
46
  let cleanup = () => Promise.resolve();
@@ -58,7 +58,7 @@ export async function launchPreparedShellWorker(invocation, { allowDegraded, con
58
58
  ({ stdinPath, cleanup } = await prepareWorkerStdin(bytes, stdinRuntimeRoot));
59
59
  }
60
60
  const launch = await planDirectWorkerLaunch({ invocation, config, allowDegraded, name, live, readers,
61
- thresholdOverlay, memoryMaxGb, stdinPath, ownedGroup: true, isolation,
61
+ thresholdOverlay, memoryMaxGb, stdinPath, ownedGroup: true, isolation, admission, invocationId, dispatchGuard,
62
62
  spawn, spawnAsync, onSpawnAttempt: value => {
63
63
  attempt = value;
64
64
  value.child?.once('close', (exitCode, signalCode) => { Object.assign(value, { closed: true, exitCode, signalCode }); });
@@ -410,6 +410,14 @@ export function assertWorkerTerminal(observation) {
410
410
  throw new WorkerContractError('OG_CONTRACT_INVALID', 'Worker terminal evidence is missing, erroneous or uncertain; exit zero alone is insufficient.');
411
411
  }
412
412
  }
413
+ export function assertWorkerAttemptTerminal(observation) {
414
+ if (!observation || !['shell', 'mcp'].includes(observation.transport)
415
+ || observation.status !== 'completed' || observation.attemptOutcome !== 'succeeded'
416
+ || (observation.transport === 'shell' ? observation.exitCode !== 0 : observation.exitCode !== null && observation.exitCode !== 0)
417
+ || (observation.isError !== undefined && observation.isError !== false)) {
418
+ throw new WorkerContractError('OG_CONTRACT_INVALID', 'Worker attempt evidence is missing, erroneous or uncertain; exit zero alone is insufficient.');
419
+ }
420
+ }
413
421
  /**
414
422
  * Assemble detached, immutable evidence from separately attributed producers.
415
423
  * artifactBytes are read by the conductor from the exact artifact (or protected
@@ -6,9 +6,11 @@
6
6
  * @architecture @.aiwg/architecture/enhanced-model-selection-design.md
7
7
  */
8
8
  import { readFile } from 'fs/promises';
9
- import { resolve } from 'path';
9
+ import { dirname, resolve } from 'path';
10
+ import { fileURLToPath } from 'node:url';
10
11
  import { homedir } from 'os';
11
12
  import { existsSync } from 'fs';
13
+ import { findPackageRoot } from '../cli/find-package-root.js';
12
14
  import { workerConfigDigest, validateWorkerGroupReferences } from '../config/aiwg-config.js';
13
15
  import { DEFAULT_WORKER_SELECTION, mergeWorkerSelection, WorkerSelectionPolicySchema, validateUserProjectModelConfig } from './provider-policy.js';
14
16
  /**
@@ -20,7 +22,10 @@ export class ConfigLoader {
20
22
  inputDigests = null;
21
23
  locations;
22
24
  constructor(projectPath, options) {
23
- const aiwgHome = options?.aiwgHome || process.env.AIWG_HOME || resolve(homedir(), '.local/share/ai-writing-guide');
25
+ const aiwgHome = options?.aiwgHome || process.env.AIWG_HOME
26
+ || findPackageRoot(dirname(fileURLToPath(import.meta.url)));
27
+ if (!aiwgHome)
28
+ throw new Error('Failed to load AIWG default config: could not locate the running AIWG package root');
24
29
  const userConfigDir = process.env.AIWG_CONFIG || resolve(homedir(), '.config/aiwg');
25
30
  this.locations = {
26
31
  aiwgDefault: resolve(aiwgHome, 'agentic/code/frameworks/sdlc-complete/config/models-v2.json'),
@@ -208,6 +208,28 @@
208
208
  }
209
209
  },
210
210
  "claude": {
211
+ "sessionEnvAllowlist": [
212
+ "ANTHROPIC_API_KEY",
213
+ "ANTHROPIC_AUTH_TOKEN",
214
+ "ANTHROPIC_BASE_URL",
215
+ "ANTHROPIC_MODEL",
216
+ "CLAUDE_CONFIG_DIR",
217
+ "CLAUDE_CODE_OAUTH_TOKEN",
218
+ "CLAUDE_CODE_DISABLE_1M_CONTEXT",
219
+ "CLAUDE_CODE_USE_BEDROCK",
220
+ "CLAUDE_CODE_USE_VERTEX",
221
+ "AWS_PROFILE",
222
+ "AWS_REGION",
223
+ "AWS_DEFAULT_REGION",
224
+ "AWS_ACCESS_KEY_ID",
225
+ "AWS_SECRET_ACCESS_KEY",
226
+ "AWS_SESSION_TOKEN",
227
+ "AWS_CONFIG_FILE",
228
+ "AWS_SHARED_CREDENTIALS_FILE",
229
+ "GOOGLE_APPLICATION_CREDENTIALS",
230
+ "ANTHROPIC_VERTEX_PROJECT_ID",
231
+ "CLOUD_ML_REGION"
232
+ ],
211
233
  "agent": "native",
212
234
  "skill": "native",
213
235
  "globalChild": "inherited",
@@ -429,6 +451,14 @@
429
451
  }
430
452
  },
431
453
  "codex": {
454
+ "sessionEnvAllowlist": [
455
+ "OPENAI_API_KEY",
456
+ "CODEX_API_KEY",
457
+ "OPENAI_BASE_URL",
458
+ "OPENAI_ORG_ID",
459
+ "OPENAI_PROJECT_ID",
460
+ "CODEX_HOME"
461
+ ],
432
462
  "agent": "native",
433
463
  "skill": "unsupported",
434
464
  "globalChild": "inherited",
@@ -452,7 +482,7 @@
452
482
  "id": "codex",
453
483
  "provider": "codex",
454
484
  "adapterVersion": "1",
455
- "supportedVersions": "0.161.0",
485
+ "supportedVersions": ">=0.161.0 <0.200.0",
456
486
  "runtimeDriver": "installed:codex",
457
487
  "catalogAdapter": {
458
488
  "kind": "file",
@@ -513,7 +543,7 @@
513
543
  "invocationProfiles": {
514
544
  "direct-shell": {
515
545
  "transport": "shell",
516
- "supportedVersions": "0.161.0",
546
+ "supportedVersions": ">=0.161.0 <0.200.0",
517
547
  "controls": {
518
548
  "model": {
519
549
  "support": "unqualified",
@@ -640,6 +670,12 @@
640
670
  }
641
671
  },
642
672
  "copilot": {
673
+ "sessionEnvAllowlist": [
674
+ "COPILOT_GITHUB_TOKEN",
675
+ "GH_TOKEN",
676
+ "GITHUB_TOKEN",
677
+ "COPILOT_HOME"
678
+ ],
643
679
  "agent": "native",
644
680
  "skill": "unsupported",
645
681
  "globalChild": "inherited",
@@ -1025,6 +1061,9 @@
1025
1061
  }
1026
1062
  },
1027
1063
  "factory": {
1064
+ "sessionEnvAllowlist": [
1065
+ "FACTORY_API_KEY"
1066
+ ],
1028
1067
  "agent": "native",
1029
1068
  "skill": "informational",
1030
1069
  "globalChild": "native",
@@ -1220,6 +1259,23 @@
1220
1259
  }
1221
1260
  },
1222
1261
  "opencode": {
1262
+ "sessionEnvAllowlist": [
1263
+ "OPENAI_API_KEY",
1264
+ "ANTHROPIC_API_KEY",
1265
+ "GEMINI_API_KEY",
1266
+ "GOOGLE_API_KEY",
1267
+ "OPENROUTER_API_KEY",
1268
+ "GROQ_API_KEY",
1269
+ "MISTRAL_API_KEY",
1270
+ "XAI_API_KEY",
1271
+ "DEEPSEEK_API_KEY",
1272
+ "OPENCODE_API_KEY",
1273
+ "OPENCODE_CONFIG",
1274
+ "OPENCODE_CONFIG_DIR",
1275
+ "OPENCODE_CONFIG_CONTENT",
1276
+ "OPENCODE_SERVER_USERNAME",
1277
+ "OPENCODE_SERVER_PASSWORD"
1278
+ ],
1223
1279
  "agent": "native",
1224
1280
  "skill": "unsupported",
1225
1281
  "globalChild": "inherited",
@@ -1596,6 +1652,19 @@
1596
1652
  }
1597
1653
  },
1598
1654
  "omp": {
1655
+ "sessionEnvAllowlist": [
1656
+ "OPENAI_API_KEY",
1657
+ "ANTHROPIC_API_KEY",
1658
+ "GEMINI_API_KEY",
1659
+ "GOOGLE_API_KEY",
1660
+ "OPENROUTER_API_KEY",
1661
+ "GROQ_API_KEY",
1662
+ "MISTRAL_API_KEY",
1663
+ "XAI_API_KEY",
1664
+ "DEEPSEEK_API_KEY",
1665
+ "PI_CONFIG_DIR",
1666
+ "PI_CODING_AGENT_DIR"
1667
+ ],
1599
1668
  "agent": "native",
1600
1669
  "skill": "inherited",
1601
1670
  "globalChild": "native",
@@ -1796,6 +1865,21 @@
1796
1865
  }
1797
1866
  },
1798
1867
  "pi": {
1868
+ "sessionEnvAllowlist": [
1869
+ "OPENAI_API_KEY",
1870
+ "ANTHROPIC_API_KEY",
1871
+ "GEMINI_API_KEY",
1872
+ "GOOGLE_API_KEY",
1873
+ "OPENROUTER_API_KEY",
1874
+ "GROQ_API_KEY",
1875
+ "MISTRAL_API_KEY",
1876
+ "XAI_API_KEY",
1877
+ "DEEPSEEK_API_KEY",
1878
+ "PI_CODING_AGENT_DIR",
1879
+ "PI_CODING_AGENT_SESSION_DIR",
1880
+ "AIWG_PI_STUB_SCENARIO",
1881
+ "AIWG_PI_STUB_RECEIPT"
1882
+ ],
1799
1883
  "agent": "native",
1800
1884
  "skill": "inherited",
1801
1885
  "globalChild": "native",
@@ -1992,6 +2076,10 @@
1992
2076
  }
1993
2077
  },
1994
2078
  "deepseek-harness": {
2079
+ "sessionEnvAllowlist": [
2080
+ "DEEPSEEK_API_KEY",
2081
+ "DSH_HOME"
2082
+ ],
1995
2083
  "agent": "native",
1996
2084
  "skill": "inherited",
1997
2085
  "globalChild": "native",
@@ -3001,6 +3089,10 @@
3001
3089
  }
3002
3090
  },
3003
3091
  "muse": {
3092
+ "sessionEnvAllowlist": [
3093
+ "AIWG_MUSE_STUB_SCENARIO",
3094
+ "AIWG_MUSE_STUB_RECEIPT"
3095
+ ],
3004
3096
  "agent": "unsupported",
3005
3097
  "skill": "unsupported",
3006
3098
  "globalChild": "informational",
@@ -3027,7 +3119,7 @@
3027
3119
  "id": "muse",
3028
3120
  "provider": "muse",
3029
3121
  "adapterVersion": "1",
3030
- "supportedVersions": "1.4.4-R5419.1",
3122
+ "supportedVersions": ">=1.4.4-R5419.1 <2.0.0",
3031
3123
  "runtimeDriver": "tools/ralph-external/lib/muse-adapter.mjs",
3032
3124
  "catalogAdapter": {
3033
3125
  "kind": "file",
@@ -3087,7 +3179,7 @@
3087
3179
  "invocationProfiles": {
3088
3180
  "direct-shell": {
3089
3181
  "transport": "shell",
3090
- "supportedVersions": "1.4.4-R5419.1",
3182
+ "supportedVersions": ">=1.4.4-R5419.1 <2.0.0",
3091
3183
  "controls": {
3092
3184
  "model": {
3093
3185
  "support": "unqualified",
@@ -1,3 +1,4 @@
1
+ import { signalChild } from '../process/signal-child.mjs';
1
2
  import { hasSelectionRelevantConflicts, validateWorkerCatalogDiscovery } from './provider-policy.js';
2
3
  import { spawn } from 'node:child_process';
3
4
  import { createHash } from 'node:crypto';
@@ -6,6 +7,7 @@ import { lstat, mkdir, open, readFile, readdir, realpath, rename, writeFile } fr
6
7
  import { dirname, join, relative } from 'node:path';
7
8
  import { constants } from 'node:fs';
8
9
  import { homedir, tmpdir } from 'node:os';
10
+ import { stripVTControlCharacters } from 'node:util';
9
11
  export const PROVIDER_DISCOVERY_DECISIONS = {
10
12
  claude: {
11
13
  provider: 'claude',
@@ -125,6 +127,12 @@ export const PROVIDER_DISCOVERY_DECISIONS = {
125
127
  };
126
128
  const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
127
129
  export function classifyDiscoveryError(message) {
130
+ message = stripVTControlCharacters(message);
131
+ if (/\b(?:EROFS|EACCES|EPERM)\b|read-only file system/i.test(message)
132
+ || (/FileSystem\.open|permission denied|operation not permitted/i.test(message)
133
+ && /[\\/]opencode[\\/]log[\\/]|[\\/]\.pi[\\/]agent(?:[\\/]|$)/i.test(message))) {
134
+ return 'environment';
135
+ }
128
136
  if (/(?:401|403|unauth|forbidden|credential|login|required auth)/i.test(message)) {
129
137
  return 'authentication';
130
138
  }
@@ -138,7 +146,7 @@ const SECRET_ASSIGNMENT = /((?:api[_-]?key|authorization|token|secret|password|c
138
146
  const BEARER_SECRET = /(bearer\s+)[A-Za-z0-9._~+/=-]+/gi;
139
147
  /** Keep native diagnostics useful without ever echoing credential material. */
140
148
  export function redactModelDiscoveryText(value) {
141
- return value
149
+ return stripVTControlCharacters(value)
142
150
  .replace(SECRET_ASSIGNMENT, '$1[REDACTED]')
143
151
  .replace(BEARER_SECRET, '$1[REDACTED]')
144
152
  .slice(0, 4096);
@@ -162,7 +170,7 @@ export const runModelDiscoveryCommand = (command, args, options = {}) => new Pro
162
170
  child.stdout.on('data', chunk => { stdout += String(chunk); if (Buffer.byteLength(stdout) > 8 * 1024 * 1024) {
163
171
  stdout = '';
164
172
  stderr = 'Model catalog exceeded 8 MiB limit';
165
- child.kill('SIGKILL');
173
+ signalChild(child, 'SIGKILL');
166
174
  finish(1);
167
175
  } });
168
176
  child.stderr.on('data', chunk => { stderr = (stderr + String(chunk)).slice(-4096); });
@@ -173,7 +181,7 @@ export const runModelDiscoveryCommand = (command, args, options = {}) => new Pro
173
181
  child.on('close', code => finish(code ?? 1));
174
182
  const timer = setTimeout(() => {
175
183
  stderr = `${stderr}${stderr ? '\n' : ''}${command} timed out`;
176
- child.kill();
184
+ signalChild(child);
177
185
  finish(124);
178
186
  }, options.timeoutMs ?? 10_000);
179
187
  });
@@ -450,7 +458,7 @@ export async function discoverCodexModels(command = 'codex', timeoutMs = 10_000,
450
458
  settled = true;
451
459
  clearTimeout(timer);
452
460
  lines.close();
453
- child.kill();
461
+ signalChild(child);
454
462
  resolveDiscovery(result);
455
463
  };
456
464
  child.stderr.on('data', chunk => { stderr = (stderr + String(chunk)).slice(-4096); });
@@ -656,6 +664,8 @@ export async function resolveDynamicModelCatalog(options) {
656
664
  };
657
665
  }
658
666
  const next = providerDiscovery[provider];
667
+ if (next.error)
668
+ next.error = stripVTControlCharacters(next.error);
659
669
  const instanceId = discoveryInstanceId(next);
660
670
  const prior = cached?.discovery?.instances?.[instanceId]
661
671
  ?? cached?.discovery?.providers[provider];
@@ -967,6 +977,30 @@ function validTimestamp(value) {
967
977
  return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i.test(value)
968
978
  && validDate(value.slice(0, 10)) && Number.isFinite(Date.parse(value));
969
979
  }
980
+ /** Use the active window, capped by the reported maximum, with native headroom. */
981
+ function codexContextTokens(row) {
982
+ const window = normalizePositiveInt(row.context_window);
983
+ const maximum = normalizePositiveInt(row.max_context_window);
984
+ const percent = row.effective_context_window_percent;
985
+ // A malformed supplied limit must not silently enlarge the qualified window.
986
+ if (window === null || row.max_context_window != null && maximum === null
987
+ || percent != null && (typeof percent !== 'number' || !Number.isFinite(percent) || percent <= 0 || percent > 100))
988
+ return null;
989
+ return normalizePositiveInt(Math.floor(Math.min(window, maximum ?? window) * (typeof percent === 'number' ? percent : 100) / 100));
990
+ }
991
+ function catalogEfforts(ids, raw) {
992
+ return ids.map(id => {
993
+ const entry = Array.isArray(raw) ? raw.find(value => value && typeof value === 'object'
994
+ && (value.effort ?? value.tier) === id) : undefined;
995
+ // Conductor-verified Codex/Muse catalog rule (#3083): only ultra is
996
+ // "Maximum reasoning with automatic task delegation". Prefer that explicit
997
+ // description when present; other listed levels do not self-delegate.
998
+ // multi_agent_reasoning_effort/version describe child execution, not which
999
+ // selected effort activates delegation (e.g. xhigh is not self-delegating).
1000
+ return { id, selfDelegates: id === 'ultra'
1001
+ || typeof entry?.description === 'string' && /automatic task delegation/i.test(entry.description) };
1002
+ });
1003
+ }
970
1004
  function normalizeObservationRows(rows, options, cli) {
971
1005
  const diagnostics = [];
972
1006
  const models = [];
@@ -987,12 +1021,17 @@ function normalizeObservationRows(rows, options, cli) {
987
1021
  continue;
988
1022
  }
989
1023
  const times = observationTimestamps(sourceOptions);
990
- for (const field of [cli === 'muse' ? 'context_limit' : 'context_window', 'output_limit']) {
1024
+ for (const field of [...(cli === 'muse' ? ['context_limit'] : ['context_window', 'max_context_window']), 'output_limit']) {
991
1025
  if (row[field] != null && (typeof row[field] !== 'number' || !Number.isSafeInteger(row[field]) || row[field] < 1)) {
992
1026
  diagnostics.push(`${source}: malformed ${field} for ${id}; retained as unknown`);
993
1027
  // Do not round or coerce malformed evidence into a valid observation.
994
1028
  }
995
1029
  }
1030
+ if (cli === 'codex' && row.effective_context_window_percent != null
1031
+ && (typeof row.effective_context_window_percent !== 'number' || !Number.isFinite(row.effective_context_window_percent)
1032
+ || row.effective_context_window_percent <= 0 || row.effective_context_window_percent > 100)) {
1033
+ diagnostics.push(`${source}: malformed effective_context_window_percent for ${id}; context retained as unknown`);
1034
+ }
996
1035
  for (const field of ['is_default', 'is_current', 'retired', 'deprecated']) {
997
1036
  if (row[field] != null && typeof row[field] !== 'boolean')
998
1037
  diagnostics.push(`${source}: malformed ${field} for ${id}`);
@@ -1040,8 +1079,16 @@ function normalizeObservationRows(rows, options, cli) {
1040
1079
  }
1041
1080
  const efforts = cli === 'muse' ? museEfforts(row) : [...new Set([...stringList(row.supported_reasoning_levels),
1042
1081
  ...(typeof row.default_reasoning_level === 'string' && row.default_reasoning_level ? [row.default_reasoning_level] : [])])];
1082
+ // Codex's registry provider and catalogAdapter contain no inference-provider
1083
+ // identity. Its providerless native catalog is OpenAI; never infer from a slug.
1084
+ // Muse's real local catalogs use provider_id, not provider.
1085
+ const llmProvider = typeof row.provider === 'string' && row.provider ? row.provider
1086
+ : cli === 'muse' && typeof row.provider_id === 'string' && row.provider_id ? row.provider_id
1087
+ : cli === 'codex' ? 'openai' : undefined;
1043
1088
  const model = {
1044
- id, ...(visibility === 'unknown' ? {} : { hidden: visibility === 'hidden' }),
1089
+ id, apiModelId: typeof row.api_model_id === 'string' ? row.api_model_id : id,
1090
+ ...(llmProvider ? { llmProvider } : {}),
1091
+ ...(visibility === 'unknown' ? {} : { hidden: visibility === 'hidden' }),
1045
1092
  ...(cli === 'muse' ? { isDefault: row.is_default === true } : {}),
1046
1093
  ...(efforts.length ? { reasoningEfforts: efforts } : {}),
1047
1094
  worker: buildWorkerObservation(sourceOptions, {
@@ -1049,18 +1096,25 @@ function normalizeObservationRows(rows, options, cli) {
1049
1096
  visibility, ...lifecycle, releaseDate,
1050
1097
  availability: lifecycle.lifecycle === 'retired' ? 'unavailable'
1051
1098
  : cli === 'muse' ? (row.is_current === true ? 'accessible' : 'unknown') : 'accessible',
1052
- contextTokens: normalizePositiveInt(cli === 'muse' ? row.context_limit : row.context_window),
1099
+ contextTokens: cli === 'muse' ? normalizePositiveInt(row.context_limit) : codexContextTokens(row),
1053
1100
  maxOutputTokens: normalizePositiveInt(row.output_limit),
1054
- efforts: efforts.map(id => ({ id, selfDelegates: null })),
1101
+ efforts: catalogEfforts(efforts, rawEfforts),
1102
+ // The native renderer passes model_reasoning_effort (Muse: --reasoning-effort).
1055
1103
  effortSupport: efforts.length ? 'explicit' : 'unknown',
1056
1104
  ...(cli === 'muse' ? { dataUseNote: 'contributor policy is config-scoped' } : {}),
1057
1105
  provenanceFields: cli === 'muse'
1058
- ? ['/id:model_id', '/isDefault:is_default', '/availability:is_current', '/efforts:reasoning_effort_variants[].tier', '/contextTokens:context_limit', '/maxOutputTokens:output_limit', '/releaseDate:release_date']
1059
- : ['/id:slug', '/efforts:supported_reasoning_levels+default_reasoning_level', '/contextTokens:context_window'],
1106
+ ? ['/id:model_id', '/isDefault:is_default', '/availability:is_current', '/efforts:reasoning_effort_variants[].tier',
1107
+ '/efforts/selfDelegates:catalog-description+verified-ultra-rule', '/contextTokens:context_limit', '/maxOutputTokens:output_limit', '/releaseDate:release_date']
1108
+ : ['/id:slug', '/efforts:supported_reasoning_levels+default_reasoning_level',
1109
+ '/efforts/selfDelegates:catalog-description+verified-ultra-rule',
1110
+ '/contextTokens:context_window+max_context_window+effective_context_window_percent'],
1060
1111
  }),
1061
1112
  };
1062
1113
  // Bind compatibility-row values without including unrelated credential-bearing row keys.
1063
- const compatibility = { id: model.id, hidden: model.hidden, isDefault: model.isDefault, reasoningEfforts: model.reasoningEfforts };
1114
+ model.worker.provenance[0].fields.push('/apiModelId');
1115
+ if (model.llmProvider)
1116
+ model.worker.provenance[0].fields.push('/llmProvider', `/llmProvider:${typeof row.provider === 'string' && row.provider ? 'provider' : cli === 'muse' ? 'provider_id' : 'codex-openai-default'}`);
1117
+ const compatibility = { id: model.id, apiModelId: model.apiModelId, llmProvider: model.llmProvider, hidden: model.hidden, isDefault: model.isDefault, reasoningEfforts: model.reasoningEfforts };
1064
1118
  model.worker.evidenceDigest = discoveryDigest(JSON.stringify({
1065
1119
  workerDigest: model.worker.evidenceDigest, ...compatibility,
1066
1120
  }));
@@ -1132,6 +1186,17 @@ function reconcileDuplicateObservations(models, diagnostics, sourcePointers) {
1132
1186
  break;
1133
1187
  }
1134
1188
  }
1189
+ // Provider/API identity is selection evidence too. Conflicting rows must
1190
+ // not acquire a qualified route merely because one row appeared first.
1191
+ for (const field of ['apiModelId', 'llmProvider']) {
1192
+ if (new Set(group.map(row => row[field])).size < 2)
1193
+ continue;
1194
+ conflicts.push({ field: `/${field}`, evidence: group.map(row => ({ value: row[field] ?? null,
1195
+ source: row.worker.provenance[0].source, pointer: field === 'apiModelId' ? '/api_model_id' : '/provider',
1196
+ observedAt: row.worker.observedAt, digest: row.worker.provenance[0].digest })) });
1197
+ diagnostics.push(`${model.id}: conflicting ${field}`);
1198
+ delete model[field];
1199
+ }
1135
1200
  // Compatibility defaults also remain evidence rather than silently keeping the first.
1136
1201
  if (new Set(group.map(m => m.isDefault)).size > 1) {
1137
1202
  diagnostics.push(`${model.id}: conflicting isDefault values from ${group.map(m => m.worker.provenance[0].source).join(', ')}`);