@aiwg/cli 2026.10.6 → 2026.10.7
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/bin/aiwg.mjs +4 -2
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/decision.js +3 -2
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +1 -1
- package/dist/src/cli/handlers/init.js +1 -1
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/setup.js +24 -20
- package/dist/src/cli/handlers/use.js +57 -15
- package/dist/src/cli/prompt-utils.js +73 -0
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +19 -6
- package/dist/src/decision/qualification/gate-evidence.js +6 -4
- package/dist/src/decision/qualification/load-staged.js +47 -10
- package/dist/src/decision/qualification/runner.js +7 -3
- package/dist/src/discovery/interview.js +10 -5
- package/dist/src/discovery/plan-apply.js +26 -30
- package/dist/src/discovery/plan-validation.js +2 -2
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +2 -2
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +17 -90
- package/dist/src/mcp/grok-build-config.mjs +2 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/worker-installation.d.mts +1 -1
- package/dist/src/models/worker-installation.mjs +65 -4
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/serve/worker-invocation.js +10 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/package.json +1 -1
- package/tools/agents/providers/base.mjs +5 -1
- package/tools/agents/providers/claude.mjs +5 -2
- package/tools/agents/providers/codex.mjs +4 -1
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +5 -2
- package/tools/agents/providers/hermes.mjs +5 -2
- package/tools/agents/providers/omp.mjs +3 -1
- package/tools/agents/providers/rule-delivery.mjs +29 -12
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +6 -3
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/process-monitor.mjs +42 -4
- package/tools/ralph-external/session-launcher.mjs +125 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* OpenAI Codex Provider
|
|
3
4
|
*
|
|
@@ -68,6 +69,8 @@ import {
|
|
|
68
69
|
resolveAiwgRoot
|
|
69
70
|
} from './base.mjs';
|
|
70
71
|
|
|
72
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
73
|
+
|
|
71
74
|
function resolveCodexHelperRoot(srcRoot) {
|
|
72
75
|
const resolved = resolveAiwgRoot(srcRoot);
|
|
73
76
|
if (resolved) return resolved;
|
|
@@ -403,7 +406,7 @@ export function aggregateToAgentsMd(agentFiles, destPath, opts) {
|
|
|
403
406
|
}
|
|
404
407
|
const out = `${lines.join('\n')}\n`;
|
|
405
408
|
if (opts.dryRun) console.log(`[dry-run] write ${destPath}`);
|
|
406
|
-
else
|
|
409
|
+
else writeContextFileSync(destPath, out, 'utf8');
|
|
407
410
|
|
|
408
411
|
}
|
|
409
412
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-file writes for deployment providers (#3037).
|
|
3
|
+
*
|
|
4
|
+
* The journaled writer lives in compiled TypeScript. In source-resolution
|
|
5
|
+
* mode (AIWG_RESOLVE_IMPL_FROM=src without a TS loader) it cannot load, and
|
|
6
|
+
* read-only consumers of provider modules (for example the remotes topology
|
|
7
|
+
* block) must still import cleanly. Writes then fail closed with the original
|
|
8
|
+
* load error, so nothing ever writes context files without the safety layer.
|
|
9
|
+
*/
|
|
10
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
11
|
+
|
|
12
|
+
// Mirrors CONTEXT_STAGE_ENV in src/smiths/context-pipeline/context-file-safety.ts.
|
|
13
|
+
export const CONTEXT_STAGE_ENV = 'AIWG_CONTEXT_STAGE_LOG';
|
|
14
|
+
|
|
15
|
+
let loadError = null;
|
|
16
|
+
let writer = null;
|
|
17
|
+
try {
|
|
18
|
+
writer = await importImpl(import.meta.url, 'smiths/context-pipeline/context-file-safety.js');
|
|
19
|
+
} catch (error) {
|
|
20
|
+
loadError = error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function writeContextFileSync(file, content, encoding) {
|
|
24
|
+
if (!writer) {
|
|
25
|
+
throw new Error(`Context-file safety layer is unavailable; refusing to write ${file}`, { cause: loadError });
|
|
26
|
+
}
|
|
27
|
+
return writer.writeContextFileSync(file, content, encoding);
|
|
28
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* GitHub Copilot Provider
|
|
3
4
|
*
|
|
@@ -54,6 +55,8 @@ import {
|
|
|
54
55
|
deploySoulCompanions,
|
|
55
56
|
loadRuntimeModelCatalog
|
|
56
57
|
} from './base.mjs';
|
|
58
|
+
|
|
59
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
57
60
|
const modelCatalog = loadRuntimeModelCatalog(staticModelCatalog);
|
|
58
61
|
|
|
59
62
|
// ============================================================================
|
|
@@ -477,14 +480,14 @@ export function createCopilotInstructions(target, srcRoot, dryRun) {
|
|
|
477
480
|
if (dryRun) {
|
|
478
481
|
console.log(`[dry-run] Would update existing copilot-instructions.md with AIWG section`);
|
|
479
482
|
} else {
|
|
480
|
-
|
|
483
|
+
writeContextFileSync(destPath, combined, 'utf8');
|
|
481
484
|
console.log('Updated copilot-instructions.md with AIWG SDLC framework section');
|
|
482
485
|
}
|
|
483
486
|
} else {
|
|
484
487
|
if (dryRun) {
|
|
485
488
|
console.log(`[dry-run] Would create copilot-instructions.md from template`);
|
|
486
489
|
} else {
|
|
487
|
-
|
|
490
|
+
writeContextFileSync(destPath, template, 'utf8');
|
|
488
491
|
console.log('Created copilot-instructions.md from template');
|
|
489
492
|
}
|
|
490
493
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Hermes Agent Provider
|
|
3
4
|
*
|
|
@@ -41,6 +42,8 @@ import {
|
|
|
41
42
|
stripPlatformsFromContent,
|
|
42
43
|
} from './base.mjs';
|
|
43
44
|
|
|
45
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
46
|
+
|
|
44
47
|
// ============================================================================
|
|
45
48
|
// Hermes home resolution (HERMES_HOME) — #2119
|
|
46
49
|
// ============================================================================
|
|
@@ -355,7 +358,7 @@ MCP is optional; \`mcp_aiwg_rule_show\` exposes the same rule catalog.
|
|
|
355
358
|
if (dryRun) {
|
|
356
359
|
console.log(`[dry-run] Would write AGENTS.md (${output.length} chars, ${Math.round(output.length / 4)} tokens estimated)`);
|
|
357
360
|
} else {
|
|
358
|
-
|
|
361
|
+
writeContextFileSync(destPath, output, 'utf8');
|
|
359
362
|
const charCount = output.length;
|
|
360
363
|
const tokenEstimate = Math.round(charCount / 4);
|
|
361
364
|
let budgetNote;
|
|
@@ -397,7 +400,7 @@ when the full AIWG project context is needed.
|
|
|
397
400
|
if (dryRun) {
|
|
398
401
|
console.log(`[dry-run] Would write .hermes.md (${body.length} chars)`);
|
|
399
402
|
} else {
|
|
400
|
-
|
|
403
|
+
writeContextFileSync(destPath, mergeHermesContext(destPath, body, 'routing'), 'utf8');
|
|
401
404
|
console.log(` Created .hermes.md (${body.length} chars)`);
|
|
402
405
|
}
|
|
403
406
|
return 1;
|
|
@@ -5,6 +5,8 @@ import { createHash } from 'node:crypto';
|
|
|
5
5
|
import YAML from 'yaml';
|
|
6
6
|
import { importImpl } from '../../_resolve-impl.mjs';
|
|
7
7
|
import { withRuleDeployment, collectFrameworkArtifacts, getAddonFiles, isKernelSkill, listMdFiles, listSkillDirs, normalizeDeploymentMode, resolveAiwgRoot } from './base.mjs';
|
|
8
|
+
|
|
9
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
8
10
|
export const name = 'omp';
|
|
9
11
|
export const aliases = ['oh-my-pi'];
|
|
10
12
|
export const paths = { agents: '.omp/agents', commands: '.omp/prompts', skills: '.agents/skills', rules: '.omp/rules' };
|
|
@@ -168,7 +170,7 @@ export function createAgentsMd(target, srcRoot, dryRun = false) {
|
|
|
168
170
|
const content = current.includes(start) && current.includes(end) ? current.slice(0, current.indexOf(start)) + block + current.slice(current.indexOf(end) + end.length) : `${current}${current ? '\n\n' : ''}${block}\n`;
|
|
169
171
|
if (!dryRun) {
|
|
170
172
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
171
|
-
if (content !== current)
|
|
173
|
+
if (content !== current) writeContextFileSync(dest, content);
|
|
172
174
|
const receiptPath = path.join(path.dirname(dest), '.aiwg-manifest.json');
|
|
173
175
|
// Block ownership must never claim the operator's complete context file.
|
|
174
176
|
receipt.managed['AGENTS.md'] = { blockHash: `sha256:${hash(block)}`, source: 'omp-bootstrap', provider: 'omp', transformation: 'managed-block' };
|
|
@@ -19,6 +19,7 @@ const { loadRulePinConfigs, installedRuleCapabilities, resolveRulePins } = await
|
|
|
19
19
|
const { guardRulesInlineBudget } = await importImpl(import.meta.url, 'rules/inline-budget.mjs');
|
|
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
|
+
import { writeContextFileSync, CONTEXT_STAGE_ENV } from './context-write.mjs';
|
|
22
23
|
const PREPARATION_HOOK = Symbol.for('aiwg.provider-preparation-hook.v1');
|
|
23
24
|
|
|
24
25
|
export function installRuleDeploymentPreparationHook(hook) {
|
|
@@ -190,14 +191,17 @@ export async function prepareRuleDelivery(opts) {
|
|
|
190
191
|
const key = `${anchor}\0${entry.path}`;
|
|
191
192
|
if (!entryPaths.has(key)) entryPaths.set(key, path.resolve(anchor, entry.path));
|
|
192
193
|
const resolvedPath = entryPaths.get(key);
|
|
193
|
-
|
|
194
|
+
// Canonical IDs hash the indexed package-relative path. Keep it through
|
|
195
|
+
// resolution; the selected target is anchored to root below.
|
|
196
|
+
return { ...entry, path: entry.path.startsWith('agentic/code/') ? entry.path
|
|
197
|
+
: opts.mapInputPath ? opts.mapInputPath(resolvedPath) : resolvedPath };
|
|
194
198
|
});
|
|
195
199
|
let rebuilt = false;
|
|
196
200
|
const cacheDir = process.env.AIWG_RULE_BINDING_INDEX_CACHE;
|
|
197
201
|
const cacheFile = cacheDir && path.join(cacheDir, `${contentHash(opts.logicalInputPath ? opts.logicalInputPath(root) : root)}.json`);
|
|
198
202
|
const buildSourceEntries = async (refresh = false) => {
|
|
199
203
|
if (!refresh && cacheFile) {
|
|
200
|
-
try { return JSON.parse(fs.readFileSync(cacheFile, 'utf8')).map(entry => ({ ...entry, path: opts.mapInputPath ? opts.mapInputPath(entry.path) : entry.path })); }
|
|
204
|
+
try { return JSON.parse(fs.readFileSync(cacheFile, 'utf8')).map(entry => ({ ...entry, path: path.isAbsolute(entry.path) && opts.mapInputPath ? opts.mapInputPath(entry.path) : entry.path })); }
|
|
201
205
|
catch (error) {
|
|
202
206
|
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
|
|
203
207
|
}
|
|
@@ -206,7 +210,8 @@ export async function prepareRuleDelivery(opts) {
|
|
|
206
210
|
const sourceEntries = await collectRuleBindingEntries(root,
|
|
207
211
|
root === opts.srcRoot && !fs.existsSync(path.join(root, 'agentic')) ? '.' : 'agentic/code');
|
|
208
212
|
rebuilt = true;
|
|
209
|
-
const entries = sourceEntries.map(entry => ({ ...entry, path: path.
|
|
213
|
+
const entries = sourceEntries.map(entry => ({ ...entry, path: entry.path.startsWith('agentic/code/')
|
|
214
|
+
? entry.path : path.resolve(root, entry.path) }));
|
|
210
215
|
if (cacheFile) {
|
|
211
216
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
212
217
|
// Workers may publish the same immutable source snapshot concurrently.
|
|
@@ -324,7 +329,7 @@ function block(content, body) {
|
|
|
324
329
|
|
|
325
330
|
function putStartup(file, body) {
|
|
326
331
|
const content = fs.existsSync(file) ? read(file) : '<!-- aiwg-managed -->\n';
|
|
327
|
-
|
|
332
|
+
writeContextFileSync(file, block(content, body));
|
|
328
333
|
}
|
|
329
334
|
|
|
330
335
|
/** Tagged rules bypass legacy aggregate writers. Untagged projections retain their tier policy. */
|
|
@@ -337,7 +342,7 @@ function projectRules(opts, provider) {
|
|
|
337
342
|
const content = read(file);
|
|
338
343
|
const clean = content.replace(/(<!-- AIWG:hermes-context:start -->)([\s\S]*?)(<!-- AIWG:hermes-context:end -->)/g,
|
|
339
344
|
(_, start, body, end) => start + body.replace(/^### Rule: [\s\S]*?(?=^#{1,3} |(?![\s\S]))/gm, '') + end);
|
|
340
|
-
if (clean !== content)
|
|
345
|
+
if (clean !== content) writeContextFileSync(file, clean);
|
|
341
346
|
}
|
|
342
347
|
const nativePaths = ['claude', 'cursor'].includes(provider.name);
|
|
343
348
|
const rulesDir = path.isAbsolute(provider.paths.rules || '') ? provider.paths.rules : path.join(opts.target, provider.paths.rules || `.${provider.name}/rules`);
|
|
@@ -487,14 +492,14 @@ function projectRules(opts, provider) {
|
|
|
487
492
|
if (!fs.existsSync(file)) continue;
|
|
488
493
|
const content = read(file);
|
|
489
494
|
const clean = block(content, '');
|
|
490
|
-
if (clean !== content)
|
|
495
|
+
if (clean !== content) writeContextFileSync(file, clean);
|
|
491
496
|
}
|
|
492
497
|
return;
|
|
493
498
|
}
|
|
494
499
|
if (inlineStartup[provider.name]) {
|
|
495
500
|
// Retire the B1 indirect startup block when migrating to the actual loader.
|
|
496
501
|
const former = path.join(opts.target, 'AIWG.md');
|
|
497
|
-
if (fs.existsSync(former))
|
|
502
|
+
if (fs.existsSync(former)) writeContextFileSync(former, block(read(former), ''));
|
|
498
503
|
putStartup(path.join(opts.target, inlineStartup[provider.name]), body);
|
|
499
504
|
} else if (provider.name === 'hermes') {
|
|
500
505
|
const dest = path.join(opts.target, 'AGENTS.md');
|
|
@@ -916,12 +921,12 @@ function expectedAnchorPaths(paths) {
|
|
|
916
921
|
/** Adopt a detached worker plan into the coordinator's process-local lease. */
|
|
917
922
|
export function adoptRuleDeploymentPlan(detached, { scratch, target, inputPool, cleanup }) {
|
|
918
923
|
const invalid = () => { throw new Error('Invalid transferred rule deployment plan'); };
|
|
919
|
-
if (!exactObjectKeys(detached, ['version', 'lease', 'temp', 'target', 'stage', 'rootNames', 'workspaceDirs', 'preimages', 'poolSeal', 'anchors', 'snapshotRoot', 'corpusRoot', 'sourceMappings', 'staged', 'report', 'mappings']) ||
|
|
924
|
+
if (!exactObjectKeys(detached, ['version', 'lease', 'temp', 'target', 'stage', 'rootNames', 'workspaceDirs', 'preimages', 'poolSeal', 'anchors', 'snapshotRoot', 'corpusRoot', 'sourceMappings', 'staged', 'report', 'mappings', 'contextWrites']) ||
|
|
920
925
|
detached.version !== 1 || typeof detached.lease !== 'string' || !(detached.poolSeal === null || typeof detached.poolSeal === 'string') ||
|
|
921
926
|
!detached.report || typeof detached.report !== 'object' || Array.isArray(detached.report) || typeof scratch !== 'string' || typeof target !== 'string' || typeof cleanup !== 'function') invalid();
|
|
922
927
|
const root = path.resolve(scratch), expectedTarget = path.resolve(target);
|
|
923
928
|
if (path.resolve(detached.temp || '') !== root || path.resolve(detached.stage || '') !== path.join(root, 'project') || path.resolve(detached.target || '') !== expectedTarget) invalid();
|
|
924
|
-
if (!Array.isArray(detached.staged) || !Array.isArray(detached.mappings) || !Array.isArray(detached.rootNames) || !Array.isArray(detached.preimages) ||
|
|
929
|
+
if (!Array.isArray(detached.contextWrites) || detached.contextWrites.some(file => typeof file !== 'string' || !within(root, file)) || !Array.isArray(detached.staged) || !Array.isArray(detached.mappings) || !Array.isArray(detached.rootNames) || !Array.isArray(detached.preimages) ||
|
|
925
930
|
!Array.isArray(detached.workspaceDirs) || !Array.isArray(detached.anchors) || !Array.isArray(detached.sourceMappings) ||
|
|
926
931
|
typeof detached.snapshotRoot !== 'string' || !path.isAbsolute(detached.snapshotRoot) || typeof detached.corpusRoot !== 'string' || !path.isAbsolute(detached.corpusRoot)) invalid();
|
|
927
932
|
if (detached.rootNames.some(name => !safeRelative(expectedTarget, name))) invalid();
|
|
@@ -1011,6 +1016,10 @@ export async function prepareRuleDeployment(original, provider, deploy) {
|
|
|
1011
1016
|
const removeTemp = () => { removeStage(); if (pool) releaseInputPool(pool); };
|
|
1012
1017
|
const lease = randomUUID();
|
|
1013
1018
|
stagedCleanups.set(lease, { temp, cleanup: removeTemp, detach: () => { if (pool) releaseInputPool(pool); }, pool, seal: null, transferable });
|
|
1019
|
+
const priorContextStage = process.env[CONTEXT_STAGE_ENV];
|
|
1020
|
+
const contextStageLog = path.join(temp, 'context-writes.jsonl');
|
|
1021
|
+
fs.writeFileSync(contextStageLog, '');
|
|
1022
|
+
process.env[CONTEXT_STAGE_ENV] = contextStageLog;
|
|
1014
1023
|
const mappings = [];
|
|
1015
1024
|
const preimages = [...initialPreimages];
|
|
1016
1025
|
let retained = false;
|
|
@@ -1338,7 +1347,8 @@ export async function prepareRuleDeployment(original, provider, deploy) {
|
|
|
1338
1347
|
}
|
|
1339
1348
|
};
|
|
1340
1349
|
collectDirs(path.join(stage, '.aiwg'), '.aiwg');
|
|
1341
|
-
const
|
|
1350
|
+
const contextWrites = [...new Set(fs.readFileSync(contextStageLog, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line)))];
|
|
1351
|
+
const plan = { version: 1, lease, temp, target, stage, contextWrites, rootNames, workspaceDirs, preimages, poolSeal: pool?.inventorySeal || null,
|
|
1342
1352
|
anchors: captureAnchors([target, ...mappings.map(mapping => mapping.from), ...workspaceDirs.map(dir => path.join(target, dir))]),
|
|
1343
1353
|
snapshotRoot, corpusRoot, sourceMappings, staged: snapshotPreimages([stage, ...mappings.map(mapping => mapping.to)]), report: guarded.report,
|
|
1344
1354
|
mappings: mappings.map(({ from, to, before }) => ({ from, to, before: [...before] })) };
|
|
@@ -1346,6 +1356,7 @@ export async function prepareRuleDeployment(original, provider, deploy) {
|
|
|
1346
1356
|
retained = true;
|
|
1347
1357
|
return plan;
|
|
1348
1358
|
} finally {
|
|
1359
|
+
if (priorContextStage === undefined) delete process.env[CONTEXT_STAGE_ENV]; else process.env[CONTEXT_STAGE_ENV] = priorContextStage;
|
|
1349
1360
|
Object.assign(provider.paths, originals);
|
|
1350
1361
|
if (priorAiwgRoot === undefined) delete process.env.AIWG_ROOT; else process.env.AIWG_ROOT = priorAiwgRoot;
|
|
1351
1362
|
if (priorRegistry === undefined) delete process.env.AIWG_USER_REGISTRY_PATH; else process.env.AIWG_USER_REGISTRY_PATH = priorRegistry;
|
|
@@ -1418,10 +1429,16 @@ export function commitRuleDeploymentPlan(plan) {
|
|
|
1418
1429
|
output = Buffer.from(`${JSON.stringify(receipt, null, 2)}\n`);
|
|
1419
1430
|
}
|
|
1420
1431
|
fs.mkdirSync(path.dirname(path.join(from, rel)), { recursive: true });
|
|
1421
|
-
|
|
1432
|
+
if (plan.contextWrites.includes(file)) writeContextFileSync(path.join(from, rel), output);
|
|
1433
|
+
else fs.writeFileSync(path.join(from, rel), output);
|
|
1422
1434
|
}
|
|
1423
1435
|
}
|
|
1424
|
-
for (const name of rootNames) if (fs.existsSync(path.join(stage, name)))
|
|
1436
|
+
for (const name of rootNames) if (fs.existsSync(path.join(stage, name))) {
|
|
1437
|
+
const output = normalize(read(path.join(stage, name)));
|
|
1438
|
+
if (output === read(path.join(target, name))) continue;
|
|
1439
|
+
const write = plan.contextWrites.includes(path.join(stage, name)) ? writeContextFileSync : put;
|
|
1440
|
+
write(path.join(target, name), output);
|
|
1441
|
+
}
|
|
1425
1442
|
return plan.report;
|
|
1426
1443
|
} finally {
|
|
1427
1444
|
disposeRuleDeploymentPlan(plan);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Warp Terminal Provider
|
|
3
4
|
*
|
|
@@ -48,6 +49,8 @@ import {
|
|
|
48
49
|
collectFrameworkArtifacts
|
|
49
50
|
} from './base.mjs';
|
|
50
51
|
|
|
52
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
53
|
+
|
|
51
54
|
// ============================================================================
|
|
52
55
|
// Provider Configuration
|
|
53
56
|
// ============================================================================
|
|
@@ -206,7 +209,7 @@ export async function deployWarp(targetDir, srcRoot, opts) {
|
|
|
206
209
|
// Preserve operator sections even for a forced artifact refresh.
|
|
207
210
|
const output = mergeWarpMd(existing, content, false);
|
|
208
211
|
ensureDir(targetDir, opts.dryRun);
|
|
209
|
-
if (!opts.dryRun)
|
|
212
|
+
if (!opts.dryRun) writeContextFileSync(dest, output);
|
|
210
213
|
|
|
211
214
|
}
|
|
212
215
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Devin Desktop Provider (Windsurf compatibility adapter)
|
|
3
4
|
*
|
|
@@ -55,6 +56,8 @@ import {
|
|
|
55
56
|
deploySoulCompanions
|
|
56
57
|
} from './base.mjs';
|
|
57
58
|
|
|
59
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
60
|
+
|
|
58
61
|
// ============================================================================
|
|
59
62
|
// Provider Configuration
|
|
60
63
|
// ============================================================================
|
|
@@ -285,7 +288,7 @@ export function generateAgentsMd(files, destPath, opts) {
|
|
|
285
288
|
if (dryRun) {
|
|
286
289
|
console.log(`[dry-run] Would write AGENTS.md with ${agents.length} agents`);
|
|
287
290
|
} else {
|
|
288
|
-
|
|
291
|
+
writeContextFileSync(destPath, output, 'utf8');
|
|
289
292
|
console.log(`Created AGENTS.md with ${agents.length} agents at ${path.relative(process.cwd(), destPath)}`);
|
|
290
293
|
}
|
|
291
294
|
|
|
@@ -409,7 +412,7 @@ export function generateWindsurfRules(srcRoot, target, opts) {
|
|
|
409
412
|
console.log(`[dry-run] Would write .windsurf/rules/aiwg-orchestration.md (trigger: always_on)`);
|
|
410
413
|
} else {
|
|
411
414
|
ensureDir(rulesDir);
|
|
412
|
-
|
|
415
|
+
writeContextFileSync(orchestrationPath, output, 'utf8');
|
|
413
416
|
console.log(`Created .windsurf/rules/aiwg-orchestration.md (trigger: always_on)`);
|
|
414
417
|
}
|
|
415
418
|
|
|
@@ -430,7 +433,7 @@ export function generateWindsurfRules(srcRoot, target, opts) {
|
|
|
430
433
|
if (dryRun) {
|
|
431
434
|
console.log(`[dry-run] Would write .windsurfrules (deprecated stub)`);
|
|
432
435
|
} else {
|
|
433
|
-
|
|
436
|
+
writeContextFileSync(stubPath, stubLines.join('\n'), 'utf8');
|
|
434
437
|
console.log(`Created .windsurfrules (deprecated stub → .windsurf/rules/aiwg-orchestration.md)`);
|
|
435
438
|
}
|
|
436
439
|
}
|
|
@@ -308,6 +308,9 @@ const HOST_CODES = new Set(['RESOURCE', 'NOT_FOUND', 'SANDBOX']);
|
|
|
308
308
|
const PROVIDER_CODES = new Set(['NETWORK']);
|
|
309
309
|
const INSTALLATION_CODES = new Set(['AUTH']);
|
|
310
310
|
const MODEL_CODES = new Set(['MODEL']);
|
|
311
|
+
const STATUS_REASON_CODES = new Set(['NOT_FOUND', 'SPAWN_REFUSED', 'RATE_LIMIT', 'RESOURCE', 'SANDBOX',
|
|
312
|
+
'NETWORK', 'AUTH', 'MODEL', 'UNKNOWN']);
|
|
313
|
+
export const RECOVERY_STATUS_LIMIT = 32;
|
|
311
314
|
|
|
312
315
|
export const scopeKeyFor = Object.freeze({
|
|
313
316
|
invocation: (id) => `invocation:${id ?? 'unknown'}`,
|
|
@@ -511,6 +514,7 @@ export class ScopedFailureBreaker extends EventEmitter {
|
|
|
511
514
|
this._scopes = new Map();
|
|
512
515
|
this._outstanding = new Map();
|
|
513
516
|
this._hostFailureFacts = [];
|
|
517
|
+
this._transitions = [];
|
|
514
518
|
this._canarySeq = 0;
|
|
515
519
|
this._observedInvocations = new Set();
|
|
516
520
|
this.canariesObserved = 0;
|
|
@@ -1022,6 +1026,10 @@ export class ScopedFailureBreaker extends EventEmitter {
|
|
|
1022
1026
|
const from = rec.status;
|
|
1023
1027
|
if (from === to) return;
|
|
1024
1028
|
rec.status = to;
|
|
1029
|
+
if (rec.scopeKey.length <= 256) this._transitions.push({ scopeKey: rec.scopeKey, scope: rec.scope, from, to,
|
|
1030
|
+
reasons: [...rec.evidence.keys()].filter(code => STATUS_REASON_CODES.has(code)).slice(0, 8),
|
|
1031
|
+
atMs: this.nowMs(), nextEligibleAtMs: to === 'closed' ? null : rec.nextProbeAtMs });
|
|
1032
|
+
if (this._transitions.length > RECOVERY_STATUS_LIMIT) this._transitions.shift();
|
|
1025
1033
|
this.emit('transition', { scopeKey: rec.scopeKey, from, to, action,
|
|
1026
1034
|
distinctFailures: new Set(rec.failures.map(failure => failure.invocationId)).size, reasons: [...rec.evidence.keys()],
|
|
1027
1035
|
nextEligibleAtMs: rec.nextProbeAtMs, resetAtMs: rec.resetAtMs,
|
|
@@ -1454,6 +1462,23 @@ export class ScopedFailureBreaker extends EventEmitter {
|
|
|
1454
1462
|
};
|
|
1455
1463
|
}
|
|
1456
1464
|
|
|
1465
|
+
/** Pure status projection: never sweep reservations, probe, or advance state. */
|
|
1466
|
+
recoveryStatus() {
|
|
1467
|
+
const recentKeys = [...new Set(this._transitions.map(event => event.scopeKey).reverse())];
|
|
1468
|
+
const active = [...this._scopes.values()].filter(rec => rec.status !== 'closed');
|
|
1469
|
+
const keys = [...new Set([...active.map(rec => rec.scopeKey), ...recentKeys])];
|
|
1470
|
+
const scopes = keys.filter(key => key.length <= 256).slice(0, RECOVERY_STATUS_LIMIT).map(scopeKey => {
|
|
1471
|
+
const rec = this._scopes.get(scopeKey);
|
|
1472
|
+
const last = this._transitions.findLast(event => event.scopeKey === scopeKey);
|
|
1473
|
+
return { scopeKey, scope: rec?.scope ?? last.scope, state: rec?.status ?? last.to,
|
|
1474
|
+
reasons: rec ? [...rec.evidence.keys()].filter(code => STATUS_REASON_CODES.has(code)).slice(0, 8) : [...last.reasons],
|
|
1475
|
+
nextEligibleAtMs: !rec || rec.status === 'closed' ? null : rec.nextProbeAtMs,
|
|
1476
|
+
canaryInFlight: rec?.activeCanary != null };
|
|
1477
|
+
});
|
|
1478
|
+
return { observedAtMs: this.nowMs(), scopes, omittedScopeCount: Math.max(0, keys.length - scopes.length),
|
|
1479
|
+
transitions: this._transitions.map(event => ({ ...event, reasons: [...event.reasons] })) };
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1457
1482
|
/** Correlate launcher-observed host faults across resolved inference backends.
|
|
1458
1483
|
* Replay the first fault once corroborated, preserving the three-distinct
|
|
1459
1484
|
* threshold even when the first backend alone could not establish scope.
|
|
@@ -1550,6 +1575,7 @@ export class ScopedFailureBreaker extends EventEmitter {
|
|
|
1550
1575
|
|
|
1551
1576
|
destroy() {
|
|
1552
1577
|
this._hostFailureFacts = [];
|
|
1578
|
+
this._transitions = [];
|
|
1553
1579
|
for (const rec of this._scopes.values()) rec.breaker.destroy();
|
|
1554
1580
|
this._scopes.clear();
|
|
1555
1581
|
this._outstanding.clear();
|
|
@@ -3,6 +3,7 @@ import { runRecoveryProbe } from './recovery-probe.mjs';
|
|
|
3
3
|
/** Recovery evidence observed outside worker output by the launch owner. */
|
|
4
4
|
const observations = new WeakMap();
|
|
5
5
|
const failureHints = new WeakMap();
|
|
6
|
+
const assessed = new WeakSet();
|
|
6
7
|
|
|
7
8
|
export function recordLauncherRecoveryHint(result, reasonCode) {
|
|
8
9
|
failureHints.set(result, reasonCode);
|
|
@@ -23,9 +24,10 @@ export function launcherRecoveryEvidence(result) {
|
|
|
23
24
|
*/
|
|
24
25
|
export async function observeLauncherRecoveryOutcome({ invocation, result, installation, breaker,
|
|
25
26
|
failureHint = '', beforeRequest, onDispatch, fetchImpl = globalThis.fetch, wallNowMs = Date.now, timeoutMs = 10_000 } = {}) {
|
|
26
|
-
if (!result || result.attemptOutcome !== 'failed' || result.cancelled) return;
|
|
27
|
+
if (!result || result.attemptOutcome !== 'failed' || result.cancelled || assessed.has(result)) return;
|
|
27
28
|
const backend = invocation?.selected?.inferenceProvider;
|
|
28
29
|
if (HOST_ERRORS.has(result.errorCode) || HOST_RESULTS.has(result.nativeResult)) {
|
|
30
|
+
assessed.add(result);
|
|
29
31
|
const corroboratedBackends = breaker?.observeHostFailure({ invocationId: result.invocationId,
|
|
30
32
|
inferenceProvider: backend, reasonCode: 'RESOURCE' }) ?? [];
|
|
31
33
|
if (corroboratedBackends.length >= 2) observations.set(result, {
|
|
@@ -43,6 +45,10 @@ export async function observeLauncherRecoveryOutcome({ invocation, result, insta
|
|
|
43
45
|
if (!await beforeRequest()) return;
|
|
44
46
|
const requestTimeoutMs = remaining();
|
|
45
47
|
if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) return;
|
|
48
|
+
// One diagnostic request per attempt, including unsuccessful diagnostics.
|
|
49
|
+
// Re-reading a result cannot consume allowance or create fresh evidence.
|
|
50
|
+
if (assessed.has(result)) return;
|
|
51
|
+
assessed.add(result);
|
|
46
52
|
const observed = await runRecoveryProbe({ driver: { qualification: { installation } }, invocation: invocation ?? { transport: 'shell' },
|
|
47
53
|
timeoutMs: Math.min(10_000, requestTimeoutMs), maxOutputBytes: 65_536, onDispatch, fetchImpl, wallNowMs });
|
|
48
54
|
if (observed.reason === 'probe-rate-limited') {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { RecoveryPolicy } from './consecutive-failure-circuit-breaker.mjs';
|
|
2
|
+
|
|
1
3
|
/** A recovery probe is a conductor-owned request, never an uncontained worker. */
|
|
2
4
|
export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000, maxOutputBytes = 65_536,
|
|
3
|
-
onDispatch, fetchImpl = globalThis.fetch, wallNowMs = Date.now }) {
|
|
5
|
+
onDispatch, fetchImpl = globalThis.fetch, wallNowMs = Date.now, nowMs = () => performance.now() }) {
|
|
4
6
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 10_000
|
|
5
7
|
|| !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 65_536) {
|
|
6
8
|
throw new RangeError('Recovery probe exceeds the 10s/64KiB bounds');
|
|
@@ -19,6 +21,7 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
|
|
|
19
21
|
return failed('probe-endpoint-unavailable', { operatorIntervention: true });
|
|
20
22
|
}
|
|
21
23
|
const controller = new AbortController();
|
|
24
|
+
const deadlineMs = nowMs() + timeoutMs;
|
|
22
25
|
const timer = setTimeout(() => controller.abort(), Math.max(1, Math.floor(timeoutMs)));
|
|
23
26
|
let aborted;
|
|
24
27
|
const timeout = new Promise((_, reject) => {
|
|
@@ -30,14 +33,19 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
|
|
|
30
33
|
timeout.catch(() => {});
|
|
31
34
|
try {
|
|
32
35
|
await onDispatch?.();
|
|
36
|
+
if (nowMs() >= deadlineMs) controller.abort();
|
|
33
37
|
if (controller.signal.aborted) return failed('probe-timeout');
|
|
34
38
|
try {
|
|
35
39
|
// HEAD has no task, credentials, inference body or worker filesystem.
|
|
36
40
|
// A successful endpoint check permits only one subsequent task canary.
|
|
37
41
|
const response = await Promise.race([fetchImpl(endpoint, { method: 'HEAD', redirect: 'error', signal: controller.signal }), timeout]);
|
|
38
42
|
let headerBytes = 16 + Buffer.byteLength(response.statusText ?? '');
|
|
39
|
-
for (const [name, value] of response.headers) headerBytes += Buffer.byteLength(name) + Buffer.byteLength(value) + 4;
|
|
40
43
|
try {
|
|
44
|
+
if (nowMs() >= deadlineMs) { controller.abort(); return failed('probe-timeout'); }
|
|
45
|
+
for (const [name, value] of response.headers) {
|
|
46
|
+
headerBytes += Buffer.byteLength(name) + Buffer.byteLength(value) + 4;
|
|
47
|
+
if (headerBytes > maxOutputBytes) return failed('probe-output-limit');
|
|
48
|
+
}
|
|
41
49
|
if (headerBytes > maxOutputBytes) return failed('probe-output-limit');
|
|
42
50
|
if (response.body) return failed('probe-body-unexpected');
|
|
43
51
|
const success = response.status >= 200 && response.status <= 299;
|
|
@@ -45,7 +53,8 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
|
|
|
45
53
|
if (response.status === 429) {
|
|
46
54
|
const after = response.headers.get('retry-after');
|
|
47
55
|
const seconds = after != null && /^\d+(?:\.\d+)?$/.test(after.trim()) ? Number(after) : null;
|
|
48
|
-
const date = seconds === null &&
|
|
56
|
+
const date = seconds === null && /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(after ?? '')
|
|
57
|
+
? Date.parse(after) : NaN;
|
|
49
58
|
const reset = response.headers.get('x-ratelimit-reset');
|
|
50
59
|
const resetEpoch = reset != null && /^\d+(?:\.\d+)?$/.test(reset.trim()) ? Number(reset) * 1000 : NaN;
|
|
51
60
|
const resetDelay = response.headers.get('ratelimit-reset');
|
|
@@ -53,7 +62,8 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
|
|
|
53
62
|
Number.isFinite(resetEpoch) ? (resetEpoch - wallNowMs()) / 1000 : null,
|
|
54
63
|
resetDelay != null && /^\d+(?:\.\d+)?$/.test(resetDelay.trim()) ? Number(resetDelay) : null]
|
|
55
64
|
.filter(value => Number.isFinite(value) && value >= 0);
|
|
56
|
-
return failed('probe-rate-limited', { retryAfterSeconds: delays.length
|
|
65
|
+
return failed('probe-rate-limited', { retryAfterSeconds: delays.length
|
|
66
|
+
? Math.min(RecoveryPolicy.maxRetryAfterSeconds, Math.max(...delays)) : null });
|
|
57
67
|
}
|
|
58
68
|
return failed('probe-failed', { status: response.status });
|
|
59
69
|
} finally {
|
|
@@ -15,7 +15,7 @@ import { hostname } from 'node:os';
|
|
|
15
15
|
import { join, dirname } from 'path';
|
|
16
16
|
import { constants } from 'node:fs';
|
|
17
17
|
import { open, opendir, rename, unlink } from 'node:fs/promises';
|
|
18
|
-
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { createWorkerHealthObserver } from './lib/worker-health.mjs';
|
|
21
21
|
|
|
@@ -38,6 +38,9 @@ import { createWorkerHealthObserver } from './lib/worker-health.mjs';
|
|
|
38
38
|
// error, a thrown assertion, ...) and silently report every live-process
|
|
39
39
|
// health query as unavailable instead of surfacing the actual failure.
|
|
40
40
|
let boundedExecFileSync = null;
|
|
41
|
+
let projectRecoveryStatus = null;
|
|
42
|
+
let mergeRecoveryStatuses = null;
|
|
43
|
+
let projectStartupCorrection = null;
|
|
41
44
|
let processHealthUnavailableReason = null;
|
|
42
45
|
let importImpl;
|
|
43
46
|
try {
|
|
@@ -58,6 +61,11 @@ if (importImpl) {
|
|
|
58
61
|
if (!['AIWG_IMPLEMENTATION_MISSING', 'AIWG_PACKAGE_ROOT_MISSING'].includes(error?.code)) throw error;
|
|
59
62
|
processHealthUnavailableReason = `bounded-child-process helper unavailable: implementation missing: ${error.message}`;
|
|
60
63
|
}
|
|
64
|
+
try {
|
|
65
|
+
({ projectRecoveryStatus, mergeRecoveryStatuses, projectStartupCorrection } = await importImpl(import.meta.url, 'mission-protocol/worker-status.js'));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (!['AIWG_IMPLEMENTATION_MISSING', 'AIWG_PACKAGE_ROOT_MISSING'].includes(error?.code)) throw error;
|
|
68
|
+
}
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
/** Side-effect-free Linux sample. Unknown identity never grants signal authority. */
|
|
@@ -848,8 +856,10 @@ export async function sampleWorkerStatus(options, memory = new Map()) {
|
|
|
848
856
|
const monotonicNow = options.monotonicNow ?? monotonicMs;
|
|
849
857
|
const began = monotonicNow();
|
|
850
858
|
const report = { version: 1, sampledAt, scope: 'direct-conductor', observerCoverage: 'unobserved',
|
|
851
|
-
workers: [], breakers: [],
|
|
859
|
+
workers: [], breakers: [], recovery: { source: 'unavailable', observedAt: null, observedAtMs: null,
|
|
860
|
+
scopes: [], transitions: [], omittedScopeCount: 0, truncated: false }, nextCheckAt: null, complete: true, findings: [], exitCode: 0 };
|
|
852
861
|
let root;
|
|
862
|
+
const recoveryOwners = new Map();
|
|
853
863
|
try {
|
|
854
864
|
root = await openStatusDirectory(options.runDir ?? options.runsRoot);
|
|
855
865
|
const listing = options.runDir ? { entries: [{ name: null }], complete: true }
|
|
@@ -870,6 +880,11 @@ export async function sampleWorkerStatus(options, memory = new Map()) {
|
|
|
870
880
|
process: await statusRecord(directory, 'process.json', true), health: await statusRecord(directory, 'health.json'),
|
|
871
881
|
exit: await statusRecord(directory, 'exit.json'), scratch: await statusRecord(directory, 'last-observed.json') };
|
|
872
882
|
const ownerRecord = await statusRecord(directory, '.status-observer');
|
|
883
|
+
const supervisor = records.process.supervisor;
|
|
884
|
+
const ownerHost = safeLabel(supervisor?.host), ownerBoot = safeLabel(supervisor?.bootId), ownerStart = safeLabel(supervisor?.startTicks);
|
|
885
|
+
const launcherOwnerRecorded = records.process.origin === 'launcher'
|
|
886
|
+
&& supervisor?.uid === process.getuid() && Number.isSafeInteger(supervisor.pid) && supervisor.pid > 0
|
|
887
|
+
&& ownerHost && ownerBoot && ownerStart;
|
|
873
888
|
const expected = records.process.worker;
|
|
874
889
|
// classifyWorkerStatus() also ignores an unverified worker for this
|
|
875
890
|
// comparison; skip the resample too so this never samples an
|
|
@@ -884,6 +899,22 @@ export async function sampleWorkerStatus(options, memory = new Map()) {
|
|
|
884
899
|
? (options.sampleIdentity ?? sampleWorkerIdentity)(ownerIdentity.pid) : null;
|
|
885
900
|
const row = classifyWorkerStatus(records, { now, monotonicNow: sampleTime, bootId, actual, counters,
|
|
886
901
|
previous: memory.get(key), ownerRecord, actualObserver });
|
|
902
|
+
if (projectRecoveryStatus && launcherOwnerRecorded && records.process.recovery) {
|
|
903
|
+
const recovery = projectRecoveryStatus(records.process.recovery, 'recorded');
|
|
904
|
+
const ownerKey = JSON.stringify([ownerHost, ownerBoot, supervisor.pid, ownerStart]);
|
|
905
|
+
const previous = recoveryOwners.get(ownerKey);
|
|
906
|
+
if (recovery.observedAt && (!previous || Date.parse(recovery.observedAt) > Date.parse(previous.observedAt)
|
|
907
|
+
|| recovery.observedAt === previous.observedAt && recovery.transitions.length >= previous.transitions.length)) {
|
|
908
|
+
const digest = createHash('sha256').update(ownerKey).digest('hex').slice(0, 12);
|
|
909
|
+
const owner = `${supervisor.pid}@${ownerHost.slice(0, 80)}:${ownerStart.slice(0, 32)}:${digest}`;
|
|
910
|
+
for (const scope of recovery.scopes) scope.owner = owner;
|
|
911
|
+
for (const transition of recovery.transitions) transition.owner = owner;
|
|
912
|
+
recoveryOwners.set(ownerKey, recovery);
|
|
913
|
+
report.recovery = mergeRecoveryStatuses([...recoveryOwners.values()]);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const correction = launcherOwnerRecorded && projectStartupCorrection?.(records.process.startupCorrection);
|
|
917
|
+
if (correction) row.startupCorrection = correction;
|
|
887
918
|
report.findings.push(...row.findings.map(finding => `${row.invocationId}: ${finding}`));
|
|
888
919
|
if (row.observerOwnership === 'unavailable') report.findings.push('OBSERVER_OWNER_UNAVAILABLE: reconcile reservation before observing');
|
|
889
920
|
memory.set(key, { cpuTicks: actual?.cpuTicks ?? null, counters, health: row.health });
|
|
@@ -975,12 +1006,19 @@ export function createStatusObservationOwner() {
|
|
|
975
1006
|
export function formatWorkerStatus(report) {
|
|
976
1007
|
const label = value => value == null ? '?' : String(value);
|
|
977
1008
|
const seconds = value => value == null ? '?' : `${Math.round(value * 1000) / 1000}s`;
|
|
1009
|
+
const recovery = report.recovery;
|
|
1010
|
+
const recoveryLines = !recovery || recovery.source === 'unavailable'
|
|
1011
|
+
? ['Recovery scope status unavailable: no recorded recovery owner observation']
|
|
1012
|
+
: [`Recovery scopes (${recovery.source}) observed=${label(recovery.observedAt)} omitted=${recovery.omittedScopeCount} truncated=${recovery.truncated}`,
|
|
1013
|
+
...recovery.scopes.map(scope => ` ${scope.scopeKey} state=${scope.state} reasons=${scope.reasons.join(',') || 'UNKNOWN'} next eligible=${label(scope.nextEligibleAt ?? scope.nextEligibleAtMs)} canary in flight=${scope.canaryInFlight}${scope.owner ? ` owner=${scope.owner}` : ''}`),
|
|
1014
|
+
...recovery.transitions.map(transition => ` ${transition.scopeKey}: ${transition.from} -> ${transition.to} at=${label(transition.at ?? transition.atMs)} reasons=${transition.reasons.join(',') || 'UNKNOWN'} next eligible=${label(transition.nextEligibleAt ?? transition.nextEligibleAtMs)}${transition.owner ? ` owner=${transition.owner}` : ''}`)];
|
|
978
1015
|
return [`Workers (${report.scope}) coverage=${report.observerCoverage} complete=${report.complete} breakers=${report.breakers.length} (no shared Tier 0 breaker authority)`,
|
|
979
1016
|
'invocation | member | state | coverage | age | last progress | remaining | cause | next action',
|
|
980
1017
|
...report.workers.map(row => row.error ? `? | ? | unreadable | ? | ? | ? | ? | ${row.error.code} | ${row.nextAction}`
|
|
981
1018
|
: `${row.invocationId} | ${label(row.member)} | ${row.health.state} | ${row.health.coverage} | ${seconds(row.elapsedSeconds)} | ${seconds(row.ages.progressSeconds)} | ${seconds(row.remainingSeconds)} | ${row.health.reasonCode} | ${row.nextAction}\n`
|
|
982
|
-
+ ` thresholds=${JSON.stringify(row.thresholds)} termination=${row.termination} verification=${label(row.verification)} observer-gap=${row.health.maxCheckGapSeconds}s`
|
|
983
|
-
|
|
1019
|
+
+ ` thresholds=${JSON.stringify(row.thresholds)} termination=${row.termination} verification=${label(row.verification)} observer-gap=${row.health.maxCheckGapSeconds}s`
|
|
1020
|
+
+ (row.startupCorrection ? `\n startup correction=${row.startupCorrection.action} state=${row.startupCorrection.state} reason=${row.startupCorrection.reasonCode} stop=${label(row.startupCorrection.stopCode)}` : '')),
|
|
1021
|
+
...recoveryLines, ...report.findings, `next check=${label(report.nextCheckAt)}`].join('\n');
|
|
984
1022
|
}
|
|
985
1023
|
|
|
986
1024
|
/** One bounded foreground loop. Timeout only stops watching, never the worker. */
|