cool-workflow 0.1.79 → 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 +9 -1
- 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 +38 -0
- package/dist/capability-registry.js +11 -8
- package/dist/cli.js +10 -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 +48 -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/version.js +1 -1
- package/docs/agent-delegation-drive.7.md +58 -0
- package/docs/canonical-workflow-apps.7.md +37 -0
- package/docs/cli-mcp-parity.7.md +12 -0
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/index.md +1 -0
- package/docs/launch/demo.tape +28 -0
- package/docs/launch/launch-kit.md +59 -3
- package/docs/launch/pre-launch-checklist.md +53 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +13 -5
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/routines.md +23 -0
- package/docs/run-registry-control-plane.7.md +42 -1
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/source-context-profiles.7.md +119 -0
- package/docs/state-explosion-management.7.md +11 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/unix-principles.md +49 -1
- package/docs/web-desktop-workbench.7.md +4 -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,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/);
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// source-context — opt-in JSONL source context exporter.
|
|
5
|
+
//
|
|
6
|
+
// Policy is data in manifest/source-context-profiles.json. This script is only
|
|
7
|
+
// mechanism: enumerate tracked files for a git ref, classify them through the
|
|
8
|
+
// selected profile, hash the committed bytes, and print JSONL to stdout.
|
|
9
|
+
|
|
10
|
+
const crypto = require("node:crypto");
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const path = require("node:path");
|
|
13
|
+
const { spawnSync } = require("node:child_process");
|
|
14
|
+
|
|
15
|
+
const pluginRoot = path.resolve(__dirname, "..");
|
|
16
|
+
const defaultRepoRoot = path.resolve(pluginRoot, "..", "..");
|
|
17
|
+
let repoRoot = defaultRepoRoot;
|
|
18
|
+
const DEFAULT_PROFILE_FILE = path.join(pluginRoot, "manifest", "source-context-profiles.json");
|
|
19
|
+
|
|
20
|
+
const command = process.argv[2];
|
|
21
|
+
const args = process.argv.slice(3);
|
|
22
|
+
|
|
23
|
+
function main() {
|
|
24
|
+
if (!["export", "manifest", "profiles"].includes(command)) {
|
|
25
|
+
usage(1, `unknown command: ${command || "(missing)"}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const profileFile = valueArg("--profile-file") || DEFAULT_PROFILE_FILE;
|
|
30
|
+
repoRoot = path.resolve(valueArg("--repo-root") || defaultRepoRoot);
|
|
31
|
+
const profiles = readProfiles(profileFile);
|
|
32
|
+
|
|
33
|
+
if (command === "profiles") {
|
|
34
|
+
for (const [id, profile] of Object.entries(profiles.profiles)) {
|
|
35
|
+
writeJsonl({
|
|
36
|
+
schemaVersion: profiles.schemaVersion,
|
|
37
|
+
id,
|
|
38
|
+
description: profile.description || "",
|
|
39
|
+
maxLines: Number(profile.maxLines) || null,
|
|
40
|
+
include: profile.include || [],
|
|
41
|
+
exclude: profile.exclude || []
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const profileId = valueArg("--profile") || "core";
|
|
48
|
+
const profile = profiles.profiles[profileId];
|
|
49
|
+
if (!profile) die(`unknown profile: ${profileId}`);
|
|
50
|
+
|
|
51
|
+
const ref = resolveRef(valueArg("--ref") || "HEAD");
|
|
52
|
+
const changedFrom = valueArg("--changed-from") ? resolveRef(valueArg("--changed-from")) : "";
|
|
53
|
+
const changedPaths = changedFrom ? changedPathSet(changedFrom, ref) : null;
|
|
54
|
+
const cacheDir = command === "export" ? valueArg("--cache-dir") : "";
|
|
55
|
+
const cachePath = cacheDir ? sourceContextCachePath(cacheDir, profileId, ref, profile, changedFrom) : "";
|
|
56
|
+
if (cachePath && fs.existsSync(cachePath)) {
|
|
57
|
+
process.stdout.write(readValidCache(cachePath, profileId, ref, changedFrom));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const files = gitLines(["ls-tree", "-r", "--name-only", ref]).filter((file) => !changedPaths || changedPaths.has(file));
|
|
62
|
+
let exportedLines = 0;
|
|
63
|
+
const buffered = cachePath ? [] : null;
|
|
64
|
+
const emit = (value) => {
|
|
65
|
+
if (buffered) buffered.push(JSON.stringify(value));
|
|
66
|
+
else writeJsonl(value);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const classification = classify(file, profile);
|
|
71
|
+
const blob = gitBlob(ref, file);
|
|
72
|
+
const binary = isBinary(blob);
|
|
73
|
+
const record = {
|
|
74
|
+
schemaVersion: profiles.schemaVersion,
|
|
75
|
+
profile: profileId,
|
|
76
|
+
ref,
|
|
77
|
+
path: file,
|
|
78
|
+
bytes: blob.length,
|
|
79
|
+
lines: binary ? null : countLines(blob),
|
|
80
|
+
sha256: sha256(blob),
|
|
81
|
+
included: classification.included,
|
|
82
|
+
reason: classification.reason,
|
|
83
|
+
...(changedFrom ? { changedFrom } : {})
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (command === "manifest") {
|
|
87
|
+
emit(record);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!classification.included) continue;
|
|
92
|
+
if (binary) die(`included file is binary: ${file}`);
|
|
93
|
+
exportedLines += record.lines || 0;
|
|
94
|
+
emit({ ...record, content: blob.toString("utf8") });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const maxLines = Number(profile.maxLines) || 0;
|
|
98
|
+
if (command === "export" && maxLines > 0 && exportedLines > maxLines) {
|
|
99
|
+
die(`profile ${profileId} exported ${exportedLines} lines, above maxLines ${maxLines}`);
|
|
100
|
+
}
|
|
101
|
+
if (cachePath && buffered) {
|
|
102
|
+
const text = buffered.map((line) => `${line}\n`).join("");
|
|
103
|
+
writeCache(cachePath, text);
|
|
104
|
+
process.stdout.write(text);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function usage(code, message) {
|
|
109
|
+
if (message) process.stderr.write(`source-context: ${message}\n`);
|
|
110
|
+
process.stderr.write(
|
|
111
|
+
[
|
|
112
|
+
"usage:",
|
|
113
|
+
" node scripts/source-context.js profiles",
|
|
114
|
+
" node scripts/source-context.js manifest [--profile core] [--ref HEAD] [--changed-from REF] [--repo-root DIR]",
|
|
115
|
+
" node scripts/source-context.js export [--profile core] [--ref HEAD] [--changed-from REF] [--repo-root DIR] [--cache-dir DIR]"
|
|
116
|
+
].join("\n") + "\n"
|
|
117
|
+
);
|
|
118
|
+
process.exitCode = code;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function valueArg(name) {
|
|
122
|
+
const eq = args.find((arg) => arg.startsWith(`${name}=`));
|
|
123
|
+
if (eq) return eq.slice(name.length + 1);
|
|
124
|
+
const idx = args.indexOf(name);
|
|
125
|
+
return idx >= 0 ? args[idx + 1] : "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readProfiles(file) {
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
die(`cannot read profile file ${rel(file)}: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
if (!parsed || parsed.schemaVersion !== 1 || !parsed.profiles || typeof parsed.profiles !== "object") {
|
|
136
|
+
die(`invalid source context profile file: ${rel(file)}`);
|
|
137
|
+
}
|
|
138
|
+
for (const [id, profile] of Object.entries(parsed.profiles)) {
|
|
139
|
+
if (!Array.isArray(profile.include) || !Array.isArray(profile.exclude)) {
|
|
140
|
+
die(`profile ${id} must define include and exclude arrays`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveRef(ref) {
|
|
147
|
+
return git(["rev-parse", "--verify", `${ref}^{commit}`]).trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function gitLines(argv) {
|
|
151
|
+
return git(argv).split(/\r?\n/).filter(Boolean);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function changedPathSet(base, ref) {
|
|
155
|
+
return new Set(gitLines(["diff", "--name-only", "--diff-filter=ACMRT", `${base}..${ref}`]));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function git(argv) {
|
|
159
|
+
const result = spawnSync("git", argv, { cwd: repoRoot, encoding: "utf8" });
|
|
160
|
+
if (result.status !== 0) die((result.stderr || result.stdout || `git ${argv.join(" ")} failed`).trim());
|
|
161
|
+
return result.stdout;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function gitBlob(ref, file) {
|
|
165
|
+
const result = spawnSync("git", ["show", `${ref}:${file}`], { cwd: repoRoot, encoding: "buffer", maxBuffer: 1024 * 1024 * 64 });
|
|
166
|
+
if (result.status !== 0) die((result.stderr || result.stdout || `cannot read ${file} at ${ref}`).toString().trim());
|
|
167
|
+
return result.stdout;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function classify(file, profile) {
|
|
171
|
+
const excludedBy = (profile.exclude || []).find((pattern) => matches(pattern, file));
|
|
172
|
+
if (excludedBy) return { included: false, reason: `excluded:${excludedBy}` };
|
|
173
|
+
const includedBy = (profile.include || []).find((pattern) => matches(pattern, file));
|
|
174
|
+
if (includedBy) return { included: true, reason: `included:${includedBy}` };
|
|
175
|
+
return { included: false, reason: "not-included" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function matches(pattern, file) {
|
|
179
|
+
if (pattern.endsWith("/**")) {
|
|
180
|
+
const dir = pattern.slice(0, -3);
|
|
181
|
+
return file === dir || file.startsWith(`${dir}/`);
|
|
182
|
+
}
|
|
183
|
+
if (!pattern.includes("*")) return file === pattern;
|
|
184
|
+
const escaped = pattern
|
|
185
|
+
.split("*")
|
|
186
|
+
.map((part) => part.replace(/[|\\{}()[\]^$+?.]/g, "\\$&"))
|
|
187
|
+
.join("[^/]*");
|
|
188
|
+
return new RegExp(`^${escaped}$`).test(file);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function countLines(buffer) {
|
|
192
|
+
if (buffer.length === 0) return 0;
|
|
193
|
+
let count = 0;
|
|
194
|
+
for (const byte of buffer) if (byte === 10) count++;
|
|
195
|
+
return buffer[buffer.length - 1] === 10 ? count : count + 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isBinary(buffer) {
|
|
199
|
+
return buffer.includes(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sha256(buffer) {
|
|
203
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function profileDigest(profileId, profile) {
|
|
207
|
+
return sha256(Buffer.from(stableStringify({ profileId, profile }), "utf8"));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function sourceContextCachePath(cacheDir, profileId, ref, profile, changedFrom) {
|
|
211
|
+
const safeProfile = String(profileId).replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
212
|
+
const diffPart = changedFrom ? `-changed-${changedFrom.slice(0, 12)}` : "";
|
|
213
|
+
const digest = sha256(Buffer.from(stableStringify({ profileId, profile, changedFrom: changedFrom || "" }), "utf8")).slice(0, 16);
|
|
214
|
+
return path.join(path.resolve(cacheDir), `${safeProfile}-${ref.slice(0, 12)}${diffPart}-${digest}.jsonl`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function readValidCache(file, profileId, ref, changedFrom) {
|
|
218
|
+
let text;
|
|
219
|
+
try {
|
|
220
|
+
text = fs.readFileSync(file, "utf8");
|
|
221
|
+
} catch (error) {
|
|
222
|
+
die(`cannot read source context cache ${rel(file)}: ${error.message}`);
|
|
223
|
+
}
|
|
224
|
+
if (text.length > 0 && !text.endsWith("\n")) {
|
|
225
|
+
die(`invalid source context cache ${rel(file)}: missing trailing newline`);
|
|
226
|
+
}
|
|
227
|
+
for (const line of text.split(/\n/)) {
|
|
228
|
+
if (!line) continue;
|
|
229
|
+
let record;
|
|
230
|
+
try {
|
|
231
|
+
record = JSON.parse(line);
|
|
232
|
+
} catch {
|
|
233
|
+
die(`invalid source context cache ${rel(file)}: non-JSONL record`);
|
|
234
|
+
}
|
|
235
|
+
if (
|
|
236
|
+
!record ||
|
|
237
|
+
record.profile !== profileId ||
|
|
238
|
+
record.ref !== ref ||
|
|
239
|
+
String(record.changedFrom || "") !== String(changedFrom || "") ||
|
|
240
|
+
record.included !== true ||
|
|
241
|
+
typeof record.path !== "string" ||
|
|
242
|
+
typeof record.content !== "string" ||
|
|
243
|
+
!/^[0-9a-f]{64}$/.test(String(record.sha256 || ""))
|
|
244
|
+
) {
|
|
245
|
+
die(`invalid source context cache ${rel(file)}: record does not match profile/ref`);
|
|
246
|
+
}
|
|
247
|
+
const contentBytes = Buffer.from(record.content, "utf8");
|
|
248
|
+
if (
|
|
249
|
+
record.sha256 !== sha256(contentBytes) ||
|
|
250
|
+
record.bytes !== contentBytes.length ||
|
|
251
|
+
record.lines !== countLines(contentBytes)
|
|
252
|
+
) {
|
|
253
|
+
die(`invalid source context cache ${rel(file)}: content digest mismatch`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return text;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function writeCache(file, text) {
|
|
260
|
+
try {
|
|
261
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
262
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
263
|
+
fs.writeFileSync(tmp, text, "utf8");
|
|
264
|
+
fs.renameSync(tmp, file);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
die(`cannot write source context cache ${rel(file)}: ${error.message}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function stableStringify(value) {
|
|
271
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
272
|
+
if (value && typeof value === "object") {
|
|
273
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
274
|
+
}
|
|
275
|
+
return JSON.stringify(value);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function writeJsonl(value) {
|
|
279
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function rel(file) {
|
|
283
|
+
return path.relative(repoRoot, path.resolve(file));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function die(message) {
|
|
287
|
+
process.stderr.write(`source-context: ${message}\n`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
main();
|
|
@@ -49,6 +49,7 @@ function readReleaseSource(relativePath) {
|
|
|
49
49
|
const VERSION = JSON.parse(readReleaseSource("plugins/cool-workflow/package.json").text).version;
|
|
50
50
|
const canonicalApps = [
|
|
51
51
|
"architecture-review",
|
|
52
|
+
"architecture-review-fast",
|
|
52
53
|
"end-to-end-golden-path",
|
|
53
54
|
"pr-review-fix-ci",
|
|
54
55
|
"release-cut",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ci-triage
|
|
3
|
+
description: >-
|
|
4
|
+
Diagnose failing CI, build, release-gate, or test runs for Cool Workflow. Use
|
|
5
|
+
when a GitHub Actions check, local npm test, npm run build, release gate,
|
|
6
|
+
smoke test, or generated-manifest check fails and Codex must identify the
|
|
7
|
+
first actionable failure with logs and verifier commands.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# CI Triage
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Triage the failure before editing. Keep stdout/log evidence separate from
|
|
15
|
+
diagnosis, identify the first failing command, and end with one verifier command
|
|
16
|
+
that proves the proposed fix.
|
|
17
|
+
|
|
18
|
+
## Workflow
|
|
19
|
+
|
|
20
|
+
1. Capture the failing command, exit code, and the earliest meaningful error.
|
|
21
|
+
2. Classify the failure as code, test expectation, generated artifact drift,
|
|
22
|
+
environment, timeout, or external service.
|
|
23
|
+
3. Inspect only the files needed to explain that first failure.
|
|
24
|
+
4. Propose or implement the smallest fix.
|
|
25
|
+
5. Run the narrow verifier first, then the full gate when the fix is plausible.
|
|
26
|
+
6. Write the lesson back to `PROJECT_MEMORY.md`, an eval case, or the matching
|
|
27
|
+
workflow skill when the failure pattern is likely to recur.
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm run build
|
|
33
|
+
npm test
|
|
34
|
+
npm run gen:manifests -- --check
|
|
35
|
+
npm run index:check
|
|
36
|
+
git diff --check
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use smoke tests directly for narrow verification:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node test/<name>-smoke.js
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Output Rules
|
|
46
|
+
|
|
47
|
+
- Lead with the failing command and root cause.
|
|
48
|
+
- Quote only the shortest relevant log lines.
|
|
49
|
+
- Separate "confirmed" from "inference".
|
|
50
|
+
- Include the verifier commands actually run or still required.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cool-workflow
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Use when the user asks for Cool Workflow, CW, agent workflow control-plane,
|
|
5
|
+
TypeScript workflow orchestration, phased multi-agent work, background
|
|
6
|
+
workflow tasks, reusable workflow apps, or auditable agent run state.
|
|
4
7
|
---
|
|
5
8
|
|
|
6
9
|
# Cool Workflow
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-check
|
|
3
|
+
description: >-
|
|
4
|
+
Verify Cool Workflow release, publish, deploy, or package readiness. Use when
|
|
5
|
+
Codex must check build/test gates, generated manifests, project index sync,
|
|
6
|
+
dist/source contract, changelog/release notes, npm package contents, or
|
|
7
|
+
pre-tag risk before shipping.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Deploy Check
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Deploy check is the verifier side of release work. It confirms the artifact a
|
|
15
|
+
user receives matches the source, docs, manifests, and stated capability.
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
1. Confirm the intended capability and release scope.
|
|
20
|
+
2. Inspect branch status and generated artifact drift.
|
|
21
|
+
3. Run the deterministic gates.
|
|
22
|
+
4. Check docs/man-page coverage for shipped behavior.
|
|
23
|
+
5. Check package contents and `dist/` policy.
|
|
24
|
+
6. Report risk before any tag or publish step.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run build
|
|
30
|
+
npm test
|
|
31
|
+
npm run gen:manifests -- --check
|
|
32
|
+
npm run index:check
|
|
33
|
+
npm run version:sync
|
|
34
|
+
npm run release:check
|
|
35
|
+
git diff --check
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For package inspection:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm pack --dry-run
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Red Lines
|
|
45
|
+
|
|
46
|
+
- Do not tag without test evidence.
|
|
47
|
+
- Do not write reviewer verdict files by hand.
|
|
48
|
+
- Do not silently skip `dist/` drift if the package still ships `dist/`.
|
|
49
|
+
- Do not publish undocumented behavior.
|
|
50
|
+
- Do not call a release ready if generated manifests or project index drift.
|
|
51
|
+
|
|
52
|
+
## Output Rules
|
|
53
|
+
|
|
54
|
+
Return a ship/no-ship verdict, gate results, artifact risks, and the next
|
|
55
|
+
operator action.
|