@chllming/wave-orchestration 0.5.4 → 0.6.1
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/CHANGELOG.md +52 -3
- package/README.md +33 -5
- package/docs/README.md +18 -4
- package/docs/agents/wave-cont-eval-role.md +36 -0
- package/docs/agents/{wave-evaluator-role.md → wave-cont-qa-role.md} +14 -11
- package/docs/agents/wave-documentation-role.md +1 -1
- package/docs/agents/wave-infra-role.md +1 -1
- package/docs/agents/wave-integration-role.md +3 -3
- package/docs/agents/wave-launcher-role.md +4 -3
- package/docs/agents/wave-security-role.md +40 -0
- package/docs/concepts/context7-vs-skills.md +1 -1
- package/docs/concepts/what-is-a-wave.md +56 -6
- package/docs/evals/README.md +166 -0
- package/docs/evals/benchmark-catalog.json +663 -0
- package/docs/guides/author-and-run-waves.md +135 -0
- package/docs/guides/planner.md +5 -0
- package/docs/guides/terminal-surfaces.md +2 -0
- package/docs/plans/component-cutover-matrix.json +1 -1
- package/docs/plans/component-cutover-matrix.md +1 -1
- package/docs/plans/current-state.md +19 -1
- package/docs/plans/examples/wave-example-live-proof.md +435 -0
- package/docs/plans/migration.md +42 -0
- package/docs/plans/wave-orchestrator.md +46 -7
- package/docs/plans/waves/wave-0.md +4 -4
- package/docs/reference/live-proof-waves.md +177 -0
- package/docs/reference/migration-0.2-to-0.5.md +26 -19
- package/docs/reference/npmjs-trusted-publishing.md +6 -5
- package/docs/reference/runtime-config/README.md +14 -4
- package/docs/reference/sample-waves.md +87 -0
- package/docs/reference/skills.md +110 -42
- package/docs/research/agent-context-sources.md +130 -11
- package/docs/research/coordination-failure-review.md +266 -0
- package/docs/roadmap.md +6 -2
- package/package.json +2 -2
- package/releases/manifest.json +35 -2
- package/scripts/research/agent-context-archive.mjs +83 -1
- package/scripts/research/manifests/agent-context-expanded-2026-03-22.mjs +811 -0
- package/scripts/wave-orchestrator/adhoc.mjs +1331 -0
- package/scripts/wave-orchestrator/agent-state.mjs +358 -6
- package/scripts/wave-orchestrator/artifact-schemas.mjs +173 -0
- package/scripts/wave-orchestrator/clarification-triage.mjs +10 -3
- package/scripts/wave-orchestrator/config.mjs +48 -12
- package/scripts/wave-orchestrator/context7.mjs +2 -0
- package/scripts/wave-orchestrator/coord-cli.mjs +51 -19
- package/scripts/wave-orchestrator/coordination-store.mjs +26 -4
- package/scripts/wave-orchestrator/coordination.mjs +83 -9
- package/scripts/wave-orchestrator/dashboard-state.mjs +20 -8
- package/scripts/wave-orchestrator/dep-cli.mjs +5 -2
- package/scripts/wave-orchestrator/docs-queue.mjs +8 -2
- package/scripts/wave-orchestrator/evals.mjs +451 -0
- package/scripts/wave-orchestrator/feedback.mjs +15 -1
- package/scripts/wave-orchestrator/install.mjs +32 -9
- package/scripts/wave-orchestrator/launcher-closure.mjs +281 -0
- package/scripts/wave-orchestrator/launcher-runtime.mjs +334 -0
- package/scripts/wave-orchestrator/launcher.mjs +709 -601
- package/scripts/wave-orchestrator/ledger.mjs +123 -20
- package/scripts/wave-orchestrator/local-executor.mjs +99 -12
- package/scripts/wave-orchestrator/planner.mjs +177 -42
- package/scripts/wave-orchestrator/replay.mjs +6 -3
- package/scripts/wave-orchestrator/role-helpers.mjs +84 -0
- package/scripts/wave-orchestrator/shared.mjs +75 -11
- package/scripts/wave-orchestrator/skills.mjs +637 -106
- package/scripts/wave-orchestrator/traces.mjs +71 -48
- package/scripts/wave-orchestrator/wave-files.mjs +947 -101
- package/scripts/wave.mjs +9 -0
- package/skills/README.md +202 -0
- package/skills/provider-aws/SKILL.md +111 -0
- package/skills/provider-aws/adapters/claude.md +1 -0
- package/skills/provider-aws/adapters/codex.md +1 -0
- package/skills/provider-aws/references/service-verification.md +39 -0
- package/skills/provider-aws/skill.json +50 -1
- package/skills/provider-custom-deploy/SKILL.md +59 -0
- package/skills/provider-custom-deploy/skill.json +46 -1
- package/skills/provider-docker-compose/SKILL.md +90 -0
- package/skills/provider-docker-compose/adapters/local.md +1 -0
- package/skills/provider-docker-compose/skill.json +49 -1
- package/skills/provider-github-release/SKILL.md +116 -1
- package/skills/provider-github-release/adapters/claude.md +1 -0
- package/skills/provider-github-release/adapters/codex.md +1 -0
- package/skills/provider-github-release/skill.json +51 -1
- package/skills/provider-kubernetes/SKILL.md +137 -0
- package/skills/provider-kubernetes/adapters/claude.md +1 -0
- package/skills/provider-kubernetes/adapters/codex.md +1 -0
- package/skills/provider-kubernetes/references/kubectl-patterns.md +58 -0
- package/skills/provider-kubernetes/skill.json +48 -1
- package/skills/provider-railway/SKILL.md +118 -1
- package/skills/provider-railway/references/verification-commands.md +39 -0
- package/skills/provider-railway/skill.json +67 -1
- package/skills/provider-ssh-manual/SKILL.md +91 -0
- package/skills/provider-ssh-manual/skill.json +50 -1
- package/skills/repo-coding-rules/SKILL.md +84 -0
- package/skills/repo-coding-rules/skill.json +30 -1
- package/skills/role-cont-eval/SKILL.md +90 -0
- package/skills/role-cont-eval/adapters/codex.md +1 -0
- package/skills/role-cont-eval/skill.json +36 -0
- package/skills/role-cont-qa/SKILL.md +93 -0
- package/skills/role-cont-qa/adapters/claude.md +1 -0
- package/skills/role-cont-qa/skill.json +36 -0
- package/skills/role-deploy/SKILL.md +90 -0
- package/skills/role-deploy/skill.json +32 -1
- package/skills/role-documentation/SKILL.md +66 -0
- package/skills/role-documentation/skill.json +32 -1
- package/skills/role-implementation/SKILL.md +62 -0
- package/skills/role-implementation/skill.json +32 -1
- package/skills/role-infra/SKILL.md +74 -0
- package/skills/role-infra/skill.json +32 -1
- package/skills/role-integration/SKILL.md +79 -1
- package/skills/role-integration/skill.json +32 -1
- package/skills/role-research/SKILL.md +58 -0
- package/skills/role-research/skill.json +32 -1
- package/skills/role-security/SKILL.md +60 -0
- package/skills/role-security/skill.json +36 -0
- package/skills/runtime-claude/SKILL.md +60 -1
- package/skills/runtime-claude/skill.json +32 -1
- package/skills/runtime-codex/SKILL.md +52 -1
- package/skills/runtime-codex/skill.json +32 -1
- package/skills/runtime-local/SKILL.md +39 -0
- package/skills/runtime-local/skill.json +32 -1
- package/skills/runtime-opencode/SKILL.md +51 -0
- package/skills/runtime-opencode/skill.json +32 -1
- package/skills/wave-core/SKILL.md +107 -0
- package/skills/wave-core/references/marker-syntax.md +62 -0
- package/skills/wave-core/skill.json +31 -1
- package/wave.config.json +35 -6
- package/skills/role-evaluator/SKILL.md +0 -6
- package/skills/role-evaluator/skill.json +0 -5
|
@@ -3,12 +3,15 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_CODEX_SANDBOX_MODE,
|
|
6
|
+
DEFAULT_CONT_EVAL_AGENT_ID,
|
|
7
|
+
DEFAULT_CONT_EVAL_ROLE_PROMPT_PATH,
|
|
8
|
+
DEFAULT_CONT_QA_AGENT_ID,
|
|
9
|
+
DEFAULT_CONT_QA_ROLE_PROMPT_PATH,
|
|
6
10
|
DEFAULT_DOCUMENTATION_AGENT_ID,
|
|
7
11
|
DEFAULT_DOCUMENTATION_ROLE_PROMPT_PATH,
|
|
8
|
-
DEFAULT_EVALUATOR_AGENT_ID,
|
|
9
|
-
DEFAULT_EVALUATOR_ROLE_PROMPT_PATH,
|
|
10
12
|
DEFAULT_INTEGRATION_AGENT_ID,
|
|
11
13
|
DEFAULT_INTEGRATION_ROLE_PROMPT_PATH,
|
|
14
|
+
DEFAULT_SECURITY_ROLE_PROMPT_PATH,
|
|
12
15
|
DEFAULT_WAVE_LANE,
|
|
13
16
|
loadWaveConfig,
|
|
14
17
|
normalizeCodexSandboxMode,
|
|
@@ -26,6 +29,7 @@ import {
|
|
|
26
29
|
REPORT_VERDICT_REGEX,
|
|
27
30
|
WAVE_VERDICT_REGEX,
|
|
28
31
|
walkFiles,
|
|
32
|
+
toIsoTimestamp,
|
|
29
33
|
writeJsonAtomic,
|
|
30
34
|
} from "./shared.mjs";
|
|
31
35
|
import { normalizeContext7Config, hashAgentPromptFingerprint } from "./context7.mjs";
|
|
@@ -35,19 +39,43 @@ import {
|
|
|
35
39
|
readMaterializedCoordinationState,
|
|
36
40
|
} from "./coordination-store.mjs";
|
|
37
41
|
import {
|
|
42
|
+
agentSummaryPathFromStatusPath,
|
|
43
|
+
buildAgentExecutionSummary,
|
|
38
44
|
normalizeExitContract,
|
|
39
45
|
readAgentExecutionSummary,
|
|
46
|
+
validateContEvalSummary,
|
|
47
|
+
validateContQaSummary,
|
|
40
48
|
validateDocumentationClosureSummary,
|
|
41
|
-
validateEvaluatorSummary,
|
|
42
49
|
validateExitContractShape,
|
|
43
50
|
validateIntegrationSummary,
|
|
44
51
|
validateImplementationSummary,
|
|
52
|
+
validateSecuritySummary,
|
|
53
|
+
writeAgentExecutionSummary,
|
|
45
54
|
} from "./agent-state.mjs";
|
|
55
|
+
import { parseEvalTargets, validateEvalTargets } from "./evals.mjs";
|
|
46
56
|
import { normalizeSkillId, resolveAgentSkills } from "./skills.mjs";
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
import {
|
|
58
|
+
isContEvalImplementationOwningAgent,
|
|
59
|
+
isContEvalReportOnlyAgent,
|
|
60
|
+
isContEvalReportPath,
|
|
61
|
+
isContQaReportPath,
|
|
62
|
+
isSecurityRolePromptPath,
|
|
63
|
+
isSecurityReviewAgent,
|
|
64
|
+
resolveSecurityReviewReportPath,
|
|
65
|
+
} from "./role-helpers.mjs";
|
|
66
|
+
import {
|
|
67
|
+
RUN_STATE_KIND,
|
|
68
|
+
RUN_STATE_SCHEMA_VERSION,
|
|
69
|
+
normalizeManifest,
|
|
70
|
+
readAssignmentSnapshot,
|
|
71
|
+
readDependencySnapshot,
|
|
72
|
+
} from "./artifact-schemas.mjs";
|
|
73
|
+
|
|
74
|
+
export const WAVE_CONT_QA_ROLE_PROMPT_PATH = DEFAULT_CONT_QA_ROLE_PROMPT_PATH;
|
|
75
|
+
export const WAVE_CONT_EVAL_ROLE_PROMPT_PATH = DEFAULT_CONT_EVAL_ROLE_PROMPT_PATH;
|
|
49
76
|
export const WAVE_INTEGRATION_ROLE_PROMPT_PATH = DEFAULT_INTEGRATION_ROLE_PROMPT_PATH;
|
|
50
77
|
export const WAVE_DOCUMENTATION_ROLE_PROMPT_PATH = DEFAULT_DOCUMENTATION_ROLE_PROMPT_PATH;
|
|
78
|
+
export const WAVE_SECURITY_ROLE_PROMPT_PATH = DEFAULT_SECURITY_ROLE_PROMPT_PATH;
|
|
51
79
|
export const SHARED_PLAN_DOC_PATHS = [
|
|
52
80
|
"docs/plans/current-state.md",
|
|
53
81
|
"docs/plans/master-plan.md",
|
|
@@ -55,6 +83,22 @@ export const SHARED_PLAN_DOC_PATHS = [
|
|
|
55
83
|
];
|
|
56
84
|
|
|
57
85
|
const COMPONENT_ID_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
|
|
86
|
+
const COMPONENT_MATURITY_LEVELS = [
|
|
87
|
+
"inventoried",
|
|
88
|
+
"contract-frozen",
|
|
89
|
+
"repo-landed",
|
|
90
|
+
"baseline-proved",
|
|
91
|
+
"pilot-live",
|
|
92
|
+
"qa-proved",
|
|
93
|
+
"fleet-ready",
|
|
94
|
+
"cutover-ready",
|
|
95
|
+
"deprecation-ready",
|
|
96
|
+
];
|
|
97
|
+
const COMPONENT_MATURITY_ORDER = Object.fromEntries(
|
|
98
|
+
COMPONENT_MATURITY_LEVELS.map((level, index) => [level, index]),
|
|
99
|
+
);
|
|
100
|
+
const PROOF_CENTRIC_COMPONENT_LEVEL = "pilot-live";
|
|
101
|
+
const RETRY_POLICY_VALUES = new Set(["sticky", "fallback-allowed"]);
|
|
58
102
|
|
|
59
103
|
function resolveLaneProfileForOptions(options = {}) {
|
|
60
104
|
if (options.laneProfile) {
|
|
@@ -64,6 +108,26 @@ function resolveLaneProfileForOptions(options = {}) {
|
|
|
64
108
|
return resolveLaneProfile(config, options.lane || config.defaultLane || DEFAULT_WAVE_LANE);
|
|
65
109
|
}
|
|
66
110
|
|
|
111
|
+
function resolveSecurityRolePromptPath(laneProfile) {
|
|
112
|
+
return laneProfile?.roles?.securityRolePromptPath || DEFAULT_SECURITY_ROLE_PROMPT_PATH;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeSecurityCapabilities(capabilities, rolePromptPaths, securityRolePromptPath) {
|
|
116
|
+
const normalized = Array.isArray(capabilities) ? [...capabilities] : [];
|
|
117
|
+
const hasSecurityRolePrompt = Array.isArray(rolePromptPaths)
|
|
118
|
+
? rolePromptPaths.some((rolePromptPath) =>
|
|
119
|
+
isSecurityRolePromptPath(rolePromptPath, securityRolePromptPath),
|
|
120
|
+
)
|
|
121
|
+
: false;
|
|
122
|
+
if (
|
|
123
|
+
hasSecurityRolePrompt &&
|
|
124
|
+
!normalized.some((capability) => String(capability || "").trim().toLowerCase() === "security-review")
|
|
125
|
+
) {
|
|
126
|
+
normalized.push("security-review");
|
|
127
|
+
}
|
|
128
|
+
return normalized;
|
|
129
|
+
}
|
|
130
|
+
|
|
67
131
|
export function waveNumberFromFileName(fileName) {
|
|
68
132
|
const match = fileName.match(/^wave-(\d+)\.md$/);
|
|
69
133
|
if (!match) {
|
|
@@ -316,6 +380,147 @@ function validateAgentDeliverables(deliverables, ownedPaths, filePath, agentId)
|
|
|
316
380
|
}
|
|
317
381
|
}
|
|
318
382
|
|
|
383
|
+
function normalizeMaturityLevel(value, label, filePath) {
|
|
384
|
+
const normalized = String(value || "").trim();
|
|
385
|
+
if (!COMPONENT_MATURITY_ORDER.hasOwnProperty(normalized)) {
|
|
386
|
+
throw new Error(`Invalid maturity level "${value}" in ${label} (${filePath})`);
|
|
387
|
+
}
|
|
388
|
+
return normalized;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function proofCentricLevelReached(level) {
|
|
392
|
+
return (
|
|
393
|
+
COMPONENT_MATURITY_ORDER[String(level || "").trim()] >=
|
|
394
|
+
COMPONENT_MATURITY_ORDER[PROOF_CENTRIC_COMPONENT_LEVEL]
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function waveRequiresProofCentricValidation(wave) {
|
|
399
|
+
return Array.isArray(wave?.componentPromotions)
|
|
400
|
+
? wave.componentPromotions.some((promotion) => proofCentricLevelReached(promotion?.targetLevel))
|
|
401
|
+
: false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function agentHighestComponentTargetLevel(agent) {
|
|
405
|
+
const levels = Array.isArray(agent?.components)
|
|
406
|
+
? agent.components
|
|
407
|
+
.map((componentId) => agent?.componentTargets?.[componentId] || null)
|
|
408
|
+
.filter(Boolean)
|
|
409
|
+
: [];
|
|
410
|
+
if (levels.length === 0) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
return levels.sort((left, right) => COMPONENT_MATURITY_ORDER[right] - COMPONENT_MATURITY_ORDER[left])[0];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function agentRequiresProofCentricValidation(agent) {
|
|
417
|
+
const highestTarget = agentHighestComponentTargetLevel(agent);
|
|
418
|
+
if (highestTarget && proofCentricLevelReached(highestTarget)) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
return Array.isArray(agent?.proofArtifacts) && agent.proofArtifacts.some((artifact) => {
|
|
422
|
+
if (!Array.isArray(artifact?.requiredFor) || artifact.requiredFor.length === 0) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
return artifact.requiredFor.some((level) => proofCentricLevelReached(level));
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function parseProofArtifacts(blockText, filePath, label) {
|
|
430
|
+
if (!blockText) {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
const artifacts = [];
|
|
434
|
+
const seenPaths = new Set();
|
|
435
|
+
for (const line of String(blockText || "").split(/\r?\n/)) {
|
|
436
|
+
const trimmed = line.trim();
|
|
437
|
+
if (!trimmed) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const bulletMatch = trimmed.match(/^-\s+(.+?)\s*$/);
|
|
441
|
+
if (!bulletMatch) {
|
|
442
|
+
throw new Error(`Malformed proof artifact entry "${trimmed}" in ${label} (${filePath})`);
|
|
443
|
+
}
|
|
444
|
+
const rawEntry = bulletMatch[1].trim();
|
|
445
|
+
let artifact = null;
|
|
446
|
+
if (!rawEntry.includes("|") && !/^path\s*:/i.test(rawEntry)) {
|
|
447
|
+
const relPath = rawEntry.replace(/[`"']/g, "").trim();
|
|
448
|
+
if (!isRepoContainedPath(relPath)) {
|
|
449
|
+
throw new Error(`Path "${relPath}" in ${label} (${filePath}) must stay within the repo root`);
|
|
450
|
+
}
|
|
451
|
+
artifact = {
|
|
452
|
+
path: relPath,
|
|
453
|
+
kind: null,
|
|
454
|
+
requiredFor: [],
|
|
455
|
+
};
|
|
456
|
+
} else {
|
|
457
|
+
const fields = {};
|
|
458
|
+
for (const segment of rawEntry.split("|")) {
|
|
459
|
+
const pair = segment.trim();
|
|
460
|
+
if (!pair) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const separatorIndex = pair.indexOf(":");
|
|
464
|
+
if (separatorIndex <= 0) {
|
|
465
|
+
throw new Error(`Malformed proof artifact field "${pair}" in ${label} (${filePath})`);
|
|
466
|
+
}
|
|
467
|
+
const key = pair.slice(0, separatorIndex).trim().toLowerCase();
|
|
468
|
+
const value = pair
|
|
469
|
+
.slice(separatorIndex + 1)
|
|
470
|
+
.trim()
|
|
471
|
+
.replace(/^["'`]|["'`]$/g, "");
|
|
472
|
+
if (!key || !value) {
|
|
473
|
+
throw new Error(`Malformed proof artifact field "${pair}" in ${label} (${filePath})`);
|
|
474
|
+
}
|
|
475
|
+
fields[key] = value;
|
|
476
|
+
}
|
|
477
|
+
const relPath = String(fields.path || "").trim();
|
|
478
|
+
if (!relPath) {
|
|
479
|
+
throw new Error(`Proof artifact entry in ${label} (${filePath}) must include path`);
|
|
480
|
+
}
|
|
481
|
+
if (!isRepoContainedPath(relPath)) {
|
|
482
|
+
throw new Error(`Path "${relPath}" in ${label} (${filePath}) must stay within the repo root`);
|
|
483
|
+
}
|
|
484
|
+
const requiredFor = String(fields["required-for"] || "")
|
|
485
|
+
.split(",")
|
|
486
|
+
.map((entry) => entry.trim())
|
|
487
|
+
.filter(Boolean)
|
|
488
|
+
.map((entry) => normalizeMaturityLevel(entry, label, filePath));
|
|
489
|
+
artifact = {
|
|
490
|
+
path: relPath,
|
|
491
|
+
kind: String(fields.kind || "").trim() || null,
|
|
492
|
+
requiredFor,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
if (normalizeRepoRelativePath(artifact.path).endsWith("/")) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`Proof artifact "${artifact.path}" in ${label} (${filePath}) must be a file path, not a directory path`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (seenPaths.has(artifact.path)) {
|
|
501
|
+
throw new Error(`Duplicate proof artifact "${artifact.path}" in ${label} (${filePath})`);
|
|
502
|
+
}
|
|
503
|
+
seenPaths.add(artifact.path);
|
|
504
|
+
artifacts.push(artifact);
|
|
505
|
+
}
|
|
506
|
+
return artifacts;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function validateAgentProofArtifacts(proofArtifacts, ownedPaths, filePath, agentId) {
|
|
510
|
+
if (!Array.isArray(proofArtifacts) || proofArtifacts.length === 0) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const owned = Array.isArray(ownedPaths) ? ownedPaths : [];
|
|
514
|
+
for (const artifact of proofArtifacts) {
|
|
515
|
+
const normalized = normalizeRepoRelativePath(artifact?.path);
|
|
516
|
+
if (!owned.some((ownedPath) => deliverableIsOwned(normalized, ownedPath))) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
`Proof artifact "${artifact?.path}" for agent ${agentId} in ${filePath} must stay within the agent's declared file ownership`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
319
524
|
function extractFencedBlock(blockText, messagePrefix) {
|
|
320
525
|
const fencedBlockMatch = String(blockText || "").match(
|
|
321
526
|
/```(?:[a-zA-Z0-9_-]+)?\r?\n([\s\S]*?)\r?\n```/,
|
|
@@ -466,6 +671,8 @@ export function normalizeAgentExecutorConfig(rawSettings, filePath, label) {
|
|
|
466
671
|
fallbacks: [],
|
|
467
672
|
tags: [],
|
|
468
673
|
budget: null,
|
|
674
|
+
retryPolicy: null,
|
|
675
|
+
allowFallbackOnRetry: null,
|
|
469
676
|
codex: null,
|
|
470
677
|
claude: null,
|
|
471
678
|
opencode: null,
|
|
@@ -476,6 +683,8 @@ export function normalizeAgentExecutorConfig(rawSettings, filePath, label) {
|
|
|
476
683
|
"model",
|
|
477
684
|
"fallbacks",
|
|
478
685
|
"tags",
|
|
686
|
+
"retry-policy",
|
|
687
|
+
"allow-fallback-on-retry",
|
|
479
688
|
"budget.turns",
|
|
480
689
|
"budget.minutes",
|
|
481
690
|
"codex.command",
|
|
@@ -530,6 +739,20 @@ export function normalizeAgentExecutorConfig(rawSettings, filePath, label) {
|
|
|
530
739
|
);
|
|
531
740
|
} else if (key === "tags") {
|
|
532
741
|
executorConfig.tags = parseExecutorStringList(value);
|
|
742
|
+
} else if (key === "retry-policy") {
|
|
743
|
+
const normalizedPolicy = value.toLowerCase();
|
|
744
|
+
if (!RETRY_POLICY_VALUES.has(normalizedPolicy)) {
|
|
745
|
+
throw new Error(
|
|
746
|
+
`Invalid ${label}.retry-policy "${value}" in ${filePath}; expected sticky or fallback-allowed`,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
executorConfig.retryPolicy = normalizedPolicy;
|
|
750
|
+
} else if (key === "allow-fallback-on-retry") {
|
|
751
|
+
executorConfig.allowFallbackOnRetry = parseExecutorBoolean(
|
|
752
|
+
value,
|
|
753
|
+
`${label}.allow-fallback-on-retry`,
|
|
754
|
+
filePath,
|
|
755
|
+
);
|
|
533
756
|
} else if (key === "budget.turns" || key === "budget.minutes") {
|
|
534
757
|
executorConfig.budget = {
|
|
535
758
|
...(executorConfig.budget || { turns: null, minutes: null }),
|
|
@@ -793,6 +1016,13 @@ export function extractAgentDeliverablesFromSection(sectionText, filePath, agent
|
|
|
793
1016
|
return parsePathList(block, filePath, `agent ${agentId} deliverables`);
|
|
794
1017
|
}
|
|
795
1018
|
|
|
1019
|
+
export function extractAgentProofArtifactsFromSection(sectionText, filePath, agentId) {
|
|
1020
|
+
const block = extractSectionBody(sectionText, "Proof artifacts", filePath, agentId, {
|
|
1021
|
+
required: false,
|
|
1022
|
+
});
|
|
1023
|
+
return parseProofArtifacts(block, filePath, `agent ${agentId} proof artifacts`);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
796
1026
|
export function slugify(value) {
|
|
797
1027
|
return value
|
|
798
1028
|
.toLowerCase()
|
|
@@ -890,21 +1120,102 @@ export function composeResolvedPrompt(rolePromptPaths, localPrompt, filePath, ag
|
|
|
890
1120
|
.join("\n\n");
|
|
891
1121
|
}
|
|
892
1122
|
|
|
893
|
-
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
if (!evaluator) {
|
|
1123
|
+
function resolveAgentReportPath(wave, agentId, pattern) {
|
|
1124
|
+
const agent = wave?.agents?.find((entry) => entry.agentId === agentId);
|
|
1125
|
+
if (!agent) {
|
|
897
1126
|
return null;
|
|
898
1127
|
}
|
|
899
1128
|
return (
|
|
900
|
-
|
|
901
|
-
|
|
1129
|
+
agent.ownedPaths.find((ownedPath) =>
|
|
1130
|
+
pattern.test(ownedPath),
|
|
902
1131
|
) ??
|
|
903
|
-
|
|
1132
|
+
agent.ownedPaths[0] ??
|
|
904
1133
|
null
|
|
905
1134
|
);
|
|
906
1135
|
}
|
|
907
1136
|
|
|
1137
|
+
export function resolveContQaReportPath(wave, options = {}) {
|
|
1138
|
+
const contQaAgentId = options.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
|
|
1139
|
+
return resolveAgentReportPath(
|
|
1140
|
+
wave,
|
|
1141
|
+
contQaAgentId,
|
|
1142
|
+
{ test: isContQaReportPath },
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
export function resolveContEvalReportPath(wave, options = {}) {
|
|
1147
|
+
const contEvalAgentId = options.contEvalAgentId || DEFAULT_CONT_EVAL_AGENT_ID;
|
|
1148
|
+
return resolveAgentReportPath(
|
|
1149
|
+
wave,
|
|
1150
|
+
contEvalAgentId,
|
|
1151
|
+
{ test: isContEvalReportPath },
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function isImplementationOwningWaveAgent(
|
|
1156
|
+
agent,
|
|
1157
|
+
{
|
|
1158
|
+
contQaAgentId,
|
|
1159
|
+
contEvalAgentId,
|
|
1160
|
+
integrationAgentId,
|
|
1161
|
+
documentationAgentId,
|
|
1162
|
+
securityRolePromptPath,
|
|
1163
|
+
},
|
|
1164
|
+
) {
|
|
1165
|
+
return (
|
|
1166
|
+
![contQaAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId) &&
|
|
1167
|
+
!isContEvalReportOnlyAgent(agent, { contEvalAgentId }) &&
|
|
1168
|
+
!isSecurityReviewAgent(agent, { securityRolePromptPath })
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function resolveAgentSummaryReportPath(wave, agentId, { contQaAgentId, contEvalAgentId } = {}) {
|
|
1173
|
+
if (agentId === contQaAgentId && wave.contQaReportPath) {
|
|
1174
|
+
return path.resolve(REPO_ROOT, wave.contQaReportPath);
|
|
1175
|
+
}
|
|
1176
|
+
if (agentId === contEvalAgentId && wave.contEvalReportPath) {
|
|
1177
|
+
return path.resolve(REPO_ROOT, wave.contEvalReportPath);
|
|
1178
|
+
}
|
|
1179
|
+
const agent = wave?.agents?.find((entry) => entry.agentId === agentId);
|
|
1180
|
+
if (isSecurityReviewAgent(agent)) {
|
|
1181
|
+
const securityReportPath = resolveSecurityReviewReportPath(agent);
|
|
1182
|
+
if (securityReportPath) {
|
|
1183
|
+
return path.resolve(REPO_ROOT, securityReportPath);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function materializeLiveExecutionSummaryIfMissing({
|
|
1190
|
+
wave,
|
|
1191
|
+
agent,
|
|
1192
|
+
statusPath,
|
|
1193
|
+
statusRecord,
|
|
1194
|
+
logsDir,
|
|
1195
|
+
contQaAgentId,
|
|
1196
|
+
contEvalAgentId,
|
|
1197
|
+
}) {
|
|
1198
|
+
const existing = readAgentExecutionSummary(statusPath);
|
|
1199
|
+
if (existing) {
|
|
1200
|
+
return existing;
|
|
1201
|
+
}
|
|
1202
|
+
const logPath = logsDir ? path.join(logsDir, `wave-${wave.wave}-${agent.slug}.log`) : null;
|
|
1203
|
+
if (!statusRecord || !logPath || !fs.existsSync(logPath)) {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
const summary = buildAgentExecutionSummary({
|
|
1207
|
+
agent,
|
|
1208
|
+
statusRecord,
|
|
1209
|
+
logPath,
|
|
1210
|
+
reportPath: resolveAgentSummaryReportPath(wave, agent.agentId, {
|
|
1211
|
+
contQaAgentId,
|
|
1212
|
+
contEvalAgentId,
|
|
1213
|
+
}),
|
|
1214
|
+
});
|
|
1215
|
+
writeAgentExecutionSummary(statusPath, summary);
|
|
1216
|
+
return summary;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
908
1219
|
function normalizeMatrixStringArray(values, label, filePath) {
|
|
909
1220
|
if (!Array.isArray(values)) {
|
|
910
1221
|
return [];
|
|
@@ -1049,11 +1360,13 @@ export function requiredDocumentationStewardPathsForWave(waveNumber, options = {
|
|
|
1049
1360
|
export function validateWaveDefinition(wave, options = {}) {
|
|
1050
1361
|
const laneProfile = resolveLaneProfileForOptions(options);
|
|
1051
1362
|
const lane = laneProfile.lane;
|
|
1052
|
-
const
|
|
1363
|
+
const contQaAgentId = laneProfile.roles.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
|
|
1364
|
+
const contEvalAgentId = laneProfile.roles.contEvalAgentId || DEFAULT_CONT_EVAL_AGENT_ID;
|
|
1053
1365
|
const integrationAgentId =
|
|
1054
1366
|
laneProfile.roles.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID;
|
|
1055
1367
|
const documentationAgentId =
|
|
1056
1368
|
laneProfile.roles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
|
|
1369
|
+
const securityRolePromptPath = resolveSecurityRolePromptPath(laneProfile);
|
|
1057
1370
|
const documentationThreshold = laneProfile.validation.requireDocumentationStewardFromWave;
|
|
1058
1371
|
const context7Threshold = laneProfile.validation.requireContext7DeclarationsFromWave;
|
|
1059
1372
|
const exitContractThreshold = laneProfile.validation.requireExitContractsFromWave;
|
|
@@ -1094,10 +1407,27 @@ export function validateWaveDefinition(wave, options = {}) {
|
|
|
1094
1407
|
if (duplicateAgentIds.length > 0) {
|
|
1095
1408
|
errors.push(`must not repeat agent ids (${Array.from(new Set(duplicateAgentIds)).join(", ")})`);
|
|
1096
1409
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1410
|
+
const contEvalAgent = wave.agents.find((agent) => agent.agentId === contEvalAgentId) || null;
|
|
1411
|
+
const contEvalImplementationOwning = contEvalAgent
|
|
1412
|
+
? isContEvalImplementationOwningAgent(contEvalAgent, { contEvalAgentId })
|
|
1413
|
+
: false;
|
|
1414
|
+
const implementationOwningAgents = wave.agents.filter((agent) =>
|
|
1415
|
+
isImplementationOwningWaveAgent(agent, {
|
|
1416
|
+
contQaAgentId,
|
|
1417
|
+
contEvalAgentId,
|
|
1418
|
+
integrationAgentId,
|
|
1419
|
+
documentationAgentId,
|
|
1420
|
+
securityRolePromptPath,
|
|
1421
|
+
}),
|
|
1422
|
+
);
|
|
1423
|
+
if (!wave.agents.some((agent) => agent.agentId === contQaAgentId)) {
|
|
1424
|
+
errors.push(`must include Agent ${contQaAgentId} as the cont-QA closure role`);
|
|
1099
1425
|
}
|
|
1100
|
-
if (
|
|
1426
|
+
if (
|
|
1427
|
+
componentPromotionRuleActive &&
|
|
1428
|
+
promotedComponents.size === 0 &&
|
|
1429
|
+
implementationOwningAgents.length > 0
|
|
1430
|
+
) {
|
|
1101
1431
|
errors.push(
|
|
1102
1432
|
`Wave ${wave.wave} must declare a ## Component promotions section in waves ${componentPromotionThreshold} and later`,
|
|
1103
1433
|
);
|
|
@@ -1203,7 +1533,11 @@ export function validateWaveDefinition(wave, options = {}) {
|
|
|
1203
1533
|
);
|
|
1204
1534
|
}
|
|
1205
1535
|
}
|
|
1206
|
-
if (
|
|
1536
|
+
if (
|
|
1537
|
+
[contQaAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId) ||
|
|
1538
|
+
isContEvalReportOnlyAgent(agent, { contEvalAgentId }) ||
|
|
1539
|
+
isSecurityReviewAgent(agent, { securityRolePromptPath })
|
|
1540
|
+
) {
|
|
1207
1541
|
if (Array.isArray(agent.components) && agent.components.length > 0) {
|
|
1208
1542
|
errors.push(`Agent ${agent.agentId} must not declare a ### Components section`);
|
|
1209
1543
|
}
|
|
@@ -1230,7 +1564,11 @@ export function validateWaveDefinition(wave, options = {}) {
|
|
|
1230
1564
|
}
|
|
1231
1565
|
}
|
|
1232
1566
|
if (exitContractThreshold !== null && wave.wave >= exitContractThreshold) {
|
|
1233
|
-
if (
|
|
1567
|
+
if (
|
|
1568
|
+
![contQaAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId) &&
|
|
1569
|
+
!isContEvalReportOnlyAgent(agent, { contEvalAgentId }) &&
|
|
1570
|
+
!isSecurityReviewAgent(agent, { securityRolePromptPath })
|
|
1571
|
+
) {
|
|
1234
1572
|
if (!agent.exitContract) {
|
|
1235
1573
|
errors.push(
|
|
1236
1574
|
`Agent ${agent.agentId} must declare a ### Exit contract section in waves ${exitContractThreshold} and later`,
|
|
@@ -1245,22 +1583,77 @@ export function validateWaveDefinition(wave, options = {}) {
|
|
|
1245
1583
|
}
|
|
1246
1584
|
}
|
|
1247
1585
|
}
|
|
1586
|
+
if (
|
|
1587
|
+
agentRequiresProofCentricValidation(agent) &&
|
|
1588
|
+
(!Array.isArray(agent.proofArtifacts) || agent.proofArtifacts.length === 0) &&
|
|
1589
|
+
![contQaAgentId, integrationAgentId, documentationAgentId].includes(agent.agentId) &&
|
|
1590
|
+
!isContEvalReportOnlyAgent(agent, { contEvalAgentId }) &&
|
|
1591
|
+
!isSecurityReviewAgent(agent, { securityRolePromptPath })
|
|
1592
|
+
) {
|
|
1593
|
+
errors.push(
|
|
1594
|
+
`Agent ${agent.agentId} must declare a ### Proof artifacts section when it targets ${PROOF_CENTRIC_COMPONENT_LEVEL} or above`,
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
if (
|
|
1598
|
+
agentRequiresProofCentricValidation(agent) &&
|
|
1599
|
+
(agent.executorConfig?.id === "local" || agent.executorResolved?.id === "local")
|
|
1600
|
+
) {
|
|
1601
|
+
errors.push(
|
|
1602
|
+
`Agent ${agent.agentId} must not use executor=local when it carries proof-centric validation artifacts`,
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1248
1605
|
}
|
|
1249
1606
|
for (const agent of wave.agents) {
|
|
1250
|
-
for (const requiredRef of laneProfile.validation.requiredPromptReferences) {
|
|
1607
|
+
for (const requiredRef of laneProfile.validation.requiredPromptReferences || []) {
|
|
1251
1608
|
if (!agent.prompt.includes(requiredRef)) {
|
|
1252
1609
|
errors.push(`Agent ${agent.agentId} must reference ${requiredRef}`);
|
|
1253
1610
|
}
|
|
1254
1611
|
}
|
|
1255
1612
|
}
|
|
1256
|
-
const
|
|
1257
|
-
if (!
|
|
1613
|
+
const contQaAgent = wave.agents.find((agent) => agent.agentId === contQaAgentId);
|
|
1614
|
+
if (!contQaAgent?.rolePromptPaths?.includes(laneProfile.roles.contQaRolePromptPath)) {
|
|
1258
1615
|
errors.push(
|
|
1259
|
-
`Agent ${
|
|
1616
|
+
`Agent ${contQaAgentId} must import ${laneProfile.roles.contQaRolePromptPath}`,
|
|
1260
1617
|
);
|
|
1261
1618
|
}
|
|
1262
|
-
if (!
|
|
1263
|
-
errors.push(`Agent ${
|
|
1619
|
+
if (!resolveContQaReportPath(wave, { contQaAgentId })) {
|
|
1620
|
+
errors.push(`Agent ${contQaAgentId} must own a cont-QA report path`);
|
|
1621
|
+
}
|
|
1622
|
+
if (contEvalAgent) {
|
|
1623
|
+
if (!contEvalAgent.rolePromptPaths?.includes(laneProfile.roles.contEvalRolePromptPath)) {
|
|
1624
|
+
errors.push(
|
|
1625
|
+
`Agent ${contEvalAgentId} must import ${laneProfile.roles.contEvalRolePromptPath}`,
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
if (!resolveContEvalReportPath(wave, { contEvalAgentId })) {
|
|
1629
|
+
errors.push(`Agent ${contEvalAgentId} must own a cont-EVAL report path`);
|
|
1630
|
+
}
|
|
1631
|
+
if (!Array.isArray(wave.evalTargets) || wave.evalTargets.length === 0) {
|
|
1632
|
+
errors.push(`Wave ${wave.wave} must declare a ## Eval targets section when ${contEvalAgentId} is present`);
|
|
1633
|
+
} else {
|
|
1634
|
+
try {
|
|
1635
|
+
validateEvalTargets(wave.evalTargets, {
|
|
1636
|
+
benchmarkCatalogPath: laneProfile.paths.benchmarkCatalogPath,
|
|
1637
|
+
});
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
errors.push(error.message);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
} else if (Array.isArray(wave.evalTargets) && wave.evalTargets.length > 0) {
|
|
1643
|
+
errors.push(`Wave ${wave.wave} declares ## Eval targets but does not include Agent ${contEvalAgentId}`);
|
|
1644
|
+
}
|
|
1645
|
+
const securityReviewers = wave.agents.filter((agent) =>
|
|
1646
|
+
isSecurityReviewAgent(agent, { securityRolePromptPath }),
|
|
1647
|
+
);
|
|
1648
|
+
for (const securityReviewer of securityReviewers) {
|
|
1649
|
+
if (!securityReviewer.rolePromptPaths?.includes(securityRolePromptPath)) {
|
|
1650
|
+
errors.push(
|
|
1651
|
+
`Security reviewer ${securityReviewer.agentId} must import ${securityRolePromptPath}`,
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
if (!resolveSecurityReviewReportPath(securityReviewer)) {
|
|
1655
|
+
errors.push(`Security reviewer ${securityReviewer.agentId} must own a security review report path`);
|
|
1656
|
+
}
|
|
1264
1657
|
}
|
|
1265
1658
|
if (integrationRuleActive) {
|
|
1266
1659
|
const integrationStewards = wave.agents.filter((agent) =>
|
|
@@ -1317,8 +1710,13 @@ export function validateWaveDefinition(wave, options = {}) {
|
|
|
1317
1710
|
}
|
|
1318
1711
|
for (const [componentId, owners] of componentOwners.entries()) {
|
|
1319
1712
|
if (owners.size === 0) {
|
|
1713
|
+
const requiredOwnerIds = [
|
|
1714
|
+
contQaAgentId,
|
|
1715
|
+
...(contEvalImplementationOwning ? [] : [contEvalAgentId]),
|
|
1716
|
+
documentationAgentId,
|
|
1717
|
+
];
|
|
1320
1718
|
errors.push(
|
|
1321
|
-
`Wave ${wave.wave} must assign promoted component "${componentId}" to at least one non-${
|
|
1719
|
+
`Wave ${wave.wave} must assign promoted component "${componentId}" to at least one non-${requiredOwnerIds.join("/")} agent`,
|
|
1322
1720
|
);
|
|
1323
1721
|
}
|
|
1324
1722
|
}
|
|
@@ -1330,6 +1728,7 @@ export function validateWaveDefinition(wave, options = {}) {
|
|
|
1330
1728
|
|
|
1331
1729
|
export function parseWaveContent(content, filePath, options = {}) {
|
|
1332
1730
|
const laneProfile = resolveLaneProfileForOptions(options);
|
|
1731
|
+
const securityRolePromptPath = resolveSecurityRolePromptPath(laneProfile);
|
|
1333
1732
|
const fileName = path.basename(filePath);
|
|
1334
1733
|
const waveNumber = waveNumberFromFileName(fileName);
|
|
1335
1734
|
const commitMessageMatch = content.match(/\*\*Commit message\*\*:\s*`([^`]+)`/);
|
|
@@ -1358,13 +1757,22 @@ export function parseWaveContent(content, filePath, options = {}) {
|
|
|
1358
1757
|
const exitContract = extractExitContractFromSection(sectionText, filePath, current.agentId);
|
|
1359
1758
|
const executorConfig = extractExecutorConfigFromSection(sectionText, filePath, current.agentId);
|
|
1360
1759
|
const components = extractAgentComponentsFromSection(sectionText, filePath, current.agentId);
|
|
1361
|
-
const capabilities =
|
|
1760
|
+
const capabilities = normalizeSecurityCapabilities(
|
|
1761
|
+
extractAgentCapabilitiesFromSection(sectionText, filePath, current.agentId),
|
|
1762
|
+
rolePromptPaths,
|
|
1763
|
+
securityRolePromptPath,
|
|
1764
|
+
);
|
|
1362
1765
|
const skills = extractAgentSkillsFromSection(sectionText, filePath, current.agentId);
|
|
1363
1766
|
const deliverables = extractAgentDeliverablesFromSection(
|
|
1364
1767
|
sectionText,
|
|
1365
1768
|
filePath,
|
|
1366
1769
|
current.agentId,
|
|
1367
1770
|
);
|
|
1771
|
+
const proofArtifacts = extractAgentProofArtifactsFromSection(
|
|
1772
|
+
sectionText,
|
|
1773
|
+
filePath,
|
|
1774
|
+
current.agentId,
|
|
1775
|
+
);
|
|
1368
1776
|
const promptOverlay = extractPromptFromSection(sectionText, filePath, current.agentId);
|
|
1369
1777
|
const prompt = composeResolvedPrompt(
|
|
1370
1778
|
rolePromptPaths,
|
|
@@ -1377,6 +1785,7 @@ export function parseWaveContent(content, filePath, options = {}) {
|
|
|
1377
1785
|
);
|
|
1378
1786
|
const ownedPaths = extractOwnedPaths(promptOverlay);
|
|
1379
1787
|
validateAgentDeliverables(deliverables, ownedPaths, filePath, current.agentId);
|
|
1788
|
+
validateAgentProofArtifacts(proofArtifacts, ownedPaths, filePath, current.agentId);
|
|
1380
1789
|
agents.push({
|
|
1381
1790
|
agentId: current.agentId,
|
|
1382
1791
|
title: current.title,
|
|
@@ -1391,6 +1800,7 @@ export function parseWaveContent(content, filePath, options = {}) {
|
|
|
1391
1800
|
capabilities,
|
|
1392
1801
|
skills,
|
|
1393
1802
|
deliverables,
|
|
1803
|
+
proofArtifacts,
|
|
1394
1804
|
ownedPaths,
|
|
1395
1805
|
});
|
|
1396
1806
|
}
|
|
@@ -1416,12 +1826,22 @@ export function parseWaveContent(content, filePath, options = {}) {
|
|
|
1416
1826
|
}),
|
|
1417
1827
|
filePath,
|
|
1418
1828
|
),
|
|
1829
|
+
evalTargets: parseEvalTargets(
|
|
1830
|
+
extractTopLevelSectionBody(content, "Eval targets", filePath, {
|
|
1831
|
+
required: false,
|
|
1832
|
+
}),
|
|
1833
|
+
filePath,
|
|
1834
|
+
),
|
|
1419
1835
|
context7Defaults: extractWaveContext7Defaults(content, filePath),
|
|
1420
1836
|
componentPromotions,
|
|
1421
1837
|
agents: agentsWithComponentTargets,
|
|
1422
|
-
|
|
1838
|
+
contQaReportPath: resolveContQaReportPath(
|
|
1839
|
+
{ agents: agentsWithComponentTargets },
|
|
1840
|
+
{ contQaAgentId: laneProfile.roles.contQaAgentId },
|
|
1841
|
+
),
|
|
1842
|
+
contEvalReportPath: resolveContEvalReportPath(
|
|
1423
1843
|
{ agents: agentsWithComponentTargets },
|
|
1424
|
-
{
|
|
1844
|
+
{ contEvalAgentId: laneProfile.roles.contEvalAgentId },
|
|
1425
1845
|
),
|
|
1426
1846
|
};
|
|
1427
1847
|
}
|
|
@@ -1459,8 +1879,11 @@ function mergeExecutorSections(baseSection, profileSection, inlineSection, array
|
|
|
1459
1879
|
}
|
|
1460
1880
|
|
|
1461
1881
|
function inferAgentRuntimeRole(agent, laneProfile) {
|
|
1462
|
-
if (agent?.agentId === laneProfile.roles.
|
|
1463
|
-
return "
|
|
1882
|
+
if (agent?.agentId === laneProfile.roles.contQaAgentId) {
|
|
1883
|
+
return "cont-qa";
|
|
1884
|
+
}
|
|
1885
|
+
if (agent?.agentId === laneProfile.roles.contEvalAgentId) {
|
|
1886
|
+
return "cont-eval";
|
|
1464
1887
|
}
|
|
1465
1888
|
if (agent?.agentId === laneProfile.roles.integrationAgentId) {
|
|
1466
1889
|
return "integration";
|
|
@@ -1468,6 +1891,9 @@ function inferAgentRuntimeRole(agent, laneProfile) {
|
|
|
1468
1891
|
if (agent?.agentId === laneProfile.roles.documentationAgentId) {
|
|
1469
1892
|
return "documentation";
|
|
1470
1893
|
}
|
|
1894
|
+
if (isSecurityReviewAgent(agent)) {
|
|
1895
|
+
return "security";
|
|
1896
|
+
}
|
|
1471
1897
|
const capabilities = Array.isArray(agent?.capabilities)
|
|
1472
1898
|
? agent.capabilities.map((entry) => String(entry || "").trim().toLowerCase())
|
|
1473
1899
|
: [];
|
|
@@ -1535,6 +1961,8 @@ export function resolveAgentExecutor(agent, options = {}) {
|
|
|
1535
1961
|
const laneProfile = resolveLaneProfileForOptions(options);
|
|
1536
1962
|
const executorConfig = agent?.executorConfig || null;
|
|
1537
1963
|
const role = inferAgentRuntimeRole(agent, laneProfile);
|
|
1964
|
+
const proofCentricAgent =
|
|
1965
|
+
agentRequiresProofCentricValidation(agent) || waveRequiresProofCentricValidation(options.wave);
|
|
1538
1966
|
const profileName = executorConfig?.profile || null;
|
|
1539
1967
|
if (profileName && !laneProfile.executors.profiles?.[profileName]) {
|
|
1540
1968
|
throw new Error(
|
|
@@ -1574,12 +2002,31 @@ export function resolveAgentExecutor(agent, options = {}) {
|
|
|
1574
2002
|
profile?.fallbacks,
|
|
1575
2003
|
executorConfig?.fallbacks,
|
|
1576
2004
|
);
|
|
2005
|
+
const explicitAllowFallback =
|
|
2006
|
+
executorConfig?.allowFallbackOnRetry ??
|
|
2007
|
+
profile?.allowFallbackOnRetry ??
|
|
2008
|
+
null;
|
|
2009
|
+
const explicitRetryPolicy =
|
|
2010
|
+
executorConfig?.retryPolicy ||
|
|
2011
|
+
profile?.retryPolicy ||
|
|
2012
|
+
null;
|
|
2013
|
+
const allowFallbackOnRetry =
|
|
2014
|
+
explicitAllowFallback !== null
|
|
2015
|
+
? explicitAllowFallback
|
|
2016
|
+
: explicitRetryPolicy
|
|
2017
|
+
? explicitRetryPolicy !== "sticky"
|
|
2018
|
+
: !proofCentricAgent;
|
|
2019
|
+
const retryPolicy =
|
|
2020
|
+
explicitRetryPolicy ||
|
|
2021
|
+
(allowFallbackOnRetry ? "fallback-allowed" : "sticky");
|
|
1577
2022
|
const runtimeFallbacks =
|
|
1578
|
-
fallbacks.length > 0
|
|
2023
|
+
allowFallbackOnRetry && fallbacks.length > 0
|
|
1579
2024
|
? fallbacks
|
|
1580
|
-
:
|
|
2025
|
+
: allowFallbackOnRetry
|
|
2026
|
+
? (laneProfile.runtimePolicy?.fallbackExecutorOrder || []).filter(
|
|
1581
2027
|
(candidate) => candidate !== executorId,
|
|
1582
|
-
)
|
|
2028
|
+
)
|
|
2029
|
+
: [];
|
|
1583
2030
|
const runtimeTags = mergeUniqueStringArrays(profile?.tags, executorConfig?.tags);
|
|
1584
2031
|
const runtimeBudget = {
|
|
1585
2032
|
turns:
|
|
@@ -1600,6 +2047,8 @@ export function resolveAgentExecutor(agent, options = {}) {
|
|
|
1600
2047
|
selectedBy,
|
|
1601
2048
|
fallbacks: runtimeFallbacks,
|
|
1602
2049
|
tags: runtimeTags,
|
|
2050
|
+
retryPolicy,
|
|
2051
|
+
allowFallbackOnRetry,
|
|
1603
2052
|
budget:
|
|
1604
2053
|
runtimeBudget.turns !== null || runtimeBudget.minutes !== null ? runtimeBudget : null,
|
|
1605
2054
|
fallbackUsed: false,
|
|
@@ -1742,12 +2191,12 @@ export function buildManifest(lanePaths, waves) {
|
|
|
1742
2191
|
})
|
|
1743
2192
|
.toSorted((a, b) => a.path.localeCompare(b.path));
|
|
1744
2193
|
|
|
1745
|
-
return {
|
|
2194
|
+
return normalizeManifest({
|
|
1746
2195
|
generatedAt: new Date().toISOString(),
|
|
1747
2196
|
source: `${path.relative(REPO_ROOT, lanePaths.docsDir).replaceAll(path.sep, "/")}/**/*`,
|
|
1748
2197
|
waves,
|
|
1749
2198
|
docs,
|
|
1750
|
-
};
|
|
2199
|
+
});
|
|
1751
2200
|
}
|
|
1752
2201
|
|
|
1753
2202
|
export function validateWaveComponentPromotions(wave, summariesByAgentId = {}, options = {}) {
|
|
@@ -1762,21 +2211,43 @@ export function validateWaveComponentPromotions(wave, summariesByAgentId = {}, o
|
|
|
1762
2211
|
};
|
|
1763
2212
|
}
|
|
1764
2213
|
const promotions = Array.isArray(wave.componentPromotions) ? wave.componentPromotions : [];
|
|
2214
|
+
const roles = laneProfile.roles || {};
|
|
2215
|
+
const contQaAgentId = roles.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
|
|
2216
|
+
const contEvalAgentId = roles.contEvalAgentId || DEFAULT_CONT_EVAL_AGENT_ID;
|
|
2217
|
+
const integrationAgentId = roles.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID;
|
|
2218
|
+
const documentationAgentId =
|
|
2219
|
+
roles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
|
|
2220
|
+
const securityRolePromptPath = resolveSecurityRolePromptPath(laneProfile);
|
|
2221
|
+
const implementationOwningAgents = (wave.agents || []).filter((agent) =>
|
|
2222
|
+
isImplementationOwningWaveAgent(agent, {
|
|
2223
|
+
contQaAgentId,
|
|
2224
|
+
contEvalAgentId,
|
|
2225
|
+
integrationAgentId,
|
|
2226
|
+
documentationAgentId,
|
|
2227
|
+
securityRolePromptPath,
|
|
2228
|
+
}),
|
|
2229
|
+
);
|
|
1765
2230
|
if (promotions.length === 0) {
|
|
1766
2231
|
return {
|
|
1767
|
-
ok:
|
|
1768
|
-
statusCode:
|
|
1769
|
-
|
|
2232
|
+
ok: implementationOwningAgents.length === 0,
|
|
2233
|
+
statusCode:
|
|
2234
|
+
implementationOwningAgents.length === 0 ? "pass" : "missing-component-promotions",
|
|
2235
|
+
detail:
|
|
2236
|
+
implementationOwningAgents.length === 0
|
|
2237
|
+
? `Wave ${wave.wave} has no implementation-owned component promotions to prove.`
|
|
2238
|
+
: `Wave ${wave.wave} is missing component promotions.`,
|
|
1770
2239
|
componentId: null,
|
|
1771
2240
|
};
|
|
1772
2241
|
}
|
|
1773
|
-
const roles = laneProfile.roles || {};
|
|
1774
|
-
const evaluatorAgentId = roles.evaluatorAgentId || DEFAULT_EVALUATOR_AGENT_ID;
|
|
1775
|
-
const documentationAgentId =
|
|
1776
|
-
roles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
|
|
1777
2242
|
const satisfied = new Set();
|
|
1778
2243
|
for (const agent of wave.agents) {
|
|
1779
|
-
if (
|
|
2244
|
+
if (!isImplementationOwningWaveAgent(agent, {
|
|
2245
|
+
contQaAgentId,
|
|
2246
|
+
contEvalAgentId,
|
|
2247
|
+
integrationAgentId,
|
|
2248
|
+
documentationAgentId,
|
|
2249
|
+
securityRolePromptPath,
|
|
2250
|
+
})) {
|
|
1780
2251
|
continue;
|
|
1781
2252
|
}
|
|
1782
2253
|
const summary = summariesByAgentId[agent.agentId] || null;
|
|
@@ -1815,14 +2286,33 @@ export function validateWaveComponentMatrixCurrentLevels(wave, options = {}) {
|
|
|
1815
2286
|
const laneProfile = resolveLaneProfileForOptions(options);
|
|
1816
2287
|
const componentThreshold = laneProfile.validation.requireComponentPromotionsFromWave;
|
|
1817
2288
|
const promotions = Array.isArray(wave.componentPromotions) ? wave.componentPromotions : [];
|
|
2289
|
+
const roles = laneProfile.roles || {};
|
|
2290
|
+
const contQaAgentId = roles.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
|
|
2291
|
+
const contEvalAgentId = roles.contEvalAgentId || DEFAULT_CONT_EVAL_AGENT_ID;
|
|
2292
|
+
const integrationAgentId = roles.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID;
|
|
2293
|
+
const documentationAgentId = roles.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
|
|
2294
|
+
const securityRolePromptPath = resolveSecurityRolePromptPath(laneProfile);
|
|
2295
|
+
const implementationOwningAgents = (wave.agents || []).filter((agent) =>
|
|
2296
|
+
isImplementationOwningWaveAgent(agent, {
|
|
2297
|
+
contQaAgentId,
|
|
2298
|
+
contEvalAgentId,
|
|
2299
|
+
integrationAgentId,
|
|
2300
|
+
documentationAgentId,
|
|
2301
|
+
securityRolePromptPath,
|
|
2302
|
+
}),
|
|
2303
|
+
);
|
|
1818
2304
|
if (
|
|
1819
2305
|
promotions.length === 0 &&
|
|
1820
|
-
(componentThreshold === null || wave.wave < componentThreshold)
|
|
2306
|
+
((componentThreshold === null || wave.wave < componentThreshold) ||
|
|
2307
|
+
implementationOwningAgents.length === 0)
|
|
1821
2308
|
) {
|
|
1822
2309
|
return {
|
|
1823
2310
|
ok: true,
|
|
1824
2311
|
statusCode: "pass",
|
|
1825
|
-
detail:
|
|
2312
|
+
detail:
|
|
2313
|
+
implementationOwningAgents.length === 0
|
|
2314
|
+
? `Wave ${wave.wave} has no implementation-owned component promotions to reconcile.`
|
|
2315
|
+
: "Component current-level gate is not active for this wave.",
|
|
1826
2316
|
componentId: null,
|
|
1827
2317
|
};
|
|
1828
2318
|
}
|
|
@@ -1861,7 +2351,7 @@ export function validateWaveComponentMatrixCurrentLevels(wave, options = {}) {
|
|
|
1861
2351
|
}
|
|
1862
2352
|
|
|
1863
2353
|
export function writeManifest(manifestPath, manifest) {
|
|
1864
|
-
writeJsonAtomic(manifestPath, manifest);
|
|
2354
|
+
writeJsonAtomic(manifestPath, normalizeManifest(manifest));
|
|
1865
2355
|
}
|
|
1866
2356
|
|
|
1867
2357
|
export function normalizeCompletedWaves(values) {
|
|
@@ -1877,28 +2367,253 @@ export function normalizeCompletedWaves(values) {
|
|
|
1877
2367
|
).toSorted((a, b) => a - b);
|
|
1878
2368
|
}
|
|
1879
2369
|
|
|
1880
|
-
|
|
1881
|
-
|
|
2370
|
+
function fileHashOrNull(filePath) {
|
|
2371
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
2372
|
+
return null;
|
|
2373
|
+
}
|
|
2374
|
+
return hashText(fs.readFileSync(filePath, "utf8"));
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function relativeRepoPathOrNull(filePath) {
|
|
2378
|
+
return filePath ? path.relative(REPO_ROOT, filePath) : null;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function normalizeRunStateWaveEntry(rawEntry, waveNumber) {
|
|
2382
|
+
const source = rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {};
|
|
2383
|
+
const normalizedWave = normalizeCompletedWaves([waveNumber])[0] ?? normalizeCompletedWaves([source.wave])[0] ?? null;
|
|
2384
|
+
return {
|
|
2385
|
+
wave: normalizedWave,
|
|
2386
|
+
currentState: String(source.currentState || "completed").trim().toLowerCase() || "completed",
|
|
2387
|
+
lastTransitionAt:
|
|
2388
|
+
typeof source.lastTransitionAt === "string"
|
|
2389
|
+
? source.lastTransitionAt
|
|
2390
|
+
: typeof source.updatedAt === "string"
|
|
2391
|
+
? source.updatedAt
|
|
2392
|
+
: typeof source.completedAt === "string"
|
|
2393
|
+
? source.completedAt
|
|
2394
|
+
: null,
|
|
2395
|
+
lastSource: typeof source.lastSource === "string" ? source.lastSource : null,
|
|
2396
|
+
lastReasonCode: typeof source.lastReasonCode === "string" ? source.lastReasonCode : null,
|
|
2397
|
+
lastDetail: typeof source.lastDetail === "string" ? source.lastDetail : "",
|
|
2398
|
+
lastEvidence:
|
|
2399
|
+
source.lastEvidence && typeof source.lastEvidence === "object" && !Array.isArray(source.lastEvidence)
|
|
2400
|
+
? source.lastEvidence
|
|
2401
|
+
: null,
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
function normalizeRunStateHistoryEntry(rawEntry, seqFallback) {
|
|
2406
|
+
const source = rawEntry && typeof rawEntry === "object" && !Array.isArray(rawEntry) ? rawEntry : {};
|
|
2407
|
+
return {
|
|
2408
|
+
seq: normalizeCompletedWaves([source.seq])[0] ?? seqFallback,
|
|
2409
|
+
at: typeof source.at === "string" ? source.at : toIsoTimestamp(),
|
|
2410
|
+
wave: normalizeCompletedWaves([source.wave])[0] ?? null,
|
|
2411
|
+
fromState: typeof source.fromState === "string" ? source.fromState : null,
|
|
2412
|
+
toState: typeof source.toState === "string" ? source.toState : null,
|
|
2413
|
+
source: typeof source.source === "string" ? source.source : null,
|
|
2414
|
+
reasonCode: typeof source.reasonCode === "string" ? source.reasonCode : null,
|
|
2415
|
+
detail: typeof source.detail === "string" ? source.detail : "",
|
|
2416
|
+
evidence:
|
|
2417
|
+
source.evidence && typeof source.evidence === "object" && !Array.isArray(source.evidence)
|
|
2418
|
+
? source.evidence
|
|
2419
|
+
: null,
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function completedWavesFromStateEntries(waves) {
|
|
2424
|
+
return normalizeCompletedWaves(
|
|
2425
|
+
Object.values(waves || {})
|
|
2426
|
+
.filter((entry) => entry?.currentState === "completed")
|
|
2427
|
+
.map((entry) => entry.wave),
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
function normalizeRunStateWaves(rawWaves, completedWaves, lastUpdatedAt) {
|
|
2432
|
+
const normalized = {};
|
|
2433
|
+
for (const waveNumber of completedWaves) {
|
|
2434
|
+
normalized[String(waveNumber)] = {
|
|
2435
|
+
wave: waveNumber,
|
|
2436
|
+
currentState: "completed",
|
|
2437
|
+
lastTransitionAt: lastUpdatedAt,
|
|
2438
|
+
lastSource: "legacy-run-state",
|
|
2439
|
+
lastReasonCode: "legacy-completed-wave",
|
|
2440
|
+
lastDetail: "Imported from legacy completedWaves state.",
|
|
2441
|
+
lastEvidence: null,
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
if (!rawWaves || typeof rawWaves !== "object" || Array.isArray(rawWaves)) {
|
|
2445
|
+
return normalized;
|
|
2446
|
+
}
|
|
2447
|
+
for (const [waveKey, rawEntry] of Object.entries(rawWaves)) {
|
|
2448
|
+
const waveNumber = normalizeCompletedWaves([waveKey])[0];
|
|
2449
|
+
if (waveNumber === undefined) {
|
|
2450
|
+
continue;
|
|
2451
|
+
}
|
|
2452
|
+
normalized[String(waveNumber)] = normalizeRunStateWaveEntry(rawEntry, waveNumber);
|
|
2453
|
+
}
|
|
2454
|
+
return normalized;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function normalizeRunStateInternal(payload) {
|
|
2458
|
+
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
2459
|
+
const completedWaves = normalizeCompletedWaves(source.completedWaves);
|
|
2460
|
+
const lastUpdatedAt = typeof source.lastUpdatedAt === "string" ? source.lastUpdatedAt : undefined;
|
|
2461
|
+
const waves = normalizeRunStateWaves(source.waves, completedWaves, lastUpdatedAt);
|
|
2462
|
+
const history = Array.isArray(source.history)
|
|
2463
|
+
? source.history
|
|
2464
|
+
.map((entry, index) => normalizeRunStateHistoryEntry(entry, index + 1))
|
|
2465
|
+
.filter((entry) => Number.isFinite(entry.seq) && entry.wave !== null)
|
|
2466
|
+
: [];
|
|
1882
2467
|
return {
|
|
1883
|
-
|
|
1884
|
-
|
|
2468
|
+
schemaVersion: RUN_STATE_SCHEMA_VERSION,
|
|
2469
|
+
kind: RUN_STATE_KIND,
|
|
2470
|
+
completedWaves: completedWavesFromStateEntries(waves),
|
|
2471
|
+
lastUpdatedAt,
|
|
2472
|
+
waves,
|
|
2473
|
+
history,
|
|
1885
2474
|
};
|
|
1886
2475
|
}
|
|
1887
2476
|
|
|
2477
|
+
export function readRunState(runStatePath) {
|
|
2478
|
+
return normalizeRunStateInternal(readJsonOrNull(runStatePath));
|
|
2479
|
+
}
|
|
2480
|
+
|
|
1888
2481
|
export function writeRunState(runStatePath, state) {
|
|
1889
2482
|
ensureDirectory(path.dirname(runStatePath));
|
|
2483
|
+
const normalized = normalizeRunStateInternal(state);
|
|
1890
2484
|
const payload = {
|
|
1891
|
-
|
|
2485
|
+
...normalized,
|
|
2486
|
+
completedWaves: completedWavesFromStateEntries(normalized.waves),
|
|
1892
2487
|
lastUpdatedAt: new Date().toISOString(),
|
|
1893
2488
|
};
|
|
1894
2489
|
writeJsonAtomic(runStatePath, payload);
|
|
1895
2490
|
return payload;
|
|
1896
2491
|
}
|
|
1897
2492
|
|
|
1898
|
-
|
|
2493
|
+
function nextRunStateSequence(history) {
|
|
2494
|
+
return (history || []).reduce((max, entry) => Math.max(max, Number(entry?.seq) || 0), 0) + 1;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
function appendRunStateTransition(state, {
|
|
2498
|
+
waveNumber,
|
|
2499
|
+
toState,
|
|
2500
|
+
source,
|
|
2501
|
+
reasonCode,
|
|
2502
|
+
detail,
|
|
2503
|
+
evidence = null,
|
|
2504
|
+
at = toIsoTimestamp(),
|
|
2505
|
+
}) {
|
|
2506
|
+
const nextState = normalizeRunStateInternal(state);
|
|
2507
|
+
const waveKey = String(waveNumber);
|
|
2508
|
+
const previousEntry = nextState.waves[waveKey] || null;
|
|
2509
|
+
const currentState = previousEntry?.currentState || null;
|
|
2510
|
+
const currentEvidence = previousEntry?.lastEvidence || null;
|
|
2511
|
+
const effectiveDetail = String(detail || "").trim();
|
|
2512
|
+
const effectiveEvidence =
|
|
2513
|
+
evidence && typeof evidence === "object" && !Array.isArray(evidence) ? evidence : null;
|
|
2514
|
+
if (
|
|
2515
|
+
previousEntry &&
|
|
2516
|
+
currentState === toState &&
|
|
2517
|
+
previousEntry.lastSource === source &&
|
|
2518
|
+
previousEntry.lastReasonCode === reasonCode &&
|
|
2519
|
+
previousEntry.lastDetail === effectiveDetail &&
|
|
2520
|
+
JSON.stringify(currentEvidence || null) === JSON.stringify(effectiveEvidence || null)
|
|
2521
|
+
) {
|
|
2522
|
+
return nextState;
|
|
2523
|
+
}
|
|
2524
|
+
const historyEntry = {
|
|
2525
|
+
seq: nextRunStateSequence(nextState.history),
|
|
2526
|
+
at,
|
|
2527
|
+
wave: waveNumber,
|
|
2528
|
+
fromState: currentState,
|
|
2529
|
+
toState,
|
|
2530
|
+
source,
|
|
2531
|
+
reasonCode,
|
|
2532
|
+
detail: effectiveDetail,
|
|
2533
|
+
evidence: effectiveEvidence,
|
|
2534
|
+
};
|
|
2535
|
+
nextState.waves[waveKey] = {
|
|
2536
|
+
wave: waveNumber,
|
|
2537
|
+
currentState: toState,
|
|
2538
|
+
lastTransitionAt: at,
|
|
2539
|
+
lastSource: source,
|
|
2540
|
+
lastReasonCode: reasonCode,
|
|
2541
|
+
lastDetail: effectiveDetail,
|
|
2542
|
+
lastEvidence: effectiveEvidence,
|
|
2543
|
+
};
|
|
2544
|
+
nextState.history = [...nextState.history, historyEntry];
|
|
2545
|
+
nextState.completedWaves = completedWavesFromStateEntries(nextState.waves);
|
|
2546
|
+
return nextState;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
export function buildRunStateEvidence({
|
|
2550
|
+
wave,
|
|
2551
|
+
agentRuns = [],
|
|
2552
|
+
statusEntries = [],
|
|
2553
|
+
coordinationLogPath = null,
|
|
2554
|
+
assignmentsPath = null,
|
|
2555
|
+
dependencySnapshotPath = null,
|
|
2556
|
+
gateSnapshot = null,
|
|
2557
|
+
traceDir = null,
|
|
2558
|
+
blockedReasons = [],
|
|
2559
|
+
}) {
|
|
2560
|
+
const observations =
|
|
2561
|
+
statusEntries.length > 0
|
|
2562
|
+
? statusEntries
|
|
2563
|
+
: agentRuns.map((run) => ({
|
|
2564
|
+
agentId: run.agent?.agentId || null,
|
|
2565
|
+
statusPath: run.statusPath,
|
|
2566
|
+
summaryPath: agentSummaryPathFromStatusPath(run.statusPath),
|
|
2567
|
+
statusRecord: readStatusRecordIfPresent(run.statusPath),
|
|
2568
|
+
}));
|
|
2569
|
+
return {
|
|
2570
|
+
waveFileHash: wave?.file ? fileHashOrNull(path.resolve(REPO_ROOT, wave.file)) : null,
|
|
2571
|
+
traceDir: relativeRepoPathOrNull(traceDir),
|
|
2572
|
+
statusFiles: observations
|
|
2573
|
+
.filter((entry) => entry?.statusPath)
|
|
2574
|
+
.map((entry) => ({
|
|
2575
|
+
agentId: entry.agentId || null,
|
|
2576
|
+
path: relativeRepoPathOrNull(entry.statusPath),
|
|
2577
|
+
promptHash: entry.statusRecord?.promptHash || null,
|
|
2578
|
+
code:
|
|
2579
|
+
entry.statusRecord && Number.isFinite(Number(entry.statusRecord.code))
|
|
2580
|
+
? Number(entry.statusRecord.code)
|
|
2581
|
+
: null,
|
|
2582
|
+
completedAt: entry.statusRecord?.completedAt || null,
|
|
2583
|
+
sha256: fileHashOrNull(entry.statusPath),
|
|
2584
|
+
})),
|
|
2585
|
+
summaryFiles: observations
|
|
2586
|
+
.filter((entry) => entry?.summaryPath && fs.existsSync(entry.summaryPath))
|
|
2587
|
+
.map((entry) => ({
|
|
2588
|
+
agentId: entry.agentId || null,
|
|
2589
|
+
path: relativeRepoPathOrNull(entry.summaryPath),
|
|
2590
|
+
sha256: fileHashOrNull(entry.summaryPath),
|
|
2591
|
+
})),
|
|
2592
|
+
coordinationLogSha256: fileHashOrNull(coordinationLogPath),
|
|
2593
|
+
assignmentsSha256: fileHashOrNull(assignmentsPath),
|
|
2594
|
+
dependencySnapshotSha256: fileHashOrNull(dependencySnapshotPath),
|
|
2595
|
+
gateSnapshotSha256: gateSnapshot ? hashText(JSON.stringify(gateSnapshot)) : null,
|
|
2596
|
+
blockedReasons: Array.isArray(blockedReasons)
|
|
2597
|
+
? blockedReasons.map((reason) => ({
|
|
2598
|
+
code: String(reason?.code || "").trim(),
|
|
2599
|
+
detail: String(reason?.detail || "").trim(),
|
|
2600
|
+
}))
|
|
2601
|
+
: [],
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
export function markWaveCompleted(runStatePath, waveNumber, options = {}) {
|
|
1899
2606
|
const state = readRunState(runStatePath);
|
|
1900
|
-
|
|
1901
|
-
|
|
2607
|
+
const nextState = appendRunStateTransition(state, {
|
|
2608
|
+
waveNumber,
|
|
2609
|
+
toState: "completed",
|
|
2610
|
+
source: options.source || "live-launcher",
|
|
2611
|
+
reasonCode: options.reasonCode || "wave-complete",
|
|
2612
|
+
detail: options.detail || `Wave ${waveNumber} completed.`,
|
|
2613
|
+
evidence: options.evidence || null,
|
|
2614
|
+
at: options.at || toIsoTimestamp(),
|
|
2615
|
+
});
|
|
2616
|
+
return writeRunState(runStatePath, nextState);
|
|
1902
2617
|
}
|
|
1903
2618
|
|
|
1904
2619
|
export function resolveAutoNextWaveStart(allWaves, runStatePath) {
|
|
@@ -1924,52 +2639,51 @@ export function arraysEqual(a, b) {
|
|
|
1924
2639
|
return true;
|
|
1925
2640
|
}
|
|
1926
2641
|
|
|
1927
|
-
export function
|
|
1928
|
-
const
|
|
1929
|
-
const
|
|
1930
|
-
|
|
1931
|
-
if (!evaluator) {
|
|
2642
|
+
export function readWaveContQaArtifacts(wave, { logsDir, contQaAgentId } = {}) {
|
|
2643
|
+
const resolvedContQaAgentId = contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
|
|
2644
|
+
const contQa = wave.agents.find((agent) => agent.agentId === resolvedContQaAgentId) ?? null;
|
|
2645
|
+
if (!contQa) {
|
|
1932
2646
|
return {
|
|
1933
2647
|
ok: false,
|
|
1934
|
-
statusCode: "missing-
|
|
1935
|
-
detail: `Agent ${
|
|
2648
|
+
statusCode: "missing-cont-qa",
|
|
2649
|
+
detail: `Agent ${resolvedContQaAgentId} is missing.`,
|
|
1936
2650
|
};
|
|
1937
2651
|
}
|
|
1938
|
-
const
|
|
1939
|
-
? path.resolve(REPO_ROOT, wave.
|
|
2652
|
+
const contQaReportPath = wave.contQaReportPath
|
|
2653
|
+
? path.resolve(REPO_ROOT, wave.contQaReportPath)
|
|
1940
2654
|
: null;
|
|
1941
2655
|
const reportText =
|
|
1942
|
-
|
|
1943
|
-
? fs.readFileSync(
|
|
2656
|
+
contQaReportPath && fs.existsSync(contQaReportPath)
|
|
2657
|
+
? fs.readFileSync(contQaReportPath, "utf8")
|
|
1944
2658
|
: "";
|
|
1945
2659
|
const reportVerdict = parseVerdictFromText(reportText, REPORT_VERDICT_REGEX);
|
|
1946
2660
|
if (reportVerdict.verdict) {
|
|
1947
2661
|
return {
|
|
1948
2662
|
ok: reportVerdict.verdict === "pass",
|
|
1949
|
-
statusCode: reportVerdict.verdict === "pass" ? "pass" : `
|
|
1950
|
-
detail: reportVerdict.detail || "Verdict read from
|
|
2663
|
+
statusCode: reportVerdict.verdict === "pass" ? "pass" : `cont-qa-${reportVerdict.verdict}`,
|
|
2664
|
+
detail: reportVerdict.detail || "Verdict read from cont-QA report.",
|
|
1951
2665
|
};
|
|
1952
2666
|
}
|
|
1953
|
-
const
|
|
1954
|
-
? path.join(logsDir, `wave-${wave.wave}-${
|
|
2667
|
+
const contQaLogPath = logsDir
|
|
2668
|
+
? path.join(logsDir, `wave-${wave.wave}-${contQa.slug}.log`)
|
|
1955
2669
|
: null;
|
|
1956
2670
|
const logVerdict = parseVerdictFromText(
|
|
1957
|
-
|
|
2671
|
+
contQaLogPath ? readFileTail(contQaLogPath, 30000) : "",
|
|
1958
2672
|
WAVE_VERDICT_REGEX,
|
|
1959
2673
|
);
|
|
1960
2674
|
if (logVerdict.verdict) {
|
|
1961
2675
|
return {
|
|
1962
2676
|
ok: logVerdict.verdict === "pass",
|
|
1963
|
-
statusCode: logVerdict.verdict === "pass" ? "pass" : `
|
|
1964
|
-
detail: logVerdict.detail || "Verdict read from
|
|
2677
|
+
statusCode: logVerdict.verdict === "pass" ? "pass" : `cont-qa-${logVerdict.verdict}`,
|
|
2678
|
+
detail: logVerdict.detail || "Verdict read from cont-QA log marker.",
|
|
1965
2679
|
};
|
|
1966
2680
|
}
|
|
1967
2681
|
return {
|
|
1968
2682
|
ok: false,
|
|
1969
|
-
statusCode: "missing-
|
|
1970
|
-
detail:
|
|
1971
|
-
? `Missing
|
|
1972
|
-
: "Missing
|
|
2683
|
+
statusCode: "missing-cont-qa-verdict",
|
|
2684
|
+
detail: contQaReportPath
|
|
2685
|
+
? `Missing cont-QA verdict in ${path.relative(REPO_ROOT, contQaReportPath)}.`
|
|
2686
|
+
: "Missing cont-QA report path and cont-QA log verdict.",
|
|
1973
2687
|
};
|
|
1974
2688
|
}
|
|
1975
2689
|
|
|
@@ -1991,7 +2705,12 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
|
|
|
1991
2705
|
const logsDir = options.logsDir || path.join(path.resolve(statusDir, ".."), "logs");
|
|
1992
2706
|
const coordinationDir =
|
|
1993
2707
|
options.coordinationDir || path.join(path.resolve(statusDir, ".."), "coordination");
|
|
1994
|
-
const
|
|
2708
|
+
const assignmentsDir =
|
|
2709
|
+
options.assignmentsDir || path.join(path.resolve(statusDir, ".."), "assignments");
|
|
2710
|
+
const dependencySnapshotsDir =
|
|
2711
|
+
options.dependencySnapshotsDir || path.join(path.resolve(statusDir, ".."), "dependencies");
|
|
2712
|
+
const contQaAgentId = options.contQaAgentId || DEFAULT_CONT_QA_AGENT_ID;
|
|
2713
|
+
const contEvalAgentId = options.contEvalAgentId || DEFAULT_CONT_EVAL_AGENT_ID;
|
|
1995
2714
|
const integrationAgentId = options.integrationAgentId || DEFAULT_INTEGRATION_AGENT_ID;
|
|
1996
2715
|
const documentationAgentId =
|
|
1997
2716
|
options.documentationAgentId || DEFAULT_DOCUMENTATION_AGENT_ID;
|
|
@@ -2005,8 +2724,12 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
|
|
|
2005
2724
|
|
|
2006
2725
|
const reasons = [];
|
|
2007
2726
|
const summariesByAgentId = {};
|
|
2727
|
+
const statusEntries = [];
|
|
2008
2728
|
const missingStatusAgents = [];
|
|
2009
2729
|
let statusesReady = wave.agents.length > 0;
|
|
2730
|
+
const coordinationLogPath = path.join(coordinationDir, `wave-${wave.wave}.jsonl`);
|
|
2731
|
+
const assignmentsPath = path.join(assignmentsDir, `wave-${wave.wave}.json`);
|
|
2732
|
+
const dependencySnapshotPath = path.join(dependencySnapshotsDir, `wave-${wave.wave}.json`);
|
|
2010
2733
|
|
|
2011
2734
|
for (const agent of wave.agents) {
|
|
2012
2735
|
const statusPath = path.join(statusDir, `wave-${wave.wave}-${agent.slug}.status`);
|
|
@@ -2016,6 +2739,13 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
|
|
|
2016
2739
|
statusesReady = false;
|
|
2017
2740
|
continue;
|
|
2018
2741
|
}
|
|
2742
|
+
const summaryPath = agentSummaryPathFromStatusPath(statusPath);
|
|
2743
|
+
statusEntries.push({
|
|
2744
|
+
agentId: agent.agentId,
|
|
2745
|
+
statusPath,
|
|
2746
|
+
summaryPath,
|
|
2747
|
+
statusRecord,
|
|
2748
|
+
});
|
|
2019
2749
|
const expectedPromptHash = hashAgentPromptFingerprint(agent);
|
|
2020
2750
|
if (statusRecord.code !== 0) {
|
|
2021
2751
|
pushWaveCompletionReason(
|
|
@@ -2035,22 +2765,67 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
|
|
|
2035
2765
|
statusesReady = false;
|
|
2036
2766
|
continue;
|
|
2037
2767
|
}
|
|
2038
|
-
const summary =
|
|
2768
|
+
const summary = materializeLiveExecutionSummaryIfMissing({
|
|
2769
|
+
wave,
|
|
2770
|
+
agent,
|
|
2771
|
+
statusPath,
|
|
2772
|
+
statusRecord,
|
|
2773
|
+
logsDir,
|
|
2774
|
+
contQaAgentId,
|
|
2775
|
+
contEvalAgentId,
|
|
2776
|
+
});
|
|
2039
2777
|
summariesByAgentId[agent.agentId] = summary;
|
|
2040
|
-
if (agent.agentId ===
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2778
|
+
if (agent.agentId === contQaAgentId) {
|
|
2779
|
+
const validation = validateContQaSummary(agent, summary, { mode: "live" });
|
|
2780
|
+
if (!validation.ok) {
|
|
2781
|
+
pushWaveCompletionReason(
|
|
2782
|
+
reasons,
|
|
2783
|
+
"invalid-cont-qa-summary",
|
|
2784
|
+
`${agent.agentId}: ${validation.statusCode}: ${validation.detail}`,
|
|
2785
|
+
);
|
|
2786
|
+
statusesReady = false;
|
|
2787
|
+
}
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
if (agent.agentId === contEvalAgentId) {
|
|
2791
|
+
const evalValidation = validateContEvalSummary(agent, summary, {
|
|
2792
|
+
mode: "live",
|
|
2793
|
+
evalTargets: wave.evalTargets,
|
|
2794
|
+
benchmarkCatalogPath: laneProfile.paths.benchmarkCatalogPath,
|
|
2795
|
+
});
|
|
2796
|
+
if (!evalValidation.ok) {
|
|
2797
|
+
pushWaveCompletionReason(
|
|
2798
|
+
reasons,
|
|
2799
|
+
"invalid-cont-eval-summary",
|
|
2800
|
+
`${agent.agentId}: ${evalValidation.statusCode}: ${evalValidation.detail}`,
|
|
2801
|
+
);
|
|
2802
|
+
statusesReady = false;
|
|
2803
|
+
}
|
|
2804
|
+
if (isContEvalImplementationOwningAgent(agent, { contEvalAgentId })) {
|
|
2805
|
+
const implementationValidation = validateImplementationSummary(agent, summary);
|
|
2806
|
+
if (!implementationValidation.ok) {
|
|
2044
2807
|
pushWaveCompletionReason(
|
|
2045
2808
|
reasons,
|
|
2046
|
-
"invalid-
|
|
2047
|
-
`${agent.agentId}: ${
|
|
2809
|
+
"invalid-cont-eval-implementation-summary",
|
|
2810
|
+
`${agent.agentId}: ${implementationValidation.statusCode}: ${implementationValidation.detail}`,
|
|
2048
2811
|
);
|
|
2049
2812
|
statusesReady = false;
|
|
2050
2813
|
}
|
|
2051
2814
|
}
|
|
2052
2815
|
continue;
|
|
2053
2816
|
}
|
|
2817
|
+
if (isSecurityReviewAgent(agent)) {
|
|
2818
|
+
const validation = validateSecuritySummary(agent, summary);
|
|
2819
|
+
if (!validation.ok) {
|
|
2820
|
+
pushWaveCompletionReason(
|
|
2821
|
+
reasons,
|
|
2822
|
+
"invalid-security-summary",
|
|
2823
|
+
`${agent.agentId}: ${validation.statusCode}: ${validation.detail}`,
|
|
2824
|
+
);
|
|
2825
|
+
statusesReady = false;
|
|
2826
|
+
}
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2054
2829
|
if (
|
|
2055
2830
|
agent.agentId === integrationAgentId &&
|
|
2056
2831
|
integrationThreshold !== null &&
|
|
@@ -2126,19 +2901,8 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
|
|
|
2126
2901
|
}
|
|
2127
2902
|
}
|
|
2128
2903
|
|
|
2129
|
-
if (statusesReady) {
|
|
2130
|
-
const evaluatorArtifacts = readWaveEvaluatorArtifacts(wave, {
|
|
2131
|
-
logsDir,
|
|
2132
|
-
evaluatorAgentId,
|
|
2133
|
-
});
|
|
2134
|
-
if (!evaluatorArtifacts.ok) {
|
|
2135
|
-
pushWaveCompletionReason(reasons, evaluatorArtifacts.statusCode, evaluatorArtifacts.detail);
|
|
2136
|
-
statusesReady = false;
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
2904
|
const coordinationState = readMaterializedCoordinationState(
|
|
2141
|
-
|
|
2905
|
+
coordinationLogPath,
|
|
2142
2906
|
);
|
|
2143
2907
|
const openClarificationIds = coordinationState.clarifications
|
|
2144
2908
|
.filter((record) => isOpenCoordinationStatus(record.status))
|
|
@@ -2180,10 +2944,66 @@ function analyzeWaveCompletionFromStatusFiles(wave, statusDir, options = {}) {
|
|
|
2180
2944
|
`Open human feedback records: ${openHumanFeedbackIds.join(", ")}.`,
|
|
2181
2945
|
);
|
|
2182
2946
|
}
|
|
2947
|
+
const capabilityAssignments = readAssignmentSnapshot(assignmentsPath, {
|
|
2948
|
+
lane: options.lane || null,
|
|
2949
|
+
wave: wave.wave,
|
|
2950
|
+
});
|
|
2951
|
+
const blockingAssignments = Array.isArray(capabilityAssignments?.assignments)
|
|
2952
|
+
? capabilityAssignments.assignments.filter((assignment) => assignment?.blocking)
|
|
2953
|
+
: [];
|
|
2954
|
+
const unresolvedAssignments = blockingAssignments.filter((assignment) => !assignment?.assignedAgentId);
|
|
2955
|
+
if (unresolvedAssignments.length > 0) {
|
|
2956
|
+
pushWaveCompletionReason(
|
|
2957
|
+
reasons,
|
|
2958
|
+
"helper-assignment-unresolved",
|
|
2959
|
+
`Helper assignments remain unresolved (${unresolvedAssignments.map((assignment) => assignment.requestId || assignment.id).join(", ")}).`,
|
|
2960
|
+
);
|
|
2961
|
+
} else if (blockingAssignments.length > 0) {
|
|
2962
|
+
pushWaveCompletionReason(
|
|
2963
|
+
reasons,
|
|
2964
|
+
"helper-assignment-open",
|
|
2965
|
+
`Helper assignments remain open (${blockingAssignments.map((assignment) => assignment.requestId || assignment.id).join(", ")}).`,
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
const dependencySnapshot = readDependencySnapshot(dependencySnapshotPath, {
|
|
2969
|
+
lane: options.lane || null,
|
|
2970
|
+
wave: wave.wave,
|
|
2971
|
+
});
|
|
2972
|
+
const unresolvedInboundAssignments = Array.isArray(dependencySnapshot?.unresolvedInboundAssignments)
|
|
2973
|
+
? dependencySnapshot.unresolvedInboundAssignments
|
|
2974
|
+
: [];
|
|
2975
|
+
if (unresolvedInboundAssignments.length > 0) {
|
|
2976
|
+
pushWaveCompletionReason(
|
|
2977
|
+
reasons,
|
|
2978
|
+
"dependency-assignment-unresolved",
|
|
2979
|
+
`Required inbound dependencies are not assigned (${unresolvedInboundAssignments.map((record) => record.id || record).join(", ")}).`,
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
const requiredInbound = Array.isArray(dependencySnapshot?.requiredInbound)
|
|
2983
|
+
? dependencySnapshot.requiredInbound
|
|
2984
|
+
: [];
|
|
2985
|
+
const requiredOutbound = Array.isArray(dependencySnapshot?.requiredOutbound)
|
|
2986
|
+
? dependencySnapshot.requiredOutbound
|
|
2987
|
+
: [];
|
|
2988
|
+
if (requiredInbound.length > 0 || requiredOutbound.length > 0) {
|
|
2989
|
+
pushWaveCompletionReason(
|
|
2990
|
+
reasons,
|
|
2991
|
+
"dependency-open",
|
|
2992
|
+
`Open required dependencies remain (${[...requiredInbound, ...requiredOutbound].map((record) => record.id || record).join(", ")}).`,
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2183
2995
|
|
|
2184
2996
|
return {
|
|
2185
2997
|
ok: reasons.length === 0,
|
|
2186
2998
|
reasons,
|
|
2999
|
+
evidence: buildRunStateEvidence({
|
|
3000
|
+
wave,
|
|
3001
|
+
statusEntries,
|
|
3002
|
+
coordinationLogPath,
|
|
3003
|
+
assignmentsPath,
|
|
3004
|
+
dependencySnapshotPath,
|
|
3005
|
+
blockedReasons: reasons,
|
|
3006
|
+
}),
|
|
2187
3007
|
};
|
|
2188
3008
|
}
|
|
2189
3009
|
|
|
@@ -2206,13 +3026,39 @@ export function reconcileRunStateFromStatusFiles(allWaves, runStatePath, statusD
|
|
|
2206
3026
|
.filter((diagnostic) => diagnostic.ok)
|
|
2207
3027
|
.map((diagnostic) => diagnostic.wave);
|
|
2208
3028
|
const before = readRunState(runStatePath);
|
|
2209
|
-
const firstMerge = normalizeCompletedWaves(
|
|
3029
|
+
const firstMerge = normalizeCompletedWaves(
|
|
3030
|
+
diagnostics
|
|
3031
|
+
.filter((diagnostic) => diagnostic.ok)
|
|
3032
|
+
.map((diagnostic) => diagnostic.wave)
|
|
3033
|
+
.concat(
|
|
3034
|
+
before.completedWaves.filter((waveNumber) => {
|
|
3035
|
+
const diagnostic = diagnostics.find((entry) => entry.wave === waveNumber);
|
|
3036
|
+
return !diagnostic || diagnostic.ok;
|
|
3037
|
+
}),
|
|
3038
|
+
),
|
|
3039
|
+
);
|
|
2210
3040
|
const latest = readRunState(runStatePath);
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
3041
|
+
let nextState = latest;
|
|
3042
|
+
for (const diagnostic of diagnostics) {
|
|
3043
|
+
const toState = diagnostic.ok ? "completed" : "blocked";
|
|
3044
|
+
const reasonCode = diagnostic.ok
|
|
3045
|
+
? "status-reconcile-complete"
|
|
3046
|
+
: diagnostic.reasons[0]?.code || "status-reconcile-blocked";
|
|
3047
|
+
const detail = diagnostic.ok
|
|
3048
|
+
? `Wave ${diagnostic.wave} reconstructed as complete from status files.`
|
|
3049
|
+
: diagnostic.reasons.map((reason) => reason.detail).filter(Boolean).join(" ");
|
|
3050
|
+
nextState = appendRunStateTransition(nextState, {
|
|
3051
|
+
waveNumber: diagnostic.wave,
|
|
3052
|
+
toState,
|
|
3053
|
+
source: "status-reconcile",
|
|
3054
|
+
reasonCode,
|
|
3055
|
+
detail,
|
|
3056
|
+
evidence: diagnostic.evidence || null,
|
|
3057
|
+
at: diagnostic.evidence?.statusFiles?.find((entry) => entry.completedAt)?.completedAt || toIsoTimestamp(),
|
|
3058
|
+
});
|
|
2215
3059
|
}
|
|
3060
|
+
const state = writeRunState(runStatePath, nextState);
|
|
3061
|
+
const merged = state.completedWaves;
|
|
2216
3062
|
return {
|
|
2217
3063
|
completedFromStatus,
|
|
2218
3064
|
addedFromBefore: firstMerge.filter((waveNumber) => !before.completedWaves.includes(waveNumber)),
|