cool-workflow 0.1.86 → 0.1.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +111 -68
- 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/agent-config.js +42 -1
- package/dist/capability-core.js +9 -4
- package/dist/capability-registry.js +48 -0
- package/dist/cli/command-surface.js +182 -11
- package/dist/cli.js +2 -1
- package/dist/doctor.js +27 -5
- package/dist/drive.js +222 -16
- package/dist/execution-backend.js +12 -4
- package/dist/loop-expansion.js +60 -0
- package/dist/onramp.js +25 -0
- package/dist/operator-ux/format.js +21 -15
- package/dist/operator-ux.js +2 -1
- package/dist/orchestrator/lifecycle-operations.js +134 -3
- package/dist/orchestrator.js +122 -76
- package/dist/run-export.js +106 -2
- package/dist/state-node.js +13 -3
- package/dist/state.js +21 -0
- package/dist/telemetry-attestation.js +30 -6
- package/dist/telemetry-demo.js +29 -1
- package/dist/telemetry-ledger.js +6 -0
- package/dist/term.js +93 -0
- package/dist/version.js +1 -1
- package/dist/worker-accept/telemetry-ledger.js +12 -2
- package/dist/workflow-api.js +33 -0
- package/dist/workflow-app-framework.js +20 -0
- package/docs/agent-delegation-drive.7.md +28 -6
- package/docs/capability-topology-registry.7.md +69 -46
- package/docs/cli-mcp-parity.7.md +22 -2
- package/docs/contract-migration-tooling.7.md +8 -0
- package/docs/control-plane-scheduling.7.md +8 -0
- package/docs/durable-state-and-locking.7.md +8 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +8 -0
- package/docs/execution-backends.7.md +8 -0
- package/docs/launch/launch-kit.md +9 -9
- package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
- package/docs/multi-agent-eval-replay-harness.7.md +8 -0
- package/docs/multi-agent-operator-ux.7.md +8 -0
- package/docs/node-snapshot-diff-replay.7.md +8 -0
- package/docs/observability-cost-accounting.7.md +8 -0
- package/docs/project-index.md +26 -5
- package/docs/readme-v0.1.87-full.md +301 -0
- package/docs/real-execution-backends.7.md +8 -0
- package/docs/release-and-migration.7.md +8 -0
- package/docs/release-history.md +10 -0
- package/docs/release-tooling.7.md +16 -0
- package/docs/report-verifiable-bundle.7.md +34 -2
- package/docs/run-registry-control-plane.7.md +18 -0
- package/docs/run-retention-reclamation.7.md +8 -0
- package/docs/state-explosion-management.7.md +8 -0
- package/docs/team-collaboration.7.md +8 -0
- package/docs/trust-model.md +6 -4
- package/docs/web-desktop-workbench.7.md +8 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +8 -5
- package/scripts/agents/agent-adapter-core.js +180 -0
- package/scripts/agents/builtin-templates.json +5 -1
- package/scripts/agents/claude-p-agent.js +7 -33
- package/scripts/agents/codex-agent.js +134 -0
- package/scripts/agents/cw-attest-wrap.js +9 -1
- package/scripts/agents/gemini-agent.js +115 -0
- package/scripts/agents/opencode-agent.js +119 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/coverage-gate.js +15 -1
- package/scripts/cw.js +0 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +6 -5
- package/scripts/release-check.js +3 -3
- package/scripts/release-flow.js +49 -2
- package/scripts/release-gate.sh +1 -1
- package/tsconfig.json +3 -1
package/dist/drive.js
CHANGED
|
@@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.DRIVE_SCHEMA_VERSION = void 0;
|
|
28
|
+
exports.MAX_SUB_WORKFLOW_DEPTH = exports.DRIVE_SCHEMA_VERSION = void 0;
|
|
29
29
|
exports.driveStep = driveStep;
|
|
30
30
|
exports.driveConcurrentRound = driveConcurrentRound;
|
|
31
31
|
exports.drive = drive;
|
|
@@ -38,9 +38,14 @@ const worker_isolation_1 = require("./worker-isolation");
|
|
|
38
38
|
const agent_config_1 = require("./agent-config");
|
|
39
39
|
const scheduling_1 = require("./scheduling");
|
|
40
40
|
const observability_1 = require("./observability");
|
|
41
|
+
const loop_expansion_1 = require("./loop-expansion");
|
|
42
|
+
const telemetry_attestation_1 = require("./telemetry-attestation");
|
|
41
43
|
const state_1 = require("./state");
|
|
44
|
+
const trust_audit_1 = require("./trust-audit");
|
|
42
45
|
const compare_1 = require("./compare");
|
|
43
46
|
exports.DRIVE_SCHEMA_VERSION = 1;
|
|
47
|
+
/** Hard cap on inline sub-workflow nesting depth (fail-closed bound on recursion). */
|
|
48
|
+
exports.MAX_SUB_WORKFLOW_DEPTH = 4;
|
|
44
49
|
/** The task the next drive step would advance: a RUNNING (already-dispatched,
|
|
45
50
|
* awaiting fulfillment / retry) task first, else the next PENDING task in the
|
|
46
51
|
* first runnable phase. Deterministic; honors the existing phase gate. */
|
|
@@ -71,10 +76,12 @@ function exitCodeFromEvidence(evidence) {
|
|
|
71
76
|
function agentConfigured(config) {
|
|
72
77
|
return Boolean(config.command || config.endpoint);
|
|
73
78
|
}
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
79
|
+
/** Progress to STDERR (stdout stays clean JSON). On by default when stderr is a
|
|
80
|
+
* TTY; silent in CI/pipes. CW_DRIVE_PROGRESS=0 forces off, =1 forces on. */
|
|
76
81
|
function emitProgress(message) {
|
|
77
|
-
|
|
82
|
+
const forcedOff = process.env.CW_DRIVE_PROGRESS === "0";
|
|
83
|
+
const forcedOn = process.env.CW_DRIVE_PROGRESS === "1";
|
|
84
|
+
if ((Boolean(process.stderr.isTTY) && !forcedOff) || forcedOn)
|
|
78
85
|
process.stderr.write(`[drive] ${message}\n`);
|
|
79
86
|
}
|
|
80
87
|
/** Advance exactly ONE deterministic step. Pure-ish: all mutation is through the
|
|
@@ -202,7 +209,7 @@ function processSelectedTask(ctx, selected, preparedOutcome) {
|
|
|
202
209
|
// immediate activity instead of a long silence on the first worker. task.label
|
|
203
210
|
// is the human-facing display name; the id stays the stable reference.
|
|
204
211
|
const promptDigest = node_fs_1.default.existsSync(manifest.inputPath) ? (0, execution_backend_1.sha256)(node_fs_1.default.readFileSync(manifest.inputPath, "utf8")) : (0, execution_backend_1.sha256)(manifest.prompt || "");
|
|
205
|
-
const cachePath = resultCachePath(run, selected, (0, execution_backend_1.sha256)(selected.prompt));
|
|
212
|
+
const cachePath = resultCachePath(run, selected, (0, execution_backend_1.sha256)(selected.prompt), ctx.incremental, ctx.incremental ? incrementalDelegationDigest(selected, manifest, ctx.config) : "");
|
|
206
213
|
if (cachePath && node_fs_1.default.existsSync(cachePath)) {
|
|
207
214
|
emitProgress(`↺ ${selected.label || selected.id} (${selected.phase}) — accepting cached result`);
|
|
208
215
|
try {
|
|
@@ -220,6 +227,12 @@ function processSelectedTask(ctx, selected, preparedOutcome) {
|
|
|
220
227
|
reason: "result cache hit"
|
|
221
228
|
});
|
|
222
229
|
}
|
|
230
|
+
// Sub-workflow fulfillment (alternative to the agent backend): plan + drive a
|
|
231
|
+
// CHILD run and bind its report back as this task's result. Leaf work is still
|
|
232
|
+
// external-agent delegation at every level; CW imports no model SDK here.
|
|
233
|
+
if (selected.subWorkflow) {
|
|
234
|
+
return runSubWorkflow(ctx, run, selected, workerId, manifest);
|
|
235
|
+
}
|
|
223
236
|
emitProgress(`→ ${selected.label || selected.id} (${selected.phase}) — ${dispatched ? "dispatched, " : ""}spawning agent, may take minutes…`);
|
|
224
237
|
const envelope = (0, execution_backend_1.runBackend)(buildAgentRequest(ctx, run, selected, manifest, preparedOutcome));
|
|
225
238
|
const handle = envelope.provenance.handle;
|
|
@@ -269,7 +282,63 @@ function processSelectedTask(ctx, selected, preparedOutcome) {
|
|
|
269
282
|
reportedModel
|
|
270
283
|
});
|
|
271
284
|
}
|
|
272
|
-
function
|
|
285
|
+
function cacheFilePath(run, task, digest) {
|
|
286
|
+
return node_path_1.default.join(run.cwd, ".cw", "cache", "worker-results", (0, state_1.safeFileName)(run.workflow.id), `${(0, state_1.safeFileName)(task.id)}-${digest.replace(/^sha256:/, "").slice(0, 32)}.md`);
|
|
287
|
+
}
|
|
288
|
+
/** Digest of the per-task DELEGATION config that determines a result but is NOT
|
|
289
|
+
* carried by the prompt or run.inputs: the resolved model (task override OR the
|
|
290
|
+
* global agent-config model), the agent IDENTITY (which binary/endpoint actually
|
|
291
|
+
* produces the bytes — `command`/`args`/`endpoint`), the backend driver, and the
|
|
292
|
+
* resolved sandbox PROFILE ID. All of these are operator flags/env (`--agent-model`,
|
|
293
|
+
* `--agent-command`, `--agent-endpoint`, ...) stripped from run.inputs by
|
|
294
|
+
* DRIVE_RUNTIME_KEYS, so they must be folded here or swapping the model/agent/
|
|
295
|
+
* endpoint would serve a stale result (and attest the wrong producer). The sandbox
|
|
296
|
+
* PROFILE ID (not the full resolved policy) is used because the policy's read/write
|
|
297
|
+
* paths embed the per-run worker dir, so the full policy is NOT stable across runs
|
|
298
|
+
* (it would defeat all reuse); the id is stable and changes on a profile swap. Args
|
|
299
|
+
* are secret-stripped (no credential lands in the digest input). `config.args` is
|
|
300
|
+
* the un-substituted template (e.g. `{{result}}`), so it is stable across runs.
|
|
301
|
+
* Deterministic: stableStringify, no clock/random. */
|
|
302
|
+
function incrementalDelegationDigest(task, manifest, config) {
|
|
303
|
+
return (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)({
|
|
304
|
+
model: task.model || config.model || "",
|
|
305
|
+
agentType: task.agentType || "agent",
|
|
306
|
+
sandboxProfileId: manifest.sandboxPolicy?.id || task.sandboxProfileId || "",
|
|
307
|
+
command: config.command || "",
|
|
308
|
+
args: config.args ? (0, execution_backend_1.stripSecretArgs)(config.args) : [],
|
|
309
|
+
endpoint: config.endpoint || ""
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
function resultCachePath(run, task, promptDigest, incremental, delegationDigest) {
|
|
313
|
+
// Incremental resume (opt-in, run-level): EVERY task is keyed by content so a
|
|
314
|
+
// re-run reuses the longest unchanged prefix. The key folds the rendered prompt,
|
|
315
|
+
// the full run.inputs, the per-task DELEGATION config (model/backend/sandbox —
|
|
316
|
+
// result-determining but NOT carried by prompt/inputs, so editing the model must
|
|
317
|
+
// invalidate), and the UPSTREAM RESULT digests (not just prompts: a CW prompt is
|
|
318
|
+
// rendered from run.inputs and does NOT carry an upstream task's result bytes, so
|
|
319
|
+
// a changed/nondeterministic upstream result must invalidate downstream — which
|
|
320
|
+
// keying on upstream result bytes does). A changed prompt/input/model perturbs
|
|
321
|
+
// that task's key, and its changed result perturbs every downstream key, so the
|
|
322
|
+
// "first changed task and everything after" re-run falls out for free. The phase
|
|
323
|
+
// barrier guarantees every upstream task is `completed` before this task runs, so
|
|
324
|
+
// its result bytes are available to digest. schemaVersion:2 never collides with
|
|
325
|
+
// the opt-in schemaVersion:1 cache below.
|
|
326
|
+
if (incremental) {
|
|
327
|
+
const upstreamResultsDigest = previousPhaseResultsDigest(run, task);
|
|
328
|
+
if (upstreamResultsDigest === undefined)
|
|
329
|
+
return undefined;
|
|
330
|
+
const digest = (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)({
|
|
331
|
+
schemaVersion: 2,
|
|
332
|
+
workflowId: run.workflow.id,
|
|
333
|
+
taskId: task.id,
|
|
334
|
+
promptDigest,
|
|
335
|
+
runInputsDigest: (0, execution_backend_1.sha256)((0, telemetry_attestation_1.stableStringify)(run.inputs || {})),
|
|
336
|
+
delegationDigest,
|
|
337
|
+
upstreamResultsDigest
|
|
338
|
+
}));
|
|
339
|
+
return cacheFilePath(run, task, digest);
|
|
340
|
+
}
|
|
341
|
+
// Default: the per-task OPT-IN cache (unchanged — POLA).
|
|
273
342
|
const policy = task.resultCache;
|
|
274
343
|
if (!policy || policy.mode !== "read-write")
|
|
275
344
|
return undefined;
|
|
@@ -288,12 +357,13 @@ function resultCachePath(run, task, promptDigest) {
|
|
|
288
357
|
keyValue,
|
|
289
358
|
promptDigest,
|
|
290
359
|
completedResultsDigest
|
|
291
|
-
}))
|
|
292
|
-
return
|
|
360
|
+
}));
|
|
361
|
+
return cacheFilePath(run, task, digest);
|
|
293
362
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
363
|
+
/** Digest of the result bytes of every task in strictly-earlier phases (deterministic
|
|
364
|
+
* id order). `undefined` when any such task is not yet completed/readable — so a
|
|
365
|
+
* caller never keys on partial upstream state. */
|
|
366
|
+
function previousPhaseResultsDigest(run, task) {
|
|
297
367
|
const phaseIndex = run.phases.findIndex((phase) => phase.name === task.phase || phase.id === task.phase);
|
|
298
368
|
if (phaseIndex < 0)
|
|
299
369
|
return undefined;
|
|
@@ -310,6 +380,11 @@ function completedResultsCacheDigest(run, task) {
|
|
|
310
380
|
return undefined;
|
|
311
381
|
return (0, execution_backend_1.sha256)(JSON.stringify(records));
|
|
312
382
|
}
|
|
383
|
+
function completedResultsCacheDigest(run, task) {
|
|
384
|
+
if (task.resultCache?.includeCompletedResults !== "previous-phases")
|
|
385
|
+
return "";
|
|
386
|
+
return previousPhaseResultsDigest(run, task);
|
|
387
|
+
}
|
|
313
388
|
function writeResultCache(file, content) {
|
|
314
389
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
315
390
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
@@ -393,7 +468,7 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
393
468
|
continue;
|
|
394
469
|
}
|
|
395
470
|
const manifest = runner.showWorkerManifest(runId, workerId);
|
|
396
|
-
const cachePath = resultCachePath(run, task, (0, execution_backend_1.sha256)(task.prompt));
|
|
471
|
+
const cachePath = resultCachePath(run, task, (0, execution_backend_1.sha256)(task.prompt), ctx.incremental, ctx.incremental ? incrementalDelegationDigest(task, manifest, ctx.config) : "");
|
|
397
472
|
if (cachePath && node_fs_1.default.existsSync(cachePath))
|
|
398
473
|
continue;
|
|
399
474
|
const job = (0, execution_backend_1.prepareAgentSpawn)(buildAgentRequest(ctx, run, task, manifest));
|
|
@@ -458,19 +533,150 @@ function handleHop(ctx, task, workerId, reason) {
|
|
|
458
533
|
function step(action, status, fields) {
|
|
459
534
|
return { schemaVersion: 1, action, status, ...fields };
|
|
460
535
|
}
|
|
536
|
+
function errMessage(error) {
|
|
537
|
+
return error instanceof Error ? error.message : String(error);
|
|
538
|
+
}
|
|
539
|
+
/** Render a sub-workflow's input templates against the PARENT run's inputs, so the
|
|
540
|
+
* child inputs are a pure function of recorded parent inputs (deterministic). */
|
|
541
|
+
function renderSubInputs(spec, parentInputs) {
|
|
542
|
+
const out = {};
|
|
543
|
+
for (const [key, template] of Object.entries(spec.inputs || {})) {
|
|
544
|
+
out[key] = String(template).replace(/\{\{(\w+)\}\}/g, (_, name) => String(parentInputs[name] ?? ""));
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
/** Fulfill a sub-workflow task: plan + drive a CHILD run, then bind its report (or
|
|
549
|
+
* verdict result) back as the parent task's result through the SAME accept path, so
|
|
550
|
+
* the parent's verifier/schema/evidence gate and downstream tasks treat it like any
|
|
551
|
+
* other result. Fail-closed: bounded recursion + cycle detection BEFORE any child
|
|
552
|
+
* state is minted; a child that does not complete parks the parent hop. The child's
|
|
553
|
+
* own telemetry/audit live in the child run; the parent records ONE honest
|
|
554
|
+
* `worker.sub-workflow` cross-link (child run id + report digest + child audit
|
|
555
|
+
* verdict) — nothing is summed or fabricated. */
|
|
556
|
+
function runSubWorkflow(ctx, run, selected, workerId, manifest) {
|
|
557
|
+
const spec = selected.subWorkflow;
|
|
558
|
+
const parentApp = run.workflow.id;
|
|
559
|
+
// Fail-closed BEFORE planning a child (no child state minted when refused).
|
|
560
|
+
if (ctx.depth + 1 > exports.MAX_SUB_WORKFLOW_DEPTH) {
|
|
561
|
+
return handleHop(ctx, selected, workerId, `sub-workflow depth limit exceeded (> ${exports.MAX_SUB_WORKFLOW_DEPTH})`);
|
|
562
|
+
}
|
|
563
|
+
// Include the CURRENT app on the path, so a direct self-cycle (A→A) is caught at
|
|
564
|
+
// depth 0 — before any child run dir is minted.
|
|
565
|
+
if ([...ctx.visitedAppIds, parentApp].includes(spec.appId)) {
|
|
566
|
+
return handleHop(ctx, selected, workerId, `sub-workflow cycle detected: ${[...ctx.visitedAppIds, parentApp, spec.appId].join(" -> ")}`);
|
|
567
|
+
}
|
|
568
|
+
// Deterministic child run id derived from the parent run + task (no clock/random).
|
|
569
|
+
const childRunId = `sub-${run.id}-${(0, state_1.safeFileName)(selected.id)}`;
|
|
570
|
+
const childInputs = {
|
|
571
|
+
repo: run.inputs.repo ?? run.cwd,
|
|
572
|
+
cwd: run.cwd,
|
|
573
|
+
question: run.inputs.question ?? "",
|
|
574
|
+
...renderSubInputs(spec, run.inputs),
|
|
575
|
+
runId: childRunId
|
|
576
|
+
};
|
|
577
|
+
emitProgress(`⧉ ${selected.label || selected.id} (${selected.phase}) — sub-workflow ${spec.appId}…`);
|
|
578
|
+
let childRun;
|
|
579
|
+
try {
|
|
580
|
+
childRun = ctx.runner.plan(spec.appId, childInputs);
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
return handleHop(ctx, selected, workerId, `sub-workflow plan failed (${spec.appId}): ${errMessage(error)}`);
|
|
584
|
+
}
|
|
585
|
+
const childResult = drive(ctx.runner, childRun.id, {
|
|
586
|
+
now: ctx.now,
|
|
587
|
+
agentConfig: ctx.config,
|
|
588
|
+
policy: ctx.policy,
|
|
589
|
+
incremental: ctx.incremental,
|
|
590
|
+
depth: ctx.depth + 1,
|
|
591
|
+
visitedAppIds: [...ctx.visitedAppIds, parentApp]
|
|
592
|
+
});
|
|
593
|
+
if (childResult.status !== "complete") {
|
|
594
|
+
return handleHop(ctx, selected, workerId, `sub-workflow ${spec.appId} did not complete (status: ${childResult.status})`);
|
|
595
|
+
}
|
|
596
|
+
// Bind the child's bytes: the rendered report (default) or the verdict result.
|
|
597
|
+
const finalChild = ctx.runner.loadRun(childRun.id);
|
|
598
|
+
let childBytes;
|
|
599
|
+
if (spec.bindResult === "verdict-result") {
|
|
600
|
+
const verdict = finalChild.tasks.find((t) => /^verdict[:/]|^synthesis[:/]/i.test(t.id) && t.status === "completed");
|
|
601
|
+
childBytes = verdict?.resultPath && node_fs_1.default.existsSync(verdict.resultPath) ? node_fs_1.default.readFileSync(verdict.resultPath, "utf8") : undefined;
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
childBytes = node_fs_1.default.existsSync(finalChild.paths.report) ? node_fs_1.default.readFileSync(finalChild.paths.report, "utf8") : undefined;
|
|
605
|
+
}
|
|
606
|
+
if (childBytes === undefined) {
|
|
607
|
+
return handleHop(ctx, selected, workerId, `sub-workflow ${spec.appId} produced no ${spec.bindResult || "report"}`);
|
|
608
|
+
}
|
|
609
|
+
// Accept through the SAME path as any other result (verifier/schema/evidence gate).
|
|
610
|
+
try {
|
|
611
|
+
node_fs_1.default.writeFileSync(manifest.resultPath, childBytes, "utf8");
|
|
612
|
+
ctx.runner.recordWorkerOutput(run.id, workerId, manifest.resultPath, {});
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
return handleHop(ctx, selected, workerId, `sub-workflow result rejected by parent gate: ${errMessage(error)}`);
|
|
616
|
+
}
|
|
617
|
+
// Honest cross-link (provenance only — never fails the accepted hop): one
|
|
618
|
+
// worker.sub-workflow audit event on the parent pins the child run + report digest
|
|
619
|
+
// + the child's own audit-chain verdict, and the task points at the child run dir.
|
|
620
|
+
try {
|
|
621
|
+
const afterAccept = ctx.runner.loadRun(run.id);
|
|
622
|
+
const task = afterAccept.tasks.find((t) => t.id === selected.id);
|
|
623
|
+
const childAudit = (0, trust_audit_1.verifyTrustAudit)(finalChild);
|
|
624
|
+
(0, trust_audit_1.recordTrustAuditEvent)(afterAccept, {
|
|
625
|
+
kind: "worker.sub-workflow",
|
|
626
|
+
decision: "recorded",
|
|
627
|
+
source: "runtime-derived",
|
|
628
|
+
workerId,
|
|
629
|
+
taskId: selected.id,
|
|
630
|
+
nodeId: task?.resultNodeId,
|
|
631
|
+
metadata: {
|
|
632
|
+
subWorkflowAppId: spec.appId,
|
|
633
|
+
subRunId: childRun.id,
|
|
634
|
+
childReportDigest: (0, execution_backend_1.sha256)(childBytes),
|
|
635
|
+
childAuditVerified: childAudit.verified,
|
|
636
|
+
bindResult: spec.bindResult || "report"
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
if (task) {
|
|
640
|
+
task.subRunId = childRun.id;
|
|
641
|
+
task.subRunDir = finalChild.paths.runDir;
|
|
642
|
+
}
|
|
643
|
+
(0, state_1.saveCheckpoint)(afterAccept);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
/* the cross-link is provenance; a failure here must not undo an accepted hop */
|
|
647
|
+
}
|
|
648
|
+
return step("accept", "ok", {
|
|
649
|
+
runId: run.id,
|
|
650
|
+
taskId: selected.id,
|
|
651
|
+
phase: selected.phase,
|
|
652
|
+
handleKind: "sub-workflow",
|
|
653
|
+
reason: `sub-workflow ${spec.appId} → ${childRun.id}`
|
|
654
|
+
});
|
|
655
|
+
}
|
|
461
656
|
/** Drive a run: `--once` advances exactly one step; otherwise run to completion,
|
|
462
657
|
* park, or a blocked stop. Composes the existing verbs + the agent backend only. */
|
|
463
658
|
function drive(runner, runId, options = {}) {
|
|
464
659
|
const now = options.now || new Date().toISOString();
|
|
465
660
|
const policy = (0, scheduling_1.normalizeSchedulingPolicy)(options.policy || scheduling_1.DEFAULT_SCHEDULING_POLICY);
|
|
466
661
|
const config = options.agentConfig || (0, agent_config_1.resolveAgentConfig)(options.args || {});
|
|
467
|
-
const ctx = {
|
|
662
|
+
const ctx = {
|
|
663
|
+
runner, runId, now, policy, config, attempts: new Map(),
|
|
664
|
+
incremental: Boolean(options.incremental),
|
|
665
|
+
depth: Math.max(0, Math.floor(options.depth || 0)),
|
|
666
|
+
visitedAppIds: options.visitedAppIds || []
|
|
667
|
+
};
|
|
468
668
|
const steps = [];
|
|
469
669
|
const run0 = runner.loadRun(runId);
|
|
470
670
|
const plannedWorkers = run0.tasks.length;
|
|
471
|
-
// Safety bound: every worker, every retry, plus the terminal commit + slack.
|
|
472
|
-
//
|
|
473
|
-
|
|
671
|
+
// Safety bound: every worker, every retry, plus the terminal commit + slack. Each
|
|
672
|
+
// concurrent round retires >=1 worker, so this bounds rounds too. A bounded dynamic
|
|
673
|
+
// loop can append up to (maxRounds-1)×templateTasks MORE tasks at runtime, so the
|
|
674
|
+
// iteration bound (NOT plannedWorkers, which stays the initial count for status) adds
|
|
675
|
+
// the worst-case expansion derived STATICALLY from the declaration — a pure function
|
|
676
|
+
// of the workflow, never of runtime results — so the bound is replay-stable and the
|
|
677
|
+
// drive is provably terminating; it reduces to the original value when there are no
|
|
678
|
+
// loop phases.
|
|
679
|
+
const maxIterations = (plannedWorkers + (0, loop_expansion_1.maxLoopExpansion)(run0)) * (policy.maxAttempts + 1) + 5;
|
|
474
680
|
const concurrency = Math.max(1, Math.floor(options.concurrency || 1));
|
|
475
681
|
// The parallel() on-ramp: a phase authored with mode "parallel" is fulfilled
|
|
476
682
|
// concurrently through EVERY shipping surface (run --drive, quickstart) — no
|
|
@@ -411,9 +411,17 @@ function executeLocal(descriptor, policy, request, label, attestation) {
|
|
|
411
411
|
// shell backend runs via /bin/sh -c; node/bun run the command directly
|
|
412
412
|
// (bun is Node-compatible by default so evidence stays byte-stable with node).
|
|
413
413
|
// spawnStyle comes from the registered driver, not a hardcoded id check.
|
|
414
|
+
const isTTY = process.stderr.isTTY;
|
|
415
|
+
const shortLabel = command.split("/").pop() || command;
|
|
416
|
+
if (isTTY)
|
|
417
|
+
process.stderr.write(`● Running ${shortLabel}...\n`);
|
|
418
|
+
const startedAt = process.hrtime.bigint();
|
|
414
419
|
const result = getBackendDriver(descriptor.id)?.spawnStyle === "shell"
|
|
415
420
|
? (0, node_child_process_1.spawnSync)([command, ...args].join(" "), { ...options, shell: true })
|
|
416
421
|
: (0, node_child_process_1.spawnSync)(command, args, { ...options, shell: false });
|
|
422
|
+
const elapsedMs = Number((process.hrtime.bigint() - startedAt) / 1000000n);
|
|
423
|
+
if (isTTY)
|
|
424
|
+
process.stderr.write(`✓ Done (${elapsedMs}ms)\n`);
|
|
417
425
|
const exitCode = typeof result.status === "number" ? result.status : null;
|
|
418
426
|
const spawnError = result.error ? (0, util_1.messageOf)(result.error) : undefined;
|
|
419
427
|
const stdout = String(result.stdout || "");
|
|
@@ -670,10 +678,10 @@ function runAgentProcess(descriptor, policy, request, label, handle, attestation
|
|
|
670
678
|
outcome = request.preparedAgentOutcome;
|
|
671
679
|
}
|
|
672
680
|
else {
|
|
673
|
-
// Live output
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
const streamStderr = process.env.CW_AGENT_STREAM
|
|
681
|
+
// Live output on by default when stderr is a TTY. stdout is always
|
|
682
|
+
// captured as data. CI/pipes stay silent. CW_AGENT_STREAM=0 or
|
|
683
|
+
// CW_NO_STREAM=1 forces off; CW_AGENT_STREAM=1 forces on.
|
|
684
|
+
const streamStderr = process.env.CW_AGENT_STREAM !== "0" && Boolean(process.stderr.isTTY) && process.env.CW_NO_STREAM !== "1";
|
|
677
685
|
const child = (0, node_child_process_1.spawnSync)(resolved.binary, realArgs, {
|
|
678
686
|
cwd: request.cwd,
|
|
679
687
|
env: { ...process.env },
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Bounded dynamic control flow (#2) — a declarative LOOP phase whose tasks are a
|
|
3
|
+
// per-round TEMPLATE: after each round completes, a registered PURE predicate decides
|
|
4
|
+
// whether to run another round (a fresh phase appended after this one, with the same
|
|
5
|
+
// tasks under round-suffixed ids) or stop. Hard-capped at `maxRounds`.
|
|
6
|
+
//
|
|
7
|
+
// REPLAY-DETERMINISM: the predicate is a pure function of RECORDED round results +
|
|
8
|
+
// recorded usage — no IO/clock/random in its context by construction. The engine
|
|
9
|
+
// RECORDS each decision as a `loop-control` state node, so a replay walks the recorded
|
|
10
|
+
// round sequence; re-running the predicate is a verification cross-check, not the
|
|
11
|
+
// source of truth. Predicates are NAMED (registry refs), never inline closures —
|
|
12
|
+
// closures cannot be serialized into state or re-evaluated byte-identically on replay.
|
|
13
|
+
//
|
|
14
|
+
// The materialization (cloning tasks, appending phases, recording the loop-control
|
|
15
|
+
// node) lives in the orchestrator (maybeExpandLoop), which owns writeTaskFiles + the
|
|
16
|
+
// plan pipeline; THIS module is just the pure registry + the STATIC expansion bound.
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.registerLoopPredicate = registerLoopPredicate;
|
|
19
|
+
exports.getLoopPredicate = getLoopPredicate;
|
|
20
|
+
exports.hasLoopPredicate = hasLoopPredicate;
|
|
21
|
+
exports.maxLoopExpansion = maxLoopExpansion;
|
|
22
|
+
const REGISTRY = new Map();
|
|
23
|
+
/** Register a named pure loop predicate (apps register theirs at load; CW ships a few
|
|
24
|
+
* built-ins below). Re-registering a name overwrites it. */
|
|
25
|
+
function registerLoopPredicate(name, fn) {
|
|
26
|
+
REGISTRY.set(name, fn);
|
|
27
|
+
}
|
|
28
|
+
function getLoopPredicate(name) {
|
|
29
|
+
return REGISTRY.get(name);
|
|
30
|
+
}
|
|
31
|
+
function hasLoopPredicate(name) {
|
|
32
|
+
return REGISTRY.has(name);
|
|
33
|
+
}
|
|
34
|
+
// ---- Built-in predicates ---------------------------------------------------------
|
|
35
|
+
/** Stop once a round produced no findings (a convergence loop: keep going while the
|
|
36
|
+
* agent still reports findings; stop when a round comes back empty). */
|
|
37
|
+
registerLoopPredicate("no-new-findings", (ctx) => {
|
|
38
|
+
const empty = ctx.roundResults.every((r) => !r || !Array.isArray(r.findings) || r.findings.length === 0);
|
|
39
|
+
return empty
|
|
40
|
+
? { done: true, reason: "no-new-findings: the latest round produced no findings" }
|
|
41
|
+
: { done: false, reason: "no-new-findings: the latest round still has findings" };
|
|
42
|
+
});
|
|
43
|
+
/** Always stop after the first round (a degenerate loop ≈ a normal phase; useful as a
|
|
44
|
+
* default / for tests). */
|
|
45
|
+
registerLoopPredicate("single-round", () => ({ done: true, reason: "single-round: stop after one round" }));
|
|
46
|
+
/** Static worst-case number of EXTRA tasks a fully-expanded run could mint, derived
|
|
47
|
+
* purely from the workflow DECLARATION (every loop origin phase contributes
|
|
48
|
+
* (maxRounds-1) × its round-1 task count). Used to keep the drive's iteration bound
|
|
49
|
+
* safe as tasks grow, WITHOUT reading any runtime result — so the bound is itself
|
|
50
|
+
* replay-stable and the loop is provably bounded. Zero when there are no loop phases. */
|
|
51
|
+
function maxLoopExpansion(run) {
|
|
52
|
+
let extra = 0;
|
|
53
|
+
for (const phase of run.phases) {
|
|
54
|
+
if (phase.loop && typeof phase.loop.maxRounds === "number" && phase.loop.maxRounds > 1) {
|
|
55
|
+
const templateTaskCount = phase.taskIds.length;
|
|
56
|
+
extra += (phase.loop.maxRounds - 1) * templateTaskCount;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return extra;
|
|
60
|
+
}
|
package/dist/onramp.js
CHANGED
|
@@ -131,6 +131,12 @@ function buildDoctorOnramp(options = {}) {
|
|
|
131
131
|
command: "cw demo tamper",
|
|
132
132
|
reason: "Shows the core trust check without an agent or a repo."
|
|
133
133
|
},
|
|
134
|
+
{
|
|
135
|
+
id: "demo-bundle",
|
|
136
|
+
title: "Prove portable checks",
|
|
137
|
+
command: "cw demo bundle",
|
|
138
|
+
reason: "Shows the bundle proof in one step — create, check, forge, detect. No agent needed."
|
|
139
|
+
},
|
|
134
140
|
{
|
|
135
141
|
id: "setup",
|
|
136
142
|
title: "Check setup",
|
|
@@ -163,6 +169,25 @@ function buildDoctorOnramp(options = {}) {
|
|
|
163
169
|
}
|
|
164
170
|
]
|
|
165
171
|
},
|
|
172
|
+
{
|
|
173
|
+
id: "no-agent",
|
|
174
|
+
title: "No Agent?",
|
|
175
|
+
summary: "The demo steps above need no agent. A real report needs one. Pick and install one agent before the next step.",
|
|
176
|
+
actions: [
|
|
177
|
+
{
|
|
178
|
+
id: "agent-claude",
|
|
179
|
+
title: "Claude Code (npm)",
|
|
180
|
+
command: "npm install -g @anthropic-ai/claude-code",
|
|
181
|
+
reason: "First-party Claude Code tool; CW works with Claude Code, Codex, Gemini CLI, and OpenCode."
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "agent-check",
|
|
185
|
+
title: "Check agent setup",
|
|
186
|
+
command: "cw doctor",
|
|
187
|
+
reason: "Doctor names missing agent or setup trouble and tells you what to fix."
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
},
|
|
166
191
|
{
|
|
167
192
|
id: "change-loop",
|
|
168
193
|
title: "Change Loop",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.formatOperatorStatus = formatOperatorStatus;
|
|
4
|
+
exports.formatOperatorSummary = formatOperatorSummary;
|
|
4
5
|
exports.formatOperatorReport = formatOperatorReport;
|
|
5
6
|
exports.formatOperatorGraph = formatOperatorGraph;
|
|
6
7
|
exports.formatWorkerSummary = formatWorkerSummary;
|
|
@@ -11,18 +12,10 @@ exports.formatMultiAgentSummary = formatMultiAgentSummary;
|
|
|
11
12
|
exports.formatTopologySummary = formatTopologySummary;
|
|
12
13
|
exports.formatMultiAgentTrustAudit = formatMultiAgentTrustAudit;
|
|
13
14
|
const multi_agent_operator_ux_1 = require("../multi-agent-operator-ux");
|
|
15
|
+
const term_1 = require("../term");
|
|
14
16
|
function formatOperatorStatus(summary) {
|
|
15
|
-
const operator = summary.multiAgentOperator;
|
|
16
17
|
return [
|
|
17
|
-
|
|
18
|
-
`Workflow: ${summary.workflowId}${summary.appId ? ` (${summary.appId}@${summary.appVersion || "unknown"})` : ""}`,
|
|
19
|
-
`Loop Stage: ${summary.loopStage}`,
|
|
20
|
-
`Active Phase: ${summary.activePhase || "none"}`,
|
|
21
|
-
`Blocked: ${summary.blocked ? summary.blockedReasons.join("; ") : "no"}`,
|
|
22
|
-
`Tasks: ${formatCounts(summary.tasks.byStatus)}; total=${summary.tasks.total}`,
|
|
23
|
-
"",
|
|
24
|
-
"Phases",
|
|
25
|
-
...summary.phases.map((phase) => ` ${phase.name}: ${phase.status} (${phase.tasks.completed}/${phase.tasks.total} completed)`),
|
|
18
|
+
formatOperatorSummary(summary),
|
|
26
19
|
"",
|
|
27
20
|
formatWorkerPanel(summary.workers),
|
|
28
21
|
"",
|
|
@@ -37,9 +30,9 @@ function formatOperatorStatus(summary) {
|
|
|
37
30
|
formatMultiAgentPanel(summary.multiAgent),
|
|
38
31
|
"",
|
|
39
32
|
"Multi-Agent Operator UX",
|
|
40
|
-
` active=${operator.activeMultiAgentRunIds.join(", ") || "none"}; topologies=${operator.topologyRunIds.join(", ") || "none"}; blocked=${operator.blocked ? "yes" : "no"}`,
|
|
41
|
-
` dependencies=${operator.dependencies.length}; failures=${operator.failures.length}; adoptedEvidence=${operator.adoptedEvidence.length}; missingEvidence=${operator.missingEvidence.length}${operator.inspectableEvidence.length ? ` (inspectable=${operator.inspectableEvidence.length})` : ""}`,
|
|
42
|
-
` next=${operator.nextAction}`,
|
|
33
|
+
` active=${operator(summary).activeMultiAgentRunIds.join(", ") || "none"}; topologies=${operator(summary).topologyRunIds.join(", ") || "none"}; blocked=${operator(summary).blocked ? "yes" : "no"}`,
|
|
34
|
+
` dependencies=${operator(summary).dependencies.length}; failures=${operator(summary).failures.length}; adoptedEvidence=${operator(summary).adoptedEvidence.length}; missingEvidence=${operator(summary).missingEvidence.length}${operator(summary).inspectableEvidence.length ? ` (inspectable=${operator(summary).inspectableEvidence.length})` : ""}`,
|
|
35
|
+
` next=${operator(summary).nextAction}`,
|
|
43
36
|
"",
|
|
44
37
|
formatBlackboardPanel(summary.blackboard),
|
|
45
38
|
"",
|
|
@@ -47,10 +40,23 @@ function formatOperatorStatus(summary) {
|
|
|
47
40
|
"",
|
|
48
41
|
formatMultiAgentTrustAudit(summary.multiAgentTrust),
|
|
49
42
|
"",
|
|
50
|
-
`Report: ${summary.reportPath}
|
|
43
|
+
`Report: ${summary.reportPath}`
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
function operator(summary) { return summary.multiAgentOperator; }
|
|
47
|
+
/** Compact summary — the default `cw status` output. `cw status --verbose` shows the full panel. */
|
|
48
|
+
function formatOperatorSummary(summary) {
|
|
49
|
+
return [
|
|
50
|
+
`Run: ${summary.runId}`,
|
|
51
|
+
`Workflow: ${summary.workflowId}${summary.appId ? ` (${summary.appId}@${summary.appVersion || "unknown"})` : ""}`,
|
|
52
|
+
`Phase: ${summary.activePhase || "none"} | Stage: ${summary.loopStage} | Blocked: ${summary.blocked ? summary.blockedReasons.join("; ") : "no"}`,
|
|
53
|
+
`Tasks: ${formatCounts(summary.tasks.byStatus)}; total=${summary.tasks.total}`,
|
|
54
|
+
...summary.phases.map((phase) => ` ${phase.name}: ${phase.status} (${phase.tasks.completed}/${phase.tasks.total} completed)`),
|
|
51
55
|
"",
|
|
52
56
|
"Next Action",
|
|
53
|
-
...formatRecommendations(summary.nextActions)
|
|
57
|
+
...formatRecommendations(summary.nextActions),
|
|
58
|
+
"",
|
|
59
|
+
(0, term_1.dim)(`(use --verbose for full worker/candidate/feedback/commit/trust panels)`)
|
|
54
60
|
].join("\n");
|
|
55
61
|
}
|
|
56
62
|
function formatOperatorReport(summary) {
|
package/dist/operator-ux.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.formatMultiAgentTrustAudit = exports.formatTopologySummary = exports.formatMultiAgentSummary = exports.formatCommitSummary = exports.formatFeedbackSummary = exports.formatCandidateSummary = exports.formatWorkerSummary = exports.formatOperatorGraph = exports.formatOperatorReport = exports.formatOperatorStatus = void 0;
|
|
6
|
+
exports.formatMultiAgentTrustAudit = exports.formatTopologySummary = exports.formatMultiAgentSummary = exports.formatCommitSummary = exports.formatFeedbackSummary = exports.formatCandidateSummary = exports.formatWorkerSummary = exports.formatOperatorGraph = exports.formatOperatorReport = exports.formatOperatorSummary = exports.formatOperatorStatus = void 0;
|
|
7
7
|
exports.summarizeOperatorRun = summarizeOperatorRun;
|
|
8
8
|
exports.adviseNoRun = adviseNoRun;
|
|
9
9
|
exports.summarizeOperatorWorkers = summarizeOperatorWorkers;
|
|
@@ -619,6 +619,7 @@ function safeId(value) {
|
|
|
619
619
|
// surface.
|
|
620
620
|
var format_1 = require("./operator-ux/format");
|
|
621
621
|
Object.defineProperty(exports, "formatOperatorStatus", { enumerable: true, get: function () { return format_1.formatOperatorStatus; } });
|
|
622
|
+
Object.defineProperty(exports, "formatOperatorSummary", { enumerable: true, get: function () { return format_1.formatOperatorSummary; } });
|
|
622
623
|
Object.defineProperty(exports, "formatOperatorReport", { enumerable: true, get: function () { return format_1.formatOperatorReport; } });
|
|
623
624
|
Object.defineProperty(exports, "formatOperatorGraph", { enumerable: true, get: function () { return format_1.formatOperatorGraph; } });
|
|
624
625
|
Object.defineProperty(exports, "formatWorkerSummary", { enumerable: true, get: function () { return format_1.formatWorkerSummary; } });
|