@open-agent-toolkit/cli 0.2.41 → 0.2.46

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 (212) hide show
  1. package/assets/agents/oat-phase-implementer.md +11 -1
  2. package/assets/bundle-metadata.json +1 -1
  3. package/assets/docs/cli-utilities/configuration.md +11 -1
  4. package/assets/docs/cli-utilities/tool-packs.md +55 -9
  5. package/assets/docs/quickstart.md +9 -0
  6. package/assets/docs/reference/cli-reference.md +56 -1
  7. package/assets/docs/reference/file-locations.md +18 -1
  8. package/assets/docs/reference/oat-directory-structure.md +20 -4
  9. package/assets/docs/reference/troubleshooting.md +18 -11
  10. package/assets/docs/workflows/projects/artifacts.md +27 -1
  11. package/assets/docs/workflows/projects/implementation-execution.md +22 -0
  12. package/assets/docs/workflows/projects/index.md +6 -0
  13. package/assets/docs/workflows/projects/lifecycle.md +19 -5
  14. package/assets/docs/workflows/projects/picking-up-projects.md +75 -0
  15. package/assets/docs/workflows/projects/pr-flow.md +22 -1
  16. package/assets/docs/workflows/projects/reviewing-oat-prs.md +60 -0
  17. package/assets/public-package-versions.json +4 -4
  18. package/assets/skills/oat-brainstorm/SKILL.md +135 -32
  19. package/assets/skills/oat-brainstorm/references/destinations.md +9 -6
  20. package/assets/skills/oat-cursor-cloud-projects/SKILL.md +11 -1
  21. package/assets/skills/oat-doctor/SKILL.md +8 -1
  22. package/assets/skills/oat-project-autonomous/SKILL.md +45 -1
  23. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +8 -8
  24. package/assets/skills/oat-project-capture/SKILL.md +29 -9
  25. package/assets/skills/oat-project-complete/SKILL.md +610 -38
  26. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +265 -0
  27. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +764 -0
  28. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +134 -0
  29. package/assets/skills/oat-project-complete/scripts/validate-nonarchive-lifecycle-receipt.mjs +160 -0
  30. package/assets/skills/oat-project-design/SKILL.md +33 -9
  31. package/assets/skills/oat-project-discover/SKILL.md +16 -3
  32. package/assets/skills/oat-project-document/SKILL.md +10 -4
  33. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +8 -8
  34. package/assets/skills/oat-project-implement/SKILL.md +14 -3
  35. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +18 -6
  36. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +8 -8
  37. package/assets/skills/oat-project-implement/references/phase-execution.md +9 -3
  38. package/assets/skills/oat-project-implement/references/plan-and-resume.md +3 -0
  39. package/assets/skills/oat-project-import-plan/SKILL.md +15 -9
  40. package/assets/skills/oat-project-new/SKILL.md +7 -5
  41. package/assets/skills/oat-project-next/SKILL.md +32 -10
  42. package/assets/skills/oat-project-plan/SKILL.md +18 -3
  43. package/assets/skills/oat-project-pr-final/SKILL.md +23 -2
  44. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +8 -8
  45. package/assets/skills/oat-project-pr-progress/SKILL.md +23 -2
  46. package/assets/skills/oat-project-progress/SKILL.md +23 -9
  47. package/assets/skills/oat-project-promote-spec-driven/SKILL.md +20 -2
  48. package/assets/skills/oat-project-quick-start/SKILL.md +48 -18
  49. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +8 -8
  50. package/assets/skills/oat-project-reconcile/SKILL.md +14 -10
  51. package/assets/skills/oat-project-retro/SKILL.md +3 -3
  52. package/assets/skills/oat-project-retro/references/apply-procedure.md +102 -6
  53. package/assets/skills/oat-project-retro-file/SKILL.md +57 -6
  54. package/assets/skills/oat-project-review-provide/SKILL.md +45 -4
  55. package/assets/skills/oat-project-review-receive/SKILL.md +16 -7
  56. package/assets/skills/oat-project-review-receive-remote/SKILL.md +9 -3
  57. package/assets/skills/oat-project-revise/SKILL.md +12 -3
  58. package/assets/skills/oat-project-spec/SKILL.md +16 -3
  59. package/assets/skills/oat-project-summary/SKILL.md +113 -13
  60. package/assets/skills/oat-wave-execute/SKILL.md +34 -2
  61. package/assets/skills/oat-worktree-bootstrap/SKILL.md +13 -1
  62. package/assets/skills/oat-worktree-bootstrap-auto/SKILL.md +13 -1
  63. package/dist/commands/config/index.d.ts +1 -0
  64. package/dist/commands/config/index.d.ts.map +1 -1
  65. package/dist/commands/config/index.js +34 -3
  66. package/dist/commands/doctor/index.d.ts +2 -0
  67. package/dist/commands/doctor/index.d.ts.map +1 -1
  68. package/dist/commands/doctor/index.js +27 -3
  69. package/dist/commands/doctor/synced-projects.d.ts +12 -0
  70. package/dist/commands/doctor/synced-projects.d.ts.map +1 -0
  71. package/dist/commands/doctor/synced-projects.js +208 -0
  72. package/dist/commands/init/gitattributes.d.ts +4 -0
  73. package/dist/commands/init/gitattributes.d.ts.map +1 -0
  74. package/dist/commands/init/gitattributes.js +12 -0
  75. package/dist/commands/init/gitignore.d.ts +8 -1
  76. package/dist/commands/init/gitignore.d.ts.map +1 -1
  77. package/dist/commands/init/gitignore.js +146 -41
  78. package/dist/commands/init/index.d.ts +2 -0
  79. package/dist/commands/init/index.d.ts.map +1 -1
  80. package/dist/commands/init/index.js +3 -0
  81. package/dist/commands/init/managed-block.d.ts +10 -0
  82. package/dist/commands/init/managed-block.d.ts.map +1 -0
  83. package/dist/commands/init/managed-block.js +40 -0
  84. package/dist/commands/init/tools/index.d.ts +1 -0
  85. package/dist/commands/init/tools/index.d.ts.map +1 -1
  86. package/dist/commands/init/tools/index.js +37 -9
  87. package/dist/commands/init/tools/workflows/install-workflows.js +1 -1
  88. package/dist/commands/local/index.d.ts.map +1 -1
  89. package/dist/commands/local/index.js +5 -1
  90. package/dist/commands/local/sync.d.ts +4 -0
  91. package/dist/commands/local/sync.d.ts.map +1 -1
  92. package/dist/commands/local/sync.js +123 -2
  93. package/dist/commands/pjm/adoption.d.ts +2 -0
  94. package/dist/commands/pjm/adoption.d.ts.map +1 -1
  95. package/dist/commands/pjm/adoption.js +1 -1
  96. package/dist/commands/project/archive/archive-utils.d.ts +24 -0
  97. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  98. package/dist/commands/project/archive/archive-utils.js +349 -22
  99. package/dist/commands/project/archive/index.d.ts.map +1 -1
  100. package/dist/commands/project/archive/index.js +1 -0
  101. package/dist/commands/project/archive/push-runner.d.ts +5 -1
  102. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  103. package/dist/commands/project/archive/push-runner.js +33 -4
  104. package/dist/commands/project/complete-state/index.d.ts.map +1 -1
  105. package/dist/commands/project/complete-state/index.js +3 -1
  106. package/dist/commands/project/index.d.ts.map +1 -1
  107. package/dist/commands/project/index.js +12 -0
  108. package/dist/commands/project/links/compute.d.ts +9 -0
  109. package/dist/commands/project/links/compute.d.ts.map +1 -0
  110. package/dist/commands/project/links/compute.js +27 -0
  111. package/dist/commands/project/links/index.d.ts +17 -0
  112. package/dist/commands/project/links/index.d.ts.map +1 -0
  113. package/dist/commands/project/links/index.js +72 -0
  114. package/dist/commands/project/links/refresh.d.ts +26 -0
  115. package/dist/commands/project/links/refresh.d.ts.map +1 -0
  116. package/dist/commands/project/links/refresh.js +92 -0
  117. package/dist/commands/project/links/render.d.ts +24 -0
  118. package/dist/commands/project/links/render.d.ts.map +1 -0
  119. package/dist/commands/project/links/render.js +68 -0
  120. package/dist/commands/project/list.d.ts +5 -0
  121. package/dist/commands/project/list.d.ts.map +1 -1
  122. package/dist/commands/project/list.js +182 -9
  123. package/dist/commands/project/migrate/index.d.ts +22 -0
  124. package/dist/commands/project/migrate/index.d.ts.map +1 -0
  125. package/dist/commands/project/migrate/index.js +79 -0
  126. package/dist/commands/project/new/index.d.ts +2 -0
  127. package/dist/commands/project/new/index.d.ts.map +1 -1
  128. package/dist/commands/project/new/index.js +14 -2
  129. package/dist/commands/project/new/scaffold.d.ts +22 -1
  130. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  131. package/dist/commands/project/new/scaffold.js +287 -69
  132. package/dist/commands/project/open/index.d.ts +16 -0
  133. package/dist/commands/project/open/index.d.ts.map +1 -1
  134. package/dist/commands/project/open/index.js +151 -19
  135. package/dist/commands/project/pause/index.d.ts +8 -0
  136. package/dist/commands/project/pause/index.d.ts.map +1 -1
  137. package/dist/commands/project/pause/index.js +129 -19
  138. package/dist/commands/project/prune/index.d.ts +21 -0
  139. package/dist/commands/project/prune/index.d.ts.map +1 -0
  140. package/dist/commands/project/prune/index.js +95 -0
  141. package/dist/commands/project/pull/index.d.ts +28 -0
  142. package/dist/commands/project/pull/index.d.ts.map +1 -0
  143. package/dist/commands/project/pull/index.js +157 -0
  144. package/dist/commands/project/push/index.d.ts +22 -0
  145. package/dist/commands/project/push/index.d.ts.map +1 -0
  146. package/dist/commands/project/push/index.js +100 -0
  147. package/dist/commands/project/scope/index.d.ts +17 -0
  148. package/dist/commands/project/scope/index.d.ts.map +1 -0
  149. package/dist/commands/project/scope/index.js +107 -0
  150. package/dist/commands/project/split/run.d.ts +4 -0
  151. package/dist/commands/project/split/run.d.ts.map +1 -1
  152. package/dist/commands/project/split/run.js +48 -10
  153. package/dist/commands/project/sync/git.d.ts +18 -0
  154. package/dist/commands/project/sync/git.d.ts.map +1 -0
  155. package/dist/commands/project/sync/git.js +60 -0
  156. package/dist/commands/project/sync/record.d.ts +22 -0
  157. package/dist/commands/project/sync/record.d.ts.map +1 -0
  158. package/dist/commands/project/sync/record.js +133 -0
  159. package/dist/commands/project/sync/ref-sync.d.ts +120 -0
  160. package/dist/commands/project/sync/ref-sync.d.ts.map +1 -0
  161. package/dist/commands/project/sync/ref-sync.js +962 -0
  162. package/dist/commands/project/sync/resolve-target.d.ts +27 -0
  163. package/dist/commands/project/sync/resolve-target.d.ts.map +1 -0
  164. package/dist/commands/project/sync/resolve-target.js +159 -0
  165. package/dist/commands/shared/oat-paths.d.ts +4 -1
  166. package/dist/commands/shared/oat-paths.d.ts.map +1 -1
  167. package/dist/commands/shared/oat-paths.js +2 -2
  168. package/dist/commands/shared/project-scope.d.ts +23 -0
  169. package/dist/commands/shared/project-scope.d.ts.map +1 -0
  170. package/dist/commands/shared/project-scope.js +106 -0
  171. package/dist/commands/tools/migrate/index.js +1 -1
  172. package/dist/commands/tools/shared/pack-inventory.d.ts.map +1 -1
  173. package/dist/commands/tools/shared/pack-inventory.js +79 -8
  174. package/dist/commands/tools/shared/pack-lifecycle.js +1 -1
  175. package/dist/commands/tools/shared/pack-reconcile.d.ts.map +1 -1
  176. package/dist/commands/tools/shared/pack-reconcile.js +4 -1
  177. package/dist/commands/tools/shared/project-tools-config.d.ts +6 -2
  178. package/dist/commands/tools/shared/project-tools-config.d.ts.map +1 -1
  179. package/dist/commands/tools/shared/project-tools-config.js +7 -5
  180. package/dist/commands/tools/update/index.d.ts.map +1 -1
  181. package/dist/commands/tools/update/index.js +38 -14
  182. package/dist/commands/tools/update/update-tools.d.ts +2 -0
  183. package/dist/commands/tools/update/update-tools.d.ts.map +1 -1
  184. package/dist/config/oat-config.d.ts +6 -3
  185. package/dist/config/oat-config.d.ts.map +1 -1
  186. package/dist/config/oat-config.js +41 -7
  187. package/dist/config/resolve.d.ts.map +1 -1
  188. package/dist/config/resolve.js +2 -1
  189. package/dist/fs/io.d.ts +2 -1
  190. package/dist/fs/io.d.ts.map +1 -1
  191. package/dist/fs/io.js +5 -2
  192. package/dist/projects/split/finalize.d.ts.map +1 -1
  193. package/dist/projects/split/finalize.js +55 -2
  194. package/dist/projects/split/resume.d.ts +3 -0
  195. package/dist/projects/split/resume.d.ts.map +1 -1
  196. package/dist/projects/split/resume.js +34 -7
  197. package/dist/projects/split/seed-children.d.ts.map +1 -1
  198. package/dist/projects/split/seed-children.js +6 -0
  199. package/dist/projects/split/write-parent.d.ts +7 -0
  200. package/dist/projects/split/write-parent.d.ts.map +1 -1
  201. package/dist/projects/split/write-parent.js +7 -4
  202. package/dist/release/public-package-contract.d.ts.map +1 -1
  203. package/dist/release/public-package-contract.js +1 -0
  204. package/dist/validation/skills.d.ts.map +1 -1
  205. package/dist/validation/skills.js +434 -0
  206. package/package.json +4 -4
  207. package/dist/__tests__/skills/split-flow-fixtures.d.ts +0 -50
  208. package/dist/__tests__/skills/split-flow-fixtures.d.ts.map +0 -1
  209. package/dist/__tests__/skills/split-flow-fixtures.js +0 -161
  210. package/dist/commands/__tests__/helpers.d.ts +0 -12
  211. package/dist/commands/__tests__/helpers.d.ts.map +0 -1
  212. package/dist/commands/__tests__/helpers.js +0 -36
@@ -0,0 +1,764 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { basename } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { promisify } from 'node:util';
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ const PIN_SOURCE_MESSAGE = 'chore(oat): finalize project lifecycle';
9
+ const FINAL_ARTIFACT_MESSAGE = 'chore(oat): publish final project links';
10
+ const EVIDENCE_MESSAGE = 'chore(oat): attest final project recap';
11
+ const LINKS_START = '<!-- oat:project-links:start -->';
12
+ const LINKS_END = '<!-- oat:project-links:end -->';
13
+ const FULL_SHA = /^[0-9a-f]{40}$/;
14
+ const UTC_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
15
+ const COMPLETION_SEAL =
16
+ /(?:^|\n)### (\d{4}-\d{2}-\d{2}) · structural · oat-project-complete · seal\n\nCompletion sealed at (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z); project-log roll-up status: ok\.\s*$/;
17
+
18
+ function completionReceiptError(message) {
19
+ const error = new Error(message);
20
+ error.code = 'E_COMPLETION_RECEIPTS';
21
+ return error;
22
+ }
23
+
24
+ function requireRelativeGitPath(value, label) {
25
+ if (
26
+ typeof value !== 'string' ||
27
+ value.length === 0 ||
28
+ value.startsWith('/') ||
29
+ value.startsWith('-') ||
30
+ value.includes('\\') ||
31
+ value.includes(':') ||
32
+ value
33
+ .split('/')
34
+ .some((part) => part === '' || part === '.' || part === '..')
35
+ ) {
36
+ throw completionReceiptError(
37
+ `${label} must be a normalized relative Git path.`,
38
+ );
39
+ }
40
+ return value;
41
+ }
42
+
43
+ async function git(projectPath, args, options = {}) {
44
+ try {
45
+ const result = await execFileAsync('git', args, {
46
+ cwd: projectPath,
47
+ encoding: 'utf8',
48
+ maxBuffer: 10 * 1024 * 1024,
49
+ });
50
+ return result.stdout.trim();
51
+ } catch (error) {
52
+ if (options.allowFailure) return null;
53
+ const detail = error?.stderr?.trim() || error?.message || String(error);
54
+ throw completionReceiptError(
55
+ `git ${args[0] ?? 'command'} failed while recovering completion receipts: ${detail}`,
56
+ );
57
+ }
58
+ }
59
+
60
+ async function commitSubject(projectPath, commit) {
61
+ return git(projectPath, ['show', '-s', '--format=%s', commit]);
62
+ }
63
+
64
+ async function singleParent(projectPath, commit, label) {
65
+ const line = await git(projectPath, [
66
+ 'rev-list',
67
+ '--parents',
68
+ '-n',
69
+ '1',
70
+ commit,
71
+ ]);
72
+ const parts = line.split(/\s+/);
73
+ if (
74
+ parts.length !== 2 ||
75
+ parts[0] !== commit ||
76
+ !FULL_SHA.test(parts[1] ?? '')
77
+ ) {
78
+ throw completionReceiptError(`${label} must have exactly one parent.`);
79
+ }
80
+ return parts[1];
81
+ }
82
+
83
+ async function changedPaths(projectPath, commit) {
84
+ const output = await git(projectPath, [
85
+ 'diff-tree',
86
+ '--no-commit-id',
87
+ '--name-only',
88
+ '-r',
89
+ commit,
90
+ ]);
91
+ return output === '' ? [] : output.split('\n').sort();
92
+ }
93
+
94
+ async function committedFile(projectPath, commit, path, label) {
95
+ const content = await git(projectPath, ['show', `${commit}:${path}`], {
96
+ allowFailure: true,
97
+ });
98
+ if (content === null) {
99
+ throw completionReceiptError(
100
+ `Recovered pin-source receipt is missing required ${label} ${path}.`,
101
+ );
102
+ }
103
+ return content;
104
+ }
105
+
106
+ async function committedFileIfPresent(projectPath, commit, path, label) {
107
+ const matchedPath = await git(projectPath, [
108
+ 'ls-tree',
109
+ '--name-only',
110
+ commit,
111
+ '--',
112
+ path,
113
+ ]);
114
+ if (matchedPath === '') {
115
+ return null;
116
+ }
117
+ if (matchedPath !== path) {
118
+ throw completionReceiptError(
119
+ `Recovered pin-source receipt resolved unexpected ${label} path ${JSON.stringify(matchedPath)}.`,
120
+ );
121
+ }
122
+ return committedFile(projectPath, commit, path, label);
123
+ }
124
+
125
+ function frontmatterScalar(content, field) {
126
+ const frontmatter = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content)?.[1];
127
+ if (frontmatter === undefined) {
128
+ throw completionReceiptError(
129
+ 'Recovered pin-source state.md is missing canonical frontmatter.',
130
+ );
131
+ }
132
+ const matcher = new RegExp(`^${field}:\\s*([^\\n]*?)(?:\\s+#.*)?$`, 'gm');
133
+ const matches = [...frontmatter.matchAll(matcher)];
134
+ if (matches.length !== 1) {
135
+ throw completionReceiptError(
136
+ `Recovered pin-source state.md must contain exactly one ${field} field.`,
137
+ );
138
+ }
139
+ const raw = matches[0][1]?.trim() ?? '';
140
+ const value =
141
+ raw.length >= 2 &&
142
+ ((raw.startsWith('"') && raw.endsWith('"')) ||
143
+ (raw.startsWith("'") && raw.endsWith("'")))
144
+ ? raw.slice(1, -1)
145
+ : raw;
146
+ if (value.length === 0) {
147
+ throw completionReceiptError(
148
+ `Recovered pin-source state.md has an empty ${field} field.`,
149
+ );
150
+ }
151
+ return value;
152
+ }
153
+
154
+ function requireCompletedLifecycleState(content) {
155
+ const lifecycle = frontmatterScalar(content, 'oat_lifecycle');
156
+ if (lifecycle !== 'complete') {
157
+ throw completionReceiptError(
158
+ 'Recovered pin-source state.md must have oat_lifecycle: complete.',
159
+ );
160
+ }
161
+ const completedAt = frontmatterScalar(content, 'oat_project_completed');
162
+ const updatedAt = frontmatterScalar(content, 'oat_project_state_updated');
163
+ if (
164
+ !UTC_TIMESTAMP.test(completedAt) ||
165
+ !UTC_TIMESTAMP.test(updatedAt) ||
166
+ Number.isNaN(Date.parse(completedAt)) ||
167
+ Number.isNaN(Date.parse(updatedAt))
168
+ ) {
169
+ throw completionReceiptError(
170
+ 'Recovered pin-source state.md completion timestamps must be valid UTC timestamps.',
171
+ );
172
+ }
173
+ if (completedAt !== updatedAt) {
174
+ throw completionReceiptError(
175
+ 'Recovered pin-source state.md completion timestamps must identify the same lifecycle mutation.',
176
+ );
177
+ }
178
+ }
179
+
180
+ function requireCompletionSeal(content) {
181
+ const seal = COMPLETION_SEAL.exec(content);
182
+ if (!seal || seal[1] !== seal[2]?.slice(0, 10)) {
183
+ throw completionReceiptError(
184
+ 'Recovered pin-source project-log.md must end with the canonical oat-project-complete seal.',
185
+ );
186
+ }
187
+ }
188
+
189
+ function requireExactPaths(actual, expected, label) {
190
+ const normalizedExpected = [...expected].sort();
191
+ if (
192
+ actual.length !== normalizedExpected.length ||
193
+ actual.some((path, index) => path !== normalizedExpected[index])
194
+ ) {
195
+ throw completionReceiptError(
196
+ `${label} changed ${JSON.stringify(actual)}; expected exactly ${JSON.stringify(normalizedExpected)}.`,
197
+ );
198
+ }
199
+ }
200
+
201
+ async function retainedRemoteCommit(projectPath, remote, retainedRef) {
202
+ const output = await git(projectPath, [
203
+ 'ls-remote',
204
+ '--refs',
205
+ remote,
206
+ retainedRef,
207
+ ]);
208
+ const lines = output === '' ? [] : output.split('\n');
209
+ if (lines.length !== 1) {
210
+ throw completionReceiptError(
211
+ `Retained remote ref ${retainedRef} must resolve exactly once on ${remote}.`,
212
+ );
213
+ }
214
+ const [sha, resolvedRef, ...extra] = lines[0].split(/\s+/);
215
+ if (
216
+ !FULL_SHA.test(sha ?? '') ||
217
+ resolvedRef !== retainedRef ||
218
+ extra.length > 0
219
+ ) {
220
+ throw completionReceiptError(
221
+ `Retained remote ref ${retainedRef} returned a malformed receipt.`,
222
+ );
223
+ }
224
+ return sha;
225
+ }
226
+
227
+ function parseGitHubRepository(url) {
228
+ const scp = /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/.exec(url);
229
+ if (scp) return `${scp[1]}/${scp[2]}`;
230
+ try {
231
+ const parsed = new URL(url);
232
+ if (parsed.hostname !== 'github.com') return null;
233
+ const parts = parsed.pathname.replace(/^\/+|\/+$/g, '').split('/');
234
+ if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
235
+ return `${parts[0]}/${parts[1].replace(/\.git$/, '')}`;
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+
241
+ function requireSingleLinksBlock(
242
+ content,
243
+ retainedRef,
244
+ pinSourceCommit,
245
+ expectedSlug,
246
+ expectedRepository,
247
+ ) {
248
+ const starts = content.split(LINKS_START).length - 1;
249
+ const ends = content.split(LINKS_END).length - 1;
250
+ const start = content.indexOf(LINKS_START);
251
+ const end = content.indexOf(LINKS_END);
252
+ if (starts !== 1 || ends !== 1 || start < 0 || end <= start) {
253
+ throw completionReceiptError(
254
+ 'Final PR artifact must contain exactly one well-ordered project links block.',
255
+ );
256
+ }
257
+ const block = content.slice(start, end + LINKS_END.length);
258
+ const headerMarkers = block.split('**OAT project**').length - 1;
259
+ const header = block
260
+ .split('\n')
261
+ .find((line) => line.includes('**OAT project**'));
262
+ const headerMatch =
263
+ /^\*\*OAT project\*\* `([A-Za-z0-9._-]+)` \(synced\) — pinned to `([^`]+)` @ `([0-9a-f]{7})` \(([^)\r\n]+)\)$/.exec(
264
+ header ?? '',
265
+ );
266
+ if (headerMarkers !== 1 || !headerMatch) {
267
+ throw completionReceiptError(
268
+ 'Final PR artifact must contain exactly one canonical project links header.',
269
+ );
270
+ }
271
+ if (headerMatch[1] !== expectedSlug) {
272
+ throw completionReceiptError(
273
+ `Final PR artifact links header must name project ${expectedSlug}.`,
274
+ );
275
+ }
276
+ if (headerMatch[2] !== retainedRef) {
277
+ throw completionReceiptError(
278
+ `Final PR artifact links header must name retained ref ${retainedRef}.`,
279
+ );
280
+ }
281
+ if (headerMatch[3] !== pinSourceCommit.slice(0, 7)) {
282
+ throw completionReceiptError(
283
+ `Final PR artifact links block must be pinned to ${pinSourceCommit}.`,
284
+ );
285
+ }
286
+
287
+ const blobMarkers = block.split('/blob/').length - 1;
288
+ const blobLinks = [
289
+ ...block.matchAll(
290
+ /https:\/\/github\.com\/([^/\s)]+)\/([^/\s)]+)\/blob\/([^/\s)]+)\//g,
291
+ ),
292
+ ];
293
+ const blobPins = blobLinks.map((match) => match[3]);
294
+ if (blobPins.length !== blobMarkers) {
295
+ throw completionReceiptError(
296
+ 'Final PR artifact links block contains a malformed blob URL.',
297
+ );
298
+ }
299
+ if (
300
+ blobPins.some(
301
+ (commit) => !FULL_SHA.test(commit ?? '') || commit !== pinSourceCommit,
302
+ )
303
+ ) {
304
+ throw completionReceiptError(
305
+ `Final PR artifact blob links must use pin-source commit ${pinSourceCommit}.`,
306
+ );
307
+ }
308
+ if (blobLinks.length > 0) {
309
+ if (expectedRepository === null) {
310
+ throw completionReceiptError(
311
+ 'Final PR artifact must not contain GitHub blob links for a non-GitHub project remote.',
312
+ );
313
+ }
314
+ if (
315
+ blobLinks.some(
316
+ (match) => `${match[1]}/${match[2]}` !== expectedRepository,
317
+ )
318
+ ) {
319
+ throw completionReceiptError(
320
+ `Final PR artifact blob links must name repository ${expectedRepository}.`,
321
+ );
322
+ }
323
+ }
324
+ }
325
+
326
+ export function resolveCompletionArchiveDecision({
327
+ configuredPreference,
328
+ interactiveAnswer,
329
+ localNonArchive,
330
+ }) {
331
+ const suppliedSources = [
332
+ configuredPreference !== undefined && configuredPreference !== null,
333
+ interactiveAnswer !== undefined && interactiveAnswer !== null,
334
+ localNonArchive !== undefined && localNonArchive !== null,
335
+ ].filter(Boolean).length;
336
+ if (suppliedSources !== 1) {
337
+ throw completionReceiptError(
338
+ 'Completion archive decision requires exactly one decision source.',
339
+ );
340
+ }
341
+ if (localNonArchive === true) {
342
+ return { shouldArchive: false, source: 'local-default' };
343
+ }
344
+ if (localNonArchive !== undefined && localNonArchive !== null) {
345
+ throw completionReceiptError(
346
+ 'Local non-archive decision must be explicitly true.',
347
+ );
348
+ }
349
+ if (typeof configuredPreference === 'boolean') {
350
+ return { shouldArchive: configuredPreference, source: 'configured' };
351
+ }
352
+ if (configuredPreference !== undefined && configuredPreference !== null) {
353
+ throw completionReceiptError(
354
+ 'Configured archive preference must be true, false, or unset.',
355
+ );
356
+ }
357
+ if (typeof interactiveAnswer === 'boolean') {
358
+ return { shouldArchive: interactiveAnswer, source: 'interactive' };
359
+ }
360
+ throw completionReceiptError(
361
+ 'An unset archive preference requires an explicit interactive answer.',
362
+ );
363
+ }
364
+
365
+ export async function detectCompletionReceiptCandidate({
366
+ projectPath,
367
+ retainedRef,
368
+ }) {
369
+ if (typeof projectPath !== 'string' || projectPath.length === 0) {
370
+ throw completionReceiptError('Project path is required.');
371
+ }
372
+ if (
373
+ typeof retainedRef !== 'string' ||
374
+ !/^refs\/oat\/projects\/[A-Za-z0-9._-]+$/.test(retainedRef)
375
+ ) {
376
+ throw completionReceiptError(
377
+ 'Retained ref must be a canonical OAT project ref.',
378
+ );
379
+ }
380
+ const receiptSubjects = new Set([FINAL_ARTIFACT_MESSAGE, EVIDENCE_MESSAGE]);
381
+ const head = await git(projectPath, ['rev-parse', 'HEAD']);
382
+ const retained = await git(
383
+ projectPath,
384
+ ['rev-parse', '--verify', retainedRef],
385
+ { allowFailure: true },
386
+ );
387
+ const headSubject = await commitSubject(projectPath, head);
388
+ const retainedSubject = FULL_SHA.test(retained ?? '')
389
+ ? await commitSubject(projectPath, retained)
390
+ : null;
391
+ const receiptCandidate =
392
+ receiptSubjects.has(headSubject) ||
393
+ (retainedSubject !== null && receiptSubjects.has(retainedSubject));
394
+ const pinSourceCandidate =
395
+ headSubject === PIN_SOURCE_MESSAGE ||
396
+ retainedSubject === PIN_SOURCE_MESSAGE;
397
+ const candidate = receiptCandidate || pinSourceCandidate;
398
+ if (candidate) {
399
+ const status = await git(projectPath, [
400
+ 'status',
401
+ '--porcelain=v1',
402
+ '--untracked-files=all',
403
+ ]);
404
+ if (status !== '') {
405
+ throw completionReceiptError(
406
+ 'Recognized completion receipt candidate requires a clean synced checkout.',
407
+ );
408
+ }
409
+ }
410
+ return {
411
+ status: candidate ? 'candidate' : 'none',
412
+ candidate,
413
+ candidateType: receiptCandidate
414
+ ? 'receipt'
415
+ : pinSourceCandidate
416
+ ? 'pin-source'
417
+ : null,
418
+ };
419
+ }
420
+
421
+ export async function recoverCompletionPinSource({
422
+ projectPath,
423
+ retainedRef,
424
+ prArtifactPath,
425
+ remote = 'origin',
426
+ }) {
427
+ if (typeof projectPath !== 'string' || projectPath.length === 0) {
428
+ throw completionReceiptError('Project path is required.');
429
+ }
430
+ if (
431
+ typeof retainedRef !== 'string' ||
432
+ !/^refs\/oat\/projects\/[A-Za-z0-9._-]+$/.test(retainedRef)
433
+ ) {
434
+ throw completionReceiptError(
435
+ 'Retained ref must be a canonical OAT project ref.',
436
+ );
437
+ }
438
+ const finalArtifactPath = requireRelativeGitPath(
439
+ prArtifactPath,
440
+ 'PR artifact path',
441
+ );
442
+ const expectedSlug = retainedRef.slice('refs/oat/projects/'.length);
443
+ if (basename(projectPath) !== expectedSlug) {
444
+ throw completionReceiptError(
445
+ `Project checkout basename must match retained project ${expectedSlug}.`,
446
+ );
447
+ }
448
+ const status = await git(projectPath, [
449
+ 'status',
450
+ '--porcelain=v1',
451
+ '--untracked-files=all',
452
+ ]);
453
+ if (status !== '') {
454
+ throw completionReceiptError(
455
+ 'Recognized pin-source candidate requires a clean synced checkout.',
456
+ );
457
+ }
458
+ const localCommit = await git(projectPath, ['rev-parse', 'HEAD']);
459
+ const localRetainedRefCommit = await git(
460
+ projectPath,
461
+ ['rev-parse', '--verify', retainedRef],
462
+ { allowFailure: true },
463
+ );
464
+ const remoteCommit = await retainedRemoteCommit(
465
+ projectPath,
466
+ remote,
467
+ retainedRef,
468
+ );
469
+ if (
470
+ localCommit !== localRetainedRefCommit ||
471
+ localCommit !== remoteCommit ||
472
+ (await commitSubject(projectPath, localCommit)) !== PIN_SOURCE_MESSAGE
473
+ ) {
474
+ throw completionReceiptError(
475
+ 'Pin-source retry requires equal checkout, local retained ref, and remote retained ref commits with the exact pin-source subject.',
476
+ );
477
+ }
478
+ requireCompletedLifecycleState(
479
+ await committedFile(
480
+ projectPath,
481
+ localCommit,
482
+ 'state.md',
483
+ 'lifecycle state',
484
+ ),
485
+ );
486
+ const pinSourceLog = await committedFileIfPresent(
487
+ projectPath,
488
+ localCommit,
489
+ 'project-log.md',
490
+ 'completion log',
491
+ );
492
+ if (pinSourceLog !== null) {
493
+ requireCompletionSeal(pinSourceLog);
494
+ }
495
+ await committedFile(
496
+ projectPath,
497
+ localCommit,
498
+ finalArtifactPath,
499
+ 'PR artifact',
500
+ );
501
+ return {
502
+ status: 'recovered',
503
+ retainedRef,
504
+ localCommit,
505
+ remoteCommit,
506
+ projectLinksPinCommit: localCommit,
507
+ };
508
+ }
509
+
510
+ export async function recoverCompletionReceipts({
511
+ projectPath,
512
+ retainedRef,
513
+ prArtifactPath,
514
+ evidencePaths = [],
515
+ remote = 'origin',
516
+ }) {
517
+ if (typeof projectPath !== 'string' || projectPath.length === 0) {
518
+ throw completionReceiptError('Project path is required.');
519
+ }
520
+ if (
521
+ typeof retainedRef !== 'string' ||
522
+ !/^refs\/oat\/projects\/[A-Za-z0-9._-]+$/.test(retainedRef)
523
+ ) {
524
+ throw completionReceiptError(
525
+ 'Retained ref must be a canonical OAT project ref.',
526
+ );
527
+ }
528
+ const finalArtifactPath = requireRelativeGitPath(
529
+ prArtifactPath,
530
+ 'PR artifact path',
531
+ );
532
+ const expectedSlug = retainedRef.slice('refs/oat/projects/'.length);
533
+ if (basename(projectPath) !== expectedSlug) {
534
+ throw completionReceiptError(
535
+ `Project checkout basename must match retained project ${expectedSlug}.`,
536
+ );
537
+ }
538
+ const exactEvidencePaths = evidencePaths.map((path, index) =>
539
+ requireRelativeGitPath(path, `Evidence path ${index + 1}`),
540
+ );
541
+ if (new Set(exactEvidencePaths).size !== exactEvidencePaths.length) {
542
+ throw completionReceiptError('Evidence paths must be unique.');
543
+ }
544
+
545
+ const status = await git(projectPath, [
546
+ 'status',
547
+ '--porcelain=v1',
548
+ '--untracked-files=all',
549
+ ]);
550
+ if (status !== '') {
551
+ throw completionReceiptError(
552
+ 'Synced project checkout must be clean before receipt recovery.',
553
+ );
554
+ }
555
+ const localCommit = await git(projectPath, ['rev-parse', 'HEAD']);
556
+ const localRetainedRefCommit = await git(
557
+ projectPath,
558
+ ['rev-parse', '--verify', retainedRef],
559
+ { allowFailure: true },
560
+ );
561
+ if (!FULL_SHA.test(localRetainedRefCommit ?? '')) {
562
+ throw completionReceiptError(
563
+ `Synced project checkout must retain the local ref ${retainedRef}.`,
564
+ );
565
+ }
566
+
567
+ const remoteCommit = await retainedRemoteCommit(
568
+ projectPath,
569
+ remote,
570
+ retainedRef,
571
+ );
572
+ const remoteUrl = await git(projectPath, [
573
+ 'config',
574
+ '--get',
575
+ `remote.${remote}.url`,
576
+ ]);
577
+ const expectedRepository = parseGitHubRepository(remoteUrl);
578
+ const localSubject = await commitSubject(projectPath, localCommit);
579
+
580
+ let finalArtifactCommit;
581
+ let evidenceCommit = null;
582
+ let evidencePushRequired = false;
583
+
584
+ if (localSubject === FINAL_ARTIFACT_MESSAGE) {
585
+ if (
586
+ localCommit !== remoteCommit ||
587
+ localRetainedRefCommit !== localCommit
588
+ ) {
589
+ throw completionReceiptError(
590
+ 'Final-artifact receipt recovery requires equal checkout, local retained ref, and remote retained ref commits.',
591
+ );
592
+ }
593
+ finalArtifactCommit = localCommit;
594
+ } else if (localSubject === EVIDENCE_MESSAGE) {
595
+ if (exactEvidencePaths.length === 0) {
596
+ throw completionReceiptError(
597
+ 'Recap-evidence receipt recovery requires exact evidence paths.',
598
+ );
599
+ }
600
+ evidenceCommit = localCommit;
601
+ requireExactPaths(
602
+ await changedPaths(projectPath, evidenceCommit),
603
+ exactEvidencePaths,
604
+ 'Recap evidence commit',
605
+ );
606
+ finalArtifactCommit = await singleParent(
607
+ projectPath,
608
+ evidenceCommit,
609
+ 'Recap evidence commit',
610
+ );
611
+ if (remoteCommit === finalArtifactCommit) {
612
+ if (localRetainedRefCommit !== finalArtifactCommit) {
613
+ throw completionReceiptError(
614
+ 'Unpublished recap evidence requires the local retained ref to remain at the final-artifact parent.',
615
+ );
616
+ }
617
+ evidencePushRequired = true;
618
+ } else if (remoteCommit === evidenceCommit) {
619
+ if (localRetainedRefCommit !== evidenceCommit) {
620
+ throw completionReceiptError(
621
+ 'Published recap evidence requires equal local and remote retained refs.',
622
+ );
623
+ }
624
+ } else {
625
+ throw completionReceiptError(
626
+ 'Recap-evidence recovery requires the remote retained ref to equal either the evidence commit or its final-artifact parent.',
627
+ );
628
+ }
629
+ } else {
630
+ throw completionReceiptError(
631
+ `HEAD subject ${JSON.stringify(localSubject)} is not an exact completion receipt.`,
632
+ );
633
+ }
634
+
635
+ if (
636
+ (await commitSubject(projectPath, finalArtifactCommit)) !==
637
+ FINAL_ARTIFACT_MESSAGE
638
+ ) {
639
+ throw completionReceiptError(
640
+ 'Recovered final-artifact receipt has an unexpected commit subject.',
641
+ );
642
+ }
643
+ requireExactPaths(
644
+ await changedPaths(projectPath, finalArtifactCommit),
645
+ [finalArtifactPath],
646
+ 'Final-artifact commit',
647
+ );
648
+ const projectLinksPinCommit = await singleParent(
649
+ projectPath,
650
+ finalArtifactCommit,
651
+ 'Final-artifact commit',
652
+ );
653
+ if (
654
+ (await commitSubject(projectPath, projectLinksPinCommit)) !==
655
+ PIN_SOURCE_MESSAGE
656
+ ) {
657
+ throw completionReceiptError(
658
+ 'Recovered pin-source receipt has an unexpected commit subject.',
659
+ );
660
+ }
661
+ const pinSourceState = await committedFile(
662
+ projectPath,
663
+ projectLinksPinCommit,
664
+ 'state.md',
665
+ 'lifecycle state',
666
+ );
667
+ requireCompletedLifecycleState(pinSourceState);
668
+ const pinSourceLog = await committedFileIfPresent(
669
+ projectPath,
670
+ projectLinksPinCommit,
671
+ 'project-log.md',
672
+ 'completion log',
673
+ );
674
+ if (pinSourceLog !== null) {
675
+ requireCompletionSeal(pinSourceLog);
676
+ }
677
+ const finalArtifact = await git(projectPath, [
678
+ 'show',
679
+ `${finalArtifactCommit}:${finalArtifactPath}`,
680
+ ]);
681
+ requireSingleLinksBlock(
682
+ finalArtifact,
683
+ retainedRef,
684
+ projectLinksPinCommit,
685
+ expectedSlug,
686
+ expectedRepository,
687
+ );
688
+
689
+ return {
690
+ status: 'recovered',
691
+ retainedRef,
692
+ localCommit,
693
+ remoteCommit,
694
+ projectLinksPinCommit,
695
+ projectRefCommit: finalArtifactCommit,
696
+ evidenceCommit,
697
+ evidencePushRequired,
698
+ };
699
+ }
700
+
701
+ function parseBoolean(value, label) {
702
+ if (value === 'true') return true;
703
+ if (value === 'false') return false;
704
+ throw completionReceiptError(`${label} must be true or false.`);
705
+ }
706
+
707
+ function parseArguments(argv) {
708
+ const result = { evidencePaths: [] };
709
+ for (let index = 0; index < argv.length; index += 2) {
710
+ const flag = argv[index];
711
+ const value = argv[index + 1];
712
+ if (value === undefined) {
713
+ throw completionReceiptError(`Missing value for ${flag ?? 'argument'}.`);
714
+ }
715
+ if (flag === '--project-path') result.projectPath = value;
716
+ else if (flag === '--retained-ref') result.retainedRef = value;
717
+ else if (flag === '--pr-artifact') result.prArtifactPath = value;
718
+ else if (flag === '--evidence-path') result.evidencePaths.push(value);
719
+ else if (flag === '--remote') result.remote = value;
720
+ else if (flag === '--detect-candidate') {
721
+ result.detectCandidate = parseBoolean(value, flag);
722
+ } else if (flag === '--archive-preference') {
723
+ result.configuredPreference = parseBoolean(value, flag);
724
+ } else if (flag === '--interactive-archive') {
725
+ result.interactiveAnswer = parseBoolean(value, flag);
726
+ } else if (flag === '--local-nonarchive') {
727
+ result.localNonArchive = parseBoolean(value, flag);
728
+ } else {
729
+ throw completionReceiptError(`Unsupported argument: ${flag}.`);
730
+ }
731
+ }
732
+ return result;
733
+ }
734
+
735
+ async function main(argv) {
736
+ const options = parseArguments(argv);
737
+ if (
738
+ options.configuredPreference !== undefined ||
739
+ options.interactiveAnswer !== undefined ||
740
+ options.localNonArchive !== undefined
741
+ ) {
742
+ return resolveCompletionArchiveDecision(options);
743
+ }
744
+ if (options.detectCandidate === true) {
745
+ return detectCompletionReceiptCandidate(options);
746
+ }
747
+ return recoverCompletionReceipts(options);
748
+ }
749
+
750
+ if (
751
+ process.argv[1] &&
752
+ import.meta.url === pathToFileURL(process.argv[1]).href
753
+ ) {
754
+ main(process.argv.slice(2))
755
+ .then((result) => {
756
+ process.stdout.write(`${JSON.stringify(result)}\n`);
757
+ })
758
+ .catch((error) => {
759
+ process.stderr.write(
760
+ `${JSON.stringify({ ok: false, code: error.code, message: error.message })}\n`,
761
+ );
762
+ process.exitCode = 1;
763
+ });
764
+ }