cool-workflow 0.2.3 → 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.
Files changed (143) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.gemini-plugin/mcp.json +10 -0
  4. package/.gemini-plugin/plugin.json +40 -0
  5. package/.opencode-plugin/mcp.json +10 -0
  6. package/.opencode-plugin/plugin.json +40 -0
  7. package/README.md +202 -48
  8. package/apps/architecture-review/app.json +1 -1
  9. package/apps/architecture-review-fast/app.json +1 -1
  10. package/apps/end-to-end-golden-path/app.json +1 -1
  11. package/apps/pr-review-fix-ci/app.json +1 -1
  12. package/apps/release-cut/app.json +1 -1
  13. package/apps/research-synthesis/app.json +1 -1
  14. package/dist/cli/dispatch.js +33 -4
  15. package/dist/cli/entry.js +11 -19
  16. package/dist/cli/global-flags.js +113 -0
  17. package/dist/cli/io.js +6 -20
  18. package/dist/cli/parseargv.js +7 -1
  19. package/dist/core/capability-data.js +339 -10
  20. package/dist/core/format/help.js +80 -4
  21. package/dist/core/format/recovery-hint.js +32 -0
  22. package/dist/core/multi-agent/collaboration.js +35 -6
  23. package/dist/core/multi-agent/runtime.js +7 -0
  24. package/dist/core/multi-agent/trust-policy.js +7 -1
  25. package/dist/core/pipeline/contract.js +7 -0
  26. package/dist/core/pipeline/error-feedback.js +2 -2
  27. package/dist/core/trust/evidence-grounding.js +13 -1
  28. package/dist/core/util/cli-args.js +55 -0
  29. package/dist/core/version.js +1 -1
  30. package/dist/mcp/dispatch.js +22 -2
  31. package/dist/mcp/server.js +124 -13
  32. package/dist/mcp-server.js +20 -0
  33. package/dist/shell/commit.js +8 -2
  34. package/dist/shell/coordinator-io.js +73 -1
  35. package/dist/shell/drive.js +129 -64
  36. package/dist/shell/error-feedback-io.js +6 -0
  37. package/dist/shell/execution-backend/agent.js +205 -24
  38. package/dist/shell/execution-backend/container.js +44 -10
  39. package/dist/shell/execution-backend/local.js +47 -10
  40. package/dist/shell/fs-atomic.js +93 -12
  41. package/dist/shell/ledger-cli.js +9 -2
  42. package/dist/shell/multi-agent-cli.js +5 -1
  43. package/dist/shell/onramp.js +48 -5
  44. package/dist/shell/pipeline-cli.js +20 -2
  45. package/dist/shell/pipeline.js +2 -1
  46. package/dist/shell/reclamation-io.js +76 -7
  47. package/dist/shell/registry-cli.js +4 -0
  48. package/dist/shell/run-export.js +52 -4
  49. package/dist/shell/run-registry-io.js +1 -0
  50. package/dist/shell/run-store.js +156 -0
  51. package/dist/shell/scheduler-io.js +101 -10
  52. package/dist/shell/telemetry-ledger-io.js +36 -24
  53. package/dist/shell/trust-audit.js +104 -10
  54. package/dist/shell/workbench-host.js +121 -10
  55. package/dist/shell/workbench.js +79 -5
  56. package/dist/shell/worker-isolation.js +1 -1
  57. package/dist/shell/workflow-app-loader.js +67 -1
  58. package/dist/wiring/capability-table/basics.js +7 -2
  59. package/dist/wiring/capability-table/exec-backend.js +42 -24
  60. package/dist/wiring/capability-table/multi-agent.js +69 -69
  61. package/dist/wiring/capability-table/pipeline.js +60 -28
  62. package/dist/wiring/capability-table/registry-core.js +29 -6
  63. package/dist/wiring/capability-table/reporting.js +45 -39
  64. package/dist/wiring/capability-table/scheduling-registry.js +66 -60
  65. package/dist/wiring/capability-table/state.js +34 -34
  66. package/dist/wiring/capability-table/trust-ledger.js +66 -24
  67. package/dist/wiring/capability-table/workflow-apps.js +15 -15
  68. package/docs/agent-delegation-drive.7.md +17 -0
  69. package/docs/cli-mcp-parity.7.md +43 -13
  70. package/docs/contract-migration-tooling.7.md +4 -0
  71. package/docs/control-plane-scheduling.7.md +38 -0
  72. package/docs/cross-agent-ledger.7.md +20 -8
  73. package/docs/durable-state-and-locking.7.md +4 -0
  74. package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
  75. package/docs/execution-backends.7.md +4 -0
  76. package/docs/fix.7.md +4 -4
  77. package/docs/getting-started.md +40 -32
  78. package/docs/index.md +17 -0
  79. package/docs/launch/demo.tape +4 -3
  80. package/docs/mcp-app-surface.7.md +6 -0
  81. package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
  82. package/docs/multi-agent-eval-replay-harness.7.md +4 -0
  83. package/docs/multi-agent-operator-ux.7.md +4 -0
  84. package/docs/node-snapshot-diff-replay.7.md +4 -0
  85. package/docs/observability-cost-accounting.7.md +4 -0
  86. package/docs/project-index.md +43 -9
  87. package/docs/real-execution-backends.7.md +4 -0
  88. package/docs/release-and-migration.7.md +4 -0
  89. package/docs/release-tooling.7.md +22 -0
  90. package/docs/routine.7.md +22 -0
  91. package/docs/run-registry-control-plane.7.md +4 -0
  92. package/docs/run-retention-reclamation.7.md +28 -3
  93. package/docs/state-explosion-management.7.md +4 -0
  94. package/docs/team-collaboration.7.md +4 -0
  95. package/docs/trust-audit-anchor.7.md +4 -0
  96. package/docs/web-desktop-workbench.7.md +26 -2
  97. package/docs/workflow-app-framework.7.md +31 -0
  98. package/manifest/plugin.manifest.json +1 -1
  99. package/manifest/source-context-profiles.json +4 -3
  100. package/package.json +5 -2
  101. package/scripts/agents/agent-adapter-core.js +26 -0
  102. package/scripts/agents/claude-p-agent.js +4 -1
  103. package/scripts/agents/codex-agent.js +4 -0
  104. package/scripts/agents/cw-attest-wrap.js +1 -1
  105. package/scripts/agents/gemini-agent.js +4 -0
  106. package/scripts/agents/opencode-agent.js +5 -0
  107. package/scripts/block-unapproved-tag.js +160 -0
  108. package/scripts/bump-version.js +27 -10
  109. package/scripts/canonical-apps.js +4 -4
  110. package/scripts/children/batch-delegate-child.js +72 -4
  111. package/scripts/children/http-batch-delegate-child.js +132 -0
  112. package/scripts/children/http-delegate-child.js +8 -0
  113. package/scripts/dogfood-release.js +2 -2
  114. package/scripts/fake-date-for-reproduction.js +1 -1
  115. package/scripts/golden-path.js +4 -4
  116. package/scripts/lang-policy-check.js +82 -0
  117. package/scripts/purity-baseline.json +0 -30
  118. package/scripts/purity-gate.js +16 -3
  119. package/scripts/release-check.js +2 -1
  120. package/scripts/release-flow.js +264 -41
  121. package/scripts/release-gate.js +184 -0
  122. package/scripts/release-oneclick.js +438 -0
  123. package/scripts/release-tags.js +47 -0
  124. package/scripts/verdict-keygen.js +2 -2
  125. package/scripts/verify-bump-reproduction.js +193 -0
  126. package/scripts/verify-verdict-signature.js +1 -1
  127. package/scripts/version-sync-check.js +39 -22
  128. package/ui/workbench/app.css +41 -9
  129. package/ui/workbench/app.js +160 -27
  130. package/ui/workbench/index.html +2 -2
  131. package/docs/agent-framework.md +0 -177
  132. package/docs/designs/handoff-ledger.md +0 -145
  133. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
  134. package/docs/launch/launch-kit.md +0 -195
  135. package/docs/launch/pre-launch-checklist.md +0 -53
  136. package/docs/readme-v0.1.87-full.md +0 -301
  137. package/docs/routines.md +0 -101
  138. package/docs/scheduled-tasks.md +0 -80
  139. package/scripts/agents/claude-p-agent.sh +0 -9
  140. package/scripts/block-unapproved-tag.sh +0 -60
  141. package/scripts/release-gate.sh +0 -94
  142. package/scripts/verify-bump-reproduction.sh +0 -148
  143. package/scripts/verify-container-selfref.js +0 -64
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // HTTP batch delegate child — the endpoint-mode sibling of batch-delegate-child.js.
5
+ // Spawned via `node <this-path>` (shell:false) by runEndpointBatchOutcomes when a
6
+ // concurrent drive round (`--concurrency N`) has one or more ENDPOINT-configured
7
+ // agents. Without it, endpoint tasks in a concurrent round each spawned their own
8
+ // blocking http-delegate-child one after another, so `--concurrency N` ran N
9
+ // endpoint delegations strictly SERIALLY. This child POSTs all N at once from a
10
+ // single process (one Node process holds N in-flight fetches trivially — no per-job
11
+ // child process is needed for HTTP the way it is for a vendor CLI).
12
+ //
13
+ // Reads a JSON array `[{ endpoint, job, timeoutMs }, ...]` on stdin, where `job` is
14
+ // the ALREADY-serialized POST body (byte-identical to the string the serial
15
+ // runAgentEndpoint builds, so the concurrent path posts the same bytes). Fires all
16
+ // jobs concurrently, each POST + optional jobId poll matching http-delegate-child.js,
17
+ // and streams ONE NDJSON line per job — `{i, spawnError?, exitCode, stdout}\n` — the
18
+ // INSTANT that job settles. `i` is the job's index (settle order is concurrent, not
19
+ // submission order, so the parent cannot map a line to its job without it). The line
20
+ // shape and the incremental-streaming reason are shared with batch-delegate-child.js:
21
+ // the parent (runEndpointBatchOutcomes) reuses reconcileBatchOutcomes to read them.
22
+ //
23
+ // THE RED LINE: this child speaks ONLY plain HTTP to the operator-configured
24
+ // endpoint. It imports NO model SDK, holds NO API key, and constructs NO model API
25
+ // request. It re-uses the caller's inherited env for any endpoint auth exactly as
26
+ // http-delegate-child.js does; it reads no credential itself.
27
+
28
+ const MAX_STDIN_BYTES = 32 * 1024 * 1024;
29
+ // Per-job caps mirror batch-delegate-child.js: CAP bounds each job's RAW stdout
30
+ // bytes; LINE_CAP bounds the ESCAPED NDJSON line (JSON escaping grows bytes), held
31
+ // under the parent's 34MB-per-job maxBuffer grant with a small margin so one job's
32
+ // line can never push the combined stream past the parent's buffer and ENOBUFS the
33
+ // whole batch.
34
+ const CAP = 32 * 1024 * 1024;
35
+ const LINE_CAP = 33 * 1024 * 1024;
36
+
37
+ // In-flight fetch controllers so a stop signal aborts every open request at once.
38
+ // No grandchild processes exist here (unlike batch-delegate-child.js), so there is
39
+ // no wedged-pipe deadlock to guard — aborting the fetches empties the event loop
40
+ // and the process exits on its own.
41
+ const controllers = new Set();
42
+ let stopSignalReceived = false;
43
+ function onStopSignal(signal) {
44
+ if (stopSignalReceived) {
45
+ process.exit(signal === "SIGINT" ? 130 : 143);
46
+ return;
47
+ }
48
+ stopSignalReceived = true;
49
+ for (const controller of controllers) {
50
+ try { controller.abort(); } catch {}
51
+ }
52
+ // If aborting does not drain the loop promptly (a hung socket), force-exit.
53
+ setTimeout(() => process.exit(signal === "SIGINT" ? 130 : 143), 5000).unref();
54
+ }
55
+ process.on("SIGINT", () => onStopSignal("SIGINT"));
56
+ process.on("SIGTERM", () => onStopSignal("SIGTERM"));
57
+
58
+ function writeSettleLine(i, outcome) {
59
+ let line = JSON.stringify({ i, ...outcome }) + "\n";
60
+ if (Buffer.byteLength(line) > LINE_CAP) {
61
+ // Same fail-closed shape as the raw-cap path: capped output is never evidence,
62
+ // so drop it entirely and name the cap — never ship a line the parent's buffer
63
+ // cannot hold.
64
+ line = JSON.stringify({ i, spawnError: `serialized stdout line exceeded ${LINE_CAP} byte cap (${Buffer.byteLength(line)} bytes)`, exitCode: null, stdout: "" }) + "\n";
65
+ }
66
+ process.stdout.write(line);
67
+ }
68
+
69
+ // One job: POST the pre-serialized body, poll a returned jobId until done, and
70
+ // return { exitCode, stdout } or throw. Byte-for-byte the same POST + poll contract
71
+ // as http-delegate-child.js, plus a per-job deadline so one hung endpoint settles as
72
+ // its own error instead of pinning the whole batch to the parent's backstop timeout.
73
+ async function runOne(entry) {
74
+ const { endpoint, job, timeoutMs } = entry;
75
+ if (!endpoint) throw new Error("no endpoint");
76
+ const deadline = Date.now() + (Number(timeoutMs) > 0 ? Number(timeoutMs) : 600000);
77
+ const controller = new AbortController();
78
+ controllers.add(controller);
79
+ const abortAt = setTimeout(() => { try { controller.abort(); } catch {} }, Math.max(0, deadline - Date.now()));
80
+ try {
81
+ const post = await fetch(endpoint, {
82
+ method: "POST",
83
+ headers: { "content-type": "application/json" },
84
+ body: typeof job === "string" ? job : JSON.stringify(job),
85
+ signal: controller.signal,
86
+ });
87
+ if (!post.ok) throw new Error("runner responded " + post.status);
88
+ let data = await post.json();
89
+ let guard = 0;
90
+ while (data && data.jobId && data.done !== true && guard++ < 600) {
91
+ if (Date.now() >= deadline) throw new Error("endpoint poll timed out");
92
+ await new Promise((r) => setTimeout(r, 1000));
93
+ const poll = await fetch(endpoint + (endpoint.includes("?") ? "&" : "?") + "jobId=" + encodeURIComponent(data.jobId), { signal: controller.signal });
94
+ if (!poll.ok) throw new Error("poll responded " + poll.status);
95
+ data = await poll.json();
96
+ }
97
+ if (typeof data.exitCode !== "number") throw new Error("runner did not report an exitCode");
98
+ let stdout = String(data.stdout || "");
99
+ if (Buffer.byteLength(stdout) > CAP) throw new Error(`stdout exceeded ${CAP} byte cap (${Buffer.byteLength(stdout)} bytes)`);
100
+ return { exitCode: data.exitCode, stdout };
101
+ } finally {
102
+ clearTimeout(abortAt);
103
+ controllers.delete(controller);
104
+ }
105
+ }
106
+
107
+ (async () => {
108
+ // setEncoding("utf8") (as batch-delegate-child.js does), NOT `b += <Buffer>`:
109
+ // without it each data chunk is a raw Buffer coerced to a string on its own, so
110
+ // a multibyte char split across a ~64KB pipe-chunk boundary decodes to U+FFFD
111
+ // and the POSTed prompt is silently corrupted. With the encoding set, Node's
112
+ // StringDecoder carries partial bytes across chunk boundaries.
113
+ const read = () => new Promise((res) => { let b = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (c) => { if (b.length < MAX_STDIN_BYTES) b += c; }); process.stdin.on("end", () => res(b)); });
114
+ let jobs;
115
+ try {
116
+ jobs = JSON.parse((await read()) || "[]");
117
+ } catch (e) {
118
+ process.stdout.write(JSON.stringify([{ spawnError: `invalid stdin JSON: ${String((e && e.message) || e)}`, exitCode: null, stdout: "" }]));
119
+ return;
120
+ }
121
+ if (!Array.isArray(jobs) || !jobs.length) { process.stdout.write("[]"); return; }
122
+ // allSettled, not Promise.all: one failing job never rejects the others, and
123
+ // every job's line is already written by runOne's settle below regardless.
124
+ await Promise.allSettled(jobs.map(async (entry, i) => {
125
+ try {
126
+ const out = await runOne(entry);
127
+ writeSettleLine(i, { exitCode: out.exitCode, stdout: out.stdout });
128
+ } catch (e) {
129
+ writeSettleLine(i, { spawnError: String((e && e.message) || e), exitCode: null, stdout: "" });
130
+ }
131
+ }));
132
+ })();
@@ -16,6 +16,14 @@
16
16
 
17
17
  (async () => {
18
18
  const MAX_STDIN_BYTES = 32 * 1024 * 1024;
19
+ // setEncoding("utf8") before reading: Node's StringDecoder then carries a
20
+ // multibyte UTF-8 char that straddles a ~64KB pipe-chunk boundary across the
21
+ // two chunks. Without it, each Buffer chunk was coerced to a string on its
22
+ // own and a split char decoded to U+FFFD on both sides — silently corrupting
23
+ // a > ~64KB non-ASCII job (this project's prompts are frequently Chinese), or
24
+ // making the outer JSON.parse throw so the delegation refused. Matches the
25
+ // batch-delegate-child.js reader.
26
+ process.stdin.setEncoding("utf8");
19
27
  const read = () => new Promise((res) => { let b = ""; process.stdin.on("data", (c) => { if (b.length < MAX_STDIN_BYTES) b += c; }); process.stdin.on("end", () => res(b)); });
20
28
  try {
21
29
  const job = JSON.parse((await read()) || "{}");
@@ -6,7 +6,7 @@ const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
  const { CoolWorkflowRunner } = require("../dist/shell/orchestrator.js");
8
8
 
9
- const TARGET_VERSION = "0.2.3";
9
+ const TARGET_VERSION = "0.2.5";
10
10
  const PREVIOUS_VERSION = "0.1.31";
11
11
  const pluginRoot = path.resolve(__dirname, "..");
12
12
  const repoRoot = path.resolve(pluginRoot, "..", "..");
@@ -291,7 +291,7 @@ function commandsForTask(taskId, context) {
291
291
  "-e",
292
292
  [
293
293
  releaseSourceReaderSnippet(),
294
- "const files=['docs/dogfood-one-real-repo.7.md','plugins/cool-workflow/docs/dogfood-one-real-repo.7.md','README.md','plugins/cool-workflow/README.md','CHANGELOG.md','RELEASE.md'];",
294
+ "const files=['plugins/cool-workflow/docs/dogfood-one-real-repo.7.md','README.md','plugins/cool-workflow/README.md','CHANGELOG.md','RELEASE.md'];",
295
295
  "for (const f of files) { if (!surfaceExists(f)) throw new Error('missing '+f); }",
296
296
  "const changelog=readSurface('CHANGELOG.md');",
297
297
  "if (changelog===null) throw new Error('CHANGELOG.md missing from release commit');",
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  // fake-date-for-reproduction.js — a Node `--require` preload used ONLY by
4
- // verify-bump-reproduction.sh. It pins `new Date()` (no-args construction)
4
+ // verify-bump-reproduction.js. It pins `new Date()` (no-args construction)
5
5
  // and `Date.now()` to a fixed instant, read from CW_FAKE_DATE (an ISO date or
6
6
  // YYYY-MM-DD string). `new Date(explicit args)` is left untouched.
7
7
  //
@@ -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.3");
36
+ assert.equal(appValidation.summary.version, "0.2.5");
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.3 end-to-end golden path."
45
+ "Prove the deterministic v0.2.5 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.3");
55
+ assert.equal(state.workflow.app.version, "0.2.5");
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\.3/);
198
+ assert.match(report, /Workflow App: end-to-end-golden-path@0\.2\.5/);
199
199
  assert.match(report, /## Candidates/);
200
200
  assert.match(report, /## Trust Audit/);
201
201
  assert.match(report, /## Acceptance Rationale/);
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // lang-policy-check.js — this project is written in JavaScript and
4
+ // TypeScript only (AGENTS.md hard rule, 2026-07-13). Every git-tracked file
5
+ // must be JS/TS, a recognized non-code file (docs, data, config, binary
6
+ // asset), or one of the explicit, path-scoped exceptions below — a real
7
+ // non-JS/TS file the project genuinely needs. Fail closed: an unrecognized
8
+ // extension/basename is a hard stop, forcing a deliberate choice (write it
9
+ // in JS/TS, or add a new exception here with a reason) instead of a silent
10
+ // pass. Mechanism only; the allowlists ARE the policy, kept as plain data
11
+ // in this one file — no separate config to drift from it.
12
+
13
+ const { execFileSync } = require("node:child_process");
14
+ const path = require("node:path");
15
+
16
+ const CODE_EXTENSIONS = new Set(["js", "mjs", "cjs", "ts", "tsx", "jsx"]);
17
+
18
+ // Real, needed files this project cannot write in JS/TS -- the exact same
19
+ // set already carved out of GitHub's Languages stats in .gitattributes, for
20
+ // the same reasons (see that file's comments). Scoped to an EXACT path,
21
+ // never a whole directory, so a new file dropped in the same folder does
22
+ // NOT silently inherit the exception.
23
+ const EXCEPT_PATHS = new Set([
24
+ "Formula/cool-workflow.rb", // Homebrew formula must be Ruby
25
+ "plugins/cool-workflow/ui/workbench/index.html", // served from disk by design
26
+ "plugins/cool-workflow/ui/workbench/app.css" // served from disk by design
27
+ ]);
28
+
29
+ // Not "code" at all -- docs, data, config, and binary assets. Fine
30
+ // anywhere in the tree; this policy is only about what the project is
31
+ // WRITTEN in, not about every file it ships or tracks.
32
+ const NON_CODE_EXTENSIONS = new Set([
33
+ "md", "mdc", "txt", "json", "jsonl", "yml", "yaml", "toml",
34
+ "verdict", "sig", "pub", "tape",
35
+ "svg", "png", "jpg", "jpeg", "gif", "webp", "ico", "pdf",
36
+ "woff", "woff2", "gz", "tgz", "zip", "lock"
37
+ ]);
38
+ // Extensionless / dotfile basenames that are also not code.
39
+ const NON_CODE_BASENAMES = new Set([
40
+ "LICENSE", ".gitignore", ".gitattributes", ".editorconfig", ".windsurfrules", ".npmrc", ".nvmrc"
41
+ ]);
42
+
43
+ // `EXCEPT_PATHS` are written repo-root-relative, so tracked files must be
44
+ // resolved the same way regardless of the invocation cwd (this runs both as
45
+ // `npm run lang:check` from plugins/cool-workflow/ AND from release-check.js
46
+ // at various cwds) -- resolve the real top-level first, never trust the
47
+ // invocation cwd to already BE it.
48
+ function trackedFiles() {
49
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
50
+ return execFileSync("git", ["-C", repoRoot, "ls-files"], { encoding: "utf8" })
51
+ .split("\n")
52
+ .filter(Boolean);
53
+ }
54
+
55
+ function classify(file) {
56
+ if (EXCEPT_PATHS.has(file)) return { ok: true };
57
+ const base = path.basename(file);
58
+ if (NON_CODE_BASENAMES.has(base)) return { ok: true };
59
+ const dot = base.lastIndexOf(".");
60
+ if (dot <= 0) return { ok: false, ext: null };
61
+ const ext = base.slice(dot + 1).toLowerCase();
62
+ if (CODE_EXTENSIONS.has(ext) || NON_CODE_EXTENSIONS.has(ext)) return { ok: true };
63
+ return { ok: false, ext };
64
+ }
65
+
66
+ const files = trackedFiles();
67
+ const violations = [];
68
+ for (const file of files) {
69
+ const result = classify(file);
70
+ if (!result.ok) violations.push({ file, ext: result.ext });
71
+ }
72
+
73
+ if (violations.length > 0) {
74
+ process.stderr.write("lang-policy-check: this project is JavaScript/TypeScript only. Unrecognized file(s):\n");
75
+ for (const v of violations) {
76
+ process.stderr.write(` - ${v.file}${v.ext ? ` (.${v.ext})` : " (no extension)"}\n`);
77
+ }
78
+ process.stderr.write("Write it in JavaScript/TypeScript, or add a scoped, reasoned exception to scripts/lang-policy-check.js.\n");
79
+ process.exit(1);
80
+ }
81
+
82
+ process.stdout.write(`lang-policy-check: ${files.length} tracked files, all JS/TS or an allowed non-code/exception file.\n`);
@@ -36,36 +36,6 @@
36
36
  "layerViolations": {
37
37
  "core/capability-table.ts": [
38
38
  "../wiring/capability-table"
39
- ],
40
- "wiring/capability-table/basics.ts": [
41
- "../../cli/io"
42
- ],
43
- "wiring/capability-table/exec-backend.ts": [
44
- "../../cli/io"
45
- ],
46
- "wiring/capability-table/multi-agent.ts": [
47
- "../../cli/io"
48
- ],
49
- "wiring/capability-table/pipeline.ts": [
50
- "../../cli/io"
51
- ],
52
- "wiring/capability-table/registry-core.ts": [
53
- "../../cli/io"
54
- ],
55
- "wiring/capability-table/reporting.ts": [
56
- "../../cli/io"
57
- ],
58
- "wiring/capability-table/scheduling-registry.ts": [
59
- "../../cli/io"
60
- ],
61
- "wiring/capability-table/state.ts": [
62
- "../../cli/io"
63
- ],
64
- "wiring/capability-table/trust-ledger.ts": [
65
- "../../cli/io"
66
- ],
67
- "wiring/capability-table/workflow-apps.ts": [
68
- "../../cli/io"
69
39
  ]
70
40
  }
71
41
  }
@@ -112,8 +112,12 @@ function stripComments(text) {
112
112
 
113
113
  // Every `from "spec"` (import or export-from, type or value — a type-only
114
114
  // import still names a module, so it counts the same way a real dependency
115
- // scan would) plus every `require("spec")`. Non-greedy across `from` finds
116
- // each import's own clause without needing a real parser.
115
+ // scan would) plus every `require("spec")` and every dynamic `import("spec")`.
116
+ // Non-greedy across `from` finds each import's own clause without needing a
117
+ // real parser. The dynamic-import form matters because a core/ file could
118
+ // otherwise smuggle in an impure or cross-layer module through
119
+ // `await import("node:fs")` — the one import shape the static and require
120
+ // scans did not see, so the ratchet was blind to it.
117
121
  function extractSpecifiers(codeText) {
118
122
  const specs = [];
119
123
  const fromRe = /(?:^|\n)\s*(?:import|export)\s[\s\S]*?from\s+["']([^"']+)["']/g;
@@ -121,6 +125,8 @@ function extractSpecifiers(codeText) {
121
125
  while ((m = fromRe.exec(codeText))) specs.push(m[1]);
122
126
  const requireRe = /require\(\s*["']([^"']+)["']\s*\)/g;
123
127
  while ((m = requireRe.exec(codeText))) specs.push(m[1]);
128
+ const dynamicImportRe = /import\(\s*["']([^"']+)["']\s*\)/g;
129
+ while ((m = dynamicImportRe.exec(codeText))) specs.push(m[1]);
124
130
  return specs;
125
131
  }
126
132
 
@@ -236,4 +242,11 @@ function main() {
236
242
  process.stdout.write("purity gate: src/ matches the committed baseline (no new core/shell layer breaks, no clock/env drift).\n");
237
243
  }
238
244
 
239
- main();
245
+ // Run the gate only when invoked directly (node scripts/purity-gate.js). When
246
+ // required by a test, expose the internal helpers so the specifier scan can be
247
+ // unit-checked without shelling out or writing into the real src/ tree.
248
+ if (require.main === module) {
249
+ main();
250
+ }
251
+
252
+ module.exports = { stripComments, extractSpecifiers, layerOf, scan };
@@ -60,12 +60,13 @@ const checks = [
60
60
  // from that fresh build — strictly stronger than a bare `npm run build`.
61
61
  { name: "dist freshness", command: ["npm", "run", "dist:check"] },
62
62
  { name: "core/shell purity", command: ["npm", "run", "purity:check"] },
63
+ { name: "language policy (JS/TS only)", command: ["npm", "run", "lang:check"] },
63
64
  { name: "type check", command: ["npm", "run", "check"] },
64
65
  { name: "onramp contract", command: ["npm", "run", "onramp:check"] },
65
66
  { name: "run-state schema consistency", command: ["node", "scripts/validate-run-state-schema.js"] },
66
67
  // Every test path uses --concurrency auto by default (parallel, race-free:
67
68
  // each smoke runs in a private cwd + state roots). The release tag-gate
68
- // (release-gate.sh) forces CW_TEST_CONCURRENCY=1 to stay sequential as the
69
+ // (release-gate.js) forces CW_TEST_CONCURRENCY=1 to stay sequential as the
69
70
  // deterministic backstop.
70
71
  { name: "tests", command: ["npm", "run", "test:ci"] },
71
72
  // Pure core/ unit tests (test/*.test.js) — a separate suite from the smoke