@open-agent-toolkit/cli 0.2.51 → 0.2.52

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 (213) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/bootstrap.md +6 -2
  3. package/assets/docs/cli-utilities/tool-packs.md +87 -33
  4. package/assets/docs/provider-sync/commands.md +30 -2
  5. package/assets/docs/provider-sync/config.md +19 -0
  6. package/assets/docs/provider-sync/manifest-and-drift.md +37 -0
  7. package/assets/docs/provider-sync/providers.md +69 -2
  8. package/assets/docs/provider-sync/scope-and-surface.md +44 -0
  9. package/assets/docs/reference/cli-reference.md +1 -0
  10. package/assets/docs/reference/oat-directory-structure.md +11 -4
  11. package/assets/docs/workflows/projects/evidence-layers.md +110 -1
  12. package/assets/docs/workflows/projects/implementation-execution.md +13 -0
  13. package/assets/docs/workflows/projects/orchestration-model.md +18 -0
  14. package/assets/public-package-versions.json +4 -4
  15. package/assets/skills/oat-dispatch-subagents/SKILL.md +57 -1
  16. package/assets/skills/oat-dispatch-subagents/references/record-schema.md +77 -1
  17. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
  18. package/assets/skills/oat-project-dispatch-subagents/SKILL.md +24 -1
  19. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
  20. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +11 -0
  21. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
  22. package/assets/skills/oat-project-plan-writing/SKILL.md +12 -1
  23. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
  24. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
  25. package/assets/skills/oat-project-review-provide/SKILL.md +12 -1
  26. package/assets/skills/oat-project-review-provide-remote/SKILL.md +12 -1
  27. package/dist/agents/canonical/index.d.ts +2 -0
  28. package/dist/agents/canonical/index.d.ts.map +1 -1
  29. package/dist/agents/canonical/index.js +1 -0
  30. package/dist/agents/canonical/resolve.d.ts +45 -0
  31. package/dist/agents/canonical/resolve.d.ts.map +1 -0
  32. package/dist/agents/canonical/resolve.js +175 -0
  33. package/dist/commands/decision/agents-guidance.d.ts +2 -2
  34. package/dist/commands/decision/agents-guidance.d.ts.map +1 -1
  35. package/dist/commands/decision/agents-guidance.js +1 -4
  36. package/dist/commands/decision/index.d.ts.map +1 -1
  37. package/dist/commands/decision/index.js +25 -6
  38. package/dist/commands/docs/init/index.d.ts +7 -1
  39. package/dist/commands/docs/init/index.d.ts.map +1 -1
  40. package/dist/commands/docs/init/index.js +53 -20
  41. package/dist/commands/doctor/index.d.ts +8 -2
  42. package/dist/commands/doctor/index.d.ts.map +1 -1
  43. package/dist/commands/doctor/index.js +157 -46
  44. package/dist/commands/init/index.d.ts +8 -3
  45. package/dist/commands/init/index.d.ts.map +1 -1
  46. package/dist/commands/init/index.js +64 -26
  47. package/dist/commands/init/tools/index.d.ts +12 -12
  48. package/dist/commands/init/tools/index.d.ts.map +1 -1
  49. package/dist/commands/init/tools/index.js +430 -99
  50. package/dist/commands/init/tools/install-state.d.ts +2 -0
  51. package/dist/commands/init/tools/install-state.d.ts.map +1 -1
  52. package/dist/commands/init/tools/install-state.js +11 -0
  53. package/dist/commands/init/tools/project-guidance.d.ts +49 -0
  54. package/dist/commands/init/tools/project-guidance.d.ts.map +1 -0
  55. package/dist/commands/init/tools/project-guidance.js +144 -0
  56. package/dist/commands/init/tools/workflows/index.d.ts +7 -0
  57. package/dist/commands/init/tools/workflows/index.d.ts.map +1 -1
  58. package/dist/commands/init/tools/workflows/index.js +160 -7
  59. package/dist/commands/pjm/index.d.ts.map +1 -1
  60. package/dist/commands/pjm/index.js +28 -3
  61. package/dist/commands/pjm/init.d.ts +5 -0
  62. package/dist/commands/pjm/init.d.ts.map +1 -1
  63. package/dist/commands/pjm/init.js +19 -3
  64. package/dist/commands/project/dispatch/index.d.ts +10 -0
  65. package/dist/commands/project/dispatch/index.d.ts.map +1 -0
  66. package/dist/commands/project/dispatch/index.js +137 -0
  67. package/dist/commands/project/dispatch/record.d.ts +91 -0
  68. package/dist/commands/project/dispatch/record.d.ts.map +1 -0
  69. package/dist/commands/project/dispatch/record.js +402 -0
  70. package/dist/commands/project/index.d.ts.map +1 -1
  71. package/dist/commands/project/index.js +2 -0
  72. package/dist/commands/providers/inspect/inspect.d.ts +8 -1
  73. package/dist/commands/providers/inspect/inspect.d.ts.map +1 -1
  74. package/dist/commands/providers/inspect/inspect.js +146 -26
  75. package/dist/commands/providers/list/list.d.ts +8 -1
  76. package/dist/commands/providers/list/list.d.ts.map +1 -1
  77. package/dist/commands/providers/list/list.js +139 -23
  78. package/dist/commands/providers/providers.types.d.ts +25 -1
  79. package/dist/commands/providers/providers.types.d.ts.map +1 -1
  80. package/dist/commands/providers/set/index.d.ts +9 -1
  81. package/dist/commands/providers/set/index.d.ts.map +1 -1
  82. package/dist/commands/providers/set/index.js +43 -33
  83. package/dist/commands/remove/skill/remove-skill.d.ts.map +1 -1
  84. package/dist/commands/remove/skill/remove-skill.js +11 -0
  85. package/dist/commands/shared/agents-md.d.ts +42 -14
  86. package/dist/commands/shared/agents-md.d.ts.map +1 -1
  87. package/dist/commands/shared/agents-md.js +364 -70
  88. package/dist/commands/status/index.d.ts +8 -3
  89. package/dist/commands/status/index.d.ts.map +1 -1
  90. package/dist/commands/status/index.js +147 -32
  91. package/dist/commands/sync/apply.d.ts.map +1 -1
  92. package/dist/commands/sync/apply.js +220 -29
  93. package/dist/commands/sync/dry-run.d.ts.map +1 -1
  94. package/dist/commands/sync/dry-run.js +30 -6
  95. package/dist/commands/sync/index.d.ts +17 -2
  96. package/dist/commands/sync/index.d.ts.map +1 -1
  97. package/dist/commands/sync/index.js +106 -41
  98. package/dist/commands/sync/sync.types.d.ts +39 -2
  99. package/dist/commands/sync/sync.types.d.ts.map +1 -1
  100. package/dist/commands/sync/sync.utils.d.ts +5 -1
  101. package/dist/commands/sync/sync.utils.d.ts.map +1 -1
  102. package/dist/commands/sync/sync.utils.js +73 -0
  103. package/dist/commands/tools/info/info-tool.d.ts +2 -0
  104. package/dist/commands/tools/info/info-tool.d.ts.map +1 -1
  105. package/dist/commands/tools/info/info-tool.js +34 -12
  106. package/dist/commands/tools/install/index.d.ts.map +1 -1
  107. package/dist/commands/tools/install/index.js +19 -3
  108. package/dist/commands/tools/list/list-tools.d.ts +2 -0
  109. package/dist/commands/tools/list/list-tools.d.ts.map +1 -1
  110. package/dist/commands/tools/list/list-tools.js +36 -7
  111. package/dist/commands/tools/remove/index.d.ts.map +1 -1
  112. package/dist/commands/tools/remove/index.js +150 -21
  113. package/dist/commands/tools/remove/remove-tools.d.ts +20 -0
  114. package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
  115. package/dist/commands/tools/remove/remove-tools.js +122 -1
  116. package/dist/commands/tools/shared/format-pack-inventory.d.ts +19 -0
  117. package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -1
  118. package/dist/commands/tools/shared/format-pack-inventory.js +109 -0
  119. package/dist/commands/tools/shared/pack-evidence.d.ts +66 -0
  120. package/dist/commands/tools/shared/pack-evidence.d.ts.map +1 -0
  121. package/dist/commands/tools/shared/pack-evidence.js +132 -0
  122. package/dist/commands/tools/shared/pack-inventory.d.ts +1 -0
  123. package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
  124. package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts +44 -0
  125. package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts.map +1 -0
  126. package/dist/commands/tools/shared/pack-lifecycle-outcome.js +91 -0
  127. package/dist/commands/tools/update/index.d.ts.map +1 -1
  128. package/dist/commands/tools/update/index.js +74 -4
  129. package/dist/commands/tools/update/update-tools.d.ts +5 -0
  130. package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
  131. package/dist/commands/tools/update/update-tools.js +64 -2
  132. package/dist/config/oat-config.d.ts +5 -0
  133. package/dist/config/oat-config.d.ts.map +1 -1
  134. package/dist/config/oat-config.js +17 -5
  135. package/dist/config/user-sync-config.d.ts +1 -0
  136. package/dist/config/user-sync-config.d.ts.map +1 -1
  137. package/dist/config/user-sync-config.js +8 -0
  138. package/dist/drift/detector.d.ts +2 -2
  139. package/dist/drift/detector.d.ts.map +1 -1
  140. package/dist/drift/detector.js +18 -0
  141. package/dist/drift/strays.d.ts.map +1 -1
  142. package/dist/drift/strays.js +5 -0
  143. package/dist/engine/collection-sync.d.ts +14 -0
  144. package/dist/engine/collection-sync.d.ts.map +1 -0
  145. package/dist/engine/collection-sync.js +282 -0
  146. package/dist/engine/compute-plan.d.ts +8 -1
  147. package/dist/engine/compute-plan.d.ts.map +1 -1
  148. package/dist/engine/compute-plan.js +303 -12
  149. package/dist/engine/engine.types.d.ts +61 -0
  150. package/dist/engine/engine.types.d.ts.map +1 -1
  151. package/dist/engine/execute-plan.d.ts +22 -2
  152. package/dist/engine/execute-plan.d.ts.map +1 -1
  153. package/dist/engine/execute-plan.js +459 -27
  154. package/dist/engine/provider-path-safety.d.ts +1 -0
  155. package/dist/engine/provider-path-safety.d.ts.map +1 -1
  156. package/dist/engine/provider-path-safety.js +15 -0
  157. package/dist/engine/scanner.d.ts +2 -2
  158. package/dist/engine/scanner.d.ts.map +1 -1
  159. package/dist/engine/scanner.js +21 -4
  160. package/dist/fs/io.d.ts +67 -0
  161. package/dist/fs/io.d.ts.map +1 -1
  162. package/dist/fs/io.js +536 -3
  163. package/dist/manifest/index.d.ts +2 -2
  164. package/dist/manifest/index.d.ts.map +1 -1
  165. package/dist/manifest/index.js +1 -1
  166. package/dist/manifest/manager.d.ts +2 -2
  167. package/dist/manifest/manager.d.ts.map +1 -1
  168. package/dist/manifest/manager.js +20 -3
  169. package/dist/manifest/manifest.types.d.ts +704 -13
  170. package/dist/manifest/manifest.types.d.ts.map +1 -1
  171. package/dist/manifest/manifest.types.js +131 -15
  172. package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
  173. package/dist/providers/codex/codec/sync-extension.js +41 -10
  174. package/dist/providers/cursor/codec/sync-extension.d.ts.map +1 -1
  175. package/dist/providers/cursor/codec/sync-extension.js +41 -10
  176. package/dist/providers/identity/absolute-paths.d.ts +68 -0
  177. package/dist/providers/identity/absolute-paths.d.ts.map +1 -0
  178. package/dist/providers/identity/absolute-paths.js +144 -0
  179. package/dist/providers/identity/claude-runtime-observation.d.ts +105 -0
  180. package/dist/providers/identity/claude-runtime-observation.d.ts.map +1 -0
  181. package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts +40 -0
  182. package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts.map +1 -0
  183. package/dist/providers/identity/claude-runtime-observation.fixtures.js +146 -0
  184. package/dist/providers/identity/claude-runtime-observation.js +229 -0
  185. package/dist/providers/identity/codex-runtime-observation.d.ts +75 -0
  186. package/dist/providers/identity/codex-runtime-observation.d.ts.map +1 -0
  187. package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts +37 -0
  188. package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts.map +1 -0
  189. package/dist/providers/identity/codex-runtime-observation.fixtures.js +539 -0
  190. package/dist/providers/identity/codex-runtime-observation.js +255 -0
  191. package/dist/providers/identity/generic-dispatch-record.d.ts +313 -0
  192. package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -0
  193. package/dist/providers/identity/generic-dispatch-record.js +539 -0
  194. package/dist/providers/identity/oat-dispatch-record.d.ts +1964 -0
  195. package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -0
  196. package/dist/providers/identity/oat-dispatch-record.js +691 -0
  197. package/dist/providers/identity/runtime-observation.d.ts +100 -0
  198. package/dist/providers/identity/runtime-observation.d.ts.map +1 -0
  199. package/dist/providers/identity/runtime-observation.js +169 -0
  200. package/dist/providers/shared/index.d.ts +2 -0
  201. package/dist/providers/shared/index.d.ts.map +1 -1
  202. package/dist/providers/shared/index.js +1 -0
  203. package/dist/providers/shared/materialization-extension.d.ts +12 -0
  204. package/dist/providers/shared/materialization-extension.d.ts.map +1 -1
  205. package/dist/providers/shared/registry.d.ts +69 -0
  206. package/dist/providers/shared/registry.d.ts.map +1 -0
  207. package/dist/providers/shared/registry.js +252 -0
  208. package/dist/providers/shared/restart-adviser.d.ts +26 -0
  209. package/dist/providers/shared/restart-adviser.d.ts.map +1 -0
  210. package/dist/providers/shared/restart-adviser.js +96 -0
  211. package/dist/shared/types.d.ts +4 -0
  212. package/dist/shared/types.d.ts.map +1 -1
  213. package/package.json +2 -2
@@ -0,0 +1,691 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { redactAbsolutePathsDeep } from './absolute-paths.js';
4
+ import { assertBoundedDispatchRecordSize, assertNoSensitiveDispatchContent, genericDispatchRecordSchema, normalizeDispatchKey, parseGenericDispatchRecord, } from './generic-dispatch-record.js';
5
+ /**
6
+ * A pre-start native-selection rejection is a closed event set. It is proof
7
+ * that the native launch surface refused the exact target before any child
8
+ * started, and it is the only category that can authorize one canonical
9
+ * fallback. It is deliberately disjoint from terminal child-outcome codes.
10
+ */
11
+ export const QUALIFYING_PRE_START_REJECTION_CODES = [
12
+ 'native-role-unavailable',
13
+ 'native-target-unavailable',
14
+ 'native-selector-unsupported',
15
+ 'native-catalog-unsatisfying',
16
+ 'capability-unresolved-or-unsupported',
17
+ 'wrapper-payload-rejected',
18
+ 'wrapper-launch-refused',
19
+ ];
20
+ /**
21
+ * Terminal or post-acceptance outcome families. None of these ever authorizes
22
+ * replacement, so they are rejected with an explicit diagnostic rather than
23
+ * falling through the generic closed-set message.
24
+ */
25
+ const PROHIBITED_REJECTION_FAMILIES = [
26
+ { term: 'timeout', category: 'timeout' },
27
+ { term: 'timedout', category: 'timeout' },
28
+ { term: 'deadlineexceeded', category: 'timeout' },
29
+ { term: 'blocked', category: 'BLOCKED' },
30
+ { term: 'refus', category: 'refusal' },
31
+ { term: 'declin', category: 'refusal' },
32
+ { term: 'interrupt', category: 'interruption' },
33
+ { term: 'cancel', category: 'interruption' },
34
+ { term: 'abort', category: 'interruption' },
35
+ { term: 'mismatch', category: 'runtime mismatch' },
36
+ { term: 'missingtelemetry', category: 'missing telemetry' },
37
+ { term: 'notreported', category: 'missing telemetry' },
38
+ { term: 'malformed', category: 'malformed output' },
39
+ { term: 'postacceptance', category: 'post-acceptance' },
40
+ { term: 'postlaunch', category: 'post-acceptance' },
41
+ { term: 'poststart', category: 'post-acceptance' },
42
+ ];
43
+ function prohibitedRejectionCategory(code) {
44
+ const normalized = normalizeDispatchKey(code);
45
+ return (PROHIBITED_REJECTION_FAMILIES.find(({ term }) => normalized.includes(term))
46
+ ?.category ?? null);
47
+ }
48
+ const preStartRejectionCodeSchema = z
49
+ .string()
50
+ .min(1)
51
+ .superRefine((code, context) => {
52
+ // The closed qualifying set is authoritative. Family classification only
53
+ // supplies a better diagnostic for a code that is already not qualifying.
54
+ if (QUALIFYING_PRE_START_REJECTION_CODES.includes(code)) {
55
+ return;
56
+ }
57
+ const prohibited = prohibitedRejectionCategory(code);
58
+ context.addIssue({
59
+ code: z.ZodIssueCode.custom,
60
+ message: prohibited === null
61
+ ? `Pre-start rejection code must be one of the qualifying codes: ${QUALIFYING_PRE_START_REJECTION_CODES.join(', ')}.`
62
+ : `A ${prohibited} outcome is not a pre-start native-selection rejection and never authorizes fallback or replacement.`,
63
+ });
64
+ });
65
+ /**
66
+ * Immutable configured controls across a fallback link.
67
+ *
68
+ * The rule, so a newly added generic field has a default: a field is immutable
69
+ * when it describes the configured selection, the authorization for that
70
+ * selection, or the evidence that justified it. A field is mutable when it
71
+ * describes this record's own identity, how the work is briefed to its own
72
+ * child, its own launch lifecycle, or its own child's observed behaviour. When
73
+ * a new field is ambiguous, treat it as immutable — a false "controls
74
+ * preserved" claim is worse than a false mismatch.
75
+ *
76
+ * All 46 generic fields are accounted for. The 31 immutable ones are listed
77
+ * below. The 15 mutable ones, each with its reason:
78
+ * - `request_id`: the fallback is a distinct request and must have its own ID.
79
+ * - `caller`: the adapter layer that launches the fallback may differ.
80
+ * - `objective`: a canonical-instruction fallback necessarily rebriefs the work
81
+ * for a generic child rather than a resolved role.
82
+ * - `role_name`, `role_selector`: the fallback deliberately targets a generic
83
+ * worker; substituting the role is the whole point of the approximation, and
84
+ * the resolved role identity is bound separately through `roleInstructions`.
85
+ * - `candidates_considered`: the fallback evaluated a different candidate set.
86
+ * - `selection_reason`: constrained separately — a fallback must declare
87
+ * `pre-start-rejection`, which by definition differs from the trigger's.
88
+ * - `launch_status`, `child_outcome`: each record's own lifecycle state.
89
+ * - `runtime_confirmation`: observation of that record's own child. The
90
+ * fallback is a different child, so equality would be a false assertion.
91
+ * - `diagnostics`: per-record narration. A fallback legitimately explains why
92
+ * it exists.
93
+ * - `continuation_events`: continuation linkage belongs to the record that
94
+ * owns the handle.
95
+ * - `expected_output`, `verification_evidence`, `escalate_when`: the brief
96
+ * given to this record's own child, in the same class as `objective`.
97
+ *
98
+ * Included even though they are evidence rather than controls, because they
99
+ * record why the preserved selection was allowed:
100
+ * - `configured_invocation_evidence`: names the configuration that authorized
101
+ * the preserved route.
102
+ * - `catalog_snapshot`: names the catalog the selection was made against.
103
+ * - `guidance_reference`, `guidance_version`, `guidance_verified_at`,
104
+ * `guidance_status`: name the dated model guidance behind the selection and
105
+ * how fresh it was; a fallback restating `stale` as `fresh` would misstate
106
+ * provenance for a preserved target.
107
+ * - `classification_reason`: the rationale for `task_class`, which is itself a
108
+ * compared authorization control.
109
+ */
110
+ export const IMMUTABLE_FALLBACK_CONTROL_FIELDS = [
111
+ 'provider',
112
+ 'selection_source',
113
+ 'model_selector',
114
+ 'model_selector_granularity',
115
+ 'effort_selector',
116
+ 'reasoning_mode_selector',
117
+ 'service_tier_selector',
118
+ 'selected_route',
119
+ 'authority',
120
+ 'authorization_scope',
121
+ 'deadline_seconds',
122
+ 'retry_limit',
123
+ 'payload',
124
+ 'fallback',
125
+ 'catalog_snapshot',
126
+ 'configured_invocation_evidence',
127
+ 'guidance_reference',
128
+ 'guidance_version',
129
+ 'guidance_verified_at',
130
+ 'guidance_status',
131
+ 'dispatch_context',
132
+ 'dispatch_policy',
133
+ 'dispatch_ceiling',
134
+ 'scope',
135
+ 'action',
136
+ 'role_class',
137
+ 'task_class',
138
+ 'model_class_floor',
139
+ 'classification_source',
140
+ 'classification_reason',
141
+ 'floor_satisfaction',
142
+ ];
143
+ /**
144
+ * The mutable complement, kept executable rather than only described above so a
145
+ * newly added generic field fails `covers every generic dispatch field` until
146
+ * somebody records a decision for it.
147
+ */
148
+ export const MUTABLE_FALLBACK_CONTROL_FIELDS = [
149
+ 'request_id',
150
+ 'caller',
151
+ 'objective',
152
+ 'role_name',
153
+ 'role_selector',
154
+ 'candidates_considered',
155
+ 'selection_reason',
156
+ 'launch_status',
157
+ 'child_outcome',
158
+ 'runtime_confirmation',
159
+ 'diagnostics',
160
+ 'continuation_events',
161
+ 'expected_output',
162
+ 'verification_evidence',
163
+ 'escalate_when',
164
+ ];
165
+ export function canonicalEvidenceJson(value) {
166
+ if (Array.isArray(value)) {
167
+ return `[${value.map(canonicalEvidenceJson).join(',')}]`;
168
+ }
169
+ if (value !== null && typeof value === 'object') {
170
+ return `{${Object.entries(value)
171
+ .filter(([, entry]) => entry !== undefined)
172
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
173
+ .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalEvidenceJson(entry)}`)
174
+ .join(',')}}`;
175
+ }
176
+ return JSON.stringify(value ?? null);
177
+ }
178
+ function controlProjection(record) {
179
+ return Object.fromEntries(IMMUTABLE_FALLBACK_CONTROL_FIELDS.map((field) => [
180
+ field,
181
+ record[field] ?? null,
182
+ ]));
183
+ }
184
+ export function configuredControlDigest(record) {
185
+ return `sha256:${createHash('sha256')
186
+ .update(canonicalEvidenceJson(controlProjection(record)))
187
+ .digest('hex')}`;
188
+ }
189
+ function differingControlFields(left, right) {
190
+ const leftControls = controlProjection(left);
191
+ const rightControls = controlProjection(right);
192
+ return IMMUTABLE_FALLBACK_CONTROL_FIELDS.filter((field) => canonicalEvidenceJson(leftControls[field]) !==
193
+ canonicalEvidenceJson(rightControls[field]));
194
+ }
195
+ const redactedPathSchema = z
196
+ .string()
197
+ .regex(/^<(?:loaded|user|project)>\/agents\/[a-z0-9][a-z0-9_-]*\.md$/);
198
+ const candidateMissSchema = z
199
+ .object({
200
+ tier: z.enum(['loaded', 'user', 'project']),
201
+ candidate: redactedPathSchema,
202
+ outcome: z.enum([
203
+ 'missing',
204
+ 'broken-symlink',
205
+ 'escaping-symlink',
206
+ 'noncanonical-copy',
207
+ 'wrong-target',
208
+ 'invalid-role',
209
+ ]),
210
+ })
211
+ .strict();
212
+ const resolvedRoleSchema = z
213
+ .object({
214
+ status: z.literal('resolved'),
215
+ dependency: z.string().min(1),
216
+ canonicalRole: z.string().min(1),
217
+ tier: z.enum(['loaded', 'user', 'project']),
218
+ validation: z.enum(['direct-canonical', 'exact-canonical-symlink']),
219
+ canonicalPath: redactedPathSchema,
220
+ selectedPath: redactedPathSchema,
221
+ roleVersion: z.string().min(1),
222
+ contentDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/),
223
+ candidateMisses: z.array(candidateMissSchema),
224
+ })
225
+ .strict();
226
+ const missingRoleSchema = z
227
+ .object({
228
+ status: z.literal('missing'),
229
+ dependency: z.string().min(1),
230
+ canonicalRole: z.string().min(1),
231
+ candidateMisses: z.array(candidateMissSchema),
232
+ recovery: z.array(z.object({ command: z.string().min(1) }).strict()).min(1),
233
+ })
234
+ .strict();
235
+ const canonicalRoleSchema = z.discriminatedUnion('status', [
236
+ resolvedRoleSchema,
237
+ missingRoleSchema,
238
+ ]);
239
+ const rejectionSchema = z
240
+ .object({
241
+ code: preStartRejectionCodeSchema,
242
+ rejectedAt: z.string().datetime(),
243
+ provesNoChildStarted: z.literal(true),
244
+ })
245
+ .strict();
246
+ const exactTargetSchema = z
247
+ .object({
248
+ provider: z.string().min(1),
249
+ modelSelector: z.string().min(1).nullable(),
250
+ effortSelector: z.string().min(1).nullable(),
251
+ reasoningModeSelector: z.string().min(1).nullable(),
252
+ serviceTierSelector: z.string().min(1).nullable(),
253
+ selectedRoute: z.string().min(1),
254
+ })
255
+ .strict();
256
+ const fallbackDispatchSchema = z
257
+ .object({
258
+ status: z.literal('fallback-dispatch'),
259
+ triggerRequestId: z.string().min(1),
260
+ fallbackRequestId: z.string().min(1),
261
+ trigger: z.literal('pre-start-rejection'),
262
+ fallbackReason: z.string().min(1),
263
+ kind: z.literal('canonical-instruction-fresh-child'),
264
+ approximation: z.literal(true),
265
+ preservedTarget: exactTargetSchema,
266
+ rejection: rejectionSchema.extend({
267
+ source: z.literal('provider-wrapper'),
268
+ }),
269
+ roleInstructions: resolvedRoleSchema,
270
+ })
271
+ .strict();
272
+ const fallbackSchema = z.discriminatedUnion('status', [
273
+ z
274
+ .object({
275
+ status: z.literal('not-applicable'),
276
+ reason: z.string().min(1),
277
+ })
278
+ .strict(),
279
+ fallbackDispatchSchema,
280
+ ]);
281
+ /**
282
+ * Observation values are provider-reported identifiers, not prose. They are
283
+ * bounded at the same 256-character caller-authored identifier limit the
284
+ * generic record uses, so a transcript body cannot ride in through an
285
+ * optional metadata field.
286
+ */
287
+ const MAX_OBSERVATION_VALUE_LENGTH = 256;
288
+ /**
289
+ * Provider identifiers only. Neither `/` nor `:` appears in any observed model,
290
+ * effort, tier, role, lineage, or source value, and admitting them let absolute
291
+ * paths (`C:/Users/...`), relative path-ish values, and URLs through in some
292
+ * spellings while NFR1 only ever intended none. Excluding both separators
293
+ * closes every spelling at once rather than denying them one at a time.
294
+ */
295
+ const OBSERVATION_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
296
+ /** The axes an observation and a configured invocation can be compared on. */
297
+ const COMPARED_OBSERVATION_AXES = [
298
+ 'role',
299
+ 'model',
300
+ 'effort',
301
+ 'serviceTier',
302
+ ];
303
+ /**
304
+ * The single canonical observation-value validator. Both the metadata path and
305
+ * a caller-supplied observation resolve through it, so a value can never be
306
+ * stored on one path that would be refused on the other.
307
+ */
308
+ export function observationIdentifier(value) {
309
+ if (typeof value !== 'string')
310
+ return null;
311
+ const trimmed = value.trim();
312
+ if (trimmed.length === 0 || trimmed.length > MAX_OBSERVATION_VALUE_LENGTH) {
313
+ return null;
314
+ }
315
+ return OBSERVATION_IDENTIFIER_PATTERN.test(trimmed) ? trimmed : null;
316
+ }
317
+ const observationValue = () => z
318
+ .string()
319
+ .min(1)
320
+ .max(MAX_OBSERVATION_VALUE_LENGTH)
321
+ .regex(OBSERVATION_IDENTIFIER_PATTERN, 'Observation values must be provider identifiers.');
322
+ const runtimeObservationSchema = z.discriminatedUnion('status', [
323
+ z.object({ status: z.literal('not-reported') }).strict(),
324
+ z
325
+ .object({
326
+ status: z.literal('reported'),
327
+ provider: observationValue(),
328
+ childLineage: observationValue().optional(),
329
+ role: observationValue().optional(),
330
+ model: observationValue().optional(),
331
+ effort: observationValue().optional(),
332
+ serviceTier: observationValue().optional(),
333
+ source: observationValue(),
334
+ observedAt: z.string().datetime(),
335
+ match: z.enum(['matching', 'mismatching', 'not-comparable']),
336
+ /**
337
+ * The axes the verdict actually rests on. Optional so a record written
338
+ * before this field existed still parses, but always written now: a
339
+ * `matching` that rests on one incidental axis is a materially weaker
340
+ * claim than one resting on model and role, and a consumer reading only
341
+ * the scalar `match` cannot tell them apart.
342
+ */
343
+ comparedAxes: z
344
+ .array(observationValue())
345
+ .max(COMPARED_OBSERVATION_AXES.length)
346
+ .optional(),
347
+ })
348
+ .strict(),
349
+ ]);
350
+ /** The literal an axis carries when the provider exposes no selectable value. */
351
+ export const NOT_EXPOSED_OBSERVATION_VALUE = 'not-exposed';
352
+ /**
353
+ * Project the record's immutable configured selection axes for comparison.
354
+ *
355
+ * This is a read-only projection: it never mutates the record, and its output
356
+ * is only ever compared against, never copied into, an observation.
357
+ */
358
+ export function configuredInvocationForObservation(record) {
359
+ const roles = [record.role_name, record.role_selector].filter((value) => typeof value === 'string' && value !== '');
360
+ return {
361
+ role: [...new Set(roles)],
362
+ model: record.model_selector,
363
+ effort: record.effort_selector,
364
+ serviceTier: record.service_tier_selector ?? null,
365
+ };
366
+ }
367
+ /**
368
+ * Values that name the absence of an axis rather than a runtime fact. They can
369
+ * never establish agreement or disagreement, so they are excluded from the
370
+ * comparable set instead of being compared as literal strings.
371
+ */
372
+ const NON_COMPARABLE_OBSERVED_VALUES = new Set([
373
+ NOT_EXPOSED_OBSERVATION_VALUE,
374
+ 'not-reported',
375
+ 'unknown',
376
+ ]);
377
+ function comparableValue(value) {
378
+ if (typeof value !== 'string')
379
+ return null;
380
+ const trimmed = value.trim();
381
+ if (trimmed === '')
382
+ return null;
383
+ return NON_COMPARABLE_OBSERVED_VALUES.has(trimmed.toLowerCase())
384
+ ? null
385
+ : trimmed.toLowerCase();
386
+ }
387
+ function comparableValues(axis) {
388
+ const values = typeof axis === 'string' ? [axis] : (axis ?? []);
389
+ return values
390
+ .map(comparableValue)
391
+ .filter((value) => value !== null);
392
+ }
393
+ /**
394
+ * Compare observed metadata with the configured invocation.
395
+ *
396
+ * Only axes both sides report are compared. With no comparable axis the result
397
+ * is `not-comparable` rather than `matching`: silence is never agreement. A
398
+ * mismatch is evidence only — it never authorizes replacement, retry, or
399
+ * fallback.
400
+ */
401
+ export function comparedObservationAxes(metadata, configured) {
402
+ return COMPARED_OBSERVATION_AXES.filter((axis) => comparableValue(metadata[axis]) !== null &&
403
+ comparableValues(configured?.[axis]).length > 0);
404
+ }
405
+ export function compareObservedRuntimeMetadata(metadata, configured) {
406
+ const compared = comparedObservationAxes(metadata, configured);
407
+ if (compared.length === 0)
408
+ return 'not-comparable';
409
+ const mismatched = compared.some((axis) => {
410
+ const observed = comparableValue(metadata[axis]);
411
+ const expected = comparableValues(configured?.[axis]);
412
+ return observed === null || !expected.includes(observed);
413
+ });
414
+ return mismatched ? 'mismatching' : 'matching';
415
+ }
416
+ /**
417
+ * Build one source-qualified runtime observation.
418
+ *
419
+ * Absent, empty, or schema-invalid metadata yields `not-reported`. The
420
+ * configured invocation is only ever read for the comparison, so a parse
421
+ * failure cannot copy a requested value into observed state.
422
+ */
423
+ export function buildRuntimeObservation(input) {
424
+ const metadata = input.metadata;
425
+ if (metadata === null)
426
+ return { status: 'not-reported' };
427
+ assertNoSensitiveDispatchContent(metadata, '<observation>');
428
+ const reported = {};
429
+ for (const axis of ['childLineage', ...COMPARED_OBSERVATION_AXES]) {
430
+ const value = metadata[axis];
431
+ if (typeof value === 'string' && value.trim() !== '') {
432
+ reported[axis] = value;
433
+ }
434
+ }
435
+ if (Object.keys(reported).length === 0)
436
+ return { status: 'not-reported' };
437
+ const candidate = {
438
+ status: 'reported',
439
+ provider: input.provider,
440
+ ...reported,
441
+ source: input.source,
442
+ observedAt: input.observedAt,
443
+ match: compareObservedRuntimeMetadata(metadata, input.configured),
444
+ comparedAxes: comparedObservationAxes(metadata, input.configured),
445
+ };
446
+ const parsed = runtimeObservationSchema.safeParse(candidate);
447
+ return parsed.success ? parsed.data : { status: 'not-reported' };
448
+ }
449
+ /**
450
+ * The rejected trigger owns the single-fallback right. Publishing a fallback
451
+ * record requires this durable claim on the trigger first, so two concurrent
452
+ * callers cannot each observe "no fallback yet" and then create their own.
453
+ */
454
+ const fallbackClaimSchema = z
455
+ .object({
456
+ fallbackRequestId: z.string().min(1),
457
+ claimedAt: z.string().datetime(),
458
+ })
459
+ .strict();
460
+ const oatRecordSchema = z
461
+ .object({
462
+ schemaVersion: z.literal(1),
463
+ canonicalRole: canonicalRoleSchema.nullable(),
464
+ preStartRejection: rejectionSchema.nullable(),
465
+ fallbackClaim: fallbackClaimSchema.nullable().default(null),
466
+ fallback: fallbackSchema,
467
+ runtimeObservation: runtimeObservationSchema,
468
+ })
469
+ .strict();
470
+ export const persistedOatDispatchRecordSchema = genericDispatchRecordSchema
471
+ .innerType()
472
+ .extend({ oat: oatRecordSchema })
473
+ .strict();
474
+ /**
475
+ * Strict observation parse. Used where a caller supplied the observation
476
+ * directly, so a malformed value is refused rather than quietly degraded to
477
+ * `not-reported` — absent evidence and a bad claim are different facts.
478
+ */
479
+ export function parseRuntimeObservation(value) {
480
+ return runtimeObservationSchema.parse(value);
481
+ }
482
+ const oatDispatchEvidenceEventSchema = z.discriminatedUnion('kind', [
483
+ z
484
+ .object({
485
+ kind: z.literal('canonical-role-resolution'),
486
+ requestId: z.string().min(1),
487
+ source: z.literal('canonical-role-resolver'),
488
+ evidence: canonicalRoleSchema,
489
+ })
490
+ .strict(),
491
+ z
492
+ .object({
493
+ kind: z.literal('pre-start-rejection-attestation'),
494
+ requestId: z.string().min(1),
495
+ source: z.literal('provider-wrapper'),
496
+ expectedLaunchStatus: z.literal('blocked-before-start'),
497
+ rejection: rejectionSchema,
498
+ })
499
+ .strict(),
500
+ z
501
+ .object({
502
+ kind: z.literal('fallback-claim'),
503
+ requestId: z.string().min(1),
504
+ source: z.literal('provider-wrapper'),
505
+ claim: fallbackClaimSchema,
506
+ })
507
+ .strict(),
508
+ z
509
+ .object({
510
+ kind: z.literal('fallback-link'),
511
+ requestId: z.string().min(1),
512
+ source: z.literal('provider-wrapper'),
513
+ evidence: fallbackDispatchSchema,
514
+ })
515
+ .strict(),
516
+ z
517
+ .object({
518
+ kind: z.literal('runtime-observation'),
519
+ requestId: z.string().min(1),
520
+ source: z.literal('runtime-observer'),
521
+ observation: runtimeObservationSchema,
522
+ })
523
+ .strict(),
524
+ ]);
525
+ function initialOatRecord() {
526
+ return {
527
+ schemaVersion: 1,
528
+ canonicalRole: null,
529
+ preStartRejection: null,
530
+ fallbackClaim: null,
531
+ fallback: { status: 'not-applicable', reason: 'No fallback recorded.' },
532
+ runtimeObservation: { status: 'not-reported' },
533
+ };
534
+ }
535
+ function genericPart(record) {
536
+ const { oat: _oat, ...generic } = record;
537
+ return parseGenericDispatchRecord(generic);
538
+ }
539
+ function oatPart(record) {
540
+ return 'oat' in record
541
+ ? oatRecordSchema.parse(record.oat)
542
+ : initialOatRecord();
543
+ }
544
+ function targetFor(record) {
545
+ return {
546
+ provider: record.provider,
547
+ modelSelector: record.model_selector,
548
+ effortSelector: record.effort_selector,
549
+ reasoningModeSelector: record.reasoning_mode_selector ?? null,
550
+ serviceTierSelector: record.service_tier_selector ?? null,
551
+ selectedRoute: record.selected_route,
552
+ };
553
+ }
554
+ function assertMatchingRequest(record, requestId) {
555
+ if (record.request_id !== requestId) {
556
+ throw new Error('OAT event request ID must match the generic record.');
557
+ }
558
+ }
559
+ export function parsePersistedOatDispatchRecord(value) {
560
+ assertNoSensitiveDispatchContent(value);
561
+ assertBoundedDispatchRecordSize(value);
562
+ const parsed = persistedOatDispatchRecordSchema.parse(value);
563
+ const { oat, ...generic } = parsed;
564
+ const sanitized = {
565
+ ...parseGenericDispatchRecord(generic),
566
+ // The namespaced augmentation is nested evidence throughout — rejection
567
+ // reasons, fallback narration, observation values — so it is redacted
568
+ // rather than rejected. The canonical `<tier>/agents/<role>.md` role form
569
+ // is not an absolute path and survives untouched.
570
+ oat: oatRecordSchema.parse(redactAbsolutePathsDeep(oat)),
571
+ };
572
+ // Same ordering rule as the generic record: the size check above ran on the
573
+ // untransformed input, so it is re-run on the redacted result.
574
+ assertBoundedDispatchRecordSize(sanitized);
575
+ return sanitized;
576
+ }
577
+ export function augmentDispatchRecord(input) {
578
+ assertNoSensitiveDispatchContent(input.event);
579
+ const event = oatDispatchEvidenceEventSchema.parse(input.event);
580
+ const record = genericPart(input.record);
581
+ const oat = oatPart(input.record);
582
+ assertMatchingRequest(record, event.requestId);
583
+ switch (event.kind) {
584
+ case 'canonical-role-resolution': {
585
+ if (oat.canonicalRole !== null &&
586
+ JSON.stringify(oat.canonicalRole) !== JSON.stringify(event.evidence)) {
587
+ throw new Error('Canonical role evidence is immutable once recorded.');
588
+ }
589
+ oat.canonicalRole = canonicalRoleSchema.parse(event.evidence);
590
+ break;
591
+ }
592
+ case 'pre-start-rejection-attestation': {
593
+ if (event.expectedLaunchStatus !== 'blocked-before-start' ||
594
+ record.launch_status !== 'blocked-before-start') {
595
+ throw new Error('Pre-start rejection requires generic launch_status blocked-before-start.');
596
+ }
597
+ if (oat.preStartRejection !== null &&
598
+ JSON.stringify(oat.preStartRejection) !==
599
+ JSON.stringify(event.rejection)) {
600
+ throw new Error('Pre-start rejection evidence is immutable once recorded.');
601
+ }
602
+ oat.preStartRejection = rejectionSchema.parse(event.rejection);
603
+ break;
604
+ }
605
+ case 'fallback-claim': {
606
+ const claim = event.claim;
607
+ if (record.launch_status !== 'blocked-before-start') {
608
+ throw new Error('Only a request blocked before start can claim a fallback.');
609
+ }
610
+ if (oat.preStartRejection === null) {
611
+ throw new Error('A fallback claim requires proven pre-start rejection evidence.');
612
+ }
613
+ if (oat.canonicalRole === null ||
614
+ oat.canonicalRole.status !== 'resolved') {
615
+ throw new Error('A fallback claim requires resolved canonical role evidence.');
616
+ }
617
+ if (oat.fallbackClaim !== null &&
618
+ oat.fallbackClaim.fallbackRequestId !== claim.fallbackRequestId) {
619
+ throw new Error('The rejected request already has a fallback.');
620
+ }
621
+ oat.fallbackClaim = claim;
622
+ break;
623
+ }
624
+ case 'fallback-link': {
625
+ const trigger = input.triggerRecord
626
+ ? parsePersistedOatDispatchRecord(input.triggerRecord)
627
+ : null;
628
+ if (!trigger) {
629
+ throw new Error('Fallback requires the rejected trigger record.');
630
+ }
631
+ const evidence = fallbackDispatchSchema.parse(event.evidence);
632
+ if (evidence.triggerRequestId !== trigger.request_id ||
633
+ evidence.fallbackRequestId !== record.request_id ||
634
+ trigger.launch_status !== 'blocked-before-start' ||
635
+ trigger.oat.preStartRejection === null ||
636
+ evidence.rejection.source !== 'provider-wrapper' ||
637
+ evidence.rejection.code !== trigger.oat.preStartRejection.code ||
638
+ evidence.rejection.rejectedAt !==
639
+ trigger.oat.preStartRejection.rejectedAt ||
640
+ evidence.rejection.provesNoChildStarted !== true) {
641
+ throw new Error('Fallback lacks matching pre-start rejection evidence.');
642
+ }
643
+ if (record.selection_reason !== 'pre-start-rejection') {
644
+ throw new Error('A fallback record must state selection_reason pre-start-rejection.');
645
+ }
646
+ if (canonicalEvidenceJson(evidence.preservedTarget) !==
647
+ canonicalEvidenceJson(targetFor(record)) ||
648
+ canonicalEvidenceJson(targetFor(trigger)) !==
649
+ canonicalEvidenceJson(targetFor(record))) {
650
+ throw new Error('Fallback must preserve the exact target and controls.');
651
+ }
652
+ const changedControls = differingControlFields(trigger, record);
653
+ if (changedControls.length > 0) {
654
+ throw new Error(`Fallback must preserve the exact target and controls; ${changedControls.join(', ')} changed.`);
655
+ }
656
+ if (configuredControlDigest(trigger) !== configuredControlDigest(record)) {
657
+ throw new Error('Fallback must preserve the exact target and controls; the configured control digest changed.');
658
+ }
659
+ if (trigger.oat.fallbackClaim === null ||
660
+ trigger.oat.fallbackClaim.fallbackRequestId !== record.request_id) {
661
+ throw new Error('The rejected trigger must durably claim this fallback request before the fallback can publish.');
662
+ }
663
+ const triggerRole = trigger.oat.canonicalRole;
664
+ if (triggerRole === null || triggerRole.status !== 'resolved') {
665
+ throw new Error('Fallback requires resolved canonical role evidence on the rejected trigger.');
666
+ }
667
+ if (canonicalEvidenceJson(evidence.roleInstructions) !==
668
+ canonicalEvidenceJson(triggerRole)) {
669
+ throw new Error("Fallback role evidence must equal the trigger's resolved canonical role evidence exactly.");
670
+ }
671
+ if ((input.relatedRecords ?? [])
672
+ .map(parsePersistedOatDispatchRecord)
673
+ .some((related) => related.oat.fallback.status === 'fallback-dispatch' &&
674
+ related.oat.fallback.triggerRequestId === trigger.request_id) ||
675
+ oat.fallback.status === 'fallback-dispatch') {
676
+ throw new Error('The rejected request already has a fallback.');
677
+ }
678
+ oat.fallback = evidence;
679
+ break;
680
+ }
681
+ case 'runtime-observation':
682
+ if (oat.runtimeObservation.status === 'reported' &&
683
+ JSON.stringify(oat.runtimeObservation) !==
684
+ JSON.stringify(event.observation)) {
685
+ throw new Error('Runtime observation is immutable once reported.');
686
+ }
687
+ oat.runtimeObservation = runtimeObservationSchema.parse(event.observation);
688
+ break;
689
+ }
690
+ return parsePersistedOatDispatchRecord({ ...record, oat });
691
+ }