cool-workflow 0.2.4 → 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/mcp.json +10 -0
- package/.gemini-plugin/plugin.json +40 -0
- package/.opencode-plugin/mcp.json +10 -0
- package/.opencode-plugin/plugin.json +40 -0
- package/README.md +204 -48
- 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/cli/dispatch.js +32 -4
- package/dist/cli/entry.js +11 -19
- package/dist/cli/global-flags.js +113 -0
- package/dist/cli/parseargv.js +7 -1
- package/dist/core/capability-data.js +337 -8
- package/dist/core/format/help.js +73 -3
- package/dist/core/format/recovery-hint.js +32 -0
- package/dist/core/multi-agent/collaboration.js +35 -6
- package/dist/core/multi-agent/runtime.js +7 -0
- package/dist/core/multi-agent/trust-policy.js +7 -1
- package/dist/core/pipeline/contract.js +7 -0
- package/dist/core/pipeline/error-feedback.js +2 -2
- package/dist/core/state/run-paths.js +3 -30
- package/dist/core/trust/evidence-grounding.js +13 -1
- package/dist/core/util/cli-args.js +22 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/dispatch.js +22 -2
- package/dist/mcp/server.js +215 -36
- package/dist/mcp/tool-process.js +181 -0
- package/dist/mcp-server.js +28 -1
- package/dist/shell/agent-config.js +11 -2
- package/dist/shell/audit-cli.js +1 -1
- package/dist/shell/commit.js +8 -2
- package/dist/shell/coordinator-io.js +73 -1
- package/dist/shell/drive.js +195 -128
- package/dist/shell/error-feedback-io.js +6 -0
- package/dist/shell/execution-backend/agent.js +201 -26
- package/dist/shell/execution-backend/container.js +44 -10
- package/dist/shell/execution-backend/local.js +33 -0
- package/dist/shell/fs-atomic.js +96 -12
- package/dist/shell/ledger-cli.js +9 -2
- package/dist/shell/metrics-cli.js +1 -1
- package/dist/shell/multi-agent-cli.js +9 -2
- package/dist/shell/multi-agent-operator-ux.js +1 -1
- package/dist/shell/observability.js +7 -4
- package/dist/shell/onramp.js +48 -5
- package/dist/shell/operator-ux.js +1 -1
- package/dist/shell/perf-trace.js +136 -0
- package/dist/shell/pipeline.js +3 -2
- package/dist/shell/reclamation-io.js +10 -9
- package/dist/shell/report-view-cli.js +2 -0
- package/dist/shell/run-export-cli.js +5 -2
- package/dist/shell/run-export.js +293 -31
- package/dist/shell/run-registry-io.js +8 -0
- package/dist/shell/run-store.js +183 -3
- package/dist/shell/scheduler-io.js +101 -10
- package/dist/shell/state-explosion-cli.js +4 -1
- package/dist/shell/telemetry-demo.js +1 -1
- package/dist/shell/telemetry-ledger-io.js +36 -24
- package/dist/shell/trust-audit.js +178 -36
- package/dist/shell/workbench-host.js +121 -10
- package/dist/shell/workbench.js +83 -6
- package/dist/shell/worker-isolation.js +1 -1
- package/dist/wiring/capability-table/basics.js +5 -0
- package/dist/wiring/capability-table/exec-backend.js +40 -22
- package/dist/wiring/capability-table/parity.js +10 -5
- package/dist/wiring/capability-table/pipeline.js +32 -0
- package/dist/wiring/capability-table/registry-core.js +26 -3
- package/dist/wiring/capability-table/reporting.js +7 -1
- package/dist/wiring/capability-table/scheduling-registry.js +8 -2
- package/dist/wiring/capability-table/trust-ledger.js +54 -12
- package/docs/agent-delegation-drive.7.md +17 -0
- package/docs/cli-mcp-parity.7.md +46 -5
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +38 -0
- package/docs/cross-agent-ledger.7.md +20 -8
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/fix.7.md +4 -4
- package/docs/getting-started.md +40 -32
- package/docs/index.md +17 -0
- package/docs/launch/demo.tape +4 -3
- package/docs/mcp-app-surface.7.md +27 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +53 -9
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +12 -0
- package/docs/routine.7.md +22 -0
- package/docs/run-registry-control-plane.7.md +37 -9
- package/docs/run-retention-reclamation.7.md +12 -4
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/trust-audit-anchor.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +72 -2
- package/docs/workflow-app-framework.7.md +31 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +4 -3
- package/package.json +6 -3
- package/scripts/agents/agent-adapter-core.js +26 -0
- package/scripts/agents/claude-p-agent.js +4 -1
- package/scripts/agents/codex-agent.js +4 -0
- package/scripts/agents/cw-attest-wrap.js +1 -1
- package/scripts/agents/gemini-agent.js +4 -0
- package/scripts/agents/opencode-agent.js +5 -0
- package/scripts/block-unapproved-tag.js +160 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +72 -4
- package/scripts/children/http-batch-delegate-child.js +132 -0
- package/scripts/children/http-delegate-child.js +8 -0
- package/scripts/dogfood-release.js +2 -2
- package/scripts/fake-date-for-reproduction.js +1 -1
- package/scripts/gen-manifests.js +32 -61
- package/scripts/golden-path.js +4 -4
- package/scripts/lang-policy-check.js +82 -0
- package/scripts/parity-check.js +42 -23
- package/scripts/purity-baseline.json +0 -3
- package/scripts/purity-gate.js +16 -3
- package/scripts/release-check.js +2 -1
- package/scripts/release-flow.js +169 -41
- package/scripts/release-gate.js +208 -0
- package/scripts/release-oneclick.js +38 -7
- package/scripts/release-tags.js +47 -0
- package/scripts/schema-version-inventory.json +31 -0
- package/scripts/validate-run-state-schema.js +95 -4
- package/scripts/verdict-keygen.js +2 -2
- package/scripts/verify-bump-reproduction.js +193 -0
- package/scripts/verify-release-verdict.js +139 -0
- package/scripts/verify-verdict-signature.js +1 -1
- package/ui/workbench/app.css +50 -10
- package/ui/workbench/app.js +269 -34
- package/ui/workbench/index.html +4 -2
- package/ui/workbench/inspection.js +51 -0
- package/ui/workbench/navigation.js +44 -0
- package/docs/agent-framework.md +0 -177
- package/docs/designs/handoff-ledger.md +0 -145
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
- package/docs/launch/launch-kit.md +0 -195
- package/docs/launch/pre-launch-checklist.md +0 -53
- package/docs/readme-v0.1.87-full.md +0 -301
- package/docs/routines.md +0 -101
- package/docs/scheduled-tasks.md +0 -80
- package/scripts/agents/claude-p-agent.sh +0 -9
- package/scripts/block-unapproved-tag.sh +0 -75
- package/scripts/release-gate.sh +0 -94
- package/scripts/verify-bump-reproduction.sh +0 -148
- package/scripts/verify-container-selfref.js +0 -64
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// release-gate.js — deterministic release checks for cool-workflow.
|
|
4
|
+
// Pass = writes .cw-release/gate-<HEAD-sha>.ok
|
|
5
|
+
// This script encodes everything that does NOT need LLM judgment.
|
|
6
|
+
// (Node port of the former release-gate.sh; behavior kept line for line —
|
|
7
|
+
// same six steps, same messages, same exit map: 0 = PASSED, 1 = REJECTED.)
|
|
8
|
+
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
const { spawnSync } = require("node:child_process");
|
|
12
|
+
const { resolvePrevReleaseTag } = require("./release-tags.js");
|
|
13
|
+
|
|
14
|
+
const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
15
|
+
if (top.status !== 0) {
|
|
16
|
+
process.stderr.write("release-gate: not inside a git work tree\n");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const REPO_ROOT = top.stdout.trim();
|
|
20
|
+
|
|
21
|
+
function gitOut(args) {
|
|
22
|
+
const r = spawnSync("git", args, { cwd: REPO_ROOT, encoding: "utf8" });
|
|
23
|
+
return r.status === 0 ? (r.stdout || "").trim() : "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SHA = gitOut(["rev-parse", "HEAD"]);
|
|
27
|
+
let FAIL = 0;
|
|
28
|
+
function say(msg) {
|
|
29
|
+
process.stdout.write(`${msg}\n`);
|
|
30
|
+
}
|
|
31
|
+
function fail(msg) {
|
|
32
|
+
say(`GATE FAIL: ${msg}`);
|
|
33
|
+
FAIL = 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Resolve the PREVIOUS release tag by SEMVER ORDER, not ancestry — shared
|
|
37
|
+
// with release-flow.js via release-tags.js (see that file's header for the
|
|
38
|
+
// full story: `git describe --tags` walks ancestry and silently SKIPS the
|
|
39
|
+
// true previous release tag under the tag-only-push design, making the
|
|
40
|
+
// substance/test-evidence/cadence checks below compare against the wrong
|
|
41
|
+
// baseline and pass permanently; ITERATION_LOG cycles 35/36).
|
|
42
|
+
const PREV_TAG = resolvePrevReleaseTag(gitOut);
|
|
43
|
+
|
|
44
|
+
// An empty PREV_TAG is ambiguous: (a) the genuine first release, so skipping
|
|
45
|
+
// substance/evidence/cadence is right, or (b) tags DO exist but this clone can
|
|
46
|
+
// not see them — a shallow clone hides them in history, and a `git clone
|
|
47
|
+
// --no-tags` / `fetch-tags: false` clone of a long-tagged repo shows 0 local
|
|
48
|
+
// tags too. (a) and (b) look IDENTICAL from local state (both give an empty
|
|
49
|
+
// describe and 0 tags), so we can NOT auto-tell them apart. Fail CLOSED: skip
|
|
50
|
+
// the checks ONLY when the operator positively declares a first release with
|
|
51
|
+
// CW_FIRST_RELEASE=1 (explicit + logged, the same shape the cadence HOTFIX
|
|
52
|
+
// override uses). Otherwise REJECT — a mis-fetched clone must never look like a
|
|
53
|
+
// first release and silently pass. Found by a 2026-07-12 security check; the
|
|
54
|
+
// shallow signal alone closed only half the hole (the `--no-tags` full clone
|
|
55
|
+
// still slipped through).
|
|
56
|
+
if (!PREV_TAG) {
|
|
57
|
+
const shallow = spawnSync("git", ["rev-parse", "--is-shallow-repository"], { cwd: REPO_ROOT, encoding: "utf8" });
|
|
58
|
+
const IS_SHALLOW = shallow.status === 0 ? shallow.stdout.trim() : "false";
|
|
59
|
+
if (IS_SHALLOW === "true") {
|
|
60
|
+
fail("cannot resolve the previous release tag: this is a shallow git clone (git rev-parse --is-shallow-repository = true), so an older tag may be hidden and substance/test-evidence/cadence cannot be trusted. Fetch full history (actions/checkout with fetch-depth: 0) before running this gate.");
|
|
61
|
+
} else if (process.env.CW_FIRST_RELEASE === "1") {
|
|
62
|
+
say("no previous tag; genuine first release declared (CW_FIRST_RELEASE=1) — substance/evidence/cadence will be skipped");
|
|
63
|
+
} else {
|
|
64
|
+
fail("cannot resolve the previous release tag on a full (non-shallow) clone. Either tags were not fetched (git clone --no-tags, or actions/checkout fetch-tags: false — fetch tags and re-run), or this is the genuine first release (set CW_FIRST_RELEASE=1 to declare that explicitly). Refusing to silently skip substance/test-evidence/cadence.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const MARKER_DIR = path.join(REPO_ROOT, ".cw-release");
|
|
68
|
+
fs.mkdirSync(MARKER_DIR, { recursive: true });
|
|
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
|
+
|
|
88
|
+
function runNpm(args, extraEnv) {
|
|
89
|
+
const r = spawnSync("npm", args, {
|
|
90
|
+
cwd: REPO_ROOT,
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
|
|
93
|
+
});
|
|
94
|
+
return r;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- 1. Build & tests (run, don't trust pasted output) -----------------
|
|
98
|
+
say("[1/6] build");
|
|
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
|
+
}
|
|
104
|
+
|
|
105
|
+
say("[2/6] tests");
|
|
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
|
+
}
|
|
111
|
+
|
|
112
|
+
if (PREV_TAG) {
|
|
113
|
+
const RANGE = `${PREV_TAG}..HEAD`;
|
|
114
|
+
const changedFiles = gitOut(["diff", "--name-only", RANGE]).split("\n").filter(Boolean);
|
|
115
|
+
|
|
116
|
+
// --- 2. Substance: changes must exist outside src/types/ and dist/ ---
|
|
117
|
+
// The spec (AGENTS.md / reviewer-agent.md Gate 1) is "at least one changed
|
|
118
|
+
// file outside src/types/ and dist/" — ANY such file (src, scripts, docs,
|
|
119
|
+
// workflows, tests). Count every changed path that is not under those two
|
|
120
|
+
// generated/declaration-only trees; declared-but-unread spec accretion is the
|
|
121
|
+
// reviewer agent's deeper judgment call, not this deterministic floor.
|
|
122
|
+
say("[3/6] substance (diff outside src/types/ and dist/)");
|
|
123
|
+
const SUBSTANCE = changedFiles.filter((f) => !/^plugins\/cool-workflow\/(src\/types\/|dist\/)/.test(f)).length;
|
|
124
|
+
if (!(SUBSTANCE > 0)) fail(`only types/dist changed since ${PREV_TAG} (spec accretion)`);
|
|
125
|
+
|
|
126
|
+
// --- 3. Test evidence: test files must have changed ------------------
|
|
127
|
+
say("[4/6] test evidence");
|
|
128
|
+
const TESTS_CHANGED = changedFiles.filter((f) => /\.(test|spec)\.|\/tests?\//.test(f)).length;
|
|
129
|
+
if (!(TESTS_CHANGED > 0)) fail(`zero test changes since ${PREV_TAG}`);
|
|
130
|
+
|
|
131
|
+
// --- 4. Cadence: >=4 cycles logged OR >=24h since previous tag, or a recorded HOTFIX ---
|
|
132
|
+
say("[5/6] cadence");
|
|
133
|
+
let CYCLES = 0;
|
|
134
|
+
const logDiff = fs.existsSync(path.join(REPO_ROOT, "ITERATION_LOG.md"))
|
|
135
|
+
? gitOut(["diff", RANGE, "--", "ITERATION_LOG.md"]).split("\n")
|
|
136
|
+
: [];
|
|
137
|
+
CYCLES = logDiff.filter((line) => /^\+.*\|/.test(line)).length;
|
|
138
|
+
const PREV_TS = Number(gitOut(["log", "-1", "--format=%ct", PREV_TAG]));
|
|
139
|
+
const NOW_TS = Math.floor(Date.now() / 1000);
|
|
140
|
+
const HOURS = Math.floor((NOW_TS - PREV_TS) / 3600);
|
|
141
|
+
// Hotfix path: an urgent fix may ship inside the cadence window, but ONLY via an
|
|
142
|
+
// EXPLICIT, RECORDED declaration — a "HOTFIX:" line added to ITERATION_LOG.md in this
|
|
143
|
+
// release range, carrying a reason. It is committed (auditable in the tag's history)
|
|
144
|
+
// and echoed here, so the bypass is never silent and a reviewer sees the reason.
|
|
145
|
+
const hotfixLine = logDiff.find((line) => /^\+.*HOTFIX:/.test(line));
|
|
146
|
+
const HOTFIX = hotfixLine ? hotfixLine.replace(/^\+\s*/, "") : "";
|
|
147
|
+
if (CYCLES < 4 && HOURS < 24) {
|
|
148
|
+
if (HOTFIX) {
|
|
149
|
+
say(` cadence bypassed by recorded HOTFIX (${HOURS}h, ${CYCLES} cycle-lines): ${HOTFIX}`);
|
|
150
|
+
} else {
|
|
151
|
+
fail(`cadence: only ${CYCLES} cycles logged and ${HOURS}h since ${PREV_TAG} (need >=4 cycles, >=24h, or a recorded 'HOTFIX:' line in ITERATION_LOG.md)`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
say("[3-5/6] no previous tag; substance/evidence/cadence checks skipped");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- 5. Branch naming: forbid version-number branches -------------------
|
|
159
|
+
say("[6/6] branch naming");
|
|
160
|
+
// On a normal checkout `git rev-parse --abbrev-ref HEAD` is the branch name. On
|
|
161
|
+
// a DETACHED HEAD it prints the literal string "HEAD" — and the tag-push CI
|
|
162
|
+
// (release-gate.yml) ALWAYS checks out the tag, so HEAD is detached there. A
|
|
163
|
+
// literal "HEAD" can never match the version-branch regex below, so this check
|
|
164
|
+
// would silently pass exactly where it is meant to be the backstop. Handle the
|
|
165
|
+
// detached case explicitly: gather the real candidate ref name(s) — the
|
|
166
|
+
// CI-provided source branch (GITHUB_HEAD_REF for a PR, else GITHUB_REF_NAME),
|
|
167
|
+
// plus every local/remote branch whose tip contains this commit — and judge
|
|
168
|
+
// each one. A truly detached checkout with no resolvable branch (a bare
|
|
169
|
+
// `git checkout <sha>`) has no branch to name, so there is nothing to forbid;
|
|
170
|
+
// that is now an explicit, understood pass, not an accidental regex miss.
|
|
171
|
+
const BRANCH = gitOut(["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
|
|
172
|
+
let candidateBranches = [BRANCH];
|
|
173
|
+
if (BRANCH === "HEAD") {
|
|
174
|
+
candidateBranches = [];
|
|
175
|
+
// The CI source branch: a PR run exposes it as GITHUB_HEAD_REF; a plain push
|
|
176
|
+
// exposes the pushed ref as GITHUB_REF_NAME (a tag on a tag push, which simply
|
|
177
|
+
// will not match the feat/ regex — harmless to include).
|
|
178
|
+
for (const envRef of [process.env.GITHUB_HEAD_REF || "", process.env.GITHUB_REF_NAME || ""]) {
|
|
179
|
+
if (envRef) candidateBranches.push(envRef);
|
|
180
|
+
}
|
|
181
|
+
// Every local branch whose tip contains this commit, plus every remote-tracking
|
|
182
|
+
// branch (with its "<remote>/" prefix stripped so the regex still anchors on
|
|
183
|
+
// "feat/"). --format avoids the "* " current-branch marker git branch prints.
|
|
184
|
+
const localC = gitOut(["branch", "--contains", "HEAD", "--format=%(refname:short)"]);
|
|
185
|
+
const remoteC = gitOut(["branch", "-r", "--contains", "HEAD", "--format=%(refname:short)"])
|
|
186
|
+
.split("\n")
|
|
187
|
+
.map((b) => b.replace(/^[^/]+\//, ""))
|
|
188
|
+
.join("\n");
|
|
189
|
+
for (const chunk of [localC, remoteC]) {
|
|
190
|
+
if (chunk) candidateBranches.push(...chunk.split("\n"));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const B of candidateBranches) {
|
|
194
|
+
if (!B || B === "HEAD") continue;
|
|
195
|
+
if (/^feat\/(batch-)?v?[0-9]+/.test(B)) {
|
|
196
|
+
fail(`branch '${B}' is version-number-driven; name the capability instead`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- Verdict ------------------------------------------------------------
|
|
201
|
+
if (FAIL !== 0) {
|
|
202
|
+
fs.rmSync(path.join(MARKER_DIR, `gate-${SHA}.ok`), { force: true });
|
|
203
|
+
say(`RELEASE GATE: REJECTED (${SHA})`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fs.writeFileSync(path.join(MARKER_DIR, `gate-${SHA}.ok`), `${new Date().toISOString().replace(/\.\d{3}Z$/, "Z")}\n`);
|
|
208
|
+
say(`RELEASE GATE: PASSED (${SHA}) — next step: release-reviewer agent must record APPROVED`);
|
|
@@ -120,6 +120,28 @@ function alreadyCut() {
|
|
|
120
120
|
return remote.code === 0 && Boolean(remote.out);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// A cut tags the verdict commit LOCALLY and only then pushes the tag. If that
|
|
124
|
+
// push dies (a network drop, an auth expiry) the tag is left local-only:
|
|
125
|
+
// alreadyCut() still sees it and resumes at stage 3, but stage 3 would then
|
|
126
|
+
// wait 45 minutes for a release-gate run CI never started, because the tag
|
|
127
|
+
// never reached origin. So on resume, before the CI wait, make sure the tag is
|
|
128
|
+
// actually on origin — re-push it (the same tag-only refspec cut() uses) when
|
|
129
|
+
// it is only local. This keeps the tag-only-push contract: no branch is pushed,
|
|
130
|
+
// and the tag commit's first parent is untouched (it is exactly what was cut).
|
|
131
|
+
function ensureTagPushedOnResume() {
|
|
132
|
+
const remote = git(["ls-remote", "origin", `refs/tags/v${version}`]);
|
|
133
|
+
if (remote.code !== 0) {
|
|
134
|
+
die("could not reach origin to check the tag — fix network/auth and re-run to resume.", remote.err);
|
|
135
|
+
}
|
|
136
|
+
if (remote.out) return; // already on origin — nothing to do
|
|
137
|
+
say(`tag v${version} is local-only (the cut's tag push must have failed) — re-pushing refs/tags/v${version}.`);
|
|
138
|
+
const push = git(["push", "origin", `refs/tags/v${version}`]);
|
|
139
|
+
if (push.code !== 0) {
|
|
140
|
+
die("re-push of the local-only tag failed (nothing else was done) — fix the issue and re-run to resume.", push.err);
|
|
141
|
+
}
|
|
142
|
+
say(`re-pushed refs/tags/v${version} to origin.`);
|
|
143
|
+
}
|
|
144
|
+
|
|
123
145
|
// ---- stage 0: preflight -----------------------------------------------------
|
|
124
146
|
function runPreflight() {
|
|
125
147
|
stage(0, "preflight");
|
|
@@ -318,12 +340,12 @@ function runRecordAndWaitStage() {
|
|
|
318
340
|
}
|
|
319
341
|
|
|
320
342
|
say(`waiting for release-gate on tag v${version}...`);
|
|
321
|
-
const
|
|
343
|
+
const gate = poll({
|
|
322
344
|
label: "release-gate wait",
|
|
323
345
|
timeoutMs: 45 * 60 * 1000,
|
|
324
346
|
intervalMs: 20000,
|
|
325
347
|
probe: () => {
|
|
326
|
-
const list = gh(["run", "list", "--workflow", "release-gate", "--limit", "10", "--json", "databaseId,headBranch,status,conclusion"]);
|
|
348
|
+
const list = gh(["run", "list", "--workflow", "release-gate", "--limit", "10", "--json", "databaseId,headBranch,status,conclusion,updatedAt"]);
|
|
327
349
|
if (list.code !== 0) return { error: true };
|
|
328
350
|
let runs;
|
|
329
351
|
try {
|
|
@@ -333,22 +355,29 @@ function runRecordAndWaitStage() {
|
|
|
333
355
|
}
|
|
334
356
|
const gateRun = runs.find((r) => r.headBranch === `v${version}`);
|
|
335
357
|
if (gateRun && gateRun.status === "completed") {
|
|
336
|
-
|
|
358
|
+
// Carry the gate's OWN completion time out of the poll — the npm-publish
|
|
359
|
+
// cutoff below must key off when the gate actually finished, not off the
|
|
360
|
+
// wall clock now (a resume can start long after the gate completed).
|
|
361
|
+
if (gateRun.conclusion === "success") return { done: true, value: { id: gateRun.databaseId, completedMs: Date.parse(gateRun.updatedAt) } };
|
|
337
362
|
die(`release-gate FAILED — inspect: gh run view ${gateRun.databaseId} --log-failed`);
|
|
338
363
|
}
|
|
339
364
|
return {};
|
|
340
365
|
}
|
|
341
366
|
});
|
|
342
|
-
say(`release-gate: SUCCESS (run ${
|
|
367
|
+
say(`release-gate: SUCCESS (run ${gate.id})`);
|
|
343
368
|
|
|
344
369
|
// npm-publish is created only AFTER release-gate completes (workflow_run
|
|
345
370
|
// trigger), so the run to wait for may not exist yet — and the newest
|
|
346
371
|
// completed npm-publish run at this moment is usually the PREVIOUS
|
|
347
372
|
// release's. Only accept a run created at-or-after the moment the gate
|
|
348
|
-
//
|
|
349
|
-
// exists and completes.
|
|
373
|
+
// FINISHED (small clock-skew allowance), and keep waiting until that run
|
|
374
|
+
// exists and completes. The cutoff is the gate run's own completion time,
|
|
375
|
+
// NOT Date.now(): on a resume minutes/days later, a now-based cutoff sits in
|
|
376
|
+
// the future relative to an already-completed npm-publish run and would drop
|
|
377
|
+
// it, hanging the wait for a publish that is already done. Fall back to now
|
|
378
|
+
// only if the gate run reported no parseable completion time.
|
|
350
379
|
say("waiting for npm-publish...");
|
|
351
|
-
const gateDoneMs = Date.now();
|
|
380
|
+
const gateDoneMs = Number.isFinite(gate.completedMs) ? gate.completedMs : Date.now();
|
|
352
381
|
poll({
|
|
353
382
|
label: "npm-publish wait",
|
|
354
383
|
timeoutMs: 30 * 60 * 1000,
|
|
@@ -396,7 +425,9 @@ if (alreadyCut()) {
|
|
|
396
425
|
say(`tag v${version} already exists — the cut already happened; resuming at stage 3 (CI wait + record + confirm).`);
|
|
397
426
|
if (DRY_RUN) {
|
|
398
427
|
say("[dry-run] would resume at stage 3: record PR + wait for release-gate/npm-publish + npm view confirmation.");
|
|
428
|
+
say("[dry-run] would first re-push refs/tags/v" + version + " if it is only local (the cut's tag push had failed).");
|
|
399
429
|
} else {
|
|
430
|
+
ensureTagPushedOnResume();
|
|
400
431
|
runRecordAndWaitStage();
|
|
401
432
|
}
|
|
402
433
|
} else {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// release-tags.js — the ONE semver-ordered release-tag lookup, shared by
|
|
3
|
+
// release-flow.js and release-gate.js so the two can never drift (the gate
|
|
4
|
+
// used to carry a bash port of this logic; a mirror copy is exactly the kind
|
|
5
|
+
// of thing that rots).
|
|
6
|
+
//
|
|
7
|
+
// "What was the last release" is a SEMVER question, not an ancestry question:
|
|
8
|
+
// a cut's own tag commit lives on an ephemeral line that is NEVER merged as
|
|
9
|
+
// an ancestor of `main` (cut() tags the verdict-record commit directly), so
|
|
10
|
+
// `git describe --tags --abbrev=0`, which walks ONLY ancestors, always skips
|
|
11
|
+
// the true previous release tag and silently lands one release further back
|
|
12
|
+
// (verified live: `git describe --tags v0.2.4^` -> v0.2.2, skipping v0.2.3).
|
|
13
|
+
// List every vX.Y.Z tag and pick by version order instead.
|
|
14
|
+
|
|
15
|
+
function parseSemverTag(tag) {
|
|
16
|
+
const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
|
|
17
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compareSemver(a, b) {
|
|
21
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// gitOut: (args: string[]) => string — the caller's own git runner (each
|
|
26
|
+
// caller binds its own repo root), returning trimmed stdout, "" on failure.
|
|
27
|
+
function listReleaseTagsDesc(gitOut) {
|
|
28
|
+
const out = gitOut(["tag", "-l", "v*"]);
|
|
29
|
+
if (!out) return [];
|
|
30
|
+
return out
|
|
31
|
+
.split("\n")
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map((tag) => ({ tag, semver: parseSemverTag(tag) }))
|
|
34
|
+
.filter((t) => t.semver)
|
|
35
|
+
.sort((a, b) => compareSemver(b.semver, a.semver));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The most recent ALREADY-RELEASED tag as of right now: the highest vX.Y.Z
|
|
39
|
+
// tag that does NOT already point at HEAD — so this works whether run before
|
|
40
|
+
// tagging or re-run on a freshly tagged commit.
|
|
41
|
+
function resolvePrevReleaseTag(gitOut) {
|
|
42
|
+
const headTags = new Set(gitOut(["tag", "--points-at", "HEAD"]).split("\n").filter(Boolean));
|
|
43
|
+
const found = listReleaseTagsDesc(gitOut).find((t) => !headTags.has(t.tag));
|
|
44
|
+
return found ? found.tag : "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { parseSemverTag, compareSemver, listReleaseTagsDesc, resolvePrevReleaseTag };
|
|
@@ -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 };
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// The PRIVATE key stays with whoever runs `release-flow.js --cut`/`--check`
|
|
10
10
|
// (CW_RELEASE_VERDICT_PRIVKEY) and NEVER gets committed. The PUBLIC key is
|
|
11
11
|
// committed to the repo at .cw-release/verdict-signing.pub so CI's
|
|
12
|
-
// release-gate.yml, npm-publish.yml, and the local block-unapproved-tag.
|
|
12
|
+
// release-gate.yml, npm-publish.yml, and the local block-unapproved-tag.js
|
|
13
13
|
// hook can all verify a committed verdict without holding any secret.
|
|
14
14
|
//
|
|
15
15
|
// Verification is opt-in and backward compatible: as long as
|
|
@@ -76,7 +76,7 @@ process.stdout.write(
|
|
|
76
76
|
` git -C "$(git rev-parse --show-toplevel)" commit -m "chore: add release-verdict signing public key"`,
|
|
77
77
|
``,
|
|
78
78
|
`Once .cw-release/verdict-signing.pub is committed, release-gate.yml, npm-publish.yml,`,
|
|
79
|
-
`and block-unapproved-tag.
|
|
79
|
+
`and block-unapproved-tag.js all start REQUIRING a valid signature on top of the`,
|
|
80
80
|
`existing APPROVED text check — an unsigned or hand-written verdict fails closed.`,
|
|
81
81
|
``
|
|
82
82
|
].join("\n")
|