@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.
- package/agentic/code/providers/model-capabilities.v1.json +197 -3
- package/bin/aiwg.mjs +8 -0
- package/dist/src/artifacts/cli.js +9 -5
- package/dist/src/auth/config.js +3 -8
- package/dist/src/auth/resource-credentials.js +35 -2
- package/dist/src/cli/handlers/diagnose.js +13 -0
- package/dist/src/cli/handlers/mc-admission.js +39 -0
- package/dist/src/cli/handlers/mc.js +2 -31
- package/dist/src/cli/handlers/models-help.js +52 -0
- package/dist/src/cli/handlers/models.js +11 -81
- package/dist/src/cli/handlers/resource-versions.js +8 -5
- package/dist/src/cli/handlers/script-runner.js +13 -1
- package/dist/src/cli/handlers/use-addon-profile.js +72 -0
- package/dist/src/cli/handlers/use.js +209 -233
- package/dist/src/cli/ui.js +1 -0
- package/dist/src/config/aiwg-config.js +38 -184
- package/dist/src/config/cli-help.js +54 -0
- package/dist/src/config/cli.js +5 -69
- package/dist/src/config/config-normalization.js +10 -0
- package/dist/src/config/worker-activation-gate.js +26 -0
- package/dist/src/config/worker-adoption-cli.js +102 -0
- package/dist/src/config/worker-adoption.js +368 -0
- package/dist/src/config/worker-config-digest.js +14 -0
- package/dist/src/config/worker-group-templates.js +150 -0
- package/dist/src/config/worker-guidance-warning.js +28 -0
- package/dist/src/config/worker-owner-lock.js +152 -0
- package/dist/src/config/worker-parallelism.js +154 -0
- package/dist/src/config/worker-write.js +75 -0
- package/dist/src/decision/telemetry/exporter.js +2 -1
- package/dist/src/decision/telemetry/index.js +1 -0
- package/dist/src/decision/telemetry/mapping.js +43 -0
- package/dist/src/decision/telemetry/otlp-http.js +5 -2
- package/dist/src/decision/telemetry/redaction.js +6 -1
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/telemetry/worker.js +69 -0
- package/dist/src/lint/rule-activation.js +15 -7
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/server.mjs +2 -11
- package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
- package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
- package/dist/src/mcp/tools/worker-invoke.mjs +6 -6
- package/dist/src/mission-protocol/codecs.js +8 -38
- package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
- package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
- package/dist/src/models/conductor-data-policy.d.mts +8 -0
- package/dist/src/models/conductor-data-policy.mjs +190 -0
- package/dist/src/models/config-loader.js +19 -3
- package/dist/src/models/config-writer.js +27 -0
- package/dist/src/models/model-capabilities.v1.json +197 -3
- package/dist/src/models/provider-policy.js +7 -155
- package/dist/src/models/resolver.js +2 -293
- package/dist/src/models/worker-artifact-binding.mjs +12 -0
- package/dist/src/models/worker-direct-readiness-policy.js +155 -0
- package/dist/src/models/worker-direct-readiness.js +9 -218
- package/dist/src/models/worker-health-readiness.js +105 -26
- package/dist/src/models/worker-installation.mjs +12 -14
- package/dist/src/models/worker-member-references.js +64 -0
- package/dist/src/models/worker-plan.js +317 -0
- package/dist/src/models/worker-selection-policy.js +217 -0
- package/dist/src/models/wrapper-deployment.js +1 -1
- package/dist/src/resources/web-release.js +76 -65
- package/dist/src/serve/shared-host-admission-checks.js +116 -0
- package/dist/src/serve/shared-host-scheduler.js +25 -107
- package/dist/src/serve/shared-host-starvation.js +73 -0
- package/dist/src/serve/worker-artifacts.d.mts +4 -0
- package/dist/src/serve/worker-artifacts.mjs +56 -0
- package/dist/src/serve/worker-command-request.js +68 -0
- package/dist/src/serve/worker-invocation-paths.js +144 -0
- package/dist/src/serve/worker-invocation-preparation.js +169 -0
- package/dist/src/serve/worker-invocation.js +24 -291
- package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
- package/dist/src/serve/worker-stdin-frame.mjs +6 -6
- package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
- package/dist/src/storage/backends/fortemi.js +33 -29
- package/dist/src/storage/backends/logseq.js +16 -3
- package/dist/src/storage/backends/postgres.js +74 -33
- package/dist/src/storage/backends/postgrest.js +37 -13
- package/dist/src/storage/config.js +6 -1
- package/dist/src/uhp/config.js +4 -0
- package/package.json +9 -1
- package/schemas/mission-v1.schema.json +20 -19
- package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
- package/tools/agents/providers/claude-post-deploy.mjs +35 -0
- package/tools/agents/providers/claude.mjs +3 -30
- package/tools/agents/providers/codex-post-deploy.mjs +13 -0
- package/tools/agents/providers/codex.mjs +2 -8
- package/tools/agents/providers/conductor-data-guard.mjs +61 -0
- package/tools/agents/providers/rule-delivery.mjs +3 -16
- package/tools/agents/providers/rule-presentation.mjs +38 -0
- package/tools/plugin/package-plugins.mjs +1 -1
- package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
- package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
- package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
- package/tools/ralph-external/lib/session-attempt.mjs +85 -0
- package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
- package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
- package/tools/ralph-external/lib/worker-final.mjs +143 -0
- package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +2 -44
- package/tools/ralph-external/lib/worker-launch-cli.mjs +47 -0
- package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
- package/tools/ralph-external/session-launcher.mjs +73 -217
- package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { normalizeAiwgConfigCandidate } from './config-normalization.js';
|
|
2
|
+
// Reviews and atomically activates the existing worker policy pair with full-copy recovery.
|
|
3
|
+
import { readFile, rm, access } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { ConfigLoader } from '../models/config-loader.js';
|
|
6
|
+
import { validateUserProjectModelConfig } from '../models/provider-policy.js';
|
|
7
|
+
import { emptyConfig, getConfigPath, workerActivationDigests, workerConfigDigest, validateParallelismConfig } from './aiwg-config.js';
|
|
8
|
+
import { projectControlPath, resolveProjectAiwgDir } from './project-artifacts.js';
|
|
9
|
+
import { acquireWorkerWriteLock, reclaimWorkerWriteLock, atomicConfigWrite, runWorkerMaintenance } from './worker-write.js';
|
|
10
|
+
const json = (input) => input.bytes === null ? null : JSON.parse(input.bytes);
|
|
11
|
+
async function capture(file) {
|
|
12
|
+
let bytes = null;
|
|
13
|
+
try {
|
|
14
|
+
bytes = await readFile(file, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code !== 'ENOENT')
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
return { file: resolve(file), bytes, digest: workerConfigDigest(bytes) };
|
|
21
|
+
}
|
|
22
|
+
const revisionPath = (project) => projectControlPath(project, 'aiwg.config.apply-lock.revision.json');
|
|
23
|
+
async function adoptionRevision(project, activationRevision = 0) {
|
|
24
|
+
const input = await capture(revisionPath(project));
|
|
25
|
+
const revision = input.bytes === null ? 0 : json(input).revision;
|
|
26
|
+
if (!Number.isSafeInteger(revision) || revision < 0 || revision >= Number.MAX_SAFE_INTEGER)
|
|
27
|
+
throw new Error('Invalid adoption revision');
|
|
28
|
+
return Math.max(revision, activationRevision);
|
|
29
|
+
}
|
|
30
|
+
async function inventory(project, options) {
|
|
31
|
+
const locations = new ConfigLoader(project, options).getLocations();
|
|
32
|
+
const compatibility = join(resolveProjectAiwgDir(project), 'aiwg.config');
|
|
33
|
+
const files = [getConfigPath(project), locations.projectConfig, locations.userConfig, locations.aiwgDefault];
|
|
34
|
+
if (!files.includes(compatibility)) {
|
|
35
|
+
// The config writer synchronizes a compatibility copy when its directory is reachable.
|
|
36
|
+
try {
|
|
37
|
+
await access(resolveProjectAiwgDir(project));
|
|
38
|
+
files.push(compatibility);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code !== 'ENOENT')
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return Promise.all([...new Set(files.map(file => resolve(file)))].map(capture));
|
|
46
|
+
}
|
|
47
|
+
const pair = (config, models) => {
|
|
48
|
+
const parallelism = structuredClone(config.parallelism ?? null);
|
|
49
|
+
if (parallelism)
|
|
50
|
+
delete parallelism.activation;
|
|
51
|
+
return { parallelism, workerSelection: structuredClone(models?.defaults?.workerSelection ?? null) };
|
|
52
|
+
};
|
|
53
|
+
function candidate(inputs, after) {
|
|
54
|
+
const configInput = [inputs[0], ...inputs.slice(4)].find(input => input.bytes !== null);
|
|
55
|
+
const config = normalizeAiwgConfigCandidate(configInput ? json(configInput) : emptyConfig());
|
|
56
|
+
const models = json(inputs[1]) ?? {};
|
|
57
|
+
if (after) {
|
|
58
|
+
config.parallelism = structuredClone(after.parallelism ?? undefined);
|
|
59
|
+
models.defaults ??= {};
|
|
60
|
+
if (after.workerSelection === null)
|
|
61
|
+
delete models.defaults.workerSelection;
|
|
62
|
+
else
|
|
63
|
+
models.defaults.workerSelection = structuredClone(after.workerSelection);
|
|
64
|
+
}
|
|
65
|
+
return { config, models };
|
|
66
|
+
}
|
|
67
|
+
async function validate(project, config, models, options) {
|
|
68
|
+
const errors = validateParallelismConfig(config.parallelism);
|
|
69
|
+
const validation = validateUserProjectModelConfig(models);
|
|
70
|
+
if (errors.length || !validation.valid)
|
|
71
|
+
throw new Error([...errors, ...validation.diagnostics.map(item => item.message)].join('; '));
|
|
72
|
+
const loader = new ConfigLoader(project, { aiwgHome: options.aiwgHome, workerCandidate: { projectModels: models } });
|
|
73
|
+
await loader.load(config.parallelism);
|
|
74
|
+
return { ...workerActivationDigests(config), ...loader.getInputDigests() };
|
|
75
|
+
}
|
|
76
|
+
async function adoptedCandidate(proposal, options) {
|
|
77
|
+
const next = candidate(proposal.inputs, proposal.after);
|
|
78
|
+
const digests = await validate(proposal.project, next.config, next.models, options);
|
|
79
|
+
const previous = candidate(proposal.inputs).config.parallelism?.activation;
|
|
80
|
+
if (next.config.parallelism?.worker_mode === 'managed') {
|
|
81
|
+
next.config.parallelism.activation = { ...digests, revision: proposal.baseRevision + 1,
|
|
82
|
+
snapshotRef: proposal.digest, ...(previous ? { previousRef: previous.snapshotRef } : {}) };
|
|
83
|
+
}
|
|
84
|
+
return { ...next, digests, configBytes: JSON.stringify(next.config, null, 2) + '\n', modelsBytes: JSON.stringify(next.models, null, 2) + '\n' };
|
|
85
|
+
}
|
|
86
|
+
export async function proposeWorkerAdoption(project, changes, options = {}) {
|
|
87
|
+
project = resolve(project);
|
|
88
|
+
const inputs = await inventory(project, options);
|
|
89
|
+
validateRetainedCopies(inputs);
|
|
90
|
+
const { config, models } = candidate(inputs);
|
|
91
|
+
await validate(project, config, models, options);
|
|
92
|
+
const before = pair(config, models);
|
|
93
|
+
const after = { ...before, ...structuredClone(changes) };
|
|
94
|
+
if (after.parallelism?.activation !== undefined)
|
|
95
|
+
throw new Error('Activation markers are produced by apply, never by proposals');
|
|
96
|
+
const next = candidate(inputs, after);
|
|
97
|
+
await validate(project, next.config, next.models, options);
|
|
98
|
+
const proposal = { project, baseRevision: await adoptionRevision(project, config.parallelism?.activation?.revision), inputs, before, after };
|
|
99
|
+
return { ...proposal, digest: workerConfigDigest(proposal) };
|
|
100
|
+
}
|
|
101
|
+
function changedFields(before, after, pointer = '') {
|
|
102
|
+
if (workerConfigDigest(before) === workerConfigDigest(after))
|
|
103
|
+
return [];
|
|
104
|
+
const present = [before, after].filter(value => value !== undefined && value !== null);
|
|
105
|
+
if (present.length && present.every(value => typeof value === 'object' && !Array.isArray(value))) {
|
|
106
|
+
const keys = [...new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})])];
|
|
107
|
+
if (!keys.length)
|
|
108
|
+
return [pointer];
|
|
109
|
+
return keys.flatMap(key => changedFields(before?.[key], after?.[key], `${pointer}/${key.replace(/~/g, '~0').replace(/\//g, '~1')}`));
|
|
110
|
+
}
|
|
111
|
+
return [pointer];
|
|
112
|
+
}
|
|
113
|
+
function authorize(proposal, authority) {
|
|
114
|
+
const { digest, ...body } = proposal;
|
|
115
|
+
if (workerConfigDigest(body) !== digest)
|
|
116
|
+
throw new Error('Proposal digest mismatch; review again');
|
|
117
|
+
if (authority && 'digest' in authority && authority.digest === digest)
|
|
118
|
+
return;
|
|
119
|
+
if (authority && 'fields' in authority && authority.project === proposal.project && authority.revision === proposal.baseRevision
|
|
120
|
+
&& Array.isArray(authority.fields) && authority.fields.length && authority.fields.every(field => typeof field === 'string' && /^\/(parallelism|workerSelection)(\/[^/]+)*$/.test(field) && field !== '/parallelism/activation' && !field.startsWith('/parallelism/activation/'))
|
|
121
|
+
&& changedFields(proposal.before, proposal.after).every(field => authority.fields.some(scope => field === scope || field.startsWith(`${scope}/`))))
|
|
122
|
+
return;
|
|
123
|
+
throw new Error('Worker adoption requires the exact proposal digest or standing authorization for every changed field and base revision');
|
|
124
|
+
}
|
|
125
|
+
function validateRetainedCopies(inputs) {
|
|
126
|
+
const canonical = [inputs[0], ...inputs.slice(4)].find(input => input.bytes !== null);
|
|
127
|
+
for (const copy of inputs.slice(4)) {
|
|
128
|
+
if (copy.bytes !== null && workerConfigDigest(json(copy)) !== workerConfigDigest(canonical && json(canonical)))
|
|
129
|
+
throw new Error(`Compatibility config diverges from the canonical validated copy: ${copy.file}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function recheck(proposal, options, checkRevision = true) {
|
|
133
|
+
const current = await inventory(proposal.project, options);
|
|
134
|
+
if (workerConfigDigest(current) !== workerConfigDigest(proposal.inputs))
|
|
135
|
+
throw new Error('Stale proposal: input bytes/digests or participating copies changed');
|
|
136
|
+
const { config, models } = candidate(current);
|
|
137
|
+
if ((checkRevision && await adoptionRevision(proposal.project, config.parallelism?.activation?.revision) !== proposal.baseRevision) || workerConfigDigest(pair(config, models)) !== workerConfigDigest(proposal.before))
|
|
138
|
+
throw new Error('Stale proposal: base revision or reviewed pair changed');
|
|
139
|
+
return current;
|
|
140
|
+
}
|
|
141
|
+
export async function regenerateWorkerGuidance(project, config) {
|
|
142
|
+
const { generate, discoverDeployedArtifacts } = await import('../smiths/context-pipeline/index.js');
|
|
143
|
+
const { getProviderContextDiscoveryPathStrings } = await import('../providers/provider-definitions.js');
|
|
144
|
+
const providers = [...new Set([...(config.providers ?? []), ...Object.values(config.installed ?? {}).flatMap(entry => Object.keys(entry.deployedTo ?? {}))])];
|
|
145
|
+
if (!providers.length)
|
|
146
|
+
throw new Error('Cannot regenerate guidance: configure at least one provider');
|
|
147
|
+
for (const provider of providers) {
|
|
148
|
+
const paths = getProviderContextDiscoveryPathStrings(provider);
|
|
149
|
+
if (!paths)
|
|
150
|
+
throw new Error(`Cannot regenerate guidance for ${provider}`);
|
|
151
|
+
const sections = await discoverDeployedArtifacts(project, paths);
|
|
152
|
+
const result = await generate({ provider: provider, projectPath: project, sections, detectExistingFiles: true });
|
|
153
|
+
if (result.warnings.length)
|
|
154
|
+
throw new Error(`Guidance regeneration for ${provider}: ${result.warnings.join('; ')}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function applyWorkerAdoption(project, proposal, authority, options = {}) {
|
|
158
|
+
if (proposal.project !== resolve(project))
|
|
159
|
+
throw new Error('Proposal belongs to a different project');
|
|
160
|
+
authorize(proposal, authority);
|
|
161
|
+
if (proposal.after.parallelism?.activation !== undefined || workerConfigDigest(Object.keys(proposal.after).sort()) !== workerConfigDigest(['parallelism', 'workerSelection']))
|
|
162
|
+
throw new Error('Proposal exceeds the worker policy pair');
|
|
163
|
+
// Reject staleness before writing even the lock or recovery record.
|
|
164
|
+
await recheck(proposal, options);
|
|
165
|
+
const lock = await acquireWorkerWriteLock(project, true, proposal.digest);
|
|
166
|
+
const write = (file, content) => atomicConfigWrite(file, content, lock.assertOwned);
|
|
167
|
+
let keepBlocked = false;
|
|
168
|
+
let regenerationStarted = false;
|
|
169
|
+
try {
|
|
170
|
+
await options.checkpoint?.('locked');
|
|
171
|
+
lock.assertOwned();
|
|
172
|
+
await runWorkerMaintenance(async () => {
|
|
173
|
+
const inputs = await recheck(proposal, options);
|
|
174
|
+
validateRetainedCopies(inputs);
|
|
175
|
+
const previous = candidate(inputs);
|
|
176
|
+
await validate(project, previous.config, previous.models, options);
|
|
177
|
+
const next = await adoptedCandidate(proposal, options);
|
|
178
|
+
const { digests, configBytes, modelsBytes } = next;
|
|
179
|
+
const configCopies = [inputs[0], ...inputs.slice(4)];
|
|
180
|
+
// Keep the validated bytes durably before the first participating write.
|
|
181
|
+
await write(join(lock.path, 'recovery.json'), JSON.stringify({ proposal, inputs }, null, 2));
|
|
182
|
+
await options.checkpoint?.('blocked');
|
|
183
|
+
lock.assertOwned();
|
|
184
|
+
options.signal?.throwIfAborted();
|
|
185
|
+
await recheck(proposal, options);
|
|
186
|
+
let committed = false;
|
|
187
|
+
try {
|
|
188
|
+
await write(inputs[1].file, modelsBytes);
|
|
189
|
+
await options.checkpoint?.('models');
|
|
190
|
+
options.signal?.throwIfAborted();
|
|
191
|
+
for (const [index, input] of configCopies.entries()) {
|
|
192
|
+
await write(input.file, configBytes);
|
|
193
|
+
await options.checkpoint?.(`config:${index}`);
|
|
194
|
+
options.signal?.throwIfAborted();
|
|
195
|
+
}
|
|
196
|
+
const check = async () => {
|
|
197
|
+
lock.assertOwned();
|
|
198
|
+
const current = await inventory(project, options);
|
|
199
|
+
const expected = inputs.map(input => ({ ...input, bytes: input.file === inputs[1].file ? modelsBytes : configCopies.some(copy => copy.file === input.file) ? configBytes : input.bytes }));
|
|
200
|
+
if (current.length !== expected.length || current.some((input, index) => input.file !== expected[index].file || input.bytes !== expected[index].bytes))
|
|
201
|
+
throw new Error('Activation copies or model inputs changed during apply');
|
|
202
|
+
const actual = await validate(project, next.config, next.models, options);
|
|
203
|
+
if (workerConfigDigest(actual) !== workerConfigDigest(digests))
|
|
204
|
+
throw new Error('Activation digest verification failed');
|
|
205
|
+
};
|
|
206
|
+
await check();
|
|
207
|
+
await options.checkpoint?.('activation');
|
|
208
|
+
options.signal?.throwIfAborted();
|
|
209
|
+
lock.assertOwned();
|
|
210
|
+
regenerationStarted = true;
|
|
211
|
+
await (options.regenerate ?? regenerateWorkerGuidance)(project, next.config);
|
|
212
|
+
await options.checkpoint?.('regenerated');
|
|
213
|
+
options.signal?.throwIfAborted();
|
|
214
|
+
await check();
|
|
215
|
+
await write(revisionPath(project), JSON.stringify({ revision: proposal.baseRevision + 1 }) + '\n');
|
|
216
|
+
committed = true;
|
|
217
|
+
await options.checkpoint?.('committed');
|
|
218
|
+
lock.assertOwned();
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (committed) {
|
|
222
|
+
keepBlocked = true;
|
|
223
|
+
throw new Error(`Worker adoption committed: ${error}. Dispatch remains closed at ${lock.path}; finish with authorized recovery`);
|
|
224
|
+
}
|
|
225
|
+
const failures = [];
|
|
226
|
+
// Attempt EVERY owned copy, including a write that may have renamed then thrown.
|
|
227
|
+
for (const input of [inputs[1], ...configCopies]) {
|
|
228
|
+
try {
|
|
229
|
+
lock.assertOwned();
|
|
230
|
+
if (input.bytes === null)
|
|
231
|
+
await rm(input.file, { force: true });
|
|
232
|
+
else
|
|
233
|
+
await write(input.file, input.bytes);
|
|
234
|
+
}
|
|
235
|
+
catch (restoreError) {
|
|
236
|
+
failures.push(`${input.file}: ${restoreError}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
await recheck(proposal, options);
|
|
241
|
+
await validate(project, previous.config, previous.models, options);
|
|
242
|
+
}
|
|
243
|
+
catch (restoreError) {
|
|
244
|
+
failures.push(`restored pair validation: ${restoreError}`);
|
|
245
|
+
}
|
|
246
|
+
// Regenerated guidance may be partial; keep submissions closed until explicit recovery.
|
|
247
|
+
keepBlocked = failures.length > 0 || regenerationStarted;
|
|
248
|
+
throw new Error(`Worker adoption failed: ${error}. ${keepBlocked ? `Dispatch remains closed at ${lock.path}; ${failures.join('; ') || 'guidance consistency requires recovery'}` : 'All participating copies restored and revalidated.'}`);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
// A displaced owner must neither restore bytes nor remove a successor's exclusion.
|
|
254
|
+
let owned = true;
|
|
255
|
+
try {
|
|
256
|
+
lock.assertOwned();
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
owned = false;
|
|
260
|
+
}
|
|
261
|
+
if (owned)
|
|
262
|
+
await lock.release(keepBlocked);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** Recover an interrupted apply from its retained, authorized bytes; never infer approval. */
|
|
266
|
+
export async function recoverWorkerAdoption(project, authority, options = {}) {
|
|
267
|
+
const lock = await reclaimWorkerWriteLock(project);
|
|
268
|
+
const write = (file, content) => atomicConfigWrite(file, content, lock.assertOwned);
|
|
269
|
+
let recovered = false;
|
|
270
|
+
try {
|
|
271
|
+
await options.checkpoint?.('recovery:owned');
|
|
272
|
+
await runWorkerMaintenance(async () => {
|
|
273
|
+
lock.assertOwned();
|
|
274
|
+
const record = await capture(join(lock.path, 'recovery.json'));
|
|
275
|
+
if (record.bytes === null) {
|
|
276
|
+
// Publication precedes recovery.json, and recovery.json precedes every pair write.
|
|
277
|
+
// A missing record therefore needs validation, never speculative restoration.
|
|
278
|
+
const inputs = await inventory(project, options);
|
|
279
|
+
validateRetainedCopies(inputs);
|
|
280
|
+
const current = candidate(inputs);
|
|
281
|
+
await validate(project, current.config, current.models, options);
|
|
282
|
+
options.signal?.throwIfAborted();
|
|
283
|
+
lock.assertOwned();
|
|
284
|
+
await (options.regenerate ?? regenerateWorkerGuidance)(project, current.config);
|
|
285
|
+
if (workerConfigDigest(await inventory(project, options)) !== workerConfigDigest(inputs))
|
|
286
|
+
throw new Error('Inputs changed during incomplete-initialization recovery');
|
|
287
|
+
await validate(project, current.config, current.models, options);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
const retained = json(record);
|
|
291
|
+
const proposal = retained.proposal;
|
|
292
|
+
if (proposal.project !== resolve(project))
|
|
293
|
+
throw new Error('Recovery belongs to a different project');
|
|
294
|
+
authorize(proposal, authority);
|
|
295
|
+
if (workerConfigDigest(retained.inputs) !== workerConfigDigest(proposal.inputs))
|
|
296
|
+
throw new Error('Recovery bytes do not match the reviewed inputs');
|
|
297
|
+
const inputs = await inventory(project, options);
|
|
298
|
+
if (workerConfigDigest(inputs.map(input => input.file)) !== workerConfigDigest(proposal.inputs.map(input => input.file)))
|
|
299
|
+
throw new Error('Recovery copy inventory changed');
|
|
300
|
+
validateRetainedCopies(proposal.inputs);
|
|
301
|
+
options.signal?.throwIfAborted();
|
|
302
|
+
const previous = candidate(proposal.inputs);
|
|
303
|
+
await validate(project, previous.config, previous.models, options);
|
|
304
|
+
const next = await adoptedCandidate(proposal, options);
|
|
305
|
+
const expectedBytes = (index) => index === 1 ? next.modelsBytes : index === 0 || index >= 4 ? next.configBytes : proposal.inputs[index].bytes;
|
|
306
|
+
// The independent revision is the durable commit marker. Activation alone is
|
|
307
|
+
// insufficient: it is written before the pair has finished committing.
|
|
308
|
+
const committed = await adoptionRevision(project) > proposal.baseRevision;
|
|
309
|
+
if (committed) {
|
|
310
|
+
validateRetainedCopies(inputs);
|
|
311
|
+
const current = candidate(inputs);
|
|
312
|
+
if (workerConfigDigest(pair(current.config, current.models)) !== workerConfigDigest(proposal.after)
|
|
313
|
+
|| inputs.some((input, index) => input.bytes !== expectedBytes(index)))
|
|
314
|
+
throw new Error('Committed recovery pair does not match proposal.after; inspect the retained recovery record');
|
|
315
|
+
await validate(project, current.config, current.models, options);
|
|
316
|
+
lock.assertOwned();
|
|
317
|
+
await (options.regenerate ?? regenerateWorkerGuidance)(project, current.config);
|
|
318
|
+
if (workerConfigDigest(await inventory(project, options)) !== workerConfigDigest(inputs))
|
|
319
|
+
throw new Error('Committed recovery pair changed during guidance regeneration');
|
|
320
|
+
await validate(project, current.config, current.models, options);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
if (inputs.some((input, index) => input.bytes !== proposal.inputs[index].bytes && input.bytes !== expectedBytes(index)))
|
|
324
|
+
throw new Error('Recovery pair has unexpected input bytes matching neither before nor after; inspect the retained recovery record');
|
|
325
|
+
const failures = [];
|
|
326
|
+
for (const input of [proposal.inputs[1], proposal.inputs[0], ...proposal.inputs.slice(4)]) {
|
|
327
|
+
try {
|
|
328
|
+
lock.assertOwned();
|
|
329
|
+
if (input.bytes === null)
|
|
330
|
+
await rm(input.file, { force: true });
|
|
331
|
+
else
|
|
332
|
+
await write(input.file, input.bytes);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
failures.push(`${input.file}: ${error}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (failures.length)
|
|
339
|
+
throw new Error(failures.join('; '));
|
|
340
|
+
await recheck(proposal, options, false);
|
|
341
|
+
lock.assertOwned();
|
|
342
|
+
await (options.regenerate ?? regenerateWorkerGuidance)(project, previous.config);
|
|
343
|
+
await recheck(proposal, options, false);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
options.signal?.throwIfAborted();
|
|
347
|
+
lock.assertOwned();
|
|
348
|
+
await options.checkpoint?.('recovery:verified');
|
|
349
|
+
lock.assertOwned();
|
|
350
|
+
recovered = true;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
throw new Error(`Recovery failed; dispatch remains closed at ${lock.path}: ${error}`);
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
let owned = true;
|
|
358
|
+
try {
|
|
359
|
+
lock.assertOwned();
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
owned = false;
|
|
363
|
+
}
|
|
364
|
+
if (owned)
|
|
365
|
+
await lock.release(!recovered);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
//# sourceMappingURL=worker-adoption.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Hashes canonical worker configuration inputs for origins and replay checks.
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
/** Hash canonical JSON, treating absent optional model files as null. */
|
|
4
|
+
export function workerConfigDigest(value) {
|
|
5
|
+
const canonical = (node) => {
|
|
6
|
+
if (Array.isArray(node))
|
|
7
|
+
return node.map(canonical);
|
|
8
|
+
if (node !== null && typeof node === 'object')
|
|
9
|
+
return Object.fromEntries(Object.keys(node).sort().filter(key => node[key] !== undefined).map(key => [key, canonical(node[key])]));
|
|
10
|
+
return node ?? null;
|
|
11
|
+
};
|
|
12
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex')}`;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=worker-config-digest.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
|