@open-agent-toolkit/cli 0.2.53 → 0.2.54

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.
Files changed (87) hide show
  1. package/assets/agents/recon-worker.md +105 -0
  2. package/assets/bundle-metadata.json +1 -1
  3. package/assets/docs/cli-utilities/tool-packs.md +62 -8
  4. package/assets/docs/contributing/skills.md +26 -0
  5. package/assets/docs/workflows/skills/index.md +3 -0
  6. package/assets/docs/workflows/skills/recon.md +194 -0
  7. package/assets/public-package-versions.json +4 -4
  8. package/assets/skills/oat-dispatch-subagents/SKILL.md +105 -4
  9. package/assets/skills/oat-dispatch-subagents/references/record-schema.md +226 -0
  10. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
  11. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
  12. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
  13. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
  14. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
  15. package/assets/skills/recon/SKILL.md +260 -0
  16. package/assets/skills/recon/references/packet-contract.md +307 -0
  17. package/assets/skills/recon/references/profiles.md +58 -0
  18. package/assets/skills/recon/references/worker-contract.md +71 -0
  19. package/assets/skills/recon/scripts/create-review-brief.mjs +452 -0
  20. package/assets/skills/recon/scripts/lib/canonical-json.mjs +55 -0
  21. package/assets/skills/recon/scripts/lib/contracts.mjs +2462 -0
  22. package/assets/skills/recon/scripts/lib/safe-path.mjs +120 -0
  23. package/assets/skills/recon/scripts/lib/validated-run.mjs +86 -0
  24. package/assets/skills/recon/scripts/reconcile-ledger.mjs +360 -0
  25. package/assets/skills/recon/scripts/render-packet.mjs +392 -0
  26. package/assets/skills/recon/scripts/validate-artifact.mjs +96 -0
  27. package/assets/skills/recon/scripts/validate-packet.mjs +2359 -0
  28. package/dist/commands/doctor/index.js +5 -5
  29. package/dist/commands/init/index.d.ts.map +1 -1
  30. package/dist/commands/init/index.js +6 -7
  31. package/dist/commands/status/index.d.ts +2 -2
  32. package/dist/commands/status/index.d.ts.map +1 -1
  33. package/dist/commands/status/index.js +7 -4
  34. package/dist/commands/sync/index.d.ts.map +1 -1
  35. package/dist/commands/sync/index.js +24 -8
  36. package/dist/commands/sync/sync.types.d.ts +2 -2
  37. package/dist/commands/sync/sync.types.d.ts.map +1 -1
  38. package/dist/commands/tools/migrate/index.d.ts.map +1 -1
  39. package/dist/commands/tools/migrate/index.js +25 -1
  40. package/dist/commands/tools/migrate/migrate-pack.d.ts +5 -0
  41. package/dist/commands/tools/migrate/migrate-pack.d.ts.map +1 -1
  42. package/dist/commands/tools/migrate/migrate-pack.js +66 -7
  43. package/dist/commands/tools/remove/index.js +1 -1
  44. package/dist/commands/tools/remove/remove-tools.d.ts +5 -1
  45. package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
  46. package/dist/commands/tools/remove/remove-tools.js +81 -1
  47. package/dist/commands/tools/shared/apply-pack-reconcile.d.ts +3 -0
  48. package/dist/commands/tools/shared/apply-pack-reconcile.d.ts.map +1 -1
  49. package/dist/commands/tools/shared/apply-pack-reconcile.js +40 -9
  50. package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -1
  51. package/dist/commands/tools/shared/format-pack-inventory.js +3 -0
  52. package/dist/commands/tools/shared/install-sync-context.d.ts +2 -1
  53. package/dist/commands/tools/shared/install-sync-context.d.ts.map +1 -1
  54. package/dist/commands/tools/shared/install-sync-context.js +14 -3
  55. package/dist/commands/tools/shared/pack-dependencies.d.ts +14 -0
  56. package/dist/commands/tools/shared/pack-dependencies.d.ts.map +1 -0
  57. package/dist/commands/tools/shared/pack-dependencies.js +54 -0
  58. package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
  59. package/dist/commands/tools/shared/pack-inventory.js +9 -9
  60. package/dist/commands/tools/shared/pack-lifecycle.d.ts +9 -1
  61. package/dist/commands/tools/shared/pack-lifecycle.d.ts.map +1 -1
  62. package/dist/commands/tools/shared/pack-lifecycle.js +102 -9
  63. package/dist/commands/tools/shared/pack-manifest.d.ts +2 -2
  64. package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
  65. package/dist/commands/tools/shared/pack-manifest.js +91 -2
  66. package/dist/commands/tools/shared/pack-reconcile.d.ts +15 -1
  67. package/dist/commands/tools/shared/pack-reconcile.d.ts.map +1 -1
  68. package/dist/commands/tools/shared/pack-reconcile.js +75 -13
  69. package/dist/commands/tools/shared/scoped-pack-intent.d.ts +8 -0
  70. package/dist/commands/tools/shared/scoped-pack-intent.d.ts.map +1 -1
  71. package/dist/commands/tools/shared/scoped-pack-intent.js +61 -15
  72. package/dist/commands/tools/shared/types.d.ts +7 -0
  73. package/dist/commands/tools/shared/types.d.ts.map +1 -1
  74. package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
  75. package/dist/commands/tools/update/update-tools.js +7 -1
  76. package/dist/config/oat-config.d.ts +5 -1
  77. package/dist/config/oat-config.d.ts.map +1 -1
  78. package/dist/config/oat-config.js +19 -0
  79. package/dist/engine/index.d.ts +1 -1
  80. package/dist/engine/index.d.ts.map +1 -1
  81. package/dist/engine/scanner.d.ts +9 -1
  82. package/dist/engine/scanner.d.ts.map +1 -1
  83. package/dist/engine/scanner.js +61 -4
  84. package/dist/shared/types.d.ts +1 -0
  85. package/dist/shared/types.d.ts.map +1 -1
  86. package/dist/shared/types.js +3 -0
  87. package/package.json +2 -2
@@ -0,0 +1,120 @@
1
+ import { lstat, realpath } from 'node:fs/promises';
2
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
3
+
4
+ function contains(root, candidate) {
5
+ const fromRoot = relative(root, candidate);
6
+ return (
7
+ fromRoot === '' ||
8
+ (!fromRoot.startsWith(`..${sep}`) &&
9
+ fromRoot !== '..' &&
10
+ !isAbsolute(fromRoot))
11
+ );
12
+ }
13
+
14
+ async function assertNoSymlinkComponents(root, candidate, allowMissing) {
15
+ const rootIdentity = await assertCanonicalRoot(root);
16
+ const resolvedRoot = rootIdentity.path;
17
+ const resolvedCandidate = resolve(candidate);
18
+ if (!contains(resolvedRoot, resolvedCandidate)) {
19
+ throw Object.assign(new Error('Path escapes its managed root'), {
20
+ code: 'PATH_ESCAPE',
21
+ });
22
+ }
23
+ const parts = relative(resolvedRoot, resolvedCandidate)
24
+ .split(sep)
25
+ .filter(Boolean);
26
+ let current = resolvedRoot;
27
+ for (const part of parts) {
28
+ current = resolve(current, part);
29
+ try {
30
+ const stat = await lstat(current);
31
+ if (stat.isSymbolicLink()) {
32
+ throw Object.assign(
33
+ new Error(`Symlink component is forbidden: ${current}`),
34
+ {
35
+ code: 'SYMLINK_ESCAPE',
36
+ },
37
+ );
38
+ }
39
+ } catch (error) {
40
+ if (error?.code === 'ENOENT' && allowMissing) break;
41
+ throw error;
42
+ }
43
+ }
44
+ const realRoot = rootIdentity.path;
45
+ let existing = resolvedCandidate;
46
+ while (true) {
47
+ try {
48
+ existing = await realpath(existing);
49
+ break;
50
+ } catch (error) {
51
+ if (error?.code !== 'ENOENT' || !allowMissing) throw error;
52
+ const parent = dirname(existing);
53
+ if (parent === existing) throw error;
54
+ existing = parent;
55
+ }
56
+ }
57
+ if (!contains(realRoot, existing)) {
58
+ throw Object.assign(new Error('Resolved path escapes its managed root'), {
59
+ code: 'SYMLINK_ESCAPE',
60
+ });
61
+ }
62
+ return resolvedCandidate;
63
+ }
64
+
65
+ export async function assertCanonicalRoot(root) {
66
+ if (typeof root !== 'string' || !isAbsolute(root)) {
67
+ throw Object.assign(new Error('Managed root must be an absolute path'), {
68
+ code: 'NON_CANONICAL_ROOT',
69
+ });
70
+ }
71
+ const resolvedRoot = resolve(root);
72
+ const stat = await lstat(resolvedRoot);
73
+ if (stat.isSymbolicLink()) {
74
+ throw Object.assign(new Error('Managed root cannot be a symlink alias'), {
75
+ code: 'SYMLINK_ESCAPE',
76
+ });
77
+ }
78
+ const canonicalRoot = await realpath(resolvedRoot);
79
+ if (canonicalRoot !== resolvedRoot) {
80
+ throw Object.assign(
81
+ new Error('Managed root must be its canonical realpath'),
82
+ {
83
+ code: 'NON_CANONICAL_ROOT',
84
+ },
85
+ );
86
+ }
87
+ return Object.freeze({
88
+ path: canonicalRoot,
89
+ device: stat.dev,
90
+ inode: stat.ino,
91
+ });
92
+ }
93
+
94
+ export async function assertUnchangedRoot(identity) {
95
+ const current = await assertCanonicalRoot(identity?.path);
96
+ if (
97
+ current.device !== identity?.device ||
98
+ current.inode !== identity?.inode
99
+ ) {
100
+ throw Object.assign(
101
+ new Error('Managed root identity changed after validation'),
102
+ {
103
+ code: 'ROOT_IDENTITY_CHANGED',
104
+ },
105
+ );
106
+ }
107
+ return current;
108
+ }
109
+
110
+ export function isContainedPath(root, candidate) {
111
+ return contains(resolve(root), resolve(candidate));
112
+ }
113
+
114
+ export async function assertSafeExistingPath(root, candidate) {
115
+ return assertNoSymlinkComponents(root, candidate, false);
116
+ }
117
+
118
+ export async function assertSafeOutputPath(root, candidate) {
119
+ return assertNoSymlinkComponents(root, candidate, true);
120
+ }
@@ -0,0 +1,86 @@
1
+ import { canonicalJson } from './canonical-json.mjs';
2
+
3
+ const validatedRuns = new WeakSet();
4
+
5
+ function clone(value) {
6
+ return JSON.parse(canonicalJson(value));
7
+ }
8
+
9
+ function deepFreeze(value) {
10
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) {
11
+ return value;
12
+ }
13
+ for (const child of Object.values(value)) deepFreeze(child);
14
+ return Object.freeze(value);
15
+ }
16
+
17
+ function normalizeTopology(topology) {
18
+ return {
19
+ requiredLanes: clone(topology.requiredLanes),
20
+ stages: [...topology.stageByLane.entries()]
21
+ .map(([laneId, stage]) => ({ laneId, stage: clone(stage) }))
22
+ .sort((left, right) => left.laneId.localeCompare(right.laneId)),
23
+ completeArtifactIdsByMode: Object.fromEntries(
24
+ [...topology.completeArtifactIdsByMode.entries()]
25
+ .sort(([left], [right]) => left.localeCompare(right))
26
+ .map(([mode, ids]) => [mode, [...ids].sort()]),
27
+ ),
28
+ };
29
+ }
30
+
31
+ export function createValidatedRun({
32
+ packetRoot,
33
+ filesystemIdentities,
34
+ canonicalByteDigests,
35
+ manifest,
36
+ ledger,
37
+ artifactsById,
38
+ exactEvidence,
39
+ topology,
40
+ achievedProfile,
41
+ receiptedReviewIds,
42
+ reconciliationContext,
43
+ }) {
44
+ const run = {
45
+ packetRoot,
46
+ filesystemIdentities: [...filesystemIdentities.values()]
47
+ .map((identity) => clone(identity))
48
+ .sort((left, right) => left.path.localeCompare(right.path)),
49
+ canonicalByteDigests: [...canonicalByteDigests.entries()]
50
+ .map(([path, digest]) => ({ path, digest }))
51
+ .sort((left, right) => left.path.localeCompare(right.path)),
52
+ manifest: clone(manifest),
53
+ ledger: clone(ledger),
54
+ artifacts: [...artifactsById.entries()]
55
+ .map(([id, artifact]) => ({
56
+ id,
57
+ reference: clone(artifact.reference),
58
+ value: clone(artifact.value),
59
+ }))
60
+ .sort((left, right) => left.id.localeCompare(right.id)),
61
+ exactEvidenceIds: [...exactEvidence].sort(),
62
+ topology: normalizeTopology(topology),
63
+ achievedProfile,
64
+ receiptedReviewIds: [...receiptedReviewIds].sort(),
65
+ terminalReconciliation: reconciliationContext?.reconciliation
66
+ ? clone(reconciliationContext.reconciliation)
67
+ : null,
68
+ priorLedger: reconciliationContext?.priorLedger
69
+ ? clone(reconciliationContext.priorLedger)
70
+ : null,
71
+ };
72
+ deepFreeze(run);
73
+ validatedRuns.add(run);
74
+ return run;
75
+ }
76
+
77
+ export function assertValidatedRun(value) {
78
+ if (!value || !validatedRuns.has(value) || !Object.isFrozen(value)) {
79
+ throw new TypeError('Expected an immutable ValidatedRun');
80
+ }
81
+ return value;
82
+ }
83
+
84
+ export function isValidatedRun(value) {
85
+ return Boolean(value && validatedRuns.has(value) && Object.isFrozen(value));
86
+ }
@@ -0,0 +1,360 @@
1
+ import { canonicalJson } from './lib/canonical-json.mjs';
2
+ import { validateArtifactShape } from './lib/contracts.mjs';
3
+
4
+ const requiredDispositions = new Map([
5
+ ['semantic', 'affirmed'],
6
+ ['adversarial', 'unchallenged'],
7
+ ['coverage', 'covered'],
8
+ ]);
9
+
10
+ const permittedDispositions = new Map([
11
+ ['semantic', new Set(['affirmed', 'rejected', 'uncertain'])],
12
+ ['adversarial', new Set(['unchallenged', 'challenged'])],
13
+ ['coverage', new Set(['covered', 'gap'])],
14
+ ]);
15
+
16
+ const legalReconciliationTransitions = new Set([
17
+ 'provisional:verified',
18
+ 'provisional:contested',
19
+ 'provisional:unresolved',
20
+ 'provisional:unsupported',
21
+ 'supported:verified',
22
+ 'supported:contested',
23
+ 'supported:unresolved',
24
+ 'supported:unsupported',
25
+ 'verified:contested',
26
+ 'verified:unresolved',
27
+ 'verified:unsupported',
28
+ 'contested:verified',
29
+ 'contested:unresolved',
30
+ 'contested:unsupported',
31
+ 'unresolved:verified',
32
+ 'unresolved:contested',
33
+ 'unresolved:unsupported',
34
+ ]);
35
+
36
+ function exactClone(value) {
37
+ return JSON.parse(canonicalJson(value));
38
+ }
39
+
40
+ export function reconcileLedger({
41
+ priorLedger,
42
+ reviewResults,
43
+ priorReference,
44
+ runId = priorLedger?.runId,
45
+ reviewerLane = 'lane-reconciliation',
46
+ }) {
47
+ if (!priorLedger || priorLedger.runId !== runId || priorLedger.revision < 1) {
48
+ throw new Error(
49
+ 'Reconciliation requires the exact prior ledger for this run',
50
+ );
51
+ }
52
+ if (!Array.isArray(reviewResults)) {
53
+ throw new Error('Reconciliation requires an exact review-result array');
54
+ }
55
+ for (const review of reviewResults) {
56
+ const reviewArtifact = { ...review };
57
+ delete reviewArtifact.artifactReference;
58
+ const validation = validateArtifactShape(reviewArtifact);
59
+ if (!validation.valid) {
60
+ throw new Error(
61
+ `Reconciliation rejects schema-invalid review ${review.id ?? '<unknown>'}: ${validation.errors.map((error) => error.code).join(', ')}`,
62
+ );
63
+ }
64
+ }
65
+ const reviewKinds = reviewResults.map((review) => review.reviewKind);
66
+ if (
67
+ new Set(reviewKinds).size !== reviewKinds.length ||
68
+ reviewKinds.includes('reconciliation')
69
+ ) {
70
+ throw new Error(
71
+ 'Reconciliation rejects duplicate, nested, or shadow review results',
72
+ );
73
+ }
74
+ const reviews = new Map(
75
+ reviewResults.map((review) => [review.reviewKind, review]),
76
+ );
77
+ for (const [kind] of requiredDispositions) {
78
+ const review = reviews.get(kind);
79
+ if (!review || review.runId !== runId || review.status !== 'complete') {
80
+ throw new Error(`Reconciliation requires a complete ${kind} result`);
81
+ }
82
+ const allowed = permittedDispositions.get(kind);
83
+ if (!review.dispositions.every((item) => allowed.has(item.disposition))) {
84
+ throw new Error(`Reconciliation received an invalid ${kind} disposition`);
85
+ }
86
+ }
87
+ const ledger = exactClone(priorLedger);
88
+ ledger.revision = priorLedger.revision + 1;
89
+ ledger.inputArtifacts = [
90
+ ...priorLedger.inputArtifacts,
91
+ priorReference,
92
+ ...reviewResults.map(
93
+ (review) => review.artifactReference ?? review.permittedInputs[0],
94
+ ),
95
+ ];
96
+ const claimsById = new Map(ledger.claims.map((claim) => [claim.id, claim]));
97
+ const priorEvidenceById = new Map();
98
+ for (const evidence of ledger.evidence) {
99
+ if (priorEvidenceById.has(evidence.id)) {
100
+ throw new Error(
101
+ `Reconciliation rejects duplicate evidence ${evidence.id}`,
102
+ );
103
+ }
104
+ priorEvidenceById.set(evidence.id, canonicalJson(evidence));
105
+ }
106
+ for (const claim of ledger.claims) {
107
+ const links = new Set();
108
+ for (const link of claim.evidence) {
109
+ if (!priorEvidenceById.has(link.evidenceId)) {
110
+ throw new Error(
111
+ `Reconciliation rejects invented evidence link ${link.evidenceId} on ${claim.id}`,
112
+ );
113
+ }
114
+ const key = canonicalJson(link);
115
+ if (links.has(key)) {
116
+ throw new Error(
117
+ `Reconciliation rejects duplicate evidence link ${link.evidenceId} on ${claim.id}`,
118
+ );
119
+ }
120
+ links.add(key);
121
+ }
122
+ }
123
+ const incorporatedEvidence = new Map();
124
+ for (const review of reviewResults) {
125
+ const reviewEvidenceById = new Map(
126
+ review.newEvidence.map((evidence) => [evidence.id, evidence]),
127
+ );
128
+ const dispositionClaimIds = new Set(
129
+ review.dispositions.map((item) => item.claimId),
130
+ );
131
+ const associationKeys = new Set();
132
+ for (const association of review.evidenceAssociations ?? []) {
133
+ const key = canonicalJson(association);
134
+ if (associationKeys.has(key)) {
135
+ throw new Error(
136
+ `Reconciliation rejects duplicate evidence association ${association.evidenceId} -> ${association.claimId}`,
137
+ );
138
+ }
139
+ associationKeys.add(key);
140
+ if (!reviewEvidenceById.has(association.evidenceId)) {
141
+ throw new Error(
142
+ `Reconciliation rejects association for ${association.evidenceId} without matching new evidence`,
143
+ );
144
+ }
145
+ if (!claimsById.has(association.claimId)) {
146
+ throw new Error(
147
+ `Reconciliation rejects association to unknown claim ${association.claimId}`,
148
+ );
149
+ }
150
+ if (!dispositionClaimIds.has(association.claimId)) {
151
+ throw new Error(
152
+ `Reconciliation rejects association to claim ${association.claimId} without a review disposition`,
153
+ );
154
+ }
155
+ }
156
+ for (const evidence of review.newEvidence) {
157
+ const bytes = canonicalJson(evidence);
158
+ const previousBytes = priorEvidenceById.get(evidence.id);
159
+ const incorporated = incorporatedEvidence.get(evidence.id);
160
+ if (previousBytes !== undefined || incorporated) {
161
+ const existingBytes = previousBytes ?? incorporated.bytes;
162
+ const description =
163
+ existingBytes === bytes
164
+ ? 'duplicate evidence'
165
+ : 'conflicting evidence';
166
+ throw new Error(`Reconciliation rejects ${description} ${evidence.id}`);
167
+ }
168
+ const associations = (review.evidenceAssociations ?? []).filter(
169
+ (association) => association.evidenceId === evidence.id,
170
+ );
171
+ if (associations.length === 0) {
172
+ throw new Error(
173
+ `Reconciliation rejects unincorporated evidence ${evidence.id} without an affected claim`,
174
+ );
175
+ }
176
+ incorporatedEvidence.set(evidence.id, {
177
+ bytes,
178
+ evidence: exactClone(evidence),
179
+ associations: exactClone(associations),
180
+ });
181
+ }
182
+ }
183
+ for (const { evidence, associations } of incorporatedEvidence.values()) {
184
+ ledger.evidence.push(evidence);
185
+ for (const association of associations) {
186
+ claimsById.get(association.claimId).evidence.push({
187
+ evidenceId: association.evidenceId,
188
+ relation: association.relation,
189
+ });
190
+ }
191
+ }
192
+ const transitions = [];
193
+ const removals = [];
194
+ const removalDispositions = [];
195
+ for (const claim of ledger.claims) {
196
+ const supporting = reviewResults.filter((review) =>
197
+ review.dispositions.some((item) => item.claimId === claim.id),
198
+ );
199
+ const rejectedReview = supporting.find((review) =>
200
+ review.dispositions.some(
201
+ (item) => item.claimId === claim.id && item.disposition === 'rejected',
202
+ ),
203
+ );
204
+ const challenged = supporting.some(
205
+ (review) =>
206
+ review.reviewKind === 'adversarial' &&
207
+ review.dispositions.some(
208
+ (item) =>
209
+ item.claimId === claim.id && item.disposition === 'challenged',
210
+ ),
211
+ );
212
+ const uncertain = supporting.some((review) =>
213
+ review.dispositions.some(
214
+ (item) => item.claimId === claim.id && item.disposition === 'uncertain',
215
+ ),
216
+ );
217
+ const incomplete = [...requiredDispositions.keys()].some(
218
+ (kind) =>
219
+ !reviewResults.some(
220
+ (review) =>
221
+ review.reviewKind === kind &&
222
+ review.dispositions.some((item) => item.claimId === claim.id),
223
+ ),
224
+ );
225
+ const coverageGap = supporting.some(
226
+ (review) =>
227
+ review.reviewKind === 'coverage' &&
228
+ review.dispositions.some(
229
+ (item) => item.claimId === claim.id && item.disposition === 'gap',
230
+ ),
231
+ );
232
+ const materialCoverageGap = reviewResults.some(
233
+ (review) =>
234
+ review.reviewKind === 'coverage' &&
235
+ review.coverageFindings.some(
236
+ (finding) =>
237
+ finding.material === true && finding.claimIds.includes(claim.id),
238
+ ),
239
+ );
240
+ const coreComplete = [...requiredDispositions].every(
241
+ ([kind, disposition]) =>
242
+ supporting.some(
243
+ (review) =>
244
+ review.reviewKind === kind &&
245
+ review.dispositions.some(
246
+ (item) =>
247
+ item.claimId === claim.id && item.disposition === disposition,
248
+ ),
249
+ ),
250
+ );
251
+ claim.reviewIds = [
252
+ ...new Set([
253
+ ...claim.reviewIds,
254
+ ...supporting.map((review) => review.id),
255
+ ]),
256
+ ];
257
+ if (rejectedReview) {
258
+ const from = claim.status;
259
+ const to = 'unsupported';
260
+ if (legalReconciliationTransitions.has(`${from}:${to}`)) {
261
+ claim.status = to;
262
+ if (from !== to) transitions.push({ claimId: claim.id, from, to });
263
+ }
264
+ continue;
265
+ }
266
+ if (challenged) {
267
+ const from = claim.status;
268
+ const to = 'contested';
269
+ if (legalReconciliationTransitions.has(`${from}:${to}`)) {
270
+ claim.status = to;
271
+ if (from !== to) transitions.push({ claimId: claim.id, from, to });
272
+ }
273
+ continue;
274
+ }
275
+ if (uncertain || incomplete) {
276
+ const from = claim.status;
277
+ const to = 'unresolved';
278
+ if (legalReconciliationTransitions.has(`${from}:${to}`)) {
279
+ claim.status = to;
280
+ if (from !== to) transitions.push({ claimId: claim.id, from, to });
281
+ }
282
+ continue;
283
+ }
284
+ if (coverageGap && !coreComplete) {
285
+ if (
286
+ claim.status === 'verified' ||
287
+ claim.status === 'provisional' ||
288
+ materialCoverageGap
289
+ ) {
290
+ const from = claim.status;
291
+ const to = materialCoverageGap ? 'contested' : 'unresolved';
292
+ if (legalReconciliationTransitions.has(`${from}:${to}`)) {
293
+ claim.status = to;
294
+ if (from !== to) transitions.push({ claimId: claim.id, from, to });
295
+ }
296
+ continue;
297
+ }
298
+ }
299
+ if (!coreComplete) continue;
300
+ const from = claim.status;
301
+ const to = materialCoverageGap ? 'contested' : 'verified';
302
+ if (!legalReconciliationTransitions.has(`${from}:${to}`)) continue;
303
+ claim.status = to;
304
+ if (from !== claim.status)
305
+ transitions.push({ claimId: claim.id, from, to: claim.status });
306
+ }
307
+ if (removals.length > 0) {
308
+ const removed = new Set(removals);
309
+ ledger.claims = ledger.claims.filter((claim) => !removed.has(claim.id));
310
+ if (ledger.synthesis && Array.isArray(ledger.synthesis.keyClaimIds)) {
311
+ ledger.synthesis.keyClaimIds = ledger.synthesis.keyClaimIds.filter(
312
+ (id) => !removed.has(id),
313
+ );
314
+ }
315
+ }
316
+ ledger.transitions = transitions;
317
+ const coverageDispositions = (
318
+ reviews.get('coverage')?.coverageFindings ?? []
319
+ ).map((finding) => ({
320
+ findingId: finding.id,
321
+ gapId: finding.gapId,
322
+ disposition: 'accepted-gap',
323
+ }));
324
+ const reconciliation = {
325
+ kind: 'recon.review-result',
326
+ schemaVersion: 1,
327
+ id: 'review-reconciliation',
328
+ runId,
329
+ reviewKind: 'reconciliation',
330
+ reviewerLane,
331
+ status: 'complete',
332
+ inputLedger: { ...priorReference, revision: priorLedger.revision },
333
+ outputRevision: ledger.revision,
334
+ incorporatedReviewIds: reviewResults.map((review) => review.id),
335
+ transitions: exactClone(transitions),
336
+ additions: [],
337
+ removals,
338
+ removalDispositions,
339
+ coverageDispositions,
340
+ permittedInputs: [
341
+ priorReference,
342
+ ...reviewResults.map(
343
+ (review) => review.artifactReference ?? review.permittedInputs[0],
344
+ ),
345
+ ],
346
+ excludedInputs: [],
347
+ dispositions: [],
348
+ newEvidence: [],
349
+ evidenceAssociations: [],
350
+ coverageFindings: [],
351
+ unresolvedIssues: [],
352
+ };
353
+ const ledgerValidation = validateArtifactShape(ledger);
354
+ if (!ledgerValidation.valid) {
355
+ throw new Error(
356
+ `Reconciliation rejects schema-invalid review evidence: ${ledgerValidation.errors.map((error) => error.code).join(', ')}`,
357
+ );
358
+ }
359
+ return { ledger, reconciliation };
360
+ }