cool-workflow 0.2.5 → 0.2.6
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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini-plugin/plugin.json +1 -1
- package/.opencode-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/core/state/run-paths.js +3 -30
- package/dist/core/version.js +1 -1
- package/dist/mcp/server.js +95 -27
- package/dist/mcp/tool-process.js +181 -0
- package/dist/mcp-server.js +8 -1
- package/dist/shell/agent-config.js +11 -2
- package/dist/shell/audit-cli.js +1 -1
- package/dist/shell/drive.js +99 -89
- package/dist/shell/execution-backend/agent.js +18 -15
- package/dist/shell/execution-backend/local.js +1 -0
- package/dist/shell/fs-atomic.js +3 -0
- package/dist/shell/metrics-cli.js +1 -1
- package/dist/shell/multi-agent-cli.js +4 -1
- package/dist/shell/multi-agent-operator-ux.js +1 -1
- package/dist/shell/observability.js +7 -4
- package/dist/shell/operator-ux.js +1 -1
- package/dist/shell/perf-trace.js +136 -0
- package/dist/shell/pipeline.js +1 -1
- package/dist/shell/report-view-cli.js +2 -0
- package/dist/shell/run-export-cli.js +5 -2
- package/dist/shell/run-export.js +241 -27
- package/dist/shell/run-registry-io.js +8 -0
- package/dist/shell/run-store.js +27 -3
- package/dist/shell/state-explosion-cli.js +4 -1
- package/dist/shell/telemetry-demo.js +1 -1
- package/dist/shell/trust-audit.js +91 -43
- package/dist/shell/workbench.js +4 -1
- package/dist/wiring/capability-table/parity.js +10 -5
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/cli-mcp-parity.7.md +21 -5
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/mcp-app-surface.7.md +21 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +18 -4
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +10 -0
- package/docs/run-registry-control-plane.7.md +35 -9
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +48 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +3 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/gen-manifests.js +32 -61
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +42 -23
- package/scripts/purity-baseline.json +0 -3
- package/scripts/release-flow.js +43 -13
- package/scripts/release-gate.js +30 -6
- package/scripts/schema-version-inventory.json +31 -0
- package/scripts/validate-run-state-schema.js +95 -4
- package/scripts/verify-release-verdict.js +139 -0
- package/ui/workbench/app.css +15 -7
- package/ui/workbench/app.js +118 -16
- package/ui/workbench/index.html +2 -0
- package/ui/workbench/inspection.js +51 -0
- package/ui/workbench/navigation.js +44 -0
package/scripts/gen-manifests.js
CHANGED
|
@@ -110,82 +110,47 @@ function _resolveTemplate(template, src, pluginRootVar) {
|
|
|
110
110
|
|
|
111
111
|
function build(src) {
|
|
112
112
|
const { targets, vendors } = src;
|
|
113
|
-
|
|
114
|
-
if (!vendors || typeof vendors !== "object" || Object.keys(vendors).length === 0) {
|
|
115
|
-
return buildLegacy(src);
|
|
116
|
-
}
|
|
113
|
+
validateVendorRegistry(vendors);
|
|
117
114
|
const outputs = [];
|
|
118
115
|
for (const [vendorId, vendorDef] of Object.entries(vendors)) {
|
|
119
116
|
const targetConfig = targets && targets[vendorId] ? targets[vendorId] : {};
|
|
120
117
|
const pluginRootVar = targetConfig.pluginRootVar || "./";
|
|
121
|
-
const vendorOutputs = vendorDef.outputs
|
|
118
|
+
const vendorOutputs = vendorDef.outputs;
|
|
122
119
|
for (const output of vendorOutputs) {
|
|
123
120
|
const resolved = _resolveTemplate(output, src, pluginRootVar);
|
|
121
|
+
if (!isPlainObject(resolved) || typeof resolved.path !== "string" || !resolved.path.trim()) {
|
|
122
|
+
throw new Error(`vendor ${vendorId} has an output without a path`);
|
|
123
|
+
}
|
|
124
|
+
if (!isPlainObject(resolved.json)) {
|
|
125
|
+
throw new Error(`vendor ${vendorId} output ${resolved.path} has no JSON object`);
|
|
126
|
+
}
|
|
124
127
|
outputs.push({
|
|
125
|
-
path:
|
|
126
|
-
json: resolved.json
|
|
128
|
+
path: resolved.path,
|
|
129
|
+
json: resolved.json
|
|
127
130
|
});
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
return outputs;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const { identity, descriptions, interface: ui, layout, mcp, targets } = src;
|
|
136
|
-
const outputs = [];
|
|
137
|
-
|
|
138
|
-
if (targets && targets.claude) {
|
|
139
|
-
outputs.push({
|
|
140
|
-
path: targets.claude.marketplace,
|
|
141
|
-
json: { name: identity.name, owner: identity.author, metadata: { description: descriptions.standard, version: identity.version }, plugins: [{ name: identity.name, source: layout.pluginPathFromRepoRoot, description: descriptions.standard, category: ui.category.toLowerCase() }] }
|
|
142
|
-
});
|
|
143
|
-
outputs.push({
|
|
144
|
-
path: targets.claude.plugin,
|
|
145
|
-
json: { name: identity.name, description: descriptions.standard, version: identity.version, author: identity.author, homepage: identity.homepage, license: identity.license, keywords: identity.keywords }
|
|
146
|
-
});
|
|
147
|
-
outputs.push({
|
|
148
|
-
path: targets.claude.mcp,
|
|
149
|
-
json: legacyMcpConfig(mcp, layout, targets.claude.pluginRootVar)
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
if (targets && targets.codex) {
|
|
153
|
-
outputs.push({
|
|
154
|
-
path: targets.codex.marketplace,
|
|
155
|
-
json: { name: identity.name, interface: { displayName: ui.displayName }, plugins: [{ name: identity.name, source: { source: "local", path: layout.pluginPathFromRepoRoot }, policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }, category: ui.category }] }
|
|
156
|
-
});
|
|
157
|
-
outputs.push({
|
|
158
|
-
path: targets.codex.plugin,
|
|
159
|
-
json: { name: identity.name, version: identity.version, description: descriptions.standard, author: identity.author, keywords: identity.keywords, skills: layout.skillsDir, mcpServers: "./.codex-plugin/mcp.json", interface: { displayName: ui.displayName, shortDescription: descriptions.short, longDescription: descriptions.long, developerName: identity.author.name, category: ui.category, capabilities: ui.capabilities, brandColor: ui.brandColor, defaultPrompt: ui.defaultPrompt } }
|
|
160
|
-
});
|
|
161
|
-
outputs.push({
|
|
162
|
-
path: targets.codex.mcp,
|
|
163
|
-
json: legacyMcpConfig(mcp, layout, targets.codex.pluginRootVar)
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
if (targets && targets.agents) {
|
|
167
|
-
outputs.push({
|
|
168
|
-
path: targets.agents.plugin,
|
|
169
|
-
json: { name: identity.name, description: descriptions.standard, version: identity.version, author: identity.author, homepage: identity.homepage, license: identity.license, keywords: identity.keywords, skills: layout.skillsDir, mcpServers: "./.codex-plugin/mcp.json", interface: { displayName: ui.displayName, shortDescription: descriptions.short, longDescription: descriptions.long, developerName: identity.author.name, category: ui.category, capabilities: ui.capabilities, brandColor: ui.brandColor, defaultPrompt: ui.defaultPrompt } }
|
|
170
|
-
});
|
|
171
|
-
outputs.push({
|
|
172
|
-
path: targets.agents.mcp,
|
|
173
|
-
json: legacyMcpConfig(mcp, layout, targets.agents.pluginRootVar)
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return outputs;
|
|
136
|
+
function isPlainObject(value) {
|
|
137
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
178
138
|
}
|
|
179
139
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
140
|
+
/** The vendor table is required. A broken source must stop before the write
|
|
141
|
+
* loop; an old hard-coded shape is not a safe second source of truth. */
|
|
142
|
+
function validateVendorRegistry(vendors) {
|
|
143
|
+
if (!isPlainObject(vendors) || Object.keys(vendors).length === 0) {
|
|
144
|
+
throw new Error("manifest source needs a non-empty vendors object");
|
|
145
|
+
}
|
|
146
|
+
for (const [vendorId, vendorDef] of Object.entries(vendors)) {
|
|
147
|
+
if (!isPlainObject(vendorDef) || !Array.isArray(vendorDef.outputs) || vendorDef.outputs.length === 0) {
|
|
148
|
+
throw new Error(`vendor ${vendorId} needs a non-empty outputs array`);
|
|
149
|
+
}
|
|
150
|
+
for (const output of vendorDef.outputs) {
|
|
151
|
+
if (!isPlainObject(output)) throw new Error(`vendor ${vendorId} has an invalid output`);
|
|
187
152
|
}
|
|
188
|
-
}
|
|
153
|
+
}
|
|
189
154
|
}
|
|
190
155
|
|
|
191
156
|
function serialize(json) {
|
|
@@ -229,4 +194,10 @@ function main() {
|
|
|
229
194
|
}
|
|
230
195
|
}
|
|
231
196
|
|
|
232
|
-
|
|
197
|
+
try {
|
|
198
|
+
main();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
process.stderr.write(`gen-manifests: ${message}\n`);
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
}
|
package/scripts/golden-path.js
CHANGED
|
@@ -33,7 +33,7 @@ function main() {
|
|
|
33
33
|
const appValidation = runJson(["app", "validate", "end-to-end-golden-path"], pluginRoot);
|
|
34
34
|
assert.equal(appValidation.valid, true);
|
|
35
35
|
assert.equal(appValidation.summary.id, "end-to-end-golden-path");
|
|
36
|
-
assert.equal(appValidation.summary.version, "0.2.
|
|
36
|
+
assert.equal(appValidation.summary.version, "0.2.6");
|
|
37
37
|
|
|
38
38
|
const plan = runJson(
|
|
39
39
|
[
|
|
@@ -42,7 +42,7 @@ function main() {
|
|
|
42
42
|
"--repo",
|
|
43
43
|
tmp,
|
|
44
44
|
"--question",
|
|
45
|
-
"Prove the deterministic v0.2.
|
|
45
|
+
"Prove the deterministic v0.2.6 end-to-end golden path."
|
|
46
46
|
],
|
|
47
47
|
pluginRoot
|
|
48
48
|
);
|
|
@@ -52,7 +52,7 @@ function main() {
|
|
|
52
52
|
|
|
53
53
|
let state = readJson(plan.statePath);
|
|
54
54
|
assert.equal(state.workflow.app.id, "end-to-end-golden-path");
|
|
55
|
-
assert.equal(state.workflow.app.version, "0.2.
|
|
55
|
+
assert.equal(state.workflow.app.version, "0.2.6");
|
|
56
56
|
assert.equal(state.loopStage, "interpret");
|
|
57
57
|
|
|
58
58
|
const dispatch = runJson(["dispatch", plan.runId, "--limit", "1", "--sandbox", "readonly"], tmp);
|
|
@@ -195,7 +195,7 @@ function main() {
|
|
|
195
195
|
assert.equal(reportPath, plan.reportPath);
|
|
196
196
|
assert.ok(fs.existsSync(reportPath));
|
|
197
197
|
const report = fs.readFileSync(reportPath, "utf8");
|
|
198
|
-
assert.match(report, /Workflow App: end-to-end-golden-path@0\.2\.
|
|
198
|
+
assert.match(report, /Workflow App: end-to-end-golden-path@0\.2\.6/);
|
|
199
199
|
assert.match(report, /## Candidates/);
|
|
200
200
|
assert.match(report, /## Trust Audit/);
|
|
201
201
|
assert.match(report, /## Acceptance Rationale/);
|
package/scripts/parity-check.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
//
|
|
9
9
|
// FAIL CLOSED ON DRIFT (BSD discipline, same shape as gen-manifests --check):
|
|
10
10
|
// 1. STATIC parity — the declared capability registry must exactly match the
|
|
11
|
-
// live MCP tool list (tools/list)
|
|
12
|
-
// the
|
|
13
|
-
//
|
|
11
|
+
// live MCP tool list (tools/list), every declared CLI path must resolve
|
|
12
|
+
// through the live dispatcher, and real `cw help` must match the declared
|
|
13
|
+
// help surface. Any gap is release-blocking.
|
|
14
14
|
// 2. PAYLOAD parity — for every capability declared `payloadIdentical`, the
|
|
15
15
|
// `cw <cmd> --json` payload must equal the `cw_<tool>` MCP result on a real
|
|
16
16
|
// bootstrap run (whitespace + generation-moment ISO timestamps aside).
|
|
@@ -38,7 +38,6 @@ const node = process.execPath;
|
|
|
38
38
|
const cli = path.join(pluginRoot, "dist", "cli.js");
|
|
39
39
|
const mcpServer = path.join(pluginRoot, "dist", "mcp-server.js");
|
|
40
40
|
const registry = require(path.join(pluginRoot, "dist", "core", "capability-table.js"));
|
|
41
|
-
const { formatHelp } = require(path.join(pluginRoot, "dist", "core", "format", "help.js"));
|
|
42
41
|
const { loadRunFromCwd, saveCheckpoint } = require(path.join(pluginRoot, "dist", "shell", "run-store.js"));
|
|
43
42
|
const { appendRunNode, createStateNode } = require(path.join(pluginRoot, "dist", "core", "state", "state-node.js"));
|
|
44
43
|
|
|
@@ -71,15 +70,6 @@ function liveMcpTools() {
|
|
|
71
70
|
return JSON.parse(line).result.tools.map((tool) => tool.name);
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
function cliDispatchTokens() {
|
|
75
|
-
// v2 dispatch is table-driven (findCapabilityByCliPath over REGISTRY), not a
|
|
76
|
-
// switch, so the token set is the union of every CLI capability's accepted
|
|
77
|
-
// first tokens (cli.caseTokens where an alias set is declared, else cli.path).
|
|
78
|
-
// The reachability check below then proves each declared path resolves through
|
|
79
|
-
// the live dispatcher — replacing the old `case "x":` dist source-grep.
|
|
80
|
-
return [...new Set(registry.cliCapabilities().flatMap((cap) => cap.cli.caseTokens ?? cap.cli.path))];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
73
|
// Old-build top-level verbs kept in the frozen help "More commands" index line
|
|
84
74
|
// but folded in v2 into subcommands / the shell layer: init->app.init,
|
|
85
75
|
// search->run.search (declaredCliHelpTokens collapses both to app/run), and
|
|
@@ -87,8 +77,8 @@ function cliDispatchTokens() {
|
|
|
87
77
|
// discovery text, not dispatch-parity tokens — exclude them.
|
|
88
78
|
const HELP_INDEX_ONLY_TOKENS = new Set(["init", "search", "update"]);
|
|
89
79
|
|
|
90
|
-
function cliHelpTokens() {
|
|
91
|
-
const lines =
|
|
80
|
+
function cliHelpTokens(helpText) {
|
|
81
|
+
const lines = helpText.split(/\r?\n/);
|
|
92
82
|
const tokens = new Set();
|
|
93
83
|
for (const line of lines) {
|
|
94
84
|
// Command lines start with exactly 2 spaces; examples are 4-space.
|
|
@@ -104,6 +94,29 @@ function cliHelpTokens() {
|
|
|
104
94
|
return [...tokens].sort();
|
|
105
95
|
}
|
|
106
96
|
|
|
97
|
+
function liveCliHelpTokens() {
|
|
98
|
+
const helpText = execFileSync(node, [cli, "help", "--no-color"], {
|
|
99
|
+
cwd: pluginRoot,
|
|
100
|
+
encoding: "utf8",
|
|
101
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
102
|
+
});
|
|
103
|
+
return cliHelpTokens(helpText);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The CLI dispatcher calls this same lookup. Keep the probe outside the
|
|
107
|
+
* registry report so the live gate does not compare a token list made from the
|
|
108
|
+
* registry with another list made from the same registry. */
|
|
109
|
+
function cliReachabilityIssues(table) {
|
|
110
|
+
const issues = [];
|
|
111
|
+
for (const cap of table.cliCapabilities()) {
|
|
112
|
+
const resolved = table.findCapabilityByCliPath(cap.cli.path);
|
|
113
|
+
if (!resolved || !resolved.cli || typeof resolved.cli.handler !== "function") {
|
|
114
|
+
issues.push(`${cap.capability}: CLI path ${cap.cli.path.join(" ")} does not resolve through the dispatcher`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return issues;
|
|
118
|
+
}
|
|
119
|
+
|
|
107
120
|
// ---- 2. payload identity ---------------------------------------------------
|
|
108
121
|
// Generation-moment ISO timestamps are presentation metadata, not capability
|
|
109
122
|
// data: the same field carries the wall-clock instant of the render. Neutralize
|
|
@@ -907,9 +920,11 @@ async function payloadParity() {
|
|
|
907
920
|
async function main() {
|
|
908
921
|
const check = process.argv.includes("--check");
|
|
909
922
|
const tools = liveMcpTools();
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
const
|
|
923
|
+
const helpTokens = liveCliHelpTokens();
|
|
924
|
+
const report = registry.buildParityReport({ mcpTools: tools, helpTokens });
|
|
925
|
+
const reachabilityIssues = cliReachabilityIssues(registry);
|
|
926
|
+
report.registryLint.push(...reachabilityIssues);
|
|
927
|
+
report.ok = report.ok && reachabilityIssues.length === 0;
|
|
913
928
|
const payload = await payloadParity();
|
|
914
929
|
|
|
915
930
|
const ok = report.ok && payload.mismatches.length === 0;
|
|
@@ -940,13 +955,17 @@ async function main() {
|
|
|
940
955
|
if (report.payloadProbeInvalidClassifications.length) lines.push(` - payload probe classifications for invalid capabilities: ${report.payloadProbeInvalidClassifications.join(", ")}`);
|
|
941
956
|
if (report.registryLint.length) lines.push(` - registry lint: ${report.registryLint.join("; ")}`);
|
|
942
957
|
if (payload.mismatches.length) lines.push(` - cw --json != cw_<tool> payload for: ${payload.mismatches.join(", ")}`);
|
|
943
|
-
lines.push("Reconcile src/capability-
|
|
958
|
+
lines.push("Reconcile src/wiring/capability-table, src/cli, and src/mcp so both surfaces render one data source.\n");
|
|
944
959
|
process.stderr.write(lines.join("\n"));
|
|
945
960
|
process.exitCode = 1;
|
|
946
961
|
}
|
|
947
962
|
}
|
|
948
963
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
964
|
+
module.exports = { cliHelpTokens, cliReachabilityIssues };
|
|
965
|
+
|
|
966
|
+
if (require.main === module) {
|
|
967
|
+
main().catch((error) => {
|
|
968
|
+
process.stderr.write(`parity-check: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
969
|
+
process.exitCode = 1;
|
|
970
|
+
});
|
|
971
|
+
}
|
package/scripts/release-flow.js
CHANGED
|
@@ -229,12 +229,12 @@ function buildReviewerInput(resultPath) {
|
|
|
229
229
|
" <one concrete sentence: the user-visible capability this ships>",
|
|
230
230
|
"or:",
|
|
231
231
|
" REJECTED",
|
|
232
|
-
"
|
|
232
|
+
" kind: semantic-review",
|
|
233
|
+
" 1. <a semantic code finding with repo-relative file:line evidence>",
|
|
233
234
|
"",
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"lines; prose such as 'Verdict: APPROVED' is not trusted.",
|
|
235
|
+
"The control plane already ran the deterministic gate. Do not run it again",
|
|
236
|
+
"or reject with an unsupported claim that it failed. A rejection without the",
|
|
237
|
+
"semantic-review form above is invalid reviewer output and remains fail closed.",
|
|
238
238
|
""
|
|
239
239
|
].join("\n");
|
|
240
240
|
}
|
|
@@ -271,7 +271,7 @@ function extractVerdictFromStdout(stdout, resultPath) {
|
|
|
271
271
|
const rejectedLine = lines.find((line) => isRejectedLine(line));
|
|
272
272
|
if (rejectedLine) {
|
|
273
273
|
process.stderr.write(`reviewer REJECTED via stdout — full output:\n${stdout.trim()}\n`);
|
|
274
|
-
return rejectedLine.trim();
|
|
274
|
+
return lines.slice(lines.indexOf(rejectedLine)).join("\n").trim();
|
|
275
275
|
}
|
|
276
276
|
const approvedLine = lines.find((line) => /^APPROVED\s+\S+/.test(line.trim()));
|
|
277
277
|
if (approvedLine) {
|
|
@@ -432,14 +432,19 @@ function verifyVerdict(resultPath) {
|
|
|
432
432
|
const text = fs.readFileSync(resultPath, "utf8");
|
|
433
433
|
const lines = text.split(/\r?\n/);
|
|
434
434
|
const firstLine = lines[0] || "";
|
|
435
|
+
if (isRejectedLine(firstLine)) {
|
|
436
|
+
const rejection = validateSemanticRejection(lines);
|
|
437
|
+
if (!rejection.ok) {
|
|
438
|
+
die(`invalid reviewer output — ${rejection.reason}. Release remains blocked; this is not a verified code rejection.`, text.trim());
|
|
439
|
+
}
|
|
440
|
+
die("reviewer rejected the release with semantic evidence.", text.trim());
|
|
441
|
+
}
|
|
435
442
|
if (firstLine !== `APPROVED ${HEAD}`) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return (normalizedLines[1] || "").trim();
|
|
442
|
-
}
|
|
443
|
+
const normalized = extractVerdictFromStdout(text, resultPath);
|
|
444
|
+
if (normalized && normalized.split(/\r?\n/)[0] === `APPROVED ${HEAD}`) {
|
|
445
|
+
fs.writeFileSync(resultPath, `${normalized}\n`);
|
|
446
|
+
const normalizedLines = normalized.split(/\r?\n/);
|
|
447
|
+
return (normalizedLines[1] || "").trim();
|
|
443
448
|
}
|
|
444
449
|
die(`verdict first line must be exactly "APPROVED ${HEAD}" — release blocked.`, text.trim());
|
|
445
450
|
}
|
|
@@ -447,6 +452,31 @@ function verifyVerdict(resultPath) {
|
|
|
447
452
|
return cap;
|
|
448
453
|
}
|
|
449
454
|
|
|
455
|
+
function validateSemanticRejection(lines) {
|
|
456
|
+
if (lines[0] !== "REJECTED") {
|
|
457
|
+
return { ok: false, reason: 'a rejection must start with exact "REJECTED"' };
|
|
458
|
+
}
|
|
459
|
+
if (lines[1] !== "kind: semantic-review") {
|
|
460
|
+
return { ok: false, reason: 'a rejection must declare exact "kind: semantic-review"' };
|
|
461
|
+
}
|
|
462
|
+
const findings = lines.slice(2).filter((line) => /^\d+\.\s+/.test(line));
|
|
463
|
+
if (!findings.length) return { ok: false, reason: "a semantic rejection needs a numbered finding" };
|
|
464
|
+
for (const finding of findings) {
|
|
465
|
+
const match = finding.match(/^\d+\.\s+.*?\b([A-Za-z0-9][A-Za-z0-9_./-]*):(\d+)\b/);
|
|
466
|
+
if (!match) return { ok: false, reason: "each semantic finding needs a repo-relative file:line locator" };
|
|
467
|
+
const rel = match[1];
|
|
468
|
+
const line = Number(match[2]);
|
|
469
|
+
if (rel.startsWith("/") || rel.split("/").includes("..") || !Number.isSafeInteger(line) || line < 1) {
|
|
470
|
+
return { ok: false, reason: "a semantic finding has an unsafe file:line locator" };
|
|
471
|
+
}
|
|
472
|
+
const target = path.join(repoRoot, rel);
|
|
473
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
|
|
474
|
+
return { ok: false, reason: `semantic evidence path does not exist: ${rel}` };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return { ok: true };
|
|
478
|
+
}
|
|
479
|
+
|
|
450
480
|
// ---- verdict signing (optional, opt-in — see the header comment) -----------
|
|
451
481
|
function resolveVerdictPrivateKey(value) {
|
|
452
482
|
const trimmed = String(value).trim();
|
package/scripts/release-gate.js
CHANGED
|
@@ -67,23 +67,47 @@ if (!PREV_TAG) {
|
|
|
67
67
|
const MARKER_DIR = path.join(REPO_ROOT, ".cw-release");
|
|
68
68
|
fs.mkdirSync(MARKER_DIR, { recursive: true });
|
|
69
69
|
|
|
70
|
+
const MAX_CHILD_DIAGNOSTIC_BYTES = 16 * 1024;
|
|
71
|
+
|
|
72
|
+
function boundedDiagnostic(text) {
|
|
73
|
+
const value = String(text || "").trim();
|
|
74
|
+
if (value.length <= MAX_CHILD_DIAGNOSTIC_BYTES) return value;
|
|
75
|
+
return `…${value.slice(-MAX_CHILD_DIAGNOSTIC_BYTES)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function reportNpmFailure(label, result) {
|
|
79
|
+
// Failure facts are local diagnostics, never stdout data and never a saved
|
|
80
|
+
// release record. Keep the tail so a large smoke run cannot flood a terminal.
|
|
81
|
+
const out = boundedDiagnostic(result.stdout);
|
|
82
|
+
const err = boundedDiagnostic(result.stderr);
|
|
83
|
+
process.stderr.write(`release-gate: ${label} failed (exit ${result.status ?? "unknown"})\n`);
|
|
84
|
+
if (err) process.stderr.write(`[stderr]\n${err}\n`);
|
|
85
|
+
if (out) process.stderr.write(`[stdout]\n${out}\n`);
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
function runNpm(args, extraEnv) {
|
|
71
|
-
// stdio ignored on purpose (the old script sent both streams to /dev/null):
|
|
72
|
-
// the gate's verdict is the exit code; a red step is re-run by hand for detail.
|
|
73
89
|
const r = spawnSync("npm", args, {
|
|
74
90
|
cwd: REPO_ROOT,
|
|
75
|
-
|
|
91
|
+
encoding: "utf8",
|
|
76
92
|
env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
|
|
77
93
|
});
|
|
78
|
-
return
|
|
94
|
+
return r;
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
// --- 1. Build & tests (run, don't trust pasted output) -----------------
|
|
82
98
|
say("[1/6] build");
|
|
83
|
-
|
|
99
|
+
const build = runNpm(["run", "--prefix", "plugins/cool-workflow", "build"]);
|
|
100
|
+
if (build.error || build.status !== 0) {
|
|
101
|
+
fail("build failed");
|
|
102
|
+
reportNpmFailure("build", build);
|
|
103
|
+
}
|
|
84
104
|
|
|
85
105
|
say("[2/6] tests");
|
|
86
|
-
|
|
106
|
+
const tests = runNpm(["run", "test:gate", "--prefix", "plugins/cool-workflow"], { CW_TEST_CONCURRENCY: "1" });
|
|
107
|
+
if (tests.error || tests.status !== 0) {
|
|
108
|
+
fail("tests failed");
|
|
109
|
+
reportNpmFailure("tests", tests);
|
|
110
|
+
}
|
|
87
111
|
|
|
88
112
|
if (PREV_TAG) {
|
|
89
113
|
const RANGE = `${PREV_TAG}..HEAD`;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"domains": [
|
|
4
|
+
{ "domain": "agent-config", "constant": "AGENT_CONFIG_SCHEMA_VERSION", "source": "shell/agent-config.ts" },
|
|
5
|
+
{ "domain": "blackboard", "constant": "BLACKBOARD_SCHEMA_VERSION", "source": "core/multi-agent/coordinator.ts" },
|
|
6
|
+
{ "domain": "candidate", "constant": "CANDIDATE_SCHEMA_VERSION", "source": "core/multi-agent/candidate-scoring.ts" },
|
|
7
|
+
{ "domain": "collaboration", "constant": "COLLABORATION_SCHEMA_VERSION", "source": "core/multi-agent/collaboration.ts" },
|
|
8
|
+
{ "domain": "contract-migration", "constant": "CONTRACT_MIGRATION_SCHEMA_VERSION", "source": "core/state/contract-migration.ts" },
|
|
9
|
+
{ "domain": "current-run-state", "constant": "CURRENT_RUN_STATE_SCHEMA_VERSION", "source": "core/version.ts" },
|
|
10
|
+
{ "domain": "drive", "constant": "DRIVE_SCHEMA_VERSION", "source": "core/pipeline/drive-decide.ts" },
|
|
11
|
+
{ "domain": "error-feedback", "constant": "ERROR_FEEDBACK_SCHEMA_VERSION", "source": "core/pipeline/error-feedback.ts" },
|
|
12
|
+
{ "domain": "evidence-reasoning", "constant": "EVIDENCE_REASONING_SCHEMA_VERSION", "source": "shell/evidence-reasoning.ts" },
|
|
13
|
+
{ "domain": "execution-backend", "constant": "EXECUTION_BACKEND_SCHEMA_VERSION", "source": "shell/execution-backend/registry.ts" },
|
|
14
|
+
{ "domain": "legacy-run-state", "constant": "LEGACY_RUN_STATE_SCHEMA_VERSION", "source": "core/version.ts" },
|
|
15
|
+
{ "domain": "metrics", "constant": "METRICS_SCHEMA_VERSION", "source": "shell/observability.ts" },
|
|
16
|
+
{ "domain": "minimum-run-state", "constant": "MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION", "source": "core/version.ts" },
|
|
17
|
+
{ "domain": "multi-agent", "constant": "MULTI_AGENT_SCHEMA_VERSION", "source": "core/multi-agent/runtime.ts" },
|
|
18
|
+
{ "domain": "node-snapshot", "constant": "NODE_SNAPSHOT_SCHEMA_VERSION", "source": "core/state/node-snapshot.ts" },
|
|
19
|
+
{ "domain": "pipeline-contract", "constant": "PIPELINE_CONTRACT_SCHEMA_VERSION", "source": "core/state/state-node.ts" },
|
|
20
|
+
{ "domain": "run-registry", "constant": "RUN_REGISTRY_SCHEMA_VERSION", "source": "shell/run-registry-io.ts" },
|
|
21
|
+
{ "domain": "sandbox-profile", "constant": "SANDBOX_PROFILE_SCHEMA_VERSION", "source": "shell/sandbox-profile.ts" },
|
|
22
|
+
{ "domain": "scheduling", "constant": "SCHEDULING_SCHEMA_VERSION", "source": "shell/scheduling-io.ts" },
|
|
23
|
+
{ "domain": "state-explosion", "constant": "STATE_EXPLOSION_SCHEMA_VERSION", "source": "core/state/state-explosion/size.ts" },
|
|
24
|
+
{ "domain": "state-node", "constant": "STATE_NODE_SCHEMA_VERSION", "source": "core/state/state-node.ts" },
|
|
25
|
+
{ "domain": "telemetry-ledger", "constant": "TELEMETRY_LEDGER_SCHEMA_VERSION", "source": "core/trust/telemetry-ledger.ts" },
|
|
26
|
+
{ "domain": "topology", "constant": "TOPOLOGY_SCHEMA_VERSION", "source": "core/multi-agent/topology.ts" },
|
|
27
|
+
{ "domain": "trust-audit", "constant": "TRUST_AUDIT_SCHEMA_VERSION", "source": "shell/trust-audit.ts" },
|
|
28
|
+
{ "domain": "worker-isolation", "constant": "WORKER_ISOLATION_SCHEMA_VERSION", "source": "shell/worker-isolation.ts" },
|
|
29
|
+
{ "domain": "workflow-app", "constant": "WORKFLOW_APP_SCHEMA_VERSION", "source": "core/version.ts" }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -19,6 +19,8 @@ const path = require("node:path");
|
|
|
19
19
|
const PLUGIN_ROOT = path.resolve(__dirname, "..");
|
|
20
20
|
const TYPES_FILE = path.join(PLUGIN_ROOT, "src", "core", "state", "types.ts");
|
|
21
21
|
const SCHEMA_MODULE = path.join(PLUGIN_ROOT, "dist", "core", "state", "schema.js");
|
|
22
|
+
const SOURCE_ROOT = path.join(PLUGIN_ROOT, "src");
|
|
23
|
+
const VERSION_INVENTORY_FILE = path.join(__dirname, "schema-version-inventory.json");
|
|
22
24
|
|
|
23
25
|
function main() {
|
|
24
26
|
const errors = [];
|
|
@@ -43,6 +45,15 @@ function main() {
|
|
|
43
45
|
const requiredKeys = new Set(schema.REQUIRED_TOP_LEVEL_KEYS || []);
|
|
44
46
|
const optionalKeys = new Set(schema.OPTIONAL_TOP_LEVEL_KEYS || []);
|
|
45
47
|
|
|
48
|
+
let versionInventory;
|
|
49
|
+
try {
|
|
50
|
+
versionInventory = JSON.parse(fs.readFileSync(VERSION_INVENTORY_FILE, "utf8"));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`validate-run-state-schema: FATAL — cannot load ${VERSION_INVENTORY_FILE}.`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
errors.push(...validateSchemaVersionInventory(versionInventory, readTypeScriptSources(SOURCE_ROOT)));
|
|
56
|
+
|
|
46
57
|
// ---- 3. Cross-check: every type key must be either required or optional ----
|
|
47
58
|
for (const key of runKeys) {
|
|
48
59
|
if (requiredKeys.has(key)) continue;
|
|
@@ -70,9 +81,8 @@ function main() {
|
|
|
70
81
|
console.error("validate-run-state-schema: FAIL CLOSED — schema drift detected.");
|
|
71
82
|
for (const e of errors) console.error(` ${e}`);
|
|
72
83
|
console.error(
|
|
73
|
-
`\nFix: update
|
|
74
|
-
`
|
|
75
|
-
`in src/state-migrations.ts to handle any new required fields.`
|
|
84
|
+
`\nFix: update the run-state key lists or schema-version-inventory.json and its canonical source. ` +
|
|
85
|
+
`Keep each schema domain separate.`
|
|
76
86
|
);
|
|
77
87
|
process.exit(1);
|
|
78
88
|
}
|
|
@@ -82,6 +92,85 @@ function main() {
|
|
|
82
92
|
`validate-run-state-schema: ok — ${runKeys.size} WorkflowRun keys accounted for ` +
|
|
83
93
|
`(${requiredKeys.size} required + ${optionalKeys.size} optional = ${total})`
|
|
84
94
|
);
|
|
95
|
+
console.error(
|
|
96
|
+
`validate-run-state-schema: ok — ${versionInventory.domains.length} schema version domains have one definition each`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readTypeScriptSources(root, base = root) {
|
|
101
|
+
const files = [];
|
|
102
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
103
|
+
const absolute = path.join(root, entry.name);
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
files.push(...readTypeScriptSources(absolute, base));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!entry.isFile() || !entry.name.endsWith(".ts")) continue;
|
|
109
|
+
files.push({
|
|
110
|
+
path: path.relative(base, absolute).split(path.sep).join("/"),
|
|
111
|
+
source: fs.readFileSync(absolute, "utf8"),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return files;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateSchemaVersionInventory(inventory, sourceFiles) {
|
|
118
|
+
const errors = [];
|
|
119
|
+
if (!inventory || inventory.schemaVersion !== 1 || !Array.isArray(inventory.domains)) {
|
|
120
|
+
return ["Schema version inventory must have schemaVersion 1 and a domains array."];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const entriesByConstant = new Map();
|
|
124
|
+
const domains = new Set();
|
|
125
|
+
for (const entry of inventory.domains) {
|
|
126
|
+
if (!entry || typeof entry.domain !== "string" || typeof entry.constant !== "string" || typeof entry.source !== "string") {
|
|
127
|
+
errors.push("Schema version inventory entries need domain, constant, and source strings.");
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (domains.has(entry.domain)) errors.push(`Schema version domain "${entry.domain}" is listed more than once.`);
|
|
131
|
+
domains.add(entry.domain);
|
|
132
|
+
if (entriesByConstant.has(entry.constant)) {
|
|
133
|
+
errors.push(`Schema version constant "${entry.constant}" is listed more than once.`);
|
|
134
|
+
} else {
|
|
135
|
+
entriesByConstant.set(entry.constant, entry);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const definitions = new Map();
|
|
140
|
+
const pattern = /^\s*(?:export\s+)?const\s+([A-Z][A-Z0-9_]*_SCHEMA_VERSION)\s*(?::[^=;]+)?=/gm;
|
|
141
|
+
for (const file of sourceFiles) {
|
|
142
|
+
pattern.lastIndex = 0;
|
|
143
|
+
let match;
|
|
144
|
+
while ((match = pattern.exec(file.source)) !== null) {
|
|
145
|
+
const found = definitions.get(match[1]) || [];
|
|
146
|
+
found.push(file.path);
|
|
147
|
+
definitions.set(match[1], found);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const [constant, sources] of definitions) {
|
|
152
|
+
const entry = entriesByConstant.get(constant);
|
|
153
|
+
if (!entry) {
|
|
154
|
+
errors.push(`Unknown schema version domain: "${constant}" has a definition in ${sources.join(", ")}.`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (sources.length > 1) {
|
|
158
|
+
errors.push(`Schema version constant "${constant}" has more than one definition: ${sources.join(", ")}.`);
|
|
159
|
+
}
|
|
160
|
+
for (const source of sources) {
|
|
161
|
+
if (source !== entry.source) {
|
|
162
|
+
errors.push(`Schema version constant "${constant}" must be defined in ${entry.source}, not ${source}.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const [constant, entry] of entriesByConstant) {
|
|
168
|
+
if (!definitions.has(constant)) {
|
|
169
|
+
errors.push(`Schema version inventory entry "${entry.domain}" has no definition for ${constant}.`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return errors;
|
|
85
174
|
}
|
|
86
175
|
|
|
87
176
|
/**
|
|
@@ -123,4 +212,6 @@ function extractInterfaceKeys(source, interfaceName) {
|
|
|
123
212
|
return keys;
|
|
124
213
|
}
|
|
125
214
|
|
|
126
|
-
main();
|
|
215
|
+
if (require.main === module) main();
|
|
216
|
+
|
|
217
|
+
module.exports = { extractInterfaceKeys, validateSchemaVersionInventory };
|