cool-workflow 0.2.2 → 0.2.3
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/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- 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/cli/dispatch.js +11 -5
- package/dist/cli/entry.js +41 -1
- package/dist/cli/io.js +6 -2
- package/dist/cli/parseargv.js +1 -0
- package/dist/core/capability-data.js +1 -0
- package/dist/core/format/completion.js +68 -0
- package/dist/core/format/help.js +25 -6
- package/dist/core/format/safe-json.js +73 -0
- package/dist/core/format/state-explosion-text.js +1 -1
- package/dist/core/multi-agent/candidate-scoring.js +5 -1
- package/dist/core/multi-agent/collaboration.js +3 -3
- package/dist/core/multi-agent/coordinator.js +5 -5
- package/dist/core/multi-agent/runtime.js +4 -4
- package/dist/core/multi-agent/topology.js +3 -3
- package/dist/core/pipeline/dispatch.js +18 -4
- package/dist/core/pipeline/drive-decide.js +2 -1
- package/dist/core/state/migrations.js +16 -1
- package/dist/core/state/state-explosion/digest.js +13 -13
- package/dist/core/state/state-explosion/graph.js +17 -11
- package/dist/core/state/state-explosion/report.js +6 -6
- package/dist/core/util/numeric-flag.js +40 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/server.js +99 -11
- package/dist/shell/audit-cli.js +57 -25
- package/dist/shell/coordinator-io.js +73 -13
- package/dist/shell/dispatch.js +1 -1
- package/dist/shell/doctor.js +80 -1
- package/dist/shell/drive.js +243 -49
- package/dist/shell/eval-text.js +2 -2
- package/dist/shell/evidence-reasoning.js +4 -4
- package/dist/shell/execution-backend/agent.js +20 -1
- package/dist/shell/execution-backend/container.js +4 -1
- package/dist/shell/execution-backend/local.js +4 -1
- package/dist/shell/feedback-cli.js +6 -6
- package/dist/shell/fs-atomic.js +218 -29
- package/dist/shell/man-cli.js +6 -0
- package/dist/shell/metrics-cli.js +2 -1
- package/dist/shell/multi-agent-cli.js +367 -323
- package/dist/shell/multi-agent-host.js +9 -9
- package/dist/shell/multi-agent-operator-ux.js +80 -38
- package/dist/shell/node-store.js +10 -4
- package/dist/shell/observability.js +1 -1
- package/dist/shell/operator-ux-text.js +22 -22
- package/dist/shell/operator-ux.js +15 -14
- package/dist/shell/orchestrator.js +49 -38
- package/dist/shell/pipeline-cli.js +87 -40
- package/dist/shell/reclamation-io.js +14 -4
- package/dist/shell/registry-cli.js +19 -17
- package/dist/shell/remote-source.js +13 -8
- package/dist/shell/report-cli.js +45 -0
- package/dist/shell/report.js +2 -1
- package/dist/shell/run-registry-io.js +77 -19
- package/dist/shell/run-store.js +74 -2
- package/dist/shell/scheduling-io.js +12 -13
- package/dist/shell/state-cli.js +2 -7
- package/dist/shell/state-explosion-cli.js +17 -9
- package/dist/shell/topology-io.js +36 -5
- package/dist/shell/trust-audit.js +224 -22
- package/dist/shell/trust-policy-io.js +1 -1
- package/dist/shell/worker-cli.js +35 -31
- package/dist/wiring/capability-table/basics.js +33 -8
- package/dist/wiring/capability-table/exec-backend.js +20 -11
- package/dist/wiring/capability-table/multi-agent.js +172 -165
- package/dist/wiring/capability-table/parity.js +1 -0
- package/dist/wiring/capability-table/pipeline.js +54 -42
- package/dist/wiring/capability-table/registry-core.js +29 -10
- package/dist/wiring/capability-table/reporting.js +137 -96
- package/dist/wiring/capability-table/scheduling-registry.js +195 -161
- package/dist/wiring/capability-table/state.js +36 -28
- package/dist/wiring/capability-table/trust-ledger.js +46 -22
- package/dist/wiring/capability-table/workflow-apps.js +83 -68
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/candidate-scoring.7.md +1 -1
- package/docs/canonical-workflow-apps.7.md +7 -7
- package/docs/cli-mcp-parity.7.md +8 -3
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/coordinator-blackboard.7.md +17 -17
- package/docs/dogfood-one-real-repo.7.md +11 -11
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/end-to-end-golden-path.7.md +14 -14
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/getting-started.md +37 -37
- package/docs/multi-agent-cli-mcp-surface.7.md +19 -17
- package/docs/multi-agent-eval-replay-harness.7.md +15 -13
- package/docs/multi-agent-operator-ux.7.md +21 -19
- package/docs/multi-agent-runtime-core.7.md +22 -22
- package/docs/multi-agent-topologies.7.md +6 -6
- package/docs/multi-agent-trust-policy-audit.7.md +11 -11
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/operator-ux.7.md +34 -34
- package/docs/pipeline-runner.7.md +4 -4
- package/docs/project-index.md +27 -4
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +3 -1
- package/docs/release-tooling.7.md +2 -0
- package/docs/routines.md +4 -4
- package/docs/run-registry-control-plane.7.md +21 -19
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/scheduled-tasks.md +14 -14
- package/docs/security-trust-hardening.7.md +15 -15
- package/docs/state-explosion-management.7.md +12 -10
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/unix-principles.md +3 -1
- package/docs/verifier-gated-commit.7.md +1 -1
- package/docs/web-desktop-workbench.7.md +2 -0
- package/docs/workflow-app-framework.7.md +13 -13
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +2 -2
- package/scripts/agents/claude-p-agent.js +2 -2
- package/scripts/block-unapproved-tag.sh +23 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +52 -2
- package/scripts/dogfood-release.js +1 -1
- package/scripts/fake-date-for-reproduction.js +44 -0
- package/scripts/golden-path.js +4 -4
- package/scripts/purity-baseline.json +3 -0
- package/scripts/release-flow.js +57 -1
- package/scripts/verdict-keygen.js +83 -0
- package/scripts/verify-bump-reproduction.sh +148 -0
- package/scripts/verify-verdict-signature.js +61 -0
- package/skills/cool-workflow/SKILL.md +9 -9
- package/skills/cool-workflow/references/commands.md +89 -88
- package/ui/workbench/app.css +37 -1
- package/ui/workbench/app.js +124 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cool-workflow",
|
|
3
3
|
"description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cool-workflow",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "A workflow control plane and run-time you are able to check: it sends out jobs in TypeScript, makes certain of work against facts before it goes through, puts state into fixed records, orders jobs by time, runs jobs again and again, gets a group of agents to do their parts together, and talks MCP. It gives the doing of the work to outside agents — it never runs the models itself.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "COOLWHITE LLC"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "architecture-review",
|
|
4
4
|
"title": "Architecture Review",
|
|
5
5
|
"summary": "Map a repository architecture, assess risks, verify important findings, and synthesize an evidence-backed verdict.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.3",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "architecture-review-fast",
|
|
4
4
|
"title": "Architecture Review Fast",
|
|
5
5
|
"summary": "Run a shorter architecture review with parallel map and assess phases for faster first results.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.3",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "pr-review-fix-ci",
|
|
4
4
|
"title": "PR Review Fix CI",
|
|
5
5
|
"summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.3",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "release-cut",
|
|
4
4
|
"title": "Release Cut",
|
|
5
5
|
"summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.3",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "research-synthesis",
|
|
4
4
|
"title": "Research Synthesis",
|
|
5
5
|
"summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.3",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
package/dist/cli/dispatch.js
CHANGED
|
@@ -54,8 +54,14 @@ function renderCliResult(result, jsonMode, options) {
|
|
|
54
54
|
/** Tries the capability table for `args`. Matches the row whose
|
|
55
55
|
* `cli.path` is `[command]` or `[command, positionals[0]]` (the only two
|
|
56
56
|
* path lengths any milestone-2 row uses); returns true when a table row
|
|
57
|
-
* handled the command.
|
|
58
|
-
|
|
57
|
+
* handled the command. `await`ing the handler's result is a no-op for
|
|
58
|
+
* the ~199 rows whose handler returns a plain CliHandlerResult (an
|
|
59
|
+
* `await` on a non-Promise value just resolves on the next microtask,
|
|
60
|
+
* invisible to any caller that itself awaits dispatch()) -- it only
|
|
61
|
+
* matters for the live drive rows, which return a real Promise so their
|
|
62
|
+
* round loop can actually stay interruptible (see shell/drive.ts's
|
|
63
|
+
* driveAsync). */
|
|
64
|
+
async function dispatchTable(args) {
|
|
59
65
|
const candidates = [[args.command]];
|
|
60
66
|
if (args.positionals.length > 0)
|
|
61
67
|
candidates.push([args.command, args.positionals[0]]);
|
|
@@ -68,7 +74,7 @@ function dispatchTable(args) {
|
|
|
68
74
|
positionals: args.positionals.slice(consumed),
|
|
69
75
|
options: args.options,
|
|
70
76
|
};
|
|
71
|
-
const result = row.cli.handler(cliArgs);
|
|
77
|
+
const result = await row.cli.handler(cliArgs);
|
|
72
78
|
renderCliResult(result, row.cli.jsonMode, args.options);
|
|
73
79
|
return true;
|
|
74
80
|
}
|
|
@@ -161,8 +167,8 @@ function dispatchLegacy(args) {
|
|
|
161
167
|
*
|
|
162
168
|
* Tries the capability table FIRST (real rows always win), then falls
|
|
163
169
|
* back to the milestone-1 legacy switch for verbs not yet migrated. */
|
|
164
|
-
function dispatch(args) {
|
|
165
|
-
if (dispatchTable(args))
|
|
170
|
+
async function dispatch(args) {
|
|
171
|
+
if (await dispatchTable(args))
|
|
166
172
|
return;
|
|
167
173
|
dispatchLegacy(args);
|
|
168
174
|
}
|
package/dist/cli/entry.js
CHANGED
|
@@ -33,6 +33,9 @@ const io_1 = require("./io");
|
|
|
33
33
|
* comes from core/format/help.ts either way. */
|
|
34
34
|
function printVersion() {
|
|
35
35
|
const row = (0, capability_table_1.findCapability)("version");
|
|
36
|
+
// `version`'s own handler is a pure, synchronous projection (never one of
|
|
37
|
+
// the live drive rows) -- safe to read `.text` straight off it rather than
|
|
38
|
+
// route through the generic (possibly-async) dispatch path.
|
|
36
39
|
const result = row?.cli?.handler({ positionals: [], options: {} });
|
|
37
40
|
process.stdout.write(result?.text ?? "");
|
|
38
41
|
}
|
|
@@ -88,6 +91,21 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
88
91
|
process.env.CW_NO_COLOR = "1";
|
|
89
92
|
if (args.options.full)
|
|
90
93
|
process.env.CW_OUTPUT = "full";
|
|
94
|
+
// --quiet is a documented CLI spelling of the existing CW_DRIVE_PROGRESS=0
|
|
95
|
+
// env var (shell/drive.ts's emitProgress) — it only silences the terse
|
|
96
|
+
// "[drive] " progress lines, not the end-of-run summary or any other
|
|
97
|
+
// Rule of Silence gate point (SPEC/reporting-ux.md's 3 gate points are
|
|
98
|
+
// each independent; --verbose/--full don't touch them either). It also
|
|
99
|
+
// does NOT reach two OTHER TTY-gated narration channels a user might
|
|
100
|
+
// reasonably expect it to quiet: live raw agent stderr streaming
|
|
101
|
+
// (shell/execution-backend/agent.ts's shouldStreamAgentStderr, gated by
|
|
102
|
+
// CW_AGENT_STREAM/CW_NO_STREAM/isTTY) and the local backend's
|
|
103
|
+
// "● Running…"/"✓ Done" lines (shell/execution-backend/local.ts, gated
|
|
104
|
+
// by isTTY with no env override at all). Folding those in is a larger,
|
|
105
|
+
// separate change — this flag's help text says "not agent output" on
|
|
106
|
+
// purpose so the scope is clear without reading this comment.
|
|
107
|
+
if (args.options.quiet)
|
|
108
|
+
process.env.CW_DRIVE_PROGRESS = "0";
|
|
91
109
|
// `cw <verb> --help` / `-h` -> per-command help
|
|
92
110
|
// (src/cli/command-surface.ts:80-83).
|
|
93
111
|
if ((args.options.help || args.options.h) && args.command && !args.command.startsWith("-")) {
|
|
@@ -103,13 +121,35 @@ async function runCli(argv = process.argv.slice(2)) {
|
|
|
103
121
|
else if (!args.command && typeof args.options.question === "string") {
|
|
104
122
|
args.command = "quickstart";
|
|
105
123
|
}
|
|
106
|
-
(0, dispatch_1.dispatch)(args);
|
|
124
|
+
await (0, dispatch_1.dispatch)(args);
|
|
125
|
+
}
|
|
126
|
+
/** Broken pipe (`cw ... --json | head`): when the reader at the other end
|
|
127
|
+
* of a pipe goes away early, a write to stdout gives an async 'error'
|
|
128
|
+
* event that no promise .catch can see — without a listener node comes
|
|
129
|
+
* down hard with a stack trace. Raw writes are all over the CLI (help,
|
|
130
|
+
* version, printJson, human text), so ONE process-level listener here
|
|
131
|
+
* covers every write point: EPIPE says "the reader has all it needs",
|
|
132
|
+
* so stop quietly with code 0. Any other stream error is thrown again
|
|
133
|
+
* and still comes up as an uncaughtException, same as before. */
|
|
134
|
+
function exitQuietOnEpipe(stream) {
|
|
135
|
+
stream.on("error", (error) => {
|
|
136
|
+
if (error && error.code === "EPIPE")
|
|
137
|
+
process.exit(0);
|
|
138
|
+
throw error;
|
|
139
|
+
});
|
|
107
140
|
}
|
|
108
141
|
/** Top-level run wrapper matching src/cli.ts's catch shape byte-for-byte:
|
|
109
142
|
* `cw: <message>\n` then, only when a hint matches, ` Try: <hint>\n` on
|
|
110
143
|
* stderr; `process.exitCode = 1` (never a hard `process.exit`). */
|
|
111
144
|
function main(argv = process.argv.slice(2)) {
|
|
145
|
+
exitQuietOnEpipe(process.stdout);
|
|
146
|
+
exitQuietOnEpipe(process.stderr);
|
|
112
147
|
return runCli(argv).catch((error) => {
|
|
148
|
+
// On some platforms a broken-pipe write throws EPIPE in the write call
|
|
149
|
+
// itself (not as a stream 'error' event) — same broken pipe, same quiet
|
|
150
|
+
// exit 0, never a `cw: write EPIPE` line.
|
|
151
|
+
if (error?.code === "EPIPE")
|
|
152
|
+
return;
|
|
113
153
|
const message = error instanceof Error ? error.message : String(error);
|
|
114
154
|
process.stderr.write(`${(0, term_1.bold)("cw:", process.stderr)} ${(0, term_1.red)(message, process.stderr)}\n`);
|
|
115
155
|
const hint = recoveryHint(message);
|
package/dist/cli/io.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.optionalArg = optionalArg;
|
|
|
16
16
|
exports.printJson = printJson;
|
|
17
17
|
exports.wantsJson = wantsJson;
|
|
18
18
|
const help_1 = require("../core/format/help");
|
|
19
|
+
const safe_json_1 = require("../core/format/safe-json");
|
|
19
20
|
const term_1 = require("../shell/term");
|
|
20
21
|
/** Bold ONLY the fixed "Cool Workflow" header line of `formatHelp()`'s
|
|
21
22
|
* plain text. */
|
|
@@ -34,9 +35,12 @@ function required(value, label) {
|
|
|
34
35
|
function optionalArg(value) {
|
|
35
36
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
36
37
|
}
|
|
37
|
-
/** Machine payload to stdout (stdout = data; never colored, never chrome).
|
|
38
|
+
/** Machine payload to stdout (stdout = data; never colored, never chrome).
|
|
39
|
+
* Byte-capped via safeJsonStringify — an aggregate result too large to be
|
|
40
|
+
* useful (or large enough to blow V8's string limit) prints a small
|
|
41
|
+
* overflow notice instead of hundreds of MB. */
|
|
38
42
|
function printJson(value) {
|
|
39
|
-
process.stdout.write(`${
|
|
43
|
+
process.stdout.write(`${(0, safe_json_1.safeJsonStringify)(value)}\n`);
|
|
40
44
|
}
|
|
41
45
|
/** True when the caller asked for JSON output (`--json` or `--format json`). */
|
|
42
46
|
function wantsJson(options) {
|
package/dist/cli/parseargv.js
CHANGED
|
@@ -116,6 +116,7 @@ exports.KNOWN_COMMANDS = new Set([
|
|
|
116
116
|
"topology", "summary", "blackboard", "coordinator", "metrics", "operator",
|
|
117
117
|
"sched", "gc", "telemetry", "migration", "demo", "workbench", "approve",
|
|
118
118
|
"reject", "comment", "handoff", "graph", "eval", "version", "fix",
|
|
119
|
+
"completion",
|
|
119
120
|
]);
|
|
120
121
|
/** Levenshtein edit distance between two strings. */
|
|
121
122
|
function levenshtein(a, b) {
|
|
@@ -153,6 +153,7 @@ exports.MCP_TOOL_DATA = [
|
|
|
153
153
|
{ tool: "cw_coordinator_decision", capability: "coordinator.decision", requiredArgs: ["runId"], properties: ["runId", "cwd", "id", "kind", "outcome", "reason", "subject", "evidence", "artifact", "message"], description: "Record a coordinator decision." },
|
|
154
154
|
{ tool: "cw_audit_summary", capability: "audit.summary", requiredArgs: ["runId"], properties: ["runId", "cwd"], description: "Read the trust/audit summary." },
|
|
155
155
|
{ tool: "cw_audit_verify", capability: "audit.verify", requiredArgs: ["runId"], properties: ["runId", "cwd", "expectHead", "expectCount"], description: "Re-prove a run's trust-audit hash chain (fail-closed exit)." },
|
|
156
|
+
{ tool: "cw_audit_repair", capability: "audit.repair", requiredArgs: ["runId"], properties: ["runId", "cwd", "write", "expectHead", "expectCount"], description: "Repair a torn trailing write in a run's trust-audit event log (dry-run unless write is set; fails closed if the corruption is not confined to the trailing line, or if expectHead/expectCount is given and not met)." },
|
|
156
157
|
{ tool: "cw_audit_worker", capability: "audit.worker", requiredArgs: ["runId"], properties: ["runId", "cwd", "workerId"], description: "Read trust/audit for one worker." },
|
|
157
158
|
{ tool: "cw_audit_provenance", capability: "audit.provenance", requiredArgs: ["runId"], properties: ["runId", "cwd", "workerId", "worker", "candidateId", "candidate", "commitId", "commit"], description: "Inspect evidence provenance." },
|
|
158
159
|
{ tool: "cw_audit_multi_agent", capability: "audit.multi-agent", requiredArgs: ["runId"], properties: ["runId", "cwd"], description: "Read the multi-agent trust/policy/provenance audit." },
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/format/completion.ts — formatCompletionScript.
|
|
3
|
+
//
|
|
4
|
+
// Pure function of already-loaded data (MORE_COMMANDS_TOKENS from
|
|
5
|
+
// help.ts). No new command list to hand-maintain: the completion word
|
|
6
|
+
// list is the SAME token set formatHelp()'s "More commands" section
|
|
7
|
+
// displays, plus the two top-level verbs that list doesn't carry
|
|
8
|
+
// ("help" and "doctor" — see help.ts's formatHelp comment on why "help"
|
|
9
|
+
// is absent from both places). Deliberately shallow: completes only
|
|
10
|
+
// TOP-LEVEL command names, not subcommands or flags — a v1 scope that
|
|
11
|
+
// still covers the single most common completion need (typing the verb)
|
|
12
|
+
// without hand-maintaining a second, deeper copy of the capability
|
|
13
|
+
// table's subcommand shape.
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.completionWords = completionWords;
|
|
16
|
+
exports.formatCompletionScript = formatCompletionScript;
|
|
17
|
+
const help_1 = require("./help");
|
|
18
|
+
/** The full top-level completion word list: `help`/`doctor` (absent from
|
|
19
|
+
* MORE_COMMANDS_TOKENS — see help.ts) plus every token formatHelp's
|
|
20
|
+
* "More commands" section lists (already includes `version`/`fix`). */
|
|
21
|
+
function completionWords() {
|
|
22
|
+
return ["help", "doctor", ...help_1.MORE_COMMANDS_TOKENS];
|
|
23
|
+
}
|
|
24
|
+
function bashScript(words) {
|
|
25
|
+
return [
|
|
26
|
+
"# cw bash completion — generated by `cw completion bash`.",
|
|
27
|
+
"# Install: eval \"$(cw completion bash)\" (e.g. in ~/.bashrc)",
|
|
28
|
+
"_cw_complete() {",
|
|
29
|
+
' local cur="${COMP_WORDS[COMP_CWORD]}"',
|
|
30
|
+
" if [ \"$COMP_CWORD\" -eq 1 ]; then",
|
|
31
|
+
` COMPREPLY=( $(compgen -W "${words.join(" ")}" -- "$cur") )`,
|
|
32
|
+
" fi",
|
|
33
|
+
"}",
|
|
34
|
+
"complete -F _cw_complete cw",
|
|
35
|
+
"",
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
function zshScript(words) {
|
|
39
|
+
return [
|
|
40
|
+
"#compdef cw",
|
|
41
|
+
"# cw zsh completion — generated by `cw completion zsh`.",
|
|
42
|
+
"# Install: cw completion zsh > \"${fpath[1]}/_cw\" (then restart the shell)",
|
|
43
|
+
`_arguments '1: :(${words.join(" ")})'`,
|
|
44
|
+
"",
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
function fishScript(words) {
|
|
48
|
+
return [
|
|
49
|
+
"# cw fish completion — generated by `cw completion fish`.",
|
|
50
|
+
"# Install: cw completion fish > ~/.config/fish/completions/cw.fish",
|
|
51
|
+
`complete -c cw -n "__fish_use_subcommand" -a "${words.join(" ")}"`,
|
|
52
|
+
"",
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
/** `cw completion <bash|zsh|fish>` — a static completion script for the
|
|
56
|
+
* top-level command word list. Throws on an unrecognized shell name
|
|
57
|
+
* (the CLI layer turns that into the standard `cw: <message>` /
|
|
58
|
+
* `Try:` shape, same as every other input-validation error). */
|
|
59
|
+
function formatCompletionScript(shell) {
|
|
60
|
+
const words = completionWords();
|
|
61
|
+
if (shell === "bash")
|
|
62
|
+
return bashScript(words);
|
|
63
|
+
if (shell === "zsh")
|
|
64
|
+
return zshScript(words);
|
|
65
|
+
if (shell === "fish")
|
|
66
|
+
return fishScript(words);
|
|
67
|
+
throw new Error(`Unknown shell: ${shell}\n Try: cw completion bash|zsh|fish`);
|
|
68
|
+
}
|
package/dist/core/format/help.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// `tools/list` round-trip read the SAME table rows for any capability
|
|
26
26
|
// wired into both.
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.MORE_COMMANDS_TOKENS = void 0;
|
|
28
29
|
exports.formatHelp = formatHelp;
|
|
29
30
|
exports.formatCommandHelp = formatCommandHelp;
|
|
30
31
|
exports.formatInfo = formatInfo;
|
|
@@ -34,8 +35,12 @@ const capability_table_1 = require("../capability-table");
|
|
|
34
35
|
* build's order (space-joined in the source, pipe-joined for display).
|
|
35
36
|
* One change from the old capture: `update` is gone. The verb had no code
|
|
36
37
|
* behind it in this build (`cw update` said "Unknown command"), so the
|
|
37
|
-
* help must not offer it. See parseargv.ts KNOWN_COMMANDS.
|
|
38
|
-
|
|
38
|
+
* help must not offer it. See parseargv.ts KNOWN_COMMANDS. Exported (only
|
|
39
|
+
* as a read-only source list, `formatHelp` below still owns the byte-
|
|
40
|
+
* pinned display format) so `core/format/completion.ts` can build a
|
|
41
|
+
* shell-completion word list from the SAME data instead of a third
|
|
42
|
+
* hand-maintained copy. */
|
|
43
|
+
exports.MORE_COMMANDS_TOKENS = [
|
|
39
44
|
"list", "search", "info", "init", "plan", "status", "next", "dispatch",
|
|
40
45
|
"result", "state", "commit", "report", "app", "sandbox", "backend",
|
|
41
46
|
"contract", "node", "feedback", "worker", "audit", "candidate", "review",
|
|
@@ -44,7 +49,7 @@ const MORE_COMMANDS_TOKENS = [
|
|
|
44
49
|
"summary", "blackboard", "coordinator", "metrics", "operator", "sched",
|
|
45
50
|
"gc", "telemetry", "migration", "demo", "workbench", "approve", "reject",
|
|
46
51
|
"comment", "handoff", "ledger", "graph", "eval", "man", "version",
|
|
47
|
-
"fix",
|
|
52
|
+
"fix", "completion",
|
|
48
53
|
];
|
|
49
54
|
const MORE_COMMANDS_WRAP_WIDTH = 76;
|
|
50
55
|
/** src/orchestrator.ts:940-951 — greedily pack pipe-joined tokens into
|
|
@@ -128,7 +133,14 @@ const COMMAND_HELP_ROWS = {
|
|
|
128
133
|
{ command: "cw sched complete", summary: "Complete a held lease (terminal success)." },
|
|
129
134
|
{ command: "cw sched reclaim", summary: "Reclaim expired leases (each counts a failed attempt)." },
|
|
130
135
|
{ command: "cw sched reset", summary: "Reset a parked entry to ready (operator recovery)." },
|
|
131
|
-
|
|
136
|
+
// Two rows for one command string, on purpose — SPEC/cli-probe.md's
|
|
137
|
+
// "Odd things a rebuild must copy or fix on purpose" item 5 names
|
|
138
|
+
// `sched policy` as one of the show/set pairs a rebuild's help
|
|
139
|
+
// printer must keep doubled; this single merged row was a regression
|
|
140
|
+
// that lost the doubling (docs/rebuild/SPEC/cli-help/sched.txt has
|
|
141
|
+
// both), restored here with the old ground truth's exact wording.
|
|
142
|
+
{ command: "cw sched policy", summary: "Show the scheduling policy (file or default)." },
|
|
143
|
+
{ command: "cw sched policy", summary: "Set scheduling policy fields (concurrency/attempts/backoff/TTL)." },
|
|
132
144
|
],
|
|
133
145
|
registry: [
|
|
134
146
|
{ command: "cw registry refresh", summary: "Recompute and persist the derived run registry index." },
|
|
@@ -211,9 +223,14 @@ const COMMAND_HELP_ROWS = {
|
|
|
211
223
|
* stdout, so NO_COLOR/non-TTY always wins); byte content matches the
|
|
212
224
|
* plain-text capture at SPEC/cli-help/_root.txt, but for the dead
|
|
213
225
|
* `update` lines, which were taken out on purpose (no code was behind
|
|
214
|
-
* the verb — see MORE_COMMANDS_TOKENS note above)
|
|
226
|
+
* the verb — see MORE_COMMANDS_TOKENS note above), for a `--json`
|
|
227
|
+
* Flags row, added because the old build's capture predates the ~68
|
|
228
|
+
* capability-table rows that now support `--json`/`--format json`
|
|
229
|
+
* (io.ts's wantsJson) — the flag existed but was never documented here —
|
|
230
|
+
* and for a `--quiet` Flags row, a new CLI spelling of the existing
|
|
231
|
+
* CW_DRIVE_PROGRESS=0 env var (see cli/entry.ts). */
|
|
215
232
|
function formatHelp() {
|
|
216
|
-
const moreCommandsLines = wrapPipeJoined(MORE_COMMANDS_TOKENS, MORE_COMMANDS_WRAP_WIDTH);
|
|
233
|
+
const moreCommandsLines = wrapPipeJoined(exports.MORE_COMMANDS_TOKENS, MORE_COMMANDS_WRAP_WIDTH);
|
|
217
234
|
const lines = [
|
|
218
235
|
"Cool Workflow",
|
|
219
236
|
"",
|
|
@@ -234,6 +251,8 @@ function formatHelp() {
|
|
|
234
251
|
" --verbose Show full agent narration live (default: compact)",
|
|
235
252
|
" --full Verbose, plus the report printed inline at the end",
|
|
236
253
|
" --no-color Disable ANSI color (also honors NO_COLOR / FORCE_COLOR)",
|
|
254
|
+
" --json Print JSON for commands that support it",
|
|
255
|
+
" --quiet Suppress [drive] progress lines (not agent output)",
|
|
237
256
|
"",
|
|
238
257
|
"More commands",
|
|
239
258
|
...moreCommandsLines,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/format/safe-json.ts — a byte-capped JSON.stringify shared by the CLI's
|
|
3
|
+
// `--json` output (cli/io.ts) and the MCP `tools/call` result (mcp/server.ts).
|
|
4
|
+
//
|
|
5
|
+
// Both surfaces JSON.stringify a caller-selected result with no size limit.
|
|
6
|
+
// An aggregate/dashboard capability (e.g. cw_workbench_view, cw comment
|
|
7
|
+
// list --json on a run with thousands of comments) can grow to hundreds of
|
|
8
|
+
// MB: a useless dump for a calling agent's context window at best, and at
|
|
9
|
+
// worst a RangeError ("Invalid string length") once the string crosses
|
|
10
|
+
// V8's per-string ceiling (~256M UTF-16 code units) — the same failure
|
|
11
|
+
// class PR #316 fixed for batch stdout reconciliation, here on the output
|
|
12
|
+
// side instead. Both `--json` and MCP callers already catch a thrown error
|
|
13
|
+
// and report it cleanly (cli/entry.ts's top-level catch; mcp/server.ts's
|
|
14
|
+
// tools/call try/catch), so this is not a crash fix so much as a "return
|
|
15
|
+
// something useful instead of a cryptic RangeError, or an unusable giant
|
|
16
|
+
// payload" fix.
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.MAX_JSON_OUTPUT_BYTES = void 0;
|
|
19
|
+
exports.safeJsonStringify = safeJsonStringify;
|
|
20
|
+
/** Generous but bounded: comfortably above any real cw result today, far
|
|
21
|
+
* below V8's ~256M-UTF-16-code-unit ceiling. */
|
|
22
|
+
exports.MAX_JSON_OUTPUT_BYTES = 10 * 1024 * 1024;
|
|
23
|
+
/** Overflow-notice `detail` (a caught error's message) is truncated to this
|
|
24
|
+
* many characters — the notice itself must stay small and predictable
|
|
25
|
+
* regardless of how large the underlying error's message was (e.g. an
|
|
26
|
+
* error wrapping a huge stored blackboard-message body), both so the
|
|
27
|
+
* notice can never itself approach any cap, and so JSON.stringify-ing the
|
|
28
|
+
* notice can never itself throw. */
|
|
29
|
+
const MAX_DETAIL_CHARS = 500;
|
|
30
|
+
/** JSON.stringify `value` the same way `JSON.stringify(value, null, 2)`
|
|
31
|
+
* always has. If that would exceed `maxBytes` (or throws — e.g. a
|
|
32
|
+
* RangeError from a value so large stringify itself blows V8's string
|
|
33
|
+
* ceiling), returns a small, valid JSON string describing the overflow
|
|
34
|
+
* instead of the real payload. Byte-identical to the un-capped form for
|
|
35
|
+
* every result under the cap — the common case is untouched.
|
|
36
|
+
*
|
|
37
|
+
* `JSON.stringify` returns the actual value `undefined` (not a string,
|
|
38
|
+
* despite its `string` return-type declaration) for `undefined`, a
|
|
39
|
+
* function, or a Symbol — never throws for those. That is passed through
|
|
40
|
+
* unchanged (matching the un-capped form's pre-existing behavior for this
|
|
41
|
+
* edge case: `content[0].text`/`printJson`'s `${text}` end up printing
|
|
42
|
+
* literally `undefined`) rather than measured as a byte length. */
|
|
43
|
+
function safeJsonStringify(value, maxBytes = exports.MAX_JSON_OUTPUT_BYTES) {
|
|
44
|
+
let text;
|
|
45
|
+
try {
|
|
46
|
+
text = JSON.stringify(value, null, 2);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return JSON.stringify(overflowPayload(undefined, maxBytes, error), null, 2);
|
|
50
|
+
}
|
|
51
|
+
if (typeof text !== "string")
|
|
52
|
+
return text;
|
|
53
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
54
|
+
if (bytes <= maxBytes)
|
|
55
|
+
return text;
|
|
56
|
+
return JSON.stringify(overflowPayload(bytes, maxBytes), null, 2);
|
|
57
|
+
}
|
|
58
|
+
function overflowPayload(bytes, maxBytes, error) {
|
|
59
|
+
const rawDetail = error instanceof Error ? error.message : error !== undefined ? String(error) : undefined;
|
|
60
|
+
const detail = rawDetail !== undefined
|
|
61
|
+
? rawDetail.length > MAX_DETAIL_CHARS
|
|
62
|
+
? `${rawDetail.slice(0, MAX_DETAIL_CHARS)}… (truncated)`
|
|
63
|
+
: rawDetail
|
|
64
|
+
: undefined;
|
|
65
|
+
return {
|
|
66
|
+
error: "result-too-large",
|
|
67
|
+
message: `This result is too large to return in full (${bytes !== undefined ? `${bytes} bytes` : "exceeds the size limit while serializing"}, ` +
|
|
68
|
+
`cap is ${maxBytes} bytes). Narrow the query (a --limit/--status/--since filter, or a more specific tool/verb) and try again.`,
|
|
69
|
+
capBytes: maxBytes,
|
|
70
|
+
...(bytes !== undefined ? { actualBytes: bytes } : {}),
|
|
71
|
+
...(detail ? { detail } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -156,7 +156,7 @@ function stateExplosionReportLines(report) {
|
|
|
156
156
|
lines.push(` - ${reason}`);
|
|
157
157
|
if (report.compactGraph.collapsedNodeCount > 0) {
|
|
158
158
|
lines.push(`- Graph compacted: ${report.compactGraph.collapsedNodeCount} nodes collapsed into ${report.compactGraph.syntheticNodes.length} summary nodes`);
|
|
159
|
-
lines.push(` - Use: \`
|
|
159
|
+
lines.push(` - Use: \`cw multi-agent graph ${report.runId} --view full --json\``);
|
|
160
160
|
}
|
|
161
161
|
if (report.hiddenSourceRecords.length) {
|
|
162
162
|
for (const hidden of report.hiddenSourceRecords) {
|
|
@@ -123,7 +123,11 @@ function detectTies(candidates) {
|
|
|
123
123
|
const groups = new Map();
|
|
124
124
|
for (const candidate of candidates) {
|
|
125
125
|
const key = String(candidate.normalized);
|
|
126
|
-
|
|
126
|
+
const group = groups.get(key);
|
|
127
|
+
if (group)
|
|
128
|
+
group.push(candidate.candidateId);
|
|
129
|
+
else
|
|
130
|
+
groups.set(key, [candidate.candidateId]);
|
|
127
131
|
}
|
|
128
132
|
return Array.from(groups.values()).filter((group) => group.length > 1);
|
|
129
133
|
}
|
|
@@ -415,16 +415,16 @@ function buildTimeline(state) {
|
|
|
415
415
|
function buildNextActions(runId, states, policy) {
|
|
416
416
|
const actions = [];
|
|
417
417
|
if (!policy) {
|
|
418
|
-
actions.push(`
|
|
418
|
+
actions.push(`cw review policy ${runId} --requiredApprovals 1 --authorizedRoles reviewer --appliesTo commit`);
|
|
419
419
|
return actions;
|
|
420
420
|
}
|
|
421
421
|
for (const state of states) {
|
|
422
422
|
if (state.status === "pending" || state.status === "blocked" || state.status === "unattributed") {
|
|
423
|
-
actions.push(`
|
|
423
|
+
actions.push(`cw approve ${state.target.kind} ${runId} ${state.target.id} --role <authorized-role> --actor <id> --attested`);
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
426
|
if (!actions.length)
|
|
427
|
-
actions.push(`
|
|
427
|
+
actions.push(`cw review status ${runId} --json`);
|
|
428
428
|
return actions;
|
|
429
429
|
}
|
|
430
430
|
/** Self ids for a candidate/selection target: its producing worker +
|
|
@@ -443,14 +443,14 @@ function summarizeBlackboard(runId, state, blackboardId, defaultIndexPath) {
|
|
|
443
443
|
}
|
|
444
444
|
function nextAction(runId, board, openQuestions, conflicts, artifacts) {
|
|
445
445
|
if (!board)
|
|
446
|
-
return `
|
|
446
|
+
return `cw blackboard topic create ${runId} --id <topic-id> --title "<title>"`;
|
|
447
447
|
if (conflicts.length)
|
|
448
|
-
return `
|
|
448
|
+
return `cw coordinator decision ${runId} --kind conflict-resolution --outcome accepted --subject ${conflicts[0].id} --reason "<reason>"`;
|
|
449
449
|
if (openQuestions.length)
|
|
450
|
-
return `
|
|
450
|
+
return `cw blackboard message post ${runId} --topic ${openQuestions[0].topicId} --body "<answer with evidence>"`;
|
|
451
451
|
if (!artifacts.length)
|
|
452
|
-
return `
|
|
453
|
-
return `
|
|
452
|
+
return `cw blackboard artifact add ${runId} --path <path> --kind <kind>`;
|
|
453
|
+
return `cw blackboard snapshot ${runId}`;
|
|
454
454
|
}
|
|
455
455
|
function listBlackboardMessages(state, options = {}) {
|
|
456
456
|
return state.messages
|
|
@@ -841,15 +841,15 @@ function summarizeMultiAgent(run) {
|
|
|
841
841
|
function nextMultiAgentAction(run, blockedReasons) {
|
|
842
842
|
const state = ensureMultiAgentState(run);
|
|
843
843
|
if (!state.runs.length)
|
|
844
|
-
return `
|
|
844
|
+
return `cw multi-agent run ${run.id} --id <multi-agent-run-id>`;
|
|
845
845
|
if (blockedReasons.length)
|
|
846
|
-
return `
|
|
846
|
+
return `cw multi-agent fanin ${run.id} --group <group-id> --fanout <fanout-id>`;
|
|
847
847
|
const running = state.memberships.find((membership) => membership.status === "running");
|
|
848
848
|
if (running?.workerId)
|
|
849
|
-
return `
|
|
849
|
+
return `cw worker manifest ${run.id} ${running.workerId}`;
|
|
850
850
|
const groupWithoutFanin = state.groups.find((group) => group.membershipIds.length && !group.faninIds.length);
|
|
851
851
|
if (groupWithoutFanin)
|
|
852
|
-
return `
|
|
852
|
+
return `cw multi-agent fanin ${run.id} --group ${groupWithoutFanin.id}`;
|
|
853
853
|
return undefined;
|
|
854
854
|
}
|
|
855
855
|
function buildMultiAgentGraph(run) {
|
|
@@ -244,9 +244,9 @@ function topologyRunId(definition, taskIds, runId, sequence, hash) {
|
|
|
244
244
|
}
|
|
245
245
|
function nextActionsFor(runId, topologyRunId, fanoutId) {
|
|
246
246
|
return [
|
|
247
|
-
`
|
|
248
|
-
`
|
|
249
|
-
`
|
|
247
|
+
`cw dispatch ${runId} --multi-agent-fanout ${fanoutId}`,
|
|
248
|
+
`cw multi-agent fanin ${runId} ${topologyRunId}-fanin --fanout ${fanoutId}`,
|
|
249
|
+
`cw topology summary ${runId}`,
|
|
250
250
|
];
|
|
251
251
|
}
|
|
252
252
|
function statusToNodeStatus(status) {
|
|
@@ -20,7 +20,14 @@ exports.formatDispatchId = formatDispatchId;
|
|
|
20
20
|
* everything after it (`null`). */
|
|
21
21
|
function firstRunnablePhase(run) {
|
|
22
22
|
for (const phase of run.phases) {
|
|
23
|
-
|
|
23
|
+
// A Set lookup, not `taskIds.includes()` re-scanned per task: this loop is
|
|
24
|
+
// O(phases x tasks), not O(phases x tasks x taskIds-per-phase) -- the
|
|
25
|
+
// latter degrades to O(tasks^2) since total taskIds across phases scales
|
|
26
|
+
// with total tasks, and this function is called several times per drive
|
|
27
|
+
// hop (once per hop directly, again inside selectDriveTask, again inside
|
|
28
|
+
// driveConcurrentRound), compounding to O(tasks^3) over a whole run.
|
|
29
|
+
const taskIds = new Set(phase.taskIds);
|
|
30
|
+
const phaseTasks = run.tasks.filter((task) => taskIds.has(task.id));
|
|
24
31
|
if (phaseTasks.some((task) => task.status === "running"))
|
|
25
32
|
return phase;
|
|
26
33
|
if (phaseTasks.some((task) => task.status === "pending"))
|
|
@@ -34,7 +41,8 @@ function firstRunnablePhase(run) {
|
|
|
34
41
|
* running when some task is running or completed, else pending. */
|
|
35
42
|
function updatePhaseStatuses(run) {
|
|
36
43
|
for (const phase of run.phases) {
|
|
37
|
-
const
|
|
44
|
+
const taskIds = new Set(phase.taskIds);
|
|
45
|
+
const phaseTasks = run.tasks.filter((task) => taskIds.has(task.id));
|
|
38
46
|
if (phaseTasks.length > 0 && phaseTasks.every((task) => task.status === "completed")) {
|
|
39
47
|
phase.status = "completed";
|
|
40
48
|
}
|
|
@@ -69,12 +77,18 @@ function formatDispatchTask(task) {
|
|
|
69
77
|
};
|
|
70
78
|
}
|
|
71
79
|
/** `nextDispatchTasks(run, limit?)` — pending tasks of the first runnable
|
|
72
|
-
* phase, capped, mapped through formatDispatchTask.
|
|
80
|
+
* phase, capped, mapped through formatDispatchTask. `??`, not `||`: an
|
|
81
|
+
* explicit `limit: 0` (or a configured `maxConcurrentAgents: 0`) means
|
|
82
|
+
* "dispatch nothing", not "no limit was given" — `0 || fallback` used to
|
|
83
|
+
* silently replace a real zero with the fallback. Negative numbers are
|
|
84
|
+
* clamped to 0 before reaching `.slice()`, which otherwise treats a
|
|
85
|
+
* negative end index as "drop that many from the end" instead of "cap at
|
|
86
|
+
* this many". */
|
|
73
87
|
function nextDispatchTasks(run, limit) {
|
|
74
88
|
const runnablePhase = firstRunnablePhase(run);
|
|
75
89
|
if (!runnablePhase)
|
|
76
90
|
return [];
|
|
77
|
-
const max =
|
|
91
|
+
const max = Math.max(0, Math.floor(limit ?? run.workflow.limits.maxConcurrentAgents ?? 4));
|
|
78
92
|
const runnableTaskIds = new Set(runnablePhase.taskIds);
|
|
79
93
|
return run.tasks
|
|
80
94
|
.filter((task) => task.status === "pending" && runnableTaskIds.has(task.id))
|
|
@@ -45,7 +45,8 @@ function selectDriveTask(run) {
|
|
|
45
45
|
const phase = (0, dispatch_1.firstRunnablePhase)(run);
|
|
46
46
|
if (!phase)
|
|
47
47
|
return undefined;
|
|
48
|
-
const
|
|
48
|
+
const taskIds = new Set(phase.taskIds);
|
|
49
|
+
const phaseTasks = run.tasks.filter((task) => taskIds.has(task.id));
|
|
49
50
|
return phaseTasks.find((task) => task.status === "running") || phaseTasks.find((task) => task.status === "pending");
|
|
50
51
|
}
|
|
51
52
|
function countCompleted(run) {
|