cool-workflow 0.1.79 → 0.1.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +9 -1
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +64 -0
- package/apps/architecture-review-fast/workflow.js +153 -0
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/capability-core.js +38 -0
- package/dist/capability-registry.js +11 -8
- package/dist/cli.js +10 -1
- package/dist/drive.js +74 -1
- package/dist/evidence-reasoning.js +2 -2
- package/dist/execution-backend.js +6 -1
- package/dist/mcp-server.js +48 -13
- package/dist/orchestrator/lifecycle-operations.js +2 -1
- package/dist/orchestrator.js +1 -1
- package/dist/run-export.js +370 -25
- package/dist/run-registry.js +11 -4
- package/dist/state-explosion.js +100 -21
- package/dist/version.js +1 -1
- package/docs/agent-delegation-drive.7.md +58 -0
- package/docs/canonical-workflow-apps.7.md +37 -0
- package/docs/cli-mcp-parity.7.md +12 -0
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/index.md +1 -0
- package/docs/launch/demo.tape +28 -0
- package/docs/launch/launch-kit.md +59 -3
- package/docs/launch/pre-launch-checklist.md +53 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +13 -5
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/routines.md +23 -0
- package/docs/run-registry-control-plane.7.md +42 -1
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/source-context-profiles.7.md +119 -0
- package/docs/state-explosion-management.7.md +11 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/unix-principles.md +49 -1
- package/docs/web-desktop-workbench.7.md +4 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +142 -0
- package/package.json +2 -1
- package/scripts/agents/claude-p-agent.js +129 -43
- package/scripts/architecture-review-fast.js +362 -0
- package/scripts/bump-version.js +1 -0
- package/scripts/canonical-apps.js +21 -4
- package/scripts/coverage-gate.js +211 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/source-context.js +291 -0
- package/scripts/version-sync-check.js +1 -0
- package/skills/ci-triage/SKILL.md +50 -0
- package/skills/ci-triage/agents/openai.yaml +4 -0
- package/skills/cool-workflow/SKILL.md +4 -1
- package/skills/deploy-check/SKILL.md +55 -0
- package/skills/deploy-check/agents/openai.yaml +4 -0
- package/skills/design-qa/SKILL.md +49 -0
- package/skills/design-qa/agents/openai.yaml +4 -0
- package/skills/pr-review/SKILL.md +45 -0
- package/skills/pr-review/agents/openai.yaml +4 -0
|
@@ -5,27 +5,30 @@
|
|
|
5
5
|
//
|
|
6
6
|
// This is a CONFIG, NOT a CW dependency: CW spawns it out-of-process (argv-style,
|
|
7
7
|
// shell:false, cwd = the target repo) and records its attested output; the model
|
|
8
|
-
// runs in claude's process, never in CW.
|
|
9
|
-
// delegates here) so the documented onboarding path is portable (node-only repo
|
|
10
|
-
// convention, Windows included).
|
|
8
|
+
// runs in claude's process, never in CW.
|
|
11
9
|
//
|
|
12
10
|
// It fulfills ONE worker: read the worker's input.md ({{input}}), delegate the
|
|
13
11
|
// analysis to headless claude READ-ONLY, persist claude's final markdown to the
|
|
14
|
-
// worker's result.md ({{result}}), and forward claude's JSON
|
|
15
|
-
//
|
|
12
|
+
// worker's result.md ({{result}}), and forward claude's JSON on STDOUT so CW
|
|
13
|
+
// records the agent-reported provenance.
|
|
16
14
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
15
|
+
// LIVE OUTPUT (Unix discipline): default output is the legacy `--output-format
|
|
16
|
+
// json` contract, forwarded verbatim on stdout. Set CW_AGENT_STREAM=1 to opt in
|
|
17
|
+
// to claude `stream-json`; only then does this wrapper render a human-readable
|
|
18
|
+
// trace to stderr, and only when stderr is a TTY. Diagnostics stay off stdout,
|
|
19
|
+
// and vendor-specific stream parsing lives HERE in the wrapper (policy), not in
|
|
20
|
+
// CW's core (which only forwards, never parses).
|
|
21
21
|
//
|
|
22
|
-
//
|
|
22
|
+
// READ-ONLY by design: claude gets NO Write tool; the architecture-review app
|
|
23
|
+
// declares the `readonly` sandbox profile. This wrapper (the transport) writes
|
|
24
|
+
// the single result.md artifact itself, so the worker completes without granting
|
|
25
|
+
// the model file-write access.
|
|
26
|
+
//
|
|
27
|
+
// Point CW at it (from plugins/cool-workflow/), or use the `builtin:claude` alias:
|
|
23
28
|
// CW_AGENT_COMMAND="node $(pwd)/scripts/agents/claude-p-agent.js {{input}} {{result}}"
|
|
24
|
-
// or per-invocation:
|
|
25
|
-
// --agent-command "node $(pwd)/scripts/agents/claude-p-agent.js {{input}} {{result}}"
|
|
26
29
|
|
|
27
30
|
const fs = require("node:fs");
|
|
28
|
-
const { spawnSync } = require("node:child_process");
|
|
31
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
29
32
|
|
|
30
33
|
const inputPath = process.argv[2];
|
|
31
34
|
const resultPath = process.argv[3];
|
|
@@ -63,42 +66,125 @@ HARD RULES (the result is REJECTED otherwise):
|
|
|
63
66
|
- "classification", if present, MUST be one of: real, conditional, non-issue, unknown.
|
|
64
67
|
- Any finding with "severity" P0, P1, or P2 MUST include a NON-EMPTY "evidence" array.
|
|
65
68
|
- The top-level "evidence" array MUST be NON-EMPTY with REAL file:line locators from this repo.
|
|
66
|
-
- If you have no structured findings, use "findings": [] (empty) — never omit a finding's id
|
|
67
|
-
`;
|
|
69
|
+
- If you have no structured findings, use "findings": [] (empty) — never omit a finding's id.`;
|
|
68
70
|
|
|
69
71
|
const prompt = `${fs.readFileSync(inputPath, "utf8")}\n${CONTRACT}`;
|
|
72
|
+
const streamEnabled = process.env.CW_AGENT_STREAM === "1" && process.env.CW_NO_STREAM !== "1";
|
|
73
|
+
const traceEnabled = streamEnabled && Boolean(process.stderr.isTTY);
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
if (!streamEnabled) {
|
|
76
|
+
// Legacy default: --output-format json and verbatim stdout forwarding. This is
|
|
77
|
+
// the public wrapper contract existing users already scripted against.
|
|
78
|
+
const child = spawnSync("claude", ["-p", prompt, "--output-format", "json", "--allowedTools", "Read,Grep,Glob,Bash"], {
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
81
|
+
shell: false
|
|
82
|
+
});
|
|
83
|
+
if (child.error) {
|
|
84
|
+
process.stderr.write(`claude spawn failed: ${child.error.message}\n`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
if (child.status !== 0) {
|
|
88
|
+
process.stderr.write(String(child.stderr || `claude exited ${child.status}`));
|
|
89
|
+
process.exit(child.status === null ? 1 : child.status);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const out = String(child.stdout || "");
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(out);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
process.stderr.write(`claude output was not JSON: ${error.message}\n`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fs.writeFileSync(resultPath, String(parsed.result || ""), "utf8");
|
|
102
|
+
process.stdout.write(out);
|
|
103
|
+
process.exit(0);
|
|
83
104
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
|
|
106
|
+
// Live trace → stderr only. Concise; one line per meaningful event.
|
|
107
|
+
function trace(line) {
|
|
108
|
+
if (!traceEnabled) return;
|
|
109
|
+
process.stderr.write(`${line}\n`);
|
|
110
|
+
}
|
|
111
|
+
function shortInput(tool, input) {
|
|
112
|
+
if (!input || typeof input !== "object") return "";
|
|
113
|
+
const v = input.file_path || input.path || input.pattern || input.command || input.query || input.url || "";
|
|
114
|
+
const s = String(v).replace(/\s+/g, " ").trim();
|
|
115
|
+
return s ? ` ${s.length > 80 ? s.slice(0, 77) + "…" : s}` : "";
|
|
87
116
|
}
|
|
88
117
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
// stream-json so claude emits incremental NDJSON events we can render live, while
|
|
119
|
+
// we reconstruct the single {model, usage, result} object CW consumes on stdout.
|
|
120
|
+
const child = spawn(
|
|
121
|
+
"claude",
|
|
122
|
+
["-p", prompt, "--output-format", "stream-json", "--verbose", "--allowedTools", "Read,Grep,Glob,Bash"],
|
|
123
|
+
{ stdio: ["ignore", "pipe", "inherit"] } // claude's own stderr → straight through
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
let model;
|
|
127
|
+
let usage;
|
|
128
|
+
let resultText;
|
|
129
|
+
let buf = "";
|
|
130
|
+
|
|
131
|
+
trace("● claude: reading the repo (read-only)…");
|
|
132
|
+
|
|
133
|
+
child.stdout.setEncoding("utf8");
|
|
134
|
+
child.stdout.on("data", (chunk) => {
|
|
135
|
+
buf += chunk;
|
|
136
|
+
let nl;
|
|
137
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
138
|
+
const line = buf.slice(0, nl).trim();
|
|
139
|
+
buf = buf.slice(nl + 1);
|
|
140
|
+
if (!line) continue;
|
|
141
|
+
let ev;
|
|
142
|
+
try {
|
|
143
|
+
ev = JSON.parse(line);
|
|
144
|
+
} catch {
|
|
145
|
+
continue; // not a complete JSON line; ignore (defensive)
|
|
146
|
+
}
|
|
147
|
+
renderEvent(ev);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
function renderEvent(ev) {
|
|
152
|
+
if (ev.type === "assistant" && ev.message) {
|
|
153
|
+
if (!model && typeof ev.message.model === "string") model = ev.message.model;
|
|
154
|
+
for (const part of ev.message.content || []) {
|
|
155
|
+
if (part.type === "text" && part.text && part.text.trim()) {
|
|
156
|
+
trace(` ${part.text.trim().replace(/\n+/g, "\n ")}`);
|
|
157
|
+
} else if (part.type === "tool_use") {
|
|
158
|
+
trace(` → ${part.name}${shortInput(part.name, part.input)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else if (ev.type === "system" && ev.subtype === "post_turn_summary" && ev.status_detail) {
|
|
162
|
+
trace(` · ${ev.status_detail}`);
|
|
163
|
+
} else if (ev.type === "result") {
|
|
164
|
+
if (typeof ev.result === "string") resultText = ev.result;
|
|
165
|
+
if (ev.usage && typeof ev.usage === "object") usage = ev.usage;
|
|
166
|
+
if (ev.is_error) trace(" ✗ claude reported an error result");
|
|
167
|
+
}
|
|
97
168
|
}
|
|
98
169
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
170
|
+
child.on("error", (err) => {
|
|
171
|
+
process.stderr.write(`claude spawn failed: ${err.message}\n`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
|
102
174
|
|
|
103
|
-
|
|
104
|
-
|
|
175
|
+
child.on("close", (code) => {
|
|
176
|
+
if (code !== 0) {
|
|
177
|
+
process.stderr.write(`claude exited ${code === null ? "(timeout/killed)" : code}\n`);
|
|
178
|
+
process.exit(code === null ? 1 : code);
|
|
179
|
+
}
|
|
180
|
+
if (typeof resultText !== "string") {
|
|
181
|
+
// Fail closed: no result event ⇒ no result.md ⇒ CW records a failed hop.
|
|
182
|
+
process.stderr.write("claude produced no result event — refusing to fabricate a result\n");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
// Persist the AGENT's final markdown to the worker's result.md (CW is transport).
|
|
186
|
+
fs.writeFileSync(resultPath, resultText, "utf8");
|
|
187
|
+
trace("● done — result captured");
|
|
188
|
+
// The single JSON CW consumes on STDOUT (data channel): model + usage + result.
|
|
189
|
+
process.stdout.write(JSON.stringify({ model, usage, result: resultText }));
|
|
190
|
+
});
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// architecture-review-fast — userland accelerator wrapper.
|
|
5
|
+
//
|
|
6
|
+
// Mechanism only: prepare one cached JSONL source context, pass its digest into
|
|
7
|
+
// the opt-in fast app, then optionally create a background schedule for the full
|
|
8
|
+
// architecture-review app. The model still runs only through CW's external agent
|
|
9
|
+
// backend; this script imports no model SDK and holds no key.
|
|
10
|
+
|
|
11
|
+
const crypto = require("node:crypto");
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
const { spawnSync } = require("node:child_process");
|
|
15
|
+
|
|
16
|
+
const pluginRoot = path.resolve(__dirname, "..");
|
|
17
|
+
const repoRoot = path.resolve(pluginRoot, "..", "..");
|
|
18
|
+
const node = process.execPath;
|
|
19
|
+
const cw = path.join(pluginRoot, "scripts", "cw.js");
|
|
20
|
+
const sourceContext = path.join(pluginRoot, "scripts", "source-context.js");
|
|
21
|
+
|
|
22
|
+
function main() {
|
|
23
|
+
const started = nowNs();
|
|
24
|
+
const args = parseArgs(process.argv.slice(2));
|
|
25
|
+
if (args.help) return usage(0);
|
|
26
|
+
|
|
27
|
+
const repo = path.resolve(required(args.repo, "repo"));
|
|
28
|
+
const question = required(args.question, "question");
|
|
29
|
+
const requestedProfile = stringArg(args.profile);
|
|
30
|
+
const ref = stringArg(args.ref) || "HEAD";
|
|
31
|
+
const requestedProfileFile = stringArg(args.profileFile || args["profile-file"]);
|
|
32
|
+
const defaultExternalProfile = !requestedProfile && !requestedProfileFile && repo !== repoRoot;
|
|
33
|
+
const profile = defaultExternalProfile ? "repo" : requestedProfile || "core";
|
|
34
|
+
const cacheDir = path.resolve(stringArg(args.cacheDir || args["cache-dir"]) || path.join(repo, ".cw", "cache", "source-context"));
|
|
35
|
+
const contextOut = path.resolve(stringArg(args.contextOut || args["context-out"]) || path.join(repo, ".cw", "context", `${profile}-source.jsonl`));
|
|
36
|
+
const profileFile = defaultExternalProfile ? writeDefaultRepoProfile(repo, contextOut) : requestedProfileFile;
|
|
37
|
+
const includeMetrics = truthy(args.metrics);
|
|
38
|
+
const fastModel = stringArg(args.fastModel || args["fast-model"]);
|
|
39
|
+
const strongModel = stringArg(args.strongModel || args["strong-model"]);
|
|
40
|
+
const modelEnv = modelPolicyEnv(fastModel, strongModel);
|
|
41
|
+
|
|
42
|
+
const contextExport = timed(() => exportSourceContext({
|
|
43
|
+
repo,
|
|
44
|
+
profile,
|
|
45
|
+
ref,
|
|
46
|
+
profileFile,
|
|
47
|
+
cacheDir
|
|
48
|
+
}));
|
|
49
|
+
const contextText = contextExport.value;
|
|
50
|
+
assertNonEmptySourceContext(contextText, profile, repo);
|
|
51
|
+
fs.mkdirSync(path.dirname(contextOut), { recursive: true });
|
|
52
|
+
fs.writeFileSync(contextOut, contextText, "utf8");
|
|
53
|
+
const digest = `sha256:${crypto.createHash("sha256").update(contextText, "utf8").digest("hex")}`;
|
|
54
|
+
|
|
55
|
+
const reviewArgs = [
|
|
56
|
+
"quickstart",
|
|
57
|
+
"architecture-review-fast",
|
|
58
|
+
"--repo",
|
|
59
|
+
repo,
|
|
60
|
+
"--question",
|
|
61
|
+
question,
|
|
62
|
+
"--sourceContext",
|
|
63
|
+
contextOut,
|
|
64
|
+
"--sourceContextDigest",
|
|
65
|
+
digest
|
|
66
|
+
];
|
|
67
|
+
appendRepeated(reviewArgs, "--invariant", args.invariant);
|
|
68
|
+
appendOption(reviewArgs, "--focus", args.focus);
|
|
69
|
+
appendPassThrough(reviewArgs, args, [
|
|
70
|
+
"agent-command",
|
|
71
|
+
"agentCommand",
|
|
72
|
+
"agent-endpoint",
|
|
73
|
+
"agentEndpoint",
|
|
74
|
+
"agent-model",
|
|
75
|
+
"agentModel",
|
|
76
|
+
"agent-timeout-ms",
|
|
77
|
+
"agentTimeoutMs",
|
|
78
|
+
"once",
|
|
79
|
+
"preview",
|
|
80
|
+
"now"
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const fastReviewRun = timed(() => runCwJson(reviewArgs, repo, modelEnv));
|
|
84
|
+
const fastReview = fastReviewRun.value;
|
|
85
|
+
const sourceContextMeta = {
|
|
86
|
+
path: contextOut,
|
|
87
|
+
digest,
|
|
88
|
+
profile,
|
|
89
|
+
ref,
|
|
90
|
+
cacheDir
|
|
91
|
+
};
|
|
92
|
+
const fullReviewScheduleRun = truthy(args.scheduleFull || args["schedule-full"])
|
|
93
|
+
? timed(() => scheduleFullReview(repo, question, args, fastReview, sourceContextMeta))
|
|
94
|
+
: undefined;
|
|
95
|
+
const fullReviewSchedule = fullReviewScheduleRun?.value;
|
|
96
|
+
|
|
97
|
+
writeJson({
|
|
98
|
+
schemaVersion: 1,
|
|
99
|
+
appId: "architecture-review-fast",
|
|
100
|
+
sourceContext: sourceContextMeta,
|
|
101
|
+
fastReview,
|
|
102
|
+
...(fastModel || strongModel ? { modelPolicy: { ...(fastModel ? { fastModel } : {}), ...(strongModel ? { strongModel } : {}) } } : {}),
|
|
103
|
+
...(fullReviewSchedule ? { fullReviewSchedule } : {}),
|
|
104
|
+
...(includeMetrics ? { metrics: buildMetrics(started, contextText, contextExport.elapsedMs, fastReview, fastReviewRun.elapsedMs, fullReviewScheduleRun?.elapsedMs) } : {})
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function exportSourceContext(options) {
|
|
109
|
+
const argv = [
|
|
110
|
+
sourceContext,
|
|
111
|
+
"export",
|
|
112
|
+
"--profile",
|
|
113
|
+
options.profile,
|
|
114
|
+
"--ref",
|
|
115
|
+
options.ref,
|
|
116
|
+
"--repo-root",
|
|
117
|
+
options.repo,
|
|
118
|
+
"--cache-dir",
|
|
119
|
+
options.cacheDir
|
|
120
|
+
];
|
|
121
|
+
if (options.profileFile) argv.push("--profile-file", path.resolve(options.profileFile));
|
|
122
|
+
const result = spawnSync(node, argv, {
|
|
123
|
+
cwd: repoRoot,
|
|
124
|
+
encoding: "utf8",
|
|
125
|
+
maxBuffer: 1024 * 1024 * 128
|
|
126
|
+
});
|
|
127
|
+
if (result.status !== 0) die(result.stderr || result.stdout || "source context export failed");
|
|
128
|
+
return result.stdout;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeDefaultRepoProfile(repo, contextOut) {
|
|
132
|
+
const file = path.join(path.dirname(contextOut), "repo-source-profile.json");
|
|
133
|
+
const profile = {
|
|
134
|
+
schemaVersion: 1,
|
|
135
|
+
profiles: {
|
|
136
|
+
repo: {
|
|
137
|
+
description: "Default external repository profile for architecture-review-fast.",
|
|
138
|
+
maxLines: 50000,
|
|
139
|
+
include: [
|
|
140
|
+
"README.md",
|
|
141
|
+
"README.*",
|
|
142
|
+
"package.json",
|
|
143
|
+
"tsconfig.json",
|
|
144
|
+
"pyproject.toml",
|
|
145
|
+
"Cargo.toml",
|
|
146
|
+
"go.mod",
|
|
147
|
+
".github/**",
|
|
148
|
+
"src/**",
|
|
149
|
+
"lib/**",
|
|
150
|
+
"app/**",
|
|
151
|
+
"apps/**",
|
|
152
|
+
"bin/**",
|
|
153
|
+
"scripts/**",
|
|
154
|
+
"docs/**",
|
|
155
|
+
"test/**",
|
|
156
|
+
"tests/**"
|
|
157
|
+
],
|
|
158
|
+
exclude: [
|
|
159
|
+
".cw/**",
|
|
160
|
+
"dist/**",
|
|
161
|
+
"build/**",
|
|
162
|
+
"coverage/**",
|
|
163
|
+
"node_modules/**",
|
|
164
|
+
"vendor/**",
|
|
165
|
+
"target/**",
|
|
166
|
+
"docs/assets/**",
|
|
167
|
+
"assets/**",
|
|
168
|
+
"public/**"
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
174
|
+
fs.writeFileSync(file, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
|
|
175
|
+
return file;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function assertNonEmptySourceContext(text, profile, repo) {
|
|
179
|
+
const records = text.trim() ? text.trim().split(/\n/).filter(Boolean).length : 0;
|
|
180
|
+
if (records > 0) return;
|
|
181
|
+
die([
|
|
182
|
+
`source context profile "${profile}" exported zero records for ${repo}`,
|
|
183
|
+
"pass --profile-file with include rules for this repository, or choose a profile that matches tracked text files"
|
|
184
|
+
].join("; "));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function scheduleFullReview(repo, question, args, fastReview, sourceContextMeta) {
|
|
188
|
+
const delayMinutes = stringArg(args.fullDelayMinutes || args["full-delay-minutes"]) || "1";
|
|
189
|
+
const prompt = [
|
|
190
|
+
`Run full architecture-review for ${repo}.`,
|
|
191
|
+
`Question: ${question}`,
|
|
192
|
+
args.focus ? `Focus: ${args.focus}` : "",
|
|
193
|
+
`Fast review run: ${fastReview?.runId || "unknown"}.`,
|
|
194
|
+
fastReview?.reportPath ? `Fast review report: ${fastReview.reportPath}.` : "",
|
|
195
|
+
`Fast review status: ${fastReview?.status || "unknown"} (${fastReview?.completedWorkers || 0}/${fastReview?.plannedWorkers || 0} workers completed).`,
|
|
196
|
+
`Source context: ${sourceContextMeta.path} (${sourceContextMeta.digest}, profile ${sourceContextMeta.profile}, ref ${sourceContextMeta.ref}).`,
|
|
197
|
+
"Use the completed architecture-review-fast report as the foreground triage result; write the full review report path and digest when the background review finishes."
|
|
198
|
+
].filter(Boolean).join(" ");
|
|
199
|
+
return runCwJson([
|
|
200
|
+
"schedule",
|
|
201
|
+
"create",
|
|
202
|
+
"--cwd",
|
|
203
|
+
repo,
|
|
204
|
+
"--kind",
|
|
205
|
+
"reminder",
|
|
206
|
+
"--delayMinutes",
|
|
207
|
+
delayMinutes,
|
|
208
|
+
"--maxRuns",
|
|
209
|
+
"1",
|
|
210
|
+
"--workflowId",
|
|
211
|
+
"architecture-review",
|
|
212
|
+
"--prompt",
|
|
213
|
+
prompt
|
|
214
|
+
], repo);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function runCwJson(args, cwd, extraEnv = {}) {
|
|
218
|
+
const result = spawnSync(node, [cw, ...args], {
|
|
219
|
+
cwd,
|
|
220
|
+
encoding: "utf8",
|
|
221
|
+
env: { ...process.env, ...extraEnv },
|
|
222
|
+
maxBuffer: 1024 * 1024 * 64
|
|
223
|
+
});
|
|
224
|
+
if (result.status !== 0) die(result.stderr || result.stdout || `cw ${args.join(" ")} failed`);
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(result.stdout);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
die(`cw returned non-JSON output: ${error.message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseArgs(argv) {
|
|
233
|
+
const args = {};
|
|
234
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
235
|
+
const token = argv[index];
|
|
236
|
+
if (!token.startsWith("--")) {
|
|
237
|
+
(args._ ||= []).push(token);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const raw = token.slice(2);
|
|
241
|
+
const eq = raw.indexOf("=");
|
|
242
|
+
const key = eq >= 0 ? raw.slice(0, eq) : raw;
|
|
243
|
+
const value = eq >= 0 ? raw.slice(eq + 1) : argv[index + 1] && !argv[index + 1].startsWith("--") ? argv[++index] : true;
|
|
244
|
+
if (args[key] === undefined) args[key] = value;
|
|
245
|
+
else if (Array.isArray(args[key])) args[key].push(value);
|
|
246
|
+
else args[key] = [args[key], value];
|
|
247
|
+
}
|
|
248
|
+
return args;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function appendPassThrough(argv, args, keys) {
|
|
252
|
+
for (const key of keys) {
|
|
253
|
+
if (args[key] === undefined) continue;
|
|
254
|
+
const flag = key.includes("-") ? `--${key}` : `--${key}`;
|
|
255
|
+
if (args[key] === true) argv.push(flag);
|
|
256
|
+
else appendRepeated(argv, flag, args[key]);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function appendRepeated(argv, flag, value) {
|
|
261
|
+
if (value === undefined || value === false) return;
|
|
262
|
+
const values = Array.isArray(value) ? value : [value];
|
|
263
|
+
for (const entry of values) argv.push(flag, String(entry));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function appendOption(argv, flag, value) {
|
|
267
|
+
if (value === undefined || value === false || value === true || value === "") return;
|
|
268
|
+
argv.push(flag, String(value));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function required(value, name) {
|
|
272
|
+
const text = stringArg(value);
|
|
273
|
+
if (!text) die(`missing required --${name}`);
|
|
274
|
+
return text;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function stringArg(value) {
|
|
278
|
+
if (value === undefined || value === null || value === true || value === false) return "";
|
|
279
|
+
return Array.isArray(value) ? String(value[value.length - 1] || "") : String(value);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function truthy(value) {
|
|
283
|
+
return value === true || value === "true" || value === "1" || value === "yes";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function modelPolicyEnv(fastModel, strongModel) {
|
|
287
|
+
return {
|
|
288
|
+
...(fastModel ? { CW_ARCHITECTURE_REVIEW_FAST_MODEL: fastModel } : {}),
|
|
289
|
+
...(strongModel ? { CW_ARCHITECTURE_REVIEW_STRONG_MODEL: strongModel } : {})
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function nowNs() {
|
|
294
|
+
return process.hrtime.bigint();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function elapsedMs(started) {
|
|
298
|
+
return Number((process.hrtime.bigint() - started) / 1000000n);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function timed(fn) {
|
|
302
|
+
const started = nowNs();
|
|
303
|
+
const value = fn();
|
|
304
|
+
return { value, elapsedMs: elapsedMs(started) };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function buildMetrics(started, contextText, sourceContextElapsedMs, fastReview, fastReviewElapsedMs, fullReviewScheduleElapsedMs) {
|
|
308
|
+
const steps = Array.isArray(fastReview?.steps) ? fastReview.steps : [];
|
|
309
|
+
const handleKinds = countBy(steps.map((step) => step && step.handleKind).filter(Boolean));
|
|
310
|
+
const actions = countBy(steps.map((step) => step && step.action).filter(Boolean));
|
|
311
|
+
return {
|
|
312
|
+
totalElapsedMs: elapsedMs(started),
|
|
313
|
+
sourceContext: {
|
|
314
|
+
elapsedMs: sourceContextElapsedMs,
|
|
315
|
+
bytes: Buffer.byteLength(contextText, "utf8")
|
|
316
|
+
},
|
|
317
|
+
fastReview: {
|
|
318
|
+
elapsedMs: fastReviewElapsedMs,
|
|
319
|
+
status: fastReview?.status,
|
|
320
|
+
plannedWorkers: fastReview?.plannedWorkers,
|
|
321
|
+
completedWorkers: fastReview?.completedWorkers,
|
|
322
|
+
steps: steps.length,
|
|
323
|
+
actions,
|
|
324
|
+
handleKinds,
|
|
325
|
+
resultCacheHits: Number(handleKinds["result-cache"] || 0),
|
|
326
|
+
agentSpawns: steps.filter((step) => step && step.backendId === "agent" && step.handleKind && step.handleKind !== "result-cache").length
|
|
327
|
+
},
|
|
328
|
+
...(fullReviewScheduleElapsedMs === undefined ? {} : { fullReviewSchedule: { elapsedMs: fullReviewScheduleElapsedMs } })
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function countBy(values) {
|
|
333
|
+
const counts = {};
|
|
334
|
+
for (const value of values) counts[String(value)] = (counts[String(value)] || 0) + 1;
|
|
335
|
+
return counts;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function writeJson(value) {
|
|
339
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function usage(code) {
|
|
343
|
+
process.stderr.write([
|
|
344
|
+
"usage:",
|
|
345
|
+
" node scripts/architecture-review-fast.js --repo PATH --question TEXT [--agent-command CMD]",
|
|
346
|
+
"",
|
|
347
|
+
"options:",
|
|
348
|
+
" --profile core --ref HEAD --profile-file PATH --cache-dir DIR --context-out PATH",
|
|
349
|
+
" --fast-model MODEL --strong-model MODEL",
|
|
350
|
+
" --invariant TEXT --focus TEXT --preview --once",
|
|
351
|
+
" --schedule-full [--full-delay-minutes N]",
|
|
352
|
+
" --metrics"
|
|
353
|
+
].join("\n") + "\n");
|
|
354
|
+
process.exitCode = code;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function die(message) {
|
|
358
|
+
process.stderr.write(`architecture-review-fast: ${String(message).trim()}\n`);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
main();
|
package/scripts/bump-version.js
CHANGED
|
@@ -25,6 +25,23 @@ const canonicalApps = [
|
|
|
25
25
|
"Workflow App framework"
|
|
26
26
|
]
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
id: "architecture-review-fast",
|
|
30
|
+
args: (workspace) => [
|
|
31
|
+
"--repo",
|
|
32
|
+
workspace,
|
|
33
|
+
"--question",
|
|
34
|
+
"Can a user get a fast architecture answer?",
|
|
35
|
+
"--invariant",
|
|
36
|
+
"Full architecture-review remains available",
|
|
37
|
+
"--focus",
|
|
38
|
+
"Runtime speed",
|
|
39
|
+
"--sourceContext",
|
|
40
|
+
"",
|
|
41
|
+
"--sourceContextDigest",
|
|
42
|
+
""
|
|
43
|
+
]
|
|
44
|
+
},
|
|
28
45
|
{
|
|
29
46
|
id: "pr-review-fix-ci",
|
|
30
47
|
args: (workspace) => [
|
|
@@ -65,7 +82,7 @@ const canonicalApps = [
|
|
|
65
82
|
"--source",
|
|
66
83
|
"plugins/cool-workflow/docs/workflow-app-framework.7.md",
|
|
67
84
|
"--scope",
|
|
68
|
-
"Cool Workflow v0.1.
|
|
85
|
+
"Cool Workflow v0.1.80",
|
|
69
86
|
"--freshness",
|
|
70
87
|
"as of release preparation"
|
|
71
88
|
]
|
|
@@ -85,14 +102,14 @@ function main() {
|
|
|
85
102
|
assert.ok(summary, `${app.id} must appear in app list`);
|
|
86
103
|
assert.equal(summary.sourceKind, "app-directory");
|
|
87
104
|
assert.equal(summary.legacy, false);
|
|
88
|
-
assert.equal(summary.version, "0.1.
|
|
105
|
+
assert.equal(summary.version, "0.1.80");
|
|
89
106
|
|
|
90
107
|
const validation = runJson(["app", "validate", manifestPath]);
|
|
91
108
|
assert.equal(validation.valid, true, `${app.id} manifest must validate`);
|
|
92
109
|
|
|
93
110
|
const shown = runJson(["app", "show", app.id]);
|
|
94
111
|
assert.equal(shown.app.id, app.id);
|
|
95
|
-
assert.equal(shown.app.version, "0.1.
|
|
112
|
+
assert.equal(shown.app.version, "0.1.80");
|
|
96
113
|
assert.ok(shown.app.metadata.canonical, `${app.id} must be marked canonical`);
|
|
97
114
|
assert.ok(shown.app.sandboxProfiles.length > 0, `${app.id} must declare sandbox profiles`);
|
|
98
115
|
assertTaskIdsUnique(shown);
|
|
@@ -103,7 +120,7 @@ function main() {
|
|
|
103
120
|
const plan = runJson(["plan", app.id, ...app.args(workspace)]);
|
|
104
121
|
const state = JSON.parse(fs.readFileSync(plan.statePath, "utf8"));
|
|
105
122
|
assert.equal(state.workflow.app.id, app.id);
|
|
106
|
-
assert.equal(state.workflow.app.version, "0.1.
|
|
123
|
+
assert.equal(state.workflow.app.version, "0.1.80");
|
|
107
124
|
assert.equal(state.workflow.app.metadata.canonical, true);
|
|
108
125
|
assert.ok(state.tasks.some((task) => task.requiresEvidence), `${app.id} plan must include evidence gates`);
|
|
109
126
|
assert.ok(state.tasks.every((task) => task.sandboxProfileId), `${app.id} plan must include sandbox hints`);
|