@aiwg/cli 2026.10.10 → 2026.10.12

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 (156) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +78 -14
  2. package/agentic/code/providers/model-catalog.v1.json +62 -12
  3. package/dist/src/artifacts/binding-target-identity.js +51 -0
  4. package/dist/src/artifacts/checksum-manifest.js +12 -0
  5. package/dist/src/artifacts/index-builder.js +11 -13
  6. package/dist/src/artifacts/query-engine.js +5 -11
  7. package/dist/src/cli/handlers/doctor-help.js +32 -0
  8. package/dist/src/cli/handlers/gates.js +19 -18
  9. package/dist/src/cli/handlers/models-help.js +7 -1
  10. package/dist/src/cli/handlers/models.js +3 -3
  11. package/dist/src/cli/handlers/script-runner.js +8 -5
  12. package/dist/src/cli/handlers/sessions-intake.js +104 -0
  13. package/dist/src/cli/handlers/sessions-tail.js +172 -0
  14. package/dist/src/cli/handlers/sessions.js +287 -31
  15. package/dist/src/cli/handlers/skill-lint.js +10 -3
  16. package/dist/src/cli/handlers/steward-format.js +100 -0
  17. package/dist/src/cli/handlers/steward-models.js +167 -0
  18. package/dist/src/cli/handlers/steward.js +14 -248
  19. package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
  20. package/dist/src/cli/handlers/use.js +18 -28
  21. package/dist/src/cli/handlers/utilities.js +1 -24
  22. package/dist/src/config/aiwg-config.js +7 -0
  23. package/dist/src/config/authorization-cli.js +89 -0
  24. package/dist/src/config/cli-help.js +1 -0
  25. package/dist/src/config/cli.js +10 -1
  26. package/dist/src/config/worker-context.js +337 -0
  27. package/dist/src/decision/context-pruning.js +3 -2
  28. package/dist/src/decision/ensemble/contract.js +15 -4
  29. package/dist/src/decision/ensemble-study/artifacts.js +5 -0
  30. package/dist/src/decision/ensemble-study/score.js +4 -3
  31. package/dist/src/decision/feature-export.js +3 -9
  32. package/dist/src/decision/heldout/collector.js +2 -2
  33. package/dist/src/decision/issue-triage/runtime.js +3 -9
  34. package/dist/src/decision/qualification/gates.js +2 -1
  35. package/dist/src/decision/qualification/release.js +2 -3
  36. package/dist/src/decision/routing/report.js +2 -0
  37. package/dist/src/decision/sdlc-screening.js +3 -5
  38. package/dist/src/decision/sensitivity/comparative.js +5 -3
  39. package/dist/src/gates/driver.js +6 -77
  40. package/dist/src/gates/evaluate.js +4 -11
  41. package/dist/src/gates/floors.js +4 -4
  42. package/dist/src/gates/index.js +3 -3
  43. package/dist/src/gates/lattice.js +26 -0
  44. package/dist/src/gates/registry.js +2 -1
  45. package/dist/src/gates/report.js +19 -7
  46. package/dist/src/issues/audit-cli.js +120 -0
  47. package/dist/src/issues/audit-contract.js +64 -0
  48. package/dist/src/issues/audit-local.js +82 -0
  49. package/dist/src/issues/audit.js +220 -0
  50. package/dist/src/lint/rule-activation.js +15 -1
  51. package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
  52. package/dist/src/mission-protocol/canonical-codecs.js +270 -0
  53. package/dist/src/mission-protocol/codecs.js +22 -550
  54. package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
  55. package/dist/src/mission-protocol/health-trace-types.js +43 -0
  56. package/dist/src/mission-protocol/types.js +2 -42
  57. package/dist/src/mission-protocol/worker-contract-json.js +10 -0
  58. package/dist/src/mission-protocol/worker-review-gate.js +100 -0
  59. package/dist/src/mission-protocol/worker-review-types.js +2 -0
  60. package/dist/src/models/catalog-policy.js +280 -0
  61. package/dist/src/models/index.js +1 -1
  62. package/dist/src/models/model-capabilities.v1.json +78 -14
  63. package/dist/src/models/model-catalog.v1.json +62 -12
  64. package/dist/src/models/policy-resources.js +28 -0
  65. package/dist/src/models/provider-policy.js +10 -539
  66. package/dist/src/models/resolver.js +26 -8
  67. package/dist/src/models/router.js +15 -2
  68. package/dist/src/models/tiers.js +13 -0
  69. package/dist/src/models/types.js +0 -6
  70. package/dist/src/models/worker-ceilings.js +59 -0
  71. package/dist/src/models/worker-invocation-policy.js +243 -0
  72. package/dist/src/models/worker-plan.js +16 -20
  73. package/dist/src/models/worker-review-lineage.js +55 -0
  74. package/dist/src/models/worker-selection-policy.js +1 -1
  75. package/dist/src/models/wrapper-route.js +6 -3
  76. package/dist/src/policy/authorization.js +30 -1
  77. package/dist/src/policy/hitl-config.js +63 -0
  78. package/dist/src/policy/hitl-receipt.js +57 -0
  79. package/dist/src/policy/hitl-types.js +4 -0
  80. package/dist/src/policy/hitl-validation.js +39 -0
  81. package/dist/src/policy/hitl.js +117 -0
  82. package/dist/src/process/aiwg-probe-command.d.mts +15 -0
  83. package/dist/src/process/aiwg-probe-command.mjs +37 -0
  84. package/dist/src/rules/activation.d.mts +1 -1
  85. package/dist/src/rules/activation.mjs +0 -2
  86. package/dist/src/rules/gate-coverage.js +51 -0
  87. package/dist/src/rules/gate-references.js +16 -0
  88. package/dist/src/rules/rule-frontmatter.schema.json +7 -1
  89. package/dist/src/serve/worker-command-request.js +21 -4
  90. package/dist/src/serve/worker-invocation-preparation.js +3 -1
  91. package/dist/src/serve/worker-invocation.js +21 -10
  92. package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
  93. package/dist/src/sessions/adapters/grok-build-native.js +269 -0
  94. package/dist/src/sessions/adapters/grok-build.js +57 -15
  95. package/dist/src/sessions/adapters/grokbot.js +7 -0
  96. package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
  97. package/dist/src/sessions/adapters/hermes.js +42 -3
  98. package/dist/src/sessions/adapters/warp.js +1 -1
  99. package/dist/src/sessions/batch-import.js +44 -6
  100. package/dist/src/sessions/contracts.js +20 -1
  101. package/dist/src/sessions/coverage-preflight.js +7 -1
  102. package/dist/src/sessions/discovery-capability.js +38 -0
  103. package/dist/src/sessions/fortemi-export-mapping.js +33 -3
  104. package/dist/src/sessions/host-catalog.js +157 -0
  105. package/dist/src/sessions/image-blobs.js +141 -0
  106. package/dist/src/sessions/importer.js +9 -232
  107. package/dist/src/sessions/index.js +6 -0
  108. package/dist/src/sessions/intake-dir.js +71 -0
  109. package/dist/src/sessions/normalization.js +251 -0
  110. package/dist/src/sessions/refresh.js +25 -2
  111. package/dist/src/sessions/repository.js +164 -22
  112. package/dist/src/sessions/tail.js +91 -0
  113. package/dist/src/sessions/workspace-discovery.js +88 -9
  114. package/dist/src/skills/agent-skill-sidecar.js +28 -0
  115. package/dist/src/skills/agent-skills.js +4 -23
  116. package/dist/src/skills/claude-surface-projection.js +25 -0
  117. package/dist/src/skills/deployer.js +8 -288
  118. package/dist/src/skills/deployment-policy.js +114 -0
  119. package/dist/src/skills/deployment-projection.js +152 -0
  120. package/dist/src/skills/deployment-types.js +20 -0
  121. package/dist/src/skills/exporter.js +8 -31
  122. package/dist/src/skills/provider-extensions.js +41 -0
  123. package/dist/src/skills/run.js +6 -0
  124. package/dist/src/skills/types.js +0 -8
  125. package/dist/src/skills/validator-resources.js +68 -0
  126. package/dist/src/skills/validator.js +6 -55
  127. package/dist/src/storage/cli-migrate-options.js +125 -0
  128. package/dist/src/storage/cli-migrate.js +111 -0
  129. package/dist/src/storage/cli.js +4 -279
  130. package/dist/src/storage/migration-adapter.js +79 -0
  131. package/dist/src/storage/migration-copy.js +108 -0
  132. package/dist/src/storage/migration-protocol.js +14 -197
  133. package/dist/src/storage/migration-records.js +186 -0
  134. package/dist/src/storage/migration-types.js +12 -0
  135. package/dist/src/tracker/forge-http.js +2 -1
  136. package/dist/src/tracker/issue-pages.js +110 -0
  137. package/package.json +7 -1
  138. package/schemas/mission-v1.schema.json +335 -0
  139. package/schemas/models/model-catalog.v1.schema.json +19 -1
  140. package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
  141. package/schemas/models/model-policy.v1.schema.json +1 -1
  142. package/tools/agents/providers/provider-paths-staging.mjs +35 -0
  143. package/tools/agents/providers/rule-binding-targets.mjs +146 -0
  144. package/tools/agents/providers/rule-delivery.mjs +7 -126
  145. package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
  146. package/tools/ralph-external/lib/worker-context.mjs +237 -0
  147. package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
  148. package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
  149. package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
  150. package/tools/ralph-external/lib/worker-isolation.mjs +17 -29
  151. package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
  152. package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
  153. package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
  154. package/tools/ralph-external/lib/worker-termination.mjs +82 -0
  155. package/tools/ralph-external/session-launcher.mjs +51 -96
  156. package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
@@ -12,7 +12,7 @@ import {
12
12
  addManagedMarker, contentHash, replaceBoundRulesBlock, deployEmulatedBehaviors, filterAgentFiles,
13
13
  } from './base.mjs';
14
14
 
15
- const { parseRuleFrontmatter, inferRuleActivation, RULE_BINDING_TYPES, readCoreBody, validateCoreSet, CORE_RULES_START, CORE_RULES_END } = await importImpl(import.meta.url, 'rules/activation.mjs');
15
+ const { parseRuleFrontmatter, inferRuleActivation, readCoreBody, validateCoreSet, CORE_RULES_START, CORE_RULES_END } = await importImpl(import.meta.url, 'rules/activation.mjs');
16
16
  const { resolveDeployVersion } = await importImpl(import.meta.url, 'providers/deploy-version.mjs');
17
17
  const { trackTemporaryDirectory } = await importImpl(import.meta.url, 'utils/temporary-directory.js');
18
18
  const { loadRulePinConfigs, installedRuleCapabilities, resolveRulePins } = await importImpl(import.meta.url, 'rules/pins.mjs');
@@ -20,6 +20,8 @@ const { guardRulesInlineBudget } = await importImpl(import.meta.url, 'rules/inli
20
20
  const { userConfigFile } = await importImpl(import.meta.url, 'config/user-config-dir.mjs');
21
21
  const { resolveProjectAiwgDir } = await importImpl(import.meta.url, 'config/project-artifacts-runtime.mjs');
22
22
  import { writeContextFileSync, CONTEXT_STAGE_ENV } from './context-write.mjs';
23
+ import { captureProviderPathDescriptors, setStagedProviderPath, restoreProviderPaths } from './provider-paths-staging.mjs';
24
+ import { resolveDeliveryRuleBindings } from './rule-binding-targets.mjs';
23
25
  import { projectOnDemandReferences, renderRuleKernelIndex } from './rule-presentation.mjs';
24
26
  export { renderRuleKernelIndex } from './rule-presentation.mjs';
25
27
  const PREPARATION_HOOK = Symbol.for('aiwg.provider-preparation-hook.v1');
@@ -175,128 +177,7 @@ export async function prepareRuleDelivery(opts) {
175
177
  // All pins use native kernel projection, including legacy untagged rules.
176
178
  rule.tagged = true;
177
179
  }
178
- const bindings = new Map();
179
- const targets = [];
180
- const targetKeys = new Set();
181
- if (rules.some(rule => Object.values(rule.bound_to).some(ids => ids.length))) {
182
- const { loadLocalCapabilityEntries, loadLocalOverrideCapabilityEntries, createRuleBindingTargetResolver } = await importImpl(import.meta.url, 'artifacts/query-engine.js');
183
- const root = resolveAiwgRoot(opts.srcRoot) || opts.srcRoot;
184
- // Framework graph paths are package-relative; project graph paths belong
185
- // to the queried project. Normalize before merging so one source cannot
186
- // become two apparently ambiguous targets after a source-index rebuild.
187
- const indexEntries = new Map();
188
- const entryPaths = new Map();
189
- const loadEntries = (cwd, overridesOnly = false) => (overridesOnly
190
- ? loadLocalOverrideCapabilityEntries(cwd)
191
- : loadLocalCapabilityEntries(cwd, undefined, indexEntries)).map(entry => {
192
- const anchor = entry.path.startsWith('agentic/code/') ? root : cwd;
193
- const key = `${anchor}\0${entry.path}`;
194
- if (!entryPaths.has(key)) entryPaths.set(key, path.resolve(anchor, entry.path));
195
- const resolvedPath = entryPaths.get(key);
196
- // Canonical IDs hash the indexed package-relative path. Keep it through
197
- // resolution; the selected target is anchored to root below.
198
- return { ...entry, path: entry.path.startsWith('agentic/code/') ? entry.path
199
- : opts.mapInputPath ? opts.mapInputPath(resolvedPath) : resolvedPath };
200
- });
201
- let rebuilt = false;
202
- const cacheDir = process.env.AIWG_RULE_BINDING_INDEX_CACHE;
203
- const cacheFile = cacheDir && path.join(cacheDir, `${contentHash(opts.logicalInputPath ? opts.logicalInputPath(root) : root)}.json`);
204
- const buildSourceEntries = async (refresh = false) => {
205
- if (!refresh && cacheFile) {
206
- try { return JSON.parse(fs.readFileSync(cacheFile, 'utf8')).map(entry => ({ ...entry, path: path.isAbsolute(entry.path) && opts.mapInputPath ? opts.mapInputPath(entry.path) : entry.path })); }
207
- catch (error) {
208
- if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
209
- }
210
- }
211
- const { collectRuleBindingEntries } = await importImpl(import.meta.url, 'artifacts/index-builder.js');
212
- const sourceEntries = await collectRuleBindingEntries(root,
213
- root === opts.srcRoot && !fs.existsSync(path.join(root, 'agentic')) ? '.' : 'agentic/code');
214
- rebuilt = true;
215
- const entries = sourceEntries.map(entry => ({ ...entry, path: entry.path.startsWith('agentic/code/')
216
- ? entry.path : path.resolve(root, entry.path) }));
217
- if (cacheFile) {
218
- fs.mkdirSync(cacheDir, { recursive: true });
219
- // Workers may publish the same immutable source snapshot concurrently.
220
- const temporary = `${cacheFile}.${process.pid}.tmp`;
221
- fs.writeFileSync(temporary, JSON.stringify(entries.map(entry => ({ ...entry, path: opts.logicalInputPath ? opts.logicalInputPath(entry.path) : entry.path }))));
222
- fs.renameSync(temporary, cacheFile);
223
- }
224
- return entries;
225
- };
226
- const localEntries = opts.bindingEntries || loadEntries(opts.target, Boolean(packageRelations));
227
- let entries = [...localEntries];
228
- if (!opts.bindingEntries) {
229
- // The validated package table is authoritative for its exact source digest.
230
- // Keep the canonical resolver for aliases, local overrides and ambiguity.
231
- const sourceEntries = packageRelations
232
- ? Object.values(packageRelations.index.artifacts).map(entry => ({ ...entry,
233
- indexScope: 'packaged', indexGraph: 'framework' }))
234
- : loadEntries(root);
235
- // A relocated project graph may also be visible while querying the
236
- // package root. Keep its project-anchored instance only.
237
- const uniqueSources = sourceEntries.filter(entry => !(opts.sharedProjectGraph &&
238
- entry.path.startsWith(path.join(root, '.aiwg') + path.sep) &&
239
- localEntries.some(local => local.type === entry.type && local.name === entry.name)));
240
- entries.push(...uniqueSources);
241
- if (!sourceEntries.length) entries.push(...await buildSourceEntries());
242
- }
243
- // Project-local installed bundles are outside the package source index.
244
- // Reuse the existing binding index builder only for those source roots.
245
- if (!opts.bindingEntries) {
246
- for (const entry of Object.values(configs.project.installed || {})) {
247
- if (!entry.localPath) continue;
248
- const localRoot = path.resolve(opts.target, entry.localPath);
249
- if (localRoot === root || !fs.existsSync(localRoot)) continue;
250
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'aiwg-local-rule-index-'));
251
- try {
252
- const { buildIndex } = await importImpl(import.meta.url, 'artifacts/index-builder.js');
253
- await buildIndex(localRoot, { scope: '.', outputDir: temp, force: true, quiet: true });
254
- const index = JSON.parse(read(path.join(temp, '.aiwg', '.index', 'metadata.json')));
255
- const scoped = Object.values(index.entries).map(entry => ({ ...entry, path: path.resolve(localRoot, entry.path) }));
256
- localEntries.push(...scoped);
257
- entries.push(...scoped);
258
- } finally { fs.rmSync(temp, { recursive: true, force: true }); }
259
- }
260
- }
261
- // A stored index can predate the source tree (edge/dev checkouts). Rebuild once from source
262
- // before reporting a binding target as missing.
263
- const resolvedTargets = new Map();
264
- let resolveBinding = createRuleBindingTargetResolver(entries);
265
- const resolveTarget = async (type, id) => {
266
- const key = `${type}:${id}`;
267
- if (resolvedTargets.has(key)) return resolvedTargets.get(key);
268
- let target;
269
- try { target = resolveBinding(type, id); }
270
- catch (error) {
271
- if (packageRelations || rebuilt || opts.bindingEntries || !/^Missing /.test(error.message)) throw error;
272
- indexEntries.clear();
273
- entryPaths.clear();
274
- entries = [...localEntries, ...await buildSourceEntries(true)];
275
- rebuilt = true;
276
- resolveBinding = createRuleBindingTargetResolver(entries);
277
- target = resolveBinding(type, id);
278
- // A rebuild replaces the input set for every lookup.
279
- resolvedTargets.clear();
280
- }
281
- resolvedTargets.set(key, target);
282
- return target;
283
- };
284
- for (const rule of rules.filter(rule => rule.tagged)) {
285
- for (const [key, type] of Object.entries(RULE_BINDING_TYPES)) {
286
- for (const id of rule.bound_to[key]) {
287
- const target = await resolveTarget(type, id);
288
- const source = path.isAbsolute(target.path) ? target.path : path.resolve(target.path.startsWith('agentic/code/') ? root : opts.target, target.path);
289
- if (!fs.existsSync(source)) throw new Error(`${rule.file}: binding target content missing: ${source}`);
290
- bindings.set(source, [...(bindings.get(source) || []), rule]);
291
- const key = `${target.type}:${source}`;
292
- if (!targetKeys.has(key)) {
293
- targetKeys.add(key);
294
- targets.push({ ...target, source });
295
- }
296
- }
297
- }
298
- }
299
- }
180
+ const { bindings, targets } = await resolveDeliveryRuleBindings(opts, rules, configs, root, packageRelations);
300
181
  return { rules, bindings, targets, pins: pinned.rules, pinWarnings: pinned.warnings };
301
182
  }
302
183
 
@@ -1010,7 +891,7 @@ export async function prepareRuleDeployment(original, provider, deploy) {
1010
891
  const mappings = [];
1011
892
  const preimages = [...initialPreimages];
1012
893
  let retained = false;
1013
- const originals = { ...provider.paths };
894
+ const originals = captureProviderPathDescriptors(provider);
1014
895
  const logicalTarget = original.target;
1015
896
  const priorAiwgRoot = process.env.AIWG_ROOT;
1016
897
  const priorRegistry = process.env.AIWG_USER_REGISTRY_PATH;
@@ -1148,7 +1029,7 @@ export async function prepareRuleDeployment(original, provider, deploy) {
1148
1029
  if (!path.isAbsolute(dir)) continue;
1149
1030
  const to = path.join(temp, 'native', key);
1150
1031
  mount(dir, to);
1151
- provider.paths[key] = to;
1032
+ setStagedProviderPath(provider, key, to);
1152
1033
  }
1153
1034
  let kernel = provider.kernelSkillsPath;
1154
1035
  if (kernel && path.isAbsolute(kernel)) {
@@ -1344,7 +1225,7 @@ export async function prepareRuleDeployment(original, provider, deploy) {
1344
1225
  return plan;
1345
1226
  } finally {
1346
1227
  if (priorContextStage === undefined) delete process.env[CONTEXT_STAGE_ENV]; else process.env[CONTEXT_STAGE_ENV] = priorContextStage;
1347
- Object.assign(provider.paths, originals);
1228
+ restoreProviderPaths(provider, originals);
1348
1229
  if (priorAiwgRoot === undefined) delete process.env.AIWG_ROOT; else process.env.AIWG_ROOT = priorAiwgRoot;
1349
1230
  if (priorRegistry === undefined) delete process.env.AIWG_USER_REGISTRY_PATH; else process.env.AIWG_USER_REGISTRY_PATH = priorRegistry;
1350
1231
  if (priorArtifacts === undefined) delete process.env.AIWG_ARTIFACTS_PATH; else process.env.AIWG_ARTIFACTS_PATH = priorArtifacts;
@@ -31,6 +31,13 @@
31
31
  * - `--approval-mode <mode>` — common to both; "`muse exec` accepts both
32
32
  * approval flags" (configuration). Opt-in only: the adapter sets NO
33
33
  * approval posture by default (#230 out of scope: `--yolo` defaults).
34
+ * - `--trust-workspace` — loads project rules, skills and hooks for the
35
+ * workspace (#3685: untrusted Muse reports `rules.context_load`
36
+ * `project_sources=0` with user skills only and empty hook admission;
37
+ * trusted reports `project_sources=1` with project skills and hooks).
38
+ * Tier 0 workspaces are conductor-prepared worker trees the conductor
39
+ * owns, so the direct profile binds this flag by default through the
40
+ * host-controlled trust mapping; the brief never supplies it.
34
41
  * - `muse export --session <uuid> --out <path>` — offline,
35
42
  * byte-deterministic transcript projection; `--last` and `--redacted`
36
43
  * skip the picker / redact payload strings (cookbook:
@@ -173,6 +180,9 @@ export class MuseAdapter extends ProviderAdapter {
173
180
  if ((permissions.approval ?? 'never') !== 'never') throw new Error('Muse direct worker approval requires never');
174
181
  // Trust is a host-owned, qualification-bound control, like the schema.
175
182
  // Brief contents and CLI defaults cannot opt into workspace instructions.
183
+ // Tier 0 binds ["--trust-workspace"] because the workspace is always a
184
+ // conductor-prepared worker tree (#3685); untrusted trees stay refused
185
+ // at dispatch by the operator native-argv gate.
176
186
  let trust;
177
187
  try { trust = JSON.parse(profile.controls.trust.mapping); } catch { /* fail closed below */ }
178
188
  if (!Array.isArray(trust) || !(trust.length === 0 || trust.length === 1 && trust[0] === '--trust-workspace')) {
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Worker AIWG context loading and launch-time ensure, tools side (#3685).
3
+ *
4
+ * The provider-set UNION lives in `src/config/worker-context.ts` and the
5
+ * config loads through the existing `readAiwgConfig` loader (never ad-hoc
6
+ * JSON). Both cross the tools/src boundary through `importImpl`, so this
7
+ * module stays runnable from an installed package (`dist/src`) and a
8
+ * checkout (`src/`); every such seam is injectable for synthetic tests.
9
+ * `src/` must never import `tools/`, so the src module is always the
10
+ * callee, never the caller.
11
+ *
12
+ * Also home to the single-pass deploy, section verifier, and
13
+ * skip-worktree helpers extracted from `worktree-preparation.mjs` (which
14
+ * re-exports them) so that file stays agent-friendly.
15
+ */
16
+
17
+ import { spawnSync } from 'node:child_process';
18
+ import { readFileSync } from 'node:fs';
19
+ import path from 'node:path';
20
+ import { importImpl } from '../../_resolve-impl.mjs';
21
+
22
+ /** Load the project config through the existing loader. */
23
+ async function defaultReadConfig(repoRoot) {
24
+ const { readAiwgConfig } = await importImpl(import.meta.url, 'config/aiwg-config.js');
25
+ return readAiwgConfig(repoRoot);
26
+ }
27
+
28
+ /** Resolve the provider union through the src-side pure function. */
29
+ async function defaultResolveFromConfig(config) {
30
+ const { resolveWorkerContextProvidersFromConfig } = await importImpl(import.meta.url, 'config/worker-context.js');
31
+ return resolveWorkerContextProvidersFromConfig(config);
32
+ }
33
+
34
+ /** Map one orchestration CLI id through the src-side registry mapping. */
35
+ async function defaultMapCli(cli, parallelism) {
36
+ const { mapCliToProviderId } = await importImpl(import.meta.url, 'config/worker-context.js');
37
+ return mapCliToProviderId(cli, parallelism);
38
+ }
39
+
40
+ /**
41
+ * Resolve the worker-context provider set (#3685 round 2): the union of
42
+ * `.aiwg/aiwg.config` `providers` and every CLI configured for
43
+ * orchestration (`parallelism.clis` keys/adapters and the `cli` of every
44
+ * `parallelism.groups.*.members[]`), mapped through the provider
45
+ * registry. An explicit list wins and is returned verbatim.
46
+ *
47
+ * @returns {Promise<{providers: string[], skipped: string[]}>}
48
+ */
49
+ export async function resolveWorkerContextProviders({ repoRoot, providers = null, readConfig = null, resolveFromConfig = null } = {}) {
50
+ if (providers !== null && providers !== undefined) {
51
+ if (!Array.isArray(providers) || providers.length === 0
52
+ || providers.some(p => typeof p !== 'string' || p.length === 0 || p.includes('\0') || p.includes(','))) {
53
+ throw new Error('worker context providers must be a non-empty array of provider ids');
54
+ }
55
+ return { providers: [...new Set(providers)], skipped: [] };
56
+ }
57
+ if (typeof repoRoot !== 'string' || !repoRoot) {
58
+ throw new Error('worker context providers need a repoRoot or an explicit provider list');
59
+ }
60
+ const config = await (readConfig ?? defaultReadConfig)(repoRoot);
61
+ if (!config) {
62
+ throw new Error(`worker context deployment needs configured providers: cannot read ${path.join(repoRoot, '.aiwg', 'aiwg.config')} (missing or invalid); configure providers and orchestration CLIs, or pass an explicit provider list`);
63
+ }
64
+ return (resolveFromConfig ?? defaultResolveFromConfig)(config);
65
+ }
66
+
67
+ /**
68
+ * Ensure a worker workdir carries its CLI's AIWG bootstrap before launch.
69
+ * Resolves the worker CLI to its provider (from the project config, so
70
+ * orchestration CLIs map through the registry), checks presence, and
71
+ * deploys through the same one-pass helper when missing. The deploy
72
+ * target is always exactly `workdir` — never anywhere else.
73
+ *
74
+ * Injectable for synthetic tests: `ensureFn` stubs the whole ensure,
75
+ * otherwise `mapCli`, `readConfig`, `resolveFromConfig`, `deployFn`,
76
+ * `statusFn`, and `skipWorktreeFn` stub each seam.
77
+ *
78
+ * @returns {Promise<{ensured: boolean, provider: string, providers: string[], skippedBridges: string[], skipped: string[]}>}
79
+ */
80
+ export async function ensureWorkerContextForLaunch({
81
+ workdir,
82
+ cli,
83
+ config = null,
84
+ repoRoot = null,
85
+ providers = null,
86
+ readConfig = null,
87
+ resolveFromConfig = null,
88
+ mapCli = null,
89
+ ensureFn = null,
90
+ statusFn = null,
91
+ deployFn = null,
92
+ skipWorktreeFn = null,
93
+ aiwgBin = process.env.AIWG_BIN ?? 'aiwg',
94
+ } = {}) {
95
+ if (typeof workdir !== 'string' || !workdir) throw new Error('worker context ensure needs a workdir');
96
+ if (typeof cli !== 'string' || !cli) throw new Error('worker context ensure needs the worker CLI id');
97
+ let resolved = Array.isArray(providers) && providers.length > 0 ? { providers: [...new Set(providers)], skipped: [] } : null;
98
+ let loaded = config ?? null;
99
+ if (!resolved) {
100
+ if (!loaded && repoRoot) loaded = await (readConfig ?? defaultReadConfig)(repoRoot);
101
+ if (loaded) {
102
+ resolved = await (resolveFromConfig ?? defaultResolveFromConfig)(loaded);
103
+ }
104
+ }
105
+ const map = mapCli ?? defaultMapCli;
106
+ const mapped = await map(cli, loaded?.parallelism ?? null);
107
+ if (!('provider' in mapped) || !mapped.provider) {
108
+ // Typed-shaped refusal without importing the src error class (which
109
+ // would couple this sync-throwing path to the compiled `dist`
110
+ // layout): callers discriminate on `code`, never `instanceof`.
111
+ throw Object.assign(
112
+ new Error(`cannot ensure worker context: CLI '${cli}' maps to no AIWG provider (${mapped.skipped ?? 'unmapped'})`),
113
+ { code: 'OG_WORKER_CONTEXT_UNAVAILABLE',
114
+ remedy: `configure parallelism.clis.${cli} with a providers:<id> adapter, or use a CLI whose adapter maps to a known provider`,
115
+ workdir },
116
+ );
117
+ }
118
+ const provider = mapped.provider;
119
+ const deployProviders = resolved ? [...new Set([...resolved.providers, provider])] : [provider];
120
+ const ensure = ensureFn ?? (async (opts) => {
121
+ const { ensureWorkerContextBootstrap } = await importImpl(import.meta.url, 'config/worker-context.js');
122
+ return ensureWorkerContextBootstrap(opts);
123
+ });
124
+ const outcome = await ensure({
125
+ workdir, provider, providers: deployProviders, aiwgBin,
126
+ ...(statusFn ? { statusFn } : {}),
127
+ ...(deployFn ? { deployFn } : {}),
128
+ ...(skipWorktreeFn ? { skipWorktreeFn } : {}),
129
+ });
130
+ return { ...outcome, provider, skipped: resolved?.skipped ?? [] };
131
+ }
132
+
133
+ /** Materialize the union set into the worktree config through the src-side sync. */
134
+ async function defaultSyncProviders(worktreePath, providers) {
135
+ const { syncWorkerContextProviders } = await importImpl(import.meta.url, 'config/worker-context.js');
136
+ return syncWorkerContextProviders({ workdir: worktreePath, providers });
137
+ }
138
+
139
+ /** Single-pass argv through the src-side canonical builder (never `--providers`). */
140
+ async function defaultDeployArgv(providers, worktreePath) {
141
+ const { buildWorkerContextDeployArgs } = await importImpl(import.meta.url, 'config/worker-context.js');
142
+ return buildWorkerContextDeployArgs({ providers, target: worktreePath });
143
+ }
144
+
145
+ /**
146
+ * Default single-pass AIWG context deploy (#3685): sync the union set
147
+ * into the worktree config, then one `aiwg use all --target <worktree>`
148
+ * covering every configured provider. Never one deploy per provider:
149
+ * per-provider invocations rewrite AGENTS.md piecemeal and clobber each
150
+ * other's rule sections, while a single pass keeps them all. No
151
+ * `--providers` flag: it never reaches project-scope deploys
152
+ * (`executeCore` resolves from config), so the configured-provider path
153
+ * above is the supported mechanism. Injectable via
154
+ * `prepareWorkerWorktree()`'s `deployContextFn` for synthetic tests;
155
+ * `syncFn` stubs the config sync as `(worktreePath, providers)` and
156
+ * `argvFn` stubs the argv builder as `(providers, target)`.
157
+ *
158
+ * @returns {Promise<{providers: string[]}>}
159
+ */
160
+ export async function defaultDeployWorkerContext({ worktreePath, providers, aiwgBin = 'aiwg', spawn = spawnSync, timeoutMs = 300000, syncFn = null, argvFn = null } = {}) {
161
+ if (typeof worktreePath !== 'string' || !worktreePath || typeof aiwgBin !== 'string' || !aiwgBin) {
162
+ throw new Error('worker context deploy requires a worktree path and an aiwg binary');
163
+ }
164
+ if (!Array.isArray(providers) || providers.length === 0) {
165
+ throw new Error('worker context deploy requires a non-empty provider list');
166
+ }
167
+ const sync = syncFn ?? defaultSyncProviders;
168
+ const synced = await sync(worktreePath, providers);
169
+ const syncedProviders = Array.isArray(synced?.providers) && synced.providers.length > 0 ? synced.providers : [...providers];
170
+ const buildArgv = argvFn ?? defaultDeployArgv;
171
+ const argv = await buildArgv(syncedProviders, worktreePath);
172
+ const result = spawn(aiwgBin, argv, { cwd: worktreePath, encoding: 'utf8', timeout: timeoutMs });
173
+ if (result.error) throw new Error(`aiwg ${argv.join(' ')} failed to run: ${result.error.message}`);
174
+ if (result.status !== 0) {
175
+ throw new Error(`aiwg ${argv.join(' ')} exited ${result.status}: ${String(result.stderr ?? '').trim().slice(0, 2000)}`);
176
+ }
177
+ return { providers: [...syncedProviders] };
178
+ }
179
+
180
+ /** AIWG-managed rule-section markers written into a deployed AGENTS.md. */
181
+ const WORKER_CONTEXT_RULE_MARKER = /<!--\s*AIWG:[A-Za-z0-9_-]+:(start|end)\s*-->/;
182
+
183
+ /**
184
+ * Verify a prepared worktree actually carries AIWG rule sections (#3685):
185
+ * the worktree's AGENTS.md must exist and contain AIWG-managed section
186
+ * markers. Throws before launch otherwise — a worktree without them would
187
+ * run the worker with no project rules/skills/hooks. Injectable via
188
+ * `prepareWorkerWorktree()`'s `verifyContextFn` for synthetic tests.
189
+ *
190
+ * @returns {{verified: boolean, providers: string[]}}
191
+ */
192
+ export function verifyWorkerContextSections({ worktreePath, providers = [] } = {}) {
193
+ const agentsMd = path.join(worktreePath, 'AGENTS.md');
194
+ let content;
195
+ try {
196
+ content = readFileSync(agentsMd, 'utf8');
197
+ } catch {
198
+ throw new Error(`worker context verification failed: ${agentsMd} is missing; the worker would run without AIWG rule sections`);
199
+ }
200
+ if (!WORKER_CONTEXT_RULE_MARKER.test(content)) {
201
+ throw new Error(`worker context verification failed: ${agentsMd} has no AIWG rule sections (expected <!-- AIWG:<rule>:start --> markers for ${[...providers].join(',') || 'the configured providers'})`);
202
+ }
203
+ return { verified: true, providers: [...providers] };
204
+ }
205
+
206
+ /**
207
+ * Tracked files the context deploy modified, so the caller can hide them
208
+ * from the worker diff. Parses `git status --porcelain=v1` with untracked
209
+ * files excluded: every remaining entry is a tracked modification (the
210
+ * fresh worktree starts clean, so each one is deploy-caused).
211
+ *
212
+ * @returns {string[]} Repo-relative paths of tracked-modified files.
213
+ */
214
+ export function listDeployDirtiedBridges({ worktreePath, spawn = spawnSync, timeoutMs = 30000 } = {}) {
215
+ // Raw spawn output, never a trimmed helper: trim would eat the
216
+ // significant leading space of the first porcelain line (` M AGENTS.md`
217
+ // trims to `M AGENTS.md`, shifting slice(3) one byte into the filename).
218
+ const result = spawn('git', ['status', '--porcelain=v1', '--untracked-files=no'], { cwd: worktreePath, encoding: 'utf8', timeout: timeoutMs });
219
+ if (result.error) throw new Error(`git status failed to run: ${result.error.message}`);
220
+ if (result.status !== 0) {
221
+ throw new Error(`git status exited ${result.status}: ${String(result.stderr ?? '').trim()}`);
222
+ }
223
+ const files = [];
224
+ for (const line of String(result.stdout ?? '').split('\n')) {
225
+ if (!line.trim()) continue;
226
+ // `XY PATH` or `XY ORIG -> NEW` for renames; take the new path, which is
227
+ // what the worker diff would report.
228
+ const raw = line.slice(3);
229
+ const arrow = raw.indexOf(' -> ');
230
+ const rel = arrow >= 0 ? raw.slice(arrow + 4) : raw;
231
+ // Porcelain quotes paths with special bytes; a deploy that creates such
232
+ // a name is refused rather than mis-flagged.
233
+ if (!rel || rel.startsWith('"')) throw new Error(`worker context deploy dirtied an unflaggeable path: ${line}`);
234
+ files.push(rel);
235
+ }
236
+ return files;
237
+ }
@@ -0,0 +1,132 @@
1
+ // Classify unreadable same-uid processes as possible escaped worker descendants or unrelated trees.
2
+ import { constants } from 'node:fs';
3
+
4
+ // Ancestry walks stay bounded: a pathological ppid chain can never turn this
5
+ // proof into an unbounded scan. Depth and the caller's time bound both apply.
6
+ export const MAX_WORKER_ANCESTRY_DEPTH = 32;
7
+
8
+ const vanished = error => ['ENOENT', 'ESRCH'].includes(error?.code);
9
+
10
+ function readSmallFile(fs, filename, maxBytes = 4096) {
11
+ let fd;
12
+ try {
13
+ fd = fs.openSync(filename, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
14
+ const buffer = Buffer.alloc(maxBytes + 1);
15
+ const chunks = [];
16
+ let bytes = 0;
17
+ for (;;) {
18
+ const size = fs.readSync(fd, buffer, 0, buffer.length, null);
19
+ if (!size) break;
20
+ chunks.push(buffer.subarray(0, size));
21
+ bytes += size;
22
+ if (bytes > maxBytes) return { tooLarge: true };
23
+ }
24
+ return { text: Buffer.concat(chunks).toString('utf8') };
25
+ } catch (error) { return { error }; }
26
+ finally { if (fd !== undefined) fs.closeSync(fd); }
27
+ }
28
+
29
+ // Parse the link fields of /proc/<pid>/stat: ppid, process group, session and
30
+ // start time. The comm field may contain spaces and parentheses, so field 3
31
+ // begins after its LAST ')'. Returns null when the text is malformed.
32
+ export function parseProcStatLink(text) {
33
+ if (typeof text !== 'string') return null;
34
+ const end = text.lastIndexOf(')');
35
+ if (end < 0) return null;
36
+ const fields = text.slice(end + 1).trim().split(/\s+/);
37
+ if (fields.length < 20) return null;
38
+ const [ppid, pgrp, session] = [fields[1], fields[2], fields[3]].map(Number);
39
+ const start = fields[19];
40
+ if (![ppid, pgrp, session].every(Number.isSafeInteger) || typeof start !== 'string' || !/^\d+$/.test(start)) return null;
41
+ return { ppid, pgrp, session, start };
42
+ }
43
+
44
+ // Read /proc/<pid>/cgroup verbatim (cgroup membership stays readable for
45
+ // non-dumpable processes). Normalized like sampleProcessCgroup: the bare path
46
+ // for the unified hierarchy, raw content otherwise. Null when unreadable.
47
+ export function readProcCgroup(fs, path) {
48
+ const read = readSmallFile(fs, `${path}/cgroup`);
49
+ if (read.error || read.tooLarge) return null;
50
+ const raw = String(read.text ?? '').trim();
51
+ if (!raw) return null;
52
+ const unified = raw.split('\n').find(line => line.startsWith('0::'));
53
+ const group = unified ? unified.slice(3) : raw;
54
+ return group || null;
55
+ }
56
+
57
+ const isTicks = value => typeof value === 'string' && /^\d+$/.test(value);
58
+
59
+ /**
60
+ * Decide whether an unreadable same-uid process could be an escaped descendant
61
+ * of the recorded worker leader, using only evidence that stays readable for
62
+ * non-dumpable (hardened, e.g. Codex) processes: /proc/<pid>/stat (ppid,
63
+ * process group, session, start time) and /proc/<pid>/cgroup.
64
+ *
65
+ * Returns 'predates' (started strictly before the worker: cannot descend from
66
+ * it), 'unrelated' (its readable ancestry reaches another live process that
67
+ * is not ours, e.g. a different conductor's worker), 'vanished' (it
68
+ * disappeared mid-scan), or 'possible-descendant' (fail closed: ancestry
69
+ * reaches the worker leader or the worker's original session/process group,
70
+ * shares the worker's private cgroup, is reparented to init/a subreaper with
71
+ * no other link, or any link in the chain is unreadable).
72
+ */
73
+ export function classifyUnreadableWorkerProcess({ fs, procRoot, pid, uid, workerPid,
74
+ workerPgid, workerSession, workerStart, workerCgroup, selfCgroup,
75
+ now = () => performance.now(), deadline = now() + 100, maxDepth = MAX_WORKER_ANCESTRY_DEPTH } = {}) {
76
+ const path = `${procRoot}/${pid}`;
77
+ const first = readSmallFile(fs, `${path}/stat`);
78
+ if (first.error) return vanished(first.error) ? 'vanished' : 'possible-descendant';
79
+ const link = !first.tooLarge && parseProcStatLink(first.text);
80
+ if (!link) return 'possible-descendant';
81
+ if (!isTicks(workerStart)) return 'possible-descendant';
82
+ if (BigInt(link.start) < BigInt(workerStart)) return 'predates';
83
+ // A process still in the worker's original session or process group is
84
+ // treated as a possible descendant even when its ppid chain is broken
85
+ // (an orphan that never called setsid keeps both).
86
+ if ((Number.isSafeInteger(workerPgid) && workerPgid > 0 && link.pgrp === workerPgid)
87
+ || (Number.isSafeInteger(workerSession) && workerSession > 0 && link.session === workerSession)) {
88
+ return 'possible-descendant';
89
+ }
90
+ // A shared cgroup links the process to the worker only when the worker's
91
+ // own cgroup is private: a degraded worker shares its session cgroup with
92
+ // every sibling, so that match proves nothing and ancestry decides.
93
+ if (workerCgroup) {
94
+ const candidate = readProcCgroup(fs, path);
95
+ if (candidate !== null && candidate === workerCgroup && workerCgroup !== selfCgroup) return 'possible-descendant';
96
+ }
97
+ let current = link.ppid;
98
+ for (let depth = 0; ; depth++) {
99
+ if (now() >= deadline) return 'possible-descendant';
100
+ if (depth >= maxDepth) return 'possible-descendant';
101
+ if (current === workerPid) {
102
+ // A pid number alone is not proof: verify the generation, since the
103
+ // number may have been reused after the leader exited.
104
+ const leader = readSmallFile(fs, `${procRoot}/${current}/stat`);
105
+ if (leader.error || leader.tooLarge) return 'possible-descendant';
106
+ const leaderLink = parseProcStatLink(leader.text);
107
+ if (!leaderLink) return 'possible-descendant';
108
+ if (leaderLink.start === workerStart) return 'possible-descendant';
109
+ current = leaderLink.ppid;
110
+ continue;
111
+ }
112
+ // Reparented to init (or the kernel): with no other link this is the
113
+ // honest unknown case, never proof of absence.
114
+ if (!Number.isSafeInteger(current) || current <= 1) return 'possible-descendant';
115
+ let owner;
116
+ try { owner = fs.statSync(`${procRoot}/${current}`).uid; }
117
+ catch { return 'possible-descendant'; }
118
+ // Another identity's tree (including container shims and managers owned
119
+ // by a different uid) can never be our worker's descendant. A setuid
120
+ // escape across uids is out of scope for this same-uid scan.
121
+ if (owner !== uid) return 'unrelated';
122
+ const next = readSmallFile(fs, `${procRoot}/${current}/stat`);
123
+ if (next.error || next.tooLarge) return 'possible-descendant';
124
+ const nextLink = parseProcStatLink(next.text);
125
+ if (!nextLink) return 'possible-descendant';
126
+ // This ancestor predates the worker, so it cannot descend from it — and
127
+ // neither can anything reached through it (a sibling subtree, another
128
+ // conductor's parallel worker, a session manager).
129
+ if (BigInt(nextLink.start) < BigInt(workerStart)) return 'unrelated';
130
+ current = nextLink.ppid;
131
+ }
132
+ }