@open-agent-toolkit/cli 0.2.51 → 0.2.53

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 (218) 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 +66 -28
  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 +149 -37
  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 +108 -43
  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/index.d.ts +1 -1
  155. package/dist/engine/index.d.ts.map +1 -1
  156. package/dist/engine/index.js +1 -1
  157. package/dist/engine/provider-path-safety.d.ts +1 -0
  158. package/dist/engine/provider-path-safety.d.ts.map +1 -1
  159. package/dist/engine/provider-path-safety.js +15 -0
  160. package/dist/engine/scanner.d.ts +9 -2
  161. package/dist/engine/scanner.d.ts.map +1 -1
  162. package/dist/engine/scanner.js +58 -4
  163. package/dist/fs/io.d.ts +67 -0
  164. package/dist/fs/io.d.ts.map +1 -1
  165. package/dist/fs/io.js +536 -3
  166. package/dist/manifest/index.d.ts +2 -2
  167. package/dist/manifest/index.d.ts.map +1 -1
  168. package/dist/manifest/index.js +1 -1
  169. package/dist/manifest/manager.d.ts +2 -2
  170. package/dist/manifest/manager.d.ts.map +1 -1
  171. package/dist/manifest/manager.js +20 -3
  172. package/dist/manifest/manifest.types.d.ts +704 -13
  173. package/dist/manifest/manifest.types.d.ts.map +1 -1
  174. package/dist/manifest/manifest.types.js +131 -15
  175. package/dist/providers/codex/codec/sync-extension.d.ts +1 -1
  176. package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
  177. package/dist/providers/codex/codec/sync-extension.js +48 -17
  178. package/dist/providers/cursor/codec/sync-extension.d.ts +1 -1
  179. package/dist/providers/cursor/codec/sync-extension.d.ts.map +1 -1
  180. package/dist/providers/cursor/codec/sync-extension.js +52 -23
  181. package/dist/providers/identity/absolute-paths.d.ts +68 -0
  182. package/dist/providers/identity/absolute-paths.d.ts.map +1 -0
  183. package/dist/providers/identity/absolute-paths.js +144 -0
  184. package/dist/providers/identity/claude-runtime-observation.d.ts +105 -0
  185. package/dist/providers/identity/claude-runtime-observation.d.ts.map +1 -0
  186. package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts +40 -0
  187. package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts.map +1 -0
  188. package/dist/providers/identity/claude-runtime-observation.fixtures.js +146 -0
  189. package/dist/providers/identity/claude-runtime-observation.js +229 -0
  190. package/dist/providers/identity/codex-runtime-observation.d.ts +75 -0
  191. package/dist/providers/identity/codex-runtime-observation.d.ts.map +1 -0
  192. package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts +37 -0
  193. package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts.map +1 -0
  194. package/dist/providers/identity/codex-runtime-observation.fixtures.js +539 -0
  195. package/dist/providers/identity/codex-runtime-observation.js +255 -0
  196. package/dist/providers/identity/generic-dispatch-record.d.ts +313 -0
  197. package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -0
  198. package/dist/providers/identity/generic-dispatch-record.js +539 -0
  199. package/dist/providers/identity/oat-dispatch-record.d.ts +1964 -0
  200. package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -0
  201. package/dist/providers/identity/oat-dispatch-record.js +691 -0
  202. package/dist/providers/identity/runtime-observation.d.ts +100 -0
  203. package/dist/providers/identity/runtime-observation.d.ts.map +1 -0
  204. package/dist/providers/identity/runtime-observation.js +169 -0
  205. package/dist/providers/shared/index.d.ts +2 -0
  206. package/dist/providers/shared/index.d.ts.map +1 -1
  207. package/dist/providers/shared/index.js +1 -0
  208. package/dist/providers/shared/materialization-extension.d.ts +12 -0
  209. package/dist/providers/shared/materialization-extension.d.ts.map +1 -1
  210. package/dist/providers/shared/registry.d.ts +69 -0
  211. package/dist/providers/shared/registry.d.ts.map +1 -0
  212. package/dist/providers/shared/registry.js +252 -0
  213. package/dist/providers/shared/restart-adviser.d.ts +26 -0
  214. package/dist/providers/shared/restart-adviser.d.ts.map +1 -0
  215. package/dist/providers/shared/restart-adviser.js +96 -0
  216. package/dist/shared/types.d.ts +4 -0
  217. package/dist/shared/types.d.ts.map +1 -1
  218. package/package.json +2 -2
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Absolute-path detection for dispatch records and messages.
3
+ *
4
+ * Two patterns, deliberately different. `IDENTITY_PATH_PATTERN` is strict and
5
+ * used where a path is never a legitimate value, so ambiguity resolves to
6
+ * rejection. `PROSE_PATH_PATTERN` is conservative and used where a false
7
+ * positive would corrupt legitimate evidence, so it under-matches by design:
8
+ * colon-prefixed forms, path-shaped routes inside URLs, and trailing-slash
9
+ * candidates survive. Prose redaction is therefore best-effort, not a
10
+ * guarantee — see `spec.md` NFR1, amended 2026-09-03.
11
+ *
12
+ * Before this module there were two independent notions of
13
+ * "looks like a path": a regex inside `redactDispatchMessage`, which only
14
+ * scrubbed error text, and nothing at all on the persistence boundary. The
15
+ * durable journal is committed to a shared repository, so a username or local
16
+ * layout that reaches it is in git history permanently.
17
+ *
18
+ * Message redaction and record sanitization now share this detector, because
19
+ * the gap between them is precisely what allowed paths to be scrubbed from a
20
+ * message and written verbatim into the record in the same command.
21
+ */
22
+ /**
23
+ * Two detectors, because one regex cannot serve both jobs.
24
+ *
25
+ * `/dashboard` is a URL route, `/foo/bar/` is a regex literal, and
26
+ * `path:/Users/alice` is a real disclosure. Three rounds of tuning a single
27
+ * delimiter-sensitive pattern traded a leak for a mangled value each time, so
28
+ * the detectors are now split by what the field can legitimately contain.
29
+ *
30
+ * IDENTITY fields (caller, scope, selectors, routes, guidance references,
31
+ * authority, catalog snapshot, candidates, fallback) admit no URL, no regex and
32
+ * no path, so ambiguity there is resolved by rejecting. Its lookbehind includes
33
+ * `:` so `cwd:/Users/alice` is caught.
34
+ *
35
+ * PROSE fields carry human text and quoted evidence, where a URL or a regex is
36
+ * legitimate content. Redaction there is deliberately conservative and
37
+ * best-effort — see {@link redactAbsolutePaths}.
38
+ */
39
+ const IDENTITY_PATH_PATTERN = /(?<=^|[\s'"`([{<=,;|:])(?:file:\/\/[^\s'"`;,)\]}]*|\\\\[^\s'"`;,)\]}]+|[A-Za-z]:[\\/][^\s'"`;,)\]}]*|\/(?!\/)[^\s'"`;,)\]}]+)/g;
40
+ /** As above, minus `:`, which is what makes URLs and regexes distinguishable. */
41
+ const PROSE_PATH_PATTERN = /(?<=^|[\s'"`([{<=,;|])(?:file:\/\/[^\s'"`;,)\]}]*|\\\\[^\s'"`;,)\]}]+|[A-Za-z]:[\\/][^\s'"`;,)\]}]*|\/(?!\/)[^\s'"`;,)\]}]+)/g;
42
+ /** Spans of an http(s) URL, whose internal slashes are routes, not paths. */
43
+ const HTTP_URL_PATTERN = /https?:\/\/[^\s'"`)\]}]*/g;
44
+ /** The stable stand-in, matching the `<tier>/agents/<role>.md` precedent. */
45
+ export const REDACTED_PATH = '<redacted-path>';
46
+ function fresh(pattern) {
47
+ return new RegExp(pattern.source, 'g');
48
+ }
49
+ /**
50
+ * Identity-field detector. Rejects on ambiguity by design.
51
+ */
52
+ export function containsAbsolutePath(value) {
53
+ return fresh(IDENTITY_PATH_PATTERN).test(value);
54
+ }
55
+ function httpUrlSpans(value) {
56
+ const spans = [];
57
+ const pattern = fresh(HTTP_URL_PATTERN);
58
+ let match;
59
+ while ((match = pattern.exec(value)) !== null) {
60
+ spans.push([match.index, match.index + match[0].length]);
61
+ }
62
+ return spans;
63
+ }
64
+ /**
65
+ * Best-effort prose redaction. NOT a guarantee.
66
+ *
67
+ * Known limits, stated so they are visible rather than assumed away:
68
+ * - A colon-prefixed path (`cwd:/Users/alice`) survives, because `:` cannot be
69
+ * treated as a path delimiter without mangling every URL.
70
+ * - A candidate inside an http(s) URL is left alone, so a path-shaped route
71
+ * such as `?next=/dashboard` survives.
72
+ * - A candidate ending in `/` is left alone, because `/foo/bar/` is far more
73
+ * often a regex literal than a directory in prose.
74
+ * - Any path shape the pattern does not match survives.
75
+ *
76
+ * The bias is deliberate. A path leaked into prose is a disclosure; a mangled
77
+ * URL or regex is corrupted evidence, and corrupted evidence is worse in a
78
+ * provenance record. Identity and control fields carry the enforceable half of
79
+ * the guarantee via {@link containsAbsolutePath}.
80
+ */
81
+ export function redactAbsolutePaths(value) {
82
+ const spans = httpUrlSpans(value);
83
+ return value.replace(fresh(PROSE_PATH_PATTERN), (match, offset) => {
84
+ if (match.endsWith('/'))
85
+ return match;
86
+ const inUrl = spans.some(([from, to]) => offset >= from && offset < to);
87
+ return inUrl ? match : REDACTED_PATH;
88
+ });
89
+ }
90
+ /**
91
+ * Recursively redact every string in a JSON value. Used for prose and nested
92
+ * evidence containers, where a path is plausible incidental content rather than
93
+ * a malformed identifier.
94
+ */
95
+ export function redactAbsolutePathsDeep(value) {
96
+ if (typeof value === 'string')
97
+ return redactAbsolutePaths(value);
98
+ if (Array.isArray(value)) {
99
+ return value.map((entry) => redactAbsolutePathsDeep(entry));
100
+ }
101
+ if (value !== null && typeof value === 'object') {
102
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
103
+ key,
104
+ redactAbsolutePathsDeep(entry),
105
+ ]));
106
+ }
107
+ return value;
108
+ }
109
+ /**
110
+ * Recursively reject any absolute path. Used for identity and control fields,
111
+ * where a path is never a legitimate value and silently rewriting one would
112
+ * corrupt the identifier it claims to be.
113
+ */
114
+ export function assertNoAbsolutePath(value, path) {
115
+ if (typeof value === 'string') {
116
+ if (containsAbsolutePath(value)) {
117
+ throw new Error(`A dispatch record must not carry an absolute filesystem path at ${path}.`);
118
+ }
119
+ return;
120
+ }
121
+ if (Array.isArray(value)) {
122
+ value.forEach((entry, index) => assertNoAbsolutePath(entry, `${path}[${index}]`));
123
+ return;
124
+ }
125
+ if (value === null || typeof value !== 'object')
126
+ return;
127
+ for (const [key, entry] of Object.entries(value)) {
128
+ assertNoAbsolutePath(entry, `${path}.${key}`);
129
+ }
130
+ }
131
+ /**
132
+ * The publication postcondition, scoped to what is actually guaranteed.
133
+ *
134
+ * It hard-fails on a path in an identity or control field, which is the half of
135
+ * NFR1 that is enforceable. It deliberately does **not** claim that no absolute
136
+ * path remains anywhere in the revision: prose redaction is best-effort, and a
137
+ * postcondition that asserted more than the sanitizer can deliver would be a
138
+ * false guarantee rather than a check.
139
+ */
140
+ export function assertJournalIdentityHasNoAbsolutePath(identityFields) {
141
+ for (const [field, value] of Object.entries(identityFields)) {
142
+ assertNoAbsolutePath(value, `<journal>.${field}`);
143
+ }
144
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Claude runtime metadata parsing.
3
+ *
4
+ * Field paths here are taken from real on-disk transcripts under
5
+ * `~/.claude/projects/`, not from assumption. Every corpus figure in this file
6
+ * is an operator-environment observation as of 2026-09-02 on the capturing
7
+ * operator's machine, not a provider guarantee: a later mismatch means the
8
+ * corpus moved, not that the claim was wrong.
9
+ *
10
+ * An earlier revision read only `system`/`init` and `result` records; a scan of
11
+ * all 2,725 local transcripts found `"subtype":"init"` in zero of them, so that parser returned
12
+ * `not-reported` against every real transcript while its hand-written fixtures
13
+ * agreed with it. The on-disk `assistant` entry is now the primary shape.
14
+ *
15
+ * Reading an `assistant` entry does not mean reading a conversation. The parser
16
+ * takes allowlisted entry-level metadata plus exactly two explicit key paths
17
+ * inside `message` — `message.model` and `message.usage.service_tier`.
18
+ * `message` is never spread, enumerated, or filtered after the fact, so
19
+ * `message.content` is not merely dropped later; it is never reached. The
20
+ * content-trap test pins this: a `message` whose `content` throws on access
21
+ * still parses cleanly.
22
+ *
23
+ * Everything this module produces is per-run, non-authoritative corroboration.
24
+ * A failed or partial parse yields `not-reported` and never falls back to the
25
+ * configured invocation.
26
+ */
27
+ /**
28
+ * The complete entry-level read surface for an `assistant` entry. `message` is
29
+ * handled separately by explicit path and is deliberately absent from this
30
+ * list, so this plus `message.model` and `message.usage.service_tier` is
31
+ * everything the parser touches. Exported so a test can pin it: a future widening
32
+ * has to change this list, in view, rather than slip in at a call site.
33
+ *
34
+ * `attributionAgent` is the role identifier and is the same class of signal
35
+ * Codex carries as `agent_role`: a bounded enum-like name, not conversation
36
+ * content. Across 2,725 captured transcripts it took 8 distinct values, all
37
+ * of which pass the shared identifier validator, with a longest value of 21
38
+ * characters against the 256 bound. It appears on subagent turns only, so a
39
+ * main session reports no role rather than a synthesized one.
40
+ */
41
+ export declare const CLAUDE_ASSISTANT_KEYS: readonly ['type', 'isSidechain', 'effort', 'attributionAgent', 'sessionId', 'session_id', 'requestId'];
42
+ /**
43
+ * Stream-json entry keys, for `claude -p --output-format stream-json`.
44
+ *
45
+ * UNVERIFIED: no captured artifact of that format was available, and zero of
46
+ * the 2,725 observed local transcripts contained a `system`/`init` record. This path is
47
+ * retained as a secondary shape and must not be presented as equally grounded
48
+ * with the on-disk shape above.
49
+ */
50
+ export declare const CLAUDE_INIT_KEYS: readonly ['type', 'subtype', 'model', 'service_tier', 'serviceTier', 'agent', 'subagent_type', 'reasoning_effort', 'effort', 'request_id'];
51
+ export declare const CLAUDE_OBSERVATION_SOURCE = "claude-session-metadata";
52
+ export interface ClaudeRuntimeMetadata {
53
+ /**
54
+ * `root` for a main session, `depth-unknown` for a subagent turn.
55
+ *
56
+ * `isSidechain` is the only lineage signal Claude emits and it is binary. No
57
+ * depth, nesting, level, or ancestry key appeared on any of the 141,078
58
+ * observed assistant entries, and `parentUuid` never crosses an `agentId`
59
+ * boundary, so it chains messages within one agent rather than agents to
60
+ * each other. A depth number is therefore not derivable and is not invented.
61
+ */
62
+ childLineage: string | null;
63
+ /** Observed role, from `attributionAgent`; `null` when the run reports none. */
64
+ role: string | null;
65
+ model: string | null;
66
+ effort: string | null;
67
+ serviceTier: string | null;
68
+ /**
69
+ * OAT request correlation declared by the provider, for parity with the Codex
70
+ * parser. Claude declares none, so this is `null` on a real transcript.
71
+ * Claude's own `requestId` is a per-turn API identifier and its `sessionId` a
72
+ * transcript identifier; neither is an OAT dispatch request id, so neither is
73
+ * equality-checked against one.
74
+ */
75
+ requestId: string | null;
76
+ /** Provider session identifier, used to prove the envelope is one session. */
77
+ sessionId: string | null;
78
+ }
79
+ /**
80
+ * Extract the child's metadata from Claude transcript entries.
81
+ *
82
+ * The applicable turn is the last one: it is the most recent state the session
83
+ * reported. Earlier turns fill only the fields the last one left unreported.
84
+ * Returns `null` when no Claude metadata is present, or when the entries mix
85
+ * sessions — every captured transcript carries exactly one `sessionId`, so more
86
+ * than one means no single applicable child exists.
87
+ */
88
+ export declare function extractClaudeRuntimeMetadata(entries: readonly unknown[]): ClaudeRuntimeMetadata | null;
89
+ /**
90
+ * Neutral observation facts for the cross-provider projection.
91
+ *
92
+ * The single-session guarantee runs in-process inside
93
+ * {@link extractClaudeRuntimeMetadata}, so `sessionId` never needs to survive
94
+ * into the projected output — which matters, because `sessionId` classifies
95
+ * sensitive and previously made every real transcript unrecordable.
96
+ */
97
+ export declare function observeClaudeRuntimeFacts(entries: readonly unknown[]): {
98
+ lineage: string | null;
99
+ role: string | null;
100
+ model: string | null;
101
+ effort: string | null;
102
+ serviceTier: string | null;
103
+ correlation: string | null;
104
+ } | null;
105
+ //# sourceMappingURL=claude-runtime-observation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-runtime-observation.d.ts","sourceRoot":"","sources":["../../../src/providers/identity/claude-runtime-observation.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,YAChC,MAAM,EACN,aAAa,EACb,QAAQ,EACR,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,WAAW,CACH,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,YAC3B,MAAM,EACN,SAAS,EACT,OAAO,EACP,cAAc,EACd,aAAa,EACb,OAAO,EACP,eAAe,EACf,kBAAkB,EAClB,QAAQ,EACR,YAAY,CACJ,CAAC;AAEX,eAAO,MAAM,yBAAyB,4BAA4B,CAAC;AAEnE,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;OAQG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gFAAgF;IAChF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AA+FD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,SAAS,OAAO,EAAE,GAC1B,qBAAqB,GAAG,IAAI,CAmD9B;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG;IACtE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,GAAG,IAAI,CAWP"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Captured Claude transcript metadata, sanitized.
3
+ *
4
+ * Derived from real on-disk transcripts under `~/.claude/projects/`, which is
5
+ * the format Claude actually writes. Corpus counts below are
6
+ * operator-environment observations (as of 2026-09-02, on the capturing operator's machine). They replaced hand-written fixtures built
7
+ * around `system`/`init` and `result` records: a scan of all 2,725 local
8
+ * transcripts found `"subtype":"init"` in zero of them, so the previous parser
9
+ * returned `not-reported` against every real transcript while its own fixtures
10
+ * agreed with it.
11
+ *
12
+ * Sanitization is allowlist-based. Retained: the entry-level metadata keys the
13
+ * parser reads plus a few it does not (`version`, `userType`, `uuid`), so the
14
+ * fixtures still demonstrate that unread keys are dropped. `message` is reduced
15
+ * to the two explicit key paths the parser reaches — `message.model` and
16
+ * `message.usage.service_tier`. Dropped outright: `message.content` (the
17
+ * conversation), `cwd`, `gitBranch`, `slug`, `agentId`, and the remaining
18
+ * `attribution*` fields.
19
+ *
20
+ * Structural facts these fixtures preserve, each load-bearing:
21
+ * - `effort` is a real top-level field on an assistant entry. Claude does
22
+ * expose a selectable effort axis, so reporting `not-exposed` for it was
23
+ * wrong; observed values across 124,804 entries are `high`, `xhigh`,
24
+ * `medium`, and `max`.
25
+ * - `attributionAgent` is the role identifier, and it is exactly the signal
26
+ * Codex carries as `agent_role`. It appears on subagent turns only: present
27
+ * on 114,600 of 114,657 sidechain entries and on none of the 26,497
28
+ * main-session entries. `SIDECHAIN_TRANSCRIPT` therefore carries it and
29
+ * `MAIN_SESSION_TRANSCRIPT` does not, which is the real distinction rather
30
+ * than a constructed one.
31
+ * - `isSidechain` distinguishes a main session from a subagent turn. It is the
32
+ * only lineage signal present, and it is binary. Role names are not a depth
33
+ * signal and are never read as one.
34
+ * - `sessionId` is constant within a transcript; every one of the 2,655 local
35
+ * transcripts that carries one carries exactly one.
36
+ * - Model and service tier live only under `message`, never at the top level.
37
+ */
38
+ export declare const MAIN_SESSION_TRANSCRIPT: readonly unknown[];
39
+ export declare const SIDECHAIN_TRANSCRIPT: readonly unknown[];
40
+ //# sourceMappingURL=claude-runtime-observation.fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-runtime-observation.fixtures.d.ts","sourceRoot":"","sources":["../../../src/providers/identity/claude-runtime-observation.fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,eAAO,MAAM,uBAAuB,EAAE,SAAS,OAAO,EAoDrD,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,SAAS,OAAO,EAuDlD,CAAC"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Captured Claude transcript metadata, sanitized.
3
+ *
4
+ * Derived from real on-disk transcripts under `~/.claude/projects/`, which is
5
+ * the format Claude actually writes. Corpus counts below are
6
+ * operator-environment observations (as of 2026-09-02, on the capturing operator's machine). They replaced hand-written fixtures built
7
+ * around `system`/`init` and `result` records: a scan of all 2,725 local
8
+ * transcripts found `"subtype":"init"` in zero of them, so the previous parser
9
+ * returned `not-reported` against every real transcript while its own fixtures
10
+ * agreed with it.
11
+ *
12
+ * Sanitization is allowlist-based. Retained: the entry-level metadata keys the
13
+ * parser reads plus a few it does not (`version`, `userType`, `uuid`), so the
14
+ * fixtures still demonstrate that unread keys are dropped. `message` is reduced
15
+ * to the two explicit key paths the parser reaches — `message.model` and
16
+ * `message.usage.service_tier`. Dropped outright: `message.content` (the
17
+ * conversation), `cwd`, `gitBranch`, `slug`, `agentId`, and the remaining
18
+ * `attribution*` fields.
19
+ *
20
+ * Structural facts these fixtures preserve, each load-bearing:
21
+ * - `effort` is a real top-level field on an assistant entry. Claude does
22
+ * expose a selectable effort axis, so reporting `not-exposed` for it was
23
+ * wrong; observed values across 124,804 entries are `high`, `xhigh`,
24
+ * `medium`, and `max`.
25
+ * - `attributionAgent` is the role identifier, and it is exactly the signal
26
+ * Codex carries as `agent_role`. It appears on subagent turns only: present
27
+ * on 114,600 of 114,657 sidechain entries and on none of the 26,497
28
+ * main-session entries. `SIDECHAIN_TRANSCRIPT` therefore carries it and
29
+ * `MAIN_SESSION_TRANSCRIPT` does not, which is the real distinction rather
30
+ * than a constructed one.
31
+ * - `isSidechain` distinguishes a main session from a subagent turn. It is the
32
+ * only lineage signal present, and it is binary. Role names are not a depth
33
+ * signal and are never read as one.
34
+ * - `sessionId` is constant within a transcript; every one of the 2,655 local
35
+ * transcripts that carries one carries exactly one.
36
+ * - Model and service tier live only under `message`, never at the top level.
37
+ */
38
+ export const MAIN_SESSION_TRANSCRIPT = [
39
+ {
40
+ type: 'assistant',
41
+ isSidechain: false,
42
+ effort: 'high',
43
+ sessionId: '7f9d5ab4-3b08-4a21-adb5-405c04af2d89',
44
+ requestId: 'req_011Ce9GB4DvzreK2hzChvmgy',
45
+ uuid: '14c99a81-02ff-4153-8104-3e4d238451f9',
46
+ parentUuid: '66d090ee-2e16-4b8d-bc67-bcb1027220cb',
47
+ version: '2.1.234',
48
+ userType: 'external',
49
+ message: {
50
+ model: 'claude-fable-5',
51
+ usage: {
52
+ service_tier: 'standard',
53
+ },
54
+ },
55
+ },
56
+ {
57
+ type: 'assistant',
58
+ isSidechain: false,
59
+ effort: 'high',
60
+ sessionId: '7f9d5ab4-3b08-4a21-adb5-405c04af2d89',
61
+ requestId: 'req_011Ce9GB4DvzreK2hzChvmgy',
62
+ uuid: '50ef9865-24e6-4a77-9831-a01258db757f',
63
+ parentUuid: '14c99a81-02ff-4153-8104-3e4d238451f9',
64
+ version: '2.1.234',
65
+ userType: 'external',
66
+ message: {
67
+ model: 'claude-fable-5',
68
+ usage: {
69
+ service_tier: 'standard',
70
+ },
71
+ },
72
+ },
73
+ {
74
+ type: 'assistant',
75
+ isSidechain: false,
76
+ effort: 'high',
77
+ sessionId: '7f9d5ab4-3b08-4a21-adb5-405c04af2d89',
78
+ requestId: 'req_011Ce9GB4DvzreK2hzChvmgy',
79
+ uuid: '15864101-70c5-48fb-939f-679a458ba679',
80
+ parentUuid: '50ef9865-24e6-4a77-9831-a01258db757f',
81
+ version: '2.1.234',
82
+ userType: 'external',
83
+ message: {
84
+ model: 'claude-fable-5',
85
+ usage: {
86
+ service_tier: 'standard',
87
+ },
88
+ },
89
+ },
90
+ ];
91
+ export const SIDECHAIN_TRANSCRIPT = [
92
+ {
93
+ type: 'assistant',
94
+ isSidechain: true,
95
+ effort: 'high',
96
+ attributionAgent: 'general-purpose',
97
+ sessionId: '19c78382-cceb-45ab-bf24-bb8aa284d96b',
98
+ requestId: 'req_011CdSgeEdPwRsUpVTCihKmV',
99
+ uuid: '0f106449-eeb9-475c-8186-d70b9d14a82c',
100
+ parentUuid: '95017f46-0284-49e3-9a7c-597ad4364042',
101
+ version: '2.1.220',
102
+ userType: 'external',
103
+ message: {
104
+ model: 'claude-opus-5',
105
+ usage: {
106
+ service_tier: 'standard',
107
+ },
108
+ },
109
+ },
110
+ {
111
+ type: 'assistant',
112
+ isSidechain: true,
113
+ effort: 'high',
114
+ attributionAgent: 'general-purpose',
115
+ sessionId: '19c78382-cceb-45ab-bf24-bb8aa284d96b',
116
+ requestId: 'req_011CdSgeEdPwRsUpVTCihKmV',
117
+ uuid: '24c36d1b-c532-447f-9981-8f2b0d0988f8',
118
+ parentUuid: '0f106449-eeb9-475c-8186-d70b9d14a82c',
119
+ version: '2.1.220',
120
+ userType: 'external',
121
+ message: {
122
+ model: 'claude-opus-5',
123
+ usage: {
124
+ service_tier: 'standard',
125
+ },
126
+ },
127
+ },
128
+ {
129
+ type: 'assistant',
130
+ isSidechain: true,
131
+ effort: 'high',
132
+ attributionAgent: 'general-purpose',
133
+ sessionId: '19c78382-cceb-45ab-bf24-bb8aa284d96b',
134
+ requestId: 'req_011CdSgeEdPwRsUpVTCihKmV',
135
+ uuid: 'd3453335-a00b-4716-95bf-6a7574903336',
136
+ parentUuid: 'c0d14665-a467-46e5-abcd-85c3e15ba30e',
137
+ version: '2.1.220',
138
+ userType: 'external',
139
+ message: {
140
+ model: 'claude-opus-5',
141
+ usage: {
142
+ service_tier: 'standard',
143
+ },
144
+ },
145
+ },
146
+ ];
@@ -0,0 +1,229 @@
1
+ import { observationIdentifier as observedValue } from './oat-dispatch-record.js';
2
+ /**
3
+ * Claude runtime metadata parsing.
4
+ *
5
+ * Field paths here are taken from real on-disk transcripts under
6
+ * `~/.claude/projects/`, not from assumption. Every corpus figure in this file
7
+ * is an operator-environment observation as of 2026-09-02 on the capturing
8
+ * operator's machine, not a provider guarantee: a later mismatch means the
9
+ * corpus moved, not that the claim was wrong.
10
+ *
11
+ * An earlier revision read only `system`/`init` and `result` records; a scan of
12
+ * all 2,725 local transcripts found `"subtype":"init"` in zero of them, so that parser returned
13
+ * `not-reported` against every real transcript while its hand-written fixtures
14
+ * agreed with it. The on-disk `assistant` entry is now the primary shape.
15
+ *
16
+ * Reading an `assistant` entry does not mean reading a conversation. The parser
17
+ * takes allowlisted entry-level metadata plus exactly two explicit key paths
18
+ * inside `message` — `message.model` and `message.usage.service_tier`.
19
+ * `message` is never spread, enumerated, or filtered after the fact, so
20
+ * `message.content` is not merely dropped later; it is never reached. The
21
+ * content-trap test pins this: a `message` whose `content` throws on access
22
+ * still parses cleanly.
23
+ *
24
+ * Everything this module produces is per-run, non-authoritative corroboration.
25
+ * A failed or partial parse yields `not-reported` and never falls back to the
26
+ * configured invocation.
27
+ */
28
+ /**
29
+ * The complete entry-level read surface for an `assistant` entry. `message` is
30
+ * handled separately by explicit path and is deliberately absent from this
31
+ * list, so this plus `message.model` and `message.usage.service_tier` is
32
+ * everything the parser touches. Exported so a test can pin it: a future widening
33
+ * has to change this list, in view, rather than slip in at a call site.
34
+ *
35
+ * `attributionAgent` is the role identifier and is the same class of signal
36
+ * Codex carries as `agent_role`: a bounded enum-like name, not conversation
37
+ * content. Across 2,725 captured transcripts it took 8 distinct values, all
38
+ * of which pass the shared identifier validator, with a longest value of 21
39
+ * characters against the 256 bound. It appears on subagent turns only, so a
40
+ * main session reports no role rather than a synthesized one.
41
+ */
42
+ export const CLAUDE_ASSISTANT_KEYS = [
43
+ 'type',
44
+ 'isSidechain',
45
+ 'effort',
46
+ 'attributionAgent',
47
+ 'sessionId',
48
+ 'session_id',
49
+ 'requestId',
50
+ ];
51
+ /**
52
+ * Stream-json entry keys, for `claude -p --output-format stream-json`.
53
+ *
54
+ * UNVERIFIED: no captured artifact of that format was available, and zero of
55
+ * the 2,725 observed local transcripts contained a `system`/`init` record. This path is
56
+ * retained as a secondary shape and must not be presented as equally grounded
57
+ * with the on-disk shape above.
58
+ */
59
+ export const CLAUDE_INIT_KEYS = [
60
+ 'type',
61
+ 'subtype',
62
+ 'model',
63
+ 'service_tier',
64
+ 'serviceTier',
65
+ 'agent',
66
+ 'subagent_type',
67
+ 'reasoning_effort',
68
+ 'effort',
69
+ 'request_id',
70
+ ];
71
+ export const CLAUDE_OBSERVATION_SOURCE = 'claude-session-metadata';
72
+ function isRecord(value) {
73
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
74
+ }
75
+ /**
76
+ * The two explicit key paths inside `message`. Reaching them by path is what
77
+ * makes the projection narrow; a spread or key-filter would touch
78
+ * `message.content` on the way.
79
+ */
80
+ function messageMetadata(entry) {
81
+ const message = entry.message;
82
+ if (!isRecord(message))
83
+ return { model: undefined, serviceTier: undefined };
84
+ const usage = message.usage;
85
+ return {
86
+ model: message.model,
87
+ serviceTier: isRecord(usage) ? usage.service_tier : undefined,
88
+ };
89
+ }
90
+ function firstModelUsage(entry) {
91
+ const usage = entry.modelUsage;
92
+ if (!isRecord(usage))
93
+ return null;
94
+ const [name, detail] = Object.entries(usage)[0] ?? [];
95
+ if (name === undefined)
96
+ return null;
97
+ return {
98
+ model: observedValue(name),
99
+ serviceTier: isRecord(detail)
100
+ ? observedValue(detail.serviceTier ?? detail.service_tier)
101
+ : null,
102
+ };
103
+ }
104
+ function assistantTurn(entry) {
105
+ const { model, serviceTier } = messageMetadata(entry);
106
+ return {
107
+ isSidechain: typeof entry.isSidechain === 'boolean' ? entry.isSidechain : null,
108
+ effort: observedValue(entry.effort),
109
+ sessionId: observedValue(entry.sessionId ?? entry.session_id),
110
+ model: observedValue(model),
111
+ serviceTier: observedValue(serviceTier),
112
+ requestId: null,
113
+ // Role only. It never contributes to lineage: `isSidechain` is the sole
114
+ // lineage signal, and inferring depth from a role name would be exactly
115
+ // the invention this parser was rewritten to remove.
116
+ role: observedValue(entry.attributionAgent),
117
+ };
118
+ }
119
+ /** Secondary, unverified stream-json shape; see CLAUDE_INIT_KEYS. */
120
+ function streamJsonTurn(entry) {
121
+ if (entry.type === 'system' && entry.subtype === 'init') {
122
+ return {
123
+ isSidechain: null,
124
+ effort: observedValue(entry.reasoning_effort ?? entry.effort),
125
+ sessionId: null,
126
+ model: observedValue(entry.model),
127
+ serviceTier: observedValue(entry.service_tier ?? entry.serviceTier),
128
+ requestId: observedValue(entry.request_id),
129
+ role: observedValue(entry.agent ?? entry.subagent_type),
130
+ };
131
+ }
132
+ if (entry.type === 'result') {
133
+ const usage = firstModelUsage(entry);
134
+ if (usage === null)
135
+ return null;
136
+ return {
137
+ isSidechain: null,
138
+ effort: null,
139
+ sessionId: null,
140
+ model: usage.model,
141
+ serviceTier: usage.serviceTier,
142
+ requestId: null,
143
+ role: null,
144
+ };
145
+ }
146
+ return null;
147
+ }
148
+ /**
149
+ * Extract the child's metadata from Claude transcript entries.
150
+ *
151
+ * The applicable turn is the last one: it is the most recent state the session
152
+ * reported. Earlier turns fill only the fields the last one left unreported.
153
+ * Returns `null` when no Claude metadata is present, or when the entries mix
154
+ * sessions — every captured transcript carries exactly one `sessionId`, so more
155
+ * than one means no single applicable child exists.
156
+ */
157
+ export function extractClaudeRuntimeMetadata(entries) {
158
+ if (!Array.isArray(entries))
159
+ return null;
160
+ const turns = [];
161
+ for (const entry of entries) {
162
+ if (!isRecord(entry))
163
+ continue;
164
+ // Classification is by discriminator alone.
165
+ if (entry.type === 'assistant') {
166
+ turns.push(assistantTurn(entry));
167
+ continue;
168
+ }
169
+ const fallback = streamJsonTurn(entry);
170
+ if (fallback !== null)
171
+ turns.push(fallback);
172
+ }
173
+ if (turns.length === 0)
174
+ return null;
175
+ const sessions = new Set(turns
176
+ .map((turn) => turn.sessionId)
177
+ .filter((value) => value !== null));
178
+ if (sessions.size > 1)
179
+ return null;
180
+ const applicable = turns.at(-1);
181
+ if (!applicable)
182
+ return null;
183
+ let model = null;
184
+ let effort = null;
185
+ let serviceTier = null;
186
+ let requestId = null;
187
+ let role = null;
188
+ for (const turn of turns) {
189
+ model = turn.model ?? model;
190
+ effort = turn.effort ?? effort;
191
+ serviceTier = turn.serviceTier ?? serviceTier;
192
+ requestId = turn.requestId ?? requestId;
193
+ role = turn.role ?? role;
194
+ }
195
+ return {
196
+ childLineage: applicable.isSidechain === null
197
+ ? null
198
+ : applicable.isSidechain
199
+ ? 'depth-unknown'
200
+ : 'root',
201
+ role,
202
+ model,
203
+ effort,
204
+ serviceTier,
205
+ requestId,
206
+ sessionId: [...sessions][0] ?? null,
207
+ };
208
+ }
209
+ /**
210
+ * Neutral observation facts for the cross-provider projection.
211
+ *
212
+ * The single-session guarantee runs in-process inside
213
+ * {@link extractClaudeRuntimeMetadata}, so `sessionId` never needs to survive
214
+ * into the projected output — which matters, because `sessionId` classifies
215
+ * sensitive and previously made every real transcript unrecordable.
216
+ */
217
+ export function observeClaudeRuntimeFacts(entries) {
218
+ const metadata = extractClaudeRuntimeMetadata(entries);
219
+ if (metadata === null)
220
+ return null;
221
+ return {
222
+ lineage: metadata.childLineage,
223
+ role: metadata.role,
224
+ model: metadata.model,
225
+ effort: metadata.effort,
226
+ serviceTier: metadata.serviceTier,
227
+ correlation: metadata.requestId,
228
+ };
229
+ }