@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
@@ -29,7 +29,7 @@ import { loadCliCommandsContribution, registerCliCommands, registerHooks, } from
29
29
  import { translateSkillsToCommands, providerNeedsCommands } from '../../plugin/skill-command-translator.js';
30
30
  import * as ui from '../ui.js';
31
31
  import { readAiwgConfig, writeAiwgConfig, updateInstalled, hashManifest, emptyConfig, getProjectDir, getConfigPath } from '../../config/aiwg-config.js';
32
- import { appendGitignore } from '../../config/gitignore.js';
32
+ import { AIWG_RUNTIME_PATTERNS, appendGitignore } from '../../config/gitignore.js';
33
33
  import { getLogger } from '../log.js';
34
34
  import { installCockpit } from './cockpit.js';
35
35
  import { initHandler } from './init.js';
@@ -64,6 +64,7 @@ const PREPARABLE_BUILTIN_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'herm
64
64
  // Distinct from agentsmith (which creates subagent personas).
65
65
  import { generate as generateContextFiles, discoverDeployedArtifacts, } from '../../smiths/context-pipeline/index.js';
66
66
  import { verifyModelWrapperDeployment } from '../../models/wrapper-deployment.js';
67
+ import { countDeployedBundleFiles, countDeployedBundleRules } from './use-bundle-counts.js';
67
68
  import { aggregateUseDeploymentResult, buildDryRunUseResult, renderUseDeploymentResult, verifyProviderDeployment, } from '../services/deployment-verification.js';
68
69
  import { finalizeProviderTransformationReceipt, providerReceiptHasLocalSources, sourceVerificationsFromSignedWebRelease, } from '../../providers/transformation-receipt-integration.js';
69
70
  import { loadResourceTrustRootFile, resolveWebRelease, } from '../../resources/web-release.js';
@@ -501,11 +502,19 @@ function getProviderKernelSkillsPath(provider) {
501
502
  const PROVIDER_GENERATED_GITIGNORE_PATTERNS = {
502
503
  codex: ['.codex/', '.agents/'],
503
504
  };
504
- async function ensureProviderGeneratedDirsIgnored(projectRoot, provider, opts) {
505
+ /**
506
+ * Patterns `aiwg use` ensures in the project's .gitignore on every deploy:
507
+ * the AIWG runtime subdirs doctor expects, plus any provider-generated dirs.
508
+ * Append-only via appendGitignore — operator content is never rewritten (#3635).
509
+ */
510
+ export function gitignorePatternsForDeploy(provider) {
511
+ return [...AIWG_RUNTIME_PATTERNS, ...(PROVIDER_GENERATED_GITIGNORE_PATTERNS[provider] ?? [])];
512
+ }
513
+ export async function ensureProviderGeneratedDirsIgnored(projectRoot, provider, opts) {
505
514
  if (opts.dryRun)
506
515
  return;
507
- const patterns = PROVIDER_GENERATED_GITIGNORE_PATTERNS[provider];
508
- if (!patterns || patterns.length === 0)
516
+ const patterns = gitignorePatternsForDeploy(provider);
517
+ if (patterns.length === 0)
509
518
  return;
510
519
  try {
511
520
  const result = await appendGitignore(projectRoot, patterns);
@@ -1020,24 +1029,6 @@ async function listBundleMdStems(bundlePath, subdir) {
1020
1029
  return [];
1021
1030
  }
1022
1031
  }
1023
- async function countDeployedBundleFiles(bundlePath, subdir, target, deployPath, extensions) {
1024
- if (!deployPath)
1025
- return 0;
1026
- const stems = await listBundleMdStems(bundlePath, subdir);
1027
- if (stems.length === 0)
1028
- return 0;
1029
- const destDir = resolveDeployPath(target, deployPath);
1030
- let count = 0;
1031
- for (const stem of stems) {
1032
- for (const ext of extensions) {
1033
- if (await fileExists(path.join(destDir, `${stem}${ext}`))) {
1034
- count++;
1035
- break;
1036
- }
1037
- }
1038
- }
1039
- return count;
1040
- }
1041
1032
  async function listBundleSkillNameCandidates(bundlePath) {
1042
1033
  const skillsRoot = path.join(bundlePath, 'skills');
1043
1034
  try {
@@ -1099,7 +1090,7 @@ async function countBundleDeployedArtifacts(bundlePath, target, provider) {
1099
1090
  agents: await countDeployedBundleFiles(bundlePath, 'agents', target, paths.agents, ['.md', '.toml']),
1100
1091
  commands: await countDeployedBundleFiles(bundlePath, 'commands', target, paths.commands, ['.md']),
1101
1092
  skills: await countDeployedBundleSkills(bundlePath, target, provider),
1102
- rules: await countDeployedBundleFiles(bundlePath, 'rules', target, paths.rules, ['.md', '.mdc']),
1093
+ rules: await countDeployedBundleRules(bundlePath, target, paths.rules),
1103
1094
  };
1104
1095
  }
1105
1096
  const SKILL_SUPPORT_REFERENCE = /(?:^|[\s`('"\[])((?:templates|references|scripts|assets)\/[A-Za-z0-9._@/+\-]+)(?=$|[\s`)'"\],:;])/gm;
@@ -3513,11 +3504,10 @@ export class UseHandler {
3513
3504
  try {
3514
3505
  const manifestPath = path.join(addonSource, 'manifest.json');
3515
3506
  const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
3516
- // Provider rule aggregation may rename many source rules into one
3517
- // managed index. Record this addon's contributed artifact counts,
3518
- // matching framework registry semantics, rather than trying to
3519
- // attribute shared aggregate filenames after deployment.
3520
- const counts = await countBundleSourceArtifacts(addonSource);
3507
+ // Record only artifacts actually written for this provider. Bridge-only
3508
+ // providers (e.g. project-scope grokbot) write zero native files,
3509
+ // so source-manifest counts would leave phantom registry entries.
3510
+ const counts = await countBundleDeployedArtifacts(addonSource, target, provider);
3521
3511
  const updated = updateInstalled(config, framework, provider, counts, {
3522
3512
  version: manifest.version ?? (await getVersionInfo()).version,
3523
3513
  source: 'bundled',
@@ -13,6 +13,7 @@ import fs from 'fs';
13
13
  import fsp from 'fs/promises';
14
14
  import path from 'path';
15
15
  import { createScriptRunner } from './script-runner.js';
16
+ import { DOCTOR_HELP } from './doctor-help.js';
16
17
  import { getFrameworkRoot } from '../../channel/manager.mjs';
17
18
  import { updateInstallation } from '../../update/service.mjs';
18
19
  import { useHandler as useFrameworkHandler } from './use.js';
@@ -494,30 +495,6 @@ async function runUserScopeDoctor(verbose) {
494
495
  message: lines.join('\n') + '\n',
495
496
  };
496
497
  }
497
- const DOCTOR_HELP = `aiwg doctor — check AIWG installation health
498
-
499
- Usage:
500
- aiwg doctor [options]
501
- aiwg -doctor [options]
502
- aiwg --doctor [options]
503
-
504
- Options:
505
- --deployment Verify configured provider deployments
506
- --provider <name> Limit deployment verification to one provider
507
- --bundle <name> Limit deployment verification to one bundle
508
- --scope <scope> Verification scope: project or user
509
- --user Alias for --scope user
510
- --qualify With --probe-workers, record installation and configured model/role approval
511
- --yes Confirm worker qualification without a terminal
512
- --dry-run With --probe-workers, preview qualification without writes
513
- --cli NAME Limit worker probes/qualification to one configured CLI
514
- --decision-offload Show offline advisory decision readiness and adoption report
515
- --probe-workers Run bounded worker CLI version probes (default: config-only, no subprocess)
516
- --project-local Include project-local artifact diagnostics
517
- --json Emit machine-readable output (with --probe-workers: one worker-probe record per CLI; with --deployment: deployment verification; with --decision-offload: offline snapshot)
518
- --verbose, -v Include verbose diagnostics
519
- --help, -h Show this help
520
- `;
521
498
  /**
522
499
  * Handler for doctor command
523
500
  *
@@ -1,3 +1,4 @@
1
+ import { validateHitlPolicy } from '../policy/hitl-validation.js';
1
2
  import { normalizeAiwgConfigCandidate } from './config-normalization.js';
2
3
  import { atomicConfigWrite, withWorkerWriteLock } from './worker-write.js';
3
4
  import { workerConfigDigest } from './worker-config-digest.js';
@@ -979,6 +980,9 @@ export async function readAiwgConfigCandidate(projectDir) {
979
980
  if (workspaceErrors.length > 0) {
980
981
  throw new Error(`Invalid .aiwg/aiwg.config:\n${workspaceErrors.join('\n')}`);
981
982
  }
983
+ const hitlErrors = validateHitlPolicy(parsed.authorization?.hitl);
984
+ if (hitlErrors.length)
985
+ throw new Error(hitlErrors.join("\n"));
982
986
  const authorizationErrors = parsed.authorization
983
987
  ? validateAuthorization(parsed.authorization).filter(item => item.severity === 'error')
984
988
  : [];
@@ -1050,6 +1054,9 @@ async function writeAiwgConfigUnlocked(projectDir, config) {
1050
1054
  throw error;
1051
1055
  }
1052
1056
  }
1057
+ const hitlErrors = validateHitlPolicy(config.authorization?.hitl);
1058
+ if (hitlErrors.length)
1059
+ throw new Error(hitlErrors.join("\n"));
1053
1060
  const pinConfigs = loadRulePinConfigs(projectDir, config);
1054
1061
  const currentCapabilities = new Set(installedRuleCapabilities(pinConfigs));
1055
1062
  const removed = previous ? installedRuleCapabilities({ ...pinConfigs, project: previous }).filter(id => !currentCapabilities.has(id)) : [];
@@ -0,0 +1,89 @@
1
+ // Exposes HITL inspection, validation, preview, enable/revoke and signed receipts under config/steward.
2
+ import { readFile } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { AiwgError, EXIT_CODES } from '../cli/errors.js';
5
+ import { getProjectDir, readAiwgConfig, writeAiwgConfig } from './aiwg-config.js';
6
+ import { ensureAuthorization, loadHitlLayers } from '../policy/hitl-config.js';
7
+ import { resolveHitlAuthorization } from '../policy/hitl.js';
8
+ import { validateHitlPolicy } from '../policy/hitl-validation.js';
9
+ export const authorizationUsage = `aiwg config authorization (also aiwg steward authorization)
10
+ inspect | validate [--target DIRECTORY]
11
+ preview --action-file FILE [--target DIRECTORY]
12
+ enable --policy-file FILE [--target DIRECTORY]
13
+ revoke [--profile NAME] [--target DIRECTORY]
14
+ receipt --action-file FILE [--target DIRECTORY]
15
+ Preview is read-only. Receipt writes the decision to the existing signed effect ledger;
16
+ it does not perform the proposed action. Action files cannot supply session authorization.`;
17
+ const usageError = (message) => new AiwgError({ code: 'ERR_AUTHORIZATION_USAGE', message, exitCode: EXIT_CODES.USAGE });
18
+ async function jsonFile(file) {
19
+ try {
20
+ return JSON.parse(await readFile(file, 'utf8'));
21
+ }
22
+ catch {
23
+ throw usageError('Authorization JSON input could not be read or parsed');
24
+ }
25
+ }
26
+ export async function authorizationCli(args, cwd = process.cwd()) {
27
+ if (!args.length || args.includes('--help') || args[0] === 'help') {
28
+ console.log(authorizationUsage);
29
+ return;
30
+ }
31
+ const [operation, ...rest] = args;
32
+ const flags = {};
33
+ const allowed = ['--target', ...(operation === 'enable' ? ['--policy-file'] : []),
34
+ ...(['preview', 'receipt'].includes(operation) ? ['--action-file'] : []), ...(operation === 'revoke' ? ['--profile'] : [])];
35
+ for (let i = 0; i < rest.length; i += 2) {
36
+ if (!allowed.includes(rest[i]) || !rest[i + 1] || rest[i + 1].startsWith('--') || flags[rest[i]])
37
+ throw usageError('Invalid authorization option');
38
+ flags[rest[i]] = rest[i + 1];
39
+ }
40
+ const target = resolve(getProjectDir({ cwd }, rest));
41
+ if (operation === 'inspect' || operation === 'validate') {
42
+ const loaded = await loadHitlLayers(target);
43
+ console.log(JSON.stringify(operation === 'inspect' ? loaded : { valid: true, layers: loaded.layers.length }, null, 2));
44
+ return;
45
+ }
46
+ if (operation === 'preview' || operation === 'receipt') {
47
+ if (!flags['--action-file'])
48
+ throw usageError('--action-file is required');
49
+ const action = await jsonFile(resolve(cwd, flags['--action-file']));
50
+ const loaded = await loadHitlLayers(target);
51
+ const context = { now: new Date().toISOString(), layers: loaded.layers };
52
+ if (operation === 'preview')
53
+ console.log(JSON.stringify(resolveHitlAuthorization(action, context), null, 2));
54
+ else {
55
+ const { effectSession, openLedger } = await import('../effects/host.js');
56
+ const { recordHitlAuthorization } = await import('../policy/hitl-receipt.js');
57
+ const session = await effectSession(loaded.projectDir, 'custom');
58
+ console.log(JSON.stringify(await recordHitlAuthorization(openLedger(session), action, context), null, 2));
59
+ }
60
+ return;
61
+ }
62
+ if (operation !== 'enable' && operation !== 'revoke')
63
+ throw usageError('Unknown authorization operation');
64
+ const config = await readAiwgConfig(target);
65
+ if (!config)
66
+ throw usageError('No project config found; initialize project config first');
67
+ const authorization = ensureAuthorization(config);
68
+ if (operation === 'enable') {
69
+ if (!flags['--policy-file'])
70
+ throw usageError('--policy-file is required');
71
+ const policy = await jsonFile(resolve(cwd, flags['--policy-file']));
72
+ const errors = validateHitlPolicy(policy);
73
+ if (policy === undefined || errors.length)
74
+ throw usageError('Invalid authorization.hitl policy');
75
+ authorization.hitl = { ...policy, enabled: true };
76
+ }
77
+ else if (authorization.hitl) {
78
+ if (flags['--profile']) {
79
+ if (!Object.hasOwn(authorization.hitl.profiles, flags['--profile']))
80
+ throw usageError('Unknown authorization profile');
81
+ delete authorization.hitl.profiles[flags['--profile']];
82
+ }
83
+ else
84
+ authorization.hitl.enabled = false;
85
+ }
86
+ await writeAiwgConfig(target, config);
87
+ console.log(JSON.stringify({ operation, enabled: authorization.hitl?.enabled ?? false }));
88
+ }
89
+ //# sourceMappingURL=authorization-cli.js.map
@@ -17,6 +17,7 @@ Subcommands:
17
17
  path Print config directory path
18
18
  edit Open config in $EDITOR/$VISUAL (requires a terminal)
19
19
  edit --print-path Print the config file path instead of opening an editor
20
+ authorization Inspect/validate/preview/enable/revoke bounded HITL policy
20
21
  gitignore Show/check/fix .gitignore AIWG entries
21
22
 
22
23
  Reviewed worker policy pair (project scope, no prompts):
@@ -33,6 +33,11 @@ const _scriptDir = path.dirname(fileURLToPath(import.meta.url));
33
33
  * Main CLI entry point for `aiwg config <subcommand> [args]`
34
34
  */
35
35
  export async function main(args) {
36
+ if (args[0] === 'authorization') {
37
+ const { authorizationCli } = await import('./authorization-cli.js');
38
+ await authorizationCli(args.slice(1));
39
+ return;
40
+ }
36
41
  if (await handleWorkerAdoptionArgs(args, process.cwd(), 'config'))
37
42
  return;
38
43
  // Extract --config-dir flag before routing
@@ -291,7 +296,7 @@ async function projectConfigSet(key, raw, args, quiet = false) {
291
296
  }
292
297
  // Coerce booleans for known boolean fields
293
298
  let value = raw;
294
- if (key === 'rules' || key.startsWith('rules.') || key === 'parallelism' || /^parallelism\.(worker_dispatch|worker_mode|groups|clis|admission|budgets|health|retry|activation)(\.|$)/.test(key)) {
299
+ if (key === 'authorization' || key.startsWith('authorization.') || key === 'rules' || key.startsWith('rules.') || key === 'parallelism' || /^parallelism\.(worker_dispatch|worker_mode|groups|clis|admission|budgets|health|retry|activation)(\.|$)/.test(key)) {
295
300
  try {
296
301
  value = JSON.parse(raw);
297
302
  }
@@ -397,6 +402,10 @@ async function projectConfigSet(key, raw, args, quiet = false) {
397
402
  ? (existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : emptyConfig())
398
403
  : (await readAiwgConfig(projectDir)) ?? emptyConfig();
399
404
  const before = structuredClone(getDottedPath(cfg, key));
405
+ if (key.startsWith('authorization.hitl')) {
406
+ const { ensureAuthorization } = await import('../policy/hitl-config.js');
407
+ ensureAuthorization(cfg);
408
+ }
400
409
  setDottedPath(cfg, key, value);
401
410
  const externalLinkErrors = validateExternalLinks(cfg.externalLinks);
402
411
  if (externalLinkErrors.length > 0) {
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Worker AIWG context providers and launch-time bootstrap ensure (#3685).
3
+ *
4
+ * Orchestration must automatically `aiwg use` the providers configured for
5
+ * it, so any new worker process starts with the AIWG bootstrap in place.
6
+ * This module owns the config-side half: resolving WHICH providers
7
+ * (config `providers` plus every CLI configured under
8
+ * `parallelism.clis` / `parallelism.groups.*.members`, mapped through the
9
+ * provider registry) and checking/ensuring that bootstrap inside exactly
10
+ * one worker workdir (never outside it).
11
+ *
12
+ * The tools/ half (`tools/ralph-external/lib/worker-context.mjs`) loads the
13
+ * config through `readAiwgConfig` and injects it here; direct callers may
14
+ * pass an already-loaded config object instead.
15
+ */
16
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
17
+ import { spawnSync } from 'node:child_process';
18
+ import path from 'node:path';
19
+ import { getProviderArtifactPathStrings, getProviderDefinition, normalizeProviderDefinitionId, } from '../providers/provider-definitions.js';
20
+ import { emptyConfig, readAiwgConfig, writeAiwgConfig, } from './aiwg-config.js';
21
+ /**
22
+ * Typed refusal when a worker's AIWG bootstrap cannot be ensured.
23
+ * `remedy` names the operator action that unblocks the launch.
24
+ */
25
+ export class WorkerContextBootstrapError extends Error {
26
+ code = 'OG_WORKER_CONTEXT_UNAVAILABLE';
27
+ remedy;
28
+ provider;
29
+ workdir;
30
+ constructor(message, remedy, opts) {
31
+ super(message);
32
+ this.name = 'WorkerContextBootstrapError';
33
+ this.remedy = remedy;
34
+ if (opts?.provider !== undefined)
35
+ this.provider = opts.provider;
36
+ if (opts?.workdir !== undefined)
37
+ this.workdir = opts.workdir;
38
+ }
39
+ }
40
+ /** Explicit provider lists must be non-empty arrays of deployable ids. */
41
+ export function validateExplicitWorkerContextProviders(providers) {
42
+ if (!Array.isArray(providers) || providers.length === 0
43
+ || providers.some(p => typeof p !== 'string' || p.length === 0 || p.includes('\0') || p.includes(','))) {
44
+ throw new WorkerContextBootstrapError('worker context providers must be a non-empty array of provider ids', 'pass an explicit non-empty provider list, or configure providers plus orchestration CLIs in .aiwg/aiwg.config');
45
+ }
46
+ return [...new Set(providers)];
47
+ }
48
+ const ADAPTER_REFERENCE = /^(providers|workerAdapters):([a-z][a-z0-9-]{0,62})$/;
49
+ /**
50
+ * Map one orchestration CLI id to its AIWG provider id.
51
+ * A CLI without an explicit `parallelism.clis` entry defaults to
52
+ * `providers:<cli>` (the same default `resolveWorkerGroupInstallation`
53
+ * applies). Non-provider adapters (e.g. `workerAdapters:local-http`) and
54
+ * ids the registry does not know are reported as skipped, never guessed.
55
+ */
56
+ export function mapCliToProviderId(cli, parallelism) {
57
+ if (typeof cli !== 'string' || cli.length === 0) {
58
+ return { skipped: `empty CLI reference` };
59
+ }
60
+ const raw = typeof parallelism?.clis?.[cli]?.adapter === 'string'
61
+ && parallelism.clis[cli].adapter.length > 0
62
+ ? parallelism.clis[cli].adapter
63
+ : `providers:${cli}`;
64
+ const match = ADAPTER_REFERENCE.exec(raw);
65
+ if (!match || match[1] !== 'providers') {
66
+ return { skipped: `clis/${cli}: ${raw}` };
67
+ }
68
+ const provider = normalizeProviderDefinitionId(match[2]);
69
+ if (!provider)
70
+ return { skipped: `clis/${cli}: ${raw} (unknown provider)` };
71
+ return { provider };
72
+ }
73
+ /**
74
+ * Resolve the full worker-context provider set: the union of the config's
75
+ * own `providers` and every CLI configured for orchestration
76
+ * (`parallelism.clis` keys/adapters and the `cli` of every
77
+ * `parallelism.groups.*.members[]`), mapped to AIWG provider ids through
78
+ * the provider registry. Non-provider adapters are skipped and reported.
79
+ * Fails closed when the union is empty.
80
+ */
81
+ export function resolveWorkerContextProvidersFromConfig(config) {
82
+ const providers = [];
83
+ const skipped = [];
84
+ const add = (id) => {
85
+ if (!providers.includes(id))
86
+ providers.push(id);
87
+ };
88
+ const skip = (entry) => {
89
+ if (!skipped.includes(entry))
90
+ skipped.push(entry);
91
+ };
92
+ const configured = config?.providers;
93
+ if (Array.isArray(configured)) {
94
+ for (const entry of configured) {
95
+ if (typeof entry !== 'string' || entry.length === 0)
96
+ continue;
97
+ // Configured providers deploy verbatim when the registry does not
98
+ // know them: project-local provider adapters resolve at deploy time
99
+ // (see the `use` handler's project-local resolution), so an unknown
100
+ // id here is the deployer's decision, not this resolver's.
101
+ add(normalizeProviderDefinitionId(entry) ?? entry);
102
+ }
103
+ }
104
+ const parallelism = config?.parallelism;
105
+ for (const cli of Object.keys(parallelism?.clis ?? {})) {
106
+ const mapped = mapCliToProviderId(cli, parallelism);
107
+ if ('provider' in mapped)
108
+ add(mapped.provider);
109
+ else
110
+ skip(mapped.skipped);
111
+ }
112
+ for (const [groupId, group] of Object.entries(parallelism?.groups ?? {})) {
113
+ for (const member of group?.members ?? []) {
114
+ const cli = member?.cli;
115
+ const mapped = mapCliToProviderId(cli, parallelism);
116
+ if ('provider' in mapped)
117
+ add(mapped.provider);
118
+ else
119
+ skip(typeof cli === 'string' && cli ? `groups/${groupId}/members cli '${cli}': ${mapped.skipped}` : `groups/${groupId}/members: ${mapped.skipped}`);
120
+ }
121
+ }
122
+ if (providers.length === 0) {
123
+ throw new WorkerContextBootstrapError('worker context deployment needs configured providers: the union of config providers and orchestration CLIs is empty'
124
+ + (skipped.length > 0 ? ` (skipped: ${skipped.join('; ')})` : ''), 'configure providers and/or parallelism clis/groups in .aiwg/aiwg.config, or pass an explicit provider list');
125
+ }
126
+ return { providers, skipped };
127
+ }
128
+ /** AIWG-managed rule-section markers written into a deployed AGENTS.md. */
129
+ const WORKER_CONTEXT_RULE_MARKER = /<!--\s*AIWG:[A-Za-z0-9_-]+:(start|end)\s*-->/;
130
+ /**
131
+ * Check that a worker workdir already carries one provider's bootstrap:
132
+ * the kernel artifact dirs (`use all` writes kernel skills, rules,
133
+ * behaviors — never the full agent/command surface) and, for providers
134
+ * whose contract targets AGENTS.md, the AIWG rule sections.
135
+ */
136
+ export function workerContextBootstrapStatus({ workdir, provider }) {
137
+ const missing = [];
138
+ const definition = getProviderDefinition(provider);
139
+ if (!definition) {
140
+ return { present: false, missing: [`unknown provider '${provider}'`] };
141
+ }
142
+ const paths = getProviderArtifactPathStrings(provider);
143
+ const kernelDirs = [
144
+ definition.paths.kernelSkills,
145
+ paths?.rules ?? null,
146
+ paths?.behaviors ?? null,
147
+ ].filter((dir) => typeof dir === 'string' && dir.length > 0);
148
+ const requiredDirs = kernelDirs.length > 0
149
+ ? kernelDirs
150
+ : Object.values(paths ?? {}).filter((dir) => typeof dir === 'string' && dir.length > 0);
151
+ for (const dir of [...new Set(requiredDirs)]) {
152
+ const absolute = path.isAbsolute(dir) ? dir : path.join(workdir, dir);
153
+ let entries = null;
154
+ try {
155
+ entries = readdirSync(absolute);
156
+ }
157
+ catch {
158
+ entries = null;
159
+ }
160
+ if (!entries || entries.length === 0)
161
+ missing.push(`provider dir '${dir}'`);
162
+ }
163
+ if ((definition.context?.bootstrapTargets ?? []).includes('AGENTS.md')) {
164
+ let content = null;
165
+ try {
166
+ content = readFileSync(path.join(workdir, 'AGENTS.md'), 'utf8');
167
+ }
168
+ catch {
169
+ content = null;
170
+ }
171
+ if (!content || !WORKER_CONTEXT_RULE_MARKER.test(content)) {
172
+ missing.push('AGENTS.md rule sections');
173
+ }
174
+ }
175
+ return { present: missing.length === 0, missing };
176
+ }
177
+ /**
178
+ * Materialize the union provider set into the worker workdir's own config
179
+ * through the existing loader/writer (never ad-hoc JSON). Existing entries
180
+ * keep their order; union members absent from the worktree base are
181
+ * appended. A missing config is created minimal. An already-current config
182
+ * is left untouched so the deploy below stays the only writer.
183
+ */
184
+ export async function syncWorkerContextProviders(options) {
185
+ const { workdir } = options;
186
+ if (typeof workdir !== 'string' || !path.isAbsolute(workdir)) {
187
+ throw new WorkerContextBootstrapError(`worker context sync needs an absolute workdir, got ${JSON.stringify(workdir)}`, 'sync worker context providers only into the worker workdir', { workdir: typeof workdir === 'string' ? workdir : undefined });
188
+ }
189
+ const providers = validateExplicitWorkerContextProviders(options.providers);
190
+ const read = options.readConfig ?? ((dir) => readAiwgConfig(dir));
191
+ const existing = await read(workdir);
192
+ const current = Array.isArray(existing?.providers)
193
+ ? existing.providers.filter((entry) => typeof entry === 'string')
194
+ : [];
195
+ const merged = [...current];
196
+ for (const provider of providers) {
197
+ if (!merged.includes(provider))
198
+ merged.push(provider);
199
+ }
200
+ const unchanged = merged.length === current.length && merged.every((entry, index) => entry === current[index]);
201
+ if (unchanged)
202
+ return { providers: merged, updated: false };
203
+ const base = existing !== null && typeof existing === 'object' && 'version' in existing
204
+ ? existing
205
+ : {};
206
+ const next = { ...emptyConfig(merged), ...base, providers: merged };
207
+ const write = options.writeConfig ?? ((dir, config) => writeAiwgConfig(dir, config));
208
+ await write(workdir, next);
209
+ return { providers: merged, updated: true };
210
+ }
211
+ /**
212
+ * Canonical one-pass deploy argv: a single `aiwg use all` invocation
213
+ * covering every listed provider. Never one deploy per provider:
214
+ * per-provider invocations rewrite AGENTS.md piecemeal and clobber each
215
+ * other's rule sections, while one pass keeps them all. No `--providers`
216
+ * flag: it feeds only the global bootstrap and the orchestrated
217
+ * verification report — `executeCore` resolves project-scope providers
218
+ * from config (`src/cli/handlers/use.ts`), ignoring both the flag and any
219
+ * explicitly resolved list. The supported project-scope mechanism is the
220
+ * configured-provider path: `syncWorkerContextProviders` first, then this
221
+ * single pass reads the union back from the worktree config.
222
+ */
223
+ export function buildWorkerContextDeployArgs({ providers, target }) {
224
+ // Fail fast on a hostile list; the synced worktree config carries the set.
225
+ validateExplicitWorkerContextProviders(providers);
226
+ return ['use', 'all', '--target', target, '--yes'];
227
+ }
228
+ /**
229
+ * Ensure a worker workdir carries its CLI's AIWG bootstrap before launch.
230
+ * Present: no-op. Missing: sync the union set into the workdir config,
231
+ * then exactly one `aiwg use all` pass into the workdir (never anywhere
232
+ * else), then re-verify. Impossible (no binary, failed deploy, still
233
+ * missing afterwards): throw `WorkerContextBootstrapError` naming the
234
+ * remedy — the caller refuses the launch rather than starting a
235
+ * context-free worker.
236
+ */
237
+ export async function ensureWorkerContextBootstrap(options) {
238
+ const { workdir, provider, aiwgBin = process.env.AIWG_BIN ?? 'aiwg', spawn = spawnSync, timeoutMs = 300000, statusFn = workerContextBootstrapStatus, } = options;
239
+ if (typeof workdir !== 'string' || !path.isAbsolute(workdir) || !existsSync(workdir)) {
240
+ throw new WorkerContextBootstrapError(`worker context bootstrap needs an existing absolute workdir, got ${JSON.stringify(workdir)}`, 'launch the worker with its resolved absolute workdir', { provider, workdir: typeof workdir === 'string' ? workdir : undefined });
241
+ }
242
+ const status = statusFn({ workdir, provider });
243
+ if (status.present)
244
+ return { ensured: false, providers: [provider], skippedBridges: [] };
245
+ const providers = options.providers && options.providers.length > 0
246
+ ? validateExplicitWorkerContextProviders(options.providers)
247
+ : [provider];
248
+ // The configured-provider path: the single pass below resolves its set
249
+ // from the workdir config, so the union must be materialized there
250
+ // first — `--providers` never reaches project-scope deploys.
251
+ const sync = options.syncFn ?? syncWorkerContextProviders;
252
+ let synced;
253
+ try {
254
+ synced = await sync({ workdir, providers });
255
+ }
256
+ catch (error) {
257
+ if (error instanceof WorkerContextBootstrapError)
258
+ throw error;
259
+ throw new WorkerContextBootstrapError(`worker context sync failed: ${error instanceof Error ? error.message : String(error)}`, `write the union providers into ${path.join(workdir, '.aiwg', 'aiwg.config')} (or run 'aiwg use all --target ${workdir} --yes' manually) before launch`, { provider, workdir });
260
+ }
261
+ const argv = buildWorkerContextDeployArgs({ providers: synced.providers, target: workdir });
262
+ const deploy = options.deployFn ?? (({ worktreePath, providers: deployProviders, aiwgBin: bin }) => {
263
+ // worktreePath IS the worker workdir by construction: the only
264
+ // `--target` ever passed is `workdir` above, and the spawn cwd is the
265
+ // same directory, so no deploy write can land outside it.
266
+ if (path.resolve(worktreePath) !== path.resolve(workdir)) {
267
+ throw new WorkerContextBootstrapError(`refusing worker context deploy outside the worker workdir (${worktreePath} !== ${workdir})`, 'deploy worker context only into the worker workdir', { provider, workdir });
268
+ }
269
+ const result = spawn(bin, argv, { cwd: workdir, encoding: 'utf8', timeout: timeoutMs });
270
+ if (result.error) {
271
+ throw new WorkerContextBootstrapError(`worker context deploy failed to run (${result.error?.message});`
272
+ + ` missing in ${workdir}: ${status.missing.join('; ')}`, `install the aiwg CLI on PATH or set AIWG_BIN to its location (tried '${bin}'),`
273
+ + ` or run 'aiwg ${argv.join(' ')}' manually before launch`, { provider, workdir });
274
+ }
275
+ if (result.status !== 0) {
276
+ throw new WorkerContextBootstrapError(`worker context deploy exited ${result.status}: ${String(result.stderr ?? '').trim().slice(0, 2000)};`
277
+ + ` missing in ${workdir}: ${status.missing.join('; ')}`, `run 'aiwg ${argv.join(' ')}' manually and resolve the reported failure before launch`, { provider, workdir });
278
+ }
279
+ return { providers: [...deployProviders] };
280
+ });
281
+ let deployed;
282
+ try {
283
+ deployed = await deploy({ worktreePath: workdir, providers: synced.providers, aiwgBin });
284
+ }
285
+ catch (error) {
286
+ if (error instanceof WorkerContextBootstrapError)
287
+ throw error;
288
+ throw new WorkerContextBootstrapError(`worker context deploy failed: ${error instanceof Error ? error.message : String(error)};`
289
+ + ` missing in ${workdir}: ${status.missing.join('; ')}`, `run 'aiwg ${argv.join(' ')}' manually and resolve the reported failure before launch`, { provider, workdir });
290
+ }
291
+ const after = statusFn({ workdir, provider });
292
+ if (!after.present) {
293
+ throw new WorkerContextBootstrapError(`worker context deploy did not produce the bootstrap: still missing in ${workdir}: ${after.missing.join('; ')}`, `run 'aiwg ${argv.join(' ')}' manually and verify the provider bootstrap before launch`, { provider, workdir });
294
+ }
295
+ // Keep deploy-touched tracked bridges out of the worker diff. A workdir
296
+ // outside any git repository has no tracked files; skip silently there.
297
+ let skippedBridges = [];
298
+ if (options.skipWorktreeFn) {
299
+ skippedBridges = options.skipWorktreeFn({ workdir });
300
+ }
301
+ else {
302
+ try {
303
+ skippedBridges = flagDeployDirtiedSkipWorktree({ workdir, spawn, timeoutMs });
304
+ }
305
+ catch {
306
+ skippedBridges = [];
307
+ }
308
+ }
309
+ return { ensured: true, providers: [...deployed.providers], skippedBridges };
310
+ }
311
+ /** Tracked files the deploy modified, hidden from the worker diff. */
312
+ export function flagDeployDirtiedSkipWorktree({ workdir, spawn = spawnSync, timeoutMs = 30000 }) {
313
+ const status = spawn('git', ['status', '--porcelain=v1', '--untracked-files=no'], { cwd: workdir, encoding: 'utf8', timeout: timeoutMs });
314
+ if (status.error)
315
+ throw new Error(`git status failed: ${status.error?.message}`);
316
+ if (status.status !== 0)
317
+ throw new Error(`git status exited ${status.status}`);
318
+ const files = [];
319
+ for (const line of String(status.stdout ?? '').split('\n')) {
320
+ if (!line.trim())
321
+ continue;
322
+ const raw = line.slice(3);
323
+ const arrow = raw.indexOf(' -> ');
324
+ const rel = arrow >= 0 ? raw.slice(arrow + 4) : raw;
325
+ if (!rel || rel.startsWith('"'))
326
+ throw new Error(`worker context deploy dirtied an unflaggeable path: ${line}`);
327
+ files.push(rel);
328
+ }
329
+ if (files.length > 0) {
330
+ const flag = spawn('git', ['update-index', '--skip-worktree', '--', ...files], { cwd: workdir, encoding: 'utf8', timeout: timeoutMs });
331
+ if (flag.error || flag.status !== 0) {
332
+ throw new Error(`git update-index --skip-worktree failed: ${flag.error?.message ?? String(flag.stderr ?? '').trim()}`);
333
+ }
334
+ }
335
+ return files;
336
+ }
337
+ //# sourceMappingURL=worker-context.js.map
@@ -1,3 +1,4 @@
1
+ import { ceilingDecision } from '../gates/lattice.js';
1
2
  import { ContextBudgetManager } from '../metrics/context-budget.js';
2
3
  import { sha256 } from './compile-cache/identity.js';
3
4
  import { admitEntry } from './entry.js';
@@ -436,9 +437,9 @@ export function buildContextPruningEvaluationReport(input) {
436
437
  findings.add(`missing-input:${name}`);
437
438
  // Automatic rollback triggers from the D26 rollout plan: protected miss, demonstrated quality harm, negative
438
439
  // economics. An inconclusive non-inferiority result (no evidence of harm) holds instead.
439
- const rollback = upstreamDecision === 'ROLLBACK' || findings.has('protected-retention-breach')
440
+ const rollback = findings.has('protected-retention-breach')
440
441
  || findings.has('quality-harm') || findings.has('negative-net-economics');
441
- const decision = rollback ? 'ROLLBACK' : upstreamDecision === 'HOLD' || findings.size > 0 ? 'HOLD' : 'PROMOTE';
442
+ const decision = ceilingDecision(upstreamDecision, rollback ? 'ROLLBACK' : findings.size > 0 ? 'HOLD' : 'PROMOTE');
442
443
  const advisory = [...findings].some(item => item.startsWith('insufficient-') || item.startsWith('quality-metric-missing:')
443
444
  || item.startsWith('missing-input:') || item.startsWith('quality-outcomes-incomplete:')
444
445
  || item.startsWith('quality-non-inferiority-inconclusive:')