@open-agent-toolkit/cli 0.2.77 → 0.3.0
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/assets/agents/oat-phase-implementer.md +5 -5
- package/assets/agents/oat-reviewer.md +15 -15
- package/assets/agents/recon-worker.md +12 -12
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/workflow-gates.md +1 -1
- package/assets/docs/workflows/projects/artifacts.md +2 -2
- package/assets/docs/workflows/projects/autonomy.md +1 -1
- package/assets/docs/workflows/projects/implementation-execution.md +4 -4
- package/assets/docs/workflows/projects/lifecycle.md +1 -1
- package/assets/docs/workflows/projects/reviews.md +11 -11
- package/assets/docs/workflows/skills/recon.md +53 -27
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-agent-instructions-analyze/SKILL.md +3 -3
- package/assets/skills/oat-docs-analyze/SKILL.md +3 -3
- package/assets/skills/oat-project-autonomous/SKILL.md +2 -2
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +14 -14
- package/assets/skills/oat-project-design/SKILL.md +2 -2
- package/assets/skills/oat-project-discover/SKILL.md +2 -2
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +14 -14
- package/assets/skills/oat-project-implement/SKILL.md +2 -2
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +1 -1
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +14 -14
- package/assets/skills/oat-project-implement/references/phase-execution.md +3 -3
- package/assets/skills/oat-project-implement/references/plan-and-resume.md +2 -2
- package/assets/skills/oat-project-import-plan/SKILL.md +2 -2
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +14 -14
- package/assets/skills/oat-project-plan/SKILL.md +2 -2
- package/assets/skills/oat-project-plan-writing/SKILL.md +4 -4
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +14 -14
- package/assets/skills/oat-project-quick-start/SKILL.md +2 -2
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +14 -14
- package/assets/skills/oat-project-review-provide/SKILL.md +14 -14
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +10 -10
- package/assets/skills/oat-project-review-receive/SKILL.md +64 -45
- package/assets/skills/oat-project-review-receive-remote/SKILL.md +6 -6
- package/assets/skills/oat-project-revise/SKILL.md +2 -2
- package/assets/skills/oat-review-provide/SKILL.md +2 -2
- package/assets/skills/oat-review-provide/references/review-artifact-template.md +3 -3
- package/assets/skills/oat-review-provide-remote/SKILL.md +11 -11
- package/assets/skills/oat-review-receive/SKILL.md +18 -17
- package/assets/skills/oat-review-receive-remote/SKILL.md +13 -13
- package/assets/skills/oat-wave-execute/SKILL.md +2 -2
- package/assets/skills/recon/SKILL.md +37 -19
- package/assets/skills/recon/references/packet-contract.md +33 -30
- package/assets/skills/recon/references/profiles.md +18 -15
- package/assets/skills/recon/references/worker-contract.md +87 -7
- package/assets/skills/recon/scripts/create-review-brief.mjs +2 -25
- package/assets/skills/recon/scripts/lib/cli-entry.mjs +21 -0
- package/assets/skills/recon/scripts/lib/contracts.mjs +144 -38
- package/assets/skills/recon/scripts/lib/routing.mjs +2 -5
- package/assets/skills/recon/scripts/lib/validated-run.mjs +1 -1
- package/assets/skills/recon/scripts/prepare-routing.mjs +2 -26
- package/assets/skills/recon/scripts/reconcile-ledger.mjs +269 -2
- package/assets/skills/recon/scripts/render-packet.mjs +2 -25
- package/assets/skills/recon/scripts/validate-artifact.mjs +2 -25
- package/assets/skills/recon/scripts/validate-packet.mjs +73 -43
- package/assets/templates/plan.md +1 -1
- package/dist/commands/gate/__fixtures__/fake-runtime.mjs +4 -4
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +20 -11
- package/dist/commands/gate/review-verdict.d.ts +2 -2
- package/dist/commands/gate/review-verdict.d.ts.map +1 -1
- package/dist/commands/gate/review-verdict.js +223 -57
- package/dist/review-remote/body-builder.d.ts +3 -3
- package/dist/review-remote/body-builder.d.ts.map +1 -1
- package/dist/review-remote/body-builder.js +12 -12
- package/dist/review-remote/reviewer-dispatch.d.ts +2 -2
- package/dist/review-remote/reviewer-dispatch.d.ts.map +1 -1
- package/dist/review-remote/reviewer-dispatch.js +15 -7
- package/package.json +2 -2
|
@@ -33,7 +33,6 @@ export const workerModes = [
|
|
|
33
33
|
'verify',
|
|
34
34
|
'adversary',
|
|
35
35
|
'coverage',
|
|
36
|
-
'reconcile',
|
|
37
36
|
];
|
|
38
37
|
export const evidenceRelations = [
|
|
39
38
|
'supports',
|
|
@@ -49,7 +48,6 @@ export const waveModes = [
|
|
|
49
48
|
'semantic-verification',
|
|
50
49
|
'adversarial',
|
|
51
50
|
'coverage',
|
|
52
|
-
'reconciliation',
|
|
53
51
|
'redundant-gather',
|
|
54
52
|
'redundant-verification',
|
|
55
53
|
'contradiction-resolution',
|
|
@@ -67,6 +65,18 @@ export const conditionPredicates = [
|
|
|
67
65
|
'unresolved-material-challenge',
|
|
68
66
|
];
|
|
69
67
|
|
|
68
|
+
const reconciliationReviewPaths = Object.freeze({
|
|
69
|
+
'semantic-verification': 'reviews/semantic.json',
|
|
70
|
+
adversarial: 'reviews/adversarial.json',
|
|
71
|
+
coverage: 'reviews/coverage.json',
|
|
72
|
+
'redundant-verification': 'reviews/redundant-verification.json',
|
|
73
|
+
'contradiction-resolution': 'reviews/contradiction-resolution.json',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const singletonReviewWaveModes = new Set(
|
|
77
|
+
Object.keys(reconciliationReviewPaths),
|
|
78
|
+
);
|
|
79
|
+
|
|
70
80
|
function profilePolicy(
|
|
71
81
|
orderedSingletonWaveModes,
|
|
72
82
|
allowedWaveModes,
|
|
@@ -100,7 +110,6 @@ export const profileRoutingPolicy = Object.freeze({
|
|
|
100
110
|
'semantic-verification',
|
|
101
111
|
'adversarial',
|
|
102
112
|
'coverage',
|
|
103
|
-
'reconciliation',
|
|
104
113
|
],
|
|
105
114
|
[
|
|
106
115
|
'map',
|
|
@@ -109,7 +118,6 @@ export const profileRoutingPolicy = Object.freeze({
|
|
|
109
118
|
'semantic-verification',
|
|
110
119
|
'adversarial',
|
|
111
120
|
'coverage',
|
|
112
|
-
'reconciliation',
|
|
113
121
|
'contradiction-resolution',
|
|
114
122
|
],
|
|
115
123
|
[
|
|
@@ -131,7 +139,6 @@ export const profileRoutingPolicy = Object.freeze({
|
|
|
131
139
|
'adversarial',
|
|
132
140
|
'coverage',
|
|
133
141
|
'redundant-verification',
|
|
134
|
-
'reconciliation',
|
|
135
142
|
],
|
|
136
143
|
[
|
|
137
144
|
'map',
|
|
@@ -142,7 +149,6 @@ export const profileRoutingPolicy = Object.freeze({
|
|
|
142
149
|
'coverage',
|
|
143
150
|
'redundant-gather',
|
|
144
151
|
'redundant-verification',
|
|
145
|
-
'reconciliation',
|
|
146
152
|
'contradiction-resolution',
|
|
147
153
|
],
|
|
148
154
|
[
|
|
@@ -154,7 +160,7 @@ export const profileRoutingPolicy = Object.freeze({
|
|
|
154
160
|
'redundant-verification',
|
|
155
161
|
'contradiction-resolution',
|
|
156
162
|
],
|
|
157
|
-
{ lanes: 20, concurrency: 8, conditions:
|
|
163
|
+
{ lanes: 20, concurrency: 8, conditions: 1 },
|
|
158
164
|
),
|
|
159
165
|
});
|
|
160
166
|
|
|
@@ -359,6 +365,7 @@ const executionV2Keys = new Set([
|
|
|
359
365
|
'retryLimit',
|
|
360
366
|
'waves',
|
|
361
367
|
'conditions',
|
|
368
|
+
'reconciliation',
|
|
362
369
|
'approval',
|
|
363
370
|
]);
|
|
364
371
|
|
|
@@ -410,6 +417,57 @@ function validateExecutionV2(value, errors, path) {
|
|
|
410
417
|
requiredObject(value, 'approval', errors, path);
|
|
411
418
|
validateApprovalEvidence(value.approval, errors, `${path}.approval`);
|
|
412
419
|
|
|
420
|
+
if (Object.hasOwn(value, 'reconciliation')) {
|
|
421
|
+
const reconciliation = value.reconciliation;
|
|
422
|
+
requiredObject(value, 'reconciliation', errors, path);
|
|
423
|
+
if (isObject(reconciliation)) {
|
|
424
|
+
closedObject(
|
|
425
|
+
reconciliation,
|
|
426
|
+
new Set([
|
|
427
|
+
'producer',
|
|
428
|
+
'inputLedger',
|
|
429
|
+
'outputLedger',
|
|
430
|
+
'outputReview',
|
|
431
|
+
'requiredReviews',
|
|
432
|
+
'conditionalReviews',
|
|
433
|
+
]),
|
|
434
|
+
errors,
|
|
435
|
+
`${path}.reconciliation`,
|
|
436
|
+
);
|
|
437
|
+
for (const key of [
|
|
438
|
+
'producer',
|
|
439
|
+
'inputLedger',
|
|
440
|
+
'outputLedger',
|
|
441
|
+
'outputReview',
|
|
442
|
+
]) {
|
|
443
|
+
requiredString(reconciliation, key, errors, `${path}.reconciliation`);
|
|
444
|
+
}
|
|
445
|
+
if (reconciliation.producer !== 'controller:reconcile-ledger-v1') {
|
|
446
|
+
errors.push(
|
|
447
|
+
issue(
|
|
448
|
+
'INVALID_RECONCILIATION_PRODUCER',
|
|
449
|
+
'Controller reconciliation must use controller:reconcile-ledger-v1',
|
|
450
|
+
`${path}.reconciliation.producer`,
|
|
451
|
+
),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
for (const key of ['requiredReviews', 'conditionalReviews']) {
|
|
455
|
+
requiredArray(reconciliation, key, errors, `${path}.reconciliation`);
|
|
456
|
+
for (const [index, item] of (reconciliation[key] ?? []).entries()) {
|
|
457
|
+
if (typeof item !== 'string' || item.length === 0) {
|
|
458
|
+
errors.push(
|
|
459
|
+
issue(
|
|
460
|
+
'INVALID_RECONCILIATION_PATH',
|
|
461
|
+
`${key} must contain non-empty paths`,
|
|
462
|
+
`${path}.reconciliation.${key}[${index}]`,
|
|
463
|
+
),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
413
471
|
const waves = Array.isArray(value.waves) ? value.waves : [];
|
|
414
472
|
const waveIds = new Set();
|
|
415
473
|
const laneIds = new Set();
|
|
@@ -624,7 +682,8 @@ export function validateV2ProfileTopology(
|
|
|
624
682
|
for (const [waveIndex, wave] of waves.entries()) {
|
|
625
683
|
if (
|
|
626
684
|
policy.allowedWaveModes.includes(wave?.mode) &&
|
|
627
|
-
!policy.countedLaneModes.includes(wave.mode)
|
|
685
|
+
(!policy.countedLaneModes.includes(wave.mode) ||
|
|
686
|
+
singletonReviewWaveModes.has(wave.mode)) &&
|
|
628
687
|
Array.isArray(wave?.lanes) &&
|
|
629
688
|
wave.lanes.length !== 1
|
|
630
689
|
) {
|
|
@@ -673,25 +732,77 @@ export function validateV2ProfileTopology(
|
|
|
673
732
|
),
|
|
674
733
|
);
|
|
675
734
|
}
|
|
676
|
-
const reconciliationIndexes = waves
|
|
677
|
-
.map((wave, index) => (wave?.mode === 'reconciliation' ? index : -1))
|
|
678
|
-
.filter((index) => index !== -1);
|
|
679
735
|
const requiresReconciliation = ['standard', 'thorough'].includes(
|
|
680
736
|
requestedProfile,
|
|
681
737
|
);
|
|
682
|
-
if (
|
|
683
|
-
reconciliationIndexes.length > 1 ||
|
|
684
|
-
(requiresReconciliation && reconciliationIndexes.length !== 1) ||
|
|
685
|
-
reconciliationIndexes.some((index) => index !== waves.length - 1) ||
|
|
686
|
-
reconciliationIndexes.some((index) => waves[index]?.conditional === true)
|
|
687
|
-
) {
|
|
738
|
+
if (requiresReconciliation !== isObject(execution.reconciliation)) {
|
|
688
739
|
errors.push(
|
|
689
740
|
issue(
|
|
690
741
|
'INVALID_TERMINAL_TOPOLOGY',
|
|
691
|
-
'Standard and thorough routing require exactly one
|
|
692
|
-
`${path}.
|
|
742
|
+
'Standard and thorough routing require exactly one controller reconciliation declaration',
|
|
743
|
+
`${path}.reconciliation`,
|
|
693
744
|
),
|
|
694
745
|
);
|
|
746
|
+
} else if (requiresReconciliation) {
|
|
747
|
+
const reconciliation = execution.reconciliation;
|
|
748
|
+
const expected = {
|
|
749
|
+
producer: 'controller:reconcile-ledger-v1',
|
|
750
|
+
inputLedger: 'raw/drafts/claims-v1.json',
|
|
751
|
+
outputLedger: 'raw/drafts/claims-v2.json',
|
|
752
|
+
outputReview: 'reviews/reconciliation.json',
|
|
753
|
+
requiredReviews: [
|
|
754
|
+
reconciliationReviewPaths['semantic-verification'],
|
|
755
|
+
reconciliationReviewPaths.adversarial,
|
|
756
|
+
reconciliationReviewPaths.coverage,
|
|
757
|
+
],
|
|
758
|
+
conditionalReviews: [
|
|
759
|
+
reconciliationReviewPaths['redundant-verification'],
|
|
760
|
+
reconciliationReviewPaths['contradiction-resolution'],
|
|
761
|
+
],
|
|
762
|
+
};
|
|
763
|
+
const matchesExpected =
|
|
764
|
+
['producer', 'inputLedger', 'outputLedger', 'outputReview'].every(
|
|
765
|
+
(key) => reconciliation[key] === expected[key],
|
|
766
|
+
) &&
|
|
767
|
+
['requiredReviews', 'conditionalReviews'].every(
|
|
768
|
+
(key) =>
|
|
769
|
+
Array.isArray(reconciliation[key]) &&
|
|
770
|
+
reconciliation[key].length === expected[key].length &&
|
|
771
|
+
reconciliation[key].every(
|
|
772
|
+
(value, index) => value === expected[key][index],
|
|
773
|
+
),
|
|
774
|
+
);
|
|
775
|
+
if (!matchesExpected) {
|
|
776
|
+
errors.push(
|
|
777
|
+
issue(
|
|
778
|
+
'INVALID_RECONCILIATION_PATH',
|
|
779
|
+
'Controller reconciliation paths and review sets must exactly match the closed manifest contract',
|
|
780
|
+
`${path}.reconciliation`,
|
|
781
|
+
),
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
for (const [mode, reviewPath] of Object.entries(
|
|
785
|
+
reconciliationReviewPaths,
|
|
786
|
+
)) {
|
|
787
|
+
const matchingWaves = waves.filter((wave) => wave?.mode === mode);
|
|
788
|
+
if (matchingWaves.length !== 1 || matchingWaves[0].lanes?.length !== 1) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const writeRoot = matchingWaves[0].lanes[0].writeRoot;
|
|
792
|
+
if (
|
|
793
|
+
typeof writeRoot === 'string' &&
|
|
794
|
+
reviewPath !== writeRoot &&
|
|
795
|
+
!reviewPath.startsWith(`${writeRoot}/`)
|
|
796
|
+
) {
|
|
797
|
+
errors.push(
|
|
798
|
+
issue(
|
|
799
|
+
'INVALID_RECONCILIATION_PATH',
|
|
800
|
+
`${mode} output ${reviewPath} must be inside its approved write root`,
|
|
801
|
+
`${path}.waves`,
|
|
802
|
+
),
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
695
806
|
}
|
|
696
807
|
|
|
697
808
|
const waveIndexes = new Map(
|
|
@@ -708,7 +819,6 @@ export function validateV2ProfileTopology(
|
|
|
708
819
|
'predicate',
|
|
709
820
|
'maxActivations',
|
|
710
821
|
]);
|
|
711
|
-
const terminalIndex = reconciliationIndexes[0] ?? -1;
|
|
712
822
|
for (const [index, condition] of conditions.entries()) {
|
|
713
823
|
const conditionPath = `${path}.conditions[${index}]`;
|
|
714
824
|
if (
|
|
@@ -852,24 +962,10 @@ export function validateV2ProfileTopology(
|
|
|
852
962
|
);
|
|
853
963
|
}
|
|
854
964
|
}
|
|
855
|
-
if (
|
|
856
|
-
terminalIndex !== -1 &&
|
|
857
|
-
destinationIndex !== undefined &&
|
|
858
|
-
destinationIndex >= terminalIndex
|
|
859
|
-
) {
|
|
860
|
-
errors.push(
|
|
861
|
-
issue(
|
|
862
|
-
'INVALID_TERMINAL_TOPOLOGY',
|
|
863
|
-
'Conditional evidence waves must complete before the one terminal reconciliation',
|
|
864
|
-
`${conditionPath}.destinationWaveId`,
|
|
865
|
-
),
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
965
|
}
|
|
869
966
|
for (const wave of waves) {
|
|
870
967
|
if (
|
|
871
968
|
wave?.conditional === true &&
|
|
872
|
-
wave.mode !== 'reconciliation' &&
|
|
873
969
|
!policy.orderedSingletonWaveModes.includes(wave.mode) &&
|
|
874
970
|
!conditionDestinations.has(wave.waveId)
|
|
875
971
|
) {
|
|
@@ -927,10 +1023,7 @@ export function validateV2ProfileTopology(
|
|
|
927
1023
|
);
|
|
928
1024
|
}
|
|
929
1025
|
const conditionalRequired = stageIndexes.find(
|
|
930
|
-
({
|
|
931
|
-
mode !== 'reconciliation' &&
|
|
932
|
-
indexes.length === 1 &&
|
|
933
|
-
waves[indexes[0]]?.conditional,
|
|
1026
|
+
({ indexes }) => indexes.length === 1 && waves[indexes[0]]?.conditional,
|
|
934
1027
|
);
|
|
935
1028
|
if (conditionalRequired) {
|
|
936
1029
|
errors.push(
|
|
@@ -2192,6 +2285,19 @@ function validateReviewResult(value, errors) {
|
|
|
2192
2285
|
]) {
|
|
2193
2286
|
requiredArray(value, key, errors);
|
|
2194
2287
|
}
|
|
2288
|
+
if (Array.isArray(value.unresolvedIssues)) {
|
|
2289
|
+
for (const [index, unresolvedIssue] of value.unresolvedIssues.entries()) {
|
|
2290
|
+
if (typeof unresolvedIssue !== 'string') {
|
|
2291
|
+
errors.push(
|
|
2292
|
+
issue(
|
|
2293
|
+
'INVALID_UNRESOLVED_ISSUE',
|
|
2294
|
+
'unresolvedIssues members must be strings',
|
|
2295
|
+
`$.unresolvedIssues[${index}]`,
|
|
2296
|
+
),
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2195
2301
|
if (
|
|
2196
2302
|
![
|
|
2197
2303
|
'semantic',
|
|
@@ -52,11 +52,6 @@ const economicalDefaults = Object.freeze({
|
|
|
52
52
|
'Compare evidence with an explicit question or source inventory.',
|
|
53
53
|
taskClass: 'mechanical-recon',
|
|
54
54
|
}),
|
|
55
|
-
reconciliation: Object.freeze({
|
|
56
|
-
assignment:
|
|
57
|
-
'Assemble agreement and disagreement without deciding implications.',
|
|
58
|
-
taskClass: 'mechanical-recon',
|
|
59
|
-
}),
|
|
60
55
|
'contradiction-resolution': Object.freeze({
|
|
61
56
|
assignment: 'Seek discriminating evidence for a named contradiction.',
|
|
62
57
|
taskClass: 'mechanical-recon',
|
|
@@ -172,6 +167,7 @@ function assertProposalExecution(manifest) {
|
|
|
172
167
|
'retryLimit',
|
|
173
168
|
'waves',
|
|
174
169
|
'conditions',
|
|
170
|
+
'reconciliation',
|
|
175
171
|
'approval',
|
|
176
172
|
]),
|
|
177
173
|
'INVALID_ROUTING_PROPOSAL',
|
|
@@ -377,6 +373,7 @@ export function normalizeManifestRouting(manifest) {
|
|
|
377
373
|
target: resolveEffectiveWaveTarget(execution, wave),
|
|
378
374
|
})),
|
|
379
375
|
conditions: clone(execution.conditions),
|
|
376
|
+
reconciliation: clone(execution.reconciliation ?? null),
|
|
380
377
|
approval: clone(execution.approval),
|
|
381
378
|
};
|
|
382
379
|
return deepFreeze(routing);
|
|
@@ -58,7 +58,7 @@ export function createValidatedRun({
|
|
|
58
58
|
passes: normalizePasses(passes),
|
|
59
59
|
achievedProfile,
|
|
60
60
|
assuranceReviewIds: [...assuranceReviewIds].sort(),
|
|
61
|
-
|
|
61
|
+
controllerReconciliation: reconciliationContext?.reconciliation
|
|
62
62
|
? clone(reconciliationContext.reconciliation)
|
|
63
63
|
: null,
|
|
64
64
|
priorLedger: reconciliationContext?.priorLedger
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { realpathSync } from 'node:fs';
|
|
4
3
|
import { readFile } from 'node:fs/promises';
|
|
5
|
-
import { resolve } from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
4
|
|
|
5
|
+
import { isDirectExecution } from './lib/cli-entry.mjs';
|
|
8
6
|
import {
|
|
9
7
|
checkApprovedWaveTarget,
|
|
10
8
|
createRoutingPreview,
|
|
@@ -81,29 +79,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
81
79
|
);
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
* Direct invocation, compared as canonical paths on both sides. Comparing a raw
|
|
86
|
-
* `process.argv[1]` against `import.meta.url` makes this script a silent no-op
|
|
87
|
-
* that exits 0 whenever the skill is reached through a symlinked install root,
|
|
88
|
-
* and canonicalizing only one side has the same effect under
|
|
89
|
-
* `--preserve-symlinks-main`, which keeps the link in `import.meta.url`.
|
|
90
|
-
* A path that cannot be canonicalized is not a module Node loaded as the entry
|
|
91
|
-
* point, so a thrown `realpathSync` means "not invoked directly" and returns
|
|
92
|
-
* `false`; it never masks a direct run.
|
|
93
|
-
*/
|
|
94
|
-
function isDirectInvocation(invokedPath) {
|
|
95
|
-
if (!invokedPath) return false;
|
|
96
|
-
try {
|
|
97
|
-
return (
|
|
98
|
-
realpathSync(fileURLToPath(import.meta.url)) ===
|
|
99
|
-
realpathSync(resolve(invokedPath))
|
|
100
|
-
);
|
|
101
|
-
} catch {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (isDirectInvocation(process.argv[1])) {
|
|
82
|
+
if (isDirectExecution(import.meta.url)) {
|
|
107
83
|
main().catch((error) => {
|
|
108
84
|
const code = error instanceof RoutingContractError ? error.code : 'ERROR';
|
|
109
85
|
process.stderr.write(`${code}: ${error.message}\n`);
|
|
@@ -1,5 +1,24 @@
|
|
|
1
|
-
|
|
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 = '
|
|
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
|
+
}
|