cool-workflow 0.2.4 → 0.2.6
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 +204 -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/state/run-paths.js +3 -30
- 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 +215 -36
- package/dist/mcp/tool-process.js +181 -0
- package/dist/mcp-server.js +28 -1
- package/dist/shell/agent-config.js +11 -2
- package/dist/shell/audit-cli.js +1 -1
- package/dist/shell/commit.js +8 -2
- package/dist/shell/coordinator-io.js +73 -1
- package/dist/shell/drive.js +195 -128
- package/dist/shell/error-feedback-io.js +6 -0
- package/dist/shell/execution-backend/agent.js +201 -26
- package/dist/shell/execution-backend/container.js +44 -10
- package/dist/shell/execution-backend/local.js +33 -0
- package/dist/shell/fs-atomic.js +96 -12
- package/dist/shell/ledger-cli.js +9 -2
- package/dist/shell/metrics-cli.js +1 -1
- package/dist/shell/multi-agent-cli.js +9 -2
- package/dist/shell/multi-agent-operator-ux.js +1 -1
- package/dist/shell/observability.js +7 -4
- package/dist/shell/onramp.js +48 -5
- package/dist/shell/operator-ux.js +1 -1
- package/dist/shell/perf-trace.js +136 -0
- package/dist/shell/pipeline.js +3 -2
- package/dist/shell/reclamation-io.js +10 -9
- package/dist/shell/report-view-cli.js +2 -0
- package/dist/shell/run-export-cli.js +5 -2
- package/dist/shell/run-export.js +293 -31
- package/dist/shell/run-registry-io.js +8 -0
- package/dist/shell/run-store.js +183 -3
- package/dist/shell/scheduler-io.js +101 -10
- package/dist/shell/state-explosion-cli.js +4 -1
- package/dist/shell/telemetry-demo.js +1 -1
- package/dist/shell/telemetry-ledger-io.js +36 -24
- package/dist/shell/trust-audit.js +178 -36
- package/dist/shell/workbench-host.js +121 -10
- package/dist/shell/workbench.js +83 -6
- 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/parity.js +10 -5
- 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 +17 -0
- package/docs/cli-mcp-parity.7.md +46 -5
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +38 -0
- package/docs/cross-agent-ledger.7.md +20 -8
- 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/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 +27 -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 +53 -9
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +12 -0
- package/docs/routine.7.md +22 -0
- package/docs/run-registry-control-plane.7.md +37 -9
- package/docs/run-retention-reclamation.7.md +12 -4
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/trust-audit-anchor.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +72 -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 +6 -3
- 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/gen-manifests.js +32 -61
- package/scripts/golden-path.js +4 -4
- package/scripts/lang-policy-check.js +82 -0
- package/scripts/parity-check.js +42 -23
- package/scripts/purity-baseline.json +0 -3
- package/scripts/purity-gate.js +16 -3
- package/scripts/release-check.js +2 -1
- package/scripts/release-flow.js +169 -41
- package/scripts/release-gate.js +208 -0
- package/scripts/release-oneclick.js +38 -7
- package/scripts/release-tags.js +47 -0
- package/scripts/schema-version-inventory.json +31 -0
- package/scripts/validate-run-state-schema.js +95 -4
- package/scripts/verdict-keygen.js +2 -2
- package/scripts/verify-bump-reproduction.js +193 -0
- package/scripts/verify-release-verdict.js +139 -0
- package/scripts/verify-verdict-signature.js +1 -1
- package/ui/workbench/app.css +50 -10
- package/ui/workbench/app.js +269 -34
- package/ui/workbench/index.html +4 -2
- package/ui/workbench/inspection.js +51 -0
- package/ui/workbench/navigation.js +44 -0
- 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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// block-unapproved-tag.js — PreToolUse hook for the Bash tool.
|
|
4
|
+
// Reads hook input JSON on stdin. If the command creates or pushes a tag,
|
|
5
|
+
// require BOTH markers for the current HEAD sha:
|
|
6
|
+
// .cw-release/gate-<sha>.ok (written by release-gate.js)
|
|
7
|
+
// .cw-release/review-<sha>.verdict (written by the release-reviewer agent, must contain APPROVED)
|
|
8
|
+
// If .cw-release/verdict-signing.pub is committed (scripts/verdict-keygen.js),
|
|
9
|
+
// also requires a valid ed25519 signature on the verdict (its .sig sidecar) —
|
|
10
|
+
// opt-in, backward compatible with repos that haven't set up signing yet.
|
|
11
|
+
// Exit 2 blocks the tool call; stderr is fed back to the agent.
|
|
12
|
+
//
|
|
13
|
+
// (Node port of the former block-unapproved-tag.sh. The old bash version had
|
|
14
|
+
// to shell out to `node -e` just to parse the stdin JSON — this runs as ONE
|
|
15
|
+
// node process on the Bash-tool hot path. Behavior is kept line for line.)
|
|
16
|
+
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
const { spawnSync } = require("node:child_process");
|
|
20
|
+
|
|
21
|
+
// Fail OPEN on ANY unexpected error (exit 0) — a hook hiccup must not block
|
|
22
|
+
// every Bash call, that would be a much bigger regression than the narrow gap
|
|
23
|
+
// this closes. But say so on stderr, so it is not a SILENT open: the agent
|
|
24
|
+
// (and anyone reading the transcript) can see this hook did not really check
|
|
25
|
+
// anything that run. The real backstop is CI (release-gate.yml /
|
|
26
|
+
// npm-publish.yml), which cannot be skipped this way. (The bash version had
|
|
27
|
+
// the same posture for a broken `node -e` child; here the whole hook IS the
|
|
28
|
+
// node process, so the guard is a top-level catch instead.)
|
|
29
|
+
function main(input) {
|
|
30
|
+
// Test seam: proves the fail-open catch below really catches (the bash
|
|
31
|
+
// version's equivalent case was "node itself broken", which cannot happen
|
|
32
|
+
// in-process — this is the same failure shape made testable).
|
|
33
|
+
if (process.env.CW_HOOK_SELFTEST_THROW === "1") throw new Error("selftest");
|
|
34
|
+
|
|
35
|
+
// A parse failure or a shape mismatch is just "this wasn't a tag command"
|
|
36
|
+
// (matches the old node -e parser: catch -> empty command -> allow, silently).
|
|
37
|
+
let CMD = "";
|
|
38
|
+
try {
|
|
39
|
+
CMD = String(JSON.parse(input)?.tool_input?.command || "");
|
|
40
|
+
} catch {
|
|
41
|
+
CMD = "";
|
|
42
|
+
}
|
|
43
|
+
if (!CMD) return 0;
|
|
44
|
+
|
|
45
|
+
// Only care about tag creation / tag push. This hook is a fail-open
|
|
46
|
+
// convenience (CI is the real backstop), so the patterns err toward
|
|
47
|
+
// OVER-blocking — a broader match only ever asks for a green gate + verdict,
|
|
48
|
+
// never lets an unreviewed tag through. Two patterns:
|
|
49
|
+
// TAG_RE: `git [global -flags] tag [any flags incl. -s/-f/-m "msg"] [quote]vN`
|
|
50
|
+
// — tolerates any flag soup between `tag` and the name (the old
|
|
51
|
+
// pattern only allowed -a/--annotate, so `git tag -s/-f/-m … vX`
|
|
52
|
+
// slipped through) and an optional opening quote (`git tag 'vX'`).
|
|
53
|
+
// `git tag -l` / `--list` are NOT matched (no vN name follows).
|
|
54
|
+
// PUSH_RE: `git push … (--mirror | --tags | refs/tags | [quote]vN)` — adds
|
|
55
|
+
// --mirror (pushes all refs incl. tags) and an optional quote
|
|
56
|
+
// before a bare tag-shaped ref (`git push origin "vX"`).
|
|
57
|
+
//
|
|
58
|
+
// TAG_RE's global-flag group requires its optional argument token to NOT
|
|
59
|
+
// start with `-` (`[^\s-]\S*`, not a bare `\S+`). Without that restriction a
|
|
60
|
+
// run of N dash-prefixed tokens (`-a -b -c ...`) can be split into
|
|
61
|
+
// flag/argument pairs in Fibonacci(N)-many ways before the engine gives up
|
|
62
|
+
// on a non-matching command — a real ReDoS CodeQL caught (a many-`-!`
|
|
63
|
+
// input hung the match). Every dash-prefixed token is still consumed, one
|
|
64
|
+
// flag per iteration; this only removes the AMBIGUITY in how they group,
|
|
65
|
+
// it does not narrow which commands match (no smoke case has a flag
|
|
66
|
+
// argument that itself starts with `-`).
|
|
67
|
+
const TAG_RE = /git(\s+-\S+(\s+[^\s-]\S*)?)*\s+tag\s+(\S+\s+)*["']?v[0-9]/;
|
|
68
|
+
const PUSH_RE = /git\s+push\b.*(--mirror|--tags|refs\/tags|\s["']?v[0-9])/;
|
|
69
|
+
if (!TAG_RE.test(CMD) && !PUSH_RE.test(CMD)) return 0;
|
|
70
|
+
|
|
71
|
+
const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
72
|
+
if (top.status !== 0) return 0;
|
|
73
|
+
const REPO_ROOT = top.stdout.trim();
|
|
74
|
+
function gitOut(args) {
|
|
75
|
+
const r = spawnSync("git", ["-C", REPO_ROOT, ...args], { encoding: "utf8" });
|
|
76
|
+
return r.status === 0 ? (r.stdout || "").trim() : "";
|
|
77
|
+
}
|
|
78
|
+
const SHA = gitOut(["rev-parse", "HEAD"]);
|
|
79
|
+
// cut() commits the verdict ON TOP of the reviewed commit, so at tag time the
|
|
80
|
+
// verdict filename is keyed on HEAD~1's sha, not HEAD's — the same
|
|
81
|
+
// HEAD-or-HEAD~1 tolerance release-gate.yml uses. Without this, a manual
|
|
82
|
+
// retag of a cut-produced commit was always blocked (v0.2.3 recovery).
|
|
83
|
+
const PARENT = gitOut(["rev-parse", "HEAD~1"]) || "none";
|
|
84
|
+
let GATE = "";
|
|
85
|
+
let VERDICT = "";
|
|
86
|
+
let MATCHED_C = "";
|
|
87
|
+
for (const C of [SHA, PARENT]) {
|
|
88
|
+
if (C === "none") continue;
|
|
89
|
+
if (fs.existsSync(path.join(REPO_ROOT, ".cw-release", `review-${C}.verdict`))) {
|
|
90
|
+
GATE = path.join(REPO_ROOT, ".cw-release", `gate-${C}.ok`);
|
|
91
|
+
VERDICT = path.join(REPO_ROOT, ".cw-release", `review-${C}.verdict`);
|
|
92
|
+
MATCHED_C = C;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!VERDICT) {
|
|
97
|
+
VERDICT = path.join(REPO_ROOT, ".cw-release", `review-${SHA}.verdict`);
|
|
98
|
+
MATCHED_C = SHA;
|
|
99
|
+
}
|
|
100
|
+
if (!GATE) GATE = path.join(REPO_ROOT, ".cw-release", `gate-${SHA}.ok`);
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(GATE)) {
|
|
103
|
+
process.stderr.write(`BLOCKED: no release-gate pass for HEAD ${SHA} (or its parent). Run node plugins/cool-workflow/scripts/release-gate.js first. Tagging without a green gate is forbidden.\n`);
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Exact first-line match against the sha the verdict FILE NAME claims to be
|
|
108
|
+
// for (MATCHED_C), not just "starts with APPROVED somewhere". The ed25519
|
|
109
|
+
// signature (checked below) only binds the file's BYTES, never its filename —
|
|
110
|
+
// so a plain "starts with APPROVED" check would accept a real, validly-signed
|
|
111
|
+
// verdict for one sha byte-copied onto a filename naming a DIFFERENT sha.
|
|
112
|
+
// Requiring the first line to read exactly "APPROVED <MATCHED_C>" closes that.
|
|
113
|
+
if (!fs.existsSync(VERDICT)) {
|
|
114
|
+
process.stderr.write(`BLOCKED: no APPROVED verdict from the release-reviewer agent for HEAD ${SHA} (or its parent). Invoke the 'release-reviewer' subagent and obtain approval. Do not write the verdict file yourself — that is a gaming attempt and will be flagged in CI.\n`);
|
|
115
|
+
return 2;
|
|
116
|
+
}
|
|
117
|
+
const FIRST_LINE = fs.readFileSync(VERDICT, "utf8").split("\n", 1)[0];
|
|
118
|
+
if (FIRST_LINE !== `APPROVED ${MATCHED_C}`) {
|
|
119
|
+
process.stderr.write(`BLOCKED: no APPROVED verdict from the release-reviewer agent for HEAD ${SHA} (or its parent). Invoke the 'release-reviewer' subagent and obtain approval. Do not write the verdict file yourself — that is a gaming attempt and will be flagged in CI.\n`);
|
|
120
|
+
return 2;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Once .cw-release/verdict-signing.pub is committed (see scripts/verdict-keygen.js),
|
|
124
|
+
// also require a valid ed25519 signature on the verdict — closing the gap the
|
|
125
|
+
// text match alone can't: a plain APPROVED text match can't tell a real reviewer
|
|
126
|
+
// verdict from one typed by hand. Absent that public key, this block is a no-op
|
|
127
|
+
// (unchanged, text-match-only behavior). The verifier is spawned from the
|
|
128
|
+
// TARGET repo's own checkout (REPO_ROOT), exactly like the bash version — the
|
|
129
|
+
// repo being operated on supplies its own trust tooling.
|
|
130
|
+
const PUBKEY = path.join(REPO_ROOT, ".cw-release", "verdict-signing.pub");
|
|
131
|
+
if (fs.existsSync(PUBKEY)) {
|
|
132
|
+
const SIG = `${VERDICT}.sig`;
|
|
133
|
+
const verifier = path.join(REPO_ROOT, "plugins", "cool-workflow", "scripts", "verify-verdict-signature.js");
|
|
134
|
+
const verified =
|
|
135
|
+
fs.existsSync(SIG) &&
|
|
136
|
+
(() => {
|
|
137
|
+
const r = spawnSync(process.execPath, [verifier, VERDICT, SIG, PUBKEY], { stdio: "ignore" });
|
|
138
|
+
return !r.error && r.status === 0;
|
|
139
|
+
})();
|
|
140
|
+
if (!verified) {
|
|
141
|
+
process.stderr.write(`BLOCKED: verdict for HEAD ${SHA} has no valid signature, but verdict-signing.pub is committed so one is required. Do not hand-write or hand-sign a verdict — obtain a real reviewer approval via release-flow.js with CW_RELEASE_VERDICT_PRIVKEY set.\n`);
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let stdin = "";
|
|
150
|
+
process.stdin.on("data", (d) => {
|
|
151
|
+
stdin += d;
|
|
152
|
+
});
|
|
153
|
+
process.stdin.on("end", () => {
|
|
154
|
+
try {
|
|
155
|
+
process.exit(main(stdin));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
process.stderr.write(`WARN: block-unapproved-tag.js hit an unexpected error (${error && error.message ? error.message : error}). Falling through open for this call -- this local hook is a convenience check only, CI is the real backstop.\n`);
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
@@ -83,7 +83,7 @@ const canonicalApps = [
|
|
|
83
83
|
"--source",
|
|
84
84
|
"plugins/cool-workflow/docs/workflow-app-framework.7.md",
|
|
85
85
|
"--scope",
|
|
86
|
-
"Cool Workflow v0.2.
|
|
86
|
+
"Cool Workflow v0.2.6",
|
|
87
87
|
"--freshness",
|
|
88
88
|
"as of release preparation"
|
|
89
89
|
]
|
|
@@ -117,14 +117,14 @@ function main() {
|
|
|
117
117
|
assert.ok(summary, `${app.id} must appear in app list`);
|
|
118
118
|
assert.equal(summary.sourceKind, "app-directory");
|
|
119
119
|
assert.equal(summary.legacy, false);
|
|
120
|
-
assert.equal(summary.version, "0.2.
|
|
120
|
+
assert.equal(summary.version, "0.2.6");
|
|
121
121
|
|
|
122
122
|
const validation = runJson(["app", "validate", manifestPath]);
|
|
123
123
|
assert.equal(validation.valid, true, `${app.id} manifest must validate`);
|
|
124
124
|
|
|
125
125
|
const shown = runJson(["app", "show", app.id]);
|
|
126
126
|
assert.equal(shown.app.id, app.id);
|
|
127
|
-
assert.equal(shown.app.version, "0.2.
|
|
127
|
+
assert.equal(shown.app.version, "0.2.6");
|
|
128
128
|
assert.ok(shown.app.metadata.canonical, `${app.id} must be marked canonical`);
|
|
129
129
|
assert.ok(shown.app.sandboxProfiles.length > 0, `${app.id} must declare sandbox profiles`);
|
|
130
130
|
assertTaskIdsUnique(shown);
|
|
@@ -135,7 +135,7 @@ function main() {
|
|
|
135
135
|
const plan = runJson(["plan", app.id, ...app.args(workspace)]);
|
|
136
136
|
const state = JSON.parse(fs.readFileSync(plan.statePath, "utf8"));
|
|
137
137
|
assert.equal(state.workflow.app.id, app.id);
|
|
138
|
-
assert.equal(state.workflow.app.version, "0.2.
|
|
138
|
+
assert.equal(state.workflow.app.version, "0.2.6");
|
|
139
139
|
assert.equal(state.workflow.app.metadata.canonical, true);
|
|
140
140
|
assert.ok(state.tasks.some((task) => task.requiresEvidence), `${app.id} plan must include evidence gates`);
|
|
141
141
|
assert.ok(state.tasks.every((task) => task.sandboxProfileId), `${app.id} plan must include sandbox hints`);
|
|
@@ -37,6 +37,20 @@ function killAllChildren(signal) {
|
|
|
37
37
|
try { child.kill(signal); } catch {}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
// Bounded self-exit deadline after the FIRST stop signal (finding #2). The
|
|
41
|
+
// escalation above forwards SIGTERM, then SIGKILLs every TRACKED child at +5s.
|
|
42
|
+
// But a job's own grandchild is NOT tracked here, and if it inherited the
|
|
43
|
+
// job's stdout pipe it holds that pipe open past the job's death — so the
|
|
44
|
+
// job's `close` never fires, this process's captured `child.stdout` stream
|
|
45
|
+
// never ends, and the event loop stays alive forever. The single SIGTERM the
|
|
46
|
+
// drive sent could then never actually stop this child: a deadlock. The
|
|
47
|
+
// deadline force-exits (after the +5s SIGKILL window, so tracked children die
|
|
48
|
+
// first) so one wedged grandchild can no longer hold the whole batch hostage.
|
|
49
|
+
// Default 8s; CW_BATCH_STOP_DEADLINE_MS overrides it (opt-in, e.g. for tests).
|
|
50
|
+
const STOP_DEADLINE_MS = (() => {
|
|
51
|
+
const raw = Number(process.env.CW_BATCH_STOP_DEADLINE_MS);
|
|
52
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 8000;
|
|
53
|
+
})();
|
|
40
54
|
let stopSignalReceived = false;
|
|
41
55
|
function onStopSignal(signal) {
|
|
42
56
|
if (stopSignalReceived) {
|
|
@@ -47,11 +61,22 @@ function onStopSignal(signal) {
|
|
|
47
61
|
stopSignalReceived = true;
|
|
48
62
|
killAllChildren("SIGTERM");
|
|
49
63
|
setTimeout(() => killAllChildren("SIGKILL"), 5000).unref();
|
|
64
|
+
// unref'd on purpose: when the batch stops cleanly (every child settles and
|
|
65
|
+
// its stdout closes) the loop empties and this process exits promptly, so a
|
|
66
|
+
// graceful stop is never padded out to the full deadline. The timer only
|
|
67
|
+
// ever FIRES when something else — a wedged grandchild holding a pipe — is
|
|
68
|
+
// still keeping the loop alive; then it reaps any tracked stragglers and
|
|
69
|
+
// exits with the signal's conventional code.
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
killAllChildren("SIGKILL");
|
|
72
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
73
|
+
}, STOP_DEADLINE_MS).unref();
|
|
50
74
|
}
|
|
51
75
|
process.on("SIGINT", () => onStopSignal("SIGINT"));
|
|
52
76
|
process.on("SIGTERM", () => onStopSignal("SIGTERM"));
|
|
53
77
|
|
|
54
78
|
const { spawn } = require("node:child_process");
|
|
79
|
+
const { StringDecoder } = require("node:string_decoder");
|
|
55
80
|
let raw = "";
|
|
56
81
|
const MAX_STDIN_BYTES = 32 * 1024 * 1024;
|
|
57
82
|
process.stdin.setEncoding("utf8");
|
|
@@ -81,6 +106,31 @@ process.stdin.on("end", () => {
|
|
|
81
106
|
let stdoutBytes = 0;
|
|
82
107
|
let stdoutTruncated = false;
|
|
83
108
|
let settled = false;
|
|
109
|
+
// appendedBytes tracks only the raw bytes actually folded into `stdout`
|
|
110
|
+
// so far — an O(1) running counter. The old code re-ran
|
|
111
|
+
// Buffer.byteLength(stdout) over the WHOLE accumulated string on every
|
|
112
|
+
// chunk (O(size-so-far) per chunk, O(bytes^2) total for a chatty job),
|
|
113
|
+
// and all concurrently-running jobs in a batch share one event loop, so
|
|
114
|
+
// that rescan delayed every other job's stream/kill-timer handling too.
|
|
115
|
+
// decoder buffers any multi-byte UTF-8 sequence left incomplete at a
|
|
116
|
+
// chunk boundary and prepends it to the next write, so a character split
|
|
117
|
+
// across two stdout reads decodes correctly instead of the old
|
|
118
|
+
// per-chunk chunk.toString() turning each half into a replacement
|
|
119
|
+
// character (U+FFFD).
|
|
120
|
+
let appendedBytes = 0;
|
|
121
|
+
const decoder = new StringDecoder("utf8");
|
|
122
|
+
// Flush any bytes the decoder is still holding for an incomplete
|
|
123
|
+
// trailing multi-byte character. Must run before EVERY settle() call
|
|
124
|
+
// that reports non-empty stdout (both the "close" and "error" child
|
|
125
|
+
// events can be the one that fires first), not just one of them — a
|
|
126
|
+
// process can end via "error" before "close" (e.g. a post-spawn stream
|
|
127
|
+
// or kill error), and if only "close" flushed, those trailing bytes
|
|
128
|
+
// would be silently dropped instead of surfacing correctly or as an
|
|
129
|
+
// explicit replacement character.
|
|
130
|
+
const flushDecoder = () => {
|
|
131
|
+
const tail = decoder.end();
|
|
132
|
+
if (tail) stdout += tail;
|
|
133
|
+
};
|
|
84
134
|
const settle = (o) => {
|
|
85
135
|
if (settled) return;
|
|
86
136
|
settled = true;
|
|
@@ -96,7 +146,12 @@ process.stdin.on("end", () => {
|
|
|
96
146
|
};
|
|
97
147
|
let child;
|
|
98
148
|
try {
|
|
99
|
-
|
|
149
|
+
// stdin "ignore" (not the default inherited pipe): this child never
|
|
150
|
+
// feeds a job any stdin, but the default leaves each job's stdin a pipe
|
|
151
|
+
// we never write to and never close, so a vendor CLI that reads stdin to
|
|
152
|
+
// EOF blocks until its own timeout instead of getting an immediate EOF.
|
|
153
|
+
// Mirrors the serial path (agent.ts runAgentProcess stdio ["ignore",...]).
|
|
154
|
+
child = spawn(job.binary, job.args, { cwd: job.cwd, env: job.env || process.env, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
100
155
|
} catch (error) {
|
|
101
156
|
settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout: "" });
|
|
102
157
|
return;
|
|
@@ -108,18 +163,30 @@ process.stdin.on("end", () => {
|
|
|
108
163
|
const chunk = Buffer.isBuffer(d) ? d : Buffer.from(String(d));
|
|
109
164
|
stdoutBytes += chunk.length;
|
|
110
165
|
if (stdoutTruncated) return;
|
|
111
|
-
const remaining = CAP -
|
|
166
|
+
const remaining = CAP - appendedBytes;
|
|
112
167
|
if (remaining <= 0 || chunk.length > remaining) {
|
|
113
168
|
stdoutTruncated = true;
|
|
114
|
-
|
|
169
|
+
// The close handler's truncated branch always reports stdout:""
|
|
170
|
+
// (below), so this partial decode is dead weight on THAT path — but
|
|
171
|
+
// the error handler below does NOT discard stdout on truncation, so
|
|
172
|
+
// it is still decoded here for that path.
|
|
173
|
+
if (remaining > 0) stdout += decoder.write(chunk.subarray(0, remaining));
|
|
115
174
|
return;
|
|
116
175
|
}
|
|
117
|
-
stdout +=
|
|
176
|
+
stdout += decoder.write(chunk);
|
|
177
|
+
appendedBytes += chunk.length;
|
|
118
178
|
});
|
|
119
179
|
child.stderr.on("data", () => {});
|
|
120
180
|
child.on("error", (error) => {
|
|
121
181
|
clearTimeout(term); clearTimeout(kill);
|
|
122
182
|
children.delete(child);
|
|
183
|
+
// Flush the decoder here too, symmetric with the close handler below —
|
|
184
|
+
// "error" can fire instead of "close" (e.g. a post-spawn stream or
|
|
185
|
+
// kill error), and this path reports the accumulated `stdout` as-is
|
|
186
|
+
// (it does not discard it on truncation the way close does), so any
|
|
187
|
+
// bytes still held in the decoder for a not-yet-complete trailing
|
|
188
|
+
// multi-byte character must be flushed or they are silently lost.
|
|
189
|
+
flushDecoder();
|
|
123
190
|
settle({ spawnError: String((error && error.message) || error), exitCode: null, stdout });
|
|
124
191
|
});
|
|
125
192
|
child.on("close", (code) => {
|
|
@@ -129,6 +196,7 @@ process.stdin.on("end", () => {
|
|
|
129
196
|
settle({ spawnError: `stdout exceeded ${CAP} byte cap (${stdoutBytes} bytes)`, exitCode: null, stdout: "" });
|
|
130
197
|
return;
|
|
131
198
|
}
|
|
199
|
+
flushDecoder();
|
|
132
200
|
settle({ exitCode: typeof code === "number" ? code : null, stdout });
|
|
133
201
|
});
|
|
134
202
|
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// HTTP batch delegate child — the endpoint-mode sibling of batch-delegate-child.js.
|
|
5
|
+
// Spawned via `node <this-path>` (shell:false) by runEndpointBatchOutcomes when a
|
|
6
|
+
// concurrent drive round (`--concurrency N`) has one or more ENDPOINT-configured
|
|
7
|
+
// agents. Without it, endpoint tasks in a concurrent round each spawned their own
|
|
8
|
+
// blocking http-delegate-child one after another, so `--concurrency N` ran N
|
|
9
|
+
// endpoint delegations strictly SERIALLY. This child POSTs all N at once from a
|
|
10
|
+
// single process (one Node process holds N in-flight fetches trivially — no per-job
|
|
11
|
+
// child process is needed for HTTP the way it is for a vendor CLI).
|
|
12
|
+
//
|
|
13
|
+
// Reads a JSON array `[{ endpoint, job, timeoutMs }, ...]` on stdin, where `job` is
|
|
14
|
+
// the ALREADY-serialized POST body (byte-identical to the string the serial
|
|
15
|
+
// runAgentEndpoint builds, so the concurrent path posts the same bytes). Fires all
|
|
16
|
+
// jobs concurrently, each POST + optional jobId poll matching http-delegate-child.js,
|
|
17
|
+
// and streams ONE NDJSON line per job — `{i, spawnError?, exitCode, stdout}\n` — the
|
|
18
|
+
// INSTANT that job settles. `i` is the job's index (settle order is concurrent, not
|
|
19
|
+
// submission order, so the parent cannot map a line to its job without it). The line
|
|
20
|
+
// shape and the incremental-streaming reason are shared with batch-delegate-child.js:
|
|
21
|
+
// the parent (runEndpointBatchOutcomes) reuses reconcileBatchOutcomes to read them.
|
|
22
|
+
//
|
|
23
|
+
// THE RED LINE: this child speaks ONLY plain HTTP to the operator-configured
|
|
24
|
+
// endpoint. It imports NO model SDK, holds NO API key, and constructs NO model API
|
|
25
|
+
// request. It re-uses the caller's inherited env for any endpoint auth exactly as
|
|
26
|
+
// http-delegate-child.js does; it reads no credential itself.
|
|
27
|
+
|
|
28
|
+
const MAX_STDIN_BYTES = 32 * 1024 * 1024;
|
|
29
|
+
// Per-job caps mirror batch-delegate-child.js: CAP bounds each job's RAW stdout
|
|
30
|
+
// bytes; LINE_CAP bounds the ESCAPED NDJSON line (JSON escaping grows bytes), held
|
|
31
|
+
// under the parent's 34MB-per-job maxBuffer grant with a small margin so one job's
|
|
32
|
+
// line can never push the combined stream past the parent's buffer and ENOBUFS the
|
|
33
|
+
// whole batch.
|
|
34
|
+
const CAP = 32 * 1024 * 1024;
|
|
35
|
+
const LINE_CAP = 33 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
// In-flight fetch controllers so a stop signal aborts every open request at once.
|
|
38
|
+
// No grandchild processes exist here (unlike batch-delegate-child.js), so there is
|
|
39
|
+
// no wedged-pipe deadlock to guard — aborting the fetches empties the event loop
|
|
40
|
+
// and the process exits on its own.
|
|
41
|
+
const controllers = new Set();
|
|
42
|
+
let stopSignalReceived = false;
|
|
43
|
+
function onStopSignal(signal) {
|
|
44
|
+
if (stopSignalReceived) {
|
|
45
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
stopSignalReceived = true;
|
|
49
|
+
for (const controller of controllers) {
|
|
50
|
+
try { controller.abort(); } catch {}
|
|
51
|
+
}
|
|
52
|
+
// If aborting does not drain the loop promptly (a hung socket), force-exit.
|
|
53
|
+
setTimeout(() => process.exit(signal === "SIGINT" ? 130 : 143), 5000).unref();
|
|
54
|
+
}
|
|
55
|
+
process.on("SIGINT", () => onStopSignal("SIGINT"));
|
|
56
|
+
process.on("SIGTERM", () => onStopSignal("SIGTERM"));
|
|
57
|
+
|
|
58
|
+
function writeSettleLine(i, outcome) {
|
|
59
|
+
let line = JSON.stringify({ i, ...outcome }) + "\n";
|
|
60
|
+
if (Buffer.byteLength(line) > LINE_CAP) {
|
|
61
|
+
// Same fail-closed shape as the raw-cap path: capped output is never evidence,
|
|
62
|
+
// so drop it entirely and name the cap — never ship a line the parent's buffer
|
|
63
|
+
// cannot hold.
|
|
64
|
+
line = JSON.stringify({ i, spawnError: `serialized stdout line exceeded ${LINE_CAP} byte cap (${Buffer.byteLength(line)} bytes)`, exitCode: null, stdout: "" }) + "\n";
|
|
65
|
+
}
|
|
66
|
+
process.stdout.write(line);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// One job: POST the pre-serialized body, poll a returned jobId until done, and
|
|
70
|
+
// return { exitCode, stdout } or throw. Byte-for-byte the same POST + poll contract
|
|
71
|
+
// as http-delegate-child.js, plus a per-job deadline so one hung endpoint settles as
|
|
72
|
+
// its own error instead of pinning the whole batch to the parent's backstop timeout.
|
|
73
|
+
async function runOne(entry) {
|
|
74
|
+
const { endpoint, job, timeoutMs } = entry;
|
|
75
|
+
if (!endpoint) throw new Error("no endpoint");
|
|
76
|
+
const deadline = Date.now() + (Number(timeoutMs) > 0 ? Number(timeoutMs) : 600000);
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
controllers.add(controller);
|
|
79
|
+
const abortAt = setTimeout(() => { try { controller.abort(); } catch {} }, Math.max(0, deadline - Date.now()));
|
|
80
|
+
try {
|
|
81
|
+
const post = await fetch(endpoint, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "content-type": "application/json" },
|
|
84
|
+
body: typeof job === "string" ? job : JSON.stringify(job),
|
|
85
|
+
signal: controller.signal,
|
|
86
|
+
});
|
|
87
|
+
if (!post.ok) throw new Error("runner responded " + post.status);
|
|
88
|
+
let data = await post.json();
|
|
89
|
+
let guard = 0;
|
|
90
|
+
while (data && data.jobId && data.done !== true && guard++ < 600) {
|
|
91
|
+
if (Date.now() >= deadline) throw new Error("endpoint poll timed out");
|
|
92
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
93
|
+
const poll = await fetch(endpoint + (endpoint.includes("?") ? "&" : "?") + "jobId=" + encodeURIComponent(data.jobId), { signal: controller.signal });
|
|
94
|
+
if (!poll.ok) throw new Error("poll responded " + poll.status);
|
|
95
|
+
data = await poll.json();
|
|
96
|
+
}
|
|
97
|
+
if (typeof data.exitCode !== "number") throw new Error("runner did not report an exitCode");
|
|
98
|
+
let stdout = String(data.stdout || "");
|
|
99
|
+
if (Buffer.byteLength(stdout) > CAP) throw new Error(`stdout exceeded ${CAP} byte cap (${Buffer.byteLength(stdout)} bytes)`);
|
|
100
|
+
return { exitCode: data.exitCode, stdout };
|
|
101
|
+
} finally {
|
|
102
|
+
clearTimeout(abortAt);
|
|
103
|
+
controllers.delete(controller);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
(async () => {
|
|
108
|
+
// setEncoding("utf8") (as batch-delegate-child.js does), NOT `b += <Buffer>`:
|
|
109
|
+
// without it each data chunk is a raw Buffer coerced to a string on its own, so
|
|
110
|
+
// a multibyte char split across a ~64KB pipe-chunk boundary decodes to U+FFFD
|
|
111
|
+
// and the POSTed prompt is silently corrupted. With the encoding set, Node's
|
|
112
|
+
// StringDecoder carries partial bytes across chunk boundaries.
|
|
113
|
+
const read = () => new Promise((res) => { let b = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (c) => { if (b.length < MAX_STDIN_BYTES) b += c; }); process.stdin.on("end", () => res(b)); });
|
|
114
|
+
let jobs;
|
|
115
|
+
try {
|
|
116
|
+
jobs = JSON.parse((await read()) || "[]");
|
|
117
|
+
} catch (e) {
|
|
118
|
+
process.stdout.write(JSON.stringify([{ spawnError: `invalid stdin JSON: ${String((e && e.message) || e)}`, exitCode: null, stdout: "" }]));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!Array.isArray(jobs) || !jobs.length) { process.stdout.write("[]"); return; }
|
|
122
|
+
// allSettled, not Promise.all: one failing job never rejects the others, and
|
|
123
|
+
// every job's line is already written by runOne's settle below regardless.
|
|
124
|
+
await Promise.allSettled(jobs.map(async (entry, i) => {
|
|
125
|
+
try {
|
|
126
|
+
const out = await runOne(entry);
|
|
127
|
+
writeSettleLine(i, { exitCode: out.exitCode, stdout: out.stdout });
|
|
128
|
+
} catch (e) {
|
|
129
|
+
writeSettleLine(i, { spawnError: String((e && e.message) || e), exitCode: null, stdout: "" });
|
|
130
|
+
}
|
|
131
|
+
}));
|
|
132
|
+
})();
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
|
|
17
17
|
(async () => {
|
|
18
18
|
const MAX_STDIN_BYTES = 32 * 1024 * 1024;
|
|
19
|
+
// setEncoding("utf8") before reading: Node's StringDecoder then carries a
|
|
20
|
+
// multibyte UTF-8 char that straddles a ~64KB pipe-chunk boundary across the
|
|
21
|
+
// two chunks. Without it, each Buffer chunk was coerced to a string on its
|
|
22
|
+
// own and a split char decoded to U+FFFD on both sides — silently corrupting
|
|
23
|
+
// a > ~64KB non-ASCII job (this project's prompts are frequently Chinese), or
|
|
24
|
+
// making the outer JSON.parse throw so the delegation refused. Matches the
|
|
25
|
+
// batch-delegate-child.js reader.
|
|
26
|
+
process.stdin.setEncoding("utf8");
|
|
19
27
|
const read = () => new Promise((res) => { let b = ""; process.stdin.on("data", (c) => { if (b.length < MAX_STDIN_BYTES) b += c; }); process.stdin.on("end", () => res(b)); });
|
|
20
28
|
try {
|
|
21
29
|
const job = JSON.parse((await read()) || "{}");
|
|
@@ -6,7 +6,7 @@ const fs = require("node:fs");
|
|
|
6
6
|
const path = require("node:path");
|
|
7
7
|
const { CoolWorkflowRunner } = require("../dist/shell/orchestrator.js");
|
|
8
8
|
|
|
9
|
-
const TARGET_VERSION = "0.2.
|
|
9
|
+
const TARGET_VERSION = "0.2.6";
|
|
10
10
|
const PREVIOUS_VERSION = "0.1.31";
|
|
11
11
|
const pluginRoot = path.resolve(__dirname, "..");
|
|
12
12
|
const repoRoot = path.resolve(pluginRoot, "..", "..");
|
|
@@ -291,7 +291,7 @@ function commandsForTask(taskId, context) {
|
|
|
291
291
|
"-e",
|
|
292
292
|
[
|
|
293
293
|
releaseSourceReaderSnippet(),
|
|
294
|
-
"const files=['
|
|
294
|
+
"const files=['plugins/cool-workflow/docs/dogfood-one-real-repo.7.md','README.md','plugins/cool-workflow/README.md','CHANGELOG.md','RELEASE.md'];",
|
|
295
295
|
"for (const f of files) { if (!surfaceExists(f)) throw new Error('missing '+f); }",
|
|
296
296
|
"const changelog=readSurface('CHANGELOG.md');",
|
|
297
297
|
"if (changelog===null) throw new Error('CHANGELOG.md missing from release commit');",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
// fake-date-for-reproduction.js — a Node `--require` preload used ONLY by
|
|
4
|
-
// verify-bump-reproduction.
|
|
4
|
+
// verify-bump-reproduction.js. It pins `new Date()` (no-args construction)
|
|
5
5
|
// and `Date.now()` to a fixed instant, read from CW_FAKE_DATE (an ISO date or
|
|
6
6
|
// YYYY-MM-DD string). `new Date(explicit args)` is left untouched.
|
|
7
7
|
//
|
package/scripts/gen-manifests.js
CHANGED
|
@@ -110,82 +110,47 @@ function _resolveTemplate(template, src, pluginRootVar) {
|
|
|
110
110
|
|
|
111
111
|
function build(src) {
|
|
112
112
|
const { targets, vendors } = src;
|
|
113
|
-
|
|
114
|
-
if (!vendors || typeof vendors !== "object" || Object.keys(vendors).length === 0) {
|
|
115
|
-
return buildLegacy(src);
|
|
116
|
-
}
|
|
113
|
+
validateVendorRegistry(vendors);
|
|
117
114
|
const outputs = [];
|
|
118
115
|
for (const [vendorId, vendorDef] of Object.entries(vendors)) {
|
|
119
116
|
const targetConfig = targets && targets[vendorId] ? targets[vendorId] : {};
|
|
120
117
|
const pluginRootVar = targetConfig.pluginRootVar || "./";
|
|
121
|
-
const vendorOutputs = vendorDef.outputs
|
|
118
|
+
const vendorOutputs = vendorDef.outputs;
|
|
122
119
|
for (const output of vendorOutputs) {
|
|
123
120
|
const resolved = _resolveTemplate(output, src, pluginRootVar);
|
|
121
|
+
if (!isPlainObject(resolved) || typeof resolved.path !== "string" || !resolved.path.trim()) {
|
|
122
|
+
throw new Error(`vendor ${vendorId} has an output without a path`);
|
|
123
|
+
}
|
|
124
|
+
if (!isPlainObject(resolved.json)) {
|
|
125
|
+
throw new Error(`vendor ${vendorId} output ${resolved.path} has no JSON object`);
|
|
126
|
+
}
|
|
124
127
|
outputs.push({
|
|
125
|
-
path:
|
|
126
|
-
json: resolved.json
|
|
128
|
+
path: resolved.path,
|
|
129
|
+
json: resolved.json
|
|
127
130
|
});
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
return outputs;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const { identity, descriptions, interface: ui, layout, mcp, targets } = src;
|
|
136
|
-
const outputs = [];
|
|
137
|
-
|
|
138
|
-
if (targets && targets.claude) {
|
|
139
|
-
outputs.push({
|
|
140
|
-
path: targets.claude.marketplace,
|
|
141
|
-
json: { name: identity.name, owner: identity.author, metadata: { description: descriptions.standard, version: identity.version }, plugins: [{ name: identity.name, source: layout.pluginPathFromRepoRoot, description: descriptions.standard, category: ui.category.toLowerCase() }] }
|
|
142
|
-
});
|
|
143
|
-
outputs.push({
|
|
144
|
-
path: targets.claude.plugin,
|
|
145
|
-
json: { name: identity.name, description: descriptions.standard, version: identity.version, author: identity.author, homepage: identity.homepage, license: identity.license, keywords: identity.keywords }
|
|
146
|
-
});
|
|
147
|
-
outputs.push({
|
|
148
|
-
path: targets.claude.mcp,
|
|
149
|
-
json: legacyMcpConfig(mcp, layout, targets.claude.pluginRootVar)
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
if (targets && targets.codex) {
|
|
153
|
-
outputs.push({
|
|
154
|
-
path: targets.codex.marketplace,
|
|
155
|
-
json: { name: identity.name, interface: { displayName: ui.displayName }, plugins: [{ name: identity.name, source: { source: "local", path: layout.pluginPathFromRepoRoot }, policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }, category: ui.category }] }
|
|
156
|
-
});
|
|
157
|
-
outputs.push({
|
|
158
|
-
path: targets.codex.plugin,
|
|
159
|
-
json: { name: identity.name, version: identity.version, description: descriptions.standard, author: identity.author, keywords: identity.keywords, skills: layout.skillsDir, mcpServers: "./.codex-plugin/mcp.json", interface: { displayName: ui.displayName, shortDescription: descriptions.short, longDescription: descriptions.long, developerName: identity.author.name, category: ui.category, capabilities: ui.capabilities, brandColor: ui.brandColor, defaultPrompt: ui.defaultPrompt } }
|
|
160
|
-
});
|
|
161
|
-
outputs.push({
|
|
162
|
-
path: targets.codex.mcp,
|
|
163
|
-
json: legacyMcpConfig(mcp, layout, targets.codex.pluginRootVar)
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
if (targets && targets.agents) {
|
|
167
|
-
outputs.push({
|
|
168
|
-
path: targets.agents.plugin,
|
|
169
|
-
json: { name: identity.name, description: descriptions.standard, version: identity.version, author: identity.author, homepage: identity.homepage, license: identity.license, keywords: identity.keywords, skills: layout.skillsDir, mcpServers: "./.codex-plugin/mcp.json", interface: { displayName: ui.displayName, shortDescription: descriptions.short, longDescription: descriptions.long, developerName: identity.author.name, category: ui.category, capabilities: ui.capabilities, brandColor: ui.brandColor, defaultPrompt: ui.defaultPrompt } }
|
|
170
|
-
});
|
|
171
|
-
outputs.push({
|
|
172
|
-
path: targets.agents.mcp,
|
|
173
|
-
json: legacyMcpConfig(mcp, layout, targets.agents.pluginRootVar)
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return outputs;
|
|
136
|
+
function isPlainObject(value) {
|
|
137
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
178
138
|
}
|
|
179
139
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
140
|
+
/** The vendor table is required. A broken source must stop before the write
|
|
141
|
+
* loop; an old hard-coded shape is not a safe second source of truth. */
|
|
142
|
+
function validateVendorRegistry(vendors) {
|
|
143
|
+
if (!isPlainObject(vendors) || Object.keys(vendors).length === 0) {
|
|
144
|
+
throw new Error("manifest source needs a non-empty vendors object");
|
|
145
|
+
}
|
|
146
|
+
for (const [vendorId, vendorDef] of Object.entries(vendors)) {
|
|
147
|
+
if (!isPlainObject(vendorDef) || !Array.isArray(vendorDef.outputs) || vendorDef.outputs.length === 0) {
|
|
148
|
+
throw new Error(`vendor ${vendorId} needs a non-empty outputs array`);
|
|
149
|
+
}
|
|
150
|
+
for (const output of vendorDef.outputs) {
|
|
151
|
+
if (!isPlainObject(output)) throw new Error(`vendor ${vendorId} has an invalid output`);
|
|
187
152
|
}
|
|
188
|
-
}
|
|
153
|
+
}
|
|
189
154
|
}
|
|
190
155
|
|
|
191
156
|
function serialize(json) {
|
|
@@ -229,4 +194,10 @@ function main() {
|
|
|
229
194
|
}
|
|
230
195
|
}
|
|
231
196
|
|
|
232
|
-
|
|
197
|
+
try {
|
|
198
|
+
main();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
+
process.stderr.write(`gen-manifests: ${message}\n`);
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
}
|