@open-agent-toolkit/cli 0.2.50 → 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 (263) 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 +21 -5
  10. package/assets/docs/reference/file-locations.md +8 -2
  11. package/assets/docs/reference/oat-directory-structure.md +25 -9
  12. package/assets/docs/workflows/projects/artifacts.md +9 -4
  13. package/assets/docs/workflows/projects/evidence-layers.md +110 -1
  14. package/assets/docs/workflows/projects/implementation-execution.md +13 -0
  15. package/assets/docs/workflows/projects/lifecycle.md +17 -5
  16. package/assets/docs/workflows/projects/orchestration-model.md +18 -0
  17. package/assets/docs/workflows/projects/picking-up-projects.md +26 -9
  18. package/assets/docs/workflows/projects/pr-flow.md +5 -3
  19. package/assets/docs/workflows/projects/reviewing-oat-prs.md +12 -8
  20. package/assets/public-package-versions.json +4 -4
  21. package/assets/skills/oat-dispatch-subagents/SKILL.md +57 -1
  22. package/assets/skills/oat-dispatch-subagents/references/record-schema.md +77 -1
  23. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +3 -3
  24. package/assets/skills/oat-project-complete/SKILL.md +193 -26
  25. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +306 -0
  26. package/assets/skills/oat-project-complete/scripts/finalize-synced-archive.mjs +125 -0
  27. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +106 -0
  28. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +110 -0
  29. package/assets/skills/oat-project-complete/scripts/resolve-synced-archive-entry.mjs +164 -0
  30. package/assets/skills/oat-project-dispatch-subagents/SKILL.md +24 -1
  31. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +3 -3
  32. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +11 -0
  33. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +3 -3
  34. package/assets/skills/oat-project-plan-writing/SKILL.md +12 -1
  35. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +3 -3
  36. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +3 -3
  37. package/assets/skills/oat-project-review-provide/SKILL.md +12 -1
  38. package/assets/skills/oat-project-review-provide-remote/SKILL.md +12 -1
  39. package/dist/agents/canonical/index.d.ts +2 -0
  40. package/dist/agents/canonical/index.d.ts.map +1 -1
  41. package/dist/agents/canonical/index.js +1 -0
  42. package/dist/agents/canonical/resolve.d.ts +45 -0
  43. package/dist/agents/canonical/resolve.d.ts.map +1 -0
  44. package/dist/agents/canonical/resolve.js +175 -0
  45. package/dist/commands/decision/agents-guidance.d.ts +2 -2
  46. package/dist/commands/decision/agents-guidance.d.ts.map +1 -1
  47. package/dist/commands/decision/agents-guidance.js +1 -4
  48. package/dist/commands/decision/index.d.ts.map +1 -1
  49. package/dist/commands/decision/index.js +25 -6
  50. package/dist/commands/docs/init/index.d.ts +7 -1
  51. package/dist/commands/docs/init/index.d.ts.map +1 -1
  52. package/dist/commands/docs/init/index.js +53 -20
  53. package/dist/commands/doctor/index.d.ts +8 -2
  54. package/dist/commands/doctor/index.d.ts.map +1 -1
  55. package/dist/commands/doctor/index.js +157 -46
  56. package/dist/commands/init/index.d.ts +8 -3
  57. package/dist/commands/init/index.d.ts.map +1 -1
  58. package/dist/commands/init/index.js +64 -26
  59. package/dist/commands/init/tools/index.d.ts +12 -12
  60. package/dist/commands/init/tools/index.d.ts.map +1 -1
  61. package/dist/commands/init/tools/index.js +430 -99
  62. package/dist/commands/init/tools/install-state.d.ts +2 -0
  63. package/dist/commands/init/tools/install-state.d.ts.map +1 -1
  64. package/dist/commands/init/tools/install-state.js +11 -0
  65. package/dist/commands/init/tools/project-guidance.d.ts +49 -0
  66. package/dist/commands/init/tools/project-guidance.d.ts.map +1 -0
  67. package/dist/commands/init/tools/project-guidance.js +144 -0
  68. package/dist/commands/init/tools/workflows/index.d.ts +7 -0
  69. package/dist/commands/init/tools/workflows/index.d.ts.map +1 -1
  70. package/dist/commands/init/tools/workflows/index.js +160 -7
  71. package/dist/commands/pjm/index.d.ts.map +1 -1
  72. package/dist/commands/pjm/index.js +28 -3
  73. package/dist/commands/pjm/init.d.ts +5 -0
  74. package/dist/commands/pjm/init.d.ts.map +1 -1
  75. package/dist/commands/pjm/init.js +19 -3
  76. package/dist/commands/project/archive/archive-utils.d.ts +8 -1
  77. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  78. package/dist/commands/project/archive/archive-utils.js +147 -50
  79. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  80. package/dist/commands/project/archive/push-runner.js +15 -2
  81. package/dist/commands/project/dispatch/index.d.ts +10 -0
  82. package/dist/commands/project/dispatch/index.d.ts.map +1 -0
  83. package/dist/commands/project/dispatch/index.js +137 -0
  84. package/dist/commands/project/dispatch/record.d.ts +91 -0
  85. package/dist/commands/project/dispatch/record.d.ts.map +1 -0
  86. package/dist/commands/project/dispatch/record.js +402 -0
  87. package/dist/commands/project/index.d.ts.map +1 -1
  88. package/dist/commands/project/index.js +2 -0
  89. package/dist/commands/project/links/compute.d.ts +1 -0
  90. package/dist/commands/project/links/compute.d.ts.map +1 -1
  91. package/dist/commands/project/links/compute.js +5 -4
  92. package/dist/commands/project/links/index.d.ts +9 -0
  93. package/dist/commands/project/links/index.d.ts.map +1 -1
  94. package/dist/commands/project/links/index.js +71 -2
  95. package/dist/commands/project/list.d.ts +10 -2
  96. package/dist/commands/project/list.d.ts.map +1 -1
  97. package/dist/commands/project/list.js +205 -29
  98. package/dist/commands/project/open/index.d.ts +2 -0
  99. package/dist/commands/project/open/index.d.ts.map +1 -1
  100. package/dist/commands/project/open/index.js +16 -1
  101. package/dist/commands/project/prune/index.d.ts +10 -4
  102. package/dist/commands/project/prune/index.d.ts.map +1 -1
  103. package/dist/commands/project/prune/index.js +85 -15
  104. package/dist/commands/project/pull/index.d.ts +4 -0
  105. package/dist/commands/project/pull/index.d.ts.map +1 -1
  106. package/dist/commands/project/pull/index.js +82 -3
  107. package/dist/commands/project/sync/git.d.ts +9 -0
  108. package/dist/commands/project/sync/git.d.ts.map +1 -1
  109. package/dist/commands/project/sync/git.js +18 -0
  110. package/dist/commands/project/sync/ref-sync.d.ts +16 -1
  111. package/dist/commands/project/sync/ref-sync.d.ts.map +1 -1
  112. package/dist/commands/project/sync/ref-sync.js +288 -17
  113. package/dist/commands/project/sync/resolve-target.d.ts +10 -0
  114. package/dist/commands/project/sync/resolve-target.d.ts.map +1 -1
  115. package/dist/commands/project/sync/resolve-target.js +56 -1
  116. package/dist/commands/providers/inspect/inspect.d.ts +8 -1
  117. package/dist/commands/providers/inspect/inspect.d.ts.map +1 -1
  118. package/dist/commands/providers/inspect/inspect.js +146 -26
  119. package/dist/commands/providers/list/list.d.ts +8 -1
  120. package/dist/commands/providers/list/list.d.ts.map +1 -1
  121. package/dist/commands/providers/list/list.js +139 -23
  122. package/dist/commands/providers/providers.types.d.ts +25 -1
  123. package/dist/commands/providers/providers.types.d.ts.map +1 -1
  124. package/dist/commands/providers/set/index.d.ts +9 -1
  125. package/dist/commands/providers/set/index.d.ts.map +1 -1
  126. package/dist/commands/providers/set/index.js +43 -33
  127. package/dist/commands/remove/skill/remove-skill.d.ts.map +1 -1
  128. package/dist/commands/remove/skill/remove-skill.js +11 -0
  129. package/dist/commands/shared/agents-md.d.ts +42 -14
  130. package/dist/commands/shared/agents-md.d.ts.map +1 -1
  131. package/dist/commands/shared/agents-md.js +364 -70
  132. package/dist/commands/shared/project-scope.d.ts +2 -0
  133. package/dist/commands/shared/project-scope.d.ts.map +1 -1
  134. package/dist/commands/shared/project-scope.js +5 -0
  135. package/dist/commands/state/generate.d.ts +2 -0
  136. package/dist/commands/state/generate.d.ts.map +1 -1
  137. package/dist/commands/state/generate.js +93 -28
  138. package/dist/commands/status/index.d.ts +8 -3
  139. package/dist/commands/status/index.d.ts.map +1 -1
  140. package/dist/commands/status/index.js +147 -32
  141. package/dist/commands/sync/apply.d.ts.map +1 -1
  142. package/dist/commands/sync/apply.js +220 -29
  143. package/dist/commands/sync/dry-run.d.ts.map +1 -1
  144. package/dist/commands/sync/dry-run.js +30 -6
  145. package/dist/commands/sync/index.d.ts +17 -2
  146. package/dist/commands/sync/index.d.ts.map +1 -1
  147. package/dist/commands/sync/index.js +106 -41
  148. package/dist/commands/sync/sync.types.d.ts +39 -2
  149. package/dist/commands/sync/sync.types.d.ts.map +1 -1
  150. package/dist/commands/sync/sync.utils.d.ts +5 -1
  151. package/dist/commands/sync/sync.utils.d.ts.map +1 -1
  152. package/dist/commands/sync/sync.utils.js +73 -0
  153. package/dist/commands/tools/info/info-tool.d.ts +2 -0
  154. package/dist/commands/tools/info/info-tool.d.ts.map +1 -1
  155. package/dist/commands/tools/info/info-tool.js +34 -12
  156. package/dist/commands/tools/install/index.d.ts.map +1 -1
  157. package/dist/commands/tools/install/index.js +19 -3
  158. package/dist/commands/tools/list/list-tools.d.ts +2 -0
  159. package/dist/commands/tools/list/list-tools.d.ts.map +1 -1
  160. package/dist/commands/tools/list/list-tools.js +36 -7
  161. package/dist/commands/tools/remove/index.d.ts.map +1 -1
  162. package/dist/commands/tools/remove/index.js +150 -21
  163. package/dist/commands/tools/remove/remove-tools.d.ts +20 -0
  164. package/dist/commands/tools/remove/remove-tools.d.ts.map +1 -1
  165. package/dist/commands/tools/remove/remove-tools.js +122 -1
  166. package/dist/commands/tools/shared/format-pack-inventory.d.ts +19 -0
  167. package/dist/commands/tools/shared/format-pack-inventory.d.ts.map +1 -1
  168. package/dist/commands/tools/shared/format-pack-inventory.js +109 -0
  169. package/dist/commands/tools/shared/pack-evidence.d.ts +66 -0
  170. package/dist/commands/tools/shared/pack-evidence.d.ts.map +1 -0
  171. package/dist/commands/tools/shared/pack-evidence.js +132 -0
  172. package/dist/commands/tools/shared/pack-inventory.d.ts +1 -0
  173. package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
  174. package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts +44 -0
  175. package/dist/commands/tools/shared/pack-lifecycle-outcome.d.ts.map +1 -0
  176. package/dist/commands/tools/shared/pack-lifecycle-outcome.js +91 -0
  177. package/dist/commands/tools/update/index.d.ts.map +1 -1
  178. package/dist/commands/tools/update/index.js +74 -4
  179. package/dist/commands/tools/update/update-tools.d.ts +5 -0
  180. package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
  181. package/dist/commands/tools/update/update-tools.js +64 -2
  182. package/dist/config/oat-config.d.ts +5 -0
  183. package/dist/config/oat-config.d.ts.map +1 -1
  184. package/dist/config/oat-config.js +17 -5
  185. package/dist/config/user-sync-config.d.ts +1 -0
  186. package/dist/config/user-sync-config.d.ts.map +1 -1
  187. package/dist/config/user-sync-config.js +8 -0
  188. package/dist/drift/detector.d.ts +2 -2
  189. package/dist/drift/detector.d.ts.map +1 -1
  190. package/dist/drift/detector.js +18 -0
  191. package/dist/drift/strays.d.ts.map +1 -1
  192. package/dist/drift/strays.js +5 -0
  193. package/dist/engine/collection-sync.d.ts +14 -0
  194. package/dist/engine/collection-sync.d.ts.map +1 -0
  195. package/dist/engine/collection-sync.js +282 -0
  196. package/dist/engine/compute-plan.d.ts +8 -1
  197. package/dist/engine/compute-plan.d.ts.map +1 -1
  198. package/dist/engine/compute-plan.js +303 -12
  199. package/dist/engine/engine.types.d.ts +61 -0
  200. package/dist/engine/engine.types.d.ts.map +1 -1
  201. package/dist/engine/execute-plan.d.ts +22 -2
  202. package/dist/engine/execute-plan.d.ts.map +1 -1
  203. package/dist/engine/execute-plan.js +459 -27
  204. package/dist/engine/provider-path-safety.d.ts +1 -0
  205. package/dist/engine/provider-path-safety.d.ts.map +1 -1
  206. package/dist/engine/provider-path-safety.js +15 -0
  207. package/dist/engine/scanner.d.ts +2 -2
  208. package/dist/engine/scanner.d.ts.map +1 -1
  209. package/dist/engine/scanner.js +21 -4
  210. package/dist/fs/io.d.ts +67 -0
  211. package/dist/fs/io.d.ts.map +1 -1
  212. package/dist/fs/io.js +536 -3
  213. package/dist/manifest/index.d.ts +2 -2
  214. package/dist/manifest/index.d.ts.map +1 -1
  215. package/dist/manifest/index.js +1 -1
  216. package/dist/manifest/manager.d.ts +2 -2
  217. package/dist/manifest/manager.d.ts.map +1 -1
  218. package/dist/manifest/manager.js +20 -3
  219. package/dist/manifest/manifest.types.d.ts +704 -13
  220. package/dist/manifest/manifest.types.d.ts.map +1 -1
  221. package/dist/manifest/manifest.types.js +131 -15
  222. package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
  223. package/dist/providers/codex/codec/sync-extension.js +41 -10
  224. package/dist/providers/cursor/codec/sync-extension.d.ts.map +1 -1
  225. package/dist/providers/cursor/codec/sync-extension.js +41 -10
  226. package/dist/providers/identity/absolute-paths.d.ts +68 -0
  227. package/dist/providers/identity/absolute-paths.d.ts.map +1 -0
  228. package/dist/providers/identity/absolute-paths.js +144 -0
  229. package/dist/providers/identity/claude-runtime-observation.d.ts +105 -0
  230. package/dist/providers/identity/claude-runtime-observation.d.ts.map +1 -0
  231. package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts +40 -0
  232. package/dist/providers/identity/claude-runtime-observation.fixtures.d.ts.map +1 -0
  233. package/dist/providers/identity/claude-runtime-observation.fixtures.js +146 -0
  234. package/dist/providers/identity/claude-runtime-observation.js +229 -0
  235. package/dist/providers/identity/codex-runtime-observation.d.ts +75 -0
  236. package/dist/providers/identity/codex-runtime-observation.d.ts.map +1 -0
  237. package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts +37 -0
  238. package/dist/providers/identity/codex-runtime-observation.fixtures.d.ts.map +1 -0
  239. package/dist/providers/identity/codex-runtime-observation.fixtures.js +539 -0
  240. package/dist/providers/identity/codex-runtime-observation.js +255 -0
  241. package/dist/providers/identity/generic-dispatch-record.d.ts +313 -0
  242. package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -0
  243. package/dist/providers/identity/generic-dispatch-record.js +539 -0
  244. package/dist/providers/identity/oat-dispatch-record.d.ts +1964 -0
  245. package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -0
  246. package/dist/providers/identity/oat-dispatch-record.js +691 -0
  247. package/dist/providers/identity/runtime-observation.d.ts +100 -0
  248. package/dist/providers/identity/runtime-observation.d.ts.map +1 -0
  249. package/dist/providers/identity/runtime-observation.js +169 -0
  250. package/dist/providers/shared/index.d.ts +2 -0
  251. package/dist/providers/shared/index.d.ts.map +1 -1
  252. package/dist/providers/shared/index.js +1 -0
  253. package/dist/providers/shared/materialization-extension.d.ts +12 -0
  254. package/dist/providers/shared/materialization-extension.d.ts.map +1 -1
  255. package/dist/providers/shared/registry.d.ts +69 -0
  256. package/dist/providers/shared/registry.d.ts.map +1 -0
  257. package/dist/providers/shared/registry.js +252 -0
  258. package/dist/providers/shared/restart-adviser.d.ts +26 -0
  259. package/dist/providers/shared/restart-adviser.d.ts.map +1 -0
  260. package/dist/providers/shared/restart-adviser.js +96 -0
  261. package/dist/shared/types.d.ts +4 -0
  262. package/dist/shared/types.d.ts.map +1 -1
  263. package/package.json +2 -2
@@ -0,0 +1,402 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { publishContainedJsonRevision, redactedFsError, withContainedWriterLock, } from '../../../fs/io.js';
4
+ import { assertJournalIdentityHasNoAbsolutePath, redactAbsolutePaths, } from '../../../providers/identity/absolute-paths.js';
5
+ import { CLAUDE_OBSERVATION_SOURCE } from '../../../providers/identity/claude-runtime-observation.js';
6
+ import { CODEX_OBSERVATION_SOURCE } from '../../../providers/identity/codex-runtime-observation.js';
7
+ import { assertBoundedDispatchRecordSize, assertNoSensitiveDispatchContent, identityFieldsOf, parseGenericDispatchRecord, } from '../../../providers/identity/generic-dispatch-record.js';
8
+ import { augmentDispatchRecord, compareObservedRuntimeMetadata, comparedObservationAxes, configuredInvocationForObservation, parsePersistedOatDispatchRecord, parseRuntimeObservation, } from '../../../providers/identity/oat-dispatch-record.js';
9
+ import { observationFromFacts, parseRuntimeObservationEnvelope, projectRuntimeObservation, providerSupportsRuntimeObservation, } from '../../../providers/identity/runtime-observation.js';
10
+ const REQUEST_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
11
+ const DISPATCH_LOCK_NAME = '.dispatch-lock';
12
+ /**
13
+ * The journal is append-only. Revision 1 keeps the plain `<request-id>.json`
14
+ * name; every later revision is published create-only under
15
+ * `<request-id>@<NNNN>.json`. Publishing revision N+1 exclusively is the
16
+ * compare-and-swap: a caller that lost the race observes `EEXIST` instead of
17
+ * replacing the winner, and no publication can ever clobber an existing file.
18
+ * `@` is outside the request-ID character class, so the split is unambiguous.
19
+ */
20
+ const REVISION_SEPARATOR = '@';
21
+ const REVISION_SUFFIX_PATTERN = /^(.*)@(\d{4})$/;
22
+ const MAX_REVISION = 9999;
23
+ function revisionFileName(requestId, revision) {
24
+ return revision === 1
25
+ ? `${requestId}.json`
26
+ : `${requestId}${REVISION_SEPARATOR}${String(revision).padStart(4, '0')}.json`;
27
+ }
28
+ function parseJournalEntry(fileName) {
29
+ if (!fileName.endsWith('.json'))
30
+ return null;
31
+ const stem = fileName.slice(0, -'.json'.length);
32
+ const suffixed = REVISION_SUFFIX_PATTERN.exec(stem);
33
+ const requestId = suffixed?.[1] ?? stem;
34
+ const revision = suffixed ? Number(suffixed[2]) : 1;
35
+ if (!REQUEST_ID_PATTERN.test(requestId) || revision < 1)
36
+ return null;
37
+ return { requestId, revision, fileName };
38
+ }
39
+ /**
40
+ * Hard redaction boundary. Producers redact their own messages, but this is the
41
+ * last stop before a message reaches `--json`, so a future call site that
42
+ * forgets cannot regress NFR1. Known roots become stable labels; anything else
43
+ * that still looks like an absolute path is scrubbed.
44
+ *
45
+ * The detector is shared with the record sanitizer. Two independent notions of
46
+ * "looks like a path" is how a path could be scrubbed from an error message and
47
+ * written verbatim into the durable record by the same command.
48
+ */
49
+ export function redactDispatchMessage(message, roots = {}) {
50
+ let redacted = message;
51
+ const labelled = [
52
+ ['<project>', roots.project],
53
+ ['<repo>', roots.repo],
54
+ ['<home>', roots.home],
55
+ ];
56
+ // Longest root first so a nested project path is not masked by its repo.
57
+ for (const [label, root] of [...labelled].sort(([, left], [, right]) => (right?.length ?? 0) - (left?.length ?? 0))) {
58
+ if (root)
59
+ redacted = redacted.split(root).join(label);
60
+ }
61
+ return redactAbsolutePaths(redacted);
62
+ }
63
+ /**
64
+ * Source strings that state provenance. A parser source is only ever written by
65
+ * the projection path that actually parsed provider output; a caller-supplied
66
+ * observation is always recorded as caller-asserted.
67
+ */
68
+ const OBSERVATION_SOURCE_BY_PROVIDER = {
69
+ codex: CODEX_OBSERVATION_SOURCE,
70
+ claude: CLAUDE_OBSERVATION_SOURCE,
71
+ };
72
+ const CALLER_ASSERTED_OBSERVATION_SOURCE = 'caller-asserted';
73
+ function isRecord(value) {
74
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
75
+ }
76
+ /**
77
+ * Resolve a post-launch observation event.
78
+ *
79
+ * A caller may submit either a finished `observation` or the sanitized provider
80
+ * `metadata` it was derived from. Submitting both is ambiguous and is refused
81
+ * rather than silently preferring one.
82
+ *
83
+ * Both forms are validated here, at the durable-write boundary, rather than
84
+ * being trusted from the producer. An earlier revision normalized only the
85
+ * metadata form, which left the finished-observation form able to assert its
86
+ * own `match`, name a provider the record does not use, and store values the
87
+ * metadata path would have refused. That is the per-producer-obligation class
88
+ * this boundary exists to prevent, so `match` is always derived and never read
89
+ * from the caller, and the provider is always bound to the record's own.
90
+ *
91
+ * Value shape is enforced once more in `runtimeObservationSchema`, so both
92
+ * forms converge on the same identifier rules without either path carrying its
93
+ * own copy of them.
94
+ */
95
+ function resolveObservationEvent(record, event) {
96
+ if (event.kind !== 'runtime-observation')
97
+ return event;
98
+ if ('metadata' in event && 'observation' in event) {
99
+ throw new Error('A runtime observation event carries either observation or metadata, not both.');
100
+ }
101
+ if ('metadata' in event)
102
+ return event;
103
+ const observation = event.observation;
104
+ if (!isRecord(observation) || observation.status !== 'reported') {
105
+ return event;
106
+ }
107
+ if (observation.provider !== record.provider) {
108
+ throw new Error('A runtime observation must name the same provider as its dispatch record.');
109
+ }
110
+ if (!providerSupportsRuntimeObservation(record.provider)) {
111
+ throw new Error(`Provider ${record.provider} has no runtime observation capability, so it cannot report an observation.`);
112
+ }
113
+ return {
114
+ ...event,
115
+ observation: parseRuntimeObservation({
116
+ ...observation,
117
+ // Provenance is derived, never accepted: a hand-authored observation
118
+ // cannot borrow a parser's source string and pass as a genuine parse.
119
+ source: CALLER_ASSERTED_OBSERVATION_SOURCE,
120
+ match: compareObservedRuntimeMetadata(observation, configuredInvocationForObservation(record)),
121
+ comparedAxes: comparedObservationAxes(observation, configuredInvocationForObservation(record)),
122
+ }),
123
+ };
124
+ }
125
+ export function parseDispatchRecordInput(value) {
126
+ if (!isRecord(value)) {
127
+ throw new Error('Dispatch record input must be a JSON object.');
128
+ }
129
+ const keys = Object.keys(value).sort();
130
+ if (keys.length !== 2 || keys[0] !== 'event' || keys[1] !== 'record') {
131
+ throw new Error('Dispatch record input accepts only record and event.');
132
+ }
133
+ if (!isRecord(value.event)) {
134
+ throw new Error('Dispatch record event must be a JSON object.');
135
+ }
136
+ // Raw provider metadata is the one input a caller may legitimately supply
137
+ // unmodified, so it is projected through the owning parser's allowlist and
138
+ // asserted as the projection. Everything else is asserted exactly as given.
139
+ // Asserting the raw entries instead would refuse real rollouts outright —
140
+ // a Codex `session_id` alone classifies as sensitive — which is what pushed
141
+ // sanitization onto callers and invited a hand-rolled stripper.
142
+ const rawMetadata = isRecord(value.event.metadata)
143
+ ? value.event.metadata
144
+ : null;
145
+ assertNoSensitiveDispatchContent(rawMetadata === null
146
+ ? value
147
+ : {
148
+ ...value,
149
+ event: {
150
+ ...value.event,
151
+ metadata: { ...rawMetadata, entries: null },
152
+ },
153
+ });
154
+ const record = parseGenericDispatchRecord(value.record);
155
+ if (rawMetadata !== null) {
156
+ if ('observation' in value.event) {
157
+ throw new Error('A runtime observation event carries either observation or metadata, not both.');
158
+ }
159
+ const envelope = parseRuntimeObservationEnvelope(rawMetadata);
160
+ const projected = envelope.provider === record.provider
161
+ ? projectRuntimeObservation(envelope.provider, envelope.entries)
162
+ : {
163
+ facts: null,
164
+ reason: `observation metadata names provider ${envelope.provider}, but the record names ${record.provider}`,
165
+ };
166
+ const facts = projected.facts;
167
+ assertNoSensitiveDispatchContent(facts, '<observation-metadata>');
168
+ const { metadata: _metadata, ...rest } = value.event;
169
+ // Built here from parsed provider output, so it carries a parser source and
170
+ // must not be re-processed as if a caller had supplied it.
171
+ const observation = observationFromFacts({
172
+ record,
173
+ provider: envelope.provider,
174
+ source: OBSERVATION_SOURCE_BY_PROVIDER[envelope.provider] ?? 'unknown',
175
+ observedAt: envelope.observedAt,
176
+ facts,
177
+ });
178
+ return {
179
+ record,
180
+ event: { ...rest, observation },
181
+ observationReason: observation.status === 'reported'
182
+ ? null
183
+ : // Facts can be produced and still be declined downstream, when the
184
+ // session declares a different request than this record does.
185
+ (projected.reason ??
186
+ 'observation metadata correlates to a different request'),
187
+ };
188
+ }
189
+ return {
190
+ record,
191
+ event: resolveObservationEvent(record, value.event),
192
+ };
193
+ }
194
+ function runtimeIdentityFor(record, reason = null) {
195
+ const observation = record.oat.runtimeObservation;
196
+ return {
197
+ configured: {
198
+ roleName: record.role_name,
199
+ roleSelector: record.role_selector,
200
+ model: record.model_selector,
201
+ effort: record.effort_selector,
202
+ serviceTier: record.service_tier_selector ?? null,
203
+ },
204
+ observed: observation.status === 'reported'
205
+ ? {
206
+ provider: observation.provider,
207
+ source: observation.source,
208
+ observedAt: observation.observedAt,
209
+ childLineage: observation.childLineage ?? null,
210
+ role: observation.role ?? null,
211
+ model: observation.model ?? null,
212
+ effort: observation.effort ?? null,
213
+ serviceTier: observation.serviceTier ?? null,
214
+ }
215
+ : null,
216
+ match: observation.status === 'reported' ? observation.match : null,
217
+ comparedAxes: observation.status === 'reported' ? (observation.comparedAxes ?? []) : [],
218
+ reason: observation.status === 'reported' ? null : reason,
219
+ status: observation.status,
220
+ };
221
+ }
222
+ function genericPart(record) {
223
+ const { oat: _oat, ...generic } = record;
224
+ return parseGenericDispatchRecord(generic);
225
+ }
226
+ function sameGenericRecord(left, right) {
227
+ return JSON.stringify(left) === JSON.stringify(right);
228
+ }
229
+ function isMissingError(error) {
230
+ return (typeof error === 'object' &&
231
+ error !== null &&
232
+ 'code' in error &&
233
+ error.code === 'ENOENT');
234
+ }
235
+ async function readPersistedRecord(path, reportedPath) {
236
+ let content;
237
+ try {
238
+ content = await readFile(path, 'utf8');
239
+ }
240
+ catch (error) {
241
+ if (isMissingError(error)) {
242
+ return null;
243
+ }
244
+ // A damaged or unreadable journal must not put an absolute project path
245
+ // into durable output; NFR1 treats that as a P0 disclosure.
246
+ throw redactedFsError(error, 'read', reportedPath);
247
+ }
248
+ return parsePersistedOatDispatchRecord(JSON.parse(content));
249
+ }
250
+ /** Highest published revision per request ID. */
251
+ async function readLatestRevisions(dispatchDir) {
252
+ let names;
253
+ try {
254
+ names = await readdir(dispatchDir);
255
+ }
256
+ catch (error) {
257
+ if (isMissingError(error)) {
258
+ return new Map();
259
+ }
260
+ throw redactedFsError(error, 'scan', 'dispatch/');
261
+ }
262
+ const latest = new Map();
263
+ for (const name of names.sort()) {
264
+ const entry = parseJournalEntry(name);
265
+ if (!entry)
266
+ continue;
267
+ const known = latest.get(entry.requestId);
268
+ if (!known || entry.revision > known.revision) {
269
+ latest.set(entry.requestId, entry);
270
+ }
271
+ }
272
+ return latest;
273
+ }
274
+ function fallbackTriggerRequestId(event) {
275
+ if (event.kind !== 'fallback-link')
276
+ return null;
277
+ const requestId = event.evidence?.triggerRequestId;
278
+ if (typeof requestId !== 'string' || !REQUEST_ID_PATTERN.test(requestId)) {
279
+ throw new Error('Fallback trigger request ID is invalid.');
280
+ }
281
+ return requestId;
282
+ }
283
+ function isAlreadyPublished(error) {
284
+ return (typeof error === 'object' &&
285
+ error !== null &&
286
+ 'code' in error &&
287
+ error.code === 'EEXIST');
288
+ }
289
+ async function publishRevision(projectPath, dispatchDir, requestId, revision, record) {
290
+ if (revision > MAX_REVISION) {
291
+ throw new Error(`Dispatch journal for ${requestId} reached its ${MAX_REVISION}-revision limit.`);
292
+ }
293
+ const fileName = revisionFileName(requestId, revision);
294
+ // Last check before bytes reach the journal. The sanitizer runs at parse, but
295
+ // the journal is committed to a shared repository, so the guarantee is
296
+ // enforced on the exact value being written rather than trusted upstream.
297
+ // Guarantees identity and control fields only; prose redaction is
298
+ // best-effort by design. See absolute-paths.ts.
299
+ assertJournalIdentityHasNoAbsolutePath(identityFieldsOf(genericPart(record)));
300
+ assertBoundedDispatchRecordSize(record);
301
+ try {
302
+ await publishContainedJsonRevision(join(dispatchDir, fileName), record, projectPath);
303
+ }
304
+ catch (error) {
305
+ if (isAlreadyPublished(error)) {
306
+ throw new Error('The dispatch journal advanced to a newer revision while this write was prepared; the concurrent update was preserved and this stale write was refused.', { cause: error });
307
+ }
308
+ throw error;
309
+ }
310
+ return `dispatch/${fileName}`;
311
+ }
312
+ /**
313
+ * Parse once, here. An earlier revision parsed in the command layer and again
314
+ * here; because the metadata path resolves to a finished observation, the second
315
+ * parse saw its own output as caller-supplied and overwrote the parser source
316
+ * with `caller-asserted`. Parsing is not idempotent by design — provenance
317
+ * depends on which input form arrived — so it must happen exactly once.
318
+ */
319
+ export async function recordProjectDispatch(input) {
320
+ const parsedInput = parseDispatchRecordInput(input.input);
321
+ if (input.projectPath === null) {
322
+ const record = augmentDispatchRecord(parsedInput);
323
+ return {
324
+ status: 'validated-only',
325
+ path: null,
326
+ created: false,
327
+ record,
328
+ runtimeIdentity: runtimeIdentityFor(record, parsedInput.observationReason ?? null),
329
+ };
330
+ }
331
+ const projectPath = input.projectPath;
332
+ const dispatchDir = join(projectPath, 'dispatch');
333
+ const requestId = parsedInput.record.request_id;
334
+ const latest = await readLatestRevisions(dispatchDir);
335
+ const existingEntry = latest.get(requestId) ?? null;
336
+ const existing = existingEntry
337
+ ? await readPersistedRecord(join(dispatchDir, existingEntry.fileName), `dispatch/${existingEntry.fileName}`)
338
+ : null;
339
+ if (existing &&
340
+ !sameGenericRecord(genericPart(existing), parsedInput.record)) {
341
+ throw new Error('Existing generic fields are immutable and cannot be redefined.');
342
+ }
343
+ const relatedRecords = [];
344
+ for (const [otherId, entry] of [...latest].sort(([left], [right]) => left < right ? -1 : 1)) {
345
+ if (otherId === requestId)
346
+ continue;
347
+ const related = await readPersistedRecord(join(dispatchDir, entry.fileName), `dispatch/${entry.fileName}`);
348
+ if (related)
349
+ relatedRecords.push(related);
350
+ }
351
+ const triggerRequestId = fallbackTriggerRequestId(parsedInput.event);
352
+ const triggerEntry = triggerRequestId
353
+ ? (latest.get(triggerRequestId) ?? null)
354
+ : null;
355
+ const trigger = triggerEntry
356
+ ? await readPersistedRecord(join(dispatchDir, triggerEntry.fileName), `dispatch/${triggerEntry.fileName}`)
357
+ : null;
358
+ if (triggerRequestId !== null && trigger === null) {
359
+ throw new Error('Fallback requires the rejected trigger record.');
360
+ }
361
+ await input.raceBarriers?.afterRead?.();
362
+ // One shared, identity-bound transition. The lock serializes the trigger
363
+ // claim and the fallback publication; the exclusive revision name is the
364
+ // compare-and-swap, so a stale concurrent writer loses without any write
365
+ // being able to replace the winner's evidence.
366
+ return withContainedWriterLock(join(projectPath, DISPATCH_LOCK_NAME), projectPath, async () => {
367
+ let triggerRecord = trigger ?? undefined;
368
+ if (trigger && triggerEntry && triggerRequestId) {
369
+ const claimed = augmentDispatchRecord({
370
+ record: trigger,
371
+ event: {
372
+ kind: 'fallback-claim',
373
+ requestId: triggerRequestId,
374
+ source: 'provider-wrapper',
375
+ claim: {
376
+ fallbackRequestId: requestId,
377
+ claimedAt: new Date().toISOString(),
378
+ },
379
+ },
380
+ });
381
+ if (JSON.stringify(claimed) !== JSON.stringify(trigger)) {
382
+ await publishRevision(projectPath, dispatchDir, triggerRequestId, triggerEntry.revision + 1, claimed);
383
+ }
384
+ triggerRecord = claimed;
385
+ }
386
+ const record = augmentDispatchRecord({
387
+ record: existing ?? parsedInput.record,
388
+ event: parsedInput.event,
389
+ ...(triggerRecord ? { triggerRecord } : {}),
390
+ relatedRecords,
391
+ });
392
+ const revision = (existingEntry?.revision ?? 0) + 1;
393
+ const path = await publishRevision(projectPath, dispatchDir, requestId, revision, record);
394
+ return {
395
+ status: 'persisted',
396
+ path,
397
+ created: revision === 1,
398
+ record,
399
+ runtimeIdentity: runtimeIdentityFor(record, parsedInput.observationReason ?? null),
400
+ };
401
+ });
402
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsBpC,wBAAgB,oBAAoB,IAAI,OAAO,CAsB9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuBpC,wBAAgB,oBAAoB,IAAI,OAAO,CAuB9C"}
@@ -2,6 +2,7 @@ import { Command } from 'commander';
2
2
  import { createProjectArchiveCommand } from './archive/index.js';
3
3
  import { createProjectCompleteDiscoveryCommand } from './complete-discovery/index.js';
4
4
  import { createProjectCompleteStateCommand } from './complete-state/index.js';
5
+ import { createProjectDispatchCommand } from './dispatch/index.js';
5
6
  import { createProjectDispatchCeilingCommand } from './dispatch-ceiling/index.js';
6
7
  import { createProjectLinksCommand } from './links/index.js';
7
8
  import { createProjectListCommand } from './list.js';
@@ -24,6 +25,7 @@ export function createProjectCommand() {
24
25
  .addCommand(createProjectArchiveCommand())
25
26
  .addCommand(createProjectCompleteDiscoveryCommand())
26
27
  .addCommand(createProjectCompleteStateCommand())
28
+ .addCommand(createProjectDispatchCommand())
27
29
  .addCommand(createProjectDispatchCeilingCommand())
28
30
  .addCommand(createProjectListCommand())
29
31
  .addCommand(createProjectLinksCommand())
@@ -4,6 +4,7 @@ import { type LinksInput } from './render.js';
4
4
  export interface ComputeLinksOptions {
5
5
  durableSummaryPath?: string;
6
6
  now: Date;
7
+ ref?: string;
7
8
  }
8
9
  export declare function computeLinksInput(target: SyncTarget, git: GitRunner, options: ComputeLinksOptions): Promise<LinksInput>;
9
10
  //# sourceMappingURL=compute.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compute.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/links/compute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,IAAI,CAAC;CACX;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CA6BrB"}
1
+ {"version":3,"file":"compute.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/links/compute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,IAAI,CAAC;IACV,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CA8BrB"}
@@ -1,11 +1,12 @@
1
1
  import { LINKABLE_ARTIFACTS, } from './render.js';
2
2
  export async function computeLinksInput(target, git, options) {
3
- await git.run(['fetch', target.remote, `+${target.ref}:${target.ref}`], {
3
+ const selectedRef = options.ref ?? target.ref;
4
+ await git.run(['fetch', target.remote, `+${selectedRef}:${selectedRef}`], {
4
5
  cwd: target.repoRoot,
5
6
  });
6
7
  const [sha, tree, origin] = await Promise.all([
7
- git.run(['rev-parse', target.ref], { cwd: target.repoRoot }),
8
- git.run(['ls-tree', '--name-only', target.ref], { cwd: target.repoRoot }),
8
+ git.run(['rev-parse', selectedRef], { cwd: target.repoRoot }),
9
+ git.run(['ls-tree', '--name-only', selectedRef], { cwd: target.repoRoot }),
9
10
  git.run(['remote', 'get-url', target.remote], { cwd: target.repoRoot }),
10
11
  ]);
11
12
  const treeNames = new Set(tree.stdout
@@ -16,7 +17,7 @@ export async function computeLinksInput(target, git, options) {
16
17
  return {
17
18
  slug: target.slug,
18
19
  sha: sha.stdout,
19
- ref: target.ref,
20
+ ref: selectedRef,
20
21
  originUrl: origin.stdout,
21
22
  present,
22
23
  ...(options.durableSummaryPath
@@ -1,17 +1,26 @@
1
1
  import { type CommandContext, type GlobalOptions } from '../../../app/command-context.js';
2
2
  import { type GitRunner } from '../../project/sync/git.js';
3
+ import { type SyncTarget } from '../../project/sync/ref-sync.js';
3
4
  import { resolveSyncedTarget } from '../../project/sync/resolve-target.js';
5
+ import { resolveProjectsRoot } from '../../shared/oat-paths.js';
4
6
  import { Command } from 'commander';
5
7
  import { computeLinksInput } from './compute.js';
6
8
  interface ProjectLinksDependencies {
7
9
  buildCommandContext: (options: GlobalOptions) => CommandContext;
8
10
  resolveProjectRoot: (cwd: string) => Promise<string>;
11
+ resolveProjectsRoot: typeof resolveProjectsRoot;
9
12
  resolveSyncedTarget: typeof resolveSyncedTarget;
13
+ resolveLinksTarget: typeof resolveLinksTarget;
10
14
  computeLinksInput: typeof computeLinksInput;
11
15
  gitRunner: GitRunner;
12
16
  processEnv: NodeJS.ProcessEnv;
13
17
  now: () => Date;
14
18
  }
19
+ interface ResolvedLinksTarget {
20
+ target: SyncTarget;
21
+ ref: string;
22
+ }
23
+ export declare function resolveLinksTarget(repoRoot: string, projectsRoot: string, pathOrSlug: string | undefined, env: NodeJS.ProcessEnv, git: GitRunner, resolveTarget: typeof resolveSyncedTarget): Promise<ResolvedLinksTarget>;
15
24
  export declare function createProjectLinksCommand(overrides?: Partial<ProjectLinksDependencies>): Command;
16
25
  export {};
17
26
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/links/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,uCAAuC,CAAC;AAI5E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAQ9C,UAAU,wBAAwB;IAChC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AA4ED,wBAAgB,yBAAyB,CACvC,SAAS,GAAE,OAAO,CAAC,wBAAwB,CAAM,GAChD,OAAO,CAsBT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/links/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAEL,mBAAmB,EACpB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AASjE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAQ9C,UAAU,wBAAwB;IAChC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AAmCD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAwBD,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,EAAE,SAAS,EACd,aAAa,EAAE,OAAO,mBAAmB,GACxC,OAAO,CAAC,mBAAmB,CAAC,CAgE9B;AAoDD,wBAAgB,yBAAyB,CACvC,SAAS,GAAE,OAAO,CAAC,wBAAwB,CAAM,GAChD,OAAO,CAsBT"}
@@ -1,7 +1,10 @@
1
1
  import { isAbsolute, relative, resolve, sep } from 'node:path';
2
2
  import { buildCommandContext, } from '../../../app/command-context.js';
3
3
  import { defaultGitRunner } from '../../project/sync/git.js';
4
- import { resolveSyncedTarget } from '../../project/sync/resolve-target.js';
4
+ import { buildSyncTarget, classifyRemoteRefLookup, } from '../../project/sync/ref-sync.js';
5
+ import { probeSyncedTerminalRefs, resolveSyncedTarget, } from '../../project/sync/resolve-target.js';
6
+ import { resolveProjectsRoot } from '../../shared/oat-paths.js';
7
+ import { completedSyncedRefName, resolveProjectScope, resolveScopeRoot, } from '../../shared/project-scope.js';
5
8
  import { readGlobalOptions } from '../../shared/shared.utils.js';
6
9
  import { CliError } from '../../../errors/cli-error.js';
7
10
  import { resolveProjectRoot } from '../../../fs/paths.js';
@@ -11,7 +14,9 @@ import { renderLinksBlock } from './render.js';
11
14
  const DEFAULT_DEPENDENCIES = {
12
15
  buildCommandContext,
13
16
  resolveProjectRoot,
17
+ resolveProjectsRoot,
14
18
  resolveSyncedTarget,
19
+ resolveLinksTarget,
15
20
  computeLinksInput,
16
21
  gitRunner: defaultGitRunner,
17
22
  processEnv: process.env,
@@ -30,13 +35,77 @@ function normalizeDurableSummaryPath(repoRoot, durableSummaryPath) {
30
35
  }
31
36
  return repositoryRelative.split(sep).join('/');
32
37
  }
38
+ function linksSlug(repoRoot, projectsRoot, pathOrSlug) {
39
+ if (!pathOrSlug.includes('/') && !pathOrSlug.includes('\\')) {
40
+ return pathOrSlug;
41
+ }
42
+ const absolute = isAbsolute(pathOrSlug)
43
+ ? resolve(pathOrSlug)
44
+ : resolve(repoRoot, pathOrSlug);
45
+ const sharedRoot = resolveScopeRoot(repoRoot, projectsRoot, 'shared');
46
+ if (resolveProjectScope(absolute, sharedRoot, repoRoot) !== 'synced') {
47
+ return null;
48
+ }
49
+ const child = relative(resolveScopeRoot(repoRoot, projectsRoot, 'synced'), absolute);
50
+ return child && !child.includes(sep) ? child : null;
51
+ }
52
+ export async function resolveLinksTarget(repoRoot, projectsRoot, pathOrSlug, env, git, resolveTarget) {
53
+ let resolved = null;
54
+ let resolutionError;
55
+ try {
56
+ resolved = await resolveTarget({ repoRoot, env }, pathOrSlug, { gitRunner: git }, { allowMissingCheckout: true });
57
+ }
58
+ catch (error) {
59
+ resolutionError = error;
60
+ if (!(error instanceof CliError) ||
61
+ error.exitCode !== 1 ||
62
+ !error.message.startsWith('No synced project named ')) {
63
+ throw error;
64
+ }
65
+ }
66
+ const slug = pathOrSlug
67
+ ? linksSlug(repoRoot, projectsRoot, pathOrSlug)
68
+ : resolved?.slug;
69
+ if (!slug) {
70
+ if (resolutionError)
71
+ throw resolutionError;
72
+ throw new CliError('No synced project is available for link rendering.', 1);
73
+ }
74
+ const target = resolved ?? buildSyncTarget(repoRoot, projectsRoot, slug);
75
+ const completedRef = completedSyncedRefName(slug);
76
+ const lookup = await git.run(['ls-remote', '--exit-code', target.remote, completedRef], { cwd: repoRoot, allowFailure: true });
77
+ if (classifyRemoteRefLookup(lookup, target.remote, completedRef) === 'absent') {
78
+ if (resolutionError)
79
+ throw resolutionError;
80
+ return { target, ref: target.ref };
81
+ }
82
+ const rows = lookup.stdout.split('\n').filter(Boolean);
83
+ const [sha, ref] = rows[0]?.trim().split(/\s+/) ?? [];
84
+ if (rows.length !== 1 ||
85
+ ref !== completedRef ||
86
+ !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(sha ?? '')) {
87
+ throw new CliError(`Unable to verify terminal links for ${slug}: origin returned a malformed completed-ref advertisement.`, 2);
88
+ }
89
+ const probe = await probeSyncedTerminalRefs(target, sha, git);
90
+ if (probe.state === 'wrong-sha') {
91
+ throw new CliError(`Cannot refresh links for ${slug}: active ${probe.activeRef} is ${probe.activeSha ?? 'absent'} while completed ${probe.completedRef} is ${probe.completedSha ?? 'absent'}. Repair the terminal ref mismatch first.`, 1);
92
+ }
93
+ if (probe.state === 'completed-only' || probe.state === 'both') {
94
+ return { target, ref: completedRef };
95
+ }
96
+ if (resolutionError)
97
+ throw resolutionError;
98
+ return { target, ref: target.ref };
99
+ }
33
100
  async function runLinks(context, pathOrSlug, options, dependencies) {
34
101
  try {
35
102
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
36
- const target = await dependencies.resolveSyncedTarget({ repoRoot, env: dependencies.processEnv }, pathOrSlug, {}, { allowMissingCheckout: true });
103
+ const projectsRoot = await dependencies.resolveProjectsRoot(repoRoot, dependencies.processEnv);
104
+ const { target, ref } = await dependencies.resolveLinksTarget(repoRoot, projectsRoot, pathOrSlug, dependencies.processEnv, dependencies.gitRunner, dependencies.resolveSyncedTarget);
37
105
  const input = await dependencies.computeLinksInput(target, dependencies.gitRunner, {
38
106
  durableSummaryPath: normalizeDurableSummaryPath(repoRoot, options.durableSummary),
39
107
  now: dependencies.now(),
108
+ ref,
40
109
  });
41
110
  const markdown = renderLinksBlock(input);
42
111
  if (context.json || options.format === 'json') {
@@ -1,7 +1,9 @@
1
1
  import { type CommandContext, type GlobalOptions } from '../../app/command-context.js';
2
2
  import { type GitRunner } from '../project/sync/git.js';
3
- import { listSyncedRecords } from '../project/sync/record.js';
4
- import { type ProjectSummary } from '@open-agent-toolkit/control-plane';
3
+ import { listSyncedRecords, type SyncedProjectRecord } from '../project/sync/record.js';
4
+ import { buildSyncTarget } from '../project/sync/ref-sync.js';
5
+ import { probeSyncedTerminalRefs, type SyncedTerminalRefProbe } from '../project/sync/resolve-target.js';
6
+ import { type ProjectListRow, type ProjectSummary } from '@open-agent-toolkit/control-plane';
5
7
  import { Command } from 'commander';
6
8
  interface ProjectListDependencies {
7
9
  buildCommandContext: (options: GlobalOptions) => CommandContext;
@@ -9,6 +11,8 @@ interface ProjectListDependencies {
9
11
  resolveProjectsRoot: (repoRoot: string, env: NodeJS.ProcessEnv) => Promise<string>;
10
12
  listProjects: (projectsRoot: string) => Promise<ProjectSummary[]>;
11
13
  listSyncedRecords: typeof listSyncedRecords;
14
+ probeSyncedTerminalRefs: typeof probeSyncedTerminalRefs;
15
+ probeAuthoritativeCompletedRef: typeof probeAuthoritativeCompletedRef;
12
16
  directoryExists: (path: string) => Promise<boolean>;
13
17
  readProjectMetadata: (projectPath: string) => Promise<ProjectListMetadata>;
14
18
  processEnv: NodeJS.ProcessEnv;
@@ -19,6 +23,10 @@ interface ProjectListMetadata {
19
23
  phase: string;
20
24
  phaseStatus: string;
21
25
  }
26
+ export declare function classifyLegacySyncedRecord(record: SyncedProjectRecord, path: string, probe?: SyncedTerminalRefProbe): Extract<ProjectListRow, {
27
+ kind: 'recorded-absent' | 'recorded-terminal' | 'terminal-invalid';
28
+ }>;
29
+ export declare function probeAuthoritativeCompletedRef(target: ReturnType<typeof buildSyncTarget>, git: GitRunner): Promise<SyncedTerminalRefProbe | null>;
22
30
  export declare function createProjectListCommand(overrides?: Partial<ProjectListDependencies>): Command;
23
31
  export {};
24
32
  //# sourceMappingURL=list.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/project/list.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAWlE,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAE5C,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAClE,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,mBAAmB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC3E,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAoXD,wBAAgB,wBAAwB,CACtC,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAwBT"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/project/list.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EACL,iBAAiB,EACjB,KAAK,mBAAmB,EACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,eAAe,EAEhB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,uBAAuB,EACvB,KAAK,sBAAsB,EAC5B,MAAM,uCAAuC,CAAC;AAY/C,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAE5C,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAClE,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,uBAAuB,EAAE,OAAO,uBAAuB,CAAC;IACxD,8BAA8B,EAAE,OAAO,8BAA8B,CAAC;IACtE,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,mBAAmB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC3E,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AA6KD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,mBAAmB,EAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,sBAAsB,GAC7B,OAAO,CACR,cAAc,EACd;IAAE,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,kBAAkB,CAAA;CAAE,CACvE,CA0FA;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,EAC1C,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAwBxC;AA8TD,wBAAgB,wBAAwB,CACtC,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAwBT"}