cool-workflow 0.1.81 → 0.1.83
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 +2 -2
- package/.codex-plugin/plugin.json +4 -4
- package/README.md +125 -121
- 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/candidate-scoring.js +20 -26
- package/dist/capability-core.js +80 -93
- package/dist/capability-registry.js +30 -3
- package/dist/cli.js +12 -1
- package/dist/commit.js +217 -204
- package/dist/coordinator/util.js +6 -9
- package/dist/dispatch.js +11 -3
- package/dist/doctor.js +153 -0
- package/dist/evidence-reasoning.js +4 -1
- package/dist/execution-backend/agent.js +11 -48
- package/dist/execution-backend.js +11 -31
- package/dist/gates.js +48 -0
- package/dist/mcp-server.js +11 -0
- package/dist/multi-agent/helpers.js +6 -10
- package/dist/multi-agent/ids.js +20 -0
- package/dist/multi-agent-eval.js +27 -1
- package/dist/multi-agent-host.js +53 -21
- package/dist/multi-agent-operator-ux.js +2 -1
- package/dist/multi-agent-trust.js +5 -5
- package/dist/node-projection.js +59 -0
- package/dist/node-snapshot.js +8 -18
- package/dist/orchestrator/lifecycle-operations.js +22 -1
- package/dist/orchestrator.js +29 -2
- package/dist/reclamation/hash.js +72 -0
- package/dist/reclamation.js +31 -112
- package/dist/run-registry/queue.js +6 -7
- package/dist/run-registry.js +35 -24
- package/dist/scheduler.js +112 -57
- package/dist/topology.js +25 -4
- package/dist/trust-audit.js +70 -38
- package/dist/validation.js +328 -0
- package/dist/version.js +1 -1
- package/dist/worker-isolation.js +163 -58
- package/docs/agent-delegation-drive.7.md +90 -85
- package/docs/agent-framework.md +33 -32
- package/docs/candidate-scoring.7.md +26 -24
- package/docs/canonical-workflow-apps.7.md +40 -40
- package/docs/capability-topology-registry.7.md +24 -24
- package/docs/cli-mcp-parity.7.md +227 -154
- package/docs/contract-migration-tooling.7.md +49 -41
- package/docs/control-plane-scheduling.7.md +45 -40
- package/docs/coordinator-blackboard.7.md +30 -30
- package/docs/dogfood-one-real-repo.7.md +44 -44
- package/docs/durable-state-and-locking.7.md +35 -30
- package/docs/end-to-end-golden-path.7.md +29 -29
- package/docs/error-feedback.7.md +27 -27
- package/docs/evidence-adoption-reasoning-chain.7.md +63 -58
- package/docs/execution-backends.7.md +84 -79
- package/docs/getting-started.md +35 -18
- package/docs/index.md +3 -3
- package/docs/mcp-app-surface.7.md +64 -64
- package/docs/multi-agent-cli-mcp-surface.7.md +83 -77
- package/docs/multi-agent-eval-replay-harness.7.md +59 -54
- package/docs/multi-agent-operator-ux.7.md +70 -65
- package/docs/multi-agent-runtime-core.7.md +39 -39
- package/docs/multi-agent-topologies.7.md +24 -24
- package/docs/multi-agent-trust-policy-audit.7.md +38 -38
- package/docs/node-snapshot-diff-replay.7.md +26 -21
- package/docs/observability-cost-accounting.7.md +50 -45
- package/docs/operator-ux.7.md +30 -30
- package/docs/pipeline-runner.7.md +31 -31
- package/docs/project-index.md +15 -6
- package/docs/real-execution-backends.7.md +47 -42
- package/docs/release-and-migration.7.md +43 -38
- package/docs/release-tooling.7.md +74 -39
- package/docs/routines.md +16 -16
- package/docs/run-registry-control-plane.7.md +120 -115
- package/docs/run-retention-reclamation.7.md +46 -41
- package/docs/sandbox-profiles.7.md +32 -32
- package/docs/scheduled-tasks.md +14 -14
- package/docs/security-trust-hardening.7.md +29 -29
- package/docs/source-context-profiles.7.md +28 -28
- package/docs/state-explosion-management.7.md +64 -59
- package/docs/state-node.7.md +8 -8
- package/docs/team-collaboration.7.md +63 -58
- package/docs/trust-model.md +126 -126
- package/docs/unix-principles.md +80 -80
- package/docs/vendor-manifest-loadability.7.md +20 -20
- package/docs/verifier-gated-commit.7.md +16 -16
- package/docs/web-desktop-workbench.7.md +70 -65
- package/docs/worker-isolation.7.md +34 -37
- package/docs/workflow-app-framework.7.md +38 -38
- package/manifest/plugin.manifest.json +4 -4
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +3 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +58 -0
- package/scripts/children/http-delegate-child.js +39 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/gen-parity-doc.js +106 -0
- package/scripts/golden-path.js +4 -4
- package/scripts/release-flow.js +181 -5
- package/dist/verifier-registry.js +0 -46
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "pr-review-fix-ci",
|
|
4
4
|
"title": "PR Review Fix CI",
|
|
5
5
|
"summary": "Review a pull request or branch, inspect CI failures, diagnose actionable issues, optionally patch, verify, and summarize with evidence.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.83",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "release-cut",
|
|
4
4
|
"title": "Release Cut",
|
|
5
5
|
"summary": "Prepare a release with checklist discipline: version checks, changelog, tests, packaging, release notes, and final verification.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.83",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "research-synthesis",
|
|
4
4
|
"title": "Research Synthesis",
|
|
5
5
|
"summary": "Split a research question into claims, investigate sources, cross-check evidence, verify claims, and synthesize a concise answer.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.83",
|
|
7
7
|
"author": "COOLWHITE LLC",
|
|
8
8
|
"inputs": [
|
|
9
9
|
{
|
|
@@ -21,6 +21,8 @@ const state_node_1 = require("./state-node");
|
|
|
21
21
|
const trust_audit_1 = require("./trust-audit");
|
|
22
22
|
const collaboration_1 = require("./collaboration");
|
|
23
23
|
const compare_1 = require("./compare");
|
|
24
|
+
const gates_1 = require("./gates");
|
|
25
|
+
const validation_1 = require("./validation");
|
|
24
26
|
exports.CANDIDATE_SCHEMA_VERSION = 1;
|
|
25
27
|
/** Verdict thresholds on a score's normalized value [0,1], declared once so the
|
|
26
28
|
* numbers carry intent instead of being buried as literals in verdictFor(). A
|
|
@@ -110,7 +112,10 @@ function getCandidate(run, candidateId) {
|
|
|
110
112
|
const file = candidateFile(run, candidateId);
|
|
111
113
|
if (!node_fs_1.default.existsSync(file))
|
|
112
114
|
return undefined;
|
|
113
|
-
|
|
115
|
+
// Fail-closed integrity boundary (F4/F5): validate the parsed record against
|
|
116
|
+
// its type def BEFORE upserting it as a trusted CandidateRecord. A corrupt or
|
|
117
|
+
// forged candidate.json must throw here rather than flow into the run.
|
|
118
|
+
const candidate = (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
|
|
114
119
|
upsertCandidate(run, candidate);
|
|
115
120
|
return candidate;
|
|
116
121
|
}
|
|
@@ -245,10 +250,11 @@ function selectCandidate(run, candidateId, options = {}, scoringOptions = {}) {
|
|
|
245
250
|
else if (!verifierNode.evidence.length) {
|
|
246
251
|
failures.push(error("candidate-selection-missing-evidence", `Candidate ${candidateId} verifier node has no evidence`));
|
|
247
252
|
}
|
|
248
|
-
else if (emptyCaptureWarning(run, verifierNode)) {
|
|
249
|
-
// HARD no-false-green gate (v0.1.43) —
|
|
250
|
-
// (
|
|
251
|
-
//
|
|
253
|
+
else if ((0, gates_1.emptyCaptureWarning)(run, verifierNode)) {
|
|
254
|
+
// HARD no-false-green gate (v0.1.43) — selection and the commit gate now
|
|
255
|
+
// share ONE emptyCaptureWarning (src/gates.ts), so they CANNOT drift: a
|
|
256
|
+
// verifier node whose backing result was an empty-capture is unselectable
|
|
257
|
+
// here for the same reason it is uncommittable, by construction.
|
|
252
258
|
failures.push(error("candidate-selection-empty-capture", `Candidate ${candidateId} verifier node has no real evidence (empty-capture result)`));
|
|
253
259
|
}
|
|
254
260
|
}
|
|
@@ -312,7 +318,7 @@ function selectCandidate(run, candidateId, options = {}, scoringOptions = {}) {
|
|
|
312
318
|
scoreCriteria: bestScore?.criteria,
|
|
313
319
|
verifierNodeId: candidate.verifierNodeId,
|
|
314
320
|
evidenceCount: mergeEvidence(candidate.evidence, verifierNode?.evidence || []).length,
|
|
315
|
-
sandboxProfileId: sandboxProfileForCandidate(run, candidate),
|
|
321
|
+
sandboxProfileId: (0, gates_1.sandboxProfileForCandidate)(run, candidate),
|
|
316
322
|
workerId: candidate.workerId,
|
|
317
323
|
commitGateResult: "passed"
|
|
318
324
|
}),
|
|
@@ -528,7 +534,10 @@ function loadCandidatesFromDisk(run) {
|
|
|
528
534
|
.filter((entry) => entry.isDirectory() && entry.name !== "selections")
|
|
529
535
|
.map((entry) => node_path_1.default.join(candidateRoot(run), entry.name, "candidate.json"))
|
|
530
536
|
.filter((file) => node_fs_1.default.existsSync(file))
|
|
531
|
-
|
|
537
|
+
// Fail-closed integrity boundary (F4/F5): each candidate.json is validated
|
|
538
|
+
// against CandidateRecord before it merges into the run; a corrupt record
|
|
539
|
+
// throws rather than entering the candidate set as a trusted cast.
|
|
540
|
+
.map((file) => (0, validation_1.validateCandidateRecord)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8"))));
|
|
532
541
|
}
|
|
533
542
|
function readScores(run, candidateId) {
|
|
534
543
|
const dir = node_path_1.default.join(candidateDir(run, candidateId), "scores");
|
|
@@ -538,7 +547,10 @@ function readScores(run, candidateId) {
|
|
|
538
547
|
.readdirSync(dir)
|
|
539
548
|
.filter((file) => file.endsWith(".json"))
|
|
540
549
|
.sort()
|
|
541
|
-
|
|
550
|
+
// Fail-closed integrity boundary (F4/F5): a score file is validated against
|
|
551
|
+
// CandidateScore before it can feed ranking/selection. A corrupt score must
|
|
552
|
+
// throw, not silently widen the normalized/verdict surface the gate reads.
|
|
553
|
+
.map((file) => (0, validation_1.validateCandidateScore)(JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), "utf8"))));
|
|
542
554
|
}
|
|
543
555
|
function candidateArtifacts(run, candidate) {
|
|
544
556
|
return [
|
|
@@ -661,17 +673,6 @@ function error(code, message, options = {}) {
|
|
|
661
673
|
details: options.details
|
|
662
674
|
};
|
|
663
675
|
}
|
|
664
|
-
/** HARD no-false-green gate (v0.1.43) — kept in SYNC with commit.ts. Traces a
|
|
665
|
-
* verifier node back to its source result node and returns the empty-capture
|
|
666
|
-
* marker (set at ingest via isEmptyCapture) when present. Reads ONLY persisted
|
|
667
|
-
* state, so selection replays deterministically. */
|
|
668
|
-
function emptyCaptureWarning(run, verifierNode) {
|
|
669
|
-
const resultNodeId = (typeof verifierNode.inputs?.inputNodeId === "string" ? verifierNode.inputs.inputNodeId : undefined) ||
|
|
670
|
-
verifierNode.parents[0];
|
|
671
|
-
const resultNode = resultNodeId ? run.nodes?.find((node) => node.id === resultNodeId) : undefined;
|
|
672
|
-
const warning = resultNode?.metadata?.captureWarning;
|
|
673
|
-
return typeof warning === "string" && warning ? warning : undefined;
|
|
674
|
-
}
|
|
675
676
|
function mergeCandidates(left, right) {
|
|
676
677
|
const merged = [...left];
|
|
677
678
|
for (const candidate of right) {
|
|
@@ -708,13 +709,6 @@ function mergeEvidence(left, right) {
|
|
|
708
709
|
}
|
|
709
710
|
return merged;
|
|
710
711
|
}
|
|
711
|
-
function sandboxProfileForCandidate(run, candidate) {
|
|
712
|
-
const worker = candidate.workerId ? (run.workers || []).find((entry) => entry.id === candidate.workerId) : undefined;
|
|
713
|
-
if (worker?.sandboxProfileId)
|
|
714
|
-
return worker.sandboxProfileId;
|
|
715
|
-
const task = candidate.taskId ? (run.tasks || []).find((entry) => entry.id === candidate.taskId) : undefined;
|
|
716
|
-
return task?.sandboxProfileId;
|
|
717
|
-
}
|
|
718
712
|
function unique(values) {
|
|
719
713
|
return Array.from(new Set(values.filter(Boolean)));
|
|
720
714
|
}
|
package/dist/capability-core.js
CHANGED
|
@@ -198,18 +198,16 @@ function flag(value) {
|
|
|
198
198
|
return false;
|
|
199
199
|
return Boolean(value);
|
|
200
200
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
process.chdir(previous);
|
|
212
|
-
}
|
|
201
|
+
// F7: explicit invocation cwd — no more process.chdir bracket.
|
|
202
|
+
// The runner resolves a run from process.cwd() by default; to operate WITH a run's
|
|
203
|
+
// repo as base (run --drive --repo X from anywhere; cross-directory quickstart; run
|
|
204
|
+
// import/export against a target dir) we now pass that base EXPLICITLY —
|
|
205
|
+
// runner.withBaseDir(dir).loadRun(...) for run resolution (see
|
|
206
|
+
// CoolWorkflowRunner.withBaseDir), and invocationCwd(args) to anchor relative path
|
|
207
|
+
// args. Nothing mutates the global process.cwd(), so concurrent in-process callers
|
|
208
|
+
// can no longer corrupt each other's working directory (the former reentrancy hazard).
|
|
209
|
+
function invocationCwd(args) {
|
|
210
|
+
return node_path_1.default.resolve(optionalString(args.cwd) || process.cwd());
|
|
213
211
|
}
|
|
214
212
|
function runRegistryRefresh(reg, args) {
|
|
215
213
|
return reg.refresh({ scope: scopeOf(args, "repo") });
|
|
@@ -277,36 +275,33 @@ function runRerun(reg, runId, args) {
|
|
|
277
275
|
return reg.rerun(runId, { scope: scopeOf(args, "home"), reason: optionalString(args.reason) });
|
|
278
276
|
}
|
|
279
277
|
function runExportArchive(runner, runId, args) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
});
|
|
278
|
+
const base = invocationCwd(args);
|
|
279
|
+
const output = optionalString(args.output || args.path || args.archive) || `${runId}.cwrun.json`;
|
|
280
|
+
return (0, run_export_1.exportRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId), node_path_1.default.resolve(base, output));
|
|
284
281
|
}
|
|
285
282
|
function runImportArchive(runner, args) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
});
|
|
283
|
+
const base = invocationCwd(args);
|
|
284
|
+
const archive = optionalString(args.archive || args.path || args.file);
|
|
285
|
+
if (!archive)
|
|
286
|
+
throw new Error("run import requires an archive path (positional, --archive, --path, or --file)");
|
|
287
|
+
const target = node_path_1.default.resolve(base, optionalString(args.target || args.repo || args.cwd) || base);
|
|
288
|
+
const imported = (0, run_export_1.importRun)(node_path_1.default.resolve(base, archive), target);
|
|
289
|
+
const registry = new run_registry_1.RunRegistry(target, runner.withBaseDir(target));
|
|
290
|
+
const registryReport = registry.refresh({ scope: "repo" });
|
|
291
|
+
return { ...imported, registry: registryReport };
|
|
296
292
|
}
|
|
297
293
|
// Read-only: inspect a portable archive's integrity WITHOUT importing it. Routes
|
|
298
294
|
// both surfaces through one shared core entry. The runner is unused (no registry
|
|
299
295
|
// touch — inspection writes nothing) but kept for dispatch-signature symmetry.
|
|
300
296
|
function runInspectArchive(_runner, args) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
});
|
|
297
|
+
const base = invocationCwd(args);
|
|
298
|
+
const archive = optionalString(args.archive || args.path || args.file);
|
|
299
|
+
if (!archive)
|
|
300
|
+
throw new Error("run inspect-archive requires an archive path (positional, --archive, --path, or --file)");
|
|
301
|
+
return (0, run_export_1.inspectArchive)(node_path_1.default.resolve(base, archive));
|
|
307
302
|
}
|
|
308
303
|
function runVerifyImport(runner, runId, args) {
|
|
309
|
-
return
|
|
304
|
+
return (0, run_export_1.verifyImportedRun)(runner.withBaseDir(optionalString(args.cwd)).loadRun(runId));
|
|
310
305
|
}
|
|
311
306
|
function queueAdd(reg, args) {
|
|
312
307
|
return reg.queueAdd({
|
|
@@ -338,13 +333,12 @@ function queueShow(reg, id) {
|
|
|
338
333
|
// ---- control-plane scheduling (v0.1.37) -----------------------------------
|
|
339
334
|
function loadSchedulingPolicy(reg) {
|
|
340
335
|
const file = reg.schedulingPolicyPath();
|
|
336
|
+
// Absent policy => conservative DEFAULT (an unconfigured backend, which §4
|
|
337
|
+
// permits). But a PRESENT-but-corrupt policy must fail closed: silently
|
|
338
|
+
// substituting defaults would schedule under settings the operator never
|
|
339
|
+
// chose while their broken file sits on disk. Let readJson's throw surface it.
|
|
341
340
|
if (node_fs_1.default.existsSync(file)) {
|
|
342
|
-
|
|
343
|
-
return { policy: (0, scheduling_1.normalizeSchedulingPolicy)((0, state_1.readJson)(file)), source: "file" };
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
/* fall through to default */
|
|
347
|
-
}
|
|
341
|
+
return { policy: (0, scheduling_1.normalizeSchedulingPolicy)((0, state_1.readJson)(file)), source: "file" };
|
|
348
342
|
}
|
|
349
343
|
return { policy: scheduling_1.DEFAULT_SCHEDULING_POLICY, source: "default" };
|
|
350
344
|
}
|
|
@@ -405,8 +399,17 @@ function schedPolicySet(reg, args) {
|
|
|
405
399
|
const current = loadSchedulingPolicy(reg).policy;
|
|
406
400
|
const patch = {};
|
|
407
401
|
for (const key of ["maxConcurrent", "maxAttempts", "leaseTtlMs", "backoffBaseMs", "backoffFactor", "backoffCapMs"]) {
|
|
408
|
-
if (args[key]
|
|
409
|
-
|
|
402
|
+
if (args[key] === undefined)
|
|
403
|
+
continue;
|
|
404
|
+
// Fail closed on a non-numeric flag instead of letting normalizeSchedulingPolicy
|
|
405
|
+
// silently substitute the DEFAULT (which would report source:"file" + exit 0,
|
|
406
|
+
// so the operator believes they set a value they didn't). Matches the
|
|
407
|
+
// Number.isFinite guard the sibling reclaimPolicyFrom already uses.
|
|
408
|
+
const value = Number(args[key]);
|
|
409
|
+
if (!Number.isFinite(value)) {
|
|
410
|
+
throw new Error(`Invalid --${key} "${String(args[key])}": expected a number (e.g. --${key} 4)`);
|
|
411
|
+
}
|
|
412
|
+
patch[key] = value;
|
|
410
413
|
}
|
|
411
414
|
const policy = (0, scheduling_1.normalizeSchedulingPolicy)({ ...current, ...patch });
|
|
412
415
|
(0, state_1.writeJson)(reg.schedulingPolicyPath(), policy);
|
|
@@ -458,26 +461,25 @@ function planInputsFor(args) {
|
|
|
458
461
|
* the golden path runs each verb from the repo dir) — then restores cwd. This lets
|
|
459
462
|
* `cw run <app> --drive --repo X` work when invoked from anywhere. */
|
|
460
463
|
function runDrive(runner, args) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (!
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
runId = run.id;
|
|
471
|
-
repoCwd = run.cwd;
|
|
472
|
-
}
|
|
473
|
-
if (repoCwd && repoCwd !== process.cwd() && node_fs_1.default.existsSync(repoCwd))
|
|
474
|
-
process.chdir(repoCwd);
|
|
475
|
-
return (0, drive_1.drive)(runner, runId, { once: isTrue(args.once), now: optionalString(args.now), args });
|
|
476
|
-
}
|
|
477
|
-
finally {
|
|
478
|
-
if (process.cwd() !== cwd0)
|
|
479
|
-
process.chdir(cwd0);
|
|
464
|
+
let runId = optionalString(args.runId || args.run);
|
|
465
|
+
let repoCwd = optionalString(args.repo);
|
|
466
|
+
if (!runId) {
|
|
467
|
+
const appId = String(args.appId || args.workflowId || args.app || "");
|
|
468
|
+
if (!appId)
|
|
469
|
+
throw new Error("run --drive requires an app id (or --run <run-id> to continue)");
|
|
470
|
+
const run = runner.plan(appId, planInputsFor(args));
|
|
471
|
+
runId = run.id;
|
|
472
|
+
repoCwd = run.cwd;
|
|
480
473
|
}
|
|
474
|
+
// The runner resolves the run from its baseDir, so the drive must run WITH the
|
|
475
|
+
// run's repo as base. Pass it explicitly via withBaseDir (F7 — no process.chdir).
|
|
476
|
+
const target = repoCwd && node_fs_1.default.existsSync(repoCwd) ? repoCwd : undefined;
|
|
477
|
+
const driveRunId = runId;
|
|
478
|
+
return (0, drive_1.drive)(runner.withBaseDir(target), driveRunId, {
|
|
479
|
+
once: isTrue(args.once),
|
|
480
|
+
now: optionalString(args.now),
|
|
481
|
+
args
|
|
482
|
+
});
|
|
481
483
|
}
|
|
482
484
|
/** The app the one-command quickstart plans when none is named. */
|
|
483
485
|
exports.QUICKSTART_DEFAULT_APP = "architecture-review";
|
|
@@ -503,23 +505,16 @@ function quickstart(runner, args) {
|
|
|
503
505
|
// `--preview`: read-only, deterministic next-step projection (no spawn, no commit).
|
|
504
506
|
// Plan a fresh run (the read-only first verb) then project the next drive step.
|
|
505
507
|
if (isTrue(args.preview)) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
runId = run.id;
|
|
513
|
-
repoCwd = run.cwd;
|
|
514
|
-
}
|
|
515
|
-
if (repoCwd && repoCwd !== process.cwd() && node_fs_1.default.existsSync(repoCwd))
|
|
516
|
-
process.chdir(repoCwd);
|
|
517
|
-
return (0, drive_1.drivePreview)(runner, runId, args);
|
|
518
|
-
}
|
|
519
|
-
finally {
|
|
520
|
-
if (process.cwd() !== cwd0)
|
|
521
|
-
process.chdir(cwd0);
|
|
508
|
+
let runId = optionalString(args.runId || args.run);
|
|
509
|
+
let repoCwd = optionalString(args.repo);
|
|
510
|
+
if (!runId) {
|
|
511
|
+
const run = runner.plan(appId, planInputsFor(args));
|
|
512
|
+
runId = run.id;
|
|
513
|
+
repoCwd = run.cwd;
|
|
522
514
|
}
|
|
515
|
+
const previewRunId = runId;
|
|
516
|
+
const target = repoCwd && node_fs_1.default.existsSync(repoCwd) ? repoCwd : undefined;
|
|
517
|
+
return (0, drive_1.drivePreview)(runner.withBaseDir(target), previewRunId, args);
|
|
523
518
|
}
|
|
524
519
|
// Drive end-to-end (or one `--once` step). runDrive plans the run, delegates each
|
|
525
520
|
// worker to the agent backend, and commits — we add only the report write + a
|
|
@@ -530,23 +525,15 @@ function quickstart(runner, args) {
|
|
|
530
525
|
const result = runDrive(runner, { ...args, appId, ...(resume && !resumeRunId ? { once: true } : {}) });
|
|
531
526
|
// Always (re)write the report so the one command yields a report.md on disk, even
|
|
532
527
|
// when the drive blocked/parked (a partial report is still useful triage).
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (runRepoCwd !== process.cwd() && node_fs_1.default.existsSync(runRepoCwd))
|
|
543
|
-
process.chdir(runRepoCwd);
|
|
544
|
-
reportPath = runner.report(result.runId).path;
|
|
545
|
-
}
|
|
546
|
-
finally {
|
|
547
|
-
if (process.cwd() !== cwd0)
|
|
548
|
-
process.chdir(cwd0);
|
|
549
|
-
}
|
|
528
|
+
//
|
|
529
|
+
// runDrive restored cwd, so the runs root would resolve against the CALLER's cwd
|
|
530
|
+
// here — orphaning the run when quickstart is invoked cross-directory (cwd =
|
|
531
|
+
// plugin dir, --repo elsewhere: the README's headline command). The run's
|
|
532
|
+
// statePath (<repo>/.cw/runs/<id>/state.json) is authoritative however the run
|
|
533
|
+
// was planned or continued; resolve to ITS repo BEFORE any run read, reentrant-safe.
|
|
534
|
+
const runRepoCwd = node_path_1.default.resolve(node_path_1.default.dirname(result.statePath), "..", "..", "..");
|
|
535
|
+
const reportTarget = node_fs_1.default.existsSync(runRepoCwd) ? runRepoCwd : undefined;
|
|
536
|
+
const reportPath = runner.withBaseDir(reportTarget).report(result.runId).path;
|
|
550
537
|
let hint;
|
|
551
538
|
if (!agentConfigured) {
|
|
552
539
|
hint =
|
|
@@ -28,6 +28,7 @@ exports.CAPABILITY_REGISTRY = void 0;
|
|
|
28
28
|
exports.declaredMcpTools = declaredMcpTools;
|
|
29
29
|
exports.declaredCliTokens = declaredCliTokens;
|
|
30
30
|
exports.requiresReason = requiresReason;
|
|
31
|
+
exports.isPayloadProbeOptOut = isPayloadProbeOptOut;
|
|
31
32
|
exports.payloadIdenticalCapabilities = payloadIdenticalCapabilities;
|
|
32
33
|
exports.buildParityReport = buildParityReport;
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
@@ -51,6 +52,14 @@ const BUILTIN_CAPABILITIES = [
|
|
|
51
52
|
cli: { path: ["list"], jsonMode: "default" },
|
|
52
53
|
mcp: { tool: "cw_list" }
|
|
53
54
|
},
|
|
55
|
+
{
|
|
56
|
+
capability: "doctor",
|
|
57
|
+
summary: "Diagnose the host for setup problems (Node version, agent backend, agent binary on PATH, git, writable home/repo state) and print an actionable fix per check.",
|
|
58
|
+
entry: "runDoctor",
|
|
59
|
+
surface: "cli-only",
|
|
60
|
+
cli: { path: ["doctor"], jsonMode: "flag" },
|
|
61
|
+
reason: "Environment diagnostics are inherently local to the CLI host — Node version, $PATH, $CW_HOME/cwd writability. An MCP client diagnosing the server process's environment is not meaningful; agents already receive the same readiness facts in their typed results (e.g. status: blocked, agentConfigured). Inspired by `brew doctor`."
|
|
62
|
+
},
|
|
54
63
|
{
|
|
55
64
|
capability: "init",
|
|
56
65
|
summary: "Scaffold a new workflow definition.",
|
|
@@ -428,10 +437,28 @@ function requiresReason(cap) {
|
|
|
428
437
|
return true;
|
|
429
438
|
return false;
|
|
430
439
|
}
|
|
431
|
-
/**
|
|
432
|
-
*
|
|
440
|
+
/**
|
|
441
|
+
* Whether a `surface:"both"` capability is DOCUMENTED out of the payload-identity
|
|
442
|
+
* probe. The probe defaults capabilities IN (every both-surface, dual-bound verb —
|
|
443
|
+
* including write/complex-arg verbs) and requires an EXPLICIT, REASONED opt-out to
|
|
444
|
+
* fall out of scope. A capability escapes the probe only when it carries BOTH
|
|
445
|
+
* `payloadIdentical: false` AND a non-empty `reason`. A bare `payloadIdentical:
|
|
446
|
+
* false` with no recorded reason does NOT silently escape — it stays in the probe
|
|
447
|
+
* set so the undocumented divergence trips the gate (FAIL CLOSED). This mirrors the
|
|
448
|
+
* `requiresReason`/`reasonlessExceptions` gate, but enforces it at the probe-scope
|
|
449
|
+
* boundary too, so the marshalling-drift surface cannot be narrowed without a paper
|
|
450
|
+
* trail.
|
|
451
|
+
*/
|
|
452
|
+
function isPayloadProbeOptOut(cap) {
|
|
453
|
+
return cap.payloadIdentical === false && !!(cap.reason && cap.reason.trim());
|
|
454
|
+
}
|
|
455
|
+
/** Descriptors for the payload-identity probe. Defaults to EVERY both-surface,
|
|
456
|
+
* dual-bound capability (read OR write); a capability is excluded only by a
|
|
457
|
+
* documented opt-out (`payloadIdentical: false` + a non-empty `reason`) — see
|
|
458
|
+
* `isPayloadProbeOptOut`. Fail-closed: an undocumented `payloadIdentical: false`
|
|
459
|
+
* stays in scope so its divergence is caught, not silently excused. */
|
|
433
460
|
function payloadIdenticalCapabilities() {
|
|
434
|
-
return exports.CAPABILITY_REGISTRY.filter((cap) => cap.surface === "both" && cap.
|
|
461
|
+
return exports.CAPABILITY_REGISTRY.filter((cap) => cap.surface === "both" && cap.cli && cap.mcp && !isPayloadProbeOptOut(cap));
|
|
435
462
|
}
|
|
436
463
|
function lintRegistry() {
|
|
437
464
|
const issues = [];
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ const multi_agent_operator_ux_1 = require("./multi-agent-operator-ux");
|
|
|
21
21
|
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
22
22
|
const state_explosion_1 = require("./state-explosion");
|
|
23
23
|
const evidence_reasoning_1 = require("./evidence-reasoning");
|
|
24
|
+
const doctor_1 = require("./doctor");
|
|
24
25
|
async function main() {
|
|
25
26
|
const args = (0, orchestrator_1.parseArgv)(process.argv.slice(2));
|
|
26
27
|
const runner = new orchestrator_1.CoolWorkflowRunner({
|
|
@@ -36,6 +37,16 @@ async function main() {
|
|
|
36
37
|
case "list":
|
|
37
38
|
printJson(runner.listWorkflows());
|
|
38
39
|
return;
|
|
40
|
+
case "doctor": {
|
|
41
|
+
const report = (0, doctor_1.runDoctor)(args.options, process.env, String(args.options.cwd || process.cwd()));
|
|
42
|
+
if (wantsJson(args.options))
|
|
43
|
+
printJson(report);
|
|
44
|
+
else
|
|
45
|
+
process.stdout.write(`${(0, doctor_1.formatDoctorReport)(report)}\n`);
|
|
46
|
+
if (!report.ok)
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
39
50
|
case "init": {
|
|
40
51
|
const [workflowId] = args.positionals;
|
|
41
52
|
if (!workflowId)
|
|
@@ -1281,7 +1292,7 @@ async function main() {
|
|
|
1281
1292
|
}
|
|
1282
1293
|
function required(value, label) {
|
|
1283
1294
|
if (!value)
|
|
1284
|
-
throw new Error(`Missing ${label}
|
|
1295
|
+
throw new Error(`Missing ${label}. Run "cw.js help" for usage.`);
|
|
1285
1296
|
return value;
|
|
1286
1297
|
}
|
|
1287
1298
|
function optionalArg(value) {
|