@open-agent-toolkit/cli 0.2.77 → 0.2.79

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.
@@ -1,5 +1,24 @@
1
- import { canonicalJson } from './lib/canonical-json.mjs';
1
+ #!/usr/bin/env node
2
+
3
+ import { randomUUID } from 'node:crypto';
4
+ import {
5
+ link as linkFile,
6
+ open,
7
+ readFile,
8
+ realpath,
9
+ unlink,
10
+ } from 'node:fs/promises';
11
+ import { dirname, resolve } from 'node:path';
12
+
13
+ import { canonicalJson, hashFile } from './lib/canonical-json.mjs';
14
+ import { isDirectExecution } from './lib/cli-entry.mjs';
2
15
  import { validateArtifactShape } from './lib/contracts.mjs';
16
+ import {
17
+ assertCanonicalRoot,
18
+ assertSafeExistingPath,
19
+ assertSafeOutputPath,
20
+ assertUnchangedRoot,
21
+ } from './lib/safe-path.mjs';
3
22
 
4
23
  const requiredDispositions = new Map([
5
24
  ['semantic', 'affirmed'],
@@ -42,7 +61,7 @@ export function reconcileLedger({
42
61
  reviewResults,
43
62
  priorReference,
44
63
  runId = priorLedger?.runId,
45
- reviewerLane = 'lane-reconciliation',
64
+ reviewerLane = 'controller:reconcile-ledger-v1',
46
65
  }) {
47
66
  if (!priorLedger || priorLedger.runId !== runId || priorLedger.revision < 1) {
48
67
  throw new Error(
@@ -358,3 +377,251 @@ export function reconcileLedger({
358
377
  }
359
378
  return { ledger, reconciliation };
360
379
  }
380
+
381
+ function parseArgs(argv) {
382
+ const values = { review: [] };
383
+ for (let index = 0; index < argv.length; index += 2) {
384
+ const key = argv[index]?.replace(/^--/, '');
385
+ const value = argv[index + 1];
386
+ if (!key || value === undefined)
387
+ throw new Error('Invalid reconciliation arguments');
388
+ if (key === 'review') values.review.push(value);
389
+ else values[key] = value;
390
+ }
391
+ for (const key of [
392
+ 'manifest',
393
+ 'input-ledger',
394
+ 'output-ledger',
395
+ 'output-review',
396
+ ]) {
397
+ if (!values[key]) throw new Error(`Missing --${key}`);
398
+ }
399
+ return values;
400
+ }
401
+
402
+ async function removeCreatedPath(path) {
403
+ try {
404
+ await unlink(path);
405
+ } catch (error) {
406
+ if (error?.code !== 'ENOENT') throw error;
407
+ }
408
+ }
409
+
410
+ async function stageOutput(path, value) {
411
+ const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
412
+ const handle = await open(temporary, 'wx', 0o600);
413
+ let closed = false;
414
+ try {
415
+ await handle.writeFile(`${canonicalJson(value)}\n`, 'utf8');
416
+ await handle.sync();
417
+ await handle.close();
418
+ closed = true;
419
+ } catch (error) {
420
+ if (!closed) await handle.close().catch(() => {});
421
+ await removeCreatedPath(temporary).catch(() => {});
422
+ throw error;
423
+ }
424
+ return temporary;
425
+ }
426
+
427
+ async function writeAtomicPair(outputs, packetIdentity) {
428
+ const staged = [];
429
+ const published = [];
430
+ try {
431
+ for (const output of outputs) {
432
+ staged.push({
433
+ ...output,
434
+ temporary: await stageOutput(output.path, output.value),
435
+ });
436
+ }
437
+ await assertUnchangedRoot(packetIdentity);
438
+ for (const output of staged) {
439
+ await linkFile(output.temporary, output.path);
440
+ published.push(output.path);
441
+ }
442
+ } catch (error) {
443
+ const cleanupErrors = [];
444
+ for (const path of published.reverse()) {
445
+ try {
446
+ await removeCreatedPath(path);
447
+ } catch (cleanupError) {
448
+ cleanupErrors.push(cleanupError);
449
+ }
450
+ }
451
+ for (const { temporary } of staged) {
452
+ try {
453
+ await removeCreatedPath(temporary);
454
+ } catch (cleanupError) {
455
+ cleanupErrors.push(cleanupError);
456
+ }
457
+ }
458
+ if (cleanupErrors.length > 0) {
459
+ throw new Error(
460
+ `Reconciliation output publication failed and ${cleanupErrors.length} cleanup operation(s) also failed`,
461
+ { cause: error },
462
+ );
463
+ }
464
+ throw error;
465
+ }
466
+ for (const { temporary } of staged) {
467
+ await removeCreatedPath(temporary);
468
+ }
469
+ }
470
+
471
+ async function main(argv = process.argv.slice(2)) {
472
+ const options = parseArgs(argv);
473
+ const manifestPath = await realpath(options.manifest);
474
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
475
+ const manifestValidation = validateArtifactShape(manifest);
476
+ if (!manifestValidation.valid) {
477
+ throw new Error(
478
+ `Reconciliation rejects invalid manifest: ${manifestValidation.errors
479
+ .map((error) => error.code)
480
+ .join(', ')}`,
481
+ );
482
+ }
483
+ const declaration = manifest.execution?.reconciliation;
484
+ if (
485
+ !declaration ||
486
+ declaration.producer !== 'controller:reconcile-ledger-v1'
487
+ ) {
488
+ throw new Error(
489
+ 'Manifest does not authorize controller:reconcile-ledger-v1',
490
+ );
491
+ }
492
+ const artifacts = manifest.artifacts ?? [];
493
+ const packetRoot = dirname(manifestPath);
494
+ const packetIdentity = await assertCanonicalRoot(packetRoot);
495
+ await assertSafeExistingPath(packetRoot, resolve(options.manifest));
496
+ const expected = (path) => resolve(packetRoot, path);
497
+ const resolvedInputs = new Map();
498
+ const resolvedOutputs = new Map();
499
+ for (const [option, field] of [
500
+ ['input-ledger', 'inputLedger'],
501
+ ['output-ledger', 'outputLedger'],
502
+ ['output-review', 'outputReview'],
503
+ ]) {
504
+ if (resolve(options[option]) !== expected(declaration[field])) {
505
+ throw new Error(
506
+ `Reconciliation ${option} drifts from the manifest declaration`,
507
+ );
508
+ }
509
+ if (option === 'input-ledger') {
510
+ await assertSafeExistingPath(packetRoot, resolve(options[option]));
511
+ resolvedInputs.set(option, await realpath(options[option]));
512
+ } else {
513
+ const outputPath = resolve(options[option]);
514
+ await assertSafeExistingPath(packetRoot, dirname(outputPath));
515
+ await assertSafeOutputPath(packetRoot, outputPath);
516
+ resolvedOutputs.set(option, outputPath);
517
+ }
518
+ }
519
+ const declaredReviews = [
520
+ ...(declaration.requiredReviews ?? []),
521
+ ...(declaration.conditionalReviews ?? []).filter((path) =>
522
+ artifacts.some((reference) => reference.path === path),
523
+ ),
524
+ ];
525
+ const declaredReviewInputs = await Promise.all(
526
+ declaredReviews.map(async (path) => {
527
+ const reference = artifacts.find((artifact) => artifact.path === path);
528
+ if (!reference) {
529
+ throw new Error(
530
+ `Reconciliation review ${path} has no manifest artifact reference`,
531
+ );
532
+ }
533
+ return { path: await realpath(expected(path)), reference };
534
+ }),
535
+ );
536
+ const declaredByPath = new Map(
537
+ declaredReviewInputs.map((input) => [input.path, input.reference]),
538
+ );
539
+ const suppliedReviews = await Promise.all(
540
+ options.review.map(async (path) => {
541
+ await assertSafeExistingPath(packetRoot, resolve(path));
542
+ return realpath(path);
543
+ }),
544
+ );
545
+ if (
546
+ new Set(suppliedReviews).size !== suppliedReviews.length ||
547
+ suppliedReviews.length !== declaredByPath.size ||
548
+ suppliedReviews.some((path) => !declaredByPath.has(path))
549
+ ) {
550
+ throw new Error(
551
+ 'Reconciliation review set drifts from the manifest declaration',
552
+ );
553
+ }
554
+ const priorLedger = JSON.parse(
555
+ await readFile(resolvedInputs.get('input-ledger'), 'utf8'),
556
+ );
557
+ const priorReference = artifacts.find(
558
+ (reference) => reference.path === declaration.inputLedger,
559
+ );
560
+ if (
561
+ !priorReference ||
562
+ (await hashFile(resolvedInputs.get('input-ledger'))) !==
563
+ priorReference.digest
564
+ ) {
565
+ throw new Error(
566
+ 'Reconciliation input ledger does not match its manifest digest',
567
+ );
568
+ }
569
+ const reviewResults = await Promise.all(
570
+ declaredReviewInputs.map(async ({ path, reference: artifactReference }) => {
571
+ if ((await hashFile(path)) !== artifactReference.digest) {
572
+ throw new Error(
573
+ `Reconciliation review ${artifactReference.path} does not match its manifest digest`,
574
+ );
575
+ }
576
+ const value = JSON.parse(await readFile(path, 'utf8'));
577
+ return {
578
+ ...value,
579
+ artifactReference,
580
+ };
581
+ }),
582
+ );
583
+ const result = reconcileLedger({
584
+ priorLedger,
585
+ reviewResults,
586
+ priorReference,
587
+ runId: manifest.run.id,
588
+ reviewerLane: declaration.producer,
589
+ });
590
+ await assertUnchangedRoot(packetIdentity);
591
+ await writeAtomicPair(
592
+ [
593
+ {
594
+ path: resolvedOutputs.get('output-ledger'),
595
+ value: result.ledger,
596
+ },
597
+ {
598
+ path: resolvedOutputs.get('output-review'),
599
+ value: result.reconciliation,
600
+ },
601
+ ],
602
+ packetIdentity,
603
+ );
604
+ process.stdout.write(
605
+ `${JSON.stringify(
606
+ {
607
+ ledger: {
608
+ path: declaration.outputLedger,
609
+ digest: await hashFile(resolvedOutputs.get('output-ledger')),
610
+ },
611
+ reconciliation: {
612
+ path: declaration.outputReview,
613
+ digest: await hashFile(resolvedOutputs.get('output-review')),
614
+ },
615
+ },
616
+ null,
617
+ 2,
618
+ )}\n`,
619
+ );
620
+ }
621
+
622
+ if (isDirectExecution(import.meta.url)) {
623
+ main().catch((error) => {
624
+ process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
625
+ process.exitCode = 1;
626
+ });
627
+ }
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
- import { realpathSync } from 'node:fs';
5
4
  import { lstat, open, rename, rm } from 'node:fs/promises';
6
5
  import { join, resolve } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
6
 
9
7
  import { hashFile } from './lib/canonical-json.mjs';
8
+ import { isDirectExecution } from './lib/cli-entry.mjs';
10
9
  import {
11
10
  assertSafeExistingPath,
12
11
  assertSafeOutputPath,
@@ -437,29 +436,7 @@ async function main(argv) {
437
436
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
438
437
  }
439
438
 
440
- /**
441
- * Direct invocation, compared as canonical paths on both sides. Comparing a raw
442
- * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
443
- * that exits 0 whenever the skill is reached through a symlinked install root,
444
- * and canonicalizing only one side has the same effect under
445
- * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
446
- * A path that cannot be canonicalized is not a module Node loaded as the entry
447
- * point, so a thrown `realpathSync` means "not invoked directly" and returns
448
- * `false`; it never masks a direct run.
449
- */
450
- function isDirectInvocation(invokedPath) {
451
- if (!invokedPath) return false;
452
- try {
453
- return (
454
- realpathSync(fileURLToPath(import.meta.url)) ===
455
- realpathSync(resolve(invokedPath))
456
- );
457
- } catch {
458
- return false;
459
- }
460
- }
461
-
462
- if (isDirectInvocation(process.argv[1])) {
439
+ if (isDirectExecution(import.meta.url)) {
463
440
  main(process.argv.slice(2)).catch((error) => {
464
441
  process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
465
442
  process.exitCode = 1;
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { realpathSync } from 'node:fs';
4
3
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
5
4
  import { basename, join, resolve } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
5
 
6
+ import { isDirectExecution } from './lib/cli-entry.mjs';
8
7
  import { validateArtifactShape } from './lib/contracts.mjs';
9
8
  import {
10
9
  assertSafeExistingPath,
@@ -86,29 +85,7 @@ async function main(argv) {
86
85
  process.exitCode = result.valid ? 0 : 1;
87
86
  }
88
87
 
89
- /**
90
- * Direct invocation, compared as canonical paths on both sides. Comparing a raw
91
- * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
92
- * that exits 0 whenever the skill is reached through a symlinked install root,
93
- * and canonicalizing only one side has the same effect under
94
- * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
95
- * A path that cannot be canonicalized is not a module Node loaded as the entry
96
- * point, so a thrown `realpathSync` means "not invoked directly" and returns
97
- * `false`; it never masks a direct run.
98
- */
99
- function isDirectInvocation(invokedPath) {
100
- if (!invokedPath) return false;
101
- try {
102
- return (
103
- realpathSync(fileURLToPath(import.meta.url)) ===
104
- realpathSync(resolve(invokedPath))
105
- );
106
- } catch {
107
- return false;
108
- }
109
- }
110
-
111
- if (isDirectInvocation(process.argv[1])) {
88
+ if (isDirectExecution(import.meta.url)) {
112
89
  main(process.argv.slice(2)).catch((error) => {
113
90
  process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
114
91
  process.exitCode = 2;
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { realpathSync } from 'node:fs';
4
3
  import { readFile, rm } from 'node:fs/promises';
5
4
  import { join, resolve } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
5
 
8
6
  import { hashCanonicalJson, hashFile, sha256 } from './lib/canonical-json.mjs';
7
+ import { isDirectExecution } from './lib/cli-entry.mjs';
9
8
  import {
10
9
  isDigest,
11
10
  isObject,
@@ -30,7 +29,6 @@ const requiredPasses = {
30
29
  'semantic-verification',
31
30
  'adversarial',
32
31
  'coverage',
33
- 'reconciliation',
34
32
  ],
35
33
  thorough: [
36
34
  'map',
@@ -38,7 +36,6 @@ const requiredPasses = {
38
36
  'semantic-verification',
39
37
  'adversarial',
40
38
  'coverage',
41
- 'reconciliation',
42
39
  'redundant-gather',
43
40
  'redundant-verification',
44
41
  ],
@@ -675,10 +672,6 @@ const passContracts = {
675
672
  },
676
673
  adversarial: { kind: 'recon.review-result', reviewKind: 'adversarial' },
677
674
  coverage: { kind: 'recon.review-result', reviewKind: 'coverage' },
678
- reconciliation: {
679
- kind: 'recon.review-result',
680
- reviewKind: 'reconciliation',
681
- },
682
675
  'redundant-verification': {
683
676
  kind: 'recon.review-result',
684
677
  reviewKind: 'redundant-verification',
@@ -747,7 +740,6 @@ const reviewWaveMode = {
747
740
  semantic: 'semantic-verification',
748
741
  adversarial: 'adversarial',
749
742
  coverage: 'coverage',
750
- reconciliation: 'reconciliation',
751
743
  'redundant-verification': 'redundant-verification',
752
744
  'contradiction-resolution': 'contradiction-resolution',
753
745
  };
@@ -968,6 +960,13 @@ function validateApprovedLanes(
968
960
  const written = new Set();
969
961
  for (const [id, { reference, value }] of artifactsById) {
970
962
  if (value.runId !== manifest.run.id) continue;
963
+ if (
964
+ value.kind === 'recon.review-result' &&
965
+ value.reviewKind === 'reconciliation' &&
966
+ value.reviewerLane === 'controller:reconcile-ledger-v1'
967
+ ) {
968
+ continue;
969
+ }
971
970
  const laneId = artifactLaneId(value);
972
971
  if (laneId === null) continue;
973
972
  const approved = lanes.get(laneId);
@@ -1243,33 +1242,68 @@ function sameReference(left, right) {
1243
1242
  }
1244
1243
 
1245
1244
  function resolveTerminalReconciliation(
1245
+ manifest,
1246
1246
  artifactsById,
1247
1247
  artifactsByPath,
1248
- passes,
1249
1248
  reconciliationRequired,
1250
1249
  errors,
1251
1250
  ) {
1252
- const terminalId = passes.get('reconciliation')?.[0];
1251
+ const declaration = manifest.execution?.reconciliation;
1253
1252
  const reconciliationResults = [...artifactsById.values()].filter(
1254
1253
  ({ value }) =>
1255
1254
  value.kind === 'recon.review-result' &&
1256
1255
  value.reviewKind === 'reconciliation',
1257
1256
  );
1258
1257
  const expectedCount = reconciliationRequired ? 1 : 0;
1259
- if (
1260
- reconciliationResults.length !== expectedCount ||
1261
- (reconciliationRequired &&
1262
- reconciliationResults[0]?.value.id !== terminalId)
1263
- ) {
1258
+ if (reconciliationResults.length !== expectedCount) {
1264
1259
  errors.push(
1265
1260
  issue(
1266
1261
  'SHADOW_RECONCILIATION',
1267
1262
  'The packet must contain exactly its one complete terminal reconciliation',
1268
- terminalId ?? '$.artifacts',
1263
+ declaration?.outputReview ?? '$.artifacts',
1264
+ ),
1265
+ );
1266
+ }
1267
+ const entry = declaration
1268
+ ? artifactsByPath.get(declaration.outputReview)
1269
+ : null;
1270
+ const outputLedgerEntry = declaration
1271
+ ? artifactsByPath.get(declaration.outputLedger)
1272
+ : null;
1273
+ const reconciliation = entry?.value ?? null;
1274
+ if (
1275
+ reconciliationRequired &&
1276
+ (!entry ||
1277
+ reconciliation.reviewKind !== 'reconciliation' ||
1278
+ reconciliation.reviewerLane !== declaration.producer ||
1279
+ !sameReference(
1280
+ entry.reference,
1281
+ manifest.artifacts.find(
1282
+ (item) => item.path === declaration.outputReview,
1283
+ ),
1284
+ ))
1285
+ ) {
1286
+ errors.push(
1287
+ issue(
1288
+ 'INVALID_RECONCILIATION_BINDING',
1289
+ 'Reconciliation result must match the manifest-bound controller producer and output path',
1290
+ declaration?.outputReview ?? '$.execution.reconciliation',
1291
+ ),
1292
+ );
1293
+ }
1294
+ if (
1295
+ reconciliationRequired &&
1296
+ (!outputLedgerEntry ||
1297
+ outputLedgerEntry.value.kind !== 'recon.claim-ledger')
1298
+ ) {
1299
+ errors.push(
1300
+ issue(
1301
+ 'INVALID_RECONCILIATION_OUTPUT',
1302
+ 'Controller reconciliation must produce its manifest-declared ledger candidate',
1303
+ declaration?.outputLedger ?? '$.execution.reconciliation',
1269
1304
  ),
1270
1305
  );
1271
1306
  }
1272
- const reconciliation = artifactsById.get(terminalId)?.value ?? null;
1273
1307
  const priorLedger = reconciliation
1274
1308
  ? ([...artifactsByPath.values()].find(
1275
1309
  ({ reference, value }) =>
@@ -1277,7 +1311,11 @@ function resolveTerminalReconciliation(
1277
1311
  value.kind === 'recon.claim-ledger',
1278
1312
  )?.value ?? null)
1279
1313
  : null;
1280
- return { reconciliation, priorLedger };
1314
+ return {
1315
+ reconciliation,
1316
+ priorLedger,
1317
+ outputLedger: outputLedgerEntry?.value ?? null,
1318
+ };
1281
1319
  }
1282
1320
 
1283
1321
  function reviewBriefBindsClaim(brief, reviewKind, claim, ledger, manifest) {
@@ -2462,12 +2500,26 @@ export async function compileValidatedRun(packetDirectory) {
2462
2500
  const reconciliationRequired =
2463
2501
  achievedProfile === 'standard' || achievedProfile === 'thorough';
2464
2502
  const reconciliationContext = resolveTerminalReconciliation(
2503
+ manifest,
2465
2504
  artifactsById,
2466
2505
  artifactsByPath,
2467
- passes,
2468
2506
  reconciliationRequired,
2469
2507
  errors,
2470
2508
  );
2509
+ if (
2510
+ reconciliationRequired &&
2511
+ reconciliationContext.outputLedger &&
2512
+ hashCanonicalJson(reconciliationContext.outputLedger) !==
2513
+ hashCanonicalJson(ledger)
2514
+ ) {
2515
+ errors.push(
2516
+ issue(
2517
+ 'RECONCILIATION_PROMOTION_MISMATCH',
2518
+ 'Canonical claims.json must exactly match the validated controller ledger candidate',
2519
+ '$.claims',
2520
+ ),
2521
+ );
2522
+ }
2471
2523
  if (
2472
2524
  routing &&
2473
2525
  (!reconciliationRequired || reconciliationContext.priorLedger)
@@ -2599,29 +2651,7 @@ async function main(argv) {
2599
2651
  process.exitCode = result.publishable ? 0 : 1;
2600
2652
  }
2601
2653
 
2602
- /**
2603
- * Direct invocation, compared as canonical paths on both sides. Comparing a raw
2604
- * `process.argv[1]` against `import.meta.url` makes this script a silent no-op
2605
- * that exits 0 whenever the skill is reached through a symlinked install root,
2606
- * and canonicalizing only one side has the same effect under
2607
- * `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
2608
- * A path that cannot be canonicalized is not a module Node loaded as the entry
2609
- * point, so a thrown `realpathSync` means "not invoked directly" and returns
2610
- * `false`; it never masks a direct run.
2611
- */
2612
- function isDirectInvocation(invokedPath) {
2613
- if (!invokedPath) return false;
2614
- try {
2615
- return (
2616
- realpathSync(fileURLToPath(import.meta.url)) ===
2617
- realpathSync(resolve(invokedPath))
2618
- );
2619
- } catch {
2620
- return false;
2621
- }
2622
- }
2623
-
2624
- if (isDirectInvocation(process.argv[1])) {
2654
+ if (isDirectExecution(import.meta.url)) {
2625
2655
  main(process.argv.slice(2)).catch((error) => {
2626
2656
  process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
2627
2657
  process.exitCode = 2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.2.77",
3
+ "version": "0.2.79",
4
4
  "private": false,
5
5
  "description": "Open Agent Toolkit CLI",
6
6
  "homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
@@ -34,7 +34,7 @@
34
34
  "ora": "^9.0.0",
35
35
  "yaml": "2.8.2",
36
36
  "zod": "^3.25.76",
37
- "@open-agent-toolkit/control-plane": "0.2.77"
37
+ "@open-agent-toolkit/control-plane": "0.2.79"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",