cool-workflow 0.2.4 → 0.2.5
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/.gemini-plugin/mcp.json +10 -0
- package/.gemini-plugin/plugin.json +40 -0
- package/.opencode-plugin/mcp.json +10 -0
- package/.opencode-plugin/plugin.json +40 -0
- package/README.md +202 -48
- 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 +32 -4
- package/dist/cli/entry.js +11 -19
- package/dist/cli/global-flags.js +113 -0
- package/dist/cli/parseargv.js +7 -1
- package/dist/core/capability-data.js +337 -8
- package/dist/core/format/help.js +73 -3
- package/dist/core/format/recovery-hint.js +32 -0
- package/dist/core/multi-agent/collaboration.js +35 -6
- package/dist/core/multi-agent/runtime.js +7 -0
- package/dist/core/multi-agent/trust-policy.js +7 -1
- package/dist/core/pipeline/contract.js +7 -0
- package/dist/core/pipeline/error-feedback.js +2 -2
- package/dist/core/trust/evidence-grounding.js +13 -1
- package/dist/core/util/cli-args.js +22 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/dispatch.js +22 -2
- package/dist/mcp/server.js +124 -13
- package/dist/mcp-server.js +20 -0
- package/dist/shell/commit.js +8 -2
- package/dist/shell/coordinator-io.js +73 -1
- package/dist/shell/drive.js +120 -63
- package/dist/shell/error-feedback-io.js +6 -0
- package/dist/shell/execution-backend/agent.js +195 -23
- package/dist/shell/execution-backend/container.js +44 -10
- package/dist/shell/execution-backend/local.js +32 -0
- package/dist/shell/fs-atomic.js +93 -12
- package/dist/shell/ledger-cli.js +9 -2
- package/dist/shell/multi-agent-cli.js +5 -1
- package/dist/shell/onramp.js +48 -5
- package/dist/shell/pipeline.js +2 -1
- package/dist/shell/reclamation-io.js +10 -9
- package/dist/shell/run-export.js +52 -4
- package/dist/shell/run-store.js +156 -0
- package/dist/shell/scheduler-io.js +101 -10
- package/dist/shell/telemetry-ledger-io.js +36 -24
- package/dist/shell/trust-audit.js +104 -10
- package/dist/shell/workbench-host.js +121 -10
- package/dist/shell/workbench.js +79 -5
- package/dist/shell/worker-isolation.js +1 -1
- package/dist/wiring/capability-table/basics.js +5 -0
- package/dist/wiring/capability-table/exec-backend.js +40 -22
- package/dist/wiring/capability-table/pipeline.js +32 -0
- package/dist/wiring/capability-table/registry-core.js +26 -3
- package/dist/wiring/capability-table/reporting.js +7 -1
- package/dist/wiring/capability-table/scheduling-registry.js +8 -2
- package/dist/wiring/capability-table/trust-ledger.js +54 -12
- package/docs/agent-delegation-drive.7.md +15 -0
- package/docs/cli-mcp-parity.7.md +25 -0
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +36 -0
- package/docs/cross-agent-ledger.7.md +20 -8
- 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/fix.7.md +4 -4
- package/docs/getting-started.md +40 -32
- package/docs/index.md +17 -0
- package/docs/launch/demo.tape +4 -3
- package/docs/mcp-app-surface.7.md +6 -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 +39 -9
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +2 -0
- package/docs/routine.7.md +22 -0
- package/docs/run-registry-control-plane.7.md +2 -0
- package/docs/run-retention-reclamation.7.md +10 -4
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +24 -2
- package/docs/workflow-app-framework.7.md +31 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +4 -3
- package/package.json +4 -2
- package/scripts/agents/agent-adapter-core.js +26 -0
- package/scripts/agents/claude-p-agent.js +4 -1
- package/scripts/agents/codex-agent.js +4 -0
- package/scripts/agents/cw-attest-wrap.js +1 -1
- package/scripts/agents/gemini-agent.js +4 -0
- package/scripts/agents/opencode-agent.js +5 -0
- package/scripts/block-unapproved-tag.js +160 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +72 -4
- package/scripts/children/http-batch-delegate-child.js +132 -0
- package/scripts/children/http-delegate-child.js +8 -0
- package/scripts/dogfood-release.js +2 -2
- package/scripts/fake-date-for-reproduction.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/lang-policy-check.js +82 -0
- package/scripts/purity-gate.js +16 -3
- package/scripts/release-check.js +2 -1
- package/scripts/release-flow.js +128 -30
- package/scripts/release-gate.js +184 -0
- package/scripts/release-oneclick.js +38 -7
- package/scripts/release-tags.js +47 -0
- package/scripts/verdict-keygen.js +2 -2
- package/scripts/verify-bump-reproduction.js +193 -0
- package/scripts/verify-verdict-signature.js +1 -1
- package/ui/workbench/app.css +41 -9
- package/ui/workbench/app.js +160 -27
- package/ui/workbench/index.html +2 -2
- package/docs/agent-framework.md +0 -177
- package/docs/designs/handoff-ledger.md +0 -145
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
- package/docs/launch/launch-kit.md +0 -195
- package/docs/launch/pre-launch-checklist.md +0 -53
- package/docs/readme-v0.1.87-full.md +0 -301
- package/docs/routines.md +0 -101
- package/docs/scheduled-tasks.md +0 -80
- package/scripts/agents/claude-p-agent.sh +0 -9
- package/scripts/block-unapproved-tag.sh +0 -75
- package/scripts/release-gate.sh +0 -94
- package/scripts/verify-bump-reproduction.sh +0 -148
- package/scripts/verify-container-selfref.js +0 -64
package/dist/core/format/help.js
CHANGED
|
@@ -30,6 +30,7 @@ exports.formatHelp = formatHelp;
|
|
|
30
30
|
exports.formatCommandHelp = formatCommandHelp;
|
|
31
31
|
exports.formatInfo = formatInfo;
|
|
32
32
|
exports.formatSearchResults = formatSearchResults;
|
|
33
|
+
exports.formatWorkflowList = formatWorkflowList;
|
|
33
34
|
const capability_table_1 = require("../capability-table");
|
|
34
35
|
/** src/orchestrator.ts:934-951 — the "More commands" token set, in the old
|
|
35
36
|
* build's order (space-joined in the source, pipe-joined for display).
|
|
@@ -252,6 +253,7 @@ function formatHelp() {
|
|
|
252
253
|
" -claude Use Claude agent",
|
|
253
254
|
" -codex Use Codex agent",
|
|
254
255
|
" -gemini Use Gemini (via opencode)",
|
|
256
|
+
" -opencode Use OpenCode agent",
|
|
255
257
|
" -deepseek Use DeepSeek (via opencode)",
|
|
256
258
|
" --verbose Show full agent narration live (default: compact)",
|
|
257
259
|
" --full Verbose, plus the report printed inline at the end",
|
|
@@ -276,16 +278,44 @@ function formatHelp() {
|
|
|
276
278
|
function cliCommandHelpRows(verb) {
|
|
277
279
|
return (0, capability_table_1.cliCapabilities)()
|
|
278
280
|
.filter((row) => row.cli.path[0] === verb && !row.cli.hiddenFromHelp)
|
|
279
|
-
.map((row) => ({
|
|
281
|
+
.map((row) => ({
|
|
282
|
+
command: `cw ${(row.cli.helpPath ?? row.cli.path).join(" ")}`,
|
|
283
|
+
summary: row.summary,
|
|
284
|
+
...(row.cli.flags ? { flags: row.cli.flags } : {}),
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
/** A verb that is only a `caseTokens` alias of another verb's row (e.g.
|
|
288
|
+
* `audit-run` -> `quickstart`) has NO row whose `cli.path[0]` matches it,
|
|
289
|
+
* so `cliCommandHelpRows` above finds nothing for it. This looks the
|
|
290
|
+
* alias up the same way the dispatcher does (registry-core.ts's
|
|
291
|
+
* findCapabilityByCliPath alias branch: a row whose `caseTokens` holds
|
|
292
|
+
* the token) and returns the verb the alias dispatches to. */
|
|
293
|
+
function aliasTargetVerb(verb) {
|
|
294
|
+
for (const row of (0, capability_table_1.cliCapabilities)()) {
|
|
295
|
+
if (row.cli.caseTokens && row.cli.caseTokens.includes(verb) && row.cli.path[0] !== verb) {
|
|
296
|
+
return row.cli.path[0];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
280
300
|
}
|
|
281
301
|
/** src/orchestrator.ts:988-1007 — `formatCommandHelp(verb)`. Unknown verb
|
|
282
302
|
* gives a SOFT text (never a throw); known verb lists its registry rows,
|
|
283
303
|
* sorted by command string, padded to the longest command column + 2
|
|
284
304
|
* (capped at 40, per the old registry's own cap; none of the milestone-1
|
|
285
|
-
* fixture rows exceed it).
|
|
305
|
+
* fixture rows exceed it). An alias verb (a `caseTokens` token like
|
|
306
|
+
* `audit-run`) gets an alias header line plus its target verb's rows —
|
|
307
|
+
* before this fix it fell into the unknown-verb text and, worse, the
|
|
308
|
+
* Did-you-mean line suggested the very verb the user just typed. */
|
|
286
309
|
function formatCommandHelp(verb, suggestCommand) {
|
|
287
310
|
const rows = [...(COMMAND_HELP_ROWS[verb] ?? []), ...cliCommandHelpRows(verb)];
|
|
288
311
|
if (rows.length === 0) {
|
|
312
|
+
const target = aliasTargetVerb(verb);
|
|
313
|
+
if (target) {
|
|
314
|
+
const targetRows = [...(COMMAND_HELP_ROWS[target] ?? []), ...cliCommandHelpRows(target)];
|
|
315
|
+
if (targetRows.length > 0) {
|
|
316
|
+
return renderCommandHelpRows(`cw ${verb} — alias of cw ${target}`, targetRows);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
289
319
|
const hint = suggestCommand(verb);
|
|
290
320
|
const lines = [`Unknown command: ${verb}`];
|
|
291
321
|
if (hint)
|
|
@@ -293,13 +323,40 @@ function formatCommandHelp(verb, suggestCommand) {
|
|
|
293
323
|
lines.push(" Try: cw help (list all commands)");
|
|
294
324
|
return `${lines.join("\n")}\n`;
|
|
295
325
|
}
|
|
326
|
+
return renderCommandHelpRows(`cw ${verb}`, rows);
|
|
327
|
+
}
|
|
328
|
+
/** The row-list rendering `formatCommandHelp` has always done, extracted
|
|
329
|
+
* so the alias page above can render its target's rows under its own
|
|
330
|
+
* header line without a second copy of the sort/pad/Flags logic. */
|
|
331
|
+
function renderCommandHelpRows(header, rows) {
|
|
296
332
|
const sorted = [...rows].sort((a, b) => (a.command < b.command ? -1 : a.command > b.command ? 1 : 0));
|
|
297
333
|
const longest = Math.min(40, Math.max(...sorted.map((row) => row.command.length)));
|
|
298
|
-
const lines = [
|
|
334
|
+
const lines = [header, ""];
|
|
299
335
|
for (const row of sorted) {
|
|
300
336
|
const padded = row.command.padEnd(longest, " ");
|
|
301
337
|
lines.push(` ${padded} ${row.summary}`);
|
|
302
338
|
}
|
|
339
|
+
// Flags block(s): only for rows that declare at least one flag (most
|
|
340
|
+
// rows do not — see CliBinding.flags). Uses the SAME 4-space indent as
|
|
341
|
+
// formatHelp's own "Run cw help <command> ..." note above, on purpose:
|
|
342
|
+
// the CLI/MCP parity help-token parser reads only 2-space lines as
|
|
343
|
+
// command tokens, so any wider indent is a line it already knows to
|
|
344
|
+
// skip. One row's command name is only spelled out in the block's own
|
|
345
|
+
// header when more than one row on this page declares flags (e.g.
|
|
346
|
+
// "cw ledger" lists both propose and review); a single-row page (e.g.
|
|
347
|
+
// "cw doctor") just says "Flags".
|
|
348
|
+
const flaggedRows = sorted.filter((row) => row.flags && row.flags.length > 0);
|
|
349
|
+
if (flaggedRows.length > 0) {
|
|
350
|
+
const oneFlaggedRow = flaggedRows.length === 1;
|
|
351
|
+
for (const row of flaggedRows) {
|
|
352
|
+
const flags = row.flags;
|
|
353
|
+
const flagWidth = Math.min(40, Math.max(...flags.map((flag) => flag.name.length)));
|
|
354
|
+
lines.push("", oneFlaggedRow ? " Flags" : ` Flags (${row.command})`);
|
|
355
|
+
for (const flag of flags) {
|
|
356
|
+
lines.push(` ${flag.name.padEnd(flagWidth, " ")} ${flag.summary}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
303
360
|
return `${lines.join("\n")}\n`;
|
|
304
361
|
}
|
|
305
362
|
/** `cw info <id>` human card — MILESTONE 12. Byte-exact in spirit to
|
|
@@ -360,3 +417,16 @@ function formatSearchResults(keyword, results) {
|
|
|
360
417
|
lines.push("Use cw info <id> for full details.");
|
|
361
418
|
return lines.join("\n");
|
|
362
419
|
}
|
|
420
|
+
/** `cw list`'s TTY-only human text (the `list` row's `humanRender`, see
|
|
421
|
+
* core/capability-data.ts's CliBinding). Modeled on formatSearchResults
|
|
422
|
+
* above: one "<id> — <title>" line per workflow, then the same short
|
|
423
|
+
* next-step footer. NEVER printed to a pipe — the row's canonical JSON
|
|
424
|
+
* stays the only piped output (SPEC/cli-surface.md's jsonMode contract:
|
|
425
|
+
* "default" verbs are always JSON on a non-TTY stream). */
|
|
426
|
+
function formatWorkflowList(workflows) {
|
|
427
|
+
const rows = (Array.isArray(workflows) ? workflows : []);
|
|
428
|
+
const lines = rows.map((w) => `${w.id} — ${w.title}`);
|
|
429
|
+
lines.push("");
|
|
430
|
+
lines.push("Use cw info <id> for full details.");
|
|
431
|
+
return lines.join("\n");
|
|
432
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// core/format/recovery-hint.ts — recoveryHint, a pure content-based lookup
|
|
3
|
+
// from a thrown error's message to ONE copy-pasteable follow-up command.
|
|
4
|
+
//
|
|
5
|
+
// src/cli.ts:18-29 in the old build. Moved here (out of cli/entry.ts) so
|
|
6
|
+
// mcp/server.ts can use the same lookup for its own error text without
|
|
7
|
+
// crossing the mcp/-may-never-import-cli/ layer rule that
|
|
8
|
+
// scripts/purity-gate.js enforces — core/ may be read by both cli/ and
|
|
9
|
+
// mcp/. cli/entry.ts now just re-exports this function; its own call
|
|
10
|
+
// site and behavior are unchanged.
|
|
11
|
+
//
|
|
12
|
+
// Content-based (reads the message text, not the call site), so it stays
|
|
13
|
+
// correct no matter which command or tool threw. Returns `undefined`
|
|
14
|
+
// rather than a wrong guess when no pattern matches.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.recoveryHint = recoveryHint;
|
|
17
|
+
function recoveryHint(message) {
|
|
18
|
+
const m = message.toLowerCase();
|
|
19
|
+
if (m.startsWith("unknown command"))
|
|
20
|
+
return "cw help";
|
|
21
|
+
if (m.includes("not configured") || m.includes("agent backend"))
|
|
22
|
+
return "cw doctor";
|
|
23
|
+
if (m.includes("missing") && m.includes("repo"))
|
|
24
|
+
return 'cw -q "<question>" -dir <project-folder>';
|
|
25
|
+
if (m.includes("app") && (m.includes("not found") || m.includes("not available")))
|
|
26
|
+
return "cw app list";
|
|
27
|
+
if (m.includes("run id") || m.includes("run not found"))
|
|
28
|
+
return "cw run list";
|
|
29
|
+
if (m.includes("missing required input") && m.includes("question"))
|
|
30
|
+
return 'cw -q "<question>"';
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
@@ -36,6 +36,7 @@ exports.listComments = listComments;
|
|
|
36
36
|
exports.distinctTargets = distinctTargets;
|
|
37
37
|
exports.formatReviewStatus = formatReviewStatus;
|
|
38
38
|
exports.formatCommentList = formatCommentList;
|
|
39
|
+
const cli_args_1 = require("../util/cli-args");
|
|
39
40
|
const collate_1 = require("../util/collate");
|
|
40
41
|
exports.COLLABORATION_SCHEMA_VERSION = 1;
|
|
41
42
|
/** The single, honest stand-in for an absent identity. */
|
|
@@ -166,10 +167,10 @@ function buildHandoff(input, handoffCount, runId, now, auditEventId) {
|
|
|
166
167
|
auditEventIds: [auditEventId],
|
|
167
168
|
});
|
|
168
169
|
}
|
|
169
|
-
/**
|
|
170
|
+
/** Parse a defined tri-state flag; leave `undefined` alone so the
|
|
170
171
|
* caller's `?? existing ?? default` chain still governs an unset flag. */
|
|
171
|
-
function coerceFlag(value) {
|
|
172
|
-
return
|
|
172
|
+
function coerceFlag(value, label) {
|
|
173
|
+
return (0, cli_args_1.parseBoolFlag)(value, label);
|
|
173
174
|
}
|
|
174
175
|
function toNumber(value, fallback) {
|
|
175
176
|
if (value === undefined || value === null || value === "" || value === true)
|
|
@@ -201,8 +202,8 @@ function buildReviewPolicy(input, existing, now) {
|
|
|
201
202
|
id: existing?.id || createCollabId("policy", 0),
|
|
202
203
|
requiredApprovals: Math.max(0, Math.floor(toNumber(input.requiredApprovals, existing?.requiredApprovals ?? 0))),
|
|
203
204
|
authorizedRoles: toStringList(input.authorizedRoles, existing?.authorizedRoles ?? ["*"]),
|
|
204
|
-
allowSelfApproval: coerceFlag(input.allowSelfApproval) ?? existing?.allowSelfApproval ?? false,
|
|
205
|
-
requireAttestedActor: coerceFlag(input.requireAttestedActor) ?? existing?.requireAttestedActor ?? false,
|
|
205
|
+
allowSelfApproval: coerceFlag(input.allowSelfApproval, "allowSelfApproval") ?? existing?.allowSelfApproval ?? false,
|
|
206
|
+
requireAttestedActor: coerceFlag(input.requireAttestedActor, "requireAttestedActor") ?? existing?.requireAttestedActor ?? false,
|
|
206
207
|
appliesTo: toTargetKindList(input.appliesTo, existing?.appliesTo ?? ["commit"]),
|
|
207
208
|
updatedAt: now,
|
|
208
209
|
};
|
|
@@ -235,6 +236,17 @@ function roleAuthorized(roleId, authorizedRoles) {
|
|
|
235
236
|
return false;
|
|
236
237
|
return authorizedRoles.includes(roleId);
|
|
237
238
|
}
|
|
239
|
+
/** Whether `record` itself counts for anything at all (same eligibility a
|
|
240
|
+
* record needs to be counted as an approval or a veto in the main loop
|
|
241
|
+
* below). A record that fails this can't void another record either —
|
|
242
|
+
* otherwise a disqualified self-approval could still cancel someone
|
|
243
|
+
* else's veto via `supersedes`. */
|
|
244
|
+
function recordCountsAtAll(record, policy, selfIds) {
|
|
245
|
+
const reason = disqualify(record, policy, selfIds);
|
|
246
|
+
if (record.decision === "reject")
|
|
247
|
+
return !reason || reason === "self-approval";
|
|
248
|
+
return !reason;
|
|
249
|
+
}
|
|
238
250
|
function deriveStatus(gated, required, recorded, rejectionCount, disqualified) {
|
|
239
251
|
if (!gated)
|
|
240
252
|
return "approved";
|
|
@@ -284,7 +296,24 @@ function deriveReviewState(runId, approvalsAll, target, options = {}) {
|
|
|
284
296
|
const related = (options.relatedTargets && options.relatedTargets.length ? options.relatedTargets : [normalized]).map(normalizeTarget);
|
|
285
297
|
const selfIds = new Set((options.selfActorIds || []).filter(Boolean));
|
|
286
298
|
const approvals = approvalsAll.filter((record) => matchesAnyTarget(record.target, related));
|
|
287
|
-
|
|
299
|
+
// A record's `supersedes` only takes effect when the target is that SAME
|
|
300
|
+
// actor's own prior record (an actor may only supersede their own
|
|
301
|
+
// record, never someone else's) and the superseding record itself is
|
|
302
|
+
// eligible to count for something. Otherwise a disqualified record (e.g.
|
|
303
|
+
// a self-approval a policy forbids) could still void another actor's
|
|
304
|
+
// veto just by naming it in `supersedes`.
|
|
305
|
+
const byId = new Map(approvals.map((record) => [record.id, record]));
|
|
306
|
+
const supersededIds = new Set();
|
|
307
|
+
for (const record of approvals) {
|
|
308
|
+
if (!record.supersedes)
|
|
309
|
+
continue;
|
|
310
|
+
const target = byId.get(record.supersedes);
|
|
311
|
+
if (!target || target.actor.id !== record.actor.id)
|
|
312
|
+
continue;
|
|
313
|
+
if (!recordCountsAtAll(record, policy, selfIds))
|
|
314
|
+
continue;
|
|
315
|
+
supersededIds.add(record.supersedes);
|
|
316
|
+
}
|
|
288
317
|
const gated = Boolean(policy && policy.requiredApprovals > 0 && policy.appliesTo.includes(normalized.kind));
|
|
289
318
|
const required = gated ? policy.requiredApprovals : 0;
|
|
290
319
|
const counted = [];
|
|
@@ -651,6 +651,13 @@ function collectAgentFanin(run, input, now) {
|
|
|
651
651
|
...missingRoleIds.map((roleId) => `required role ${roleId} has no membership`),
|
|
652
652
|
...missingMembershipIds.map((membershipId) => `membership ${membershipId} has not reported required evidence`),
|
|
653
653
|
];
|
|
654
|
+
// An aggregation gate that observed zero members must not report itself
|
|
655
|
+
// ready: with no required roles and no memberships every per-item check
|
|
656
|
+
// above is vacuously empty, which used to yield verifierReady:true for a
|
|
657
|
+
// fan-in over nothing (fail-open).
|
|
658
|
+
if (!requiredRoleIds.length && !scopedMemberships.length) {
|
|
659
|
+
blockedReasons.push("fan-in has no memberships and no required roles — nothing to aggregate");
|
|
660
|
+
}
|
|
654
661
|
const requiredMemberships = scopedMemberships.filter((membership) => requiredRoleIds.includes(membership.roleId));
|
|
655
662
|
const blackboardId = input.blackboardId || group.blackboardId || multiAgentRun.blackboardId;
|
|
656
663
|
const requiresBlackboardEvidence = Boolean(blackboardId || requiredMemberships.some((membership) => membership.blackboardId));
|
|
@@ -168,9 +168,15 @@ function evaluatePolicy(policy, operation, topicId, evidenceRefs) {
|
|
|
168
168
|
}
|
|
169
169
|
const missing = missingEvidence(policy, operation, evidenceRefs);
|
|
170
170
|
if (missing.length)
|
|
171
|
-
return `operation ${operation} requires evidence refs: ${missing.join(", ")}`;
|
|
171
|
+
return `operation ${operation} requires evidence refs (at least one; expected kinds: ${missing.join(", ")})`;
|
|
172
172
|
return undefined;
|
|
173
173
|
}
|
|
174
|
+
/** The requiredEvidenceFor entries are prose descriptions ("judge
|
|
175
|
+
* messages", "score evidence"), not machine-matchable ids — so this can
|
|
176
|
+
* only check that SOME evidence ref was supplied, not that each named
|
|
177
|
+
* kind is present. The denial message above says exactly that ("at least
|
|
178
|
+
* one; expected kinds: ...") rather than promising a per-item match this
|
|
179
|
+
* check cannot do. */
|
|
174
180
|
function missingEvidence(policy, operation, evidenceRefs) {
|
|
175
181
|
if (!policy)
|
|
176
182
|
return [];
|
|
@@ -71,6 +71,13 @@ function createDefaultPipelineContract() {
|
|
|
71
71
|
},
|
|
72
72
|
],
|
|
73
73
|
artifactPolicy: { root: ".cw/runs/<run-id>", requireReadablePaths: true },
|
|
74
|
+
// highPriorityRequiresEvidence is carried for byte-compat with the old
|
|
75
|
+
// build's default contract (pinned by SPEC/pipeline-run.md) but is NOT
|
|
76
|
+
// enforced anywhere — no gate reads it, and core has no task-priority
|
|
77
|
+
// concept to key it on. The enforced contract-wide flag is
|
|
78
|
+
// `requireEvidence` (state-node.ts assertRequiredEvidence, runner.ts
|
|
79
|
+
// evidenceSatisfied). The per-stage `requiredEvidence` lists are what
|
|
80
|
+
// actually gate evidence in the default contract.
|
|
74
81
|
evidencePolicy: { highPriorityRequiresEvidence: true },
|
|
75
82
|
failurePolicy: { preserveFailureNodes: true, retryableByDefault: false },
|
|
76
83
|
commitPolicy: { requiresVerifierGate: true, acceptedVerifierStatuses: ["verified"] },
|
|
@@ -85,10 +85,10 @@ function sourceFor(classification) {
|
|
|
85
85
|
return "contract";
|
|
86
86
|
return "manual";
|
|
87
87
|
}
|
|
88
|
-
/** Feedback dedup key: joined with
|
|
88
|
+
/** Feedback dedup key: joined with the ASCII unit separator "\u001f" (runId, code, message, nodeId,
|
|
89
89
|
* stageId, contractId, path). */
|
|
90
90
|
function feedbackKey(value) {
|
|
91
|
-
return [value.runId || "", value.code || "", value.message || "", value.nodeId || "", value.stageId || "", value.contractId || "", value.path || ""].join("
|
|
91
|
+
return [value.runId || "", value.code || "", value.message || "", value.nodeId || "", value.stageId || "", value.contractId || "", value.path || ""].join("\u001f");
|
|
92
92
|
}
|
|
93
93
|
function compactMetadata(metadata) {
|
|
94
94
|
const compacted = {};
|
|
@@ -116,8 +116,13 @@ function extractEvidenceContent(locator, baseDirs, ops, readFile) {
|
|
|
116
116
|
const shape = classify(locator);
|
|
117
117
|
if (shape.kind !== "file" || !shape.pathPart)
|
|
118
118
|
return undefined;
|
|
119
|
-
|
|
119
|
+
// Match the same shapes LINE_SUFFIX_RE (classify) accepts: ":<n>" and
|
|
120
|
+
// ":<n>-<m>". A range that only matched classify used to fall through to
|
|
121
|
+
// the head-of-file slice below — returning bytes that had nothing to do
|
|
122
|
+
// with the cited lines.
|
|
123
|
+
const lineMatch = locator.match(/:(\d+)(?:-(\d+))?$/);
|
|
120
124
|
const lineNum = lineMatch ? Number(lineMatch[1]) : undefined;
|
|
125
|
+
const lineEnd = lineMatch && lineMatch[2] ? Number(lineMatch[2]) : undefined;
|
|
121
126
|
const candidatePath = ops.isAbsolute(shape.pathPart)
|
|
122
127
|
? shape.pathPart
|
|
123
128
|
: baseDirs.filter(Boolean).map((base) => ops.resolve(base, shape.pathPart)).find((p) => ops.exists(p));
|
|
@@ -128,6 +133,13 @@ function extractEvidenceContent(locator, baseDirs, ops, readFile) {
|
|
|
128
133
|
return undefined;
|
|
129
134
|
if (lineNum && lineNum > 0) {
|
|
130
135
|
const lines = content.split("\n");
|
|
136
|
+
if (lineEnd !== undefined) {
|
|
137
|
+
// Range: return exactly the cited lines; a backwards or out-of-range
|
|
138
|
+
// span returns undefined, never fabricated content.
|
|
139
|
+
if (lineEnd < lineNum || lineNum > lines.length)
|
|
140
|
+
return undefined;
|
|
141
|
+
return lines.slice(lineNum - 1, Math.min(lineEnd, lines.length)).join("\n") || undefined;
|
|
142
|
+
}
|
|
131
143
|
return lines[lineNum - 1] || undefined;
|
|
132
144
|
}
|
|
133
145
|
return content.slice(0, 200);
|
|
@@ -16,6 +16,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
16
|
exports.required = required;
|
|
17
17
|
exports.optionalArg = optionalArg;
|
|
18
18
|
exports.wantsJson = wantsJson;
|
|
19
|
+
exports.parseBoolFlag = parseBoolFlag;
|
|
19
20
|
/** Require a positional/option value or fail with a copy-pasteable recovery tip. */
|
|
20
21
|
function required(value, label) {
|
|
21
22
|
if (!value) {
|
|
@@ -31,3 +32,24 @@ function optionalArg(value) {
|
|
|
31
32
|
function wantsJson(options) {
|
|
32
33
|
return Boolean(options.json || options.format === "json");
|
|
33
34
|
}
|
|
35
|
+
/** Parse a boolean flag value that may arrive as a real boolean (bare
|
|
36
|
+
* `--flag`) or as a CLI/MCP string (`--flag false`). `Boolean("false")` is
|
|
37
|
+
* `true` in JS, so a plain Boolean() coercion silently ENABLES a flag the
|
|
38
|
+
* operator asked to turn off — on a gate-policy flag like
|
|
39
|
+
* `--allow-self-approval false` that is a fail-open. Recognized strings
|
|
40
|
+
* (case-insensitive, trimmed): true/1/yes/on and false/0/no/off/"".
|
|
41
|
+
* Anything else throws — fail closed, never guess. `undefined` and JSON
|
|
42
|
+
* `null` (an MCP caller's "unset") stay `undefined` so a caller's
|
|
43
|
+
* `?? existing ?? default` chain still governs an unset flag. */
|
|
44
|
+
function parseBoolFlag(value, label) {
|
|
45
|
+
if (value === undefined || value === null)
|
|
46
|
+
return undefined;
|
|
47
|
+
if (typeof value === "boolean")
|
|
48
|
+
return value;
|
|
49
|
+
const text = String(value).trim().toLowerCase();
|
|
50
|
+
if (text === "true" || text === "1" || text === "yes" || text === "on")
|
|
51
|
+
return true;
|
|
52
|
+
if (text === "false" || text === "0" || text === "no" || text === "off" || text === "")
|
|
53
|
+
return false;
|
|
54
|
+
throw new Error(`Invalid boolean value for ${label}: "${String(value)}" (use true or false)`);
|
|
55
|
+
}
|
package/dist/core/version.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA
|
|
|
7
7
|
// `cw version` output does not change bytes just because the code under it
|
|
8
8
|
// was rebuilt. See SPEC/cli-surface.md "Exact outputs > Version" and
|
|
9
9
|
// conformance/cases/version-basic.case.js (regex `/^\d+\.\d+\.\d+\n$/`).
|
|
10
|
-
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.2.
|
|
10
|
+
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.2.5";
|
|
11
11
|
// State-kernel schema version constants (SPEC/state-core.md "Version
|
|
12
12
|
// constants"). Pinned to the old build's src/version.ts byte-for-byte.
|
|
13
13
|
exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
|
package/dist/mcp/dispatch.js
CHANGED
|
@@ -81,6 +81,22 @@ function requiredToolArguments(name, value) {
|
|
|
81
81
|
}
|
|
82
82
|
return args;
|
|
83
83
|
}
|
|
84
|
+
/** MCP clients send JSON, so an argument the CLI would receive as a string
|
|
85
|
+
* can arrive as a number or boolean (e.g. `{"runId": 5}`). Every capability
|
|
86
|
+
* handler coerces its args through optionalString/numberArg/boolArg, all of
|
|
87
|
+
* which expect the CLI's string form — optionalString silently DROPS a
|
|
88
|
+
* number, so `{"runId": 5}` used to look like "no runId given" and returned
|
|
89
|
+
* a success-shaped WRONG answer (the "create a run first" payload). Coerce
|
|
90
|
+
* top-level scalars to their string form so an MCP arg behaves exactly like
|
|
91
|
+
* the CLI's argv, which is always strings. Arrays/objects (e.g.
|
|
92
|
+
* authorizedRoles) pass through untouched; null/undefined stay absent. */
|
|
93
|
+
function coerceScalarArgs(args) {
|
|
94
|
+
const out = {};
|
|
95
|
+
for (const [key, value] of Object.entries(args)) {
|
|
96
|
+
out[key] = typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" ? String(value) : value;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
84
100
|
/** `callTool(name, args)` — resolves `cwd` (SPEC/mcp.md invariant 7),
|
|
85
101
|
* checks required args, then calls the row's `mcp.handler`. Throws
|
|
86
102
|
* `Unknown tool: <name>` for a name with no row (src/mcp/tool-call.ts:513
|
|
@@ -90,12 +106,16 @@ function callTool(name, rawArgs) {
|
|
|
90
106
|
if (!row || !row.mcp) {
|
|
91
107
|
throw new Error(`Unknown tool: ${name}`);
|
|
92
108
|
}
|
|
93
|
-
const args = requiredToolArguments(name, rawArgs);
|
|
109
|
+
const args = coerceScalarArgs(requiredToolArguments(name, rawArgs));
|
|
94
110
|
const resolvedArgs = { ...args };
|
|
95
111
|
const cwdInput = args.cwd;
|
|
96
112
|
if (typeof cwdInput === "string" && cwdInput.length > 0) {
|
|
97
113
|
const resolved = path.resolve(cwdInput);
|
|
98
|
-
|
|
114
|
+
// throwIfNoEntry:false so a MISSING cwd yields the same crafted
|
|
115
|
+
// "not a directory" message as a non-dir path — not a raw ENOENT that
|
|
116
|
+
// gives the recovery-hint matcher nothing to key on.
|
|
117
|
+
const stat = fs.statSync(resolved, { throwIfNoEntry: false });
|
|
118
|
+
if (!stat || !stat.isDirectory()) {
|
|
99
119
|
throw new Error(`MCP cwd is not a directory: ${resolved}`);
|
|
100
120
|
}
|
|
101
121
|
resolvedArgs.cwd = resolved;
|
package/dist/mcp/server.js
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
// "Invariants and error behavior" #9 / "Edge cases" sections:
|
|
8
8
|
// - transport: stdin/stdout, ONE JSON object per line, no
|
|
9
9
|
// Content-Length headers (mcp.md:13);
|
|
10
|
-
// - `initialize` -> protocolVersion/capabilities/serverInfo
|
|
11
|
-
//
|
|
10
|
+
// - `initialize` -> protocolVersion/capabilities/serverInfo (mcp.md:
|
|
11
|
+
// 19,263-269); the reply's protocolVersion echoes the client's
|
|
12
|
+
// requested one when it is in SUPPORTED_PROTOCOL_VERSIONS below, and
|
|
13
|
+
// falls back to the newest supported entry otherwise (with today's
|
|
14
|
+
// one-entry list this is byte-identical to the old fixed reply);
|
|
12
15
|
// - `tools/list` -> { tools: [...] } from core/capability-table.ts via
|
|
13
16
|
// mcp/dispatch.ts, ignoring params (mcp.md:20,271-277);
|
|
14
17
|
// - `tools/call` -> { content: [{ type: "text", text: <2-space pretty
|
|
@@ -48,12 +51,40 @@
|
|
|
48
51
|
// blow V8's per-string limit) becomes a small overflow notice instead
|
|
49
52
|
// of a multi-hundred-MB payload — every result under the cap is
|
|
50
53
|
// untouched, so this never affects the parity gate's own fixtures.
|
|
54
|
+
// - `tools/call` FAILURE result shape (post-v0.2.4 robustness hardening,
|
|
55
|
+
// not in the original mcp.md): an unknown tool name, a missing
|
|
56
|
+
// required tool argument, or the tool's own handler throwing is a
|
|
57
|
+
// normal RESULT, not a bare -32000 JSON-RPC protocol error — many MCP
|
|
58
|
+
// hosts never surface a protocol error back to the calling model, so
|
|
59
|
+
// it could not read the message or try again. The result is shaped
|
|
60
|
+
// { content: [{ type: "text", text: <message, plus a "Try: <hint>"
|
|
61
|
+
// line when core/format/recovery-hint.ts's recoveryHint finds one>
|
|
62
|
+
// }], isError: true }, same `resultMessage` helper as the success
|
|
63
|
+
// path. The envelope-level "missing field: name" check (right above
|
|
64
|
+
// this bullet) is unchanged — it still answers -32000.
|
|
51
65
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
66
|
+
exports.negotiateProtocolVersion = negotiateProtocolVersion;
|
|
52
67
|
exports.startServer = startServer;
|
|
53
68
|
const version_1 = require("../core/version");
|
|
54
69
|
const safe_json_1 = require("../core/format/safe-json");
|
|
70
|
+
const recovery_hint_1 = require("../core/format/recovery-hint");
|
|
55
71
|
const dispatch_1 = require("./dispatch");
|
|
56
72
|
const MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
73
|
+
/** Protocol versions this server can speak, oldest first. `initialize`
|
|
74
|
+
* echoes the client's `params.protocolVersion` when it is in this list,
|
|
75
|
+
* and answers with the newest entry (the last one) otherwise — the
|
|
76
|
+
* standard MCP version-negotiation shape. With one entry this is
|
|
77
|
+
* behavior-identical to the old hard-coded reply (mechanism first; a
|
|
78
|
+
* second version is a one-line append here). */
|
|
79
|
+
const SUPPORTED_PROTOCOL_VERSIONS = ["2024-11-05"];
|
|
80
|
+
/** Picks the `initialize` reply's protocolVersion from the client's
|
|
81
|
+
* requested one (see SUPPORTED_PROTOCOL_VERSIONS). Exported for the
|
|
82
|
+
* protocol-version smoke; pure. */
|
|
83
|
+
function negotiateProtocolVersion(requested) {
|
|
84
|
+
if (typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested))
|
|
85
|
+
return requested;
|
|
86
|
+
return SUPPORTED_PROTOCOL_VERSIONS[SUPPORTED_PROTOCOL_VERSIONS.length - 1];
|
|
87
|
+
}
|
|
57
88
|
// Tools whose result carries free-form text ORIGINALLY AUTHORED by a
|
|
58
89
|
// worker/agent/operator/external caller — never computed or validated by
|
|
59
90
|
// this codebase itself (a blackboard message body, a review comment, a
|
|
@@ -117,6 +148,11 @@ function resultMessage(id, result) {
|
|
|
117
148
|
if (id !== undefined)
|
|
118
149
|
message.id = id;
|
|
119
150
|
return message;
|
|
151
|
+
// NOTE: a notification-shaped request (initialize/tools/list/tools/call
|
|
152
|
+
// with no `id`) still gets a reply here, just without an `id` key — a
|
|
153
|
+
// deliberate deviation from strict JSON-RPC (which says a notification
|
|
154
|
+
// MUST NOT be answered) pinned by SPEC/mcp.md's edge-cases. Revisit only
|
|
155
|
+
// as a deliberate spec change, not as a drive-by fix.
|
|
120
156
|
}
|
|
121
157
|
/** Handles one already-parsed JSON-RPC request object. May write zero or
|
|
122
158
|
* one reply line to stdout. `await`ing callTool's result is a no-op for
|
|
@@ -142,8 +178,9 @@ async function handleRequest(message) {
|
|
|
142
178
|
try {
|
|
143
179
|
switch (message.method) {
|
|
144
180
|
case "initialize": {
|
|
181
|
+
const params = (message.params ?? {});
|
|
145
182
|
writeMessage(resultMessage(id, {
|
|
146
|
-
protocolVersion:
|
|
183
|
+
protocolVersion: negotiateProtocolVersion(params.protocolVersion),
|
|
147
184
|
capabilities: { tools: {} },
|
|
148
185
|
serverInfo: { name: "cool-workflow", version: version_1.CURRENT_COOL_WORKFLOW_VERSION },
|
|
149
186
|
}));
|
|
@@ -153,6 +190,17 @@ async function handleRequest(message) {
|
|
|
153
190
|
writeMessage(resultMessage(id, { tools: (0, dispatch_1.toolDefinitions)() }));
|
|
154
191
|
return;
|
|
155
192
|
}
|
|
193
|
+
case "ping": {
|
|
194
|
+
// MCP (2024-11-05, the version negotiateProtocolVersion advertises)
|
|
195
|
+
// makes ping mandatory: reply promptly with an EMPTY result. Hosts
|
|
196
|
+
// ping for keep-alive and may drop a connection that never answers.
|
|
197
|
+
// Answered here in the fast protocol path (not the serial tool
|
|
198
|
+
// queue), so a ping during a long cw_run drive still gets a reply.
|
|
199
|
+
// A ping notification (no id) gets no reply, per JSON-RPC.
|
|
200
|
+
if (hasId)
|
|
201
|
+
writeMessage(resultMessage(id, {}));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
156
204
|
case "tools/call": {
|
|
157
205
|
const params = message.params ?? {};
|
|
158
206
|
const name = params.name;
|
|
@@ -160,12 +208,32 @@ async function handleRequest(message) {
|
|
|
160
208
|
throw new Error("MCP tools/call missing required field: name");
|
|
161
209
|
}
|
|
162
210
|
const args = params.arguments;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
211
|
+
// A failure from HERE down (an unknown tool name, a missing
|
|
212
|
+
// required tool argument, or the tool's own handler throwing) is a
|
|
213
|
+
// normal call outcome, not a broken request — many MCP hosts never
|
|
214
|
+
// surface a bare JSON-RPC protocol error (-32000) back to the
|
|
215
|
+
// calling model at all, so it could not see the message and try
|
|
216
|
+
// again. Answer with a normal RESULT instead, shaped isError:
|
|
217
|
+
// true, so the model always sees the message (and, when one
|
|
218
|
+
// applies, a "Try: <hint>" recovery line) and can self-correct.
|
|
219
|
+
// The envelope-level "missing field: name" check above stays OUT
|
|
220
|
+
// of this inner try/catch — that one is a malformed request, not
|
|
221
|
+
// a tool-call outcome, and keeps going through the outer
|
|
222
|
+
// try/catch as a -32000 error, unchanged.
|
|
223
|
+
try {
|
|
224
|
+
const coreResult = await (0, dispatch_1.callTool)(name, args ?? {});
|
|
225
|
+
const content = [{ type: "text", text: (0, safe_json_1.safeJsonStringify)(coreResult) }];
|
|
226
|
+
const advisory = untrustedContentAdvisory(name);
|
|
227
|
+
if (advisory)
|
|
228
|
+
content.push({ type: "text", text: advisory });
|
|
229
|
+
writeMessage(resultMessage(id, { content }));
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
233
|
+
const hint = (0, recovery_hint_1.recoveryHint)(text);
|
|
234
|
+
const errorText = hint ? `${text}\nTry: ${hint}` : text;
|
|
235
|
+
writeMessage(resultMessage(id, { content: [{ type: "text", text: errorText }], isError: true }));
|
|
236
|
+
}
|
|
169
237
|
return;
|
|
170
238
|
}
|
|
171
239
|
default: {
|
|
@@ -205,14 +273,52 @@ async function handleLine(line) {
|
|
|
205
273
|
* tool but `cw_run` is still a plain synchronous handler), so this adds
|
|
206
274
|
* no real delay, but it keeps replies in the same order the requests
|
|
207
275
|
* arrived even now that one tool (`cw_run`'s live drive loop) can take
|
|
208
|
-
* many real event-loop turns to answer.
|
|
276
|
+
* many real event-loop turns to answer. The one exception is `ping`,
|
|
277
|
+
* answered in handleRequest's fast path before any tool work, so a
|
|
278
|
+
* keep-alive ping still gets a reply while a long drive holds the queue. */
|
|
209
279
|
function startServer() {
|
|
210
280
|
process.stdin.setEncoding("utf8");
|
|
211
281
|
let buffer = "";
|
|
282
|
+
// True while the REST of an oversize line is still streaming in: emit one
|
|
283
|
+
// -32700 for the whole line and skip everything up to its terminating
|
|
284
|
+
// newline, instead of dropping the head and then re-parsing the tail as a
|
|
285
|
+
// fresh (also-failing) line — which produced a second, spurious parse
|
|
286
|
+
// error per 16MB crossed.
|
|
287
|
+
let discarding = false;
|
|
212
288
|
let queue = Promise.resolve();
|
|
289
|
+
// Chain each task onto `queue` WITH a per-task `.catch`. The `.catch` is
|
|
290
|
+
// load-bearing, not decoration: a task here can reject — a raw
|
|
291
|
+
// `writeMessage` (`process.stdout.write`) that throws mid-reply because the
|
|
292
|
+
// client closed the pipe, or any other throw out of handleLine — and
|
|
293
|
+
// without a handler that one rejection would leave `queue` REJECTED for
|
|
294
|
+
// good, so every later `queue.then(...)` is skipped and the server goes
|
|
295
|
+
// silent and answers no more requests (finding: one bad write poisons the
|
|
296
|
+
// queue). The `.catch` swallows the single failure onto stderr
|
|
297
|
+
// (diagnostics, never stdout data) and hands back a RESOLVED promise, so
|
|
298
|
+
// the next request is still served. Order is still kept: the next task
|
|
299
|
+
// only runs after this one settles.
|
|
300
|
+
const enqueue = (task) => {
|
|
301
|
+
queue = queue.then(task).catch((error) => {
|
|
302
|
+
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
303
|
+
process.stderr.write(`cool-workflow mcp: a request failed and its reply was dropped; still serving: ${detail}\n`);
|
|
304
|
+
});
|
|
305
|
+
};
|
|
213
306
|
process.stdin.on("data", (chunk) => {
|
|
214
307
|
buffer += chunk;
|
|
215
308
|
for (;;) {
|
|
309
|
+
if (discarding) {
|
|
310
|
+
// Skip the rest of an oversize line already reported. Until its
|
|
311
|
+
// terminating newline arrives, throw away what we have (so a huge
|
|
312
|
+
// line can't grow the buffer unboundedly); once found, resume
|
|
313
|
+
// normal parsing from the next line with no second error.
|
|
314
|
+
const nl = buffer.indexOf("\n");
|
|
315
|
+
if (nl === -1) {
|
|
316
|
+
buffer = "";
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
buffer = buffer.slice(nl + 1);
|
|
320
|
+
discarding = false;
|
|
321
|
+
}
|
|
216
322
|
const newlineIndex = buffer.indexOf("\n");
|
|
217
323
|
if (newlineIndex === -1)
|
|
218
324
|
break;
|
|
@@ -220,11 +326,16 @@ function startServer() {
|
|
|
220
326
|
buffer = buffer.slice(newlineIndex + 1);
|
|
221
327
|
const trimmed = line.trim();
|
|
222
328
|
if (trimmed)
|
|
223
|
-
|
|
329
|
+
enqueue(() => handleLine(trimmed));
|
|
224
330
|
}
|
|
225
|
-
|
|
331
|
+
// No newline yet and the pending (unterminated) line already exceeds the
|
|
332
|
+
// cap: report ONCE, drop the head, and discard the rest of this line
|
|
333
|
+
// until its newline arrives (guarded by `discarding` so a >32MB line
|
|
334
|
+
// yields a single -32700, not one per 16MB crossed).
|
|
335
|
+
if (!discarding && buffer.length > MAX_LINE_BYTES) {
|
|
226
336
|
buffer = "";
|
|
227
|
-
|
|
337
|
+
discarding = true;
|
|
338
|
+
enqueue(() => {
|
|
228
339
|
writeMessage(errorMessage(null, -32700, `Parse error: request line exceeds ${MAX_LINE_BYTES} bytes`));
|
|
229
340
|
});
|
|
230
341
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -6,4 +6,24 @@
|
|
|
6
6
|
// real MCP client launches it directly with `node`).
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
const server_1 = require("./mcp/server");
|
|
9
|
+
/** Broken-pipe guard for the MCP stdio server, the same idea as
|
|
10
|
+
* cli/entry.ts's `main()`. When an MCP client closes the read end of our
|
|
11
|
+
* stdout part-way through a reply, the raw write gives an async 'error'
|
|
12
|
+
* event that no promise `.catch` can see; with no listener Node comes down
|
|
13
|
+
* hard with a `write EPIPE` stack and exit 1. One process-level listener
|
|
14
|
+
* turns that into a quiet exit 0 — the reader has gone, there is nothing
|
|
15
|
+
* left to say. Any other stream error is thrown again, same as before.
|
|
16
|
+
*
|
|
17
|
+
* This is a small COPY of cli/entry.ts's helper, not an import: the purity
|
|
18
|
+
* gate (scripts/purity-gate.js) forbids an mcp/ file from importing cli/,
|
|
19
|
+
* and pulling in the whole CLI entry graph for five lines would be worse. */
|
|
20
|
+
function exitQuietOnEpipe(stream) {
|
|
21
|
+
stream.on("error", (error) => {
|
|
22
|
+
if (error && error.code === "EPIPE")
|
|
23
|
+
process.exit(0);
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
exitQuietOnEpipe(process.stdout);
|
|
28
|
+
exitQuietOnEpipe(process.stderr);
|
|
9
29
|
(0, server_1.startServer)();
|