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,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();
@@ -22,6 +22,7 @@
22
22
  const { spawnSync } = require("node:child_process");
23
23
  const fs = require("node:fs");
24
24
  const path = require("node:path");
25
+ const { CANONICAL_APP_IDS } = require("./canonical-apps-list.js");
25
26
 
26
27
  const pluginRoot = path.resolve(__dirname, "..");
27
28
  const repoRoot = path.resolve(pluginRoot, "..", "..");
@@ -89,16 +90,10 @@ function main() {
89
90
 
90
91
  // 5. canonical apps app.json (top-level version only; never minVersion).
91
92
  // ONLY the canonical apps track the runtime version — workflow-app-framework-demo
92
- // is pinned (e.g. 0.1.0) and must NOT be bumped. This list mirrors the one
93
- // version-sync-check.js asserts.
94
- const CANONICAL_APPS = [
95
- "architecture-review",
96
- "end-to-end-golden-path",
97
- "pr-review-fix-ci",
98
- "release-cut",
99
- "research-synthesis"
100
- ];
101
- for (const appId of CANONICAL_APPS) {
93
+ // is pinned (e.g. 0.1.0) and must NOT be bumped. The list is DERIVED from
94
+ // apps/ (excluding metadata.example demos) by scripts/canonical-apps-list.js,
95
+ // the single source version-sync-check.js asserts against — no hand-copy.
96
+ for (const appId of CANONICAL_APP_IDS) {
102
97
  const appJson = path.join(pluginRoot, "apps", appId, "app.json");
103
98
  if (fs.existsSync(appJson) && replaceFirstVersionField(appJson, next)) {
104
99
  note(`apps/${appId}/app.json`);
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // Single source of truth for the CANONICAL app id list.
5
+ //
6
+ // Audit finding M5: this list was hand-copied into three scripts
7
+ // (bump-version.js, version-sync-check.js, canonical-apps.js) with no gate
8
+ // enforcing agreement, so drift between the copies was silent. This module
9
+ // DERIVES the list from the `apps/` directory on disk so the three callers can
10
+ // never disagree — there is nothing left to copy.
11
+ //
12
+ // What counts as canonical: every app directory under `apps/` whose `app.json`
13
+ // is NOT a demo. The real demo marker is `metadata.example === true` (that, NOT
14
+ // `versionPinned`, is how the only non-canonical app — workflow-app-framework-demo,
15
+ // pinned at 0.1.0 — is flagged). Example apps are excluded because they are
16
+ // version-pinned and must not be bumped or version-asserted with the runtime.
17
+ //
18
+ // Portability: node fs/path only, no external tools (CI portability rule).
19
+
20
+ const fs = require("node:fs");
21
+ const path = require("node:path");
22
+
23
+ const pluginRoot = path.resolve(__dirname, "..");
24
+ const appsDir = path.join(pluginRoot, "apps");
25
+
26
+ // The end-to-end golden path is canonical (and version-tracked) but is exercised
27
+ // by its own dedicated harness (scripts/golden-path.js), not by the per-app CLI
28
+ // smoke in canonical-apps.js. Expose its id so that script can express
29
+ // "canonical minus golden-path" without re-introducing a hand-copied list.
30
+ const GOLDEN_PATH_APP_ID = "end-to-end-golden-path";
31
+
32
+ function isExampleApp(appJsonPath) {
33
+ // An app is excluded from the canonical list iff its app.json declares
34
+ // metadata.example === true. Any read/parse failure is treated as
35
+ // "not an example" so a malformed app surfaces in the canonical list (and
36
+ // therefore in the version gate) rather than being silently dropped.
37
+ try {
38
+ const json = JSON.parse(fs.readFileSync(appJsonPath, "utf8"));
39
+ return json && json.metadata && json.metadata.example === true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ function listCanonicalAppIds() {
46
+ return fs
47
+ .readdirSync(appsDir, { withFileTypes: true })
48
+ .filter((entry) => entry.isDirectory())
49
+ .map((entry) => entry.name)
50
+ .filter((id) => {
51
+ const appJson = path.join(appsDir, id, "app.json");
52
+ if (!fs.existsSync(appJson)) return false; // not an app directory
53
+ return !isExampleApp(appJson);
54
+ })
55
+ .sort(); // deterministic order (replay determinism)
56
+ }
57
+
58
+ const CANONICAL_APP_IDS = listCanonicalAppIds();
59
+
60
+ module.exports = {
61
+ CANONICAL_APP_IDS,
62
+ listCanonicalAppIds,
63
+ GOLDEN_PATH_APP_ID
64
+ };
@@ -6,6 +6,7 @@ const { execFileSync } = require("node:child_process");
6
6
  const fs = require("node:fs");
7
7
  const os = require("node:os");
8
8
  const path = require("node:path");
9
+ const { CANONICAL_APP_IDS, GOLDEN_PATH_APP_ID } = require("./canonical-apps-list.js");
9
10
 
10
11
  const pluginRoot = path.resolve(__dirname, "..");
11
12
  const cli = path.join(pluginRoot, "scripts/cw.js");
@@ -25,6 +26,23 @@ const canonicalApps = [
25
26
  "Workflow App framework"
26
27
  ]
27
28
  },
29
+ {
30
+ id: "architecture-review-fast",
31
+ args: (workspace) => [
32
+ "--repo",
33
+ workspace,
34
+ "--question",
35
+ "Can a user get a fast architecture answer?",
36
+ "--invariant",
37
+ "Full architecture-review remains available",
38
+ "--focus",
39
+ "Runtime speed",
40
+ "--sourceContext",
41
+ "",
42
+ "--sourceContextDigest",
43
+ ""
44
+ ]
45
+ },
28
46
  {
29
47
  id: "pr-review-fix-ci",
30
48
  args: (workspace) => [
@@ -65,7 +83,7 @@ const canonicalApps = [
65
83
  "--source",
66
84
  "plugins/cool-workflow/docs/workflow-app-framework.7.md",
67
85
  "--scope",
68
- "Cool Workflow v0.1.79",
86
+ "Cool Workflow v0.1.81",
69
87
  "--freshness",
70
88
  "as of release preparation"
71
89
  ]
@@ -73,6 +91,20 @@ const canonicalApps = [
73
91
  ];
74
92
 
75
93
  function main() {
94
+ // Fail-closed drift gate (audit M5): the per-app CLI smoke below must cover
95
+ // exactly the DERIVED canonical set (apps/ minus metadata.example demos) less
96
+ // the golden-path app, which scripts/golden-path.js owns. If a new canonical
97
+ // app appears (or the demo marker flips) without smoke args here, this fails
98
+ // instead of silently skipping it — there is no second hand-copied list.
99
+ const expectedSmokeIds = CANONICAL_APP_IDS.filter((id) => id !== GOLDEN_PATH_APP_ID).sort();
100
+ const actualSmokeIds = canonicalApps.map((app) => app.id).sort();
101
+ assert.deepEqual(
102
+ actualSmokeIds,
103
+ expectedSmokeIds,
104
+ `canonical-apps smoke set drifted from derived canonical list (apps/ minus example demos, minus ${GOLDEN_PATH_APP_ID}): ` +
105
+ `expected ${JSON.stringify(expectedSmokeIds)}, got ${JSON.stringify(actualSmokeIds)}`
106
+ );
107
+
76
108
  const appList = runJson(["app", "list"]);
77
109
  const workflowList = runJson(["list"]);
78
110
  assertUniqueIds(appList, "app list");
@@ -85,14 +117,14 @@ function main() {
85
117
  assert.ok(summary, `${app.id} must appear in app list`);
86
118
  assert.equal(summary.sourceKind, "app-directory");
87
119
  assert.equal(summary.legacy, false);
88
- assert.equal(summary.version, "0.1.79");
120
+ assert.equal(summary.version, "0.1.81");
89
121
 
90
122
  const validation = runJson(["app", "validate", manifestPath]);
91
123
  assert.equal(validation.valid, true, `${app.id} manifest must validate`);
92
124
 
93
125
  const shown = runJson(["app", "show", app.id]);
94
126
  assert.equal(shown.app.id, app.id);
95
- assert.equal(shown.app.version, "0.1.79");
127
+ assert.equal(shown.app.version, "0.1.81");
96
128
  assert.ok(shown.app.metadata.canonical, `${app.id} must be marked canonical`);
97
129
  assert.ok(shown.app.sandboxProfiles.length > 0, `${app.id} must declare sandbox profiles`);
98
130
  assertTaskIdsUnique(shown);
@@ -103,7 +135,7 @@ function main() {
103
135
  const plan = runJson(["plan", app.id, ...app.args(workspace)]);
104
136
  const state = JSON.parse(fs.readFileSync(plan.statePath, "utf8"));
105
137
  assert.equal(state.workflow.app.id, app.id);
106
- assert.equal(state.workflow.app.version, "0.1.79");
138
+ assert.equal(state.workflow.app.version, "0.1.81");
107
139
  assert.equal(state.workflow.app.metadata.canonical, true);
108
140
  assert.ok(state.tasks.some((task) => task.requiresEvidence), `${app.id} plan must include evidence gates`);
109
141
  assert.ok(state.tasks.every((task) => task.sandboxProfileId), `${app.id} plan must include sandbox hints`);