cool-workflow 0.1.79 → 0.1.81

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 (131) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +51 -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/agent-config.js +21 -7
  12. package/dist/candidate-scoring.js +42 -22
  13. package/dist/capability-core.js +132 -17
  14. package/dist/capability-registry.js +138 -168
  15. package/dist/cli.js +97 -98
  16. package/dist/collaboration.js +5 -6
  17. package/dist/commit.js +20 -6
  18. package/dist/compare.js +18 -0
  19. package/dist/coordinator/classify.js +45 -0
  20. package/dist/coordinator/paths.js +42 -0
  21. package/dist/coordinator/util.js +129 -0
  22. package/dist/coordinator.js +127 -300
  23. package/dist/dispatch.js +35 -0
  24. package/dist/drive.js +79 -6
  25. package/dist/error-feedback.js +8 -4
  26. package/dist/evidence-reasoning.js +3 -3
  27. package/dist/execution-backend/agent.js +331 -0
  28. package/dist/execution-backend/probes.js +96 -0
  29. package/dist/execution-backend/util.js +47 -0
  30. package/dist/execution-backend.js +73 -421
  31. package/dist/mcp-server.js +79 -183
  32. package/dist/multi-agent/graph.js +84 -0
  33. package/dist/multi-agent/helpers.js +145 -0
  34. package/dist/multi-agent/paths.js +22 -0
  35. package/dist/multi-agent-eval/format.js +194 -0
  36. package/dist/multi-agent-eval/normalize.js +51 -0
  37. package/dist/multi-agent-eval.js +39 -244
  38. package/dist/multi-agent-host.js +0 -19
  39. package/dist/multi-agent.js +125 -314
  40. package/dist/node-snapshot.js +3 -3
  41. package/dist/observability/format.js +61 -0
  42. package/dist/observability/intake.js +98 -0
  43. package/dist/observability.js +14 -160
  44. package/dist/operator-ux/format.js +364 -0
  45. package/dist/operator-ux.js +22 -363
  46. package/dist/orchestrator/lifecycle-operations.js +2 -1
  47. package/dist/orchestrator/report.js +8 -0
  48. package/dist/orchestrator.js +26 -9
  49. package/dist/reclamation.js +26 -21
  50. package/dist/run-export.js +494 -25
  51. package/dist/run-registry/derive.js +172 -0
  52. package/dist/run-registry/format.js +124 -0
  53. package/dist/run-registry/gc.js +251 -0
  54. package/dist/run-registry/policy.js +16 -0
  55. package/dist/run-registry/queue.js +116 -0
  56. package/dist/run-registry.js +89 -597
  57. package/dist/run-state-schema.js +1 -0
  58. package/dist/sandbox-profile.js +43 -2
  59. package/dist/state-explosion/format.js +159 -0
  60. package/dist/state-explosion/helpers.js +82 -0
  61. package/dist/state-explosion.js +165 -304
  62. package/dist/state-node.js +19 -4
  63. package/dist/telemetry-attestation.js +55 -0
  64. package/dist/telemetry-demo.js +15 -3
  65. package/dist/telemetry-ledger.js +60 -15
  66. package/dist/topology.js +25 -8
  67. package/dist/triggers.js +33 -14
  68. package/dist/trust-audit.js +145 -33
  69. package/dist/version.js +1 -1
  70. package/dist/worker-isolation/helpers.js +51 -0
  71. package/dist/worker-isolation/paths.js +46 -0
  72. package/dist/worker-isolation.js +39 -115
  73. package/docs/agent-delegation-drive.7.md +71 -0
  74. package/docs/canonical-workflow-apps.7.md +37 -0
  75. package/docs/cli-mcp-parity.7.md +16 -0
  76. package/docs/contract-migration-tooling.7.md +6 -0
  77. package/docs/control-plane-scheduling.7.md +6 -0
  78. package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
  79. package/docs/durable-state-and-locking.7.md +8 -0
  80. package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
  81. package/docs/execution-backends.7.md +6 -0
  82. package/docs/index.md +2 -0
  83. package/docs/launch/demo.tape +28 -0
  84. package/docs/launch/launch-kit.md +96 -17
  85. package/docs/launch/pre-launch-checklist.md +53 -0
  86. package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
  87. package/docs/multi-agent-eval-replay-harness.7.md +6 -0
  88. package/docs/multi-agent-operator-ux.7.md +6 -0
  89. package/docs/multi-agent-trust-policy-audit.7.md +27 -0
  90. package/docs/node-snapshot-diff-replay.7.md +6 -0
  91. package/docs/observability-cost-accounting.7.md +6 -0
  92. package/docs/project-index.md +27 -6
  93. package/docs/real-execution-backends.7.md +6 -0
  94. package/docs/release-and-migration.7.md +8 -0
  95. package/docs/release-tooling.7.md +6 -0
  96. package/docs/routines.md +23 -0
  97. package/docs/run-registry-control-plane.7.md +89 -2
  98. package/docs/run-retention-reclamation.7.md +8 -0
  99. package/docs/source-context-profiles.7.md +119 -0
  100. package/docs/state-explosion-management.7.md +13 -0
  101. package/docs/team-collaboration.7.md +6 -0
  102. package/docs/trust-model.md +267 -0
  103. package/docs/unix-principles.md +49 -1
  104. package/docs/vendor-manifest-loadability.7.md +43 -0
  105. package/docs/web-desktop-workbench.7.md +6 -0
  106. package/manifest/plugin.manifest.json +1 -1
  107. package/manifest/source-context-profiles.json +142 -0
  108. package/package.json +4 -1
  109. package/scripts/agents/builtin-templates.json +7 -0
  110. package/scripts/agents/claude-p-agent.js +129 -43
  111. package/scripts/architecture-review-fast.js +362 -0
  112. package/scripts/bump-version.js +5 -10
  113. package/scripts/canonical-apps-list.js +64 -0
  114. package/scripts/canonical-apps.js +36 -4
  115. package/scripts/coverage-gate.js +211 -0
  116. package/scripts/dogfood-release.js +1 -1
  117. package/scripts/golden-path.js +4 -4
  118. package/scripts/parity-check.js +5 -0
  119. package/scripts/release-check.js +5 -1
  120. package/scripts/source-context.js +291 -0
  121. package/scripts/version-sync-check.js +5 -7
  122. package/skills/ci-triage/SKILL.md +50 -0
  123. package/skills/ci-triage/agents/openai.yaml +4 -0
  124. package/skills/cool-workflow/SKILL.md +4 -1
  125. package/skills/deploy-check/SKILL.md +55 -0
  126. package/skills/deploy-check/agents/openai.yaml +4 -0
  127. package/skills/design-qa/SKILL.md +49 -0
  128. package/skills/design-qa/agents/openai.yaml +4 -0
  129. package/skills/pr-review/SKILL.md +45 -0
  130. package/skills/pr-review/agents/openai.yaml +4 -0
  131. package/dist/capability-dispatcher.js +0 -86
@@ -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.79";
8
+ const TARGET_VERSION = "0.1.81";
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.79");
36
+ assert.equal(appValidation.summary.version, "0.1.81");
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.79 end-to-end golden path."
45
+ "Prove the deterministic v0.1.81 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.79");
55
+ assert.equal(state.workflow.app.version, "0.1.81");
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\.79/);
198
+ assert.match(report, /Workflow App: end-to-end-golden-path@0\.1\.81/);
199
199
  assert.match(report, /## Candidates/);
200
200
  assert.match(report, /## Trust Audit/);
201
201
  assert.match(report, /## Acceptance Rationale/);
@@ -150,6 +150,11 @@ async function payloadParity() {
150
150
  for (const [capability, mcpTool] of GLOBAL_PROBES) {
151
151
  const cap = capById(capability);
152
152
  assert.equal(cap.mcp.tool, mcpTool, `probe/registry MCP tool mismatch for ${capability}`);
153
+ // jsonMode is the single source for the CLI's --json policy; this probe only
154
+ // appends --json for "flag" verbs and JSON.parse-es the result. The human
155
+ // rendering and "default"-verb no-flag JSON are pinned to cap.cli.jsonMode by
156
+ // the companion test/cli-jsonmode-parity-smoke.js, so cli.ts can't silently
157
+ // re-encode that policy by hand and drift from this registry data.
153
158
  const cliArgv = [...cap.cli.path, ...(cap.cli.jsonMode === "flag" ? ["--json"] : [])];
154
159
  const cliOut = JSON.parse(execFileSync(node, [cli, ...cliArgv], { cwd: workspace, encoding: "utf8" }));
155
160
  const mcpOut = await mcp.tool(mcpTool, { cwd: workspace });
@@ -58,7 +58,11 @@ const checks = [
58
58
  { name: "dist freshness", command: ["npm", "run", "dist:check"] },
59
59
  { name: "type check", command: ["npm", "run", "check"] },
60
60
  { name: "run-state schema consistency", command: ["node", "scripts/validate-run-state-schema.js"] },
61
- { name: "tests", command: ["npm", "test"] },
61
+ // Parallel suite (test:ci = run-all.js --concurrency auto). Each smoke runs in
62
+ // a private cwd + state roots (CW_HOME/HOME/TMPDIR), so concurrency is race-free.
63
+ // The bare `npm test` and the tag-gate (release-gate.sh) stay sequential as the
64
+ // deterministic backstop.
65
+ { name: "tests", command: ["npm", "run", "test:ci"] },
62
66
  { name: "canonical apps", command: ["npm", "run", "canonical-apps"] },
63
67
  { name: "golden path", command: ["npm", "run", "golden-path"] },
64
68
  { name: "CLI MCP parity", command: ["npm", "run", "parity:check"] },
@@ -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();
@@ -5,6 +5,7 @@ const assert = require("node:assert/strict");
5
5
  const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
  const { spawnSync } = require("node:child_process");
8
+ const { CANONICAL_APP_IDS } = require("./canonical-apps-list.js");
8
9
 
9
10
  const pluginRoot = path.resolve(__dirname, "..");
10
11
  const repoRoot = path.resolve(pluginRoot, "..", "..");
@@ -47,13 +48,10 @@ function readReleaseSource(relativePath) {
47
48
  // Read it from the released commit so the asserted-against version is itself
48
49
  // taken from HEAD, not a half-written working copy.
49
50
  const VERSION = JSON.parse(readReleaseSource("plugins/cool-workflow/package.json").text).version;
50
- const canonicalApps = [
51
- "architecture-review",
52
- "end-to-end-golden-path",
53
- "pr-review-fix-ci",
54
- "release-cut",
55
- "research-synthesis"
56
- ];
51
+ // Canonical app ids are DERIVED from apps/ (excluding metadata.example demos) by
52
+ // scripts/canonical-apps-list.js — the single source bump-version.js bumps and
53
+ // canonical-apps.js smoke-tests. No hand-copied list to drift (audit M5).
54
+ const canonicalApps = CANONICAL_APP_IDS;
57
55
 
58
56
  function main() {
59
57
  const checks = [];
@@ -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.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "CI Triage"
3
+ short_description: "Diagnose failing CI with evidence."
4
+ default_prompt: "Use $ci-triage to diagnose the failing CI run."
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  name: cool-workflow
3
- description: Use when the user asks for Cool Workflow, CW, agent workflow control-plane, TypeScript workflow orchestration, phased multi-agent work, background workflow tasks, or reusable workflow apps.
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