cool-workflow 0.1.78 → 0.1.80

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 (74) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +29 -3
  4. package/apps/architecture-review/app.json +1 -1
  5. package/apps/architecture-review-fast/app.json +64 -0
  6. package/apps/architecture-review-fast/workflow.js +153 -0
  7. package/apps/end-to-end-golden-path/app.json +1 -1
  8. package/apps/pr-review-fix-ci/app.json +1 -1
  9. package/apps/release-cut/app.json +1 -1
  10. package/apps/research-synthesis/app.json +1 -1
  11. package/dist/capability-core.js +71 -0
  12. package/dist/capability-registry.js +13 -8
  13. package/dist/cli.js +49 -1
  14. package/dist/drive.js +74 -1
  15. package/dist/evidence-reasoning.js +2 -2
  16. package/dist/execution-backend.js +6 -1
  17. package/dist/mcp-server.js +56 -13
  18. package/dist/orchestrator/lifecycle-operations.js +2 -1
  19. package/dist/orchestrator.js +1 -1
  20. package/dist/run-export.js +370 -25
  21. package/dist/run-registry.js +11 -4
  22. package/dist/state-explosion.js +100 -21
  23. package/dist/telemetry-demo.js +154 -0
  24. package/dist/version.js +1 -1
  25. package/docs/agent-delegation-drive.7.md +60 -0
  26. package/docs/canonical-workflow-apps.7.md +37 -0
  27. package/docs/cli-mcp-parity.7.md +14 -0
  28. package/docs/contract-migration-tooling.7.md +6 -0
  29. package/docs/control-plane-scheduling.7.md +6 -0
  30. package/docs/durable-state-and-locking.7.md +6 -0
  31. package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
  32. package/docs/execution-backends.7.md +6 -0
  33. package/docs/index.md +1 -0
  34. package/docs/launch/demo.tape +28 -0
  35. package/docs/launch/launch-kit.md +172 -0
  36. package/docs/launch/pre-launch-checklist.md +53 -0
  37. package/docs/multi-agent-cli-mcp-surface.7.md +6 -0
  38. package/docs/multi-agent-eval-replay-harness.7.md +6 -0
  39. package/docs/multi-agent-operator-ux.7.md +6 -0
  40. package/docs/node-snapshot-diff-replay.7.md +6 -0
  41. package/docs/observability-cost-accounting.7.md +6 -0
  42. package/docs/project-index.md +16 -6
  43. package/docs/real-execution-backends.7.md +6 -0
  44. package/docs/release-and-migration.7.md +6 -0
  45. package/docs/release-tooling.7.md +6 -0
  46. package/docs/routines.md +23 -0
  47. package/docs/run-registry-control-plane.7.md +44 -1
  48. package/docs/run-retention-reclamation.7.md +6 -0
  49. package/docs/source-context-profiles.7.md +119 -0
  50. package/docs/state-explosion-management.7.md +13 -0
  51. package/docs/team-collaboration.7.md +6 -0
  52. package/docs/unix-principles.md +49 -1
  53. package/docs/web-desktop-workbench.7.md +6 -0
  54. package/manifest/plugin.manifest.json +1 -1
  55. package/manifest/source-context-profiles.json +142 -0
  56. package/package.json +2 -1
  57. package/scripts/agents/claude-p-agent.js +129 -43
  58. package/scripts/architecture-review-fast.js +362 -0
  59. package/scripts/bump-version.js +1 -0
  60. package/scripts/canonical-apps.js +21 -4
  61. package/scripts/coverage-gate.js +211 -0
  62. package/scripts/dogfood-release.js +1 -1
  63. package/scripts/golden-path.js +4 -4
  64. package/scripts/source-context.js +291 -0
  65. package/scripts/version-sync-check.js +1 -0
  66. package/skills/ci-triage/SKILL.md +50 -0
  67. package/skills/ci-triage/agents/openai.yaml +4 -0
  68. package/skills/cool-workflow/SKILL.md +4 -1
  69. package/skills/deploy-check/SKILL.md +55 -0
  70. package/skills/deploy-check/agents/openai.yaml +4 -0
  71. package/skills/design-qa/SKILL.md +49 -0
  72. package/skills/design-qa/agents/openai.yaml +4 -0
  73. package/skills/pr-review/SKILL.md +45 -0
  74. package/skills/pr-review/agents/openai.yaml +4 -0
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // architecture-review-fast — userland accelerator wrapper.
5
+ //
6
+ // Mechanism only: prepare one cached JSONL source context, pass its digest into
7
+ // the opt-in fast app, then optionally create a background schedule for the full
8
+ // architecture-review app. The model still runs only through CW's external agent
9
+ // backend; this script imports no model SDK and holds no key.
10
+
11
+ const crypto = require("node:crypto");
12
+ const fs = require("node:fs");
13
+ const path = require("node:path");
14
+ const { spawnSync } = require("node:child_process");
15
+
16
+ const pluginRoot = path.resolve(__dirname, "..");
17
+ const repoRoot = path.resolve(pluginRoot, "..", "..");
18
+ const node = process.execPath;
19
+ const cw = path.join(pluginRoot, "scripts", "cw.js");
20
+ const sourceContext = path.join(pluginRoot, "scripts", "source-context.js");
21
+
22
+ function main() {
23
+ const started = nowNs();
24
+ const args = parseArgs(process.argv.slice(2));
25
+ if (args.help) return usage(0);
26
+
27
+ const repo = path.resolve(required(args.repo, "repo"));
28
+ const question = required(args.question, "question");
29
+ const requestedProfile = stringArg(args.profile);
30
+ const ref = stringArg(args.ref) || "HEAD";
31
+ const requestedProfileFile = stringArg(args.profileFile || args["profile-file"]);
32
+ const defaultExternalProfile = !requestedProfile && !requestedProfileFile && repo !== repoRoot;
33
+ const profile = defaultExternalProfile ? "repo" : requestedProfile || "core";
34
+ const cacheDir = path.resolve(stringArg(args.cacheDir || args["cache-dir"]) || path.join(repo, ".cw", "cache", "source-context"));
35
+ const contextOut = path.resolve(stringArg(args.contextOut || args["context-out"]) || path.join(repo, ".cw", "context", `${profile}-source.jsonl`));
36
+ const profileFile = defaultExternalProfile ? writeDefaultRepoProfile(repo, contextOut) : requestedProfileFile;
37
+ const includeMetrics = truthy(args.metrics);
38
+ const fastModel = stringArg(args.fastModel || args["fast-model"]);
39
+ const strongModel = stringArg(args.strongModel || args["strong-model"]);
40
+ const modelEnv = modelPolicyEnv(fastModel, strongModel);
41
+
42
+ const contextExport = timed(() => exportSourceContext({
43
+ repo,
44
+ profile,
45
+ ref,
46
+ profileFile,
47
+ cacheDir
48
+ }));
49
+ const contextText = contextExport.value;
50
+ assertNonEmptySourceContext(contextText, profile, repo);
51
+ fs.mkdirSync(path.dirname(contextOut), { recursive: true });
52
+ fs.writeFileSync(contextOut, contextText, "utf8");
53
+ const digest = `sha256:${crypto.createHash("sha256").update(contextText, "utf8").digest("hex")}`;
54
+
55
+ const reviewArgs = [
56
+ "quickstart",
57
+ "architecture-review-fast",
58
+ "--repo",
59
+ repo,
60
+ "--question",
61
+ question,
62
+ "--sourceContext",
63
+ contextOut,
64
+ "--sourceContextDigest",
65
+ digest
66
+ ];
67
+ appendRepeated(reviewArgs, "--invariant", args.invariant);
68
+ appendOption(reviewArgs, "--focus", args.focus);
69
+ appendPassThrough(reviewArgs, args, [
70
+ "agent-command",
71
+ "agentCommand",
72
+ "agent-endpoint",
73
+ "agentEndpoint",
74
+ "agent-model",
75
+ "agentModel",
76
+ "agent-timeout-ms",
77
+ "agentTimeoutMs",
78
+ "once",
79
+ "preview",
80
+ "now"
81
+ ]);
82
+
83
+ const fastReviewRun = timed(() => runCwJson(reviewArgs, repo, modelEnv));
84
+ const fastReview = fastReviewRun.value;
85
+ const sourceContextMeta = {
86
+ path: contextOut,
87
+ digest,
88
+ profile,
89
+ ref,
90
+ cacheDir
91
+ };
92
+ const fullReviewScheduleRun = truthy(args.scheduleFull || args["schedule-full"])
93
+ ? timed(() => scheduleFullReview(repo, question, args, fastReview, sourceContextMeta))
94
+ : undefined;
95
+ const fullReviewSchedule = fullReviewScheduleRun?.value;
96
+
97
+ writeJson({
98
+ schemaVersion: 1,
99
+ appId: "architecture-review-fast",
100
+ sourceContext: sourceContextMeta,
101
+ fastReview,
102
+ ...(fastModel || strongModel ? { modelPolicy: { ...(fastModel ? { fastModel } : {}), ...(strongModel ? { strongModel } : {}) } } : {}),
103
+ ...(fullReviewSchedule ? { fullReviewSchedule } : {}),
104
+ ...(includeMetrics ? { metrics: buildMetrics(started, contextText, contextExport.elapsedMs, fastReview, fastReviewRun.elapsedMs, fullReviewScheduleRun?.elapsedMs) } : {})
105
+ });
106
+ }
107
+
108
+ function exportSourceContext(options) {
109
+ const argv = [
110
+ sourceContext,
111
+ "export",
112
+ "--profile",
113
+ options.profile,
114
+ "--ref",
115
+ options.ref,
116
+ "--repo-root",
117
+ options.repo,
118
+ "--cache-dir",
119
+ options.cacheDir
120
+ ];
121
+ if (options.profileFile) argv.push("--profile-file", path.resolve(options.profileFile));
122
+ const result = spawnSync(node, argv, {
123
+ cwd: repoRoot,
124
+ encoding: "utf8",
125
+ maxBuffer: 1024 * 1024 * 128
126
+ });
127
+ if (result.status !== 0) die(result.stderr || result.stdout || "source context export failed");
128
+ return result.stdout;
129
+ }
130
+
131
+ function writeDefaultRepoProfile(repo, contextOut) {
132
+ const file = path.join(path.dirname(contextOut), "repo-source-profile.json");
133
+ const profile = {
134
+ schemaVersion: 1,
135
+ profiles: {
136
+ repo: {
137
+ description: "Default external repository profile for architecture-review-fast.",
138
+ maxLines: 50000,
139
+ include: [
140
+ "README.md",
141
+ "README.*",
142
+ "package.json",
143
+ "tsconfig.json",
144
+ "pyproject.toml",
145
+ "Cargo.toml",
146
+ "go.mod",
147
+ ".github/**",
148
+ "src/**",
149
+ "lib/**",
150
+ "app/**",
151
+ "apps/**",
152
+ "bin/**",
153
+ "scripts/**",
154
+ "docs/**",
155
+ "test/**",
156
+ "tests/**"
157
+ ],
158
+ exclude: [
159
+ ".cw/**",
160
+ "dist/**",
161
+ "build/**",
162
+ "coverage/**",
163
+ "node_modules/**",
164
+ "vendor/**",
165
+ "target/**",
166
+ "docs/assets/**",
167
+ "assets/**",
168
+ "public/**"
169
+ ]
170
+ }
171
+ }
172
+ };
173
+ fs.mkdirSync(path.dirname(file), { recursive: true });
174
+ fs.writeFileSync(file, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
175
+ return file;
176
+ }
177
+
178
+ function assertNonEmptySourceContext(text, profile, repo) {
179
+ const records = text.trim() ? text.trim().split(/\n/).filter(Boolean).length : 0;
180
+ if (records > 0) return;
181
+ die([
182
+ `source context profile "${profile}" exported zero records for ${repo}`,
183
+ "pass --profile-file with include rules for this repository, or choose a profile that matches tracked text files"
184
+ ].join("; "));
185
+ }
186
+
187
+ function scheduleFullReview(repo, question, args, fastReview, sourceContextMeta) {
188
+ const delayMinutes = stringArg(args.fullDelayMinutes || args["full-delay-minutes"]) || "1";
189
+ const prompt = [
190
+ `Run full architecture-review for ${repo}.`,
191
+ `Question: ${question}`,
192
+ args.focus ? `Focus: ${args.focus}` : "",
193
+ `Fast review run: ${fastReview?.runId || "unknown"}.`,
194
+ fastReview?.reportPath ? `Fast review report: ${fastReview.reportPath}.` : "",
195
+ `Fast review status: ${fastReview?.status || "unknown"} (${fastReview?.completedWorkers || 0}/${fastReview?.plannedWorkers || 0} workers completed).`,
196
+ `Source context: ${sourceContextMeta.path} (${sourceContextMeta.digest}, profile ${sourceContextMeta.profile}, ref ${sourceContextMeta.ref}).`,
197
+ "Use the completed architecture-review-fast report as the foreground triage result; write the full review report path and digest when the background review finishes."
198
+ ].filter(Boolean).join(" ");
199
+ return runCwJson([
200
+ "schedule",
201
+ "create",
202
+ "--cwd",
203
+ repo,
204
+ "--kind",
205
+ "reminder",
206
+ "--delayMinutes",
207
+ delayMinutes,
208
+ "--maxRuns",
209
+ "1",
210
+ "--workflowId",
211
+ "architecture-review",
212
+ "--prompt",
213
+ prompt
214
+ ], repo);
215
+ }
216
+
217
+ function runCwJson(args, cwd, extraEnv = {}) {
218
+ const result = spawnSync(node, [cw, ...args], {
219
+ cwd,
220
+ encoding: "utf8",
221
+ env: { ...process.env, ...extraEnv },
222
+ maxBuffer: 1024 * 1024 * 64
223
+ });
224
+ if (result.status !== 0) die(result.stderr || result.stdout || `cw ${args.join(" ")} failed`);
225
+ try {
226
+ return JSON.parse(result.stdout);
227
+ } catch (error) {
228
+ die(`cw returned non-JSON output: ${error.message}`);
229
+ }
230
+ }
231
+
232
+ function parseArgs(argv) {
233
+ const args = {};
234
+ for (let index = 0; index < argv.length; index += 1) {
235
+ const token = argv[index];
236
+ if (!token.startsWith("--")) {
237
+ (args._ ||= []).push(token);
238
+ continue;
239
+ }
240
+ const raw = token.slice(2);
241
+ const eq = raw.indexOf("=");
242
+ const key = eq >= 0 ? raw.slice(0, eq) : raw;
243
+ const value = eq >= 0 ? raw.slice(eq + 1) : argv[index + 1] && !argv[index + 1].startsWith("--") ? argv[++index] : true;
244
+ if (args[key] === undefined) args[key] = value;
245
+ else if (Array.isArray(args[key])) args[key].push(value);
246
+ else args[key] = [args[key], value];
247
+ }
248
+ return args;
249
+ }
250
+
251
+ function appendPassThrough(argv, args, keys) {
252
+ for (const key of keys) {
253
+ if (args[key] === undefined) continue;
254
+ const flag = key.includes("-") ? `--${key}` : `--${key}`;
255
+ if (args[key] === true) argv.push(flag);
256
+ else appendRepeated(argv, flag, args[key]);
257
+ }
258
+ }
259
+
260
+ function appendRepeated(argv, flag, value) {
261
+ if (value === undefined || value === false) return;
262
+ const values = Array.isArray(value) ? value : [value];
263
+ for (const entry of values) argv.push(flag, String(entry));
264
+ }
265
+
266
+ function appendOption(argv, flag, value) {
267
+ if (value === undefined || value === false || value === true || value === "") return;
268
+ argv.push(flag, String(value));
269
+ }
270
+
271
+ function required(value, name) {
272
+ const text = stringArg(value);
273
+ if (!text) die(`missing required --${name}`);
274
+ return text;
275
+ }
276
+
277
+ function stringArg(value) {
278
+ if (value === undefined || value === null || value === true || value === false) return "";
279
+ return Array.isArray(value) ? String(value[value.length - 1] || "") : String(value);
280
+ }
281
+
282
+ function truthy(value) {
283
+ return value === true || value === "true" || value === "1" || value === "yes";
284
+ }
285
+
286
+ function modelPolicyEnv(fastModel, strongModel) {
287
+ return {
288
+ ...(fastModel ? { CW_ARCHITECTURE_REVIEW_FAST_MODEL: fastModel } : {}),
289
+ ...(strongModel ? { CW_ARCHITECTURE_REVIEW_STRONG_MODEL: strongModel } : {})
290
+ };
291
+ }
292
+
293
+ function nowNs() {
294
+ return process.hrtime.bigint();
295
+ }
296
+
297
+ function elapsedMs(started) {
298
+ return Number((process.hrtime.bigint() - started) / 1000000n);
299
+ }
300
+
301
+ function timed(fn) {
302
+ const started = nowNs();
303
+ const value = fn();
304
+ return { value, elapsedMs: elapsedMs(started) };
305
+ }
306
+
307
+ function buildMetrics(started, contextText, sourceContextElapsedMs, fastReview, fastReviewElapsedMs, fullReviewScheduleElapsedMs) {
308
+ const steps = Array.isArray(fastReview?.steps) ? fastReview.steps : [];
309
+ const handleKinds = countBy(steps.map((step) => step && step.handleKind).filter(Boolean));
310
+ const actions = countBy(steps.map((step) => step && step.action).filter(Boolean));
311
+ return {
312
+ totalElapsedMs: elapsedMs(started),
313
+ sourceContext: {
314
+ elapsedMs: sourceContextElapsedMs,
315
+ bytes: Buffer.byteLength(contextText, "utf8")
316
+ },
317
+ fastReview: {
318
+ elapsedMs: fastReviewElapsedMs,
319
+ status: fastReview?.status,
320
+ plannedWorkers: fastReview?.plannedWorkers,
321
+ completedWorkers: fastReview?.completedWorkers,
322
+ steps: steps.length,
323
+ actions,
324
+ handleKinds,
325
+ resultCacheHits: Number(handleKinds["result-cache"] || 0),
326
+ agentSpawns: steps.filter((step) => step && step.backendId === "agent" && step.handleKind && step.handleKind !== "result-cache").length
327
+ },
328
+ ...(fullReviewScheduleElapsedMs === undefined ? {} : { fullReviewSchedule: { elapsedMs: fullReviewScheduleElapsedMs } })
329
+ };
330
+ }
331
+
332
+ function countBy(values) {
333
+ const counts = {};
334
+ for (const value of values) counts[String(value)] = (counts[String(value)] || 0) + 1;
335
+ return counts;
336
+ }
337
+
338
+ function writeJson(value) {
339
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
340
+ }
341
+
342
+ function usage(code) {
343
+ process.stderr.write([
344
+ "usage:",
345
+ " node scripts/architecture-review-fast.js --repo PATH --question TEXT [--agent-command CMD]",
346
+ "",
347
+ "options:",
348
+ " --profile core --ref HEAD --profile-file PATH --cache-dir DIR --context-out PATH",
349
+ " --fast-model MODEL --strong-model MODEL",
350
+ " --invariant TEXT --focus TEXT --preview --once",
351
+ " --schedule-full [--full-delay-minutes N]",
352
+ " --metrics"
353
+ ].join("\n") + "\n");
354
+ process.exitCode = code;
355
+ }
356
+
357
+ function die(message) {
358
+ process.stderr.write(`architecture-review-fast: ${String(message).trim()}\n`);
359
+ process.exit(1);
360
+ }
361
+
362
+ main();
@@ -93,6 +93,7 @@ function main() {
93
93
  // version-sync-check.js asserts.
94
94
  const CANONICAL_APPS = [
95
95
  "architecture-review",
96
+ "architecture-review-fast",
96
97
  "end-to-end-golden-path",
97
98
  "pr-review-fix-ci",
98
99
  "release-cut",
@@ -25,6 +25,23 @@ const canonicalApps = [
25
25
  "Workflow App framework"
26
26
  ]
27
27
  },
28
+ {
29
+ id: "architecture-review-fast",
30
+ args: (workspace) => [
31
+ "--repo",
32
+ workspace,
33
+ "--question",
34
+ "Can a user get a fast architecture answer?",
35
+ "--invariant",
36
+ "Full architecture-review remains available",
37
+ "--focus",
38
+ "Runtime speed",
39
+ "--sourceContext",
40
+ "",
41
+ "--sourceContextDigest",
42
+ ""
43
+ ]
44
+ },
28
45
  {
29
46
  id: "pr-review-fix-ci",
30
47
  args: (workspace) => [
@@ -65,7 +82,7 @@ const canonicalApps = [
65
82
  "--source",
66
83
  "plugins/cool-workflow/docs/workflow-app-framework.7.md",
67
84
  "--scope",
68
- "Cool Workflow v0.1.78",
85
+ "Cool Workflow v0.1.80",
69
86
  "--freshness",
70
87
  "as of release preparation"
71
88
  ]
@@ -85,14 +102,14 @@ function main() {
85
102
  assert.ok(summary, `${app.id} must appear in app list`);
86
103
  assert.equal(summary.sourceKind, "app-directory");
87
104
  assert.equal(summary.legacy, false);
88
- assert.equal(summary.version, "0.1.78");
105
+ assert.equal(summary.version, "0.1.80");
89
106
 
90
107
  const validation = runJson(["app", "validate", manifestPath]);
91
108
  assert.equal(validation.valid, true, `${app.id} manifest must validate`);
92
109
 
93
110
  const shown = runJson(["app", "show", app.id]);
94
111
  assert.equal(shown.app.id, app.id);
95
- assert.equal(shown.app.version, "0.1.78");
112
+ assert.equal(shown.app.version, "0.1.80");
96
113
  assert.ok(shown.app.metadata.canonical, `${app.id} must be marked canonical`);
97
114
  assert.ok(shown.app.sandboxProfiles.length > 0, `${app.id} must declare sandbox profiles`);
98
115
  assertTaskIdsUnique(shown);
@@ -103,7 +120,7 @@ function main() {
103
120
  const plan = runJson(["plan", app.id, ...app.args(workspace)]);
104
121
  const state = JSON.parse(fs.readFileSync(plan.statePath, "utf8"));
105
122
  assert.equal(state.workflow.app.id, app.id);
106
- assert.equal(state.workflow.app.version, "0.1.78");
123
+ assert.equal(state.workflow.app.version, "0.1.80");
107
124
  assert.equal(state.workflow.app.metadata.canonical, true);
108
125
  assert.ok(state.tasks.some((task) => task.requiresEvidence), `${app.id} plan must include evidence gates`);
109
126
  assert.ok(state.tasks.every((task) => task.sandboxProfileId), `${app.id} plan must include sandbox hints`);
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // coverage-gate — run the smoke suite under V8 coverage and fail closed when
5
+ // line coverage of dist/ drops below the floor.
6
+ //
7
+ // Why this exists: nothing measured coverage, so a subsystem could ship dark.
8
+ // It happened: scheduler.ts sat at 12.7% line coverage (only `schedule list`
9
+ // was ever exercised) and no gate noticed. This script makes that class of
10
+ // regression visible and blocking.
11
+ //
12
+ // MECHANISM (this file): spawn test/run-all.js with NODE_V8_COVERAGE set —
13
+ // Node's built-in inspector coverage, inherited by every child process the
14
+ // smokes spawn (CLI invocations, MCP server, workers), so the numbers reflect
15
+ // the real end-to-end surface. Merge the per-process reports byte-wise
16
+ // (covered-by-any-process wins), project onto executable lines of dist/**/*.js,
17
+ // print the worst files, and compare the overall percentage to the floor.
18
+ // node only — no c8, no dependency, same portability constraint as run-all.
19
+ //
20
+ // POLICY (flags/env): the floor. Default 80; override with --min <pct> or
21
+ // CW_COVERAGE_MIN. Raise the default as gaps close — never lower it (ratchet).
22
+ //
23
+ // FAIL CLOSED: a failing suite fails the gate with the suite's exit code; zero
24
+ // coverage reports found fails rather than passing vacuously.
25
+ //
26
+ // Usage: node scripts/coverage-gate.js [--min 80] [--concurrency <n|auto>]
27
+
28
+ const { spawn } = require("node:child_process");
29
+ const fs = require("node:fs");
30
+ const os = require("node:os");
31
+ const path = require("node:path");
32
+
33
+ const SELF = path.basename(__filename);
34
+ const packageDir = path.resolve(__dirname, "..");
35
+ const distDir = path.join(packageDir, "dist");
36
+
37
+ function flagValue(name) {
38
+ const args = process.argv.slice(2);
39
+ const eq = args.find((a) => a.startsWith(`${name}=`));
40
+ if (eq) return eq.slice(name.length + 1);
41
+ const i = args.indexOf(name);
42
+ return i >= 0 ? args[i + 1] : undefined;
43
+ }
44
+
45
+ const floor = Number(flagValue("--min") ?? process.env.CW_COVERAGE_MIN ?? 80);
46
+ if (!Number.isFinite(floor) || floor < 0 || floor > 100) {
47
+ process.stderr.write(`${SELF}: invalid coverage floor — expected 0..100.\n`);
48
+ process.exit(1);
49
+ }
50
+
51
+ const covDir = fs.mkdtempSync(path.join(os.tmpdir(), "cw-coverage-"));
52
+
53
+ function runSuite() {
54
+ return new Promise((resolve) => {
55
+ const args = [path.join(packageDir, "test", "run-all.js")];
56
+ const concurrency = flagValue("--concurrency");
57
+ if (concurrency) args.push("--concurrency", concurrency);
58
+ const child = spawn(process.execPath, args, {
59
+ cwd: packageDir,
60
+ stdio: "inherit",
61
+ env: { ...process.env, NODE_V8_COVERAGE: covDir }
62
+ });
63
+ child.on("close", (code) => resolve(code ?? 1));
64
+ });
65
+ }
66
+
67
+ // Merge per-process V8 reports for one file. Within a single process the
68
+ // ranges nest (function range, then narrower uncovered branches), so paint
69
+ // larger ranges first and let nested ranges override. Across processes a byte
70
+ // is covered if ANY process covered it — a count-0 range in one process must
71
+ // not erase another process's hit.
72
+ function paintProcess(functions, length) {
73
+ const ranges = [];
74
+ for (const fn of functions || []) for (const range of fn.ranges || []) ranges.push(range);
75
+ ranges.sort((a, b) => (b.endOffset - b.startOffset) - (a.endOffset - a.startOffset));
76
+ const view = new Uint8Array(length); // 0 unreported, 1 covered, 2 uncovered
77
+ for (const range of ranges) {
78
+ view.fill(range.count > 0 ? 1 : 2, Math.max(0, range.startOffset), Math.min(range.endOffset, length));
79
+ }
80
+ return view;
81
+ }
82
+
83
+ // A line counts as executable unless it is blank, a comment, or a lone closer.
84
+ // Heuristic, but applied uniformly — the ratchet compares like with like.
85
+ function isExecutableLine(line) {
86
+ const t = line.trim();
87
+ return (
88
+ t.length > 0 && !t.startsWith("//") && !t.startsWith("/*") && !t.startsWith("*") &&
89
+ t !== "}" && t !== "};" && t !== "});"
90
+ );
91
+ }
92
+
93
+ function listDistFiles(dir) {
94
+ const out = [];
95
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
96
+ const p = path.join(dir, entry.name);
97
+ if (entry.isDirectory()) out.push(...listDistFiles(p));
98
+ else if (entry.name.endsWith(".js")) out.push(p);
99
+ }
100
+ return out;
101
+ }
102
+
103
+ function aggregate() {
104
+ const reports = fs.readdirSync(covDir).filter((f) => f.endsWith(".json"));
105
+ if (reports.length === 0) {
106
+ process.stderr.write(`${SELF}: no V8 coverage reports produced — refusing to pass vacuously.\n`);
107
+ process.exit(1);
108
+ }
109
+ const covered = new Map(); // file -> Uint8Array, 1 = covered by any process
110
+ const uncovered = new Map(); // file -> Uint8Array, 1 = reported count-0 somewhere
111
+ const lengths = new Map();
112
+ for (const report of reports) {
113
+ let data;
114
+ try {
115
+ data = JSON.parse(fs.readFileSync(path.join(covDir, report), "utf8"));
116
+ } catch {
117
+ continue; // a process killed mid-write leaves a truncated report
118
+ }
119
+ for (const script of data.result || []) {
120
+ if (!script.url || !script.url.startsWith("file://")) continue;
121
+ const file = decodeURIComponent(script.url.slice("file://".length));
122
+ if (!file.startsWith(distDir + path.sep)) continue;
123
+ let length = lengths.get(file);
124
+ if (length === undefined) {
125
+ try {
126
+ length = fs.readFileSync(file, "utf8").length;
127
+ } catch {
128
+ continue;
129
+ }
130
+ lengths.set(file, length);
131
+ covered.set(file, new Uint8Array(length));
132
+ uncovered.set(file, new Uint8Array(length));
133
+ }
134
+ const view = paintProcess(script.functions, length);
135
+ const coveredBytes = covered.get(file);
136
+ const uncoveredBytes = uncovered.get(file);
137
+ for (let i = 0; i < length; i++) {
138
+ if (view[i] === 1) coveredBytes[i] = 1;
139
+ else if (view[i] === 2) uncoveredBytes[i] = 1;
140
+ }
141
+ }
142
+ }
143
+
144
+ const rows = [];
145
+ for (const file of listDistFiles(distDir)) {
146
+ const source = fs.readFileSync(file, "utf8");
147
+ const length = source.length;
148
+ const coveredBytes = covered.get(file) || new Uint8Array(length);
149
+ const uncoveredBytes = uncovered.get(file) || new Uint8Array(length);
150
+ const loaded = lengths.has(file);
151
+ let offset = 0;
152
+ let total = 0;
153
+ let hit = 0;
154
+ for (const line of source.split("\n")) {
155
+ if (isExecutableLine(line)) {
156
+ total += 1;
157
+ let lineCovered = false;
158
+ let lineReported = false;
159
+ for (let i = offset; i < offset + line.length; i++) {
160
+ if (coveredBytes[i]) {
161
+ lineCovered = true;
162
+ break;
163
+ }
164
+ if (uncoveredBytes[i]) lineReported = true;
165
+ }
166
+ // Unreported bytes in a loaded file are top-level code that ran at
167
+ // require time; in a never-loaded file nothing ran.
168
+ if (lineCovered || (loaded && !lineReported)) hit += 1;
169
+ }
170
+ offset += line.length + 1;
171
+ }
172
+ rows.push({ file: path.relative(distDir, file), total, hit, loaded });
173
+ }
174
+ return rows;
175
+ }
176
+
177
+ (async () => {
178
+ const suiteExit = await runSuite();
179
+ if (suiteExit !== 0) {
180
+ process.stderr.write(`${SELF}: smoke suite failed (exit ${suiteExit}) — coverage not evaluated.\n`);
181
+ process.exit(suiteExit);
182
+ }
183
+ const rows = aggregate();
184
+ let total = 0;
185
+ let hit = 0;
186
+ for (const row of rows) {
187
+ total += row.total;
188
+ hit += row.hit;
189
+ }
190
+ const overall = total ? (100 * hit) / total : 0;
191
+
192
+ rows.sort((a, b) => a.hit / Math.max(1, a.total) - b.hit / Math.max(1, b.total));
193
+ process.stdout.write(`\n${SELF}: line coverage of dist/ under the full smoke suite\n`);
194
+ process.stdout.write(" lowest-covered files:\n");
195
+ // Type-only modules compile to a 2-line "use strict" stub that is never
196
+ // require()d; they count toward the overall number but would bury the
197
+ // actionable entries in this list.
198
+ const actionable = rows.filter((row) => row.total > 5);
199
+ for (const row of actionable.slice(0, 10)) {
200
+ const pct = ((100 * row.hit) / Math.max(1, row.total)).toFixed(1).padStart(5);
201
+ process.stdout.write(` ${pct}% ${String(row.hit).padStart(5)}/${String(row.total).padEnd(5)} ${row.file}${row.loaded ? "" : " (never loaded)"}\n`);
202
+ }
203
+ process.stdout.write(` OVERALL: ${hit}/${total} executable lines = ${overall.toFixed(1)}% (floor ${floor}%)\n`);
204
+
205
+ fs.rmSync(covDir, { recursive: true, force: true });
206
+ if (overall < floor) {
207
+ process.stderr.write(`${SELF}: FAIL — overall coverage ${overall.toFixed(1)}% is below the ${floor}% floor.\n`);
208
+ process.exit(1);
209
+ }
210
+ process.stdout.write(`${SELF}: PASS — coverage holds the ${floor}% floor.\n`);
211
+ })();
@@ -5,7 +5,7 @@ const { spawnSync } = require("node:child_process");
5
5
  const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
 
8
- const TARGET_VERSION = "0.1.78";
8
+ const TARGET_VERSION = "0.1.80";
9
9
  const PREVIOUS_VERSION = "0.1.31";
10
10
  const pluginRoot = path.resolve(__dirname, "..");
11
11
  const repoRoot = path.resolve(pluginRoot, "..", "..");
@@ -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.1.78");
36
+ assert.equal(appValidation.summary.version, "0.1.80");
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.1.78 end-to-end golden path."
45
+ "Prove the deterministic v0.1.80 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.1.78");
55
+ assert.equal(state.workflow.app.version, "0.1.80");
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\.1\.78/);
198
+ assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.80/);
199
199
  assert.match(report, /## Candidates/);
200
200
  assert.match(report, /## Trust Audit/);
201
201
  assert.match(report, /## Acceptance Rationale/);