cool-workflow 0.2.4 → 0.2.5
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 +202 -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/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 +124 -13
- package/dist/mcp-server.js +20 -0
- package/dist/shell/commit.js +8 -2
- package/dist/shell/coordinator-io.js +73 -1
- package/dist/shell/drive.js +120 -63
- package/dist/shell/error-feedback-io.js +6 -0
- package/dist/shell/execution-backend/agent.js +195 -23
- package/dist/shell/execution-backend/container.js +44 -10
- package/dist/shell/execution-backend/local.js +32 -0
- package/dist/shell/fs-atomic.js +93 -12
- package/dist/shell/ledger-cli.js +9 -2
- package/dist/shell/multi-agent-cli.js +5 -1
- package/dist/shell/onramp.js +48 -5
- package/dist/shell/pipeline.js +2 -1
- package/dist/shell/reclamation-io.js +10 -9
- package/dist/shell/run-export.js +52 -4
- package/dist/shell/run-store.js +156 -0
- package/dist/shell/scheduler-io.js +101 -10
- package/dist/shell/telemetry-ledger-io.js +36 -24
- package/dist/shell/trust-audit.js +104 -10
- package/dist/shell/workbench-host.js +121 -10
- package/dist/shell/workbench.js +79 -5
- 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/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 +15 -0
- package/docs/cli-mcp-parity.7.md +25 -0
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +36 -0
- package/docs/cross-agent-ledger.7.md +20 -8
- 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/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 +6 -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 +39 -9
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +2 -0
- package/docs/routine.7.md +22 -0
- package/docs/run-registry-control-plane.7.md +2 -0
- package/docs/run-retention-reclamation.7.md +10 -4
- 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 +24 -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 +4 -2
- 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/golden-path.js +4 -4
- package/scripts/lang-policy-check.js +82 -0
- package/scripts/purity-gate.js +16 -3
- package/scripts/release-check.js +2 -1
- package/scripts/release-flow.js +128 -30
- package/scripts/release-gate.js +184 -0
- package/scripts/release-oneclick.js +38 -7
- package/scripts/release-tags.js +47 -0
- package/scripts/verdict-keygen.js +2 -2
- package/scripts/verify-bump-reproduction.js +193 -0
- package/scripts/verify-verdict-signature.js +1 -1
- package/ui/workbench/app.css +41 -9
- package/ui/workbench/app.js +160 -27
- package/ui/workbench/index.html +2 -2
- 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
|
@@ -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 };
|
|
@@ -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")
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// verify-bump-reproduction.js — closes the HEAD~1 verdict-replay bypass.
|
|
4
|
+
//
|
|
5
|
+
// release-gate.yml / npm-publish.yml tolerate a verdict written for the tag
|
|
6
|
+
// commit's PARENT (release-flow.js's cut() reviews content, then adds a
|
|
7
|
+
// mechanical version-bump + the verdict as a NEW child commit that actually
|
|
8
|
+
// gets tagged). A validly-signed verdict at the parent proves someone once
|
|
9
|
+
// approved THAT commit — it does NOT prove the child (the thing actually being
|
|
10
|
+
// tagged) is the deterministic bump cut() would have produced, rather than an
|
|
11
|
+
// attacker's own commit smuggling in arbitrary changes on top of an old,
|
|
12
|
+
// genuinely-approved parent (a real signature on a REPLAYED verdict).
|
|
13
|
+
//
|
|
14
|
+
// This script independently reproduces the bump: checks out the approved
|
|
15
|
+
// parent into a scratch worktree, runs the SAME bump:version/sync:project-index
|
|
16
|
+
// steps cut() runs, stages the SAME way cut() stages (git add -u + the
|
|
17
|
+
// explicit verdict/.sig paths, never -A), and requires the resulting tree to
|
|
18
|
+
// match the ACTUAL tagged commit's tree byte-for-byte. Any difference —
|
|
19
|
+
// anywhere, including a single added file — fails closed.
|
|
20
|
+
//
|
|
21
|
+
// (Node port of the former verify-bump-reproduction.sh; behavior kept line
|
|
22
|
+
// for line — same steps, same messages, same exit map.)
|
|
23
|
+
//
|
|
24
|
+
// Usage: verify-bump-reproduction.js <approved-parent-sha> <tagged-sha> <verdict-repo-relative-path> [sig-repo-relative-path]
|
|
25
|
+
//
|
|
26
|
+
// Exit 0: the tagged commit's tree matches the reproduced bump exactly.
|
|
27
|
+
// Exit 1: mismatch, or any step failed (install, bump, sync, worktree, etc).
|
|
28
|
+
|
|
29
|
+
const fs = require("node:fs");
|
|
30
|
+
const os = require("node:os");
|
|
31
|
+
const path = require("node:path");
|
|
32
|
+
const { spawnSync } = require("node:child_process");
|
|
33
|
+
|
|
34
|
+
// Where THIS SCRIPT ITSELF lives — distinct from REPO_ROOT (the git repo being
|
|
35
|
+
// operated ON, resolved below from the invocation cwd). These coincide in
|
|
36
|
+
// ordinary production use (the script runs from within the same checkout it
|
|
37
|
+
// verifies), but conflating them is wrong: REPO_ROOT could be an ARBITRARY git
|
|
38
|
+
// context (e.g. a test harness's throwaway clone of old history, which never
|
|
39
|
+
// has this script's own sibling files, since they postdate whatever was
|
|
40
|
+
// cloned). Sibling files this script depends on must be found relative to
|
|
41
|
+
// where it is installed, never relative to the repo it happens to be pointed at.
|
|
42
|
+
const SCRIPT_DIR = __dirname;
|
|
43
|
+
|
|
44
|
+
function err(msg) {
|
|
45
|
+
process.stderr.write(`${msg}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const [, , PARENT, TAGGED, VERDICT_REL, SIG_REL = ""] = process.argv;
|
|
49
|
+
if (!PARENT || !TAGGED || !VERDICT_REL) {
|
|
50
|
+
err("usage: verify-bump-reproduction.js <parent-sha> <tagged-sha> <verdict-rel-path> [sig-rel-path]");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function run(cmd, args, opts = {}) {
|
|
55
|
+
return spawnSync(cmd, args, { encoding: "utf8", ...opts });
|
|
56
|
+
}
|
|
57
|
+
function gitIn(dir, args, opts = {}) {
|
|
58
|
+
return run("git", ["-C", dir, ...args], opts);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const top = run("git", ["rev-parse", "--show-toplevel"]);
|
|
62
|
+
if (top.status !== 0) process.exit(1);
|
|
63
|
+
const REPO_ROOT = top.stdout.trim();
|
|
64
|
+
|
|
65
|
+
const SCRATCH_PARENT = fs.mkdtempSync(path.join(os.tmpdir(), "cw-bump-repro-"));
|
|
66
|
+
const SCRATCH = path.join(SCRATCH_PARENT, "wt");
|
|
67
|
+
function cleanup() {
|
|
68
|
+
gitIn(REPO_ROOT, ["worktree", "remove", "--force", SCRATCH], { stdio: "ignore" });
|
|
69
|
+
fs.rmSync(SCRATCH_PARENT, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function main() {
|
|
73
|
+
const wt = gitIn(REPO_ROOT, ["worktree", "add", "--quiet", "--detach", SCRATCH, PARENT], { stdio: "ignore" });
|
|
74
|
+
if (wt.status !== 0) {
|
|
75
|
+
err(`verify-bump-reproduction: could not create a scratch worktree at ${PARENT}`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The target version comes from the TAGGED commit (not the parent, which may
|
|
80
|
+
// still be pre-bump) — read via git plumbing, no separate checkout needed.
|
|
81
|
+
const pkgShow = gitIn(REPO_ROOT, ["show", `${TAGGED}:plugins/cool-workflow/package.json`]);
|
|
82
|
+
let VERSION = "";
|
|
83
|
+
if (pkgShow.status === 0) {
|
|
84
|
+
try {
|
|
85
|
+
VERSION = JSON.parse(pkgShow.stdout).version || "";
|
|
86
|
+
} catch {
|
|
87
|
+
VERSION = "";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!VERSION) {
|
|
91
|
+
err(`verify-bump-reproduction: could not read package.json version from tagged commit ${TAGGED}`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// sync-project-index.js embeds a wall-clock "Generated on <date>" line. Pin it
|
|
96
|
+
// to the TAGGED commit's own committer date (UTC) via a Node --require preload
|
|
97
|
+
// that overrides the global Date constructor (fake-date-for-reproduction.js),
|
|
98
|
+
// NOT an application-level env var: the scratch worktree runs the APPROVED
|
|
99
|
+
// PARENT's OWN checked-out copy of sync-project-index.js, which for every
|
|
100
|
+
// release cut before this mechanism existed has no idea any such env var is
|
|
101
|
+
// meant to be read — an app-level opt-in is a no-op against code that doesn't
|
|
102
|
+
// know to opt in, so it can only ever "work" for releases cut after the
|
|
103
|
+
// mechanism landed (confirmed empirically: re-running an already-shipped
|
|
104
|
+
// release's real approved-parent/tagged pair on a later calendar day produced
|
|
105
|
+
// a tree mismatch whose ONLY diff was that one date line). A global override
|
|
106
|
+
// of the Date constructor, injected before any application code runs, is
|
|
107
|
+
// transparent to the code being executed regardless of which version it is.
|
|
108
|
+
const dateShow = gitIn(REPO_ROOT, ["show", "-s", "--format=%cd", "--date=format-local:%Y-%m-%d", TAGGED], {
|
|
109
|
+
env: { ...process.env, TZ: "UTC" },
|
|
110
|
+
});
|
|
111
|
+
const CUT_DATE = dateShow.status === 0 ? dateShow.stdout.trim() : "";
|
|
112
|
+
if (!CUT_DATE) {
|
|
113
|
+
err("verify-bump-reproduction: could not read the tagged commit's committer date");
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
const FAKE_DATE_PRELOAD = path.join(SCRIPT_DIR, "fake-date-for-reproduction.js");
|
|
117
|
+
|
|
118
|
+
// Each step's exit code is checked EXPLICITLY and immediately (not via one
|
|
119
|
+
// combined chain) — a chain's overall success can silently survive an earlier
|
|
120
|
+
// step's failure if a future edit ever loosens it, which would flip this
|
|
121
|
+
// fail-closed gate to fail-open without changing its outward shape.
|
|
122
|
+
// stdout/stderr from each step are captured (not discarded) so a real
|
|
123
|
+
// failure — a transient npm registry hiccup vs. an actual reproduction
|
|
124
|
+
// mismatch — is distinguishable in the workflow log instead of both
|
|
125
|
+
// collapsing into the same generic message.
|
|
126
|
+
const stepCwd = path.join(SCRATCH, "plugins", "cool-workflow");
|
|
127
|
+
const stepEnv = {
|
|
128
|
+
...process.env,
|
|
129
|
+
NODE_OPTIONS: `--require ${FAKE_DATE_PRELOAD}`,
|
|
130
|
+
CW_FAKE_DATE: CUT_DATE,
|
|
131
|
+
};
|
|
132
|
+
function runStep(label, cmd, args) {
|
|
133
|
+
const r = run(cmd, args, { cwd: stepCwd, env: stepEnv });
|
|
134
|
+
const out = (r.stdout || "") + (r.stderr || "");
|
|
135
|
+
const code = r.error ? 1 : r.status;
|
|
136
|
+
if (code !== 0) {
|
|
137
|
+
err(`verify-bump-reproduction: ${label} failed (exit ${code}):`);
|
|
138
|
+
err(out);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const bumpOk =
|
|
145
|
+
runStep("npm install", "npm", ["install", "--no-package-lock", "--ignore-scripts"]) &&
|
|
146
|
+
runStep("bump:version", "npm", ["run", "bump:version", "--", VERSION]) &&
|
|
147
|
+
runStep("sync:project-index", "npm", ["run", "sync:project-index", "--", "--repo-only"]);
|
|
148
|
+
if (!bumpOk) {
|
|
149
|
+
err(`verify-bump-reproduction: reproducing the bump for ${PARENT} -> v${VERSION} failed`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fs.mkdirSync(path.join(SCRATCH, ".cw-release"), { recursive: true });
|
|
154
|
+
try {
|
|
155
|
+
fs.copyFileSync(path.join(REPO_ROOT, VERDICT_REL), path.join(SCRATCH, VERDICT_REL));
|
|
156
|
+
} catch {
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
if (SIG_REL && fs.existsSync(path.join(REPO_ROOT, SIG_REL))) {
|
|
160
|
+
try {
|
|
161
|
+
fs.copyFileSync(path.join(REPO_ROOT, SIG_REL), path.join(SCRATCH, SIG_REL));
|
|
162
|
+
} catch {
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Mirror cut()'s OWN staging exactly (git-add -u + the explicit verdict/.sig
|
|
168
|
+
// paths) — never -A, so an untracked stray left by npm/tooling in the scratch
|
|
169
|
+
// worktree can never silently ride into what we compare.
|
|
170
|
+
gitIn(SCRATCH, ["add", "-u"]);
|
|
171
|
+
gitIn(SCRATCH, ["add", "--", VERDICT_REL]);
|
|
172
|
+
if (SIG_REL && fs.existsSync(path.join(SCRATCH, SIG_REL))) {
|
|
173
|
+
gitIn(SCRATCH, ["add", "--", SIG_REL]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const EXPECTED_TREE = gitIn(SCRATCH, ["write-tree"]).stdout.trim();
|
|
177
|
+
const ACTUAL_TREE = gitIn(REPO_ROOT, ["rev-parse", `${TAGGED}^{tree}`]).stdout.trim();
|
|
178
|
+
|
|
179
|
+
if (EXPECTED_TREE !== ACTUAL_TREE) {
|
|
180
|
+
err(`verify-bump-reproduction: ${TAGGED}'s tree (${ACTUAL_TREE}) does not match the deterministic bump reproduced from approved parent ${PARENT} (${EXPECTED_TREE}) — refusing`);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let code = 1;
|
|
188
|
+
try {
|
|
189
|
+
code = main();
|
|
190
|
+
} finally {
|
|
191
|
+
cleanup();
|
|
192
|
+
}
|
|
193
|
+
process.exit(code);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// verify-verdict-signature.js — verify an ed25519 signature over a committed
|
|
5
5
|
// release verdict file, against the committed public key
|
|
6
6
|
// (.cw-release/verdict-signing.pub). Portable (node + node:crypto only) so
|
|
7
|
-
// CI (release-gate.yml, npm-publish.yml) and the local block-unapproved-tag.
|
|
7
|
+
// CI (release-gate.yml, npm-publish.yml) and the local block-unapproved-tag.js
|
|
8
8
|
// hook can all shell out to the SAME check instead of three drifting copies.
|
|
9
9
|
//
|
|
10
10
|
// Exit 0: signature verifies. Exit 1: missing file, unreadable key, or a
|
package/ui/workbench/app.css
CHANGED
|
@@ -27,21 +27,31 @@
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
* { box-sizing: border-box; }
|
|
30
|
+
/* The body is a flex column and main takes the leftover height (flex: 1 +
|
|
31
|
+
min-height: 0 so the inner panes can scroll) — no hardcoded header
|
|
32
|
+
height, so a header that wraps to two lines no longer pushes the panes
|
|
33
|
+
past the bottom of the window. */
|
|
30
34
|
body {
|
|
31
35
|
margin: 0;
|
|
32
36
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
33
37
|
background: var(--bg);
|
|
34
38
|
color: var(--ink);
|
|
35
39
|
font-size: 14px;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
height: 100vh;
|
|
36
43
|
}
|
|
37
44
|
header { padding: 14px 20px; border-bottom: 1px solid var(--line); }
|
|
38
45
|
header h1 { margin: 0; font-size: 18px; font-weight: 600; }
|
|
39
46
|
header .muted, .muted { color: var(--muted); font-weight: 400; }
|
|
40
47
|
header .trust { margin: 4px 0 0; font-size: 12px; color: var(--muted); }
|
|
41
48
|
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
42
|
-
main { display: grid; grid-template-columns: 320px 1fr;
|
|
49
|
+
main { display: grid; grid-template-columns: 320px 1fr; flex: 1; min-height: 0; }
|
|
43
50
|
.sidebar { border-right: 1px solid var(--line); overflow-y: auto; padding: 12px; }
|
|
44
51
|
.bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
|
52
|
+
/* The sidebar heading is a real <h2> (document outline / screen-reader
|
|
53
|
+
navigation) styled to the size the old <strong> label had. */
|
|
54
|
+
.bar h2 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
45
55
|
button {
|
|
46
56
|
background: var(--panel-2); color: var(--ink); border: 1px solid var(--line);
|
|
47
57
|
border-radius: 6px; padding: 4px 10px; cursor: pointer; font-size: 12px;
|
|
@@ -51,16 +61,28 @@ input[type="search"] {
|
|
|
51
61
|
width: 100%; background: var(--panel); color: var(--ink);
|
|
52
62
|
border: 1px solid var(--line); border-radius: 6px; padding: 6px 8px; margin-bottom: 8px;
|
|
53
63
|
}
|
|
54
|
-
.freshness { font-size:
|
|
64
|
+
.freshness { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
|
|
55
65
|
.run-list { list-style: none; margin: 0; padding: 0; }
|
|
56
|
-
.run-list li
|
|
66
|
+
.run-list li,
|
|
67
|
+
.run-list button {
|
|
57
68
|
padding: 8px; border: 1px solid var(--line); border-radius: 6px;
|
|
58
69
|
margin-bottom: 6px; cursor: pointer; background: var(--panel);
|
|
59
70
|
}
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
/* Only the run-row <button> needs a plain-element reset: a <button> has
|
|
72
|
+
its own default display/width/font/color, a plain <li> (used only for
|
|
73
|
+
the error and empty-state rows) does not and must keep its own color
|
|
74
|
+
(.err red, .muted gray) — see app.js. */
|
|
75
|
+
.run-list button {
|
|
76
|
+
display: block; width: 100%; text-align: left; font: inherit; color: inherit;
|
|
77
|
+
}
|
|
78
|
+
.run-list li:hover, .run-list button:hover { border-color: var(--accent); }
|
|
79
|
+
.run-list li.active, .run-list button.active { border-color: var(--accent); background: var(--panel-2); }
|
|
80
|
+
/* Run rows are now real <button> elements (for Tab/Enter/Space); make the
|
|
81
|
+
keyboard focus ring visible — there is no :focus-visible rule for this
|
|
82
|
+
element otherwise. */
|
|
83
|
+
.run-list button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
62
84
|
.run-list .rid { font-family: ui-monospace, monospace; font-size: 12px; display: flex; align-items: center; gap: 6px; }
|
|
63
|
-
.run-list .meta { color: var(--muted); font-size:
|
|
85
|
+
.run-list .meta { color: var(--muted); font-size: 12px; margin-top: 2px; }
|
|
64
86
|
.status-dot {
|
|
65
87
|
display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex: none;
|
|
66
88
|
background: var(--muted);
|
|
@@ -81,13 +103,23 @@ input[type="search"] {
|
|
|
81
103
|
padding: 8px 12px; border-bottom: 1px solid var(--line);
|
|
82
104
|
}
|
|
83
105
|
.panel-card > .head .title { font-weight: 600; }
|
|
84
|
-
.panel-card > .head .src { font-size:
|
|
85
|
-
.badge { font-size:
|
|
106
|
+
.panel-card > .head .src { font-size: 12px; color: var(--muted); font-family: ui-monospace, monospace; }
|
|
107
|
+
.badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); }
|
|
86
108
|
.badge.present { color: var(--present); border-color: var(--present); }
|
|
87
109
|
.badge.absent { color: var(--absent); border-color: var(--absent); }
|
|
88
110
|
.badge.stale { color: var(--absent); border-color: var(--absent); }
|
|
89
111
|
.badge.valid { color: var(--present); border-color: var(--present); }
|
|
90
112
|
.badge.missing, .badge.bad { color: var(--bad); border-color: var(--bad); }
|
|
113
|
+
/* Lifecycle states shown as a badge in the run detail header (the other
|
|
114
|
+
lifecycle values reuse the neutral/present/absent colors above). */
|
|
115
|
+
.badge.blocked { color: var(--absent); border-color: var(--absent); }
|
|
116
|
+
.badge.failed { color: var(--bad); border-color: var(--bad); }
|
|
117
|
+
.badge.completed { color: var(--present); border-color: var(--present); }
|
|
118
|
+
.badge.running { color: var(--accent); border-color: var(--accent); }
|
|
119
|
+
/* One plain next-step line under the header when a run is blocked/failed. */
|
|
120
|
+
.recovery-hint { margin: 0 0 12px; font-size: 12px; color: var(--absent); }
|
|
121
|
+
/* Second line of the empty-list state: the suggested next command. */
|
|
122
|
+
.run-list li .hint { margin-top: 4px; font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
91
123
|
pre.json {
|
|
92
124
|
margin: 0; padding: 12px; overflow-x: auto; font-size: 12px;
|
|
93
125
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; line-height: 1.5;
|
|
@@ -99,7 +131,7 @@ pre.json {
|
|
|
99
131
|
.err { color: var(--bad); }
|
|
100
132
|
.struct-block { padding: 10px 12px; border-top: 1px solid var(--line); }
|
|
101
133
|
.struct-block:first-child { border-top: none; }
|
|
102
|
-
.struct-block .struct-title { font-size:
|
|
134
|
+
.struct-block .struct-title { font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); margin-bottom: 6px; }
|
|
103
135
|
.struct-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
104
136
|
.struct-table th, .struct-table td {
|
|
105
137
|
text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--line);
|