@aiwg/cli 2026.10.8 → 2026.10.10

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 (104) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +197 -3
  2. package/bin/aiwg.mjs +8 -0
  3. package/dist/src/artifacts/cli.js +9 -5
  4. package/dist/src/auth/config.js +3 -8
  5. package/dist/src/auth/resource-credentials.js +35 -2
  6. package/dist/src/cli/handlers/diagnose.js +13 -0
  7. package/dist/src/cli/handlers/mc-admission.js +39 -0
  8. package/dist/src/cli/handlers/mc.js +2 -31
  9. package/dist/src/cli/handlers/models-help.js +52 -0
  10. package/dist/src/cli/handlers/models.js +11 -81
  11. package/dist/src/cli/handlers/resource-versions.js +8 -5
  12. package/dist/src/cli/handlers/script-runner.js +13 -1
  13. package/dist/src/cli/handlers/use-addon-profile.js +72 -0
  14. package/dist/src/cli/handlers/use.js +209 -233
  15. package/dist/src/cli/ui.js +1 -0
  16. package/dist/src/config/aiwg-config.js +38 -184
  17. package/dist/src/config/cli-help.js +54 -0
  18. package/dist/src/config/cli.js +5 -69
  19. package/dist/src/config/config-normalization.js +10 -0
  20. package/dist/src/config/worker-activation-gate.js +26 -0
  21. package/dist/src/config/worker-adoption-cli.js +102 -0
  22. package/dist/src/config/worker-adoption.js +368 -0
  23. package/dist/src/config/worker-config-digest.js +14 -0
  24. package/dist/src/config/worker-group-templates.js +150 -0
  25. package/dist/src/config/worker-guidance-warning.js +28 -0
  26. package/dist/src/config/worker-owner-lock.js +152 -0
  27. package/dist/src/config/worker-parallelism.js +154 -0
  28. package/dist/src/config/worker-write.js +75 -0
  29. package/dist/src/decision/telemetry/exporter.js +2 -1
  30. package/dist/src/decision/telemetry/index.js +1 -0
  31. package/dist/src/decision/telemetry/mapping.js +43 -0
  32. package/dist/src/decision/telemetry/otlp-http.js +5 -2
  33. package/dist/src/decision/telemetry/redaction.js +6 -1
  34. package/dist/src/decision/telemetry/types.js +3 -0
  35. package/dist/src/decision/telemetry/worker.js +69 -0
  36. package/dist/src/lint/rule-activation.js +15 -7
  37. package/dist/src/lint/runner.js +2 -2
  38. package/dist/src/mcp/server.mjs +2 -11
  39. package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
  40. package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
  41. package/dist/src/mcp/tools/worker-invoke.mjs +6 -6
  42. package/dist/src/mission-protocol/codecs.js +8 -38
  43. package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
  44. package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
  45. package/dist/src/models/conductor-data-policy.d.mts +8 -0
  46. package/dist/src/models/conductor-data-policy.mjs +190 -0
  47. package/dist/src/models/config-loader.js +19 -3
  48. package/dist/src/models/config-writer.js +27 -0
  49. package/dist/src/models/model-capabilities.v1.json +197 -3
  50. package/dist/src/models/provider-policy.js +7 -155
  51. package/dist/src/models/resolver.js +2 -293
  52. package/dist/src/models/worker-artifact-binding.mjs +12 -0
  53. package/dist/src/models/worker-direct-readiness-policy.js +155 -0
  54. package/dist/src/models/worker-direct-readiness.js +9 -218
  55. package/dist/src/models/worker-health-readiness.js +105 -26
  56. package/dist/src/models/worker-installation.mjs +12 -14
  57. package/dist/src/models/worker-member-references.js +64 -0
  58. package/dist/src/models/worker-plan.js +317 -0
  59. package/dist/src/models/worker-selection-policy.js +217 -0
  60. package/dist/src/models/wrapper-deployment.js +1 -1
  61. package/dist/src/resources/web-release.js +76 -65
  62. package/dist/src/serve/shared-host-admission-checks.js +116 -0
  63. package/dist/src/serve/shared-host-scheduler.js +25 -107
  64. package/dist/src/serve/shared-host-starvation.js +73 -0
  65. package/dist/src/serve/worker-artifacts.d.mts +4 -0
  66. package/dist/src/serve/worker-artifacts.mjs +56 -0
  67. package/dist/src/serve/worker-command-request.js +68 -0
  68. package/dist/src/serve/worker-invocation-paths.js +144 -0
  69. package/dist/src/serve/worker-invocation-preparation.js +169 -0
  70. package/dist/src/serve/worker-invocation.js +24 -291
  71. package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
  72. package/dist/src/serve/worker-stdin-frame.mjs +6 -6
  73. package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
  74. package/dist/src/storage/backends/fortemi.js +33 -29
  75. package/dist/src/storage/backends/logseq.js +16 -3
  76. package/dist/src/storage/backends/postgres.js +74 -33
  77. package/dist/src/storage/backends/postgrest.js +37 -13
  78. package/dist/src/storage/config.js +6 -1
  79. package/dist/src/uhp/config.js +4 -0
  80. package/package.json +9 -1
  81. package/schemas/mission-v1.schema.json +20 -19
  82. package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
  83. package/tools/agents/providers/claude-post-deploy.mjs +35 -0
  84. package/tools/agents/providers/claude.mjs +3 -30
  85. package/tools/agents/providers/codex-post-deploy.mjs +13 -0
  86. package/tools/agents/providers/codex.mjs +2 -8
  87. package/tools/agents/providers/conductor-data-guard.mjs +61 -0
  88. package/tools/agents/providers/rule-delivery.mjs +3 -16
  89. package/tools/agents/providers/rule-presentation.mjs +38 -0
  90. package/tools/plugin/package-plugins.mjs +1 -1
  91. package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
  92. package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
  93. package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
  94. package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
  95. package/tools/ralph-external/lib/session-attempt.mjs +85 -0
  96. package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
  97. package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
  98. package/tools/ralph-external/lib/worker-final.mjs +143 -0
  99. package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
  100. package/tools/ralph-external/lib/worker-isolation.mjs +2 -44
  101. package/tools/ralph-external/lib/worker-launch-cli.mjs +47 -0
  102. package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
  103. package/tools/ralph-external/session-launcher.mjs +73 -217
  104. package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
@@ -1,3 +1,5 @@
1
+ import { postDeploy } from './codex-post-deploy.mjs';
2
+ export { postDeploy };
1
3
  import { importImpl } from '../../_resolve-impl.mjs';
2
4
  /**
3
5
  * OpenAI Codex Provider
@@ -47,7 +49,6 @@ import {
47
49
  writeFile,
48
50
  deployFiles,
49
51
  createAgentsMdFromTemplate,
50
- initializeFrameworkWorkspace,
51
52
  getAddonAgentFiles,
52
53
  getAddonCommandFiles,
53
54
  getAddonSkillDirs,
@@ -511,13 +512,6 @@ export function generatePluginBundle(targetDir, opts = {}) {
511
512
  // Post-Deployment
512
513
  // ============================================================================
513
514
 
514
- export async function postDeploy(targetDir, opts) {
515
- initializeFrameworkWorkspace(targetDir, opts.mode, opts.dryRun, opts.srcRoot);
516
-
517
- if (opts.createAgentsMd) {
518
- if (opts.contextFiles !== false) createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
519
- }
520
- }
521
515
 
522
516
  // ============================================================================
523
517
  // File Extension
@@ -0,0 +1,61 @@
1
+ // Compile the existing project data policy into project-local provider refusals.
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { importImpl, resolveImpl } from '../../_resolve-impl.mjs';
6
+
7
+ const quote = value => `'${value.replace(/'/g, `'"'"'`)}'`;
8
+
9
+ // Local-only paths are declared only in project/user models.json or project
10
+ // parallelism (the built-in defaults declare none). Skip the full model-config
11
+ // load when none of them mention the field, so ordinary deploys never depend
12
+ // on the framework default config (absent from the slim CLI package).
13
+ function declaresLocalOnlyPaths(targetDir) {
14
+ const userDir = process.env.AIWG_CONFIG || path.join(os.homedir(), '.config/aiwg');
15
+ return [path.join(targetDir, 'models.json'), path.join(userDir, 'models.json'), path.join(targetDir, '.aiwg/aiwg.config')]
16
+ .some(file => { try { return fs.readFileSync(file, 'utf8').includes('localOnlyPaths'); } catch { return false; } });
17
+ }
18
+
19
+ export async function deployConductorDataGuard(targetDir, provider, opts = {}) {
20
+ if (!declaresLocalOnlyPaths(targetDir)) return [];
21
+ // Always resolve policy through the existing loader, including inherited restrictions.
22
+ const { ConfigLoader } = await importImpl(import.meta.url, 'models/config-loader.js');
23
+ const { readAiwgConfig } = await importImpl(import.meta.url, 'config/aiwg-config.js');
24
+ const guard = await importImpl(import.meta.url, 'models/conductor-data-policy.mjs');
25
+ const config = await readAiwgConfig(targetDir);
26
+ const models = await new ConfigLoader(targetDir).load(config?.parallelism);
27
+ const paths = guard.collectLocalOnlyPaths(models.defaults.workerSelection, config?.parallelism);
28
+ if (!paths.length || opts.dryRun) return paths;
29
+ const root = path.resolve(targetDir);
30
+ if (provider === 'claude') {
31
+ const settingsPath = path.join(root, '.claude/settings.json');
32
+ const settings = fs.existsSync(settingsPath) ? JSON.parse(fs.readFileSync(settingsPath, 'utf8')) : {};
33
+ const hookPath = path.join(root, '.claude/hooks/aiwg-local-only.mjs');
34
+ const hookSource = fs.readFileSync(resolveImpl(import.meta.url, 'models/conductor-data-policy.mjs'), 'utf8');
35
+ fs.mkdirSync(path.dirname(hookPath), { recursive: true });
36
+ fs.writeFileSync(hookPath, `${hookSource}\nrunConductorDataHook(${JSON.stringify(root)}, ${JSON.stringify(paths)});\n`);
37
+ settings.permissions ??= {};
38
+ settings.permissions.deny = [...new Set([...(settings.permissions.deny ?? []), ...paths.flatMap(file => [
39
+ `Read(//${path.join(root, file).slice(1)})`, `Read(//${path.join(root, file).slice(1)}/**)`,
40
+ ])])];
41
+ settings.hooks ??= {};
42
+ const command = `node ${quote(hookPath)}`;
43
+ const entries = settings.hooks.PreToolUse ?? [];
44
+ settings.hooks.PreToolUse = [...entries.filter(entry => !entry.hooks?.some(hook => hook.command === command)),
45
+ { matcher: '.*', hooks: [{ type: 'command', command }] }];
46
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
47
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
48
+ } else if (provider === 'codex') {
49
+ const rulesPath = path.join(root, '.codex/rules/aiwg-local-only.rules');
50
+ const lines = ['# AIWG local-only inputs: explicit command prefixes only; not filesystem isolation.'];
51
+ for (const file of paths) for (const spelling of [file, `./${file}`, path.join(root, file)]) {
52
+ for (const command of ['cat', 'head', 'tail', 'less', 'more', 'sed', 'awk', 'grep', 'rg']) {
53
+ lines.push(`prefix_rule(pattern = ${JSON.stringify([command, spelling])}, decision = "forbidden")`);
54
+ }
55
+ }
56
+ fs.mkdirSync(path.dirname(rulesPath), { recursive: true });
57
+ fs.writeFileSync(rulesPath, lines.join('\n') + '\n');
58
+ }
59
+ console.log(`Local-only conductor guard (${provider}): ${paths.join(', ')}; shell coverage is limited.`);
60
+ return paths;
61
+ }
@@ -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 { projectOnDemandReferences, renderRuleKernelIndex } from './rule-presentation.mjs';
24
+ export { renderRuleKernelIndex } from './rule-presentation.mjs';
23
25
  const PREPARATION_HOOK = Symbol.for('aiwg.provider-preparation-hook.v1');
24
26
 
25
27
  export function installRuleDeploymentPreparationHook(hook) {
@@ -298,21 +300,6 @@ export async function prepareRuleDelivery(opts) {
298
300
  return { rules, bindings, targets, pins: pinned.rules, pinWarnings: pinned.warnings };
299
301
  }
300
302
 
301
- export function renderRuleKernelIndex(rules, nativePaths = false) {
302
- const summary = rules.filter(rule => rule.tagged && (rule.canonicalActivation || rule.activation) === 'summary');
303
- const paths = rules.filter(rule => rule.tagged && (rule.canonicalActivation || rule.activation) === 'paths');
304
- const kernels = rules.filter(rule => rule.tagged && (rule.canonicalActivation || rule.activation) === 'kernel');
305
- if (!summary.length && !paths.length && !kernels.length) return '';
306
- const lines = ['# Rule kernel index', ''];
307
- for (const rule of kernels) lines.push(`- ${rule.name} — ${rule.summary || 'Fetch and follow the full rule before governed work.'} Fetch and follow: \`aiwg show rule ${rule.name} --body\`.`, '');
308
- for (const rule of summary) lines.push(`- ${rule.name} — ${rule.summary} Fetch and follow: \`aiwg show rule ${rule.name} --body\`.`, '');
309
- if (paths.length) {
310
- lines.push(nativePaths ? '## Path rules (native)' : '## Path rules (prompt emulation)', '',
311
- nativePaths ? 'The provider loads these rules for matching paths; full contracts remain available through the commands below.' : 'Before reading or changing files, compare intended project-relative paths with the globs below and fetch matching rules before their first read/write. If paths are initially unknown, check when they become known. An agent-only task has no path match. This is prompt emulation, not a filesystem hook.', '');
312
- for (const rule of paths) lines.push(`- ${rule.name} | ${rule.paths.map(glob => `\`${glob}\``).join(', ')} | ${rule.summary} | \`aiwg show rule ${rule.name} --body\``, '');
313
- }
314
- return lines.join('\n');
315
- }
316
303
 
317
304
  function managed(content, opts) {
318
305
  return addManagedMarker(content, opts.deployVersion, opts.deploySource || 'bundled');
@@ -334,7 +321,7 @@ function putStartup(file, body) {
334
321
 
335
322
  /** Tagged rules bypass legacy aggregate writers. Untagged projections retain their tier policy. */
336
323
  function projectRules(opts, provider) {
337
- const { rules } = opts.ruleDelivery;
324
+ const rules = projectOnDemandReferences(opts.ruleDelivery.rules, ['claude', 'cursor'].includes(provider.name));
338
325
  if (opts.contextFiles !== false && provider.name === 'hermes') {
339
326
  // The old context generator carries hardcoded corpus directives. Only the
340
327
  // selected contracts below may activate policy; preserve operator prose.
@@ -0,0 +1,38 @@
1
+ /** Render deployed rule references without links to catalog-only siblings. */
2
+ import path from 'node:path';
3
+
4
+ export function projectOnDemandReferences(rules, nativePaths = false) {
5
+ const byFile = new Map(rules.map(rule => [path.resolve(rule.file), rule]));
6
+ return rules.map(rule => {
7
+ const rewrite = content => content?.replace(
8
+ /\[[^\]\n]+\]\((?:\.\/)?([\w-]+)\.md\)[ \t]*\(`aiwg show rule \1(?: --body)?`, on-demand\)/g,
9
+ (reference, name) => {
10
+ const sibling = byFile.get(path.resolve(path.dirname(rule.file), `${name}.md`));
11
+ // Only known catalog rules can replace a relative file reference. Keep
12
+ // unknown/broken links and links to files selected for native delivery.
13
+ if (!sibling?.tagged || sibling.name !== name || sibling.activation === 'kernel' ||
14
+ (nativePaths && sibling.activation === 'paths')) return reference;
15
+ return `\`aiwg show rule ${name} --body\` (on-demand)`;
16
+ },
17
+ );
18
+ return { ...rule, content: rewrite(rule.content), body: rewrite(rule.body),
19
+ ...(rule.coreBody === undefined ? {} : { coreBody: rewrite(rule.coreBody) }) };
20
+ });
21
+ }
22
+
23
+ export function renderRuleKernelIndex(rules, nativePaths = false) {
24
+ const summary = rules.filter(rule => rule.tagged && (rule.canonicalActivation || rule.activation) === 'summary');
25
+ const paths = rules.filter(rule => rule.tagged && (rule.canonicalActivation || rule.activation) === 'paths');
26
+ const kernels = rules.filter(rule => rule.tagged && (rule.canonicalActivation || rule.activation) === 'kernel');
27
+ if (!summary.length && !paths.length && !kernels.length) return '';
28
+ const lines = ['# Rule kernel index', ''];
29
+ for (const rule of kernels) lines.push(`- ${rule.name} — ${rule.summary || 'Fetch and follow the full rule before governed work.'} Fetch and follow: \`aiwg show rule ${rule.name} --body\`.`, '');
30
+ for (const rule of summary) lines.push(`- ${rule.name} — ${rule.summary} Fetch and follow: \`aiwg show rule ${rule.name} --body\`.`, '');
31
+ if (paths.length) {
32
+ lines.push(nativePaths ? '## Path rules (native)' : '## Path rules (prompt emulation)', '',
33
+ nativePaths ? 'The provider loads these rules for matching paths; full contracts remain available through the commands below.' : 'Before reading or changing files, compare intended project-relative paths with the globs below and fetch matching rules before their first read/write. If paths are initially unknown, check when they become known. An agent-only task has no path match. This is prompt emulation, not a filesystem hook.', '');
34
+ for (const rule of paths) lines.push(`- ${rule.name} | ${rule.paths.map(glob => `\`${glob}\``).join(', ')} | ${rule.summary} | \`aiwg show rule ${rule.name} --body\``, '');
35
+ }
36
+ return lines.join('\n');
37
+ }
38
+
@@ -41,7 +41,7 @@ const PLUGIN_CONFIGS = {
41
41
  skills: 'agentic/code/frameworks/sdlc-complete/skills'
42
42
  },
43
43
  extraCopy: [
44
- { from: 'tools/security/threat-assessment.mjs', to: 'tools/security/threat-assessment.mjs' },
44
+ ...['threat-assessment.mjs', 'threat-assessment'].map(name => ({ from: `tools/security/${name}`, to: `tools/security/${name}` })),
45
45
  { from: 'agentic/code/frameworks/sdlc-complete/flows/flow-architecture-evolution.playbook.yaml', to: 'flows/flow-architecture-evolution.playbook.yaml' },
46
46
  { from: 'agentic/code/frameworks/sdlc-complete/flows/flow-release.playbook.yaml', to: 'flows/flow-release.playbook.yaml' }
47
47
  ],
@@ -212,8 +212,8 @@ export class ClaudeAdapter extends ProviderAdapter {
212
212
  ];
213
213
 
214
214
  // Session tracking
215
- if (options.sessionId) {
216
- args.push('--session-id', options.sessionId);
215
+ if (options.resumeSession || options.sessionId) {
216
+ args.push(options.resumeSession ? '--resume' : '--session-id', options.resumeSession || options.sessionId);
217
217
  }
218
218
 
219
219
  // Model selection (Claude passes through directly)
@@ -44,7 +44,7 @@ const MODEL_MAP = {
44
44
  *
45
45
  * codex --no-daemon exec --ignore-user-config -m M \
46
46
  * -c model_reasoning_effort="E" -s S -C W -c approval_policy="never" \
47
- * --skip-git-repo-check -o F --json - < briefPath
47
+ * --skip-git-repo-check -o F.last-message --json - < briefPath
48
48
  *
49
49
  * `--no-daemon` is a root-level flag and `--ignore-user-config` is an
50
50
  * `exec`-level flag (root `--help` lists no `--ignore-user-config`;
@@ -206,7 +206,7 @@ export class CodexAdapter extends ProviderAdapter {
206
206
  * model, explicit effort as one `-c model_reasoning_effort="E"` token
207
207
  * (TOML `"E"` quoting via `JSON.stringify`; there is no shell in argv),
208
208
  * explicit sandbox/approval (`never` only), the conductor workdir as
209
- * `-C`, the final file as `-o`, the JSONL event stream as `--json` and
209
+ * `-C`, a separate last-message capture as `-o`, the JSONL stream as `--json` and
210
210
  * `-` as the prompt argument so the brief arrives on stdin
211
211
  * (`stdin: 'brief-text'`). Permission flags from the legacy Ralph loop
212
212
  * (`--dangerously-bypass-approvals-and-sandbox`), guessed step caps and
@@ -260,7 +260,7 @@ export class CodexAdapter extends ProviderAdapter {
260
260
  '-C', cwd,
261
261
  '-c', `approval_policy=${JSON.stringify(approval)}`,
262
262
  '--skip-git-repo-check',
263
- '-o', finalPath,
263
+ '-o', `${finalPath}.last-message`,
264
264
  '--json',
265
265
  '-',
266
266
  ];
@@ -0,0 +1,86 @@
1
+ // Validate the parent reservation and disclose one-shot attempt bindings.
2
+ import { importImpl } from '../../_resolve-impl.mjs';
3
+ import { sampleWorkerIdentity } from '../process-monitor.mjs';
4
+ import { changedBindings } from './session-attempt.mjs';
5
+ const { qualificationDigest } = await importImpl(import.meta.url, 'models/qualification-digest.mjs');
6
+
7
+ export function managedAttemptBindings(plan) {
8
+ const invocation = plan.invocation;
9
+ const digests = plan.digests ?? {};
10
+ const selected = invocation?.selected ?? plan.selected;
11
+ return { policyDigest: invocation?.policyDigest ?? (digests.configDigest && digests.resolvedModelsDigest
12
+ ? qualificationDigest([digests.configDigest, digests.resolvedModelsDigest]) : null),
13
+ catalogDigest: invocation?.catalogDigest ?? digests.catalogDigest ?? null,
14
+ profileDigest: invocation?.profileDigest ?? digests.profileDigest ?? null,
15
+ selected: selected ? { group: selected.group, member: selected.member, cli: selected.cli,
16
+ inferenceProvider: selected.inferenceProvider, model: selected.model,
17
+ effort: typeof selected.effort === 'object' ? selected.effort?.value ?? null : selected.effort ?? null,
18
+ profileRef: selected.profileRef ?? selected.profile } : null, sessionId: null };
19
+ }
20
+
21
+ export async function validateManagedAttempt(options, saved, inputs, now, validate, publish) {
22
+ const attempt = await managedWorkerAttempt(options, saved, inputs, now);
23
+ await publish(attempt);
24
+ try {
25
+ const invocation = await validate(saved, inputs.snapshot, inputs.drivers, now);
26
+ return { invocation, attempt };
27
+ } catch (error) {
28
+ attempt.status = 'blocked'; attempt.errorCode = error.code ?? 'PLAN_REVALIDATION_FAILED';
29
+ await publish(attempt);
30
+ throw error;
31
+ }
32
+ }
33
+
34
+ export async function managedWorkerAttempt(options, saved, inputs, now) {
35
+ const { resolveWorkerInvocation } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
36
+ const current = await resolveWorkerInvocation(inputs.snapshot, saved.request, inputs.drivers, now);
37
+ const original = options.attempt?.originalBindings ?? managedAttemptBindings(saved);
38
+ const bindings = managedAttemptBindings(current);
39
+ return { ...options.attempt, kind: 'fresh', reason: options.attempt?.reason
40
+ ?? (options.resumeSession || options.sessionId ? 'one-shot-continuation-unsupported' : 'one-shot-profile'),
41
+ requestedSessionId: options.attempt?.requestedSessionId ?? options.resumeSession ?? options.sessionId ?? null,
42
+ requestId: options.managedAdmission.requestId, originalBindings: original,
43
+ bindings, changes: changedBindings(original, bindings),
44
+ status: current.status === 'eligible' ? 'prepared' : 'blocked',
45
+ diagnostics: current.diagnostics };
46
+ }
47
+
48
+ /** Reuse the MC authority and its registered one-shot identity at every spawn boundary. */
49
+ export async function assertManagedWorkerLaunch(options, invocation, inputs, projectRoot = process.cwd()) {
50
+ if (!options.managedAdmission || invocation.mode !== 'managed'
51
+ || inputs.snapshot.config.parallelism?.worker_mode !== 'managed') throw new Error('Managed worker admission is required');
52
+ const { createMcAdmissionScheduler } = await importImpl(import.meta.url, 'cli/handlers/mc.js');
53
+ const { loadLauncherRegistry } = await importImpl(import.meta.url, 'cli/handlers/ralph-launcher.js');
54
+ const { workerModelKey } = await importImpl(import.meta.url, 'models/provider-policy.js');
55
+ const scheduler = await createMcAdmissionScheduler(projectRoot, inputs.snapshot.config);
56
+ const { requestId, generation } = options.managedAdmission;
57
+ const snapshot = scheduler.snapshot();
58
+ const record = snapshot.records[requestId];
59
+ const deadlineAtMs = Math.min(options.cycleDeadlineAtMs ?? Infinity, ...[record?.accounting?.runKey, record?.accounting?.taskKey]
60
+ .map(key => snapshot.allowances?.[key]?.deadlineAt).filter(Boolean).map(Date.parse));
61
+ const registered = loadLauncherRegistry(projectRoot).loops[options.loopId ?? process.env.RALPH_LOOP_ID];
62
+ const worker = record?.managed;
63
+ const reservation = record?.accounting?.reservation;
64
+ if (!record || record.state !== 'admitted' || record.leaseGeneration !== generation
65
+ || !record.launchClaimedAt || !record.metadata?.invocationId || Date.parse(record.leaseExpiresAt ?? '') <= Date.now()
66
+ || deadlineAtMs <= Date.now()
67
+ || worker?.kind !== 'external' || worker.group !== invocation.selected.group
68
+ || worker.member !== invocation.selected.member || worker.cli !== invocation.selected.cli
69
+ || worker.model !== workerModelKey(invocation.selected.inferenceProvider, invocation.selected.model)
70
+ || reservation?.tokens != null && !(invocation.limits.tokens <= reservation.tokens)
71
+ || reservation?.costUsd != null && !(invocation.limits.costUsd !== null && invocation.limits.costUsd <= reservation.costUsd)
72
+ || registered?.managedAdmission?.requestId !== requestId || registered.managedAdmission.generation !== generation
73
+ || registered.managedWorker?.invocationPath !== options.invocationPath
74
+ || registered.managedWorker?.runDir !== options.runDir) {
75
+ throw new Error('Managed worker launch differs from the current claimed admission');
76
+ }
77
+ if (record.execution) {
78
+ const identity = sampleWorkerIdentity(process.pid);
79
+ if (record.execution.pid !== process.pid || !identity?.bootId || !identity.startTicks
80
+ || record.execution.startedAt !== `${identity.bootId}:${identity.startTicks}:${identity.uid}`) {
81
+ throw new Error('Managed worker supervisor identity differs');
82
+ }
83
+ }
84
+ return { ...record, deadlineAtMs };
85
+ }
86
+
@@ -1,220 +1,14 @@
1
- /**
2
- * Provider Adapter Base Class for External Ralph Loop
3
- *
4
- * Defines the interface that all CLI provider adapters must implement.
5
- * Each adapter wraps a specific agentic coding CLI (claude, codex, etc.)
6
- * and handles binary resolution, argument construction, model mapping,
7
- * and output parsing.
8
- *
9
- * @implements Plan: Multi-Provider Support for External Ralph Loop
10
- */
11
-
12
1
  import { spawn } from 'child_process';
13
2
  import { importImpl } from '../../_resolve-impl.mjs';
14
3
  import { createWorkerHealthObserver, observeProviderHealthEvent } from './worker-health.mjs';
15
4
 
16
- /**
17
- * Thrown by {@link parseStrictWorkerJson} with a stable `code` so callers can
18
- * map a scan failure to a specific contract-violation message without
19
- * string-matching free-text.
20
- */
21
- class StrictJsonScanError extends Error {
22
- constructor(code, message) {
23
- super(message);
24
- this.code = code;
25
- }
26
- }
27
-
28
- /**
29
- * RFC 8259 JSON parser used on every strict worker-final path (JSONL lines,
30
- * `format: 'json'`, and the MCP `content[0].text`/top-level envelope) instead
31
- * of `JSON.parse`. `JSON.parse` silently keeps the LAST value when an object
32
- * repeats a member name — RFC 8259 §4 explicitly leaves this to the
33
- * implementation, and V8 picks "last wins". That lets a forged worker final
34
- * smuggle a second, hidden value past a first one: a trailing duplicate
35
- * `"status":"completed"` can overwrite an earlier `"status":"failed"`, or a
36
- * trailing duplicate `"type":"result"` can collapse a `"type":"message"`
37
- * framing envelope into a bogus result in one line (#2890 BLOCKER 1+2).
38
- *
39
- * This scanner tracks member names per object independently at every
40
- * nesting level (a sibling object may reuse a name the scanner already saw
41
- * in its parent or an earlier sibling) and rejects any object that repeats
42
- * one. Names are compared only after the scanner has fully decoded them —
43
- * resolving `\"`, other single-character escapes, and `\uXXXX` escapes
44
- * (including surrogate pairs, since JS string concatenation of two decoded
45
- * UTF-16 code units forms the pair automatically) — so an escape-obfuscated
46
- * collision (`"status"` vs `"status"`) is caught exactly like a literal
47
- * one. A non-colliding unknown key, escaped or not, is left for the existing
48
- * `additionalProperties: false` schema check to reject, unchanged.
49
- *
50
- * Nesting depth is bounded during the scan (not only afterward, by
51
- * `boundedJson()`'s post-parse walk) so pathological depth fails fast with a
52
- * clear `DEPTH` code instead of relying on a recursive-descent stack
53
- * overflow to be caught as a generic syntax error.
54
- *
55
- * @param {string} text - Decoded JSON text (already UTF-8 validated by the caller)
56
- * @param {number} maxDepth - Same bound as `WORKER_MAX_NESTING_DEPTH`; root value is depth 1
57
- * @returns {unknown}
58
- */
59
- function parseStrictWorkerJson(text, maxDepth) {
60
- const length = text.length;
61
- let index = 0;
62
-
63
- function fail(code, message) {
64
- throw new StrictJsonScanError(code, `${message} (offset ${index})`);
65
- }
66
-
67
- function skipWhitespace() {
68
- while (index < length) {
69
- const code = text.charCodeAt(index);
70
- if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) index += 1;
71
- else break;
72
- }
73
- }
74
-
75
- function parseValue(depth) {
76
- skipWhitespace();
77
- if (index >= length) fail('SYNTAX', 'Unexpected end of JSON input');
78
- const code = text.charCodeAt(index);
79
- if (code === 0x7b) return parseObject(depth);
80
- if (code === 0x5b) return parseArray(depth);
81
- if (code === 0x22) return parseString();
82
- if (code === 0x2d || (code >= 0x30 && code <= 0x39)) return parseNumber();
83
- if (text.startsWith('true', index)) { index += 4; return true; }
84
- if (text.startsWith('false', index)) { index += 5; return false; }
85
- if (text.startsWith('null', index)) { index += 4; return null; }
86
- fail('SYNTAX', 'Unexpected token');
87
- }
88
-
89
- function parseObject(depth) {
90
- if (depth > maxDepth) fail('DEPTH', 'Worker nesting depth exceeds bound');
91
- index += 1; // {
92
- const result = {};
93
- const seen = new Set();
94
- skipWhitespace();
95
- if (text.charCodeAt(index) === 0x7d) { index += 1; return result; }
96
- for (;;) {
97
- skipWhitespace();
98
- if (text.charCodeAt(index) !== 0x22) fail('SYNTAX', 'Expected object member name');
99
- const key = parseString();
100
- if (seen.has(key)) fail('DUPLICATE', `Duplicate object member name ${JSON.stringify(key)}`);
101
- seen.add(key);
102
- skipWhitespace();
103
- if (text.charCodeAt(index) !== 0x3a) fail('SYNTAX', 'Expected colon after object member name');
104
- index += 1;
105
- // Define, never assign: assignment would invoke the inherited __proto__
106
- // setter, hiding the member from own-key validation and diverging from
107
- // JSON.parse, which creates an ordinary own property.
108
- Object.defineProperty(result, key, { value: parseValue(depth + 1), enumerable: true, writable: true, configurable: true });
109
- skipWhitespace();
110
- const delimiter = text.charCodeAt(index);
111
- if (delimiter === 0x2c) { index += 1; continue; }
112
- if (delimiter === 0x7d) { index += 1; break; }
113
- fail('SYNTAX', 'Expected , or } in object');
114
- }
115
- return result;
116
- }
117
-
118
- function parseArray(depth) {
119
- if (depth > maxDepth) fail('DEPTH', 'Worker nesting depth exceeds bound');
120
- index += 1; // [
121
- const result = [];
122
- skipWhitespace();
123
- if (text.charCodeAt(index) === 0x5d) { index += 1; return result; }
124
- for (;;) {
125
- result.push(parseValue(depth + 1));
126
- skipWhitespace();
127
- const delimiter = text.charCodeAt(index);
128
- if (delimiter === 0x2c) { index += 1; continue; }
129
- if (delimiter === 0x5d) { index += 1; break; }
130
- fail('SYNTAX', 'Expected , or ] in array');
131
- }
132
- return result;
133
- }
134
-
135
- function parseString() {
136
- index += 1; // opening quote
137
- let value = '';
138
- for (;;) {
139
- if (index >= length) fail('SYNTAX', 'Unterminated string');
140
- const code = text.charCodeAt(index);
141
- if (code === 0x22) { index += 1; return value; }
142
- if (code === 0x5c) {
143
- index += 1;
144
- const escape = text[index];
145
- if (escape === undefined) fail('SYNTAX', 'Unterminated escape sequence');
146
- switch (escape) {
147
- case '"': value += '"'; index += 1; break;
148
- case '\\': value += '\\'; index += 1; break;
149
- case '/': value += '/'; index += 1; break;
150
- case 'b': value += '\b'; index += 1; break;
151
- case 'f': value += '\f'; index += 1; break;
152
- case 'n': value += '\n'; index += 1; break;
153
- case 'r': value += '\r'; index += 1; break;
154
- case 't': value += '\t'; index += 1; break;
155
- case 'u': {
156
- const hex = text.slice(index + 1, index + 5);
157
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) fail('SYNTAX', 'Invalid unicode escape');
158
- // Decoded code unit is appended as-is; two consecutive decoded
159
- // surrogate halves recombine into one character automatically
160
- // through normal JS UTF-16 string concatenation.
161
- value += String.fromCharCode(parseInt(hex, 16));
162
- index += 5;
163
- break;
164
- }
165
- default: fail('SYNTAX', 'Invalid escape sequence');
166
- }
167
- } else if (code < 0x20) {
168
- fail('SYNTAX', 'Unescaped control character in string');
169
- } else {
170
- value += text[index];
171
- index += 1;
172
- }
173
- }
174
- }
175
-
176
- function parseNumber() {
177
- const start = index;
178
- if (text.charCodeAt(index) === 0x2d) index += 1;
179
- if (text.charCodeAt(index) === 0x30) {
180
- index += 1;
181
- } else if (text.charCodeAt(index) >= 0x31 && text.charCodeAt(index) <= 0x39) {
182
- index += 1;
183
- while (text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39) index += 1;
184
- } else fail('SYNTAX', 'Invalid number');
185
- if (text.charCodeAt(index) === 0x2e) {
186
- index += 1;
187
- if (!(text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39)) fail('SYNTAX', 'Invalid number fraction');
188
- while (text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39) index += 1;
189
- }
190
- if (text.charCodeAt(index) === 0x65 || text.charCodeAt(index) === 0x45) {
191
- index += 1;
192
- if (text.charCodeAt(index) === 0x2b || text.charCodeAt(index) === 0x2d) index += 1;
193
- if (!(text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39)) fail('SYNTAX', 'Invalid number exponent');
194
- while (text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39) index += 1;
195
- }
196
- return Number(text.slice(start, index));
197
- }
198
-
199
- const result = parseValue(1);
200
- skipWhitespace();
201
- if (index !== length) fail('SYNTAX', 'Unexpected trailing characters after JSON value');
202
- return result;
203
- }
5
+ import { parseStrictWorkerJson, StrictJsonScanError } from './strict-worker-json.mjs';
6
+ import { classifySessionAttempt } from './session-attempt.mjs';
204
7
 
205
8
  const installedOwner = await importImpl(import.meta.url, 'models/worker-installation.mjs');
206
9
  export const { workerNegativeFixtureSuite, workerNegativeFixtureDigest, registerProvider, createProvider, listProviders, hasProvider } = installedOwner;
207
10
 
208
- /**
209
- * Whether the optional muse exec Ralph adapter is registered.
210
- * Enabled by default; set `AIWG_MUSE_RALPH_ENABLED` to `0`, `false`, `no`,
211
- * or `off` to disable the adapter without affecting
212
- * `aiwg use --provider muse` (the deploy/writer path lives in `src/` and
213
- * never consults this registry). See
214
- * `tools/ralph-external/lib/muse-adapter.mjs` (#230).
215
- *
216
- * @returns {boolean}
217
- */
11
+ /** The optional Muse adapter is independent of provider deployment. */
218
12
  export function isMuseRalphEnabled() {
219
13
  const raw = String(process.env.AIWG_MUSE_RALPH_ENABLED ?? '').trim().toLowerCase();
220
14
  return !['0', 'false', 'no', 'off'].includes(raw);
@@ -661,24 +455,15 @@ export class ProviderAdapter {
661
455
  return null;
662
456
  }
663
457
 
664
- /**
665
- * Check if a capability is supported, and optionally log a warning.
666
- *
667
- * @param {string} capability - Capability name from ProviderCapabilities
668
- * @param {string} [context] - Context for the warning message
669
- * @returns {boolean}
670
- */
458
+ classifySessionAttempt(request) {
459
+ return classifySessionAttempt(this, request);
460
+ }
461
+
671
462
  hasCapability(capability) {
672
463
  const caps = this.getCapabilities();
673
464
  return !!caps[capability];
674
465
  }
675
466
 
676
- /**
677
- * Log a capability warning when a feature is not supported.
678
- *
679
- * @param {string} capability - Capability name
680
- * @param {string} feature - Human-readable feature description
681
- */
682
467
  warnUnsupported(capability, feature) {
683
468
  if (!this.hasCapability(capability)) {
684
469
  console.warn(
@@ -687,52 +472,18 @@ export class ProviderAdapter {
687
472
  }
688
473
  }
689
474
  }
690
-
691
- // ============================================================================
692
- // Provider Registry & Factory
693
- // ============================================================================
694
-
695
475
  export async function loadInstalledWorkerAdapters(request) {
696
476
  await ensureProvidersRegistered();
697
477
  return installedOwner.loadInstalledWorkerAdapters(request);
698
478
  }
699
479
 
700
- // ============================================================================
701
- // Auto-register built-in providers on import
702
- // ============================================================================
703
-
704
- // Lazy imports to avoid circular dependencies - adapters register themselves
705
- // when their modules are loaded. We trigger that here.
480
+ // Adapters register themselves; keep imports lazy to avoid registry cycles.
706
481
  async function registerBuiltinProviders() {
707
- try {
708
- await import('./claude-adapter.mjs');
709
- } catch { /* ignore if not found */ }
710
- try {
711
- await import('./codex-adapter.mjs');
712
- } catch { /* ignore if not found */ }
713
- try {
714
- await import('./opencode-adapter.mjs');
715
- } catch { /* ignore if not found */ }
716
- try {
717
- await import('./factory-adapter.mjs');
718
- } catch { /* ignore if not found */ }
719
- try {
720
- await import('./pi-adapter.mjs');
721
- await import('./omp-adapter.mjs');
722
- } catch { /* ignore if not found */ }
723
- try {
724
- await import('./deepseek-harness-adapter.mjs');
725
- } catch { /* ignore if not found */ }
726
- // #230: optional / post-experimental. Disabled via AIWG_MUSE_RALPH_ENABLED=0
727
- // without affecting `aiwg use --provider muse`.
728
- if (isMuseRalphEnabled()) {
729
- try {
730
- await import('./muse-adapter.mjs');
731
- } catch { /* ignore if not found */ }
482
+ for (const name of ['claude', 'codex', 'opencode', 'factory', 'pi', 'omp', 'deepseek-harness',
483
+ ...(isMuseRalphEnabled() ? ['muse'] : [])]) {
484
+ try { await import(`./${name}-adapter.mjs`); } catch { /* Optional adapter unavailable. */ }
732
485
  }
733
486
  }
734
-
735
- // Run registration — store promise so callers can await it
736
487
  const _registrationPromise = registerBuiltinProviders();
737
488
 
738
489
  /**