cool-workflow 0.1.78
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/.claude-plugin/plugin.json +20 -0
- package/.codex-plugin/mcp.json +10 -0
- package/.codex-plugin/plugin.json +38 -0
- package/.mcp.json +10 -0
- package/LICENSE +24 -0
- package/README.md +638 -0
- package/apps/architecture-review/app.json +51 -0
- package/apps/architecture-review/workflow.js +116 -0
- package/apps/end-to-end-golden-path/app.json +30 -0
- package/apps/end-to-end-golden-path/workflow.js +33 -0
- package/apps/pr-review-fix-ci/app.json +59 -0
- package/apps/pr-review-fix-ci/workflow.js +90 -0
- package/apps/release-cut/app.json +54 -0
- package/apps/release-cut/workflow.js +82 -0
- package/apps/research-synthesis/app.json +50 -0
- package/apps/research-synthesis/workflow.js +76 -0
- package/apps/workflow-app-framework-demo/app.json +29 -0
- package/apps/workflow-app-framework-demo/workflow.js +44 -0
- package/dist/agent-config.js +223 -0
- package/dist/candidate-scoring.js +715 -0
- package/dist/capability-core.js +630 -0
- package/dist/capability-dispatcher.js +86 -0
- package/dist/capability-registry.js +523 -0
- package/dist/cli.js +1276 -0
- package/dist/collaboration.js +727 -0
- package/dist/commit.js +570 -0
- package/dist/contract-migration.js +234 -0
- package/dist/coordinator.js +1163 -0
- package/dist/daemon.js +44 -0
- package/dist/dispatch.js +201 -0
- package/dist/drive.js +503 -0
- package/dist/error-feedback.js +415 -0
- package/dist/evidence-grounding.js +179 -0
- package/dist/evidence-reasoning.js +733 -0
- package/dist/execution-backend.js +1279 -0
- package/dist/harness.js +61 -0
- package/dist/mcp-server.js +1615 -0
- package/dist/multi-agent-eval.js +857 -0
- package/dist/multi-agent-host.js +764 -0
- package/dist/multi-agent-operator-ux.js +537 -0
- package/dist/multi-agent-trust.js +366 -0
- package/dist/multi-agent.js +1173 -0
- package/dist/node-snapshot.js +270 -0
- package/dist/observability.js +922 -0
- package/dist/operator-ux.js +971 -0
- package/dist/orchestrator/audit-operations.js +182 -0
- package/dist/orchestrator/candidate-operations.js +117 -0
- package/dist/orchestrator/cli-options.js +288 -0
- package/dist/orchestrator/collaboration-operations.js +86 -0
- package/dist/orchestrator/feedback-operations.js +81 -0
- package/dist/orchestrator/host-operations.js +78 -0
- package/dist/orchestrator/lifecycle-operations.js +462 -0
- package/dist/orchestrator/migration-operations.js +44 -0
- package/dist/orchestrator/multi-agent-operations.js +362 -0
- package/dist/orchestrator/report.js +369 -0
- package/dist/orchestrator/topology-operations.js +84 -0
- package/dist/orchestrator.js +874 -0
- package/dist/pipeline-contract.js +92 -0
- package/dist/pipeline-runner.js +285 -0
- package/dist/reclamation.js +882 -0
- package/dist/result-normalize.js +194 -0
- package/dist/run-export.js +64 -0
- package/dist/run-registry.js +1347 -0
- package/dist/run-state-schema.js +67 -0
- package/dist/sandbox-profile.js +471 -0
- package/dist/scheduler.js +266 -0
- package/dist/scheduling.js +184 -0
- package/dist/schema-validate.js +98 -0
- package/dist/state-explosion.js +1213 -0
- package/dist/state-migrations.js +463 -0
- package/dist/state-node.js +301 -0
- package/dist/state.js +308 -0
- package/dist/telemetry-attestation.js +156 -0
- package/dist/telemetry-ledger.js +145 -0
- package/dist/topology.js +527 -0
- package/dist/triggers.js +159 -0
- package/dist/trust-audit.js +475 -0
- package/dist/types/blackboard.js +2 -0
- package/dist/types/boundary.js +29 -0
- package/dist/types/candidate.js +2 -0
- package/dist/types/collaboration.js +2 -0
- package/dist/types/core.js +2 -0
- package/dist/types/drive.js +10 -0
- package/dist/types/error-feedback.js +2 -0
- package/dist/types/evidence-reasoning.js +2 -0
- package/dist/types/execution-backend.js +2 -0
- package/dist/types/multi-agent.js +2 -0
- package/dist/types/observability.js +2 -0
- package/dist/types/pipeline.js +2 -0
- package/dist/types/reclamation.js +8 -0
- package/dist/types/result.js +2 -0
- package/dist/types/run-registry.js +2 -0
- package/dist/types/run.js +2 -0
- package/dist/types/sandbox.js +2 -0
- package/dist/types/schedule.js +2 -0
- package/dist/types/state-node.js +2 -0
- package/dist/types/topology.js +2 -0
- package/dist/types/trust.js +2 -0
- package/dist/types/workbench.js +2 -0
- package/dist/types/worker.js +2 -0
- package/dist/types/workflow-app.js +2 -0
- package/dist/types.js +43 -0
- package/dist/verifier-registry.js +46 -0
- package/dist/verifier.js +78 -0
- package/dist/version.js +8 -0
- package/dist/workbench-host.js +172 -0
- package/dist/workbench.js +190 -0
- package/dist/worker-isolation.js +1028 -0
- package/dist/workflow-api.js +98 -0
- package/dist/workflow-app-framework.js +626 -0
- package/docs/agent-delegation-drive.7.md +190 -0
- package/docs/agent-framework.md +176 -0
- package/docs/candidate-scoring.7.md +106 -0
- package/docs/canonical-workflow-apps.7.md +137 -0
- package/docs/capability-topology-registry.7.md +168 -0
- package/docs/cli-mcp-parity.7.md +373 -0
- package/docs/contract-migration-tooling.7.md +123 -0
- package/docs/control-plane-scheduling.7.md +110 -0
- package/docs/coordinator-blackboard.7.md +183 -0
- package/docs/dogfood/architecture-review-cool-workflow.md +16 -0
- package/docs/dogfood-one-real-repo.7.md +168 -0
- package/docs/durable-state-and-locking.7.md +107 -0
- package/docs/end-to-end-golden-path.7.md +117 -0
- package/docs/error-feedback.7.md +153 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +270 -0
- package/docs/execution-backends.7.md +300 -0
- package/docs/getting-started.md +99 -0
- package/docs/index.md +41 -0
- package/docs/mcp-app-surface.7.md +235 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +265 -0
- package/docs/multi-agent-eval-replay-harness.7.md +302 -0
- package/docs/multi-agent-operator-ux.7.md +314 -0
- package/docs/multi-agent-runtime-core.7.md +231 -0
- package/docs/multi-agent-topologies.7.md +103 -0
- package/docs/multi-agent-trust-policy-audit.7.md +154 -0
- package/docs/node-snapshot-diff-replay.7.md +135 -0
- package/docs/observability-cost-accounting.7.md +194 -0
- package/docs/operator-ux.7.md +180 -0
- package/docs/pipeline-runner.7.md +136 -0
- package/docs/project-index.md +261 -0
- package/docs/real-execution-backends.7.md +142 -0
- package/docs/release-and-migration.7.md +280 -0
- package/docs/release-tooling.7.md +159 -0
- package/docs/routines.md +48 -0
- package/docs/run-registry-control-plane.7.md +312 -0
- package/docs/run-retention-reclamation.7.md +191 -0
- package/docs/sandbox-profiles.7.md +137 -0
- package/docs/scheduled-tasks.md +80 -0
- package/docs/security-trust-hardening.7.md +117 -0
- package/docs/state-explosion-management.7.md +264 -0
- package/docs/state-node.7.md +96 -0
- package/docs/team-collaboration.7.md +207 -0
- package/docs/unix-principles.md +192 -0
- package/docs/verifier-gated-commit.7.md +140 -0
- package/docs/web-desktop-workbench.7.md +215 -0
- package/docs/worker-isolation.7.md +167 -0
- package/docs/workflow-app-framework.7.md +274 -0
- package/manifest/README.md +43 -0
- package/manifest/plugin.manifest.json +316 -0
- package/manifest/pricing.policy.json +14 -0
- package/package.json +79 -0
- package/scripts/agents/claude-p-agent.js +104 -0
- package/scripts/agents/claude-p-agent.sh +9 -0
- package/scripts/agents/cw-attest-keygen.js +55 -0
- package/scripts/agents/cw-attest-wrap.js +143 -0
- package/scripts/block-unapproved-tag.sh +39 -0
- package/scripts/bump-version.js +249 -0
- package/scripts/canonical-apps.js +171 -0
- package/scripts/cw.js +4 -0
- package/scripts/dist-drift-check.js +79 -0
- package/scripts/dogfood-architecture-review.js +237 -0
- package/scripts/dogfood-release.js +624 -0
- package/scripts/forward-ref-docs.js +73 -0
- package/scripts/gen-manifests.js +232 -0
- package/scripts/golden-path.js +300 -0
- package/scripts/mcp-server.js +4 -0
- package/scripts/new-feature.js +121 -0
- package/scripts/parity-check.js +213 -0
- package/scripts/release-check.js +118 -0
- package/scripts/release-flow.js +272 -0
- package/scripts/release-gate.sh +85 -0
- package/scripts/sync-project-index.js +387 -0
- package/scripts/validate-run-state-schema.js +126 -0
- package/scripts/verify-container-selfref.js +64 -0
- package/scripts/version-sync-check.js +237 -0
- package/skills/cool-workflow/SKILL.md +162 -0
- package/skills/cool-workflow/references/commands.md +282 -0
- package/tsconfig.json +16 -0
- package/ui/workbench/app.css +76 -0
- package/ui/workbench/app.js +159 -0
- package/ui/workbench/index.html +32 -0
- package/workflows/architecture-review.workflow.js +84 -0
- package/workflows/research-synthesis.workflow.js +47 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PipelineContractError = exports.PIPELINE_CONTRACT_SCHEMA_VERSION = exports.STATE_NODE_SCHEMA_VERSION = void 0;
|
|
7
|
+
exports.createStateNode = createStateNode;
|
|
8
|
+
exports.transitionStateNode = transitionStateNode;
|
|
9
|
+
exports.validatePipelineContract = validatePipelineContract;
|
|
10
|
+
exports.assertNodeSatisfiesContract = assertNodeSatisfiesContract;
|
|
11
|
+
exports.recordNodeError = recordNodeError;
|
|
12
|
+
exports.linkStateNodes = linkStateNodes;
|
|
13
|
+
exports.appendRunNode = appendRunNode;
|
|
14
|
+
exports.upsertRunContract = upsertRunContract;
|
|
15
|
+
exports.writeRunNode = writeRunNode;
|
|
16
|
+
exports.artifactExists = artifactExists;
|
|
17
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const state_1 = require("./state");
|
|
20
|
+
exports.STATE_NODE_SCHEMA_VERSION = 1;
|
|
21
|
+
exports.PIPELINE_CONTRACT_SCHEMA_VERSION = 1;
|
|
22
|
+
class PipelineContractError extends Error {
|
|
23
|
+
structured;
|
|
24
|
+
constructor(error) {
|
|
25
|
+
super(error.message);
|
|
26
|
+
this.name = "PipelineContractError";
|
|
27
|
+
this.structured = {
|
|
28
|
+
...error,
|
|
29
|
+
at: error.at || new Date().toISOString()
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.PipelineContractError = PipelineContractError;
|
|
34
|
+
function createStateNode(input) {
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
return {
|
|
37
|
+
schemaVersion: exports.STATE_NODE_SCHEMA_VERSION,
|
|
38
|
+
id: input.id || createNodeId(input.kind),
|
|
39
|
+
kind: input.kind,
|
|
40
|
+
status: input.status || "pending",
|
|
41
|
+
loopStage: input.loopStage,
|
|
42
|
+
createdAt: now,
|
|
43
|
+
updatedAt: now,
|
|
44
|
+
inputs: input.inputs || {},
|
|
45
|
+
outputs: input.outputs || {},
|
|
46
|
+
artifacts: input.artifacts || [],
|
|
47
|
+
evidence: input.evidence || [],
|
|
48
|
+
errors: input.errors || [],
|
|
49
|
+
parents: input.parents || [],
|
|
50
|
+
children: input.children || [],
|
|
51
|
+
contractId: input.contractId,
|
|
52
|
+
metadata: input.metadata
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function transitionStateNode(node, input) {
|
|
56
|
+
if (!isLegalTransition(node.status, input.status)) {
|
|
57
|
+
throw contractError("illegal-transition", `State node ${node.id} cannot transition from ${node.status} to ${input.status}`, {
|
|
58
|
+
nodeId: node.id,
|
|
59
|
+
details: {
|
|
60
|
+
from: node.status,
|
|
61
|
+
to: input.status
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (input.status === "committed" && node.status !== "verified") {
|
|
66
|
+
throw contractError("commit-without-verifier", `State node ${node.id} cannot be committed before it is verified`, {
|
|
67
|
+
nodeId: node.id,
|
|
68
|
+
details: {
|
|
69
|
+
from: node.status,
|
|
70
|
+
to: input.status
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
...node,
|
|
76
|
+
status: input.status,
|
|
77
|
+
loopStage: input.loopStage || node.loopStage,
|
|
78
|
+
updatedAt: new Date().toISOString(),
|
|
79
|
+
outputs: input.outputs ? { ...node.outputs, ...input.outputs } : node.outputs,
|
|
80
|
+
artifacts: input.artifacts ? mergeById(node.artifacts, input.artifacts) : node.artifacts,
|
|
81
|
+
evidence: input.evidence ? mergeById(node.evidence, input.evidence) : node.evidence,
|
|
82
|
+
metadata: input.metadata ? { ...(node.metadata || {}), ...input.metadata } : node.metadata
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function validatePipelineContract(contract) {
|
|
86
|
+
if (contract.schemaVersion !== exports.PIPELINE_CONTRACT_SCHEMA_VERSION) {
|
|
87
|
+
throw contractError("invalid-contract-schema", `Pipeline contract ${contract.id || "(missing id)"} has unsupported schemaVersion`, {
|
|
88
|
+
details: { schemaVersion: contract.schemaVersion }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (!contract.id)
|
|
92
|
+
throw contractError("invalid-contract-id", "Pipeline contract id is required");
|
|
93
|
+
if (!contract.title)
|
|
94
|
+
throw contractError("invalid-contract-title", `Pipeline contract ${contract.id} title is required`);
|
|
95
|
+
if (!Array.isArray(contract.stages) || !contract.stages.length) {
|
|
96
|
+
throw contractError("invalid-contract-stages", `Pipeline contract ${contract.id} must include at least one stage`);
|
|
97
|
+
}
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
for (const stage of contract.stages) {
|
|
100
|
+
validateStage(contract, stage, seen);
|
|
101
|
+
}
|
|
102
|
+
if (!contract.compatibility) {
|
|
103
|
+
throw contractError("invalid-contract-compatibility", `Pipeline contract ${contract.id} compatibility is required`);
|
|
104
|
+
}
|
|
105
|
+
if (contract.compatibility.minSchemaVersion > exports.STATE_NODE_SCHEMA_VERSION) {
|
|
106
|
+
throw contractError("incompatible-contract", `Pipeline contract ${contract.id} requires newer StateNode schema`, {
|
|
107
|
+
details: contract.compatibility
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function assertNodeSatisfiesContract(node, contract, stageId) {
|
|
112
|
+
validatePipelineContract(contract);
|
|
113
|
+
const stage = contract.stages.find((candidate) => candidate.id === stageId);
|
|
114
|
+
if (!stage) {
|
|
115
|
+
throw contractError("unknown-contract-stage", `Pipeline contract ${contract.id} has no stage ${stageId}`, {
|
|
116
|
+
nodeId: node.id
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (!stage.acceptedInputKinds.includes(node.kind)) {
|
|
120
|
+
throw contractError("unexpected-node-kind", `Stage ${stage.id} does not accept node kind ${node.kind}`, {
|
|
121
|
+
nodeId: node.id,
|
|
122
|
+
details: { expected: stage.acceptedInputKinds, actual: node.kind }
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (!stage.acceptedInputStatuses.includes(node.status)) {
|
|
126
|
+
throw contractError("unexpected-node-status", `Stage ${stage.id} does not accept node status ${node.status}`, {
|
|
127
|
+
nodeId: node.id,
|
|
128
|
+
details: { expected: stage.acceptedInputStatuses, actual: node.status }
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
assertRequiredArtifacts(node, stage);
|
|
132
|
+
assertRequiredEvidence(node, stage, contract);
|
|
133
|
+
assertVerifierGate(node, stage, contract);
|
|
134
|
+
}
|
|
135
|
+
function recordNodeError(node, error) {
|
|
136
|
+
return {
|
|
137
|
+
...node,
|
|
138
|
+
status: "failed",
|
|
139
|
+
updatedAt: new Date().toISOString(),
|
|
140
|
+
errors: [
|
|
141
|
+
...node.errors,
|
|
142
|
+
{
|
|
143
|
+
...error,
|
|
144
|
+
at: error.at || new Date().toISOString(),
|
|
145
|
+
nodeId: error.nodeId || node.id
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function linkStateNodes(parent, child) {
|
|
151
|
+
return [
|
|
152
|
+
{
|
|
153
|
+
...parent,
|
|
154
|
+
updatedAt: new Date().toISOString(),
|
|
155
|
+
children: unique([...parent.children, child.id])
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
...child,
|
|
159
|
+
updatedAt: new Date().toISOString(),
|
|
160
|
+
parents: unique([...child.parents, parent.id])
|
|
161
|
+
}
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
function appendRunNode(run, node) {
|
|
165
|
+
const nodes = run.nodes || [];
|
|
166
|
+
const index = nodes.findIndex((candidate) => candidate.id === node.id);
|
|
167
|
+
const nextNodes = index >= 0 ? nodes.map((candidate) => (candidate.id === node.id ? node : candidate)) : [...nodes, node];
|
|
168
|
+
run.nodes = nextNodes;
|
|
169
|
+
writeRunNode(run, node);
|
|
170
|
+
return node;
|
|
171
|
+
}
|
|
172
|
+
function upsertRunContract(run, contract) {
|
|
173
|
+
validatePipelineContract(contract);
|
|
174
|
+
const contracts = run.contracts || [];
|
|
175
|
+
const index = contracts.findIndex((candidate) => candidate.id === contract.id);
|
|
176
|
+
run.contracts =
|
|
177
|
+
index >= 0 ? contracts.map((candidate) => (candidate.id === contract.id ? contract : candidate)) : [...contracts, contract];
|
|
178
|
+
return contract;
|
|
179
|
+
}
|
|
180
|
+
function writeRunNode(run, node) {
|
|
181
|
+
const dir = run.paths.stateNodesDir || node_path_1.default.join(run.paths.runDir, "nodes");
|
|
182
|
+
const file = node_path_1.default.join(dir, `${(0, state_1.safeFileName)(node.id)}.json`);
|
|
183
|
+
(0, state_1.writeJson)(file, node);
|
|
184
|
+
return file;
|
|
185
|
+
}
|
|
186
|
+
function artifactExists(artifact) {
|
|
187
|
+
return Boolean(artifact.path && node_fs_1.default.existsSync(artifact.path));
|
|
188
|
+
}
|
|
189
|
+
function validateStage(contract, stage, seen) {
|
|
190
|
+
if (!stage.id)
|
|
191
|
+
throw contractError("invalid-contract-stage-id", `Pipeline contract ${contract.id} has a stage without id`);
|
|
192
|
+
if (seen.has(stage.id))
|
|
193
|
+
throw contractError("duplicate-contract-stage", `Pipeline contract ${contract.id} repeats stage ${stage.id}`);
|
|
194
|
+
seen.add(stage.id);
|
|
195
|
+
if (!stage.name)
|
|
196
|
+
throw contractError("invalid-contract-stage-name", `Stage ${stage.id} name is required`);
|
|
197
|
+
if (!Array.isArray(stage.acceptedInputKinds) || !stage.acceptedInputKinds.length) {
|
|
198
|
+
throw contractError("invalid-contract-stage-kinds", `Stage ${stage.id} must accept at least one input kind`);
|
|
199
|
+
}
|
|
200
|
+
if (!Array.isArray(stage.acceptedInputStatuses) || !stage.acceptedInputStatuses.length) {
|
|
201
|
+
throw contractError("invalid-contract-stage-statuses", `Stage ${stage.id} must accept at least one input status`);
|
|
202
|
+
}
|
|
203
|
+
if (!stage.producedOutputKind) {
|
|
204
|
+
throw contractError("invalid-contract-stage-output", `Stage ${stage.id} producedOutputKind is required`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function assertRequiredArtifacts(node, stage) {
|
|
208
|
+
for (const required of stage.requiredArtifacts || []) {
|
|
209
|
+
const artifact = node.artifacts.find((candidate) => candidate.id === required || candidate.kind === required);
|
|
210
|
+
if (!artifact) {
|
|
211
|
+
throw contractError("missing-required-artifact", `Node ${node.id} is missing required artifact ${required}`, {
|
|
212
|
+
nodeId: node.id,
|
|
213
|
+
details: { requiredArtifact: required }
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (!artifactExists(artifact)) {
|
|
217
|
+
throw contractError("missing-artifact-path", `Node ${node.id} artifact ${artifact.id} path does not exist`, {
|
|
218
|
+
nodeId: node.id,
|
|
219
|
+
path: artifact.path,
|
|
220
|
+
details: { artifactId: artifact.id }
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function assertRequiredEvidence(node, stage, contract) {
|
|
226
|
+
const requiredEvidence = stage.requiredEvidence || [];
|
|
227
|
+
const contractRequiresEvidence = Boolean(contract.evidencePolicy?.requireEvidence);
|
|
228
|
+
if ((requiredEvidence.length || contractRequiresEvidence) && !node.evidence.length) {
|
|
229
|
+
throw contractError("missing-required-evidence", `Node ${node.id} is missing required evidence`, {
|
|
230
|
+
nodeId: node.id,
|
|
231
|
+
details: { requiredEvidence }
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
for (const required of requiredEvidence) {
|
|
235
|
+
const evidence = node.evidence.find((candidate) => candidate.id === required || candidate.source === required);
|
|
236
|
+
if (!evidence) {
|
|
237
|
+
throw contractError("missing-required-evidence", `Node ${node.id} is missing required evidence ${required}`, {
|
|
238
|
+
nodeId: node.id,
|
|
239
|
+
details: { requiredEvidence: required }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function assertVerifierGate(node, stage, contract) {
|
|
245
|
+
const gate = stage.verifierGate;
|
|
246
|
+
const commitRequiresGate = contract.commitPolicy?.requiresVerifierGate && stage.producedOutputKind === "commit";
|
|
247
|
+
if (!gate?.required && !commitRequiresGate)
|
|
248
|
+
return;
|
|
249
|
+
const acceptedStatuses = gate?.acceptedStatuses || contract.commitPolicy?.acceptedVerifierStatuses || ["verified"];
|
|
250
|
+
if (!acceptedStatuses.includes(node.status)) {
|
|
251
|
+
throw contractError("verifier-gate-blocked", `Stage ${stage.id} requires verifier status ${acceptedStatuses.join(", ")}`, {
|
|
252
|
+
nodeId: node.id,
|
|
253
|
+
details: { actual: node.status, accepted: acceptedStatuses }
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if ((gate?.requiredEvidence || contract.evidencePolicy?.requireEvidence) && !node.evidence.length) {
|
|
257
|
+
throw contractError("verifier-gate-missing-evidence", `Stage ${stage.id} requires evidence before commit`, {
|
|
258
|
+
nodeId: node.id
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function isLegalTransition(from, to) {
|
|
263
|
+
if (from === to)
|
|
264
|
+
return true;
|
|
265
|
+
const allowed = {
|
|
266
|
+
pending: ["running", "blocked", "failed", "completed", "verified", "rejected"],
|
|
267
|
+
running: ["completed", "failed", "blocked"],
|
|
268
|
+
completed: ["verified", "rejected", "failed"],
|
|
269
|
+
failed: ["pending", "blocked"],
|
|
270
|
+
blocked: ["pending", "failed"],
|
|
271
|
+
verified: ["committed", "rejected"],
|
|
272
|
+
rejected: ["pending", "failed"],
|
|
273
|
+
committed: []
|
|
274
|
+
};
|
|
275
|
+
return allowed[from].includes(to);
|
|
276
|
+
}
|
|
277
|
+
function contractError(code, message, options = {}) {
|
|
278
|
+
return new PipelineContractError({
|
|
279
|
+
code,
|
|
280
|
+
message,
|
|
281
|
+
...options
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function createNodeId(kind) {
|
|
285
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
286
|
+
return `${kind}-${stamp}-${Math.random().toString(36).slice(2, 8)}`;
|
|
287
|
+
}
|
|
288
|
+
function mergeById(existing, next) {
|
|
289
|
+
const values = [...existing];
|
|
290
|
+
for (const item of next) {
|
|
291
|
+
const index = values.findIndex((candidate) => candidate.id === item.id);
|
|
292
|
+
if (index >= 0)
|
|
293
|
+
values[index] = item;
|
|
294
|
+
else
|
|
295
|
+
values.push(item);
|
|
296
|
+
}
|
|
297
|
+
return values;
|
|
298
|
+
}
|
|
299
|
+
function unique(values) {
|
|
300
|
+
return [...new Set(values)];
|
|
301
|
+
}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CURRENT_RUN_STATE_SCHEMA_VERSION = void 0;
|
|
7
|
+
exports.createRunPaths = createRunPaths;
|
|
8
|
+
exports.ensureRunDirs = ensureRunDirs;
|
|
9
|
+
exports.loadRunFromCwd = loadRunFromCwd;
|
|
10
|
+
exports.loadRunStateFile = loadRunStateFile;
|
|
11
|
+
exports.checkRunStateFile = checkRunStateFile;
|
|
12
|
+
exports.migrateRunStateFile = migrateRunStateFile;
|
|
13
|
+
exports.saveCheckpoint = saveCheckpoint;
|
|
14
|
+
exports.compactCheckpoint = compactCheckpoint;
|
|
15
|
+
exports.readJson = readJson;
|
|
16
|
+
exports.writeJson = writeJson;
|
|
17
|
+
exports.durableAppendFileSync = durableAppendFileSync;
|
|
18
|
+
exports.realResolve = realResolve;
|
|
19
|
+
exports.isContainedPath = isContainedPath;
|
|
20
|
+
exports.withFileLock = withFileLock;
|
|
21
|
+
exports.safeFileName = safeFileName;
|
|
22
|
+
exports.hashArtifactFile = hashArtifactFile;
|
|
23
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
24
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
25
|
+
const state_migrations_1 = require("./state-migrations");
|
|
26
|
+
const version_1 = require("./version");
|
|
27
|
+
Object.defineProperty(exports, "CURRENT_RUN_STATE_SCHEMA_VERSION", { enumerable: true, get: function () { return version_1.CURRENT_RUN_STATE_SCHEMA_VERSION; } });
|
|
28
|
+
const execution_backend_1 = require("./execution-backend");
|
|
29
|
+
function createRunPaths(runDir) {
|
|
30
|
+
return {
|
|
31
|
+
runDir,
|
|
32
|
+
state: node_path_1.default.join(runDir, "state.json"),
|
|
33
|
+
report: node_path_1.default.join(runDir, "report.md"),
|
|
34
|
+
tasksDir: node_path_1.default.join(runDir, "tasks"),
|
|
35
|
+
resultsDir: node_path_1.default.join(runDir, "results"),
|
|
36
|
+
dispatchesDir: node_path_1.default.join(runDir, "dispatches"),
|
|
37
|
+
artifactsDir: node_path_1.default.join(runDir, "artifacts"),
|
|
38
|
+
commitsDir: node_path_1.default.join(runDir, "commits"),
|
|
39
|
+
stateNodesDir: node_path_1.default.join(runDir, "nodes"),
|
|
40
|
+
feedbackDir: node_path_1.default.join(runDir, "feedback"),
|
|
41
|
+
auditDir: node_path_1.default.join(runDir, "audit"),
|
|
42
|
+
workersDir: node_path_1.default.join(runDir, "workers"),
|
|
43
|
+
candidatesDir: node_path_1.default.join(runDir, "candidates"),
|
|
44
|
+
multiAgentDir: node_path_1.default.join(runDir, "multi-agent"),
|
|
45
|
+
blackboardDir: node_path_1.default.join(runDir, "blackboard"),
|
|
46
|
+
topologiesDir: node_path_1.default.join(runDir, "topologies")
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function ensureRunDirs(paths) {
|
|
50
|
+
for (const dir of [
|
|
51
|
+
paths.runDir,
|
|
52
|
+
paths.tasksDir,
|
|
53
|
+
paths.resultsDir,
|
|
54
|
+
paths.dispatchesDir,
|
|
55
|
+
paths.artifactsDir,
|
|
56
|
+
paths.commitsDir,
|
|
57
|
+
paths.stateNodesDir,
|
|
58
|
+
paths.feedbackDir,
|
|
59
|
+
paths.auditDir || node_path_1.default.join(paths.runDir, "audit"),
|
|
60
|
+
paths.workersDir || node_path_1.default.join(paths.runDir, "workers"),
|
|
61
|
+
paths.candidatesDir || node_path_1.default.join(paths.runDir, "candidates"),
|
|
62
|
+
paths.multiAgentDir || node_path_1.default.join(paths.runDir, "multi-agent"),
|
|
63
|
+
paths.blackboardDir || node_path_1.default.join(paths.runDir, "blackboard"),
|
|
64
|
+
paths.topologiesDir || node_path_1.default.join(paths.runDir, "topologies")
|
|
65
|
+
]) {
|
|
66
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function loadRunFromCwd(runId, cwd = process.cwd()) {
|
|
70
|
+
if (!runId)
|
|
71
|
+
throw new Error("Missing run id");
|
|
72
|
+
const statePath = node_path_1.default.join(cwd, ".cw", "runs", runId, "state.json");
|
|
73
|
+
const result = loadRunStateFile(statePath, { dryRun: true });
|
|
74
|
+
if (result.report.status === "unsupported") {
|
|
75
|
+
throw new Error(`Unsupported CW run state: ${result.report.errors.join("; ")}`);
|
|
76
|
+
}
|
|
77
|
+
return result.run;
|
|
78
|
+
}
|
|
79
|
+
function loadRunStateFile(statePath, options = {}) {
|
|
80
|
+
const result = (0, state_migrations_1.migrateRunState)(readJson(statePath), {
|
|
81
|
+
statePath,
|
|
82
|
+
dryRun: options.dryRun === undefined ? true : options.dryRun
|
|
83
|
+
});
|
|
84
|
+
if (result.report.status === "unsupported")
|
|
85
|
+
return result;
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
function checkRunStateFile(statePath) {
|
|
89
|
+
return loadRunStateFile(statePath, { dryRun: true });
|
|
90
|
+
}
|
|
91
|
+
function migrateRunStateFile(statePath, options = {}) {
|
|
92
|
+
const result = loadRunStateFile(statePath, { dryRun: !options.write });
|
|
93
|
+
if (result.report.status !== "unsupported" && options.write && result.report.writeRequired) {
|
|
94
|
+
writeJson(statePath, result.run);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
function saveCheckpoint(run) {
|
|
99
|
+
run.updatedAt = new Date().toISOString();
|
|
100
|
+
// state.json is the single source of truth — write it DURABLY (v0.1.40).
|
|
101
|
+
writeJson(run.paths.state, run, { durable: true });
|
|
102
|
+
}
|
|
103
|
+
/** Compact a run checkpoint by stripping empty optional arrays and null values
|
|
104
|
+
* that don't carry semantic meaning (v0.1.60). The normalization layer
|
|
105
|
+
* (normalizeRunState) backfills these on load, so stripping them saves disk
|
|
106
|
+
* without losing information. Returns the number of keys stripped. */
|
|
107
|
+
function compactCheckpoint(run) {
|
|
108
|
+
const optionalArrays = [
|
|
109
|
+
"nodes", "contracts", "feedback", "workers", "sandboxProfiles",
|
|
110
|
+
"candidates", "candidateSelections"
|
|
111
|
+
];
|
|
112
|
+
let stripped = 0;
|
|
113
|
+
const state = run;
|
|
114
|
+
for (const key of optionalArrays) {
|
|
115
|
+
if (Array.isArray(state[key]) && state[key].length === 0) {
|
|
116
|
+
delete state[key];
|
|
117
|
+
stripped++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (stripped > 0)
|
|
121
|
+
saveCheckpoint(run);
|
|
122
|
+
return stripped;
|
|
123
|
+
}
|
|
124
|
+
function readJson(file) {
|
|
125
|
+
if (!node_fs_1.default.existsSync(file))
|
|
126
|
+
throw new Error(`File not found: ${file}`);
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
throw new Error(`Invalid JSON in ${file}: ${message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Atomic, optionally-durable JSON write (v0.1.40, closes the prior P1).
|
|
137
|
+
//
|
|
138
|
+
// ORDER IS THE SAFETY PROPERTY: write to a unique temp file, then rename over the
|
|
139
|
+
// target. `rename(2)` is atomic on POSIX, so a crash/`ENOSPC` mid-write can never
|
|
140
|
+
// leave a truncated `state.json` that throws `Invalid JSON` on reload — a reader
|
|
141
|
+
// always sees EITHER the old bytes OR the new bytes, never a torn file. With
|
|
142
|
+
// `{ durable: true }` we additionally fsync the file (and best-effort the dir)
|
|
143
|
+
// before/after the rename so the bytes survive power loss — used for AUTHORITATIVE
|
|
144
|
+
// state (state.json, registry overlays, the scheduler store, reclaimed.json). The
|
|
145
|
+
// fsync is skipped for high-frequency derived/rebuildable writes so the atomic
|
|
146
|
+
// rename (the actual torn-write fix) stays cheap everywhere.
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
let atomicWriteCounter = 0;
|
|
149
|
+
function writeJson(file, value, options = {}) {
|
|
150
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
151
|
+
const tmp = `${file}.tmp.${process.pid}.${atomicWriteCounter++}`;
|
|
152
|
+
const fd = node_fs_1.default.openSync(tmp, "w");
|
|
153
|
+
try {
|
|
154
|
+
node_fs_1.default.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
155
|
+
if (options.durable)
|
|
156
|
+
node_fs_1.default.fsyncSync(fd);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
node_fs_1.default.closeSync(fd);
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
node_fs_1.default.renameSync(tmp, file);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
try {
|
|
166
|
+
node_fs_1.default.rmSync(tmp, { force: true });
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
/* best-effort temp cleanup */
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
if (options.durable) {
|
|
174
|
+
try {
|
|
175
|
+
const dirFd = node_fs_1.default.openSync(node_path_1.default.dirname(file), "r");
|
|
176
|
+
try {
|
|
177
|
+
node_fs_1.default.fsyncSync(dirFd);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
node_fs_1.default.closeSync(dirFd);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* directory fsync is best-effort (not supported on every platform) */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Durable append (v0.1.40 self-audit P1) — append a line and fsync it before
|
|
190
|
+
// returning. The trust-audit event log is the ONE artifact whose loss breaks
|
|
191
|
+
// audit-completeness/non-repudiation, so unlike high-frequency derived writes it
|
|
192
|
+
// must survive power loss. `appendFileSync` alone does NOT fsync, so a crash
|
|
193
|
+
// after it returns could drop the most recent event while durable state.json
|
|
194
|
+
// advanced past it. We open O_APPEND, write, fsync the fd, then close.
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
function durableAppendFileSync(file, data) {
|
|
197
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
198
|
+
const fd = node_fs_1.default.openSync(file, "a");
|
|
199
|
+
try {
|
|
200
|
+
node_fs_1.default.writeFileSync(fd, data, "utf8");
|
|
201
|
+
node_fs_1.default.fsyncSync(fd);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
node_fs_1.default.closeSync(fd);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Symlink-hardened path containment (v0.1.40 self-audit P1) — `path.resolve()`
|
|
209
|
+
// only normalizes `.`/`..` textually; it does NOT follow symlinks, so a planted
|
|
210
|
+
// symlink whose textual path sits "inside" an allowed root but whose real target
|
|
211
|
+
// escapes it would pass a `startsWith` containment check. `realResolve` resolves
|
|
212
|
+
// the deepest EXISTING ancestor with `realpathSync` (which follows symlinks) and
|
|
213
|
+
// re-joins the not-yet-created remainder, so a not-yet-created file is still
|
|
214
|
+
// pinned to its real parent. `isContainedPath` realpaths BOTH sides so the
|
|
215
|
+
// comparison stays consistent on platforms where the temp root itself is a
|
|
216
|
+
// symlink (e.g. macOS /tmp -> /private/tmp).
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
function realResolve(target) {
|
|
219
|
+
let current = node_path_1.default.resolve(target);
|
|
220
|
+
const tail = [];
|
|
221
|
+
// Walk up to the deepest existing ancestor, realpath it, then re-append the tail.
|
|
222
|
+
for (;;) {
|
|
223
|
+
try {
|
|
224
|
+
const real = node_fs_1.default.realpathSync.native ? node_fs_1.default.realpathSync.native(current) : node_fs_1.default.realpathSync(current);
|
|
225
|
+
return tail.length ? node_path_1.default.join(real, ...tail.reverse()) : real;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
const parent = node_path_1.default.dirname(current);
|
|
229
|
+
if (parent === current)
|
|
230
|
+
return node_path_1.default.resolve(target); // reached root; nothing existed
|
|
231
|
+
tail.push(node_path_1.default.basename(current));
|
|
232
|
+
current = parent;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function isContainedPath(candidate, allowed) {
|
|
237
|
+
const realCandidate = realResolve(candidate);
|
|
238
|
+
const realAllowed = realResolve(allowed);
|
|
239
|
+
return realCandidate === realAllowed || realCandidate.startsWith(realAllowed + node_path_1.default.sep);
|
|
240
|
+
}
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Portable advisory file lock (v0.1.40) — serialize cross-process read-modify-
|
|
243
|
+
// write on shared stores (home queue, scheduler store, archive overlay, the
|
|
244
|
+
// per-run reclamation chain) so a concurrent writer can never lose a record.
|
|
245
|
+
// O_EXCL (`wx`) is portable (no native flock); a stale holder is stolen so a
|
|
246
|
+
// crashed process can never wedge the store forever.
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
const FILE_LOCK_STALE_MS = 30_000;
|
|
249
|
+
function sleepSync(ms) {
|
|
250
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
251
|
+
}
|
|
252
|
+
/** Run `fn` while holding an advisory lock for `targetPath`; always released. */
|
|
253
|
+
function withFileLock(targetPath, fn) {
|
|
254
|
+
const lock = `${targetPath}.lock`;
|
|
255
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(lock), { recursive: true });
|
|
256
|
+
let acquired = false;
|
|
257
|
+
for (let attempt = 0; attempt < 240 && !acquired; attempt++) {
|
|
258
|
+
try {
|
|
259
|
+
const fd = node_fs_1.default.openSync(lock, "wx");
|
|
260
|
+
node_fs_1.default.writeFileSync(fd, `${process.pid}@${new Date().toISOString()}\n`, "utf8");
|
|
261
|
+
node_fs_1.default.closeSync(fd);
|
|
262
|
+
acquired = true;
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
if (!(error && typeof error === "object" && error.code === "EEXIST"))
|
|
266
|
+
throw error;
|
|
267
|
+
try {
|
|
268
|
+
if (Date.now() - node_fs_1.default.statSync(lock).mtimeMs > FILE_LOCK_STALE_MS) {
|
|
269
|
+
node_fs_1.default.rmSync(lock, { force: true });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
continue; // lock vanished between open and stat — retry immediately
|
|
275
|
+
}
|
|
276
|
+
sleepSync(25);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!acquired)
|
|
280
|
+
throw new Error(`could not acquire file lock for ${targetPath}`);
|
|
281
|
+
try {
|
|
282
|
+
return fn();
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
try {
|
|
286
|
+
node_fs_1.default.rmSync(lock, { force: true });
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
/* releasing a missing lock is fine */
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function safeFileName(value) {
|
|
294
|
+
return String(value).replace(/[^a-zA-Z0-9_.:-]+/g, "_");
|
|
295
|
+
}
|
|
296
|
+
/** Compute and set SHA256 + sizeBytes on a StateArtifact from its file path
|
|
297
|
+
* (v0.1.73). Fails silently when the file doesn't exist — does not throw. */
|
|
298
|
+
function hashArtifactFile(artifact) {
|
|
299
|
+
try {
|
|
300
|
+
const content = node_fs_1.default.readFileSync(artifact.path, "utf8");
|
|
301
|
+
artifact.sha256 = (0, execution_backend_1.sha256)(content);
|
|
302
|
+
artifact.sizeBytes = Buffer.byteLength(content, "utf8");
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
/* file missing — silently skip */
|
|
306
|
+
}
|
|
307
|
+
return artifact;
|
|
308
|
+
}
|