@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
@@ -1,8 +1,12 @@
1
1
  import { execFile as execFileCallback } from 'node:child_process';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
- import { lstat, readFile, realpath, rename, rm, writeFile, } from 'node:fs/promises';
3
+ import { lstat, readFile, readdir, realpath, rename, rm, writeFile, } from 'node:fs/promises';
4
4
  import { basename, dirname, isAbsolute, join, posix as path, relative, resolve, sep, } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
+ import { defaultGitRunner } from '../../project/sync/git.js';
7
+ import { readSyncedRecord, writeSyncedRecord, } from '../../project/sync/record.js';
8
+ import { buildSyncTarget, commitRecordChange, preflightSyncedCheckout, removeSyncedCheckout, } from '../../project/sync/ref-sync.js';
9
+ import { canonicalizePath, resolveProjectScope, resolveScopeRoot, syncedRecordPath, } from '../../shared/project-scope.js';
6
10
  import { CliError } from '../../../errors/cli-error.js';
7
11
  import { copyDirectory, copySingleFile, dirExists, ensureDir, fileExists, } from '../../../fs/io.js';
8
12
  import { loadExplainerPackageCoverage } from './explainer-package-coverage.js';
@@ -15,6 +19,19 @@ export const ARCHIVE_SNAPSHOT_METADATA_FILENAME = '.oat-archive-source.json';
15
19
  * (reviews, PR descriptions) rather than project deliverables.
16
20
  */
17
21
  export const S3_ARCHIVE_SYNC_EXCLUDES = ['reviews/*', 'pr/*'];
22
+ export function assertExactArchiveProjectRoot(options) {
23
+ const projectScope = resolveProjectScope(options.projectPath, resolveScopeRoot(options.repoRoot, options.projectsRoot, 'shared'), options.repoRoot);
24
+ if (!projectScope) {
25
+ throw new CliError(`Project path \`${options.projectPath}\` is outside the configured shared, local, and synced scope roots; refusing to archive without an originating scope.`, 1);
26
+ }
27
+ const scopeRoot = canonicalizePath(resolveScopeRoot(options.repoRoot, options.projectsRoot, projectScope));
28
+ const projectPath = canonicalizePath(options.projectPath);
29
+ if (dirname(projectPath) !== scopeRoot ||
30
+ basename(projectPath) !== options.projectName) {
31
+ throw new CliError(`Project path \`${options.projectPath}\` must identify the exact direct child \`${options.projectName}\` of the configured ${projectScope} project root; refusing to archive a descendant or mismatched project.`, 1);
32
+ }
33
+ return { canonicalProjectPath: projectPath, projectScope };
34
+ }
18
35
  function normalizeS3Uri(s3Uri) {
19
36
  return s3Uri.trim().replace(/\/+$/, '');
20
37
  }
@@ -220,6 +237,47 @@ async function resolveUniqueArchivePath(archivePath, dependencies) {
220
237
  const suffix = timestamp.replace(/[-:TZ.]/g, '').slice(0, 15);
221
238
  return `${archivePath}-${suffix}`;
222
239
  }
240
+ async function archiveMatchesSnapshot(archivePath, projectName, snapshotName, archiveScope, dependencies) {
241
+ const directoryExists = dependencies.dirExists ?? dirExists;
242
+ if (!(await directoryExists(archivePath))) {
243
+ return false;
244
+ }
245
+ try {
246
+ const metadata = JSON.parse(await readFile(join(archivePath, ARCHIVE_SNAPSHOT_METADATA_FILENAME), 'utf8'));
247
+ return (isRecord(metadata) &&
248
+ metadata.projectName === projectName &&
249
+ metadata.snapshotName === snapshotName &&
250
+ metadata.scope === archiveScope);
251
+ }
252
+ catch {
253
+ return false;
254
+ }
255
+ }
256
+ async function resolvePersistedArchivePath(archiveBasePath, projectName, snapshotName, archiveScope, dependencies) {
257
+ const archiveRoot = dirname(archiveBasePath);
258
+ const candidates = [archiveBasePath];
259
+ try {
260
+ const entries = await readdir(archiveRoot, { withFileTypes: true });
261
+ candidates.push(...entries
262
+ .filter((entry) => entry.isDirectory())
263
+ .map((entry) => join(archiveRoot, entry.name))
264
+ .filter((candidate) => candidate !== archiveBasePath)
265
+ .sort());
266
+ }
267
+ catch {
268
+ // The archive root can be absent before the first successful copy.
269
+ }
270
+ const matches = [];
271
+ for (const candidate of candidates) {
272
+ if (await archiveMatchesSnapshot(candidate, projectName, snapshotName, archiveScope, dependencies)) {
273
+ matches.push(candidate);
274
+ }
275
+ }
276
+ if (matches.length > 1) {
277
+ throw new CliError(`Persisted archive snapshot \`${snapshotName}\` resolves to multiple local archives; refusing an ambiguous retry.`);
278
+ }
279
+ return matches[0] ?? join(archiveRoot, snapshotName);
280
+ }
223
281
  function buildLocalOnlyArchiveWarning(projectName, archiveProjectPath, primaryRepoRoot) {
224
282
  const primaryMessage = primaryRepoRoot
225
283
  ? `the primary checkout \`${primaryRepoRoot}\` is unavailable`
@@ -251,10 +309,19 @@ export async function resolveArchiveProjectTarget(options, dependencies = {}) {
251
309
  }
252
310
  }
253
311
  const archiveBasePath = resolveCompletionArchivePath(archiveRepoRoot, archiveProjectPath);
254
- const archivePath = await resolveUniqueArchivePath(archiveBasePath, {
255
- dirExists: dependencies.dirExists,
256
- timestamp: dependencies.timestamp,
257
- });
312
+ let archivePath;
313
+ if (options.archiveSnapshot) {
314
+ if (!options.archiveScope) {
315
+ throw new CliError(`Persisted archive snapshot \`${options.archiveSnapshot}\` is missing its originating project scope; refusing an unscoped retry.`);
316
+ }
317
+ archivePath = await resolvePersistedArchivePath(archiveBasePath, options.projectName, options.archiveSnapshot, options.archiveScope, dependencies);
318
+ }
319
+ else {
320
+ archivePath = await resolveUniqueArchivePath(archiveBasePath, {
321
+ dirExists: dependencies.dirExists,
322
+ timestamp: dependencies.timestamp,
323
+ });
324
+ }
258
325
  return {
259
326
  archiveProjectPath,
260
327
  archiveRepoRoot,
@@ -273,6 +340,22 @@ export function assertDurableArchiveProjectTarget(target) {
273
340
  async function writeArchiveSnapshotMetadata(archivePath, metadata) {
274
341
  await writeFile(join(archivePath, ARCHIVE_SNAPSHOT_METADATA_FILENAME), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
275
342
  }
343
+ async function verifyArchiveSnapshotMetadata(archivePath, expected) {
344
+ let actual;
345
+ try {
346
+ actual = JSON.parse(await readFile(join(archivePath, ARCHIVE_SNAPSHOT_METADATA_FILENAME), 'utf8'));
347
+ }
348
+ catch {
349
+ throw new CliError(`Existing archive \`${archivePath}\` cannot be verified for retry; restore or remove it before retrying.`);
350
+ }
351
+ if (!isRecord(actual) ||
352
+ actual.projectName !== expected.projectName ||
353
+ actual.snapshotName !== expected.snapshotName ||
354
+ actual.scope !== expected.scope ||
355
+ actual.sourceRefSha !== expected.sourceRefSha) {
356
+ throw new CliError(`Existing archive \`${archivePath}\` does not match persisted snapshot \`${expected.snapshotName}\`; refusing to overwrite it.`);
357
+ }
358
+ }
276
359
  async function exportProjectSummary(archivePath, snapshotName, summaryExportPath, repoRoot, dependencies) {
277
360
  const summarySource = join(archivePath, 'summary.md');
278
361
  const exists = dependencies.fileExists ?? fileExists;
@@ -281,6 +364,16 @@ async function exportProjectSummary(archivePath, snapshotName, summaryExportPath
281
364
  }
282
365
  const summaryTarget = join(repoRoot, summaryExportPath, `${snapshotName}.md`);
283
366
  const copySummary = dependencies.copySingleFile ?? copySingleFile;
367
+ if (await pathExists(summaryTarget)) {
368
+ const [sourceContents, targetContents] = await Promise.all([
369
+ readFile(summarySource),
370
+ readFile(summaryTarget),
371
+ ]);
372
+ if (!sourceContents.equals(targetContents)) {
373
+ throw new CliError(`Existing summary export \`${summaryTarget}\` does not match persisted snapshot \`${snapshotName}\`; refusing to overwrite it.`);
374
+ }
375
+ return summaryTarget;
376
+ }
284
377
  await copySummary(summarySource, summaryTarget);
285
378
  return summaryTarget;
286
379
  }
@@ -659,7 +752,37 @@ async function exportSelectedProjectRecap(options, snapshotName, dependencies) {
659
752
  const { sourceRunRoot, manifestContents: sourceManifestContents, terminalEvidence: sourceTerminalEvidence, packagePaths, } = verified;
660
753
  const exportRoot = join(options.repoRoot, '.oat', 'repo', 'reference', 'project-recaps', snapshotName);
661
754
  if (await pathExists(exportRoot)) {
662
- throw new CliError(`Project recap export destination \`${exportRoot}\` already exists; refusing to overwrite it.`);
755
+ let exportedManifestContents;
756
+ try {
757
+ exportedManifestContents = await readFile(join(exportRoot, 'manifest.json'), 'utf8');
758
+ }
759
+ catch {
760
+ throw new CliError(`Project recap export destination \`${exportRoot}\` already exists and cannot be verified for retry.`);
761
+ }
762
+ if (exportedManifestContents !== sourceManifestContents) {
763
+ throw new CliError(`Existing project recap export \`${exportRoot}\` does not match persisted snapshot \`${snapshotName}\`.`);
764
+ }
765
+ const exportedManifest = await parseProjectRecapManifest(exportedManifestContents);
766
+ const verifiedArtifactCount = await verifyProjectRecapImmutableHashes(exportRoot, exportedManifest);
767
+ await verifyProjectRecapTerminalEvidence(exportRoot, exportedManifest, sourceTerminalEvidence ?? undefined);
768
+ const exactCoverage = (await loadExplainerPackageCoverage());
769
+ try {
770
+ await exactCoverage.enforceRunPackageInventory(exportRoot, exportedManifest, { includeTerminalEvidence: sourceTerminalEvidence !== null });
771
+ }
772
+ catch {
773
+ throw new CliError(`Existing project recap export \`${exportRoot}\` has invalid content.`);
774
+ }
775
+ return {
776
+ export: {
777
+ sourceRunRoot,
778
+ exportRoot,
779
+ manifest: {
780
+ relativePath: 'manifest.json',
781
+ verifiedArtifactCount,
782
+ },
783
+ },
784
+ createdByAttempt: false,
785
+ };
663
786
  }
664
787
  const temporaryRoot = `${exportRoot}.tmp-${randomUUID()}`;
665
788
  const makeDir = dependencies.ensureDir ?? ensureDir;
@@ -692,12 +815,15 @@ async function exportSelectedProjectRecap(options, snapshotName, dependencies) {
692
815
  }
693
816
  await renamePath(temporaryRoot, exportRoot);
694
817
  return {
695
- sourceRunRoot,
696
- exportRoot,
697
- manifest: {
698
- relativePath: 'manifest.json',
699
- verifiedArtifactCount,
818
+ export: {
819
+ sourceRunRoot,
820
+ exportRoot,
821
+ manifest: {
822
+ relativePath: 'manifest.json',
823
+ verifiedArtifactCount,
824
+ },
700
825
  },
826
+ createdByAttempt: true,
701
827
  };
702
828
  }
703
829
  catch (error) {
@@ -714,26 +840,127 @@ export async function archiveProjectOnCompletion(options, dependencies = {}) {
714
840
  const execFile = dependencies.execFile ?? execFileAsync;
715
841
  const timestamp = dependencies.timestamp?.() ?? new Date().toISOString();
716
842
  const snapshotName = buildArchiveSnapshotName(options.projectName, timestamp);
843
+ const syncedRoot = resolveScopeRoot(options.repoRoot, options.projectsRoot, 'synced');
844
+ const recordPath = syncedRecordPath(syncedRoot, options.projectName);
845
+ const readRecord = dependencies.readSyncedRecord ?? readSyncedRecord;
846
+ const writeRecord = dependencies.writeSyncedRecord ?? writeSyncedRecord;
847
+ const { canonicalProjectPath, projectScope } = assertExactArchiveProjectRoot(options);
848
+ const isSynced = projectScope === 'synced';
849
+ const record = isSynced ? await readRecord(recordPath) : null;
850
+ const syncTarget = isSynced
851
+ ? buildSyncTarget(options.repoRoot, options.projectsRoot, options.projectName)
852
+ : null;
853
+ const git = dependencies.gitRunner ?? defaultGitRunner;
854
+ let activeRecord = record;
855
+ let sourceRefSha;
856
+ if (syncTarget) {
857
+ if (!activeRecord) {
858
+ throw new CliError(`Synced project ${options.projectName} is missing its discovery record; restore the record or run oat project pull ${options.projectName} from a clean checkout to adopt the project before archiving.`, 2);
859
+ }
860
+ const preflight = await (dependencies.preflightSyncedCheckout ?? preflightSyncedCheckout)(syncTarget, git);
861
+ if (preflight.status !== 'clean' &&
862
+ !(preflight.status === 'absent' && activeRecord.status === 'complete')) {
863
+ const recoveryCommand = preflight.status === 'absent'
864
+ ? `oat project pull ${options.projectName}`
865
+ : `oat project push ${options.projectName}`;
866
+ throw new CliError(`Synced project ${options.projectName} is ${preflight.status}; run ${recoveryCommand} before archiving.`, 1);
867
+ }
868
+ sourceRefSha =
869
+ preflight.sha ??
870
+ (await git.run(['rev-parse', syncTarget.ref], { cwd: options.repoRoot }))
871
+ .stdout;
872
+ if (!/^[0-9a-f]{40}$/.test(sourceRefSha)) {
873
+ throw new CliError(`Unable to bind archive retry identity for ${options.projectName} to ${syncTarget.ref}.`, 2);
874
+ }
875
+ if (activeRecord.archiveSourceRefSha &&
876
+ activeRecord.archiveSourceRefSha !== sourceRefSha) {
877
+ throw new CliError(`Archive retry for ${options.projectName} is bound to source ref ${activeRecord.archiveSourceRefSha}, but ${syncTarget.ref} is now ${sourceRefSha}. Restore the retained ref to the recorded commit to finish this archive transaction, or preserve the existing snapshot and start a separately reviewed recovery; refusing to accept stale archive content.`, 1);
878
+ }
879
+ }
717
880
  const archiveTarget = await resolveArchiveProjectTarget({
718
881
  repoRoot: options.repoRoot,
719
882
  projectsRoot: options.projectsRoot,
720
883
  projectName: options.projectName,
884
+ ...(activeRecord?.archiveSnapshot
885
+ ? {
886
+ archiveSnapshot: activeRecord.archiveSnapshot,
887
+ archiveScope: projectScope,
888
+ }
889
+ : {}),
721
890
  }, dependencies);
722
891
  assertDurableArchiveProjectTarget(archiveTarget);
892
+ // Reassert the identity immediately before the first durable mutation. The
893
+ // archive transaction must never bind a descendant source to a sibling
894
+ // record/ref that merely shares its basename.
895
+ if (assertExactArchiveProjectRoot(options).canonicalProjectPath !==
896
+ canonicalProjectPath) {
897
+ throw new CliError(`Project path \`${options.projectPath}\` changed identity during archive preflight; refusing to mutate archive state.`, 1);
898
+ }
899
+ if (syncTarget &&
900
+ canonicalProjectPath !== canonicalizePath(syncTarget.projectPath)) {
901
+ throw new CliError(`Synced archive identity for ${options.projectName} does not match its canonical checkout, record, and ref.`, 1);
902
+ }
723
903
  const archivePath = archiveTarget.archivePath;
724
- const projectRecapExport = await exportSelectedProjectRecap(options, snapshotName, dependencies);
725
- try {
726
- await makeDir(dirname(archivePath));
727
- await copyProjectDirectory(options.projectPath, archivePath);
728
- await writeArchiveSnapshotMetadata(archivePath, {
904
+ const exportIdentity = syncTarget
905
+ ? (activeRecord?.archiveSnapshot ?? snapshotName)
906
+ : snapshotName;
907
+ const snapshotId = syncTarget ? exportIdentity : basename(archivePath);
908
+ const shouldPersistArchiveSnapshot = Boolean(syncTarget &&
909
+ activeRecord &&
910
+ (!activeRecord.archiveSnapshot || !activeRecord.archiveSourceRefSha));
911
+ if (shouldPersistArchiveSnapshot && activeRecord) {
912
+ activeRecord = {
913
+ ...activeRecord,
914
+ archiveSnapshot: exportIdentity,
915
+ archiveSourceRefSha: sourceRefSha,
916
+ };
917
+ await writeRecord(recordPath, activeRecord);
918
+ }
919
+ const archiveExists = await (dependencies.dirExists ?? dirExists)(archivePath);
920
+ if (archiveExists) {
921
+ await verifyArchiveSnapshotMetadata(archivePath, {
729
922
  projectName: options.projectName,
730
- snapshotName,
923
+ snapshotName: exportIdentity,
924
+ scope: projectScope,
925
+ ...(sourceRefSha ? { sourceRefSha } : {}),
731
926
  });
732
- await removePath(options.projectPath, { recursive: true, force: true });
927
+ }
928
+ const projectSourcePath = (await pathExists(options.projectPath))
929
+ ? options.projectPath
930
+ : archivePath;
931
+ const attemptedProjectRecapExport = await exportSelectedProjectRecap({ ...options, projectPath: projectSourcePath }, exportIdentity, dependencies);
932
+ const projectRecapExport = attemptedProjectRecapExport?.export ?? null;
933
+ if (!archiveExists) {
934
+ try {
935
+ await makeDir(dirname(archivePath));
936
+ await copyProjectDirectory(options.projectPath, archivePath, (_sourcePath, relativePath) => relativePath !== 'reviews' &&
937
+ (!syncTarget || relativePath !== '.git'));
938
+ await writeArchiveSnapshotMetadata(archivePath, {
939
+ projectName: options.projectName,
940
+ snapshotName: exportIdentity,
941
+ scope: projectScope,
942
+ ...(sourceRefSha ? { sourceRefSha } : {}),
943
+ });
944
+ }
945
+ catch (error) {
946
+ await removePath(archivePath, { recursive: true, force: true });
947
+ if (attemptedProjectRecapExport?.createdByAttempt) {
948
+ await removePath(attemptedProjectRecapExport.export.exportRoot, {
949
+ recursive: true,
950
+ force: true,
951
+ });
952
+ }
953
+ throw error;
954
+ }
955
+ }
956
+ try {
957
+ if (!syncTarget) {
958
+ await removePath(options.projectPath, { recursive: true, force: true });
959
+ }
733
960
  }
734
961
  catch (error) {
735
- if (projectRecapExport) {
736
- await removePath(projectRecapExport.exportRoot, {
962
+ if (attemptedProjectRecapExport?.createdByAttempt) {
963
+ await removePath(attemptedProjectRecapExport.export.exportRoot, {
737
964
  recursive: true,
738
965
  force: true,
739
966
  });
@@ -744,10 +971,19 @@ export async function archiveProjectOnCompletion(options, dependencies = {}) {
744
971
  let summaryExportFile = null;
745
972
  if (options.summaryExportPath) {
746
973
  try {
747
- summaryExportFile = await exportProjectSummary(archivePath, snapshotName, options.summaryExportPath, options.repoRoot, dependencies);
974
+ summaryExportFile = await exportProjectSummary(archivePath, exportIdentity, options.summaryExportPath, options.repoRoot, dependencies);
748
975
  }
749
976
  catch (error) {
750
977
  const message = error instanceof Error ? error.message : String(error);
978
+ if (syncTarget) {
979
+ if (attemptedProjectRecapExport?.createdByAttempt) {
980
+ await removePath(attemptedProjectRecapExport.export.exportRoot, {
981
+ recursive: true,
982
+ force: true,
983
+ });
984
+ }
985
+ throw new CliError(`Summary export to \`${options.summaryExportPath}\` failed: ${message}`, 1);
986
+ }
751
987
  warnings.push(`Summary export to \`${options.summaryExportPath}\` failed: ${message}`);
752
988
  }
753
989
  }
@@ -766,7 +1002,7 @@ export async function archiveProjectOnCompletion(options, dependencies = {}) {
766
1002
  warnings.push(...access.warnings);
767
1003
  if (access.ok) {
768
1004
  const remoteRepoRoot = await resolvePrimaryRepoRoot(options.repoRoot, dependencies);
769
- s3Path = buildProjectArchiveS3Uri(options.s3Uri, remoteRepoRoot, snapshotName);
1005
+ s3Path = buildProjectArchiveS3Uri(options.s3Uri, remoteRepoRoot, exportIdentity);
770
1006
  try {
771
1007
  const syncArgs = ['s3', 'sync', archivePath, s3Path];
772
1008
  for (const pattern of S3_ARCHIVE_SYNC_EXCLUDES) {
@@ -787,14 +1023,105 @@ export async function archiveProjectOnCompletion(options, dependencies = {}) {
787
1023
  }
788
1024
  }
789
1025
  }
1026
+ let lifecycleCommit = null;
1027
+ let recapExportPaths = [];
1028
+ if (syncTarget) {
1029
+ if (!activeRecord) {
1030
+ throw new CliError(`Synced project ${options.projectName} is missing its discovery record.`, 2);
1031
+ }
1032
+ if (projectRecapExport) {
1033
+ recapExportPaths = (await listArchiveExportFiles(projectRecapExport.exportRoot)).filter((filePath) => basename(filePath) !== 'manifest.json' &&
1034
+ basename(filePath) !== 'build-record.json');
1035
+ }
1036
+ activeRecord = {
1037
+ ...activeRecord,
1038
+ status: 'complete',
1039
+ completedAt: activeRecord.completedAt ?? timestamp,
1040
+ };
1041
+ await writeRecord(recordPath, activeRecord);
1042
+ if (options.commit !== false) {
1043
+ const pathspecs = [
1044
+ recordPath,
1045
+ ...(summaryExportFile ? [summaryExportFile] : []),
1046
+ ...recapExportPaths,
1047
+ ];
1048
+ const committed = await (dependencies.commitRecordChange ?? commitRecordChange)(options.repoRoot, pathspecs, `chore(oat): complete synced project ${options.projectName}`, git, {
1049
+ summaryExportPath: options.summaryExportPath,
1050
+ projectRoots: syncTarget,
1051
+ recapExportRoot: projectRecapExport?.exportRoot,
1052
+ });
1053
+ lifecycleCommit =
1054
+ committed?.sha ??
1055
+ (await recoverSyncedLifecycleCommit(options.repoRoot, pathspecs, activeRecord, `chore(oat): complete synced project ${options.projectName}`, git));
1056
+ }
1057
+ const removed = await (dependencies.removeSyncedCheckout ?? removeSyncedCheckout)(syncTarget, git);
1058
+ if (removed.status !== 'removed' && removed.status !== 'absent') {
1059
+ throw new CliError(`Synced checkout became ${removed.status} during archive; run oat project push ${options.projectName} and retry.`, 1);
1060
+ }
1061
+ }
790
1062
  return {
791
1063
  archivePath,
792
1064
  s3Path,
793
1065
  summaryExportFile,
794
1066
  projectRecapExport,
795
1067
  warnings,
1068
+ lifecycleCommit,
1069
+ recapExportPaths,
1070
+ snapshotId,
796
1071
  };
797
1072
  }
1073
+ async function recoverSyncedLifecycleCommit(repoRoot, pathspecs, record, expectedSubject, git) {
1074
+ const [recordPathspec] = pathspecs;
1075
+ if (!recordPathspec) {
1076
+ throw new CliError(`Unable to recover the prior lifecycle commit for ${record.slug}: no lifecycle paths were supplied.`, 2);
1077
+ }
1078
+ const recordPath = relative(resolve(repoRoot), resolve(recordPathspec))
1079
+ .split(sep)
1080
+ .join('/');
1081
+ const normalizedPathspecs = pathspecs
1082
+ .map((pathspec) => relative(resolve(repoRoot), resolve(pathspec)))
1083
+ .map((pathspec) => pathspec.split(sep).join('/'))
1084
+ .sort();
1085
+ const candidate = (await git.run(['log', '-1', '--format=%H', '--', recordPath], {
1086
+ cwd: repoRoot,
1087
+ })).stdout;
1088
+ if (!/^[0-9a-f]{40}$/.test(candidate)) {
1089
+ throw new CliError(`Unable to recover the prior lifecycle commit for ${record.slug}.`, 2);
1090
+ }
1091
+ const subject = (await git.run(['show', '-s', '--format=%s', candidate], { cwd: repoRoot })).stdout;
1092
+ const ancestor = await git.run(['merge-base', '--is-ancestor', candidate, 'HEAD'], { cwd: repoRoot, allowFailure: true });
1093
+ const changedPaths = (await git.run(['diff-tree', '--no-commit-id', '--name-only', '-r', candidate], { cwd: repoRoot })).stdout
1094
+ .split('\n')
1095
+ .filter(Boolean)
1096
+ .sort();
1097
+ const contentsMatch = await git.run(['diff', '--quiet', candidate, '--', ...normalizedPathspecs], { cwd: repoRoot, allowFailure: true });
1098
+ const committedRecord = await git.run(['show', `${candidate}:${recordPath}`], { cwd: repoRoot });
1099
+ let parsedRecord;
1100
+ try {
1101
+ parsedRecord = JSON.parse(committedRecord.stdout);
1102
+ }
1103
+ catch {
1104
+ parsedRecord = null;
1105
+ }
1106
+ if (subject !== expectedSubject ||
1107
+ ancestor.code !== 0 ||
1108
+ contentsMatch.code !== 0 ||
1109
+ JSON.stringify(changedPaths) !== JSON.stringify(normalizedPathspecs) ||
1110
+ JSON.stringify(parsedRecord) !== JSON.stringify(record)) {
1111
+ throw new CliError(`Unable to recover lifecycle commit ${candidate} for ${record.slug}: subject, path set, contents, or branch relationship did not match the completed archive transaction.`, 2);
1112
+ }
1113
+ return candidate;
1114
+ }
1115
+ async function listArchiveExportFiles(root, current = root) {
1116
+ const entries = await readdir(current, { withFileTypes: true });
1117
+ const files = await Promise.all(entries.map(async (entry) => {
1118
+ const entryPath = join(current, entry.name);
1119
+ return entry.isDirectory()
1120
+ ? listArchiveExportFiles(root, entryPath)
1121
+ : [entryPath];
1122
+ }));
1123
+ return files.flat().sort();
1124
+ }
798
1125
  export async function ensureS3ArchiveAccess(options, dependencies = {}) {
799
1126
  if (!requiresRemoteAccess(options)) {
800
1127
  return { ok: true, warnings: [] };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/archive/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAIL,KAAK,qCAAqC,EAC3C,MAAM,eAAe,CAAC;AACvB,OAAO,EAIL,KAAK,iCAAiC,EACvC,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,kBAAkB,EAClB,iCAAiC,GAClC,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,kBAAkB,EAClB,qCAAqC,GACtC,MAAM,eAAe,CAAC;AAEvB,KAAK,0BAA0B,GAAG,iCAAiC,GACjE,qCAAqC,CAAC;AAKxC,wBAAgB,2BAA2B,CACzC,SAAS,GAAE,OAAO,CAAC,0BAA0B,CAAM,GAClD,OAAO,CA6ET"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/archive/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAIL,KAAK,qCAAqC,EAC3C,MAAM,eAAe,CAAC;AACvB,OAAO,EAIL,KAAK,iCAAiC,EACvC,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,kBAAkB,EAClB,iCAAiC,GAClC,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,kBAAkB,EAClB,qCAAqC,GACtC,MAAM,eAAe,CAAC;AAEvB,KAAK,0BAA0B,GAAG,iCAAiC,GACjE,qCAAqC,CAAC;AAKxC,wBAAgB,2BAA2B,CACzC,SAAS,GAAE,OAAO,CAAC,0BAA0B,CAAM,GAClD,OAAO,CAiFT"}
@@ -13,6 +13,7 @@ export function createProjectArchiveCommand(overrides = {}) {
13
13
  .description('Manage archived project data')
14
14
  .argument('[project-path]', 'Project path to archive')
15
15
  .option('--dry-run', 'Preview archive without moving files or syncing S3')
16
+ .option('--no-commit', 'Manual/library use only: omit the synced lifecycle commit receipt')
16
17
  .option('--project-recap-run <path>', 'Export the selected project-relative recap run before archiving')
17
18
  .addHelpText('afterAll', '\nPull archived project data with `oat repo archive sync [project-name]`; `oat project archive sync` is deprecated.')
18
19
  .action(async (projectPath, options, command) => {
@@ -1,9 +1,11 @@
1
1
  import { buildCommandContext, type CommandContext } from '../../../app/command-context.js';
2
+ import { readSyncedRecord } from '../../project/sync/record.js';
2
3
  import { type OatConfig, type OatLocalConfig } from '../../../config/oat-config.js';
3
- import { resolveArchiveProjectTarget, resolvePrimaryRepoRoot, verifySelectedProjectRecapForArchive, type ArchiveProjectOnCompletionOptions, type ArchiveProjectOnCompletionResult } from './archive-utils.js';
4
+ import { assertExactArchiveProjectRoot, resolveArchiveProjectTarget, resolvePrimaryRepoRoot, verifySelectedProjectRecapForArchive, type ArchiveProjectOnCompletionOptions, type ArchiveProjectOnCompletionResult } from './archive-utils.js';
4
5
  export interface ArchivePushOptions {
5
6
  dryRun?: boolean;
6
7
  projectRecapRun?: string;
8
+ commit?: boolean;
7
9
  }
8
10
  export interface ProjectArchivePushCommandDependencies {
9
11
  buildCommandContext: (options: Parameters<typeof buildCommandContext>[0]) => CommandContext;
@@ -13,8 +15,10 @@ export interface ProjectArchivePushCommandDependencies {
13
15
  resolveProjectsRoot: (repoRoot: string, env: NodeJS.ProcessEnv) => Promise<string>;
14
16
  resolvePrimaryRepoRoot: typeof resolvePrimaryRepoRoot;
15
17
  resolveArchiveProjectTarget: typeof resolveArchiveProjectTarget;
18
+ readSyncedRecord: typeof readSyncedRecord;
16
19
  verifySelectedProjectRecapForArchive: typeof verifySelectedProjectRecapForArchive;
17
20
  archiveProjectOnCompletion: (options: ArchiveProjectOnCompletionOptions) => Promise<ArchiveProjectOnCompletionResult>;
21
+ assertExactArchiveProjectRoot: typeof assertExactArchiveProjectRoot;
18
22
  processEnv: NodeJS.ProcessEnv;
19
23
  timestamp: () => string;
20
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"push-runner.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/archive/push-runner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAKL,2BAA2B,EAC3B,sBAAsB,EACtB,oCAAoC,EAEpC,KAAK,iCAAiC,EACtC,KAAK,gCAAgC,EAEtC,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,qCAAqC;IACpD,mBAAmB,EAAE,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAC/C,cAAc,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,2BAA2B,EAAE,OAAO,2BAA2B,CAAC;IAChE,oCAAoC,EAAE,OAAO,oCAAoC,CAAC;IAClF,0BAA0B,EAAE,CAC1B,OAAO,EAAE,iCAAiC,KACvC,OAAO,CAAC,gCAAgC,CAAC,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,SAAS,EAAE,MAAM,MAAM,CAAC;CACzB;AAmBD,wBAAgB,4CAA4C,IAAI,qCAAqC,CAcpG;AAkJD,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,qCAAqC,EACnD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CA0Ef"}
1
+ {"version":3,"file":"push-runner.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/archive/push-runner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAOjE,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAEL,6BAA6B,EAI7B,2BAA2B,EAC3B,sBAAsB,EACtB,oCAAoC,EAEpC,KAAK,iCAAiC,EACtC,KAAK,gCAAgC,EAEtC,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,qCAAqC;IACpD,mBAAmB,EAAE,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAC/C,cAAc,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,2BAA2B,EAAE,OAAO,2BAA2B,CAAC;IAChE,gBAAgB,EAAE,OAAO,gBAAgB,CAAC;IAC1C,oCAAoC,EAAE,OAAO,oCAAoC,CAAC;IAClF,0BAA0B,EAAE,CAC1B,OAAO,EAAE,iCAAiC,KACvC,OAAO,CAAC,gCAAgC,CAAC,CAAC;IAC/C,6BAA6B,EAAE,OAAO,6BAA6B,CAAC;IACpE,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;IAC9B,SAAS,EAAE,MAAM,MAAM,CAAC;CACzB;AAsBD,wBAAgB,4CAA4C,IAAI,qCAAqC,CAgBpG;AAuJD,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,qCAAqC,EACnD,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAyGf"}
@@ -1,10 +1,12 @@
1
1
  import { basename, isAbsolute, join } from 'node:path';
2
2
  import { buildCommandContext } from '../../../app/command-context.js';
3
+ import { readSyncedRecord } from '../../project/sync/record.js';
3
4
  import { resolveProjectsRoot } from '../../shared/oat-paths.js';
5
+ import { resolveScopeRoot, syncedRecordPath, } from '../../shared/project-scope.js';
4
6
  import { readOatConfig, readOatLocalConfig, } from '../../../config/oat-config.js';
5
7
  import { CliError } from '../../../errors/cli-error.js';
6
8
  import { resolveProjectRoot } from '../../../fs/paths.js';
7
- import { archiveProjectOnCompletion, assertDurableArchiveProjectTarget, buildArchiveSnapshotName, buildProjectArchiveS3Uri, resolveArchiveProjectTarget, resolvePrimaryRepoRoot, verifySelectedProjectRecapForArchive, } from './archive-utils.js';
9
+ import { archiveProjectOnCompletion, assertExactArchiveProjectRoot, assertDurableArchiveProjectTarget, buildArchiveSnapshotName, buildProjectArchiveS3Uri, resolveArchiveProjectTarget, resolvePrimaryRepoRoot, verifySelectedProjectRecapForArchive, } from './archive-utils.js';
8
10
  export function defaultProjectArchivePushCommandDependencies() {
9
11
  return {
10
12
  buildCommandContext,
@@ -14,8 +16,10 @@ export function defaultProjectArchivePushCommandDependencies() {
14
16
  resolveProjectsRoot,
15
17
  resolvePrimaryRepoRoot,
16
18
  resolveArchiveProjectTarget,
19
+ readSyncedRecord,
17
20
  verifySelectedProjectRecapForArchive,
18
21
  archiveProjectOnCompletion,
22
+ assertExactArchiveProjectRoot,
19
23
  processEnv: process.env,
20
24
  timestamp: () => new Date().toISOString(),
21
25
  };
@@ -53,10 +57,10 @@ function buildArchiveOptions(repoRoot, config, projectsRoot, target, options) {
53
57
  projectRecapRun: options.projectRecapRun,
54
58
  awsProfile: config.archive?.awsProfile,
55
59
  awsRegion: config.archive?.awsRegion,
60
+ ...(options.commit === false ? { commit: false } : {}),
56
61
  };
57
62
  }
58
- async function buildDryRunReport(dependencies, repoRoot, config, target, archiveTarget) {
59
- const snapshotName = buildArchiveSnapshotName(target.projectName, dependencies.timestamp());
63
+ async function buildDryRunReport(dependencies, repoRoot, config, target, archiveTarget, snapshotName, projectScope) {
60
64
  const remoteRepoRoot = await dependencies.resolvePrimaryRepoRoot(repoRoot);
61
65
  const s3Path = config.archive?.s3Uri && config.archive.s3SyncOnComplete === true
62
66
  ? buildProjectArchiveS3Uri(config.archive.s3Uri, remoteRepoRoot, snapshotName)
@@ -73,6 +77,11 @@ async function buildDryRunReport(dependencies, repoRoot, config, target, archive
73
77
  s3Path,
74
78
  summaryExportFile,
75
79
  warnings: [],
80
+ lifecycleCommit: null,
81
+ recapExportPaths: [],
82
+ snapshotId: projectScope === 'synced'
83
+ ? snapshotName
84
+ : basename(archiveTarget.archivePath),
76
85
  };
77
86
  }
78
87
  function emitArchivePushText(report, summaryExportPath, logger) {
@@ -113,10 +122,27 @@ export async function runArchivePushCommand(dependencies, projectPathArg, option
113
122
  const config = await dependencies.readOatConfig(repoRoot);
114
123
  const projectsRoot = await dependencies.resolveProjectsRoot(repoRoot, dependencies.processEnv);
115
124
  const target = await resolveArchiveTarget(dependencies, repoRoot, projectPathArg);
125
+ const { projectScope } = dependencies.assertExactArchiveProjectRoot({
126
+ repoRoot,
127
+ projectPath: target.projectPath,
128
+ projectName: target.projectName,
129
+ projectsRoot,
130
+ });
131
+ const defaultSnapshotName = buildArchiveSnapshotName(target.projectName, dependencies.timestamp());
132
+ const syncedRecord = projectScope === 'synced'
133
+ ? await dependencies.readSyncedRecord(syncedRecordPath(resolveScopeRoot(repoRoot, projectsRoot, 'synced'), target.projectName))
134
+ : null;
135
+ const snapshotName = syncedRecord?.archiveSnapshot ?? defaultSnapshotName;
116
136
  const archiveTarget = await dependencies.resolveArchiveProjectTarget({
117
137
  repoRoot,
118
138
  projectsRoot,
119
139
  projectName: target.projectName,
140
+ ...(syncedRecord?.archiveSnapshot
141
+ ? {
142
+ archiveSnapshot: syncedRecord.archiveSnapshot,
143
+ archiveScope: projectScope ?? undefined,
144
+ }
145
+ : {}),
120
146
  }, {
121
147
  env: dependencies.processEnv,
122
148
  timestamp: dependencies.timestamp,
@@ -127,7 +153,7 @@ export async function runArchivePushCommand(dependencies, projectPathArg, option
127
153
  }
128
154
  const dryRun = options.dryRun === true || context.dryRun;
129
155
  if (dryRun) {
130
- const report = await buildDryRunReport(dependencies, repoRoot, config, target, archiveTarget);
156
+ const report = await buildDryRunReport(dependencies, repoRoot, config, target, archiveTarget, snapshotName, projectScope);
131
157
  emitArchivePushReport(report, config.archive?.summaryExportPath, context);
132
158
  process.exitCode = 0;
133
159
  return;
@@ -145,6 +171,9 @@ export async function runArchivePushCommand(dependencies, projectPathArg, option
145
171
  ? { projectRecapExport: result.projectRecapExport }
146
172
  : {}),
147
173
  warnings: result.warnings,
174
+ lifecycleCommit: result.lifecycleCommit,
175
+ recapExportPaths: result.recapExportPaths,
176
+ snapshotId: result.snapshotId,
148
177
  };
149
178
  emitArchivePushReport(report, config.archive?.summaryExportPath, context);
150
179
  process.exitCode = 0;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/complete-state/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,IAAI,eAAe,EAC3B,SAAS,IAAI,gBAAgB,EAC9B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGhF,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAG/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,UAAU,gCAAgC;IACxC,mBAAmB,EAAE,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAC/C,cAAc,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,EAAE,OAAO,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,gBAAgB,CAAC;IACnC,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AA0ED,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,gCAAgC,CAAM,GACxD,OAAO,CA2BT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/complete-state/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,IAAI,eAAe,EAC3B,SAAS,IAAI,gBAAgB,EAC9B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGhF,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAG/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,UAAU,gCAAgC;IACxC,mBAAmB,EAAE,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAC/C,cAAc,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,EAAE,OAAO,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,gBAAgB,CAAC;IACnC,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AA4ED,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,gCAAgC,CAAM,GACxD,OAAO,CA2BT"}
@@ -42,7 +42,9 @@ async function runProjectCompleteState(projectPath, options, context, dependenci
42
42
  filePath: statePath,
43
43
  projectPath: targetProjectPath,
44
44
  });
45
- await dependencies.writeFile(statePath, updatedContent, 'utf8');
45
+ if (updatedContent !== content) {
46
+ await dependencies.writeFile(statePath, updatedContent, 'utf8');
47
+ }
46
48
  if (context.json) {
47
49
  context.logger.json({
48
50
  status: 'ok',
@@ -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;AAgBpC,wBAAgB,oBAAoB,IAAI,OAAO,CAgB9C"}
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"}