cool-workflow 0.2.2 → 0.2.4
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 +13 -6
- package/dist/cli/entry.js +41 -1
- package/dist/cli/io.js +12 -22
- package/dist/cli/parseargv.js +1 -0
- package/dist/core/capability-data.js +3 -2
- package/dist/core/format/completion.js +68 -0
- package/dist/core/format/help.js +31 -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/cli-args.js +33 -0
- 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 +251 -49
- package/dist/shell/eval-text.js +2 -2
- package/dist/shell/evidence-reasoning.js +4 -4
- package/dist/shell/execution-backend/agent.js +30 -2
- package/dist/shell/execution-backend/container.js +4 -1
- package/dist/shell/execution-backend/local.js +19 -11
- 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 +107 -42
- package/dist/shell/reclamation-io.js +88 -10
- package/dist/shell/registry-cli.js +23 -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 +78 -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/shell/workflow-app-loader.js +67 -1
- package/dist/wiring/capability-table/basics.js +33 -8
- package/dist/wiring/capability-table/exec-backend.js +28 -19
- package/dist/wiring/capability-table/multi-agent.js +187 -180
- package/dist/wiring/capability-table/parity.js +1 -0
- package/dist/wiring/capability-table/pipeline.js +64 -52
- package/dist/wiring/capability-table/registry-core.js +30 -11
- package/dist/wiring/capability-table/reporting.js +147 -106
- package/dist/wiring/capability-table/scheduling-registry.js +201 -167
- package/dist/wiring/capability-table/state.js +59 -51
- package/dist/wiring/capability-table/trust-ledger.js +52 -28
- package/dist/wiring/capability-table/workflow-apps.js +85 -70
- package/docs/agent-delegation-drive.7.md +4 -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 +26 -16
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -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 +4 -0
- package/docs/end-to-end-golden-path.7.md +14 -14
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/getting-started.md +37 -37
- package/docs/multi-agent-cli-mcp-surface.7.md +21 -17
- package/docs/multi-agent-eval-replay-harness.7.md +17 -13
- package/docs/multi-agent-operator-ux.7.md +23 -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 +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/operator-ux.7.md +34 -34
- package/docs/pipeline-runner.7.md +4 -4
- package/docs/project-index.md +31 -4
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +5 -1
- package/docs/release-tooling.7.md +22 -0
- package/docs/routines.md +4 -4
- package/docs/run-registry-control-plane.7.md +23 -19
- package/docs/run-retention-reclamation.7.md +24 -3
- package/docs/scheduled-tasks.md +14 -14
- package/docs/security-trust-hardening.7.md +15 -15
- package/docs/state-explosion-management.7.md +14 -10
- package/docs/team-collaboration.7.md +4 -0
- package/docs/trust-audit-anchor.7.md +4 -0
- package/docs/unix-principles.md +3 -1
- package/docs/verifier-gated-commit.7.md +1 -1
- package/docs/web-desktop-workbench.7.md +4 -0
- package/docs/workflow-app-framework.7.md +13 -13
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +3 -2
- package/scripts/agents/claude-p-agent.js +2 -2
- package/scripts/block-unapproved-tag.sh +42 -6
- package/scripts/bump-version.js +27 -10
- 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 +0 -27
- package/scripts/release-flow.js +194 -13
- package/scripts/release-oneclick.js +407 -0
- package/scripts/verdict-keygen.js +83 -0
- package/scripts/verify-bump-reproduction.sh +148 -0
- package/scripts/verify-verdict-signature.js +61 -0
- package/scripts/version-sync-check.js +39 -22
- 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
|
@@ -60,6 +60,7 @@ exports.trustAuditGenesis = trustAuditGenesis;
|
|
|
60
60
|
exports.listTrustAuditEvents = listTrustAuditEvents;
|
|
61
61
|
exports.trustAuditHead = trustAuditHead;
|
|
62
62
|
exports.verifyTrustAudit = verifyTrustAudit;
|
|
63
|
+
exports.repairTrustAuditTornTail = repairTrustAuditTornTail;
|
|
63
64
|
exports.recordTrustAuditEvent = recordTrustAuditEvent;
|
|
64
65
|
exports.recordSandboxPathDecision = recordSandboxPathDecision;
|
|
65
66
|
exports.normalizeEvidence = normalizeEvidence;
|
|
@@ -106,6 +107,43 @@ function computeEventHash(event) {
|
|
|
106
107
|
void eventHash;
|
|
107
108
|
return (0, hash_1.sha256)((0, hash_1.eventHashInput)(rest));
|
|
108
109
|
}
|
|
110
|
+
function tailCachePathFor(eventLogPath) {
|
|
111
|
+
return path.join(path.dirname(eventLogPath), "tail-cache.json");
|
|
112
|
+
}
|
|
113
|
+
function readAuditTailCache(tailCachePath) {
|
|
114
|
+
if (!fs.existsSync(tailCachePath))
|
|
115
|
+
return undefined;
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(fs.readFileSync(tailCachePath, "utf8"));
|
|
118
|
+
if (parsed && parsed.schemaVersion === 1 && typeof parsed.logBytes === "number" && typeof parsed.count === "number" && typeof parsed.lastHash === "string") {
|
|
119
|
+
return parsed;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Corrupt/unreadable cache -- fall back to the full parse below.
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
function writeAuditTailCache(tailCachePath, cache) {
|
|
128
|
+
try {
|
|
129
|
+
(0, fs_atomic_1.writeJson)(tailCachePath, cache);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Best-effort: a failed cache write must never break the real append.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Deletes the tail cache so the NEXT append always re-derives ground
|
|
136
|
+
* truth from the real log, rather than trusting a byte-size coincidence.
|
|
137
|
+
* Called whenever something OTHER than a plain append changes the log's
|
|
138
|
+
* content (currently: repair). */
|
|
139
|
+
function invalidateAuditTailCache(eventLogPath) {
|
|
140
|
+
try {
|
|
141
|
+
fs.unlinkSync(tailCachePathFor(eventLogPath));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Nothing to invalidate (no cache existed yet) -- fine.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
109
147
|
/** Read events in FILE (append) order, tolerating corrupt lines — one
|
|
110
148
|
* bad line must not brick the whole audit read surface (it is counted,
|
|
111
149
|
* not thrown). The chain links append order, so this is the order
|
|
@@ -170,16 +208,17 @@ function trustAuditHead(run) {
|
|
|
170
208
|
* the hash after each event) must contain `expectHead`, and the log
|
|
171
209
|
* must reach `expectCount` events; a shortfall fails closed with
|
|
172
210
|
* `trust-audit-truncated`. Without an anchor, behavior is unchanged. */
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
211
|
+
/** Chain-walk core shared by `verifyTrustAudit` (reads from disk) and
|
|
212
|
+
* `repairTrustAuditTornTail` (re-checks an in-memory candidate result
|
|
213
|
+
* BEFORE ever writing it to disk). Pure — no fs. */
|
|
214
|
+
function verifyEventsChain(runId, events, corruptLines, anchor) {
|
|
176
215
|
const checks = [];
|
|
177
216
|
let verified = corruptLines === 0;
|
|
178
217
|
if (corruptLines > 0)
|
|
179
218
|
checks.push({ name: "parse", pass: false, code: "trust-audit-corrupt-line" });
|
|
180
219
|
let chained = 0;
|
|
181
220
|
let unchained = 0;
|
|
182
|
-
let expectedPrev = trustAuditGenesis(
|
|
221
|
+
let expectedPrev = trustAuditGenesis(runId);
|
|
183
222
|
const headTrail = new Set([expectedPrev]);
|
|
184
223
|
for (let i = 0; i < events.length; i++) {
|
|
185
224
|
const event = events[i];
|
|
@@ -195,7 +234,11 @@ function verifyTrustAudit(run, anchor) {
|
|
|
195
234
|
verified = false;
|
|
196
235
|
checks.push({ name: `event-hash[${i}]`, pass: false, code: "trust-audit-digest-mismatch" });
|
|
197
236
|
}
|
|
198
|
-
|
|
237
|
+
// No `undefined` skip here: the writer ALWAYS sets prevEventHash (the
|
|
238
|
+
// first event gets the genesis hash), so a chained event without it is
|
|
239
|
+
// a forgery — dropping the field and re-making eventHash must NOT let a
|
|
240
|
+
// cut or re-ordered chain verify green. Fail closed on the mismatch.
|
|
241
|
+
if (event.prevEventHash !== expectedPrev) {
|
|
199
242
|
verified = false;
|
|
200
243
|
checks.push({ name: `chain-link[${i}]`, pass: false, code: "trust-audit-chain-broken" });
|
|
201
244
|
}
|
|
@@ -223,6 +266,94 @@ function verifyTrustAudit(run, anchor) {
|
|
|
223
266
|
}
|
|
224
267
|
return { present: events.length > 0, verified, eventCount: events.length, chained, unchained, corruptLines, checks };
|
|
225
268
|
}
|
|
269
|
+
function verifyTrustAudit(run, anchor) {
|
|
270
|
+
const audit = ensureTrustAudit(run);
|
|
271
|
+
const { events, corruptLines } = readEventsRawCounted(audit.eventLogPath);
|
|
272
|
+
return verifyEventsChain(run.id, events, corruptLines, anchor);
|
|
273
|
+
}
|
|
274
|
+
/** Repairs a torn TRAILING write in the audit event log — the ONE
|
|
275
|
+
* corruption shape a crash mid-append can produce (`durableAppendFileSync`
|
|
276
|
+
* only ever adds bytes at the current end of file, so an interruption can
|
|
277
|
+
* only ever leave the LAST append incomplete; it can never touch earlier,
|
|
278
|
+
* already-flushed lines). Every line is always actually parsed — the
|
|
279
|
+
* file's trailing-newline byte says only whether the LAST WRITE
|
|
280
|
+
* completed, nothing about whether any line parses: if a run is RESUMED
|
|
281
|
+
* after a torn write (another event appended right after the garbled
|
|
282
|
+
* remnant, with no separating newline of its own), the two merge into one
|
|
283
|
+
* unparseable line and the file ends in a newline again, even though
|
|
284
|
+
* content is still corrupt. Refuses (`outcome: "refused"`) rather than
|
|
285
|
+
* touching anything when:
|
|
286
|
+
* - more than one line is unparseable, or the sole bad line is NOT the
|
|
287
|
+
* last one (not a shape a crash can produce — treated as possible
|
|
288
|
+
* tampering, not auto-repaired);
|
|
289
|
+
* - removing the bad trailing line still leaves an unverifiable chain,
|
|
290
|
+
* OR (when `anchor` is given) the repaired chain doesn't reach
|
|
291
|
+
* `anchor.expectCount`/contain `anchor.expectHead` — an anchor
|
|
292
|
+
* captured before the corruption is the ONLY way to catch an attacker
|
|
293
|
+
* deleting real historical events and leaving a torn-looking fragment
|
|
294
|
+
* behind (an empty/short chain otherwise "verifies" trivially — the
|
|
295
|
+
* same documented blind spot `verifyTrustAudit` itself has without an
|
|
296
|
+
* anchor; this function must never launder that shape into a
|
|
297
|
+
* confidently-"repaired" empty log).
|
|
298
|
+
* `write: false` (default) reports what WOULD happen without touching
|
|
299
|
+
* disk, matching this codebase's `cw state check [--write]` convention.
|
|
300
|
+
* Held under the SAME `withFileLock` as `recordTrustAuditEvent` (below),
|
|
301
|
+
* so a repair can never interleave with a live append. */
|
|
302
|
+
function repairTrustAuditTornTail(run, options = {}) {
|
|
303
|
+
const audit = ensureTrustAudit(run);
|
|
304
|
+
return (0, fs_atomic_1.withFileLock)(audit.eventLogPath, () => {
|
|
305
|
+
const raw = fs.readFileSync(audit.eventLogPath, "utf8");
|
|
306
|
+
const lines = raw.split("\n").filter((line) => line.trim() !== "");
|
|
307
|
+
const badIndexes = [];
|
|
308
|
+
const events = [];
|
|
309
|
+
lines.forEach((line, i) => {
|
|
310
|
+
try {
|
|
311
|
+
events.push(JSON.parse(line));
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
badIndexes.push(i);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
if (badIndexes.length === 0) {
|
|
318
|
+
return { outcome: "clean", reason: "every line parses — no torn trailing write to repair" };
|
|
319
|
+
}
|
|
320
|
+
if (badIndexes.length > 1 || badIndexes[0] !== lines.length - 1) {
|
|
321
|
+
return {
|
|
322
|
+
outcome: "refused",
|
|
323
|
+
reason: "corruption is not confined to exactly the trailing line — this is not a shape a crash mid-append can produce and will not be auto-repaired (looks like tampering)",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// `events` already holds every line EXCEPT the one bad trailing line
|
|
327
|
+
// (JSON.parse threw for it, so nothing was pushed) — exactly the "good"
|
|
328
|
+
// set, in file order.
|
|
329
|
+
const recheck = verifyEventsChain(run.id, events, 0, options.anchor);
|
|
330
|
+
if (!recheck.verified) {
|
|
331
|
+
return {
|
|
332
|
+
outcome: "refused",
|
|
333
|
+
reason: options.anchor
|
|
334
|
+
? "removing the torn trailing write still doesn't reach the given --expect-head/--expect-count anchor — refusing to repair (this looks like deleted history, not a crash)"
|
|
335
|
+
: "removing the torn trailing write still leaves an unverifiable chain — refusing to repair (this looks like tampering, not a crash)",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const removedBytes = Buffer.byteLength(lines[lines.length - 1], "utf8");
|
|
339
|
+
const repairedContent = events.length > 0 ? `${lines.slice(0, -1).join("\n")}\n` : "";
|
|
340
|
+
if (options.write) {
|
|
341
|
+
(0, fs_atomic_1.writeTextDurable)(audit.eventLogPath, repairedContent, { durable: true });
|
|
342
|
+
// The log's bytes just changed out from under the append tail cache
|
|
343
|
+
// (perf cycle P1-2) -- invalidate rather than rely on the size check
|
|
344
|
+
// alone catching every case.
|
|
345
|
+
invalidateAuditTailCache(audit.eventLogPath);
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
outcome: "repaired",
|
|
349
|
+
reason: options.write
|
|
350
|
+
? `removed a torn trailing write (${removedBytes} bytes) and restored a verified chain of ${events.length} event(s)`
|
|
351
|
+
: `would remove a torn trailing write (${removedBytes} bytes) and restore a verified chain of ${events.length} event(s) — pass --write to apply`,
|
|
352
|
+
removedLines: 1,
|
|
353
|
+
removedBytes,
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
}
|
|
226
357
|
function unique(values) {
|
|
227
358
|
return Array.from(new Set(values.filter(Boolean)));
|
|
228
359
|
}
|
|
@@ -249,9 +380,14 @@ function scrubMetadata(value) {
|
|
|
249
380
|
function compact(value) {
|
|
250
381
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
251
382
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
383
|
+
/** `count` must be the number of PRIOR events, read under the SAME lock
|
|
384
|
+
* the append itself happens under (see `recordTrustAuditEvent`) — an
|
|
385
|
+
* earlier version read this count separately, outside any lock, so two
|
|
386
|
+
* concurrent writers could mint the SAME id for two different events
|
|
387
|
+
* (the hash chain still forked-proof, but the id — referenced elsewhere
|
|
388
|
+
* as `auditEventIds`/`parentEventIds` for provenance — was not unique). */
|
|
389
|
+
function createEventId(kind, count) {
|
|
390
|
+
return `audit-${(0, fs_atomic_1.safeFileName)(kind)}-${String(count + 1).padStart(4, "0")}`;
|
|
255
391
|
}
|
|
256
392
|
/** Correlation-id keys copied verbatim (and no others) — byte-exact list/
|
|
257
393
|
* order to the old build's CORRELATION_ID_FIELDS. */
|
|
@@ -294,11 +430,20 @@ function indexCorrelationIds(event) {
|
|
|
294
430
|
}
|
|
295
431
|
return picked;
|
|
296
432
|
}
|
|
433
|
+
/** Read-modify-append: computes `prevEventHash` from the CURRENT last event
|
|
434
|
+
* and appends. Held under `withFileLock` (like every other read-modify-
|
|
435
|
+
* write in this codebase) so two processes recording events for the same
|
|
436
|
+
* run at once can never both read the same tail and compute the same
|
|
437
|
+
* `prevEventHash` — that would fork the hash chain, and a forked chain
|
|
438
|
+
* fails `verifyTrustAudit` for good, with no repair for THAT shape (unlike
|
|
439
|
+
* a torn trailing write, a fork is not confined to the last line). */
|
|
297
440
|
function recordTrustAuditEvent(run, input) {
|
|
298
441
|
const audit = ensureTrustAudit(run);
|
|
442
|
+
// `id` is NOT set here — it depends on the prior event count, which (like
|
|
443
|
+
// prevEventHash) must be read under the lock below, or two concurrent
|
|
444
|
+
// writers could mint the same id for two different events.
|
|
299
445
|
const event = compact({
|
|
300
446
|
schemaVersion: exports.TRUST_AUDIT_SCHEMA_VERSION,
|
|
301
|
-
id: createEventId(run, input.kind),
|
|
302
447
|
createdAt: new Date().toISOString(),
|
|
303
448
|
runId: run.id,
|
|
304
449
|
kind: input.kind,
|
|
@@ -322,11 +467,35 @@ function recordTrustAuditEvent(run, input) {
|
|
|
322
467
|
parentEventIds: unique(input.parentEventIds || []).sort(),
|
|
323
468
|
metadata: scrubMetadata(input.metadata || {}),
|
|
324
469
|
});
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
470
|
+
return (0, fs_atomic_1.withFileLock)(audit.eventLogPath, () => {
|
|
471
|
+
// The prior event count and last-event hash are the ONLY two things
|
|
472
|
+
// this append needs from the existing log. A tail cache (keyed on the
|
|
473
|
+
// log's own byte size) serves both without a full parse when nothing
|
|
474
|
+
// else has touched the log since it was written; any size mismatch
|
|
475
|
+
// (a repair, a torn write, this being the very first append) falls
|
|
476
|
+
// back to the full parse, same as before this cache existed.
|
|
477
|
+
const tailCachePath = tailCachePathFor(audit.eventLogPath);
|
|
478
|
+
const currentBytes = fs.existsSync(audit.eventLogPath) ? fs.statSync(audit.eventLogPath).size : 0;
|
|
479
|
+
const cached = readAuditTailCache(tailCachePath);
|
|
480
|
+
let count;
|
|
481
|
+
let prevHash;
|
|
482
|
+
if (cached && cached.logBytes === currentBytes) {
|
|
483
|
+
count = cached.count;
|
|
484
|
+
prevHash = cached.lastHash;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
const prior = readEventsRaw(audit.eventLogPath);
|
|
488
|
+
count = prior.length;
|
|
489
|
+
prevHash = prior.length ? prior[prior.length - 1].eventHash || computeEventHash(prior[prior.length - 1]) : trustAuditGenesis(run.id);
|
|
490
|
+
}
|
|
491
|
+
event.id = createEventId(input.kind, count);
|
|
492
|
+
event.prevEventHash = prevHash;
|
|
493
|
+
event.eventHash = computeEventHash(event);
|
|
494
|
+
const line = `${JSON.stringify(event)}\n`;
|
|
495
|
+
(0, fs_atomic_1.durableAppendFileSync)(audit.eventLogPath, line);
|
|
496
|
+
writeAuditTailCache(tailCachePath, { schemaVersion: 1, logBytes: currentBytes + Buffer.byteLength(line, "utf8"), count: count + 1, lastHash: event.eventHash });
|
|
497
|
+
return event;
|
|
498
|
+
});
|
|
330
499
|
}
|
|
331
500
|
function recordSandboxPathDecision(run, input) {
|
|
332
501
|
return recordTrustAuditEvent(run, {
|
|
@@ -387,11 +556,32 @@ function countBy(values, key) {
|
|
|
387
556
|
* here — the old build's extra workers/candidates/commits/multiAgent/
|
|
388
557
|
* blackboard rollups are milestone 9's own summarizeMultiAgent/
|
|
389
558
|
* candidate-scoring-io/coordinator-io surfaces, not duplicated here. */
|
|
559
|
+
/** Groups events by a key field once, so each row below is an O(1) Map
|
|
560
|
+
* lookup instead of an O(events) `.filter()` re-scan per id -- found
|
|
561
|
+
* alongside the id.find() version of this same shape while pinning perf
|
|
562
|
+
* cycle P1-1's review-fix regression test (O(ids x events) otherwise). */
|
|
563
|
+
function groupEventsBy(events, key) {
|
|
564
|
+
const groups = new Map();
|
|
565
|
+
for (const event of events) {
|
|
566
|
+
const k = key(event);
|
|
567
|
+
if (!k)
|
|
568
|
+
continue;
|
|
569
|
+
const list = groups.get(k);
|
|
570
|
+
if (list)
|
|
571
|
+
list.push(event);
|
|
572
|
+
else
|
|
573
|
+
groups.set(k, [event]);
|
|
574
|
+
}
|
|
575
|
+
return groups;
|
|
576
|
+
}
|
|
390
577
|
function workerRows(events, run) {
|
|
391
|
-
const
|
|
578
|
+
const workers = run.workers || [];
|
|
579
|
+
const workersById = new Map(workers.map((w) => [w.id, w]));
|
|
580
|
+
const eventsByWorkerId = groupEventsBy(events, (e) => e.workerId);
|
|
581
|
+
const workerIds = unique([...workers.map((w) => w.id), ...events.map((e) => e.workerId || "")]).sort();
|
|
392
582
|
return workerIds.filter(Boolean).map((workerId) => {
|
|
393
|
-
const worker =
|
|
394
|
-
const scoped =
|
|
583
|
+
const worker = workersById.get(workerId);
|
|
584
|
+
const scoped = eventsByWorkerId.get(workerId) || [];
|
|
395
585
|
return {
|
|
396
586
|
workerId,
|
|
397
587
|
taskId: worker?.taskId || scoped.find((e) => e.taskId)?.taskId,
|
|
@@ -405,11 +595,21 @@ function workerRows(events, run) {
|
|
|
405
595
|
function candidateRows(events, run) {
|
|
406
596
|
const cands = run.candidates || [];
|
|
407
597
|
const selectionsAll = run.candidateSelections || [];
|
|
598
|
+
const candsById = new Map(cands.map((c) => [c.id, c]));
|
|
599
|
+
const selectionsByCandidateId = new Map();
|
|
600
|
+
for (const selection of selectionsAll) {
|
|
601
|
+
const list = selectionsByCandidateId.get(selection.candidateId);
|
|
602
|
+
if (list)
|
|
603
|
+
list.push(selection);
|
|
604
|
+
else
|
|
605
|
+
selectionsByCandidateId.set(selection.candidateId, [selection]);
|
|
606
|
+
}
|
|
607
|
+
const eventsByCandidateId = groupEventsBy(events, (e) => e.candidateId);
|
|
408
608
|
const ids = unique([...cands.map((c) => c.id), ...events.map((e) => e.candidateId || "")]).sort();
|
|
409
609
|
return ids.filter(Boolean).map((candidateId) => {
|
|
410
|
-
const candidate =
|
|
411
|
-
const selections =
|
|
412
|
-
const scoped =
|
|
610
|
+
const candidate = candsById.get(candidateId);
|
|
611
|
+
const selections = selectionsByCandidateId.get(candidateId) || [];
|
|
612
|
+
const scoped = eventsByCandidateId.get(candidateId) || [];
|
|
413
613
|
return {
|
|
414
614
|
candidateId,
|
|
415
615
|
scoreIds: unique([...(candidate?.scores || []), ...scoped.map((e) => e.scoreId || "")]).filter(Boolean).sort(),
|
|
@@ -419,9 +619,11 @@ function candidateRows(events, run) {
|
|
|
419
619
|
});
|
|
420
620
|
}
|
|
421
621
|
function commitRows(events, run) {
|
|
422
|
-
const
|
|
622
|
+
const commits = run.commits || [];
|
|
623
|
+
const commitsById = new Map(commits.map((c) => [c.id, c]));
|
|
624
|
+
const ids = unique([...commits.map((c) => c.id), ...events.map((e) => e.commitId || "")]).sort();
|
|
423
625
|
return ids.filter(Boolean).map((commitId) => {
|
|
424
|
-
const commit =
|
|
626
|
+
const commit = commitsById.get(commitId);
|
|
425
627
|
return {
|
|
426
628
|
commitId,
|
|
427
629
|
verifierGated: Boolean(commit?.verifierGated),
|
|
@@ -209,7 +209,7 @@ function summarizeMultiAgentTrust(run) {
|
|
|
209
209
|
judgeRationales: byKind("judge.rationale"),
|
|
210
210
|
panelDecisions: byKind("judge.panel-decision"),
|
|
211
211
|
policyViolations,
|
|
212
|
-
nextAction: policyViolations.length ? `
|
|
212
|
+
nextAction: policyViolations.length ? `cw audit policy ${run.id}` : `cw audit multi-agent ${run.id} --json`,
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
function hasAcceptedJudgeRationale(run, input = {}) {
|
package/dist/shell/worker-cli.js
CHANGED
|
@@ -77,13 +77,14 @@ function workerShowCli(args) {
|
|
|
77
77
|
return scope;
|
|
78
78
|
}
|
|
79
79
|
function workerManifestCli(args) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
return (0, run_store_1.withRunStateLock)(req(args.runId, "run id"), cwdFor(args), (run) => {
|
|
81
|
+
const scope = (0, worker_isolation_1.getWorkerScope)(run, req(args.workerId, "worker id"));
|
|
82
|
+
if (!scope)
|
|
83
|
+
throw new Error(`Unknown worker for run ${run.id}: ${args.workerId}`);
|
|
84
|
+
const manifest = (0, worker_isolation_1.writeWorkerManifest)(run, scope);
|
|
85
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
86
|
+
return manifest;
|
|
87
|
+
});
|
|
87
88
|
}
|
|
88
89
|
/** `cw worker output <run> <worker> <result>` — records the worker's result
|
|
89
90
|
* and returns the full RunSummary, a byte-behavior port of the old build's
|
|
@@ -96,37 +97,40 @@ function workerManifestCli(args) {
|
|
|
96
97
|
* tasks.completed, workers.byStatus, and loopStage. The drive loop does these
|
|
97
98
|
* same steps itself around the bare accept, so it never routes through here. */
|
|
98
99
|
function workerOutputCli(args) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
return (0, run_store_1.withRunStateLock)(req(args.runId, "run id"), cwdFor(args), (run) => {
|
|
101
|
+
(0, worker_isolation_1.recordWorkerOutput)(run, req(args.workerId, "worker id"), req(args.resultPath, "result file"), {
|
|
102
|
+
requireAttestedTelemetry: (0, agent_config_1.resolveAgentConfig)(args).requireAttestedTelemetry,
|
|
103
|
+
allowUnattested: allowUnattestedOption(args),
|
|
104
|
+
});
|
|
105
|
+
run.loopStage = "observe";
|
|
106
|
+
(0, dispatch_1.updatePhaseStatuses)(run);
|
|
107
|
+
(0, drive_1.maybeExpandLoop)(run);
|
|
108
|
+
(0, commit_1.commitState)(run, `worker:${req(args.workerId, "worker id")}:result`);
|
|
109
|
+
(0, report_1.writeReport)(run);
|
|
110
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
111
|
+
return (0, operator_ux_1.summarizeRun)(run);
|
|
103
112
|
});
|
|
104
|
-
run.loopStage = "observe";
|
|
105
|
-
(0, dispatch_1.updatePhaseStatuses)(run);
|
|
106
|
-
(0, drive_1.maybeExpandLoop)(run);
|
|
107
|
-
(0, commit_1.commitState)(run, `worker:${req(args.workerId, "worker id")}:result`);
|
|
108
|
-
(0, report_1.writeReport)(run);
|
|
109
|
-
(0, run_store_1.saveCheckpoint)(run);
|
|
110
|
-
return (0, operator_ux_1.summarizeRun)(run);
|
|
111
113
|
}
|
|
112
114
|
function workerFailCli(args) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
return (0, run_store_1.withRunStateLock)(req(args.runId, "run id"), cwdFor(args), (run) => {
|
|
116
|
+
const message = String(args.message || req(args.resultPath, "failure message"));
|
|
117
|
+
const scope = (0, worker_isolation_1.recordWorkerFailure)(run, req(args.workerId, "worker id"), message, {
|
|
118
|
+
code: typeof args.code === "string" ? args.code : undefined,
|
|
119
|
+
path: typeof args.path === "string" ? args.path : undefined,
|
|
120
|
+
retryable: args.retryable !== undefined ? Boolean(args.retryable) : undefined,
|
|
121
|
+
});
|
|
122
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
123
|
+
return scope;
|
|
119
124
|
});
|
|
120
|
-
(0, run_store_1.saveCheckpoint)(run);
|
|
121
|
-
return scope;
|
|
122
125
|
}
|
|
123
126
|
/** validate returns the boundary violation (null when the write path is
|
|
124
127
|
* allowed) and signals a violation through a non-zero exit code, not just
|
|
125
128
|
* stdout — a validate verb must report an invalid verdict via its exit code. */
|
|
126
129
|
function workerValidateCli(args) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
return (0, run_store_1.withRunStateLock)(req(args.runId, "run id"), cwdFor(args), (run) => {
|
|
131
|
+
const target = args.path || args.resultPath;
|
|
132
|
+
const violation = (0, worker_isolation_1.validateWorkerBoundary)(run, req(args.workerId, "worker id"), target ? { path: String(target) } : {});
|
|
133
|
+
(0, run_store_1.saveCheckpoint)(run);
|
|
134
|
+
return { violation, exitCode: violation ? 1 : undefined };
|
|
135
|
+
});
|
|
132
136
|
}
|
|
@@ -141,14 +141,37 @@ function candidateWorkflowsRoots() {
|
|
|
141
141
|
roots.push(path.join(process.cwd(), "workflows"));
|
|
142
142
|
return roots;
|
|
143
143
|
}
|
|
144
|
+
/** Whether `candidate` resolves to `root` itself or a real descendant of it
|
|
145
|
+
* (not an ancestor, sibling, or anywhere reached only via `..` segments). */
|
|
146
|
+
function isWithinRoot(root, candidate) {
|
|
147
|
+
const relative = path.relative(root, candidate);
|
|
148
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
149
|
+
}
|
|
144
150
|
function findAppDir(appId) {
|
|
145
151
|
for (const root of candidateAppsRoots()) {
|
|
146
152
|
const dir = path.join(root, appId);
|
|
153
|
+
// appId is caller-controlled (MCP app.run, `cw plan`/`run --drive`, a
|
|
154
|
+
// sub-workflow's spec.appId). Without this check a traversal id like
|
|
155
|
+
// "../../../tmp/evil-app" walks path.join right out of every trusted
|
|
156
|
+
// root and loadWorkflowApp below require()s whatever app.json it finds
|
|
157
|
+
// there with NO manifest/entrypoint validation — the fast path this
|
|
158
|
+
// function feeds skips validateWorkflowApp entirely (see loadWorkflowApp).
|
|
159
|
+
if (!isWithinRoot(root, dir))
|
|
160
|
+
continue;
|
|
147
161
|
if (fs.existsSync(path.join(dir, "app.json")))
|
|
148
162
|
return dir;
|
|
149
163
|
}
|
|
150
164
|
return undefined;
|
|
151
165
|
}
|
|
166
|
+
/** Every root candidateAppsRoots()/candidateWorkflowsRoots() would search:
|
|
167
|
+
* bundled apps/workflows, an installed package's apps/workflows, an
|
|
168
|
+
* operator-set CW_APPS_DIR/CW_WORKFLOWS_DIR, and the caller's cwd/apps +
|
|
169
|
+
* cwd/workflows (the "cw app init" default — must stay unwarned, it is
|
|
170
|
+
* the normal flow for a user's own local apps). */
|
|
171
|
+
function isTrustedAppSourcePath(resolvedPath) {
|
|
172
|
+
const roots = [...candidateAppsRoots(), ...candidateWorkflowsRoots()];
|
|
173
|
+
return roots.some((root) => isWithinRoot(path.resolve(root), resolvedPath));
|
|
174
|
+
}
|
|
152
175
|
function validationContext() {
|
|
153
176
|
return { bundledSandboxProfileIds: (0, sandbox_profile_1.bundledSandboxProfileIds)(), currentCoolWorkflowVersion: version_1.CURRENT_COOL_WORKFLOW_VERSION };
|
|
154
177
|
}
|
|
@@ -451,6 +474,31 @@ function loadWorkflowAppRecordTarget(target) {
|
|
|
451
474
|
throw new Error("Missing workflow app path or id");
|
|
452
475
|
const resolved = path.resolve(target);
|
|
453
476
|
if (fs.existsSync(resolved)) {
|
|
477
|
+
// `validate`/`show` on a real path is the one loader entrypoint whose
|
|
478
|
+
// whole point is to let a caller inspect an app BEFORE deciding to
|
|
479
|
+
// trust it — but inspecting it means require()-ing its workflow.js
|
|
480
|
+
// (validateWorkflowApp needs the returned WorkflowDefinition, which
|
|
481
|
+
// only exists after the factory runs). A path outside every root CW
|
|
482
|
+
// already trusts (bundled apps, an installed package, CW_APPS_DIR, or
|
|
483
|
+
// the caller's own cwd/apps from `cw app init`) gets arbitrary code
|
|
484
|
+
// executed by "validate", with no OS-level containment — a warning
|
|
485
|
+
// printed after that require() call would be too late to matter, so
|
|
486
|
+
// this fails closed instead: refuse by default, and only proceed
|
|
487
|
+
// (still with a visible warning) when the caller explicitly opts in.
|
|
488
|
+
// Mirrors the existing --allow-unattested precedent in
|
|
489
|
+
// worker-isolation.ts: unsafe-but-explicit, never silent.
|
|
490
|
+
if (!isTrustedAppSourcePath(resolved)) {
|
|
491
|
+
if (!process.env.CW_ALLOW_EXTERNAL_APP_CODE) {
|
|
492
|
+
throw new app_schema_1.WorkflowAppValidationError("Untrusted workflow app source", [
|
|
493
|
+
{
|
|
494
|
+
code: "workflow-app-untrusted-source",
|
|
495
|
+
message: `Refusing to load workflow app code outside CW's trusted app roots: ${resolved}. Its workflow.js would run as ordinary Node.js code with full host privileges — CW does not sandbox app code, only delegated agent workers. Set CW_ALLOW_EXTERNAL_APP_CODE=1 to load and execute it anyway.`,
|
|
496
|
+
path: resolved,
|
|
497
|
+
},
|
|
498
|
+
]);
|
|
499
|
+
}
|
|
500
|
+
process.stderr.write(`cw: loading external workflow app code from ${resolved} — its workflow.js runs as ordinary Node.js code with full host privileges, not sandboxed.\n`);
|
|
501
|
+
}
|
|
454
502
|
const stat = fs.statSync(resolved);
|
|
455
503
|
if (stat.isDirectory())
|
|
456
504
|
return loadWorkflowAppFromManifest(path.join(resolved, "app.json"));
|
|
@@ -593,6 +641,24 @@ function renderManifestTemplate(id, title) {
|
|
|
593
641
|
function renderEntrypointTemplate(id, title) {
|
|
594
642
|
return `module.exports = ({ workflow, phase, agent, artifact, input }) => {\n const inputs = [\n input("question", { type: "string", required: true, description: "Question or task this workflow should answer." })\n ];\n\n return workflow({\n id: ${JSON.stringify(id)},\n title: ${JSON.stringify(title)},\n summary: "Describe what this workflow app does.",\n limits: {\n maxAgents: 8,\n maxConcurrentAgents: 4\n },\n inputs,\n sandboxProfiles: ["readonly"],\n phases: [\n phase("Map", [\n agent("map:context", "Map the task context, constraints, and evidence needed for {{question}}.", { sandboxProfileId: "readonly" })\n ]),\n phase("Assess", [\n agent("assess:risks", "Assess risks, tradeoffs, and unknowns for {{question}}.", { sandboxProfileId: "readonly" })\n ]),\n phase("Synthesize", [\n artifact("synthesis:report", "Synthesize the final answer for {{question}}.", { requiresEvidence: true, sandboxProfileId: "readonly" })\n ])\n ]\n });\n};\n`;
|
|
595
643
|
}
|
|
644
|
+
/** Validates a manifest CW itself just wrote to disk (from `initWorkflowApp`
|
|
645
|
+
* below) — deliberately bypasses `loadWorkflowAppRecordTarget`'s
|
|
646
|
+
* untrusted-source gate. That gate exists for `cw app validate <path>`,
|
|
647
|
+
* where the caller is inspecting code someone else wrote before deciding
|
|
648
|
+
* whether to trust it; `app init --directory <anywhere>` is the opposite
|
|
649
|
+
* case (the caller is authoring new code, from CW's own template, in a
|
|
650
|
+
* location they chose on purpose) and must keep working regardless of
|
|
651
|
+
* where `--directory` points. */
|
|
652
|
+
function validateGeneratedManifest(manifestPath) {
|
|
653
|
+
try {
|
|
654
|
+
const record = loadWorkflowAppFromManifest(manifestPath);
|
|
655
|
+
const result = (0, app_schema_1.validateWorkflowApp)(record.app, validationContext(), { appPath: sourcePathOf(record) });
|
|
656
|
+
return { ...result, summary: summarizeWorkflowAppRecord(record) };
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
return { valid: false, appId: manifestPath, appPath: path.resolve(manifestPath), issues: (0, app_schema_1.validationIssuesFromError)(error) };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
596
662
|
/** `cw app init <id>` / `cw_app_init`. Writes `app.json` + `workflow.js`
|
|
597
663
|
* from the templates, refusing system directories and (without
|
|
598
664
|
* `--force`) an existing app. Ported from `initApp`. */
|
|
@@ -614,7 +680,7 @@ function initWorkflowApp(appId, options = {}) {
|
|
|
614
680
|
fs.mkdirSync(destinationDir, { recursive: true });
|
|
615
681
|
fs.writeFileSync(manifestPath, renderManifestTemplate(id, title), "utf8");
|
|
616
682
|
fs.writeFileSync(entrypointPath, renderEntrypointTemplate(id, title), "utf8");
|
|
617
|
-
const validation =
|
|
683
|
+
const validation = validateGeneratedManifest(manifestPath);
|
|
618
684
|
if (!validation.valid) {
|
|
619
685
|
throw new app_schema_1.WorkflowAppValidationError("Generated workflow app is invalid", validation.issues);
|
|
620
686
|
}
|
|
@@ -16,32 +16,57 @@ const registry_core_1 = require("./registry-core");
|
|
|
16
16
|
// cli/dispatch.ts's generic executor performs the actual write.
|
|
17
17
|
// ---------------------------------------------------------------------
|
|
18
18
|
const version_1 = require("../../core/version");
|
|
19
|
-
const workflow_app_loader_1 = require("../../shell/workflow-app-loader");
|
|
20
19
|
const help_1 = require("../../core/format/help");
|
|
20
|
+
const completion_1 = require("../../core/format/completion");
|
|
21
|
+
const cli_args_1 = require("../../core/util/cli-args");
|
|
22
|
+
// This whole module is required unconditionally at startup for EVERY
|
|
23
|
+
// command (see wiring/capability-table/index.ts) — a top-level import of
|
|
24
|
+
// `shell/workflow-app-loader` here means even `cw --version` pays its
|
|
25
|
+
// load cost, though only `search`'s handler ever calls it.
|
|
26
|
+
function loadWorkflowAppLoader() {
|
|
27
|
+
return require("../../shell/workflow-app-loader");
|
|
28
|
+
}
|
|
21
29
|
(0, registry_core_1.addCliOnlyCapability)("version", "Print the current cool-workflow version.", {
|
|
22
30
|
path: ["version"],
|
|
23
31
|
jsonMode: "default",
|
|
24
32
|
handler: () => ({ text: `${version_1.CURRENT_COOL_WORKFLOW_VERSION}\n` }),
|
|
25
33
|
}, "version is a local, no-run-state print; the old build never gave it an MCP peer.");
|
|
34
|
+
/** `cw completion <bash|zsh|fish>` — a static shell-completion script for
|
|
35
|
+
* the top-level command word list (core/format/completion.ts). CLI-only:
|
|
36
|
+
* a shell-integration script is meaningless as an MCP tool result. */
|
|
37
|
+
(0, registry_core_1.addCliOnlyCapability)("completion", "Print a shell-completion script for the top-level cw commands (bash, zsh, or fish).", {
|
|
38
|
+
path: ["completion"],
|
|
39
|
+
jsonMode: "human",
|
|
40
|
+
handler: (args) => {
|
|
41
|
+
const shell = (0, cli_args_1.optionalArg)(args.positionals[0]);
|
|
42
|
+
if (!shell)
|
|
43
|
+
throw new Error('Missing shell name.\n Try: cw completion bash|zsh|fish');
|
|
44
|
+
return { text: (0, completion_1.formatCompletionScript)(shell) };
|
|
45
|
+
},
|
|
46
|
+
}, "shell-completion output is a script for the user's OWN shell to source, not data an MCP client could use — CLI-only, same reasoning as doctor/fix.");
|
|
26
47
|
/** `cw search <keyword>` — filters the SAME real app discovery `cw list`
|
|
27
48
|
* shows, by id/title/summary (byte-behavior port of cli/dispatch.ts's
|
|
28
49
|
* milestone-1 carry-over `search` arm, moved here so the dispatchLegacy
|
|
29
|
-
* switch shrinks per its file header's rule).
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
50
|
+
* switch shrinks per its file header's rule). The old v0.1.98 CLI DID
|
|
51
|
+
* have its own `cw help search` row (`docs/rebuild/SPEC/cli-help/
|
|
52
|
+
* search.txt`: "cw search Search workflow apps by keyword (title,
|
|
53
|
+
* description, id)."); `hiddenFromHelp` here was a rebuild regression
|
|
54
|
+
* that hid it, not a preserved old-build quirk — removed so the row
|
|
55
|
+
* shows again. The summary text below is intentionally NOT byte-matched
|
|
56
|
+
* to that old capture: the old wording says "description," but the
|
|
57
|
+
* filter below matches `summary` (there is no `description` field) — so
|
|
58
|
+
* the summary text stays accurate to current fields rather than
|
|
59
|
+
* reproducing the old capture's stale terminology. */
|
|
34
60
|
(0, registry_core_1.addCliOnlyCapability)("search", "Search bundled workflows by id/title/summary keyword.", {
|
|
35
61
|
path: ["search"],
|
|
36
62
|
jsonMode: "flag",
|
|
37
|
-
hiddenFromHelp: true,
|
|
38
63
|
handler: (args) => {
|
|
39
64
|
const keyword = args.positionals.join(" ");
|
|
40
65
|
if (!keyword.trim()) {
|
|
41
66
|
throw new Error('Missing search keyword.\n Tip: cw search architecture to find workflows about architecture.');
|
|
42
67
|
}
|
|
43
68
|
const lower = keyword.toLowerCase();
|
|
44
|
-
const results = (
|
|
69
|
+
const results = loadWorkflowAppLoader().listWorkflowApps()
|
|
45
70
|
.filter((a) => String(a.title).toLowerCase().includes(lower) ||
|
|
46
71
|
String(a.summary).toLowerCase().includes(lower) ||
|
|
47
72
|
String(a.id).toLowerCase().includes(lower))
|