@aiwg/cli 2026.10.9 → 2026.10.11

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 (116) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +275 -17
  2. package/bin/aiwg.mjs +8 -0
  3. package/dist/src/cli/handlers/mc-admission.js +39 -0
  4. package/dist/src/cli/handlers/mc.js +2 -31
  5. package/dist/src/cli/handlers/models-help.js +52 -0
  6. package/dist/src/cli/handlers/models.js +11 -81
  7. package/dist/src/cli/handlers/sessions.js +198 -15
  8. package/dist/src/cli/handlers/use-addon-profile.js +72 -0
  9. package/dist/src/cli/handlers/use.js +203 -234
  10. package/dist/src/cli/ui.js +1 -0
  11. package/dist/src/config/aiwg-config.js +38 -184
  12. package/dist/src/config/cli-help.js +54 -0
  13. package/dist/src/config/cli.js +5 -69
  14. package/dist/src/config/config-normalization.js +10 -0
  15. package/dist/src/config/worker-activation-gate.js +26 -0
  16. package/dist/src/config/worker-adoption-cli.js +102 -0
  17. package/dist/src/config/worker-adoption.js +368 -0
  18. package/dist/src/config/worker-config-digest.js +14 -0
  19. package/dist/src/config/worker-context.js +337 -0
  20. package/dist/src/config/worker-group-templates.js +150 -0
  21. package/dist/src/config/worker-guidance-warning.js +28 -0
  22. package/dist/src/config/worker-owner-lock.js +152 -0
  23. package/dist/src/config/worker-parallelism.js +154 -0
  24. package/dist/src/config/worker-write.js +75 -0
  25. package/dist/src/decision/telemetry/exporter.js +2 -1
  26. package/dist/src/decision/telemetry/index.js +1 -0
  27. package/dist/src/decision/telemetry/mapping.js +43 -0
  28. package/dist/src/decision/telemetry/otlp-http.js +5 -2
  29. package/dist/src/decision/telemetry/redaction.js +6 -1
  30. package/dist/src/decision/telemetry/types.js +3 -0
  31. package/dist/src/decision/telemetry/worker.js +69 -0
  32. package/dist/src/lint/rule-activation.js +15 -7
  33. package/dist/src/lint/runner.js +2 -2
  34. package/dist/src/mcp/server.mjs +2 -11
  35. package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
  36. package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
  37. package/dist/src/mcp/tools/worker-invoke.mjs +114 -16
  38. package/dist/src/mission-protocol/codecs.js +8 -38
  39. package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
  40. package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
  41. package/dist/src/models/conductor-data-policy.d.mts +8 -0
  42. package/dist/src/models/conductor-data-policy.mjs +190 -0
  43. package/dist/src/models/config-loader.js +19 -3
  44. package/dist/src/models/config-writer.js +27 -0
  45. package/dist/src/models/model-capabilities.v1.json +275 -17
  46. package/dist/src/models/provider-policy.js +14 -155
  47. package/dist/src/models/resolver.js +2 -293
  48. package/dist/src/models/worker-artifact-binding.mjs +12 -0
  49. package/dist/src/models/worker-direct-readiness-policy.js +155 -0
  50. package/dist/src/models/worker-direct-readiness.js +9 -218
  51. package/dist/src/models/worker-health-readiness.js +105 -26
  52. package/dist/src/models/worker-installation.mjs +12 -14
  53. package/dist/src/models/worker-member-references.js +64 -0
  54. package/dist/src/models/worker-plan.js +317 -0
  55. package/dist/src/models/worker-selection-policy.js +217 -0
  56. package/dist/src/models/wrapper-deployment.js +1 -1
  57. package/dist/src/policy/authorization.js +29 -1
  58. package/dist/src/process/aiwg-probe-command.d.mts +15 -0
  59. package/dist/src/process/aiwg-probe-command.mjs +37 -0
  60. package/dist/src/serve/shared-host-admission-checks.js +116 -0
  61. package/dist/src/serve/shared-host-scheduler.js +25 -107
  62. package/dist/src/serve/shared-host-starvation.js +73 -0
  63. package/dist/src/serve/worker-artifacts.d.mts +4 -0
  64. package/dist/src/serve/worker-artifacts.mjs +56 -0
  65. package/dist/src/serve/worker-command-request.js +68 -0
  66. package/dist/src/serve/worker-invocation-paths.js +144 -0
  67. package/dist/src/serve/worker-invocation-preparation.js +169 -0
  68. package/dist/src/serve/worker-invocation.js +24 -291
  69. package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
  70. package/dist/src/serve/worker-stdin-frame.mjs +6 -6
  71. package/dist/src/sessions/adapters/grokbot.js +7 -0
  72. package/dist/src/sessions/batch-import.js +34 -6
  73. package/dist/src/sessions/contracts.js +19 -0
  74. package/dist/src/sessions/coverage-preflight.js +7 -1
  75. package/dist/src/sessions/fortemi-export-mapping.js +33 -3
  76. package/dist/src/sessions/host-catalog.js +157 -0
  77. package/dist/src/sessions/image-blobs.js +141 -0
  78. package/dist/src/sessions/importer.js +23 -3
  79. package/dist/src/sessions/index.js +2 -0
  80. package/dist/src/sessions/refresh.js +25 -2
  81. package/dist/src/sessions/repository.js +164 -22
  82. package/dist/src/sessions/workspace-discovery.js +5 -2
  83. package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
  84. package/package.json +15 -1
  85. package/schemas/mission-v1.schema.json +20 -19
  86. package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
  87. package/tools/agents/providers/claude-post-deploy.mjs +35 -0
  88. package/tools/agents/providers/claude.mjs +3 -30
  89. package/tools/agents/providers/codex-post-deploy.mjs +13 -0
  90. package/tools/agents/providers/codex.mjs +2 -8
  91. package/tools/agents/providers/conductor-data-guard.mjs +61 -0
  92. package/tools/agents/providers/rule-delivery.mjs +3 -16
  93. package/tools/agents/providers/rule-presentation.mjs +38 -0
  94. package/tools/plugin/package-plugins.mjs +1 -1
  95. package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
  96. package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
  97. package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
  98. package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
  99. package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
  100. package/tools/ralph-external/lib/session-attempt.mjs +85 -0
  101. package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
  102. package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
  103. package/tools/ralph-external/lib/worker-context.mjs +237 -0
  104. package/tools/ralph-external/lib/worker-final.mjs +143 -0
  105. package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
  106. package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
  107. package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
  108. package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
  109. package/tools/ralph-external/lib/worker-isolation.mjs +19 -73
  110. package/tools/ralph-external/lib/worker-launch-cli.mjs +49 -0
  111. package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
  112. package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
  113. package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
  114. package/tools/ralph-external/lib/worker-termination.mjs +82 -0
  115. package/tools/ralph-external/session-launcher.mjs +124 -313
  116. package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
@@ -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
@@ -0,0 +1,150 @@
1
+ import { mergeWorkerSelection } from '../models/worker-selection-policy.js';
2
+ import { workerConfigDigest } from './worker-config-digest.js';
3
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
4
+ const escape = (value) => value.replace(/~/g, '~0').replace(/\//g, '~1');
5
+ export function expandWorkerGroupTemplates(input, source = 'parallelism') {
6
+ const origins = {};
7
+ const errors = [];
8
+ if (!object(input))
9
+ return { parallelism: input, origins, errors };
10
+ const parallelism = structuredClone(input);
11
+ const templates = input.groupTemplates ?? {};
12
+ if (parallelism.groups)
13
+ parallelism.groups = Object.assign(Object.create(null), parallelism.groups);
14
+ const fail = (pointer, message) => { errors.push(`OG_POLICY_RELAXATION ${pointer}: ${message}`); };
15
+ const merge = (base, own, pointer) => {
16
+ const result = { ...base, ...own, execution: { ...base.execution, ...own.execution } };
17
+ delete result.template;
18
+ if (base.maxParallel !== undefined && own.maxParallel !== undefined) {
19
+ if (own.maxParallel > base.maxParallel)
20
+ fail(pointer, 'Template concurrency ceiling cannot be widened');
21
+ result.maxParallel = Math.min(base.maxParallel, own.maxParallel);
22
+ }
23
+ if (base.efforts && own.efforts) {
24
+ if (own.efforts.some(effort => !base.efforts.includes(effort)))
25
+ fail(pointer, 'Template effort ceiling cannot be widened');
26
+ result.efforts = own.efforts.filter(effort => base.efforts.includes(effort));
27
+ }
28
+ if (base.members && own.members) {
29
+ for (const member of own.members) {
30
+ const prior = base.members.find(candidate => candidate.id === member.id);
31
+ if (!prior || prior.cli !== member.cli || prior.model !== member.model
32
+ || member.roles.some(role => !prior.roles.includes(role)) || prior.enabled === false && member.enabled !== false) {
33
+ fail(pointer, 'Template member ceiling cannot be widened or rebound');
34
+ }
35
+ }
36
+ }
37
+ result.requiredRoles = [...new Set([...(base.requiredRoles ?? []), ...(own.requiredRoles ?? [])])];
38
+ if (base.dataPolicy || own.dataPolicy) {
39
+ const merged = mergeWorkerSelection([
40
+ { source, authority: 'project', digest: workerConfigDigest(base), policy: base.dataPolicy ? { dataPolicy: base.dataPolicy } : undefined },
41
+ { source, authority: 'project', digest: workerConfigDigest(own), policy: own.dataPolicy ? { dataPolicy: own.dataPolicy } : undefined },
42
+ ]);
43
+ for (const diagnostic of merged.diagnostics)
44
+ fail(`${pointer}/dataPolicy`, diagnostic.message);
45
+ result.dataPolicy = merged.policy.dataPolicy;
46
+ }
47
+ for (const key of Object.keys(base.execution ?? {})) {
48
+ const before = base.execution[key];
49
+ const after = own.execution?.[key];
50
+ // Execution constraints have no universally safe cross-profile ordering.
51
+ if (after !== undefined && after !== before)
52
+ fail(`${pointer}/execution/${key}`, 'Inherited execution control cannot change');
53
+ }
54
+ return result;
55
+ };
56
+ const shape = (value, pointer) => {
57
+ if (!object(value) || value.template !== undefined && typeof value.template !== 'string'
58
+ || value.members !== undefined && (!Array.isArray(value.members) || value.members.some((member) => !object(member) || !Array.isArray(member.roles)))
59
+ || value.efforts !== undefined && !Array.isArray(value.efforts)
60
+ || value.execution !== undefined && !object(value.execution)) {
61
+ errors.push(`OG_CONFIG_REFERENCE ${pointer}: Invalid group template shape`);
62
+ return false;
63
+ }
64
+ return true;
65
+ };
66
+ const expanded = new Map();
67
+ const depths = new Map();
68
+ const failed = new Set();
69
+ const expand = (name) => {
70
+ const pending = [];
71
+ const active = new Set();
72
+ let next = name;
73
+ // Collect uncached ancestors iteratively, then expand from the root down.
74
+ while (!expanded.has(next) && !failed.has(next)) {
75
+ const pointer = `/parallelism/groupTemplates/${escape(next)}`;
76
+ if (active.has(next)) {
77
+ errors.push(`OG_CONFIG_REFERENCE ${pointer}: Cyclic or excessive template nesting`);
78
+ failed.add(next);
79
+ break;
80
+ }
81
+ if (!object(templates) || !Object.hasOwn(templates, next)) {
82
+ errors.push(`OG_CONFIG_REFERENCE ${pointer}: Unknown template`);
83
+ failed.add(next);
84
+ break;
85
+ }
86
+ const own = templates[next];
87
+ if (!shape(own, pointer)) {
88
+ failed.add(next);
89
+ break;
90
+ }
91
+ active.add(next);
92
+ pending.push(next);
93
+ if (!own.template)
94
+ break;
95
+ next = own.template;
96
+ }
97
+ for (const current of pending.reverse()) {
98
+ const own = templates[current];
99
+ if (failed.has(current) || own.template && failed.has(own.template)) {
100
+ failed.add(current);
101
+ continue;
102
+ }
103
+ const depth = (own.template ? depths.get(own.template) : 0) + 1;
104
+ if (depth > 64) {
105
+ errors.push(`OG_CONFIG_REFERENCE /parallelism/groupTemplates/${escape(current)}: Cyclic or excessive template nesting`);
106
+ failed.add(current);
107
+ continue;
108
+ }
109
+ const parent = own.template ? expanded.get(own.template) : {};
110
+ expanded.set(current, merge(parent, own, `/parallelism/groupTemplates/${escape(current)}`));
111
+ depths.set(current, depth);
112
+ }
113
+ return expanded.get(name) ?? {};
114
+ };
115
+ if (object(templates))
116
+ for (const name of Object.keys(templates))
117
+ expand(name);
118
+ if (object(input.groups)) {
119
+ for (const [id, own] of Object.entries(input.groups)) {
120
+ const pointer = `/parallelism/groups/${escape(id)}`;
121
+ if (!shape(own, pointer))
122
+ continue;
123
+ const base = own.template ? expand(own.template) : {};
124
+ const result = merge(base, own, pointer);
125
+ // Existing shape validation requires members on every fully expanded group.
126
+ parallelism.groups[id] = result;
127
+ if (own.template) {
128
+ const names = [];
129
+ for (let name = own.template; name && expanded.has(name); name = templates[name].template)
130
+ names.push(name);
131
+ names.reverse();
132
+ origins[`${pointer}/template`] = names.map(name => ({ source, authority: 'project',
133
+ digest: workerConfigDigest(templates[name]), pointer: `/parallelism/groupTemplates/${escape(name)}`,
134
+ contribution: structuredClone(templates[name]),
135
+ }));
136
+ origins[`${pointer}/template`].push({ source, authority: 'project', digest: workerConfigDigest(own), pointer, contribution: structuredClone(own) });
137
+ }
138
+ }
139
+ }
140
+ // Keep raw declarations for stable provenance; the validator checks them separately.
141
+ return { parallelism, origins, errors };
142
+ }
143
+ /** Group data declarations are further restrictions on the already merged model policy. */
144
+ export function mergeGroupWorkerPolicy(policy, group) {
145
+ return mergeWorkerSelection([
146
+ { source: 'models:effective', authority: 'project', digest: workerConfigDigest(policy), policy },
147
+ { source: 'parallelism:group', authority: 'project', digest: workerConfigDigest(group), policy: group.dataPolicy ? { dataPolicy: group.dataPolicy } : undefined },
148
+ ]);
149
+ }
150
+ //# sourceMappingURL=worker-group-templates.js.map
@@ -0,0 +1,28 @@
1
+ // Reports guidance refresh steps after direct parallelism edits.
2
+ import path from 'node:path';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { normalizeProviderDefinitionId } from '../providers/provider-definitions.js';
5
+ export function warnStaleParallelism(cfg, projectDir, key, before) {
6
+ if (key !== 'parallelism' && !key.startsWith('parallelism.'))
7
+ return;
8
+ if (isDeepStrictEqual(before, getDottedPath(cfg, key)))
9
+ return;
10
+ const configured = (cfg.providers ?? []).map(normalizeProviderDefinitionId).filter((id) => !!id);
11
+ const recorded = Object.values(cfg.installed ?? {}).flatMap(entry => Object.keys(entry.deployedTo ?? {}))
12
+ .map(normalizeProviderDefinitionId).filter((id) => !!id).sort();
13
+ const providers = [...new Set([...configured, ...recorded])];
14
+ const targets = providers.length ? providers : ['<provider>'];
15
+ const lines = [
16
+ `WARNING: ${key} changed project configuration; generated workspace/provider guidance may now be stale.`,
17
+ `From project root ${path.resolve(projectDir)}, preview and refresh every configured or recorded provider context:`,
18
+ ...targets.flatMap(provider => [
19
+ ` aiwg regenerate --workspace --provider ${provider} --dry-run`,
20
+ ` aiwg regenerate --workspace --provider ${provider}`,
21
+ ]),
22
+ ' aiwg workspace-context doctor',
23
+ `Shared parallelism defaults and labels use primary provider '${cfg.providers?.[0] ?? 'unknown'}' (providers[0]); --provider selects the adapter/context target.`,
24
+ ];
25
+ console.error(lines.join('\n'));
26
+ }
27
+ function getDottedPath(value, key) { return key.split('.').reduce((node, part) => node?.[part], value); }
28
+ //# sourceMappingURL=worker-guidance-warning.js.map
@@ -0,0 +1,152 @@
1
+ // Publishes complete owner identities atomically and fences dead-owner takeover by generation.
2
+ import { randomUUID } from 'node:crypto';
3
+ import { hostname } from 'node:os';
4
+ import { basename, dirname, join, resolve } from 'node:path';
5
+ import { mkdir, writeFile, symlink, readlink, unlink, rm, readdir } from 'node:fs/promises';
6
+ import { readFileSync, readlinkSync } from 'node:fs';
7
+ import { projectControlPath } from './project-artifacts.js';
8
+ export const workerWriteLock = (project) => projectControlPath(project, 'aiwg.config.apply-lock');
9
+ const missing = (error) => error.code === 'ENOENT';
10
+ function processStart(pid) {
11
+ try {
12
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
13
+ return `${readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim()}:${stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19]}`;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ function identity() {
20
+ return { generation: randomUUID(), host: hostname(), pid: process.pid, start: processStart(process.pid) };
21
+ }
22
+ function provablyDead(owner) {
23
+ if (owner.released)
24
+ return true; // The operation ended and explicitly relinquished exclusion.
25
+ if (owner.host !== hostname() || !Number.isSafeInteger(owner.pid) || owner.pid <= 0)
26
+ return false;
27
+ try {
28
+ process.kill(owner.pid, 0);
29
+ }
30
+ catch (error) {
31
+ return error.code === 'ESRCH';
32
+ }
33
+ const start = processStart(owner.pid);
34
+ return typeof owner.start === 'string' && owner.start.length > 0 && start !== null && owner.start !== start;
35
+ }
36
+ function latest(directory) {
37
+ let owner = JSON.parse(readFileSync(join(directory, 'owner.json'), 'utf8'));
38
+ // Immutable successor links are a compare-and-swap: only one contender can claim a generation.
39
+ for (;;) {
40
+ if (!/^[a-f0-9-]{36}$/.test(owner.generation))
41
+ throw new Error('Invalid worker lock generation');
42
+ try {
43
+ owner = JSON.parse(readlinkSync(join(directory, `next-${owner.generation}`)));
44
+ }
45
+ catch (error) {
46
+ if (missing(error))
47
+ return owner;
48
+ throw error;
49
+ }
50
+ }
51
+ }
52
+ async function sweepOrphanedOwners(path) {
53
+ const parent = dirname(path);
54
+ const prefix = `${basename(path)}.`;
55
+ for (const entry of await readdir(parent, { withFileTypes: true })) {
56
+ if (!entry.isDirectory() || !entry.name.startsWith(prefix) || !/^[a-f0-9-]{36}$/.test(entry.name.slice(prefix.length)))
57
+ continue;
58
+ const directory = join(parent, entry.name);
59
+ let owner;
60
+ try {
61
+ owner = latest(directory);
62
+ }
63
+ catch {
64
+ continue;
65
+ } // Incomplete or unreadable identities cannot prove death.
66
+ if (!provablyDead(owner))
67
+ continue;
68
+ try {
69
+ if (resolve(parent, await readlink(path)) === directory)
70
+ continue;
71
+ }
72
+ catch (error) {
73
+ if (!missing(error))
74
+ throw error;
75
+ }
76
+ await rm(directory, { recursive: true, force: true });
77
+ }
78
+ }
79
+ function lease(path, directory, owner) {
80
+ const assertOwned = () => {
81
+ try {
82
+ if (readlinkSync(path) === basename(directory) && latest(directory).generation === owner.generation)
83
+ return;
84
+ }
85
+ catch { /* Missing or replaced ownership must never authorize another write. */ }
86
+ throw new Error(`Worker configuration lock ownership lost (${owner.generation}); dispatch remains closed at ${path}`);
87
+ };
88
+ return { path, generation: owner.generation, assertOwned, release: async (keepBlocked = false) => {
89
+ assertOwned();
90
+ if (keepBlocked) {
91
+ // A caught failure can be retried even while this embedding process stays alive.
92
+ await symlink(JSON.stringify({ ...identity(), released: true }), join(directory, `next-${owner.generation}`));
93
+ }
94
+ else {
95
+ // A live owner cannot be reclaimed; unlink only while we still own this exact generation.
96
+ await unlink(path);
97
+ await rm(directory, { recursive: true });
98
+ }
99
+ } };
100
+ }
101
+ export async function acquireWorkerWriteLock(project, block = false, proposalDigest) {
102
+ const path = workerWriteLock(project);
103
+ const owner = { ...identity(), ...(proposalDigest ? { proposalDigest } : {}) };
104
+ const directory = `${path}.${owner.generation}`;
105
+ await mkdir(dirname(path), { recursive: true });
106
+ await sweepOrphanedOwners(path);
107
+ await mkdir(directory);
108
+ try {
109
+ await writeFile(join(directory, 'owner.json'), JSON.stringify(owner), { mode: 0o600 });
110
+ if (block)
111
+ await writeFile(join(directory, 'blocked'), 'Worker policy apply/recovery in progress.\n', { mode: 0o600 });
112
+ // No visible lock can exist without an owner, even if initialization is interrupted.
113
+ await symlink(basename(directory), path);
114
+ }
115
+ catch (error) {
116
+ await rm(directory, { recursive: true, force: true });
117
+ if (error.code === 'EEXIST')
118
+ throw new Error(`Worker configuration is locked: ${path}; use authorized recovery after its owner exits`);
119
+ throw error;
120
+ }
121
+ return lease(path, directory, owner);
122
+ }
123
+ export async function reclaimWorkerWriteLock(project) {
124
+ const path = workerWriteLock(project);
125
+ let directory;
126
+ let previous;
127
+ try {
128
+ directory = resolve(dirname(path), await readlink(path));
129
+ previous = latest(directory);
130
+ }
131
+ catch (error) {
132
+ if (missing(error))
133
+ throw new Error(`No interrupted apply; nothing to recover at ${path}`);
134
+ throw error;
135
+ }
136
+ if (!provablyDead(previous))
137
+ throw new Error(`Worker configuration is locked by a live or unverifiable owner (${previous.host}:${previous.pid}); ${path}`);
138
+ const owner = identity();
139
+ try {
140
+ await symlink(JSON.stringify(owner), join(directory, `next-${previous.generation}`));
141
+ }
142
+ catch (error) {
143
+ if (error.code === 'EEXIST')
144
+ throw new Error(`Worker configuration is locked: another recovery claimed generation ${previous.generation}`);
145
+ throw error;
146
+ }
147
+ const result = lease(path, directory, owner);
148
+ result.assertOwned();
149
+ await writeFile(join(directory, 'blocked'), 'Worker policy recovery in progress.\n', { mode: 0o600 });
150
+ return result;
151
+ }
152
+ //# sourceMappingURL=worker-owner-lock.js.map