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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +29 -3
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +64 -0
- package/apps/architecture-review-fast/workflow.js +153 -0
- 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/capability-core.js +71 -0
- package/dist/capability-registry.js +13 -8
- package/dist/cli.js +49 -1
- package/dist/drive.js +74 -1
- package/dist/evidence-reasoning.js +2 -2
- package/dist/execution-backend.js +6 -1
- package/dist/mcp-server.js +56 -13
- package/dist/orchestrator/lifecycle-operations.js +2 -1
- package/dist/orchestrator.js +1 -1
- package/dist/run-export.js +370 -25
- package/dist/run-registry.js +11 -4
- package/dist/state-explosion.js +100 -21
- package/dist/telemetry-demo.js +154 -0
- package/dist/version.js +1 -1
- package/docs/agent-delegation-drive.7.md +60 -0
- package/docs/canonical-workflow-apps.7.md +37 -0
- package/docs/cli-mcp-parity.7.md +14 -0
- package/docs/contract-migration-tooling.7.md +6 -0
- package/docs/control-plane-scheduling.7.md +6 -0
- package/docs/durable-state-and-locking.7.md +6 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
- package/docs/execution-backends.7.md +6 -0
- package/docs/index.md +1 -0
- package/docs/launch/demo.tape +28 -0
- package/docs/launch/launch-kit.md +172 -0
- package/docs/launch/pre-launch-checklist.md +53 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +6 -0
- package/docs/multi-agent-eval-replay-harness.7.md +6 -0
- package/docs/multi-agent-operator-ux.7.md +6 -0
- package/docs/node-snapshot-diff-replay.7.md +6 -0
- package/docs/observability-cost-accounting.7.md +6 -0
- package/docs/project-index.md +16 -6
- package/docs/real-execution-backends.7.md +6 -0
- package/docs/release-and-migration.7.md +6 -0
- package/docs/release-tooling.7.md +6 -0
- package/docs/routines.md +23 -0
- package/docs/run-registry-control-plane.7.md +44 -1
- package/docs/run-retention-reclamation.7.md +6 -0
- package/docs/source-context-profiles.7.md +119 -0
- package/docs/state-explosion-management.7.md +13 -0
- package/docs/team-collaboration.7.md +6 -0
- package/docs/unix-principles.md +49 -1
- package/docs/web-desktop-workbench.7.md +6 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +142 -0
- package/package.json +2 -1
- package/scripts/agents/claude-p-agent.js +129 -43
- package/scripts/architecture-review-fast.js +362 -0
- package/scripts/bump-version.js +1 -0
- package/scripts/canonical-apps.js +21 -4
- package/scripts/coverage-gate.js +211 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/source-context.js +291 -0
- package/scripts/version-sync-check.js +1 -0
- package/skills/ci-triage/SKILL.md +50 -0
- package/skills/ci-triage/agents/openai.yaml +4 -0
- package/skills/cool-workflow/SKILL.md +4 -1
- package/skills/deploy-check/SKILL.md +55 -0
- package/skills/deploy-check/agents/openai.yaml +4 -0
- package/skills/design-qa/SKILL.md +49 -0
- package/skills/design-qa/agents/openai.yaml +4 -0
- package/skills/pr-review/SKILL.md +45 -0
- 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();
|
package/scripts/bump-version.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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, "..", "..");
|
package/scripts/golden-path.js
CHANGED
|
@@ -33,7 +33,7 @@ function main() {
|
|
|
33
33
|
const appValidation = runJson(["app", "validate", "end-to-end-golden-path"], pluginRoot);
|
|
34
34
|
assert.equal(appValidation.valid, true);
|
|
35
35
|
assert.equal(appValidation.summary.id, "end-to-end-golden-path");
|
|
36
|
-
assert.equal(appValidation.summary.version, "0.1.
|
|
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.
|
|
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.
|
|
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\.
|
|
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/);
|