cool-workflow 0.1.91 → 0.1.93
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 +111 -434
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/architecture-review-fast/workflow.js +15 -2
- 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/apps/research-synthesis/workflow.js +1 -1
- package/dist/capability-core.js +47 -0
- package/dist/capability-registry.js +2 -0
- package/dist/cli/command-surface.js +165 -1352
- package/dist/cli/format.js +56 -0
- package/dist/cli/handlers/audit.js +82 -0
- package/dist/cli/handlers/blackboard.js +81 -0
- package/dist/cli/handlers/candidate.js +40 -0
- package/dist/cli/handlers/clones.js +34 -0
- package/dist/cli/handlers/collaboration.js +61 -0
- package/dist/cli/handlers/eval.js +40 -0
- package/dist/cli/handlers/maintenance.js +107 -0
- package/dist/cli/handlers/multi-agent.js +165 -0
- package/dist/cli/handlers/node.js +41 -0
- package/dist/cli/handlers/operational.js +155 -0
- package/dist/cli/handlers/operator.js +146 -0
- package/dist/cli/handlers/registry.js +68 -0
- package/dist/cli/handlers/run.js +153 -0
- package/dist/cli/handlers/scheduling.js +126 -0
- package/dist/cli/handlers/workbench.js +41 -0
- package/dist/cli/handlers/worker.js +45 -0
- package/dist/cli/io.js +27 -0
- package/dist/cli/run-summary.js +45 -0
- package/dist/commit.js +0 -5
- package/dist/doctor.js +1 -0
- package/dist/execution-backend.js +0 -11
- package/dist/mcp/tool-call.js +2 -0
- package/dist/mcp/tool-definitions.js +8 -0
- package/dist/orchestrator/app-operations.js +205 -0
- package/dist/orchestrator/report.js +6 -1
- package/dist/orchestrator.js +41 -153
- package/dist/state-explosion.js +0 -7
- package/dist/term.js +0 -18
- package/dist/validation.js +0 -21
- package/dist/version.js +1 -1
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/canonical-workflow-apps.7.md +12 -0
- package/docs/cli-mcp-parity.7.md +18 -1
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/mcp-app-surface.7.md +12 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +15 -3
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +11 -0
- package/docs/run-registry-control-plane.7.md +26 -2
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +2 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +3 -1
- package/scripts/agents/agent-adapter-core.js +179 -28
- package/scripts/agents/claude-p-agent.js +24 -7
- package/scripts/agents/codex-agent.js +1 -1
- package/scripts/agents/gemini-agent.js +1 -1
- package/scripts/agents/opencode-agent.js +1 -1
- package/scripts/architecture-review-fast.js +19 -5
- package/scripts/bump-version.js +20 -1
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +9 -1
- package/scripts/release-check.js +5 -1
- package/scripts/sync-readme.js +123 -0
- package/scripts/version-sync-check.js +10 -1
|
@@ -34,7 +34,7 @@ const { spawn, spawnSync } = require("node:child_process");
|
|
|
34
34
|
// wrappers instead of carrying a private copy. A drifted inline copy (ASCII
|
|
35
35
|
// hyphens silently became em-dashes here) meant claude was sent a different
|
|
36
36
|
// instruction text than the other providers for the same contract.
|
|
37
|
-
const { buildPrompt, createRenderer } = require("./agent-adapter-core");
|
|
37
|
+
const { buildPrompt, createRenderer, toolLabel, summarizeToolResult } = require("./agent-adapter-core");
|
|
38
38
|
|
|
39
39
|
const inputPath = process.argv[2];
|
|
40
40
|
const resultPath = process.argv[3];
|
|
@@ -78,17 +78,17 @@ if (!streamEnabled) {
|
|
|
78
78
|
process.exit(0);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
// The shared `toolLabel` (from the core) renders `ToolName(basename)` — the Claude-tree label format
|
|
82
|
+
// every vendor uses, so the look can't drift between wrappers.
|
|
83
|
+
function pickInput(input) {
|
|
82
84
|
if (!input || typeof input !== "object") return "";
|
|
83
|
-
|
|
84
|
-
const s = String(v).replace(/\s+/g, " ").trim();
|
|
85
|
-
return s ? ` ${s.length > 80 ? s.slice(0, 77) + "…" : s}` : "";
|
|
85
|
+
return input.file_path || input.path || input.pattern || input.command || input.query || input.url || "";
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// The live view (spinner + folding actions on a TTY, plain append-only when piped, silent when
|
|
89
89
|
// CW_AGENT_STREAM=0) + cursor hygiene + an always-on-disk transcript live in the shared core.
|
|
90
90
|
void traceEnabled; // superseded by the renderer (which does its own TTY/stream gating)
|
|
91
|
-
const render = createRenderer({ env: process.env, stderr: process.stderr });
|
|
91
|
+
const render = createRenderer({ env: process.env, stderr: process.stderr, label: "claude" });
|
|
92
92
|
const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
|
|
93
93
|
|
|
94
94
|
// stream-json so claude emits incremental NDJSON events we render live. We CAPTURE claude's
|
|
@@ -128,6 +128,13 @@ child.stdout.on("data", (chunk) => {
|
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
const toolIds = new Map(); // tool_use id -> tool name, so a later tool_result can name its tool
|
|
132
|
+
function toolResultText(content) {
|
|
133
|
+
if (typeof content === "string") return content;
|
|
134
|
+
if (Array.isArray(content)) return content.map((b) => (b && typeof b.text === "string" ? b.text : "")).join("\n");
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
|
|
131
138
|
function renderEvent(ev) {
|
|
132
139
|
if (ev.type === "assistant" && ev.message) {
|
|
133
140
|
if (!model && typeof ev.message.model === "string") model = ev.message.model;
|
|
@@ -135,7 +142,17 @@ function renderEvent(ev) {
|
|
|
135
142
|
if (part.type === "text" && part.text && part.text.trim()) {
|
|
136
143
|
render.text(part.text.trim());
|
|
137
144
|
} else if (part.type === "tool_use") {
|
|
138
|
-
|
|
145
|
+
if (part.id) toolIds.set(part.id, part.name);
|
|
146
|
+
render.action(toolLabel(part.name, pickInput(part.input)), part.id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} else if (ev.type === "user" && ev.message) {
|
|
150
|
+
// claude runs the tools itself (headless -p) and echoes each result back as a tool_result block;
|
|
151
|
+
// summarize it into the `⎿` tree line for the tool it belongs to.
|
|
152
|
+
for (const part of ev.message.content || []) {
|
|
153
|
+
if (part && part.type === "tool_result") {
|
|
154
|
+
const name = toolIds.get(part.tool_use_id) || "";
|
|
155
|
+
render.result(summarizeToolResult(name, toolResultText(part.content), part.is_error), part.is_error, part.tool_use_id);
|
|
139
156
|
}
|
|
140
157
|
}
|
|
141
158
|
} else if (ev.type === "system" && ev.subtype === "post_turn_summary" && ev.status_detail) {
|
|
@@ -42,7 +42,7 @@ try {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const prompt = buildPrompt(inputPath);
|
|
45
|
-
const render = createRenderer({ env: process.env, stderr: process.stderr });
|
|
45
|
+
const render = createRenderer({ env: process.env, stderr: process.stderr, label: "codex" });
|
|
46
46
|
const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
|
|
47
47
|
const state = { provider: "codex", buffer: "", model: undefined, usage: undefined, renderer: render };
|
|
48
48
|
const capturedStdout = [];
|
|
@@ -33,7 +33,7 @@ if (!inputPath || !resultPath) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const prompt = buildPrompt(inputPath);
|
|
36
|
-
const render = createRenderer({ env: process.env, stderr: process.stderr });
|
|
36
|
+
const render = createRenderer({ env: process.env, stderr: process.stderr, label: "gemini" });
|
|
37
37
|
const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
|
|
38
38
|
const state = { provider: "gemini", buffer: "", model: undefined, usage: undefined, textFragments: [], finalResult: undefined, renderer: render };
|
|
39
39
|
let childStderr = "";
|
|
@@ -38,7 +38,7 @@ if (!inputPath || !resultPath) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const prompt = buildPrompt(inputPath);
|
|
41
|
-
const render = createRenderer({ env: process.env, stderr: process.stderr });
|
|
41
|
+
const render = createRenderer({ env: process.env, stderr: process.stderr, label: "opencode" });
|
|
42
42
|
const transcriptPath = path.join(path.dirname(resultPath), "transcript.md");
|
|
43
43
|
const state = { provider: "opencode", buffer: "", model: undefined, usage: undefined, textFragments: [], finalResult: undefined, renderer: render };
|
|
44
44
|
let childStderr = "";
|
|
@@ -38,13 +38,20 @@ function main() {
|
|
|
38
38
|
const fastModel = stringArg(args.fastModel || args["fast-model"]);
|
|
39
39
|
const strongModel = stringArg(args.strongModel || args["strong-model"]);
|
|
40
40
|
const modelEnv = modelPolicyEnv(fastModel, strongModel);
|
|
41
|
+
// Opt-in incremental overlay: scope the exported source context to files
|
|
42
|
+
// changed since REF (e.g. origin/main). The source-context export resolves
|
|
43
|
+
// the ref, keys its cache separately, and records the base. The fast review
|
|
44
|
+
// then runs over the narrower context; the scheduled full review stays a
|
|
45
|
+
// complete audit (see scheduleFullReview).
|
|
46
|
+
const changedFrom = stringArg(args.changedFrom || args["changed-from"]);
|
|
41
47
|
|
|
42
48
|
const contextExport = timed(() => exportSourceContext({
|
|
43
49
|
repo,
|
|
44
50
|
profile,
|
|
45
51
|
ref,
|
|
46
52
|
profileFile,
|
|
47
|
-
cacheDir
|
|
53
|
+
cacheDir,
|
|
54
|
+
changedFrom
|
|
48
55
|
}));
|
|
49
56
|
const contextText = contextExport.value;
|
|
50
57
|
assertNonEmptySourceContext(contextText, profile, repo);
|
|
@@ -87,7 +94,8 @@ function main() {
|
|
|
87
94
|
digest,
|
|
88
95
|
profile,
|
|
89
96
|
ref,
|
|
90
|
-
cacheDir
|
|
97
|
+
cacheDir,
|
|
98
|
+
...(changedFrom ? { changedFrom } : {})
|
|
91
99
|
};
|
|
92
100
|
const fullReviewScheduleRun = truthy(args.scheduleFull || args["schedule-full"])
|
|
93
101
|
? timed(() => scheduleFullReview(repo, question, args, fastReview, sourceContextMeta))
|
|
@@ -101,7 +109,7 @@ function main() {
|
|
|
101
109
|
fastReview,
|
|
102
110
|
...(fastModel || strongModel ? { modelPolicy: { ...(fastModel ? { fastModel } : {}), ...(strongModel ? { strongModel } : {}) } } : {}),
|
|
103
111
|
...(fullReviewSchedule ? { fullReviewSchedule } : {}),
|
|
104
|
-
...(includeMetrics ? { metrics: buildMetrics(started, contextText, contextExport.elapsedMs, fastReview, fastReviewRun.elapsedMs, fullReviewScheduleRun?.elapsedMs) } : {})
|
|
112
|
+
...(includeMetrics ? { metrics: buildMetrics(started, contextText, contextExport.elapsedMs, fastReview, fastReviewRun.elapsedMs, fullReviewScheduleRun?.elapsedMs, changedFrom) } : {})
|
|
105
113
|
});
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -119,6 +127,7 @@ function exportSourceContext(options) {
|
|
|
119
127
|
options.cacheDir
|
|
120
128
|
];
|
|
121
129
|
if (options.profileFile) argv.push("--profile-file", path.resolve(options.profileFile));
|
|
130
|
+
if (options.changedFrom) argv.push("--changed-from", options.changedFrom);
|
|
122
131
|
const result = spawnSync(node, argv, {
|
|
123
132
|
cwd: repoRoot,
|
|
124
133
|
encoding: "utf8",
|
|
@@ -194,6 +203,9 @@ function scheduleFullReview(repo, question, args, fastReview, sourceContextMeta)
|
|
|
194
203
|
fastReview?.reportPath ? `Fast review report: ${fastReview.reportPath}.` : "",
|
|
195
204
|
`Fast review status: ${fastReview?.status || "unknown"} (${fastReview?.completedWorkers || 0}/${fastReview?.plannedWorkers || 0} workers completed).`,
|
|
196
205
|
`Source context: ${sourceContextMeta.path} (${sourceContextMeta.digest}, profile ${sourceContextMeta.profile}, ref ${sourceContextMeta.ref}).`,
|
|
206
|
+
sourceContextMeta.changedFrom
|
|
207
|
+
? `Fast review used an incremental overlay (changed-from ${sourceContextMeta.changedFrom}); this full review must audit the complete source, not only the changed files.`
|
|
208
|
+
: "",
|
|
197
209
|
"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
210
|
].filter(Boolean).join(" ");
|
|
199
211
|
return runCwJson([
|
|
@@ -304,7 +316,7 @@ function timed(fn) {
|
|
|
304
316
|
return { value, elapsedMs: elapsedMs(started) };
|
|
305
317
|
}
|
|
306
318
|
|
|
307
|
-
function buildMetrics(started, contextText, sourceContextElapsedMs, fastReview, fastReviewElapsedMs, fullReviewScheduleElapsedMs) {
|
|
319
|
+
function buildMetrics(started, contextText, sourceContextElapsedMs, fastReview, fastReviewElapsedMs, fullReviewScheduleElapsedMs, changedFrom) {
|
|
308
320
|
const steps = Array.isArray(fastReview?.steps) ? fastReview.steps : [];
|
|
309
321
|
const handleKinds = countBy(steps.map((step) => step && step.handleKind).filter(Boolean));
|
|
310
322
|
const actions = countBy(steps.map((step) => step && step.action).filter(Boolean));
|
|
@@ -314,7 +326,8 @@ function buildMetrics(started, contextText, sourceContextElapsedMs, fastReview,
|
|
|
314
326
|
sourceContext: {
|
|
315
327
|
elapsedMs: sourceContextElapsedMs,
|
|
316
328
|
bytes: Buffer.byteLength(contextText, "utf8"),
|
|
317
|
-
digest: `sha256:${crypto.createHash("sha256").update(contextText, "utf8").digest("hex")}
|
|
329
|
+
digest: `sha256:${crypto.createHash("sha256").update(contextText, "utf8").digest("hex")}`,
|
|
330
|
+
...(changedFrom ? { changedFrom } : {})
|
|
318
331
|
},
|
|
319
332
|
fastReview: {
|
|
320
333
|
elapsedMs: fastReviewElapsedMs,
|
|
@@ -388,6 +401,7 @@ function usage(code) {
|
|
|
388
401
|
"",
|
|
389
402
|
"options:",
|
|
390
403
|
" --profile core --ref HEAD --profile-file PATH --cache-dir DIR --context-out PATH",
|
|
404
|
+
" --changed-from REF (incremental overlay: scope context to files changed since REF)",
|
|
391
405
|
" --fast-model MODEL --strong-model MODEL",
|
|
392
406
|
" --invariant TEXT --focus TEXT --preview --once",
|
|
393
407
|
" --schedule-full [--full-delay-minutes N]",
|
package/scripts/bump-version.js
CHANGED
|
@@ -143,6 +143,22 @@ function main() {
|
|
|
143
143
|
note(rel);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// 5c. Homebrew formula (repo root, not under pluginRoot). Structured surface:
|
|
147
|
+
// a git-tag formula with no sha256. Homebrew reads the version from the
|
|
148
|
+
// tag, so the ONLY literal that moves is `tag: "v<version>"` (an explicit
|
|
149
|
+
// `version` line would be redundant and brew audit rejects it). A targeted
|
|
150
|
+
// exact-string swap keeps the file byte-formatting intact;
|
|
151
|
+
// version-sync-check.js asserts the tag at HEAD, so it can never drift.
|
|
152
|
+
const formula = path.join(repoRoot, "Formula", "cool-workflow.rb");
|
|
153
|
+
if (fs.existsSync(formula)) {
|
|
154
|
+
const text = fs.readFileSync(formula, "utf8");
|
|
155
|
+
const updated = text.split(`tag: "v${current}"`).join(`tag: "v${next}"`);
|
|
156
|
+
if (updated !== text) {
|
|
157
|
+
fs.writeFileSync(formula, updated);
|
|
158
|
+
note("Formula/cool-workflow.rb");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
146
162
|
process.stdout.write(`bump:version ${current} -> ${next}\n`);
|
|
147
163
|
for (const rel of touched) process.stdout.write(` updated ${rel}\n`);
|
|
148
164
|
|
|
@@ -167,7 +183,10 @@ function contentSurfaceFiles(next) {
|
|
|
167
183
|
// All files the version-sync-check script validates for VERSION or vX.Y.Z.
|
|
168
184
|
// Keep in sync with scripts/version-sync-check.js.
|
|
169
185
|
return [
|
|
170
|
-
|
|
186
|
+
// The npm package README (plugins/cool-workflow/README.md) is GENERATED from the GitHub
|
|
187
|
+
// README.md by scripts/sync-readme.js and shows the version via the live npm/release shields
|
|
188
|
+
// badges, not a hand-maintained literal — so it is NOT a version-bump surface. After a bump,
|
|
189
|
+
// `npm run sync:readme` keeps it byte-identical to the GitHub README (guarded by readme:check).
|
|
171
190
|
{ path: "plugins/cool-workflow/docs/multi-agent-cli-mcp-surface.7.md", needle: next, desc: "multi-agent CLI/MCP surface doc" },
|
|
172
191
|
{ path: "plugins/cool-workflow/docs/multi-agent-operator-ux.7.md", needle: next, desc: "multi-agent operator UX doc" },
|
|
173
192
|
{ path: "plugins/cool-workflow/docs/multi-agent-eval-replay-harness.7.md", needle: next, desc: "multi-agent eval/replay doc" },
|
|
@@ -83,7 +83,7 @@ const canonicalApps = [
|
|
|
83
83
|
"--source",
|
|
84
84
|
"plugins/cool-workflow/docs/workflow-app-framework.7.md",
|
|
85
85
|
"--scope",
|
|
86
|
-
"Cool Workflow v0.1.
|
|
86
|
+
"Cool Workflow v0.1.93",
|
|
87
87
|
"--freshness",
|
|
88
88
|
"as of release preparation"
|
|
89
89
|
]
|
|
@@ -117,14 +117,14 @@ function main() {
|
|
|
117
117
|
assert.ok(summary, `${app.id} must appear in app list`);
|
|
118
118
|
assert.equal(summary.sourceKind, "app-directory");
|
|
119
119
|
assert.equal(summary.legacy, false);
|
|
120
|
-
assert.equal(summary.version, "0.1.
|
|
120
|
+
assert.equal(summary.version, "0.1.93");
|
|
121
121
|
|
|
122
122
|
const validation = runJson(["app", "validate", manifestPath]);
|
|
123
123
|
assert.equal(validation.valid, true, `${app.id} manifest must validate`);
|
|
124
124
|
|
|
125
125
|
const shown = runJson(["app", "show", app.id]);
|
|
126
126
|
assert.equal(shown.app.id, app.id);
|
|
127
|
-
assert.equal(shown.app.version, "0.1.
|
|
127
|
+
assert.equal(shown.app.version, "0.1.93");
|
|
128
128
|
assert.ok(shown.app.metadata.canonical, `${app.id} must be marked canonical`);
|
|
129
129
|
assert.ok(shown.app.sandboxProfiles.length > 0, `${app.id} must declare sandbox profiles`);
|
|
130
130
|
assertTaskIdsUnique(shown);
|
|
@@ -135,7 +135,7 @@ function main() {
|
|
|
135
135
|
const plan = runJson(["plan", app.id, ...app.args(workspace)]);
|
|
136
136
|
const state = JSON.parse(fs.readFileSync(plan.statePath, "utf8"));
|
|
137
137
|
assert.equal(state.workflow.app.id, app.id);
|
|
138
|
-
assert.equal(state.workflow.app.version, "0.1.
|
|
138
|
+
assert.equal(state.workflow.app.version, "0.1.93");
|
|
139
139
|
assert.equal(state.workflow.app.metadata.canonical, true);
|
|
140
140
|
assert.ok(state.tasks.some((task) => task.requiresEvidence), `${app.id} plan must include evidence gates`);
|
|
141
141
|
assert.ok(state.tasks.every((task) => task.sandboxProfileId), `${app.id} plan must include sandbox hints`);
|
|
@@ -6,7 +6,7 @@ const fs = require("node:fs");
|
|
|
6
6
|
const path = require("node:path");
|
|
7
7
|
const { CoolWorkflowRunner } = require("../dist/orchestrator.js");
|
|
8
8
|
|
|
9
|
-
const TARGET_VERSION = "0.1.
|
|
9
|
+
const TARGET_VERSION = "0.1.93";
|
|
10
10
|
const PREVIOUS_VERSION = "0.1.31";
|
|
11
11
|
const pluginRoot = path.resolve(__dirname, "..");
|
|
12
12
|
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.93");
|
|
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.93 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.93");
|
|
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\.93/);
|
|
199
199
|
assert.match(report, /## Candidates/);
|
|
200
200
|
assert.match(report, /## Trust Audit/);
|
|
201
201
|
assert.match(report, /## Acceptance Rationale/);
|
package/scripts/parity-check.js
CHANGED
|
@@ -69,7 +69,15 @@ function cliDispatchTokens() {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function cliDispatchSources() {
|
|
72
|
-
|
|
72
|
+
// The dispatcher + every per-command handler module carved out of it (the
|
|
73
|
+
// command-surface god-object decomposition). A verb's subcommand `case`s may
|
|
74
|
+
// live in dist/cli/handlers/<group>.js, so scan those too or their tokens read
|
|
75
|
+
// as "missing from dist/cli.js".
|
|
76
|
+
const handlersDir = path.join(pluginRoot, "dist", "cli", "handlers");
|
|
77
|
+
const handlerFiles = fs.existsSync(handlersDir)
|
|
78
|
+
? fs.readdirSync(handlersDir).filter((name) => name.endsWith(".js")).map((name) => path.join(handlersDir, name))
|
|
79
|
+
: [];
|
|
80
|
+
return [cli, path.join(pluginRoot, "dist", "cli", "command-surface.js"), ...handlerFiles].filter((file) => fs.existsSync(file));
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
function cliHelpTokens() {
|
package/scripts/release-check.js
CHANGED
|
@@ -74,7 +74,11 @@ const checks = [
|
|
|
74
74
|
// drifted from a fresh source scan. The teeth-on-the-gate live in
|
|
75
75
|
// test/project-index-sync-smoke.js (run by `npm test`); this entry gives the
|
|
76
76
|
// real-doc check its own visible PASS/FAIL line in the release summary.
|
|
77
|
-
{ name: "project index sync", command: ["npm", "run", "index:check"] }
|
|
77
|
+
{ name: "project index sync", command: ["npm", "run", "index:check"] },
|
|
78
|
+
// Fail closed if the npm README (plugins/cool-workflow/README.md) has drifted
|
|
79
|
+
// from the GitHub README.md it is generated from. Keeps the two pages identical
|
|
80
|
+
// by construction; teeth live in test/readme-sync-smoke.js (run by `npm test`).
|
|
81
|
+
{ name: "readme sync", command: ["npm", "run", "readme:check"] }
|
|
78
82
|
];
|
|
79
83
|
|
|
80
84
|
function main() {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// sync-readme — keep the npm package README in lockstep with the GitHub README.
|
|
5
|
+
//
|
|
6
|
+
// The repo-root README.md is the SINGLE SOURCE OF TRUTH. This generates the npm
|
|
7
|
+
// page (plugins/cool-workflow/README.md) from it, changing ONLY what npm needs to
|
|
8
|
+
// render: relative URLs become absolute, because npm cannot resolve a repo's
|
|
9
|
+
// relative `docs/assets/...` image paths or `](LICENSE)` links the way GitHub can.
|
|
10
|
+
//
|
|
11
|
+
// <img src="docs/assets/x.svg"> -> raw.githubusercontent.com/<owner>/<repo>/main/docs/assets/x.svg
|
|
12
|
+
// ](LICENSE) / ](plugins/...) -> github.com/<owner>/<repo>/blob/main/...
|
|
13
|
+
//
|
|
14
|
+
// The text content is otherwise identical, so the two pages can never drift.
|
|
15
|
+
//
|
|
16
|
+
// --check : regenerate in memory, compare to the committed npm README, write
|
|
17
|
+
// nothing, exit non-zero on drift (the release/CI gate). Mirrors
|
|
18
|
+
// sync-project-index.js. Fork-safe: the owner/repo in the absolute URLs
|
|
19
|
+
// is normalized out of the compare, so a fork clone never false-REDs.
|
|
20
|
+
|
|
21
|
+
const fs = require("node:fs");
|
|
22
|
+
const path = require("node:path");
|
|
23
|
+
const { spawnSync } = require("node:child_process");
|
|
24
|
+
|
|
25
|
+
const pluginRoot = path.resolve(__dirname, "..");
|
|
26
|
+
const repoRoot = path.resolve(pluginRoot, "..", "..");
|
|
27
|
+
const sourceReadme = path.join(repoRoot, "README.md");
|
|
28
|
+
// CW_README_PATH overrides the npm README that --check compares against (used by
|
|
29
|
+
// the smoke to point --check at a throwaway drifted fixture). Default: the real one.
|
|
30
|
+
const targetReadme = process.env.CW_README_PATH
|
|
31
|
+
? path.resolve(process.env.CW_README_PATH)
|
|
32
|
+
: path.join(pluginRoot, "README.md");
|
|
33
|
+
|
|
34
|
+
const repoUrl = normalizeGitRemote(git(["config", "--get", "remote.origin.url"]).trim())
|
|
35
|
+
|| "https://github.com/coo1white/cool-workflow";
|
|
36
|
+
const ownerRepo = repoUrl.replace(/^https:\/\/github\.com\//, "");
|
|
37
|
+
const RAW_BASE = `https://raw.githubusercontent.com/${ownerRepo}/main`;
|
|
38
|
+
const BLOB_BASE = `${repoUrl}/blob/main`;
|
|
39
|
+
|
|
40
|
+
const MARKER =
|
|
41
|
+
"<!-- AUTO-GENERATED from /README.md by scripts/sync-readme.js — edit the root README, " +
|
|
42
|
+
"then run `npm run sync:readme`. DO NOT edit this file directly. -->";
|
|
43
|
+
|
|
44
|
+
const CHECK = process.argv.includes("--check");
|
|
45
|
+
|
|
46
|
+
function render() {
|
|
47
|
+
if (!fs.existsSync(sourceReadme)) {
|
|
48
|
+
process.stderr.write(`sync-readme FAILED: source ${path.relative(repoRoot, sourceReadme)} does not exist.\n`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const source = fs.readFileSync(sourceReadme, "utf8");
|
|
52
|
+
// NOTE: a relative path must be free of a literal "(", ")", or '"' — the [^)]+ / [^"]+
|
|
53
|
+
// captures below stop at those. The repo README has none; readme:check catches any drift.
|
|
54
|
+
const absolutized = source
|
|
55
|
+
// HTML image sources: <img src="docs/assets/x.svg"> -> raw URL (skip already-absolute / anchors)
|
|
56
|
+
.replace(/\bsrc="(?!https?:|#|data:)([^"]+)"/g, (_m, rel) => `src="${RAW_BASE}/${rel}"`)
|
|
57
|
+
// Markdown links: ](LICENSE) / ](plugins/...) -> blob URL (skip absolute / in-page anchors / mailto)
|
|
58
|
+
.replace(/\]\((?!https?:|#|mailto:)([^)]+)\)/g, (_m, rel) => `](${BLOB_BASE}/${rel})`);
|
|
59
|
+
return `${MARKER}\n${absolutized}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeForCompare(text) {
|
|
63
|
+
// Strip the owner/repo from the generated absolute URLs so a fork (whose git
|
|
64
|
+
// remote differs) does not false-RED against the canonical committed README.
|
|
65
|
+
// The CONTENT is what this gate guards; the owner/repo is environment-derived.
|
|
66
|
+
return text
|
|
67
|
+
.replace(/\r\n/g, "\n")
|
|
68
|
+
.replace(/https:\/\/raw\.githubusercontent\.com\/[^/]+\/[^/]+\/main/g, "<RAW>")
|
|
69
|
+
.replace(/https:\/\/github\.com\/[^/]+\/[^/]+\/blob\/main/g, "<BLOB>");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function main() {
|
|
73
|
+
const rendered = render();
|
|
74
|
+
const rel = path.relative(repoRoot, targetReadme);
|
|
75
|
+
|
|
76
|
+
if (CHECK) {
|
|
77
|
+
if (!fs.existsSync(targetReadme)) {
|
|
78
|
+
process.stderr.write(`readme check FAILED: ${rel} does not exist.\nRun: (cd plugins/cool-workflow && npm run sync:readme)\n`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const committed = normalizeForCompare(fs.readFileSync(targetReadme, "utf8"));
|
|
82
|
+
const fresh = normalizeForCompare(rendered);
|
|
83
|
+
if (committed === fresh) {
|
|
84
|
+
process.stdout.write(`${JSON.stringify({ ok: true, check: true, source: "README.md", doc: rel }, null, 2)}\n`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const a = committed.split("\n");
|
|
88
|
+
const b = fresh.split("\n");
|
|
89
|
+
let i = 0;
|
|
90
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
91
|
+
process.stderr.write(
|
|
92
|
+
`readme check FAILED: ${rel} is stale (does not match the GitHub README.md).\n` +
|
|
93
|
+
`First difference at line ${i + 1}:\n` +
|
|
94
|
+
` committed: ${JSON.stringify(a[i] ?? "<missing>")}\n` +
|
|
95
|
+
` expected: ${JSON.stringify(b[i] ?? "<missing>")}\n` +
|
|
96
|
+
`Regenerate with: (cd plugins/cool-workflow && npm run sync:readme)\n`
|
|
97
|
+
);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fs.writeFileSync(targetReadme, rendered, "utf8");
|
|
102
|
+
process.stdout.write(`${JSON.stringify({
|
|
103
|
+
ok: true,
|
|
104
|
+
source: "README.md",
|
|
105
|
+
output: rel,
|
|
106
|
+
repoUrl
|
|
107
|
+
}, null, 2)}\n`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function git(args) {
|
|
111
|
+
const result = spawnSync("git", args, { cwd: repoRoot, encoding: "utf8" });
|
|
112
|
+
return result.status === 0 ? result.stdout : "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeGitRemote(remote) {
|
|
116
|
+
if (!remote) return "";
|
|
117
|
+
if (remote.endsWith(".git")) remote = remote.slice(0, -4);
|
|
118
|
+
const sshMatch = remote.match(/^git@github\.com:(.+)$/);
|
|
119
|
+
if (sshMatch) return `https://github.com/${sshMatch[1]}`;
|
|
120
|
+
return remote;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main();
|
|
@@ -92,7 +92,11 @@ function main() {
|
|
|
92
92
|
checkIncludes("plugins/cool-workflow/dist/mcp-server.js", "CURRENT_COOL_WORKFLOW_VERSION", checks);
|
|
93
93
|
checkIncludes("plugins/cool-workflow/dist/workflow-app-framework.js", "CURRENT_COOL_WORKFLOW_VERSION", checks);
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
// The npm package README (plugins/cool-workflow/README.md) is GENERATED from the GitHub
|
|
96
|
+
// README.md (scripts/sync-readme.js) and conveys the version via the live npm/release shields
|
|
97
|
+
// badges — not a hand-maintained literal — exactly like the GitHub page. It is no longer
|
|
98
|
+
// version-pinned here; readme:check guards that the two pages stay identical, and the surfaces
|
|
99
|
+
// above (version.ts/dist/package.json/manifests/docs) still pin the release.
|
|
96
100
|
checkIncludes("plugins/cool-workflow/docs/index.md", "release and migration", checks);
|
|
97
101
|
checkIncludes("plugins/cool-workflow/docs/multi-agent-topologies.7.md", "Multi-Agent Topologies", checks);
|
|
98
102
|
checkIncludes("plugins/cool-workflow/docs/multi-agent-cli-mcp-surface.7.md", VERSION, checks);
|
|
@@ -192,6 +196,11 @@ function main() {
|
|
|
192
196
|
checkIncludes("plugins/cool-workflow/docs/release-and-migration.7.md", VERSION, checks);
|
|
193
197
|
checkIncludes("CHANGELOG.md", `## ${VERSION}`, checks);
|
|
194
198
|
checkIncludes("RELEASE.md", VERSION, checks);
|
|
199
|
+
// Homebrew formula (repo root): git-tag formula. Homebrew scans the version
|
|
200
|
+
// from the tag, so the tag is the single version surface to gate (an explicit
|
|
201
|
+
// `version` line would be redundant and brew audit rejects it). bump-version.js
|
|
202
|
+
// moves this tag.
|
|
203
|
+
checkIncludes("Formula/cool-workflow.rb", `tag: "v${VERSION}"`, checks);
|
|
195
204
|
checkIncludes("plugins/cool-workflow/skills/cool-workflow/SKILL.md", "release:check", checks);
|
|
196
205
|
|
|
197
206
|
process.stdout.write(
|