cool-workflow 0.1.84 → 0.1.86
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 +47 -331
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/architecture-review-fast/workflow.js +2 -0
- 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/capability-core.js +220 -5
- package/dist/capability-registry.js +90 -51
- package/dist/cli/command-surface.js +47 -2
- package/dist/doctor.js +37 -1
- package/dist/mcp/tool-call.js +428 -0
- package/dist/mcp/tool-definitions.js +1027 -0
- package/dist/mcp-surface.js +5 -1416
- package/dist/onramp.js +421 -0
- package/dist/orchestrator.js +20 -15
- package/dist/run-export.js +139 -1
- package/dist/telemetry-demo.js +119 -0
- package/dist/types/report-bundle.js +6 -0
- package/dist/types.js +1 -0
- package/dist/version.js +1 -1
- package/dist/workbench-host.js +1 -8
- package/docs/agent-delegation-drive.7.md +23 -2
- package/docs/cli-mcp-parity.7.md +38 -7
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/dogfood-one-real-repo.7.md +9 -1
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/getting-started.md +40 -2
- package/docs/index.md +51 -37
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +23 -7
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +5 -1
- package/docs/release-history.md +342 -0
- package/docs/release-tooling.7.md +18 -0
- package/docs/report-verifiable-bundle.7.md +123 -0
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +4 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +2 -1
- package/scripts/architecture-review-fast.js +44 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +55 -133
- package/scripts/golden-path.js +4 -4
- package/scripts/onramp-check.js +47 -0
- package/scripts/parity-check.js +740 -2
- package/scripts/release-check.js +2 -0
- package/scripts/source-context.js +31 -5
- package/scripts/version-sync-check.js +4 -2
package/dist/run-export.js
CHANGED
|
@@ -16,19 +16,26 @@ exports.importRun = importRun;
|
|
|
16
16
|
exports.verifyImportedRun = verifyImportedRun;
|
|
17
17
|
exports.inspectArchive = inspectArchive;
|
|
18
18
|
exports.importManifestPath = importManifestPath;
|
|
19
|
+
exports.verifyReportBundle = verifyReportBundle;
|
|
19
20
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
20
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
21
23
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
22
24
|
const state_1 = require("./state");
|
|
23
25
|
const version_1 = require("./version");
|
|
24
26
|
const telemetry_ledger_1 = require("./telemetry-ledger");
|
|
27
|
+
const telemetry_attestation_1 = require("./telemetry-attestation");
|
|
25
28
|
const trust_audit_1 = require("./trust-audit");
|
|
26
29
|
const compare_1 = require("./compare");
|
|
27
30
|
/** Export a run to a portable JSON archive with run-local bytes and digests. */
|
|
28
|
-
function exportRun(run, outputPath) {
|
|
31
|
+
function exportRun(run, outputPath, options = {}) {
|
|
29
32
|
const exportedAt = new Date().toISOString();
|
|
30
33
|
const files = collectArchiveFiles(run);
|
|
31
34
|
const manifestSha256 = digestManifest(files);
|
|
35
|
+
// Embed ONLY a public key. resolveTrustPublicKey normalizes an inline PEM or a
|
|
36
|
+
// file path down to inline PEM bytes so the archive is self-contained; a value
|
|
37
|
+
// that resolves to nothing (bad path) yields no trust block rather than a throw.
|
|
38
|
+
const trustPublicKeyPem = (0, telemetry_attestation_1.resolveTrustPublicKey)(options.trustPublicKey);
|
|
32
39
|
const exported = {
|
|
33
40
|
schemaVersion: 1,
|
|
34
41
|
exportedAt,
|
|
@@ -39,6 +46,7 @@ function exportRun(run, outputPath) {
|
|
|
39
46
|
fileCount: files.length,
|
|
40
47
|
manifestSha256
|
|
41
48
|
},
|
|
49
|
+
...(trustPublicKeyPem ? { trust: { publicKeyPem: trustPublicKeyPem, algorithm: "ed25519" } } : {}),
|
|
42
50
|
// Legacy field retained so old readers still find an artifact-ish list.
|
|
43
51
|
artifacts: files
|
|
44
52
|
.filter((file) => file.role === "artifact")
|
|
@@ -62,6 +70,7 @@ function exportRun(run, outputPath) {
|
|
|
62
70
|
artifactCount: files.filter((file) => file.role === "artifact").length,
|
|
63
71
|
auditFileCount: files.filter((file) => file.role === "audit").length,
|
|
64
72
|
telemetryIncluded: files.some((file) => file.role === "telemetry"),
|
|
73
|
+
trustKeyEmbedded: Boolean(trustPublicKeyPem),
|
|
65
74
|
manifestSha256,
|
|
66
75
|
archiveSha256
|
|
67
76
|
};
|
|
@@ -276,6 +285,135 @@ function inspectArchive(archivePath) {
|
|
|
276
285
|
function importManifestPath(run) {
|
|
277
286
|
return node_path_1.default.join(run.paths.runDir, "import-manifest.json");
|
|
278
287
|
}
|
|
288
|
+
/** Verify a portable run bundle OFFLINE and SELF-CONTAINED: prove the archive bytes,
|
|
289
|
+
* the telemetry hash chain, the trust-audit chain, and (with the bundle's embedded
|
|
290
|
+
* public key) the ed25519 signatures — WITHOUT a source repo, a pre-existing .cw
|
|
291
|
+
* tree, or an out-of-band key. Reuses the existing import + restore-verify path: it
|
|
292
|
+
* restores into an auto-cleaned tmpdir so a stranger's machine is left untouched.
|
|
293
|
+
* Never throws — every failure is a structured check and a false `ok` (exit-1 worthy).
|
|
294
|
+
*
|
|
295
|
+
* Key precedence is bundle > argument > environment, so the artifact verifies the
|
|
296
|
+
* same on any machine; only when the bundle omits a key do the override/env apply. */
|
|
297
|
+
function verifyReportBundle(archivePath, options = {}) {
|
|
298
|
+
const inspect = inspectArchive(archivePath);
|
|
299
|
+
const failedChecks = inspect.checks
|
|
300
|
+
.filter((check) => !check.pass)
|
|
301
|
+
.map((check) => ({ name: check.name, code: check.code }));
|
|
302
|
+
const base = {
|
|
303
|
+
schemaVersion: 1,
|
|
304
|
+
archivePath,
|
|
305
|
+
runId: inspect.runId,
|
|
306
|
+
ok: false,
|
|
307
|
+
archiveOk: inspect.ok,
|
|
308
|
+
telemetryVerified: false,
|
|
309
|
+
trustAuditVerified: false,
|
|
310
|
+
trustKeySource: "none",
|
|
311
|
+
signatureKeyProvided: false,
|
|
312
|
+
signaturesChecked: 0,
|
|
313
|
+
signaturesReverified: 0,
|
|
314
|
+
signaturesFailed: 0,
|
|
315
|
+
failedChecks
|
|
316
|
+
};
|
|
317
|
+
// A bundle that is not even a supported archive cannot be restored — report the
|
|
318
|
+
// inspection failure and stop (fail-closed, no tmpdir, nothing written).
|
|
319
|
+
if (!inspect.schemaSupported)
|
|
320
|
+
return base;
|
|
321
|
+
// Read the embedded trust key (and, if requested, the report bytes) straight from
|
|
322
|
+
// the archive JSON. inspectArchive already proved the bytes parse + digest-match.
|
|
323
|
+
let bundleKey;
|
|
324
|
+
let reportContent;
|
|
325
|
+
try {
|
|
326
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(archivePath, "utf8"));
|
|
327
|
+
bundleKey = raw.trust?.publicKeyPem;
|
|
328
|
+
if (options.extractReportTo) {
|
|
329
|
+
const reportFile = (raw.files || []).find((file) => file.relativePath === "report.md");
|
|
330
|
+
if (reportFile)
|
|
331
|
+
reportContent = Buffer.from(reportFile.contentBase64, "base64").toString("utf8");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
/* inspect already recorded the parse failure; treat key as absent */
|
|
336
|
+
}
|
|
337
|
+
const trustKeySource = bundleKey
|
|
338
|
+
? "bundle"
|
|
339
|
+
: options.pubkey
|
|
340
|
+
? "argument"
|
|
341
|
+
: process.env.CW_AGENT_ATTEST_PUBKEY
|
|
342
|
+
? "environment"
|
|
343
|
+
: "none";
|
|
344
|
+
const trustKey = (0, telemetry_attestation_1.resolveTrustPublicKey)(bundleKey || options.pubkey || process.env.CW_AGENT_ATTEST_PUBKEY);
|
|
345
|
+
const tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "cw-verify-bundle-"));
|
|
346
|
+
let telemetryVerified = false;
|
|
347
|
+
let trustAuditVerified = false;
|
|
348
|
+
let signaturesChecked = 0;
|
|
349
|
+
let signaturesReverified = 0;
|
|
350
|
+
let signaturesFailed = 0;
|
|
351
|
+
let reportExtractedTo;
|
|
352
|
+
try {
|
|
353
|
+
// Restore into the throwaway tree. importRun digest-checks every file and throws
|
|
354
|
+
// on the first mismatch (or on a stripped integrity block under
|
|
355
|
+
// CW_REQUIRE_ARCHIVE_INTEGRITY=1) — caught below as a failed "restore" check.
|
|
356
|
+
const imported = importRun(archivePath, tmpDir);
|
|
357
|
+
for (const check of imported.verification.checks) {
|
|
358
|
+
if (check.name === "telemetry-ledger")
|
|
359
|
+
telemetryVerified = check.pass;
|
|
360
|
+
if (check.name === "trust-audit")
|
|
361
|
+
trustAuditVerified = check.pass;
|
|
362
|
+
if (!check.pass)
|
|
363
|
+
failedChecks.push({ name: check.name, code: check.code });
|
|
364
|
+
}
|
|
365
|
+
// Independent ed25519 re-verification over the restored ledger using the bundle's
|
|
366
|
+
// own key (or override/env). With no key, attested records degrade to
|
|
367
|
+
// informational (signaturesFailed stays 0); the chain check above still gates ok.
|
|
368
|
+
const ledger = (0, telemetry_ledger_1.verifyTelemetryLedger)(imported.run);
|
|
369
|
+
const sig = (0, telemetry_attestation_1.verifyTelemetrySignatures)(ledger.records, trustKey);
|
|
370
|
+
signaturesChecked = sig.checked;
|
|
371
|
+
signaturesReverified = sig.reverified;
|
|
372
|
+
signaturesFailed = sig.failed;
|
|
373
|
+
for (const check of sig.checks)
|
|
374
|
+
if (!check.pass)
|
|
375
|
+
failedChecks.push({ name: check.name, code: check.code });
|
|
376
|
+
if (options.extractReportTo && reportContent !== undefined) {
|
|
377
|
+
reportExtractedTo = node_path_1.default.resolve(options.extractReportTo);
|
|
378
|
+
node_fs_1.default.writeFileSync(reportExtractedTo, reportContent);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
failedChecks.push({ name: "restore", code: messageOf(error) });
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
386
|
+
}
|
|
387
|
+
// Strict mode: a bundle that claims attested telemetry but offers no key to check
|
|
388
|
+
// it is unverifiable — refuse rather than green it.
|
|
389
|
+
const strictShortfall = Boolean(options.strictSignatures) && signaturesChecked > 0 && !trustKey;
|
|
390
|
+
if (strictShortfall)
|
|
391
|
+
failedChecks.push({ name: "signatures", code: "signature-key-required" });
|
|
392
|
+
// Extraction was requested but could not be fulfilled (no report.md in the bundle,
|
|
393
|
+
// or the write failed): fail closed rather than silently green a missing artifact —
|
|
394
|
+
// otherwise `report bundle <run> --extract-report r.md && send r.md` would ship
|
|
395
|
+
// nothing (or a stale file) with exit 0. A requested-but-absent output is a failure,
|
|
396
|
+
// not a no-op (distinct from extraction never being requested).
|
|
397
|
+
const extractShortfall = Boolean(options.extractReportTo) && !reportExtractedTo;
|
|
398
|
+
if (extractShortfall)
|
|
399
|
+
failedChecks.push({ name: "extract-report", code: "report-md-unavailable" });
|
|
400
|
+
return {
|
|
401
|
+
schemaVersion: 1,
|
|
402
|
+
archivePath,
|
|
403
|
+
runId: inspect.runId,
|
|
404
|
+
ok: inspect.ok && telemetryVerified && trustAuditVerified && signaturesFailed === 0 && !strictShortfall && !extractShortfall,
|
|
405
|
+
archiveOk: inspect.ok,
|
|
406
|
+
telemetryVerified,
|
|
407
|
+
trustAuditVerified,
|
|
408
|
+
trustKeySource,
|
|
409
|
+
signatureKeyProvided: Boolean(trustKey),
|
|
410
|
+
signaturesChecked,
|
|
411
|
+
signaturesReverified,
|
|
412
|
+
signaturesFailed,
|
|
413
|
+
reportExtractedTo,
|
|
414
|
+
failedChecks
|
|
415
|
+
};
|
|
416
|
+
}
|
|
279
417
|
function collectArchiveFiles(run) {
|
|
280
418
|
const entries = new Map();
|
|
281
419
|
for (const file of walkFiles(run.paths.runDir)) {
|
package/dist/telemetry-demo.js
CHANGED
|
@@ -22,12 +22,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
22
22
|
exports.formatTelemetryVerify = formatTelemetryVerify;
|
|
23
23
|
exports.formatTamperDemo = formatTamperDemo;
|
|
24
24
|
exports.runTamperDemo = runTamperDemo;
|
|
25
|
+
exports.runBundleDemo = runBundleDemo;
|
|
26
|
+
exports.formatBundleDemo = formatBundleDemo;
|
|
25
27
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
26
28
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
27
29
|
const node_os_1 = __importDefault(require("node:os"));
|
|
28
30
|
const node_path_1 = __importDefault(require("node:path"));
|
|
29
31
|
const telemetry_ledger_1 = require("./telemetry-ledger");
|
|
30
32
|
const telemetry_attestation_1 = require("./telemetry-attestation");
|
|
33
|
+
const run_export_1 = require("./run-export");
|
|
34
|
+
const state_1 = require("./state");
|
|
31
35
|
const execution_backend_1 = require("./execution-backend");
|
|
32
36
|
/** Human-facing render of `telemetry verify <run>`. */
|
|
33
37
|
function formatTelemetryVerify(r) {
|
|
@@ -164,3 +168,118 @@ function runTamperDemo(options = {}) {
|
|
|
164
168
|
layers.every((l) => l.before.verified && !l.after.verified && l.failures.length > 0);
|
|
165
169
|
return { schemaVersion: 1, runId, workers: HOPS.length, trustKey: "ephemeral-ed25519", baseline, layers, proven };
|
|
166
170
|
}
|
|
171
|
+
function runBundleDemo(options = {}) {
|
|
172
|
+
const workdir = options.dir || node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "cw-bundle-demo-"));
|
|
173
|
+
node_fs_1.default.mkdirSync(workdir, { recursive: true });
|
|
174
|
+
const runId = "demo-bundle-run";
|
|
175
|
+
const runDir = node_path_1.default.join(workdir, ".cw", "runs", runId);
|
|
176
|
+
const paths = (0, state_1.createRunPaths)(runDir);
|
|
177
|
+
(0, state_1.ensureRunDirs)(paths);
|
|
178
|
+
const { publicKey, privateKey } = node_crypto_1.default.generateKeyPairSync("ed25519");
|
|
179
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
180
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
181
|
+
// Build a real signed ledger + a cited report, the way an attested run would.
|
|
182
|
+
const ledgerRun = { id: runId, paths };
|
|
183
|
+
for (const hop of HOPS) {
|
|
184
|
+
const ctx = { runId, taskId: hop.taskId, promptDigest: hop.promptDigest };
|
|
185
|
+
(0, telemetry_ledger_1.appendTelemetryAttestation)(ledgerRun, {
|
|
186
|
+
workerId: hop.workerId,
|
|
187
|
+
taskId: hop.taskId,
|
|
188
|
+
promptDigest: hop.promptDigest,
|
|
189
|
+
reportedUsage: hop.usage,
|
|
190
|
+
usageSignature: hop.attestation === "attested" ? (0, telemetry_attestation_1.signTelemetry)(hop.usage, privateKeyPem, ctx) : undefined,
|
|
191
|
+
attestation: hop.attestation,
|
|
192
|
+
now: DEMO_NOW
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(runDir, "report.md"), "# Architecture review\n\nRisk: src/server.js:18 — unauthenticated route.\n", "utf8");
|
|
196
|
+
const attestedCount = HOPS.filter((h) => h.attestation === "attested").length;
|
|
197
|
+
const fullRun = {
|
|
198
|
+
schemaVersion: 1, id: runId, createdAt: DEMO_NOW, updatedAt: DEMO_NOW, cwd: workdir,
|
|
199
|
+
workflow: { id: "demo", title: "Demo", summary: "", limits: { maxAgents: 1, maxConcurrentAgents: 1 } },
|
|
200
|
+
inputs: { question: "what are the risks?" }, loopStage: "interpret",
|
|
201
|
+
phases: [], tasks: [], dispatches: [], commits: [], paths, nodes: [], contracts: []
|
|
202
|
+
};
|
|
203
|
+
(0, state_1.saveCheckpoint)(fullRun);
|
|
204
|
+
const ledgerFile = (0, telemetry_ledger_1.telemetryLedgerPath)(ledgerRun);
|
|
205
|
+
const cleanLedger = node_fs_1.default.readFileSync(ledgerFile, "utf8");
|
|
206
|
+
const exportSealed = (out) => { (0, run_export_1.exportRun)(fullRun, out, { trustPublicKey: publicKeyPem }); };
|
|
207
|
+
// Baseline: a clean sealed bundle verifies offline; the embedded key reverifies
|
|
208
|
+
// every signed hop.
|
|
209
|
+
const cleanBundle = node_path_1.default.join(workdir, "clean.cwrun.json");
|
|
210
|
+
exportSealed(cleanBundle);
|
|
211
|
+
const clean = (0, run_export_1.verifyReportBundle)(cleanBundle);
|
|
212
|
+
const baseline = { ok: clean.ok, telemetryVerified: clean.telemetryVerified, signaturesReverified: clean.signaturesReverified };
|
|
213
|
+
const layers = [];
|
|
214
|
+
// CHAIN forgery: flip record[1]'s verdict and reseal its recordHash; record[2]'s
|
|
215
|
+
// prevHash still points at the original hash, so the chain breaks — even though
|
|
216
|
+
// every archive file digest (computed at export over the tampered bytes) is valid.
|
|
217
|
+
// This is exactly what inspect-archive alone cannot catch.
|
|
218
|
+
{
|
|
219
|
+
const j = JSON.parse(cleanLedger);
|
|
220
|
+
j.records[1].attestation = "attested";
|
|
221
|
+
const { recordHash: _drop, ...rest } = j.records[1];
|
|
222
|
+
j.records[1].recordHash = (0, telemetry_ledger_1.computeRecordHash)(rest);
|
|
223
|
+
node_fs_1.default.writeFileSync(ledgerFile, JSON.stringify(j, null, 2));
|
|
224
|
+
const forged = node_path_1.default.join(workdir, "forged-chain.cwrun.json");
|
|
225
|
+
exportSealed(forged);
|
|
226
|
+
const after = (0, run_export_1.verifyReportBundle)(forged);
|
|
227
|
+
node_fs_1.default.writeFileSync(ledgerFile, cleanLedger);
|
|
228
|
+
layers.push({
|
|
229
|
+
layer: "chain",
|
|
230
|
+
tamper: `forged record[1] verdict "unattested" -> "attested" and resealed its recordHash; the archive's own file digests stay valid`,
|
|
231
|
+
before: { ok: clean.ok, detail: `${clean.signaturesReverified} signed hop(s) reverify; chain intact` },
|
|
232
|
+
after: { ok: after.ok, detail: after.telemetryVerified ? "telemetry chain still verified (UNDETECTED!)" : "the embedded hash chain broke at the next record" },
|
|
233
|
+
failures: after.ok ? [] : after.failedChecks.map((c) => `${c.name}: ${c.code}`)
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// SIGNATURE forgery: inflate the last attested hop's reported tokens and reseal its
|
|
237
|
+
// usage digest + recordHash so the chain AND archive digests still verify; only the
|
|
238
|
+
// ed25519 signature (over the original usage) no longer matches the inflated number.
|
|
239
|
+
{
|
|
240
|
+
const j = JSON.parse(cleanLedger);
|
|
241
|
+
const idx = j.records.length - 1;
|
|
242
|
+
j.records[idx].reportedUsage = { ...j.records[idx].reportedUsage, output_tokens: j.records[idx].reportedUsage.output_tokens * 10 };
|
|
243
|
+
j.records[idx].reportedUsageDigest = (0, telemetry_ledger_1.reportedUsageDigest)(j.records[idx].reportedUsage);
|
|
244
|
+
const { recordHash: _drop, ...rest } = j.records[idx];
|
|
245
|
+
j.records[idx].recordHash = (0, telemetry_ledger_1.computeRecordHash)(rest);
|
|
246
|
+
node_fs_1.default.writeFileSync(ledgerFile, JSON.stringify(j, null, 2));
|
|
247
|
+
const forged = node_path_1.default.join(workdir, "forged-sig.cwrun.json");
|
|
248
|
+
exportSealed(forged);
|
|
249
|
+
const after = (0, run_export_1.verifyReportBundle)(forged);
|
|
250
|
+
node_fs_1.default.writeFileSync(ledgerFile, cleanLedger);
|
|
251
|
+
layers.push({
|
|
252
|
+
layer: "signature",
|
|
253
|
+
tamper: `inflated the last attested hop's output_tokens 10x and resealed its digest + recordHash; the chain stays valid`,
|
|
254
|
+
before: { ok: clean.ok, detail: `the embedded public key reverifies the original signature` },
|
|
255
|
+
after: { ok: after.ok, detail: after.signaturesFailed > 0 ? `${after.signaturesFailed} signature(s) failed ed25519 reverify` : "signature still verified (UNDETECTED!)" },
|
|
256
|
+
failures: after.ok ? [] : after.failedChecks.map((c) => `${c.name}: ${c.code}`)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (!options.keepDir && !options.dir)
|
|
260
|
+
node_fs_1.default.rmSync(workdir, { recursive: true, force: true });
|
|
261
|
+
const proven = baseline.ok &&
|
|
262
|
+
baseline.telemetryVerified &&
|
|
263
|
+
baseline.signaturesReverified === attestedCount &&
|
|
264
|
+
layers.every((l) => l.before.ok && !l.after.ok && l.failures.length > 0);
|
|
265
|
+
return { schemaVersion: 1, runId, workers: HOPS.length, trustKey: "ephemeral-ed25519", baseline, layers, proven };
|
|
266
|
+
}
|
|
267
|
+
function formatBundleDemo(r) {
|
|
268
|
+
const lines = [];
|
|
269
|
+
lines.push(`cw demo bundle — portable-bundle verification proof (hermetic, ${r.trustKey} key)`);
|
|
270
|
+
lines.push("");
|
|
271
|
+
lines.push(`▶ Exported a sealed report bundle: ${r.workers} hops, public key embedded`);
|
|
272
|
+
lines.push(` ${r.baseline.ok ? "✓" : "✗"} bundle verifies offline ${r.baseline.signaturesReverified} signed hop(s) reverify with only the embedded public key`);
|
|
273
|
+
for (const l of r.layers) {
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push(`▶ ${l.layer.toUpperCase()} forgery`);
|
|
276
|
+
lines.push(` edit: ${l.tamper}`);
|
|
277
|
+
lines.push(` before: ${l.before.ok ? "✓ verifies" : "✗"} — ${l.before.detail}`);
|
|
278
|
+
lines.push(` after: ${l.after.ok ? "✓ (UNDETECTED!)" : "✗ DETECTED"} — ${l.after.detail}`);
|
|
279
|
+
}
|
|
280
|
+
lines.push("");
|
|
281
|
+
lines.push(r.proven
|
|
282
|
+
? "VERDICT: bundle verification holds ✓ — every forgery caught offline with only the bundle's embedded public key. No repo, no server, no key handed over."
|
|
283
|
+
: "VERDICT: PROOF FAILED ✗ — a forged bundle verified. This is a regression in the bundle guarantee.");
|
|
284
|
+
return lines.join("\n");
|
|
285
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Verifiable report bundle — result shapes shared across the run-export verifier,
|
|
3
|
+
// the capability-core producer (reportBundle), and the quickstart auto-bundle field
|
|
4
|
+
// on QuickstartResult. Kept in types/ (a leaf) so types/drive.ts can reference
|
|
5
|
+
// ReportBundleResult without a types->core import cycle.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/types.js
CHANGED
|
@@ -28,6 +28,7 @@ __exportStar(require("./types/sandbox"), exports);
|
|
|
28
28
|
__exportStar(require("./types/execution-backend"), exports);
|
|
29
29
|
__exportStar(require("./types/boundary"), exports);
|
|
30
30
|
__exportStar(require("./types/drive"), exports);
|
|
31
|
+
__exportStar(require("./types/report-bundle"), exports);
|
|
31
32
|
__exportStar(require("./types/multi-agent"), exports);
|
|
32
33
|
__exportStar(require("./types/topology"), exports);
|
|
33
34
|
__exportStar(require("./types/blackboard"), exports);
|
package/dist/version.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA_VERSION = exports.CURRENT_RUN_STATE_SCHEMA_VERSION = exports.WORKFLOW_APP_SCHEMA_VERSION = exports.CURRENT_COOL_WORKFLOW_VERSION = void 0;
|
|
4
|
-
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.
|
|
4
|
+
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.86";
|
|
5
5
|
exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
|
|
6
6
|
exports.CURRENT_RUN_STATE_SCHEMA_VERSION = 1;
|
|
7
7
|
exports.LEGACY_RUN_STATE_SCHEMA_VERSION = 0;
|
package/dist/workbench-host.js
CHANGED
|
@@ -99,14 +99,7 @@ class WorkbenchHost {
|
|
|
99
99
|
}
|
|
100
100
|
const runMatch = /^\/api\/run\/([^/]+)$/.exec(route);
|
|
101
101
|
if (runMatch) {
|
|
102
|
-
|
|
103
|
-
process.chdir(this.cwd);
|
|
104
|
-
try {
|
|
105
|
-
return this.send(res, 200, (0, workbench_1.buildWorkbenchRunView)(this.runner, runMatch[1]));
|
|
106
|
-
}
|
|
107
|
-
finally {
|
|
108
|
-
process.chdir(previousCwd);
|
|
109
|
-
}
|
|
102
|
+
return this.send(res, 200, (0, workbench_1.buildWorkbenchRunView)(this.runner.withBaseDir(this.cwd), runMatch[1]));
|
|
110
103
|
}
|
|
111
104
|
this.send(res, 404, { error: `no such read-only view: ${route}` });
|
|
112
105
|
}
|
|
@@ -128,6 +128,7 @@ node dist/cli.js backend agent config # show the effective config (se
|
|
|
128
128
|
node dist/cli.js backend probe agent --json # ready iff configured, else unverified
|
|
129
129
|
|
|
130
130
|
# drive a real repo end-to-end (zero hand-written result.md)
|
|
131
|
+
node dist/cli.js quickstart architecture-review --check --repo /path/to/repo --question "Is the design sound?" --agent-command "node $(pwd)/scripts/agents/claude-p-agent.js {{input}} {{result}}"
|
|
131
132
|
node dist/cli.js run architecture-review --drive --repo /path/to/repo --question "Is the design sound?"
|
|
132
133
|
node dist/cli.js run architecture-review --drive --once --repo /path/to/repo --question "..." # one step
|
|
133
134
|
node dist/cli.js run drive <run-id> --json # read-only preview of the next step
|
|
@@ -135,12 +136,22 @@ node dist/cli.js run drive <run-id> --json # read-only preview of the next
|
|
|
135
136
|
# quickstart --resume: a guided stop-then-resume a newcomer can WITNESS in <5 min
|
|
136
137
|
node dist/cli.js quickstart --resume --repo /path/to/repo --question "..." # advances ONE step, prints a continue line
|
|
137
138
|
node dist/cli.js quickstart --run <run-id> --resume # continues that run to completion
|
|
139
|
+
node dist/cli.js quickstart --run <run-id> --resume --bundle # continues, then seals a completed run
|
|
138
140
|
```
|
|
139
141
|
|
|
142
|
+
`quickstart --check` is a zero-write preflight. It does not make a run, write
|
|
143
|
+
`.cw/`, call the agent, write a report, or commit. It checks the app id, repo,
|
|
144
|
+
question, agent config, and (with `--bundle`) the trust-key shape, then gives the
|
|
145
|
+
next command to run. A blocked check exits non-zero, so scripts may use it as a
|
|
146
|
+
gate before a real run.
|
|
147
|
+
|
|
140
148
|
`quickstart --resume` with no `--run` drives a single step and prints a
|
|
141
149
|
copy-pasteable `cw quickstart --run <id> --resume` continue line; run it again with
|
|
142
150
|
the `--run <id>` to finish. The continuing invocation echoes `resumedFrom: <id>`.
|
|
143
151
|
Bare `quickstart` (no `--resume`) is unchanged — it drives straight to the end.
|
|
152
|
+
When `--bundle` is present on the fresh resume step, no bundle is sealed until
|
|
153
|
+
the run is complete; the continue line keeps `--bundle` so the second command
|
|
154
|
+
finishes and seals the report.
|
|
144
155
|
|
|
145
156
|
For faster first results, use the opt-in fast app in place of changing the full
|
|
146
157
|
review contract:
|
|
@@ -171,9 +182,15 @@ an Assess cache hit. A cache hit still goes through `recordWorkerOutput`
|
|
|
171
182
|
validation; a corrupt cached result parks/fails closed rather than spawning a
|
|
172
183
|
quiet fallback.
|
|
173
184
|
|
|
185
|
+
Verify and Verdict also get the source-context instruction so they do not have to
|
|
186
|
+
start by scanning the repo again. They are not result-cached; they still have to
|
|
187
|
+
cite evidence and make the final check from the accepted Map and Assess work.
|
|
188
|
+
|
|
174
189
|
`--metrics` is diagnostic and opt-in. It adds elapsed milliseconds, step counts,
|
|
175
|
-
agent-spawn counts,
|
|
176
|
-
|
|
190
|
+
agent-spawn counts, `result-cache` hit counts, source-context bytes/digest, and
|
|
191
|
+
one row per driven task with phase, task id, elapsed time, and spawn/cache state
|
|
192
|
+
to the wrapper JSON payload; without it, the wrapper's default output shape stays
|
|
193
|
+
unchanged.
|
|
177
194
|
|
|
178
195
|
`{{manifest}}`, `{{input}}`, `{{result}}`, `{{workerDir}}`, `{{model}}`, and
|
|
179
196
|
`{{prompt}}` are put into DISCRETE argv elements (never a shell-interpreted
|
|
@@ -270,3 +287,7 @@ Loaders fail closed on corrupt state; store writes are made safe under more than
|
|
|
270
287
|
## Privacy Release (v0.1.84)
|
|
271
288
|
|
|
272
289
|
No other change to this page in v0.1.84.
|
|
290
|
+
|
|
291
|
+
0.1.85
|
|
292
|
+
|
|
293
|
+
0.1.86
|
package/docs/cli-mcp-parity.7.md
CHANGED
|
@@ -64,6 +64,16 @@ ISO timestamps from the moment of generation. The `--json` payload is the
|
|
|
64
64
|
contract, and it is the same bytes the MCP tool gives back. The human text view
|
|
65
65
|
is policy put on top; it never changes the payload.
|
|
66
66
|
|
|
67
|
+
Some payload checks need more than `cwd` or `runId`. The registry may name a
|
|
68
|
+
scenario probe for a safe local case. A scenario gets two new temp workspaces,
|
|
69
|
+
one for CLI and one for MCP, sets up the same state, runs one capability through
|
|
70
|
+
each door, and then compares the payload after taking out temp roots, run ids,
|
|
71
|
+
time stamps, and chain hashes that are made by that workspace. This is used for
|
|
72
|
+
local deterministic work such as app show/validate/package, topology
|
|
73
|
+
show/validate/apply/summary/graph, sandbox show/validate/choose/resolve, state
|
|
74
|
+
summary refresh/show, `plan`, `approve`, `reject`, `comment.add`, `handoff`, and
|
|
75
|
+
`review.policy`.
|
|
76
|
+
|
|
67
77
|
## The Parity Matrix
|
|
68
78
|
|
|
69
79
|
The matrix below is made from the live registry — one row per capability,
|
|
@@ -72,7 +82,7 @@ relationship. `identical` means `cw <cmd> --json` is equal to the `cw_<tool>`
|
|
|
72
82
|
payload; `projected` means a declared divergence with a reason; `cli-only` marks
|
|
73
83
|
a surface-specific capability with a recorded reason. The matrix is
|
|
74
84
|
<!-- gen:parity:count -->
|
|
75
|
-
machine-complete by design:
|
|
85
|
+
machine-complete by design: 193 capabilities, 186 MCP tools.
|
|
76
86
|
<!-- /gen:parity:count -->
|
|
77
87
|
|
|
78
88
|
<!-- gen:parity:table -->
|
|
@@ -236,6 +246,8 @@ machine-complete by design: 190 capabilities, 184 MCP tools.
|
|
|
236
246
|
| `run.import` | `cw run import` | `cw_run_import` | `runImportArchive` | both | identical |
|
|
237
247
|
| `run.verify-import` | `cw run verify-import` | `cw_run_verify_import` | `runVerifyImport` | both | identical |
|
|
238
248
|
| `run.inspect-archive` | `cw run inspect-archive` | `cw_run_inspect_archive` | `runInspectArchive` | both | identical |
|
|
249
|
+
| `report.verify-bundle` | `cw report verify-bundle` | `cw_report_verify_bundle` | `runVerifyReportBundle` | both | identical |
|
|
250
|
+
| `report.bundle` | `cw report bundle` | `cw_report_bundle` | `reportBundle` | both | identical |
|
|
239
251
|
| `run.drive` | `cw run drive` | `cw_run_drive` | `runDrivePreview` | both | identical |
|
|
240
252
|
| `run.drive.step` | `cw run drive` | `cw_run_drive_step` | `runDrive` | both | projected |
|
|
241
253
|
| `quickstart` | `cw quickstart` | `—` | `quickstart` | cli-only | cli-only |
|
|
@@ -256,6 +268,7 @@ machine-complete by design: 190 capabilities, 184 MCP tools.
|
|
|
256
268
|
| `gc.verify` | `cw gc verify` | `cw_gc_verify` | `gcVerify` | both | identical |
|
|
257
269
|
| `telemetry.verify` | `cw telemetry verify` | `cw_telemetry_verify` | `telemetryVerify` | both | identical |
|
|
258
270
|
| `demo.tamper` | `cw demo tamper` | `—` | `demoTamper` | cli-only | cli-only |
|
|
271
|
+
| `demo.bundle` | `cw demo bundle` | `—` | `demoBundle` | cli-only | cli-only |
|
|
259
272
|
| `history` | `cw history` | `cw_history` | `runRegistry.history` | both | identical |
|
|
260
273
|
| `workbench.view` | `cw workbench view` | `cw_workbench_view` | `buildWorkbenchRunView` | both | identical |
|
|
261
274
|
| `workbench.serve` | `cw workbench serve` | `cw_workbench_serve` | `buildWorkbenchServeDescriptor` | both | projected |
|
|
@@ -282,14 +295,15 @@ A capability may be on one surface only, but never without word of it — it mus
|
|
|
282
295
|
carry a recorded reason in the registry.
|
|
283
296
|
|
|
284
297
|
<!-- gen:parity:cliOnly -->
|
|
285
|
-
|
|
298
|
+
Seven capabilities are CLI-only:
|
|
286
299
|
|
|
287
300
|
- `help` — Human help text. MCP hosts enumerate capabilities via tools/list, not a help command.
|
|
288
301
|
- `doctor` — 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`.
|
|
289
302
|
- `loop` — Convenience alias of `schedule create` with kind=loop. MCP hosts use cw_schedule_create with kind=loop.
|
|
290
303
|
- `schedule daemon` — Long-running desktop daemon process, not a request/response tool. MCP hosts drive ticks via cw_schedule_due + cw_schedule_run_now.
|
|
291
|
-
- `quickstart` — CLI UX convenience layer (newcomer first value in one command) over the existing run.drive.step + report verbs; it spawns nothing new and delegates worker execution to the operator's agent backend. MCP hosts compose the same outcome from cw_run_drive_step + cw_report. `audit-run` is a CLI-only alias of the same wrapper.
|
|
304
|
+
- `quickstart` — CLI UX convenience layer (newcomer first value in one command) over the existing run.drive.step + report verbs; it spawns nothing new and delegates worker execution to the operator's agent backend. MCP hosts compose the same outcome from cw_run_drive_step + cw_report (+ cw_report_bundle for --bundle). `audit-run` is a CLI-only alias of the same wrapper.
|
|
292
305
|
- `demo tamper` — Human-facing demonstration (operator/newcomer onboarding); the underlying integrity check is exposed programmatically as the both-surface telemetry.verify. No agent or MCP client needs to invoke a demo.
|
|
306
|
+
- `demo bundle` — Human-facing demonstration (operator/newcomer onboarding); the underlying integrity check is exposed programmatically as the both-surface report.verify-bundle. No agent or MCP client needs to invoke a demo.
|
|
293
307
|
<!-- /gen:parity:cliOnly -->
|
|
294
308
|
|
|
295
309
|
<!-- gen:parity:projected -->
|
|
@@ -328,10 +342,23 @@ of the rules above.
|
|
|
328
342
|
registry ⇄ CLI ⇄ MCP coverage (every declared capability is found on its declared
|
|
329
343
|
surfaces and nothing live is undeclared), makes sure `--json` output is equal to
|
|
330
344
|
the MCP payload for every `payloadIdentical` capability, makes sure of the
|
|
331
|
-
declared `commit` projection,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
345
|
+
declared `commit` projection, checks that safe write/multi-argument capabilities
|
|
346
|
+
are named scenario probes rather than deferred work, and makes sure of
|
|
347
|
+
fail-closed behavior by putting in drift — a peer taken away, an undeclared tool,
|
|
348
|
+
an exception with no reason, a bad probe classification, a changed payload — and
|
|
349
|
+
checking that the gate says no to each one. It is part of `npm test` and
|
|
350
|
+
`npm run release:check`.
|
|
351
|
+
|
|
352
|
+
The scenario probes now cover local worker/candidate/feedback read and write
|
|
353
|
+
paths. They make temp runs, send out a worker, write a fixed `result.md`, score
|
|
354
|
+
and pick a candidate, and make feedback from local state. No outside agent is
|
|
355
|
+
run; the probe checks only that CLI and MCP carry the same JSON to the same CW
|
|
356
|
+
core.
|
|
357
|
+
|
|
358
|
+
They also cover local state-node read, snapshot, diff, replay, and replay-check
|
|
359
|
+
paths from the same temp run. Snapshot and replay ids are made by CW, so the
|
|
360
|
+
parity check sets aside only those made ids and timestamps before it compares the
|
|
361
|
+
JSON.
|
|
335
362
|
|
|
336
363
|
In CW, parity is not a custom; it is a built, declared, and kept property of the
|
|
337
364
|
build. It is not done till it is put in the docs and tested.
|
|
@@ -466,3 +493,7 @@ Loaders fail closed on corrupt state; store writes are made safe under more than
|
|
|
466
493
|
## Privacy Release (v0.1.84)
|
|
467
494
|
|
|
468
495
|
No other change to this page in v0.1.84.
|
|
496
|
+
|
|
497
|
+
0.1.85
|
|
498
|
+
|
|
499
|
+
0.1.86
|
|
@@ -89,12 +89,20 @@ report, and audit tools.
|
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
91
|
node test/dogfood-release-smoke.js
|
|
92
|
+
node test/dogfood-architecture-review-smoke.js
|
|
92
93
|
```
|
|
93
94
|
|
|
94
95
|
The smoke test runs `scripts/dogfood-release.js --smoke --json`. It still
|
|
95
96
|
uses the real repository, `release-cut`, worker manifests, trust audit records,
|
|
96
97
|
candidate scoring, selection, verifier-gated commit, and a report, but keeps the
|
|
97
|
-
command set smaller so it does not do recursive release checking.
|
|
98
|
+
command set smaller so it does not do recursive release checking. Smoke mode
|
|
99
|
+
does not run `canonical-apps` or `golden-path`; `npm test` runs those checks as
|
|
100
|
+
separate smokes.
|
|
101
|
+
|
|
102
|
+
The architecture-review smoke runs beside it. That smoke uses a stub agent to
|
|
103
|
+
drive the real `architecture-review` app to a report and audit proof. Keeping it
|
|
104
|
+
as a second smoke lets the parallel gate run both dogfood halves at the same
|
|
105
|
+
time while keeping the same proof.
|
|
98
106
|
|
|
99
107
|
## Promote To Real Release Actions
|
|
100
108
|
|
package/docs/getting-started.md
CHANGED
|
@@ -17,6 +17,8 @@ start a run:
|
|
|
17
17
|
```bash
|
|
18
18
|
node scripts/cw.js doctor # human-readable
|
|
19
19
|
node scripts/cw.js doctor --json # stable payload for scripts
|
|
20
|
+
node scripts/cw.js doctor --onramp # short path for users and code work
|
|
21
|
+
node scripts/cw.js doctor --onramp --changed-from origin/main
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
It checks the Node version (v18+), whether an agent backend is set up (and its
|
|
@@ -26,6 +28,22 @@ read-only — it makes nothing on disk. It exits non-zero only on a blocking
|
|
|
26
28
|
problem; a missing agent is a warning (you are still able to run `demo` and
|
|
27
29
|
`--preview`).
|
|
28
30
|
|
|
31
|
+
Use `--onramp` when you are not certain what to do next. It keeps the main path
|
|
32
|
+
small:
|
|
33
|
+
|
|
34
|
+
1. `cw demo tamper` - prove the trust check with no agent.
|
|
35
|
+
2. `cw quickstart architecture-review --check ...` - check a real run with no
|
|
36
|
+
writes.
|
|
37
|
+
3. `cw quickstart architecture-review ...` - make the report.
|
|
38
|
+
4. `cw quickstart architecture-review ... --bundle` - make a portable report
|
|
39
|
+
file for another person.
|
|
40
|
+
5. `cw report verify-bundle report.cwrun.json` - check that file offline.
|
|
41
|
+
6. `npm run test:fast` - use the fast code check while you work.
|
|
42
|
+
7. `npm run release:check` - use the full gate only when the batch is ready.
|
|
43
|
+
|
|
44
|
+
Add `--changed-from origin/main` in a source checkout to get the nearest smoke
|
|
45
|
+
tests and guard checks for your current change.
|
|
46
|
+
|
|
29
47
|
Make a run with a canonical workflow app:
|
|
30
48
|
|
|
31
49
|
```bash
|
|
@@ -73,17 +91,37 @@ node scripts/cw.js eval report .cw/evals/<suite-id>/replay-run.json
|
|
|
73
91
|
node scripts/cw.js report <run-id> --show
|
|
74
92
|
```
|
|
75
93
|
|
|
76
|
-
Run the
|
|
94
|
+
Run the smallest check that fits the change:
|
|
77
95
|
|
|
78
96
|
```bash
|
|
79
97
|
npm run check
|
|
80
|
-
npm
|
|
98
|
+
npm run build
|
|
99
|
+
node test/<nearest-smoke>.js
|
|
100
|
+
npm run onramp:check
|
|
101
|
+
npm run test:fast
|
|
102
|
+
npm test # slow serial backstop
|
|
81
103
|
npm run canonical-apps
|
|
82
104
|
npm run golden-path
|
|
83
105
|
npm run eval:replay
|
|
84
106
|
npm run fixture-compat
|
|
85
107
|
```
|
|
86
108
|
|
|
109
|
+
When a test run is slow, make a read-only timing report:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm run test:ci -- --json-summary /tmp/cw-test-summary.json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use the `slowest` list in that file to choose one test-speed cycle. This is a
|
|
116
|
+
guide, not a release gate.
|
|
117
|
+
|
|
118
|
+
For a CLI or MCP surface change, also run:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm run parity:check
|
|
122
|
+
npm run gen:manifests -- --check
|
|
123
|
+
```
|
|
124
|
+
|
|
87
125
|
Before you cut a release, run the full dry-run gate:
|
|
88
126
|
|
|
89
127
|
```bash
|