@open-agent-toolkit/cli 0.3.10 → 0.3.12

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 (163) hide show
  1. package/README.md +2 -1
  2. package/assets/bundle-metadata.json +1 -1
  3. package/assets/docs/cli-utilities/meta.json +13 -0
  4. package/assets/docs/cli-utilities/tool-packs.md +8 -2
  5. package/assets/docs/cli-utilities/workflow-gates.md +13 -0
  6. package/assets/docs/contributing/code.md +8 -2
  7. package/assets/docs/contributing/documentation.md +7 -1
  8. package/assets/docs/contributing/meta.json +15 -0
  9. package/assets/docs/docs-tooling/add-docs-to-a-repo.md +14 -5
  10. package/assets/docs/docs-tooling/commands.md +26 -8
  11. package/assets/docs/docs-tooling/meta.json +4 -0
  12. package/assets/docs/docs-tooling/workflows.md +2 -2
  13. package/assets/docs/guide/meta.json +4 -0
  14. package/assets/docs/meta.json +14 -0
  15. package/assets/docs/provider-sync/instruction-sync.md +8 -0
  16. package/assets/docs/provider-sync/meta.json +11 -0
  17. package/assets/docs/provider-sync/scope-and-surface.md +3 -4
  18. package/assets/docs/reference/cli-reference.md +5 -2
  19. package/assets/docs/reference/docs-index-contract.md +23 -6
  20. package/assets/docs/reference/file-locations.md +7 -2
  21. package/assets/docs/reference/index.md +1 -1
  22. package/assets/docs/reference/meta.json +10 -0
  23. package/assets/docs/reference/troubleshooting.md +41 -6
  24. package/assets/docs/workflows/ideas/meta.json +4 -0
  25. package/assets/docs/workflows/meta.json +4 -0
  26. package/assets/docs/workflows/projects/cursor-cloud.md +8 -3
  27. package/assets/docs/workflows/projects/evidence-layers.md +6 -8
  28. package/assets/docs/workflows/projects/implementation-execution.md +30 -4
  29. package/assets/docs/workflows/projects/lifecycle.md +7 -1
  30. package/assets/docs/workflows/projects/meta.json +25 -0
  31. package/assets/docs/workflows/projects/orchestration-model.md +5 -6
  32. package/assets/docs/workflows/skills/meta.json +10 -0
  33. package/assets/docs/workflows/skills/recon.md +31 -3
  34. package/assets/public-package-versions.json +4 -4
  35. package/assets/skills/oat-cursor-cloud-projects/SKILL.md +30 -16
  36. package/assets/skills/oat-dispatch-subagents/SKILL.md +4 -12
  37. package/assets/skills/oat-dispatch-subagents/references/managed-claude-example.json +1 -1
  38. package/assets/skills/oat-dispatch-subagents/references/provider-codex.md +51 -0
  39. package/assets/skills/oat-dispatch-subagents/references/record-schema.md +8 -7
  40. package/assets/skills/oat-docs-analyze/SKILL.md +28 -3
  41. package/assets/skills/oat-docs-apply/SKILL.md +3 -3
  42. package/assets/skills/oat-docs-authoring/SKILL.md +1 -1
  43. package/assets/skills/oat-docs-authoring/references/oat-fumadocs-contract.md +5 -2
  44. package/assets/skills/oat-docs-bootstrap/SKILL.md +4 -3
  45. package/assets/skills/oat-docs-bootstrap/assets/AGENTS.md.template +3 -3
  46. package/assets/skills/oat-pjm-decision/SKILL.md +3 -2
  47. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +4 -4
  48. package/assets/skills/oat-project-complete/SKILL.md +47 -2
  49. package/assets/skills/oat-project-design/SKILL.md +10 -6
  50. package/assets/skills/oat-project-discover/SKILL.md +3 -3
  51. package/assets/skills/oat-project-dispatch-subagents/SKILL.md +3 -10
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +4 -4
  53. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  54. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +135 -3
  55. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +7 -9
  56. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +4 -4
  57. package/assets/skills/oat-project-implement/references/plan-and-resume.md +1 -1
  58. package/assets/skills/oat-project-import-plan/SKILL.md +3 -3
  59. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +4 -4
  60. package/assets/skills/oat-project-new/SKILL.md +3 -3
  61. package/assets/skills/oat-project-next/SKILL.md +50 -7
  62. package/assets/skills/oat-project-plan/SKILL.md +4 -4
  63. package/assets/skills/oat-project-plan-writing/SKILL.md +2 -2
  64. package/assets/skills/oat-project-pr-final/SKILL.md +11 -1
  65. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +4 -4
  66. package/assets/skills/oat-project-promote-spec-driven/SKILL.md +2 -2
  67. package/assets/skills/oat-project-quick-start/SKILL.md +12 -8
  68. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +4 -4
  69. package/assets/skills/oat-project-retro/SKILL.md +6 -3
  70. package/assets/skills/oat-project-review-provide/SKILL.md +2 -2
  71. package/assets/skills/oat-project-review-provide-remote/SKILL.md +2 -2
  72. package/assets/skills/oat-project-spec/SKILL.md +3 -3
  73. package/assets/skills/oat-project-summary/SKILL.md +22 -3
  74. package/assets/skills/oat-repo-improve/SKILL.md +2 -2
  75. package/assets/skills/recon/SKILL.md +24 -3
  76. package/assets/skills/recon/references/packet-contract.md +36 -6
  77. package/assets/skills/recon/references/profiles.md +3 -0
  78. package/assets/skills/recon/references/worker-contract.md +16 -3
  79. package/assets/skills/recon/scripts/create-review-brief.mjs +17 -86
  80. package/assets/skills/recon/scripts/lib/contracts.mjs +158 -41
  81. package/assets/skills/recon/scripts/lib/review-binding.mjs +123 -0
  82. package/assets/skills/recon/scripts/lib/routing.mjs +5 -2
  83. package/assets/skills/recon/scripts/reconcile-ledger.mjs +18 -7
  84. package/assets/skills/recon/scripts/render-packet.mjs +120 -1
  85. package/assets/skills/recon/scripts/validate-packet.mjs +119 -88
  86. package/assets/templates/state.md +1 -0
  87. package/dist/commands/backlog/new.d.ts +2 -2
  88. package/dist/commands/backlog/new.d.ts.map +1 -1
  89. package/dist/commands/backlog/new.js +2 -2
  90. package/dist/commands/decision/new.d.ts +2 -2
  91. package/dist/commands/decision/new.d.ts.map +1 -1
  92. package/dist/commands/decision/new.js +2 -2
  93. package/dist/commands/docs/nav/contents.d.ts +9 -0
  94. package/dist/commands/docs/nav/contents.d.ts.map +1 -1
  95. package/dist/commands/docs/nav/contents.js +7 -2
  96. package/dist/commands/docs/nav/fumadocs.d.ts +58 -0
  97. package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -0
  98. package/dist/commands/docs/nav/fumadocs.js +326 -0
  99. package/dist/commands/docs/nav/sync.d.ts +31 -1
  100. package/dist/commands/docs/nav/sync.d.ts.map +1 -1
  101. package/dist/commands/docs/nav/sync.js +163 -6
  102. package/dist/commands/index.d.ts.map +1 -1
  103. package/dist/commands/index.js +2 -0
  104. package/dist/commands/instructions/instructions.utils.d.ts +19 -0
  105. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  106. package/dist/commands/instructions/instructions.utils.js +89 -17
  107. package/dist/commands/instructions/sync/sync.d.ts.map +1 -1
  108. package/dist/commands/instructions/sync/sync.js +49 -2
  109. package/dist/commands/pjm/init.d.ts.map +1 -1
  110. package/dist/commands/pjm/init.js +2 -2
  111. package/dist/commands/project/closeout-check/closeout-invariant.d.ts +97 -0
  112. package/dist/commands/project/closeout-check/closeout-invariant.d.ts.map +1 -0
  113. package/dist/commands/project/closeout-check/closeout-invariant.js +301 -0
  114. package/dist/commands/project/closeout-check/index.d.ts +38 -0
  115. package/dist/commands/project/closeout-check/index.d.ts.map +1 -0
  116. package/dist/commands/project/closeout-check/index.js +117 -0
  117. package/dist/commands/project/complete-state/index.d.ts +4 -0
  118. package/dist/commands/project/complete-state/index.d.ts.map +1 -1
  119. package/dist/commands/project/complete-state/index.js +11 -13
  120. package/dist/commands/project/dispatch/index.d.ts.map +1 -1
  121. package/dist/commands/project/dispatch/index.js +6 -30
  122. package/dist/commands/project/dispatch/record.d.ts +9 -18
  123. package/dist/commands/project/dispatch/record.d.ts.map +1 -1
  124. package/dist/commands/project/dispatch/record.js +15 -212
  125. package/dist/commands/project/index.d.ts.map +1 -1
  126. package/dist/commands/project/index.js +2 -0
  127. package/dist/commands/project/new/scaffold.d.ts +0 -1
  128. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  129. package/dist/commands/project/new/scaffold.js +19 -20
  130. package/dist/commands/project/promote/promote.d.ts +2 -2
  131. package/dist/commands/project/promote/promote.d.ts.map +1 -1
  132. package/dist/commands/project/promote/promote.js +13 -11
  133. package/dist/commands/shared/frontmatter.d.ts +21 -0
  134. package/dist/commands/shared/frontmatter.d.ts.map +1 -1
  135. package/dist/commands/shared/frontmatter.js +70 -4
  136. package/dist/commands/shared/template-source.d.ts +29 -0
  137. package/dist/commands/shared/template-source.d.ts.map +1 -0
  138. package/dist/commands/{pjm → shared}/template-source.js +24 -7
  139. package/dist/commands/state/generate.js +2 -2
  140. package/dist/commands/template/index.d.ts +10 -0
  141. package/dist/commands/template/index.d.ts.map +1 -0
  142. package/dist/commands/template/index.js +87 -0
  143. package/dist/commands/template/resolve.d.ts +31 -0
  144. package/dist/commands/template/resolve.d.ts.map +1 -0
  145. package/dist/commands/template/resolve.js +86 -0
  146. package/dist/fs/io.d.ts +0 -41
  147. package/dist/fs/io.d.ts.map +1 -1
  148. package/dist/fs/io.js +2 -414
  149. package/dist/providers/identity/absolute-paths.d.ts +4 -13
  150. package/dist/providers/identity/absolute-paths.d.ts.map +1 -1
  151. package/dist/providers/identity/absolute-paths.js +4 -17
  152. package/dist/providers/identity/generic-dispatch-record.d.ts +1 -1
  153. package/dist/providers/identity/generic-dispatch-record.js +2 -2
  154. package/dist/providers/identity/oat-dispatch-record.d.ts +4 -357
  155. package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -1
  156. package/dist/providers/identity/oat-dispatch-record.js +4 -236
  157. package/dist/release/public-package-contract.d.ts.map +1 -1
  158. package/dist/release/public-package-contract.js +12 -4
  159. package/dist/validation/skills.d.ts.map +1 -1
  160. package/dist/validation/skills.js +32 -6
  161. package/package.json +2 -2
  162. package/dist/commands/pjm/template-source.d.ts +0 -14
  163. package/dist/commands/pjm/template-source.d.ts.map +0 -1
@@ -6,6 +6,13 @@ import { dirname, relative, resolve } from 'node:path';
6
6
  import { canonicalJson, hashFile } from './lib/canonical-json.mjs';
7
7
  import { isDirectExecution } from './lib/cli-entry.mjs';
8
8
  import { issue, isObject, validateArtifactShape } from './lib/contracts.mjs';
9
+ import {
10
+ projectEvidenceLink,
11
+ projectReviewQuestions,
12
+ projectReviewScope,
13
+ projectReviewSources,
14
+ reviewBriefExcludedInputs,
15
+ } from './lib/review-binding.mjs';
9
16
  import { assertSafeOutputPath } from './lib/safe-path.mjs';
10
17
 
11
18
  const commonKeys = [
@@ -38,53 +45,11 @@ const forbiddenKeys = new Set([
38
45
  'dossierPath',
39
46
  ]);
40
47
 
41
- const reviewSourceFields = {
42
- repository: ['root', 'revision', 'dirty', 'contentHashes'],
43
- file: ['path', 'contentHash'],
44
- url: ['url', 'capturePath', 'captureDigest', 'validatorState'],
45
- 'command-output': [
46
- 'argv',
47
- 'cwd',
48
- 'exitStatus',
49
- 'outputPath',
50
- 'outputDigest',
51
- 'environmentNames',
52
- ],
53
- 'connected-resource': [
54
- 'system',
55
- 'resourceId',
56
- 'resourceVersion',
57
- 'retrievalToken',
58
- 'capturePath',
59
- 'captureDigest',
60
- ],
61
- };
62
- const commonReviewSourceFields = [
63
- 'id',
64
- 'kind',
65
- 'available',
66
- 'authority',
67
- 'observedAt',
68
- 'validationState',
69
- ];
70
-
71
48
  function selectedClaims(ledger, claimIds) {
72
49
  const selected = claimIds ? new Set(claimIds) : null;
73
50
  return ledger.claims.filter((claim) => !selected || selected.has(claim.id));
74
51
  }
75
52
 
76
- function projectReviewSource(source) {
77
- const kindFields = reviewSourceFields[source.kind];
78
- if (!kindFields) {
79
- throw new Error(`Cannot project unknown source kind ${source.kind}`);
80
- }
81
- return Object.fromEntries(
82
- [...commonReviewSourceFields, ...kindFields]
83
- .filter((key) => Object.hasOwn(source, key))
84
- .map((key) => [key, structuredClone(source[key])]),
85
- );
86
- }
87
-
88
53
  function verificationBrief(input) {
89
54
  const claims = selectedClaims(input.ledger, input.claimIds);
90
55
  const evidenceById = new Map(
@@ -95,19 +60,14 @@ function verificationBrief(input) {
95
60
  id: claim.id,
96
61
  statement: claim.statement,
97
62
  evidence: claim.evidence.map((link) => {
98
- const evidence = evidenceById.get(link.evidenceId);
63
+ const evidence = projectEvidenceLink(link, evidenceById);
99
64
  if (!evidence) {
100
65
  throw new Error(
101
66
  `Claim ${claim.id} references missing evidence ${link.evidenceId}`,
102
67
  );
103
68
  }
104
69
  sourceIds.add(evidence.sourceId);
105
- return {
106
- id: evidence.id,
107
- sourceId: evidence.sourceId,
108
- displayExcerpt: evidence.displayExcerpt,
109
- locator: structuredClone(evidence.locator),
110
- };
70
+ return evidence;
111
71
  }),
112
72
  }));
113
73
  const declaredSourceIds = new Set(
@@ -125,17 +85,9 @@ function verificationBrief(input) {
125
85
  runId: input.manifest.run.id,
126
86
  mode: 'verify',
127
87
  createdAt: input.createdAt,
128
- excludedInputs: [
129
- 'worker_intermediates',
130
- 'prior_reasoning',
131
- 'consumer_summary',
132
- 'artifact_lineage',
133
- 'earlier_reviews',
134
- ],
88
+ excludedInputs: [...reviewBriefExcludedInputs.verify],
135
89
  claims: projectedClaims,
136
- sources: input.manifest.sources
137
- .filter((source) => sourceIds.has(source.id))
138
- .map(projectReviewSource),
90
+ sources: projectReviewSources(input.manifest, sourceIds),
139
91
  };
140
92
  }
141
93
 
@@ -147,19 +99,9 @@ function adversarialBrief(input) {
147
99
  runId: input.manifest.run.id,
148
100
  mode: 'adversary',
149
101
  createdAt: input.createdAt,
150
- excludedInputs: [
151
- 'worker_intermediates',
152
- 'prior_reasoning',
153
- 'consumer_summary',
154
- 'artifact_lineage',
155
- 'earlier_reviews',
156
- 'verification_conclusions',
157
- ],
158
- scope: {
159
- included: structuredClone(input.manifest.request.includedScope ?? []),
160
- excluded: structuredClone(input.manifest.request.excludedScope ?? []),
161
- },
162
- questions: structuredClone(input.manifest.request.questions ?? []),
102
+ excludedInputs: [...reviewBriefExcludedInputs.adversary],
103
+ scope: projectReviewScope(input.manifest),
104
+ questions: projectReviewQuestions(input.manifest),
163
105
  provisionalStatements: selectedClaims(input.ledger, input.claimIds).map(
164
106
  (claim) => ({ id: claim.id, statement: claim.statement }),
165
107
  ),
@@ -174,20 +116,9 @@ function coverageBrief(input) {
174
116
  runId: input.manifest.run.id,
175
117
  mode: 'coverage',
176
118
  createdAt: input.createdAt,
177
- excludedInputs: [
178
- 'worker_intermediates',
179
- 'prior_reasoning',
180
- 'consumer_summary',
181
- 'artifact_lineage',
182
- 'earlier_reviews',
183
- 'verification_conclusions',
184
- 'adversarial_conclusions',
185
- ],
186
- scope: {
187
- included: structuredClone(input.manifest.request.includedScope ?? []),
188
- excluded: structuredClone(input.manifest.request.excludedScope ?? []),
189
- },
190
- questions: structuredClone(input.manifest.request.questions ?? []),
119
+ excludedInputs: [...reviewBriefExcludedInputs.coverage],
120
+ scope: projectReviewScope(input.manifest),
121
+ questions: projectReviewQuestions(input.manifest),
191
122
  claims: selectedClaims(input.ledger, input.claimIds).map((claim) => ({
192
123
  id: claim.id,
193
124
  statement: claim.statement,
@@ -1,3 +1,8 @@
1
+ import {
2
+ commonReviewSourceFields,
3
+ reviewSourceKindFields,
4
+ } from './review-binding.mjs';
5
+
1
6
  export const SCHEMA_VERSION = 1;
2
7
  export const MANIFEST_SCHEMA_VERSION = 2;
3
8
 
@@ -1807,36 +1812,6 @@ function validateDossier(value, errors) {
1807
1812
  }
1808
1813
  }
1809
1814
 
1810
- const reviewSourceKindFields = {
1811
- repository: ['root', 'revision', 'dirty', 'contentHashes'],
1812
- file: ['path', 'contentHash'],
1813
- url: ['url', 'capturePath', 'captureDigest', 'validatorState'],
1814
- 'command-output': [
1815
- 'argv',
1816
- 'cwd',
1817
- 'exitStatus',
1818
- 'outputPath',
1819
- 'outputDigest',
1820
- 'environmentNames',
1821
- ],
1822
- 'connected-resource': [
1823
- 'system',
1824
- 'resourceId',
1825
- 'resourceVersion',
1826
- 'retrievalToken',
1827
- 'capturePath',
1828
- 'captureDigest',
1829
- ],
1830
- };
1831
- const commonReviewSourceFields = [
1832
- 'id',
1833
- 'kind',
1834
- 'available',
1835
- 'authority',
1836
- 'observedAt',
1837
- 'validationState',
1838
- ];
1839
-
1840
1815
  function validateReviewBriefSource(source, index, errors) {
1841
1816
  const path = `$.sources[${index}]`;
1842
1817
  if (!isObject(source)) {
@@ -2073,10 +2048,44 @@ function validateReviewBriefSource(source, index, errors) {
2073
2048
  }
2074
2049
  }
2075
2050
 
2051
+ const reviewBriefIdPattern = /^[a-z0-9]+(?:[-_.][a-z0-9]+)*$/;
2052
+ const reviewBriefTimestampPattern =
2053
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
2054
+
2076
2055
  function validateReviewBriefArtifact(value, errors) {
2077
2056
  for (const key of ['id', 'runId', 'mode', 'createdAt']) {
2078
2057
  requiredString(value, key, errors);
2079
2058
  }
2059
+ // A brief's identifier and timestamp are shown to a blind reviewer, so
2060
+ // both are structurally fixed: a short lowercase slug and a UTC ISO-8601
2061
+ // instant. Neither can carry free text.
2062
+ if (
2063
+ typeof value.id === 'string' &&
2064
+ (value.id.length > 64 || !reviewBriefIdPattern.test(value.id))
2065
+ ) {
2066
+ errors.push(
2067
+ issue(
2068
+ 'INVALID_REVIEW_BRIEF',
2069
+ 'Brief id must be a lowercase slug of at most 64 characters',
2070
+ '$.id',
2071
+ ),
2072
+ );
2073
+ }
2074
+ if (
2075
+ typeof value.createdAt === 'string' &&
2076
+ !(
2077
+ reviewBriefTimestampPattern.test(value.createdAt) &&
2078
+ Number.isFinite(Date.parse(value.createdAt))
2079
+ )
2080
+ ) {
2081
+ errors.push(
2082
+ issue(
2083
+ 'INVALID_REVIEW_BRIEF',
2084
+ 'Brief createdAt must be a UTC ISO-8601 timestamp',
2085
+ '$.createdAt',
2086
+ ),
2087
+ );
2088
+ }
2080
2089
  requiredArray(value, 'excludedInputs', errors);
2081
2090
  if (value.mode === 'verify') {
2082
2091
  requiredArray(value, 'claims', errors);
@@ -2271,6 +2280,124 @@ function validateReviewBriefArtifact(value, errors) {
2271
2280
  }
2272
2281
  }
2273
2282
 
2283
+ // The affirming disposition each review kind gives a claim it supports. The
2284
+ // first three are the required reviews for `verified`. The reconciler, the
2285
+ // validator, and the renderer all read this one table.
2286
+ export const affirmingDispositionByReviewKind = Object.freeze({
2287
+ semantic: 'affirmed',
2288
+ adversarial: 'unchallenged',
2289
+ coverage: 'covered',
2290
+ 'redundant-verification': 'affirmed',
2291
+ 'contradiction-resolution': 'resolved',
2292
+ });
2293
+
2294
+ export const requiredReviewKinds = Object.freeze([
2295
+ 'semantic',
2296
+ 'adversarial',
2297
+ 'coverage',
2298
+ ]);
2299
+
2300
+ // `unresolvedIssues` entries are a closed union:
2301
+ // - a string (legacy), read as a global issue;
2302
+ // - `{ text, claimIds }`, scoped to a non-empty list of unique claims the
2303
+ // review covers; or
2304
+ // - `{ text, scope: 'global' }`.
2305
+ // Returns `{ scope: 'global' }`, `{ scope: 'claims', claimIds }`, or
2306
+ // `{ error }`. `coveredClaimIds`, when given, also rejects a scoped claim the
2307
+ // review does not cover. An error is rejected at artifact acceptance and is
2308
+ // never read as "no issue": `unresolvedIssuesBlockClaim` treats it as global.
2309
+ export function classifyUnresolvedIssue(entry, coveredClaimIds = null) {
2310
+ if (typeof entry === 'string') return { scope: 'global' };
2311
+ if (!isObject(entry)) {
2312
+ return {
2313
+ error: 'unresolvedIssues members must be strings or scoped issue objects',
2314
+ };
2315
+ }
2316
+ if (typeof entry.text !== 'string' || entry.text.trim().length === 0) {
2317
+ return { error: 'Scoped unresolved issues require non-empty text' };
2318
+ }
2319
+ const hasClaimIds = Object.hasOwn(entry, 'claimIds');
2320
+ const hasScope = Object.hasOwn(entry, 'scope');
2321
+ if (hasClaimIds && hasScope) {
2322
+ return {
2323
+ error: 'An unresolved issue names claimIds or a global scope, not both',
2324
+ };
2325
+ }
2326
+ if (!hasClaimIds && !hasScope) {
2327
+ return {
2328
+ error: 'An unresolved issue object requires claimIds or scope: global',
2329
+ };
2330
+ }
2331
+ const unknown = Object.keys(entry).filter(
2332
+ (key) => !['text', hasClaimIds ? 'claimIds' : 'scope'].includes(key),
2333
+ );
2334
+ if (unknown.length > 0) {
2335
+ return {
2336
+ error: `Unresolved issue has unknown fields: ${unknown.join(', ')}`,
2337
+ };
2338
+ }
2339
+ if (hasScope) {
2340
+ return entry.scope === 'global'
2341
+ ? { scope: 'global' }
2342
+ : { error: 'An unresolved issue scope must be global' };
2343
+ }
2344
+ if (!Array.isArray(entry.claimIds) || entry.claimIds.length === 0) {
2345
+ return {
2346
+ error: 'Scoped unresolved issues require a non-empty claimIds array',
2347
+ };
2348
+ }
2349
+ if (!entry.claimIds.every((id) => typeof id === 'string' && id.length > 0)) {
2350
+ return {
2351
+ error: 'Scoped unresolved issue claim IDs must be non-empty strings',
2352
+ };
2353
+ }
2354
+ if (new Set(entry.claimIds).size !== entry.claimIds.length) {
2355
+ return { error: 'Scoped unresolved issue claim IDs must be unique' };
2356
+ }
2357
+ const uncovered = coveredClaimIds
2358
+ ? entry.claimIds.filter((id) => !coveredClaimIds.has(id))
2359
+ : [];
2360
+ if (uncovered.length > 0) {
2361
+ return {
2362
+ error: `Scoped unresolved issue names claims the review does not cover: ${uncovered.join(', ')}`,
2363
+ };
2364
+ }
2365
+ return { scope: 'claims', claimIds: [...entry.claimIds] };
2366
+ }
2367
+
2368
+ // True when any of the review's unresolved issues applies to `claimId`. Call
2369
+ // it only for a claim the review covers: a global issue applies to every
2370
+ // covered claim, a scoped issue only to the claims it names.
2371
+ export function unresolvedIssuesBlockClaim(review, claimId) {
2372
+ return (
2373
+ Array.isArray(review?.unresolvedIssues) ? review.unresolvedIssues : []
2374
+ ).some((entry) => {
2375
+ const classification = classifyUnresolvedIssue(entry);
2376
+ if (classification.scope !== 'claims') return true;
2377
+ return classification.claimIds.includes(claimId);
2378
+ });
2379
+ }
2380
+
2381
+ function validateUnresolvedIssues(value, errors) {
2382
+ const coveredClaimIds = new Set(
2383
+ (Array.isArray(value.dispositions) ? value.dispositions : [])
2384
+ .map((disposition) => disposition?.claimId)
2385
+ .filter((id) => typeof id === 'string'),
2386
+ );
2387
+ for (const [index, entry] of value.unresolvedIssues.entries()) {
2388
+ const { error } = classifyUnresolvedIssue(entry, coveredClaimIds);
2389
+ if (error) {
2390
+ errors.push(
2391
+ issue(
2392
+ 'INVALID_UNRESOLVED_ISSUE',
2393
+ error,
2394
+ `$.unresolvedIssues[${index}]`,
2395
+ ),
2396
+ );
2397
+ }
2398
+ }
2399
+ }
2400
+
2274
2401
  function validateReviewResult(value, errors) {
2275
2402
  for (const key of ['id', 'runId', 'reviewKind', 'reviewerLane', 'status']) {
2276
2403
  requiredString(value, key, errors);
@@ -2286,17 +2413,7 @@ function validateReviewResult(value, errors) {
2286
2413
  requiredArray(value, key, errors);
2287
2414
  }
2288
2415
  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
- }
2416
+ validateUnresolvedIssues(value, errors);
2300
2417
  }
2301
2418
  if (
2302
2419
  ![
@@ -0,0 +1,123 @@
1
+ // One owner for review-brief projection: the descriptor allowlists, source,
2
+ // evidence, scope, and question projections, and each mode's fixed exclusion
3
+ // list. The brief generator and the artifact-shape contract read these, and
4
+ // the packet validator checks a brief by rebuilding it through the generator,
5
+ // so there is no second projection to drift. Briefs stay blind: only the
6
+ // fields below are projected, never full manifest entries or provenance.
7
+
8
+ export const reviewSourceKindFields = Object.freeze({
9
+ repository: Object.freeze(['root', 'revision', 'dirty', 'contentHashes']),
10
+ file: Object.freeze(['path', 'contentHash']),
11
+ url: Object.freeze(['url', 'capturePath', 'captureDigest', 'validatorState']),
12
+ 'command-output': Object.freeze([
13
+ 'argv',
14
+ 'cwd',
15
+ 'exitStatus',
16
+ 'outputPath',
17
+ 'outputDigest',
18
+ 'environmentNames',
19
+ ]),
20
+ 'connected-resource': Object.freeze([
21
+ 'system',
22
+ 'resourceId',
23
+ 'resourceVersion',
24
+ 'retrievalToken',
25
+ 'capturePath',
26
+ 'captureDigest',
27
+ ]),
28
+ });
29
+
30
+ export const commonReviewSourceFields = Object.freeze([
31
+ 'id',
32
+ 'kind',
33
+ 'available',
34
+ 'authority',
35
+ 'observedAt',
36
+ 'validationState',
37
+ ]);
38
+
39
+ // The fixed exclusion declaration each brief mode carries. It is structural,
40
+ // not caller-supplied, so a brief cannot use it as a free-text channel.
41
+ export const reviewBriefExcludedInputs = Object.freeze({
42
+ verify: Object.freeze([
43
+ 'worker_intermediates',
44
+ 'prior_reasoning',
45
+ 'consumer_summary',
46
+ 'artifact_lineage',
47
+ 'earlier_reviews',
48
+ ]),
49
+ adversary: Object.freeze([
50
+ 'worker_intermediates',
51
+ 'prior_reasoning',
52
+ 'consumer_summary',
53
+ 'artifact_lineage',
54
+ 'earlier_reviews',
55
+ 'verification_conclusions',
56
+ ]),
57
+ coverage: Object.freeze([
58
+ 'worker_intermediates',
59
+ 'prior_reasoning',
60
+ 'consumer_summary',
61
+ 'artifact_lineage',
62
+ 'earlier_reviews',
63
+ 'verification_conclusions',
64
+ 'adversarial_conclusions',
65
+ ]),
66
+ });
67
+
68
+ // Adversarial and coverage briefs carry the approved request's scope and
69
+ // questions, projected here for both the generator and the validator.
70
+ export function projectReviewScope(manifest) {
71
+ return {
72
+ included: structuredClone(manifest?.request?.includedScope ?? []),
73
+ excluded: structuredClone(manifest?.request?.excludedScope ?? []),
74
+ };
75
+ }
76
+
77
+ export function projectReviewQuestions(manifest) {
78
+ return structuredClone(manifest?.request?.questions ?? []);
79
+ }
80
+
81
+ const adversaryReviewKinds = new Set([
82
+ 'adversarial',
83
+ 'contradiction-resolution',
84
+ ]);
85
+
86
+ export function projectReviewSource(source) {
87
+ const kindFields = reviewSourceKindFields[source?.kind];
88
+ if (!kindFields) {
89
+ throw new Error(`Cannot project unknown source kind ${source?.kind}`);
90
+ }
91
+ return Object.fromEntries(
92
+ [...commonReviewSourceFields, ...kindFields]
93
+ .filter((key) => Object.hasOwn(source, key))
94
+ .map((key) => [key, structuredClone(source[key])]),
95
+ );
96
+ }
97
+
98
+ // Projects the manifest sources named by `sourceIds`, in manifest order.
99
+ export function projectReviewSources(manifest, sourceIds) {
100
+ return (manifest?.sources ?? [])
101
+ .filter((source) => sourceIds.has(source?.id))
102
+ .map(projectReviewSource);
103
+ }
104
+
105
+ export function projectEvidenceLink(link, evidenceById) {
106
+ const evidence = evidenceById.get(link?.evidenceId);
107
+ if (!evidence) return null;
108
+ return {
109
+ id: evidence.id,
110
+ sourceId: evidence.sourceId,
111
+ displayExcerpt: evidence.displayExcerpt,
112
+ locator: structuredClone(evidence.locator),
113
+ };
114
+ }
115
+
116
+ // The claim-bearing entries of a brief: adversarial briefs project
117
+ // `provisionalStatements`; verification and coverage briefs project `claims`.
118
+ export function reviewBriefEntries(brief, reviewKind) {
119
+ const entries = adversaryReviewKinds.has(reviewKind)
120
+ ? brief?.provisionalStatements
121
+ : brief?.claims;
122
+ return Array.isArray(entries) ? entries : [];
123
+ }
@@ -428,7 +428,10 @@ export function createRoutingPreview(manifest) {
428
428
  maxConcurrency: execution.maxConcurrency,
429
429
  deadlineSeconds: execution.deadlineSeconds,
430
430
  retryLimit: execution.retryLimit,
431
- worstCaseLaneAttempts: laneCount * (execution.retryLimit + 1),
431
+ // retryLimit counts pre-acceptance admission retries per lane, and the
432
+ // controller makes at most one, so a lane has at most two attempts.
433
+ worstCaseLaneAttempts:
434
+ laneCount * (Math.min(execution.retryLimit, 1) + 1),
432
435
  },
433
436
  });
434
437
  }
@@ -512,7 +515,7 @@ export function renderRoutingPreview(preview, format = 'markdown') {
512
515
  `- Conditions: ${encodeMarkdownValue(preview.limits.conditionCount)}`,
513
516
  `- Concurrency: ${encodeMarkdownValue(preview.limits.maxConcurrency)}`,
514
517
  `- Deadline seconds: ${encodeMarkdownValue(preview.limits.deadlineSeconds)}`,
515
- `- Retry limit: ${encodeMarkdownValue(preview.limits.retryLimit)}`,
518
+ `- Pre-acceptance admission retries per lane (at most one used): ${encodeMarkdownValue(preview.limits.retryLimit)}`,
516
519
  `- Lane attempts: ${encodeMarkdownValue(preview.limits.worstCaseLaneAttempts)}`,
517
520
  );
518
521
  if (preview.profileCaps) {
@@ -12,7 +12,12 @@ import { dirname, resolve } from 'node:path';
12
12
 
13
13
  import { canonicalJson, hashFile } from './lib/canonical-json.mjs';
14
14
  import { isDirectExecution } from './lib/cli-entry.mjs';
15
- import { validateArtifactShape } from './lib/contracts.mjs';
15
+ import {
16
+ affirmingDispositionByReviewKind,
17
+ requiredReviewKinds,
18
+ unresolvedIssuesBlockClaim,
19
+ validateArtifactShape,
20
+ } from './lib/contracts.mjs';
16
21
  import {
17
22
  assertCanonicalRoot,
18
23
  assertSafeExistingPath,
@@ -20,11 +25,12 @@ import {
20
25
  assertUnchangedRoot,
21
26
  } from './lib/safe-path.mjs';
22
27
 
23
- const requiredDispositions = new Map([
24
- ['semantic', 'affirmed'],
25
- ['adversarial', 'unchallenged'],
26
- ['coverage', 'covered'],
27
- ]);
28
+ const requiredDispositions = new Map(
29
+ requiredReviewKinds.map((kind) => [
30
+ kind,
31
+ affirmingDispositionByReviewKind[kind],
32
+ ]),
33
+ );
28
34
 
29
35
  const permittedDispositions = new Map([
30
36
  ['semantic', new Set(['affirmed', 'rejected', 'uncertain'])],
@@ -233,6 +239,11 @@ export function reconcileLedger({
233
239
  (item) => item.claimId === claim.id && item.disposition === 'uncertain',
234
240
  ),
235
241
  );
242
+ // A covering review's global issue, or an issue scoped to this claim,
243
+ // keeps the claim below verified. Publication applies the same rule.
244
+ const issueBlocked = supporting.some((review) =>
245
+ unresolvedIssuesBlockClaim(review, claim.id),
246
+ );
236
247
  const incomplete = [...requiredDispositions.keys()].some(
237
248
  (kind) =>
238
249
  !reviewResults.some(
@@ -291,7 +302,7 @@ export function reconcileLedger({
291
302
  }
292
303
  continue;
293
304
  }
294
- if (uncertain || incomplete) {
305
+ if (uncertain || issueBlocked || incomplete) {
295
306
  const from = claim.status;
296
307
  const to = 'unresolved';
297
308
  if (legalReconciliationTransitions.has(`${from}:${to}`)) {