@6reduk/workspace-pipeline 0.1.0

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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/docs/collaboration.md +59 -0
  4. package/docs/config-fields.md +65 -0
  5. package/docs/contracts.md +149 -0
  6. package/docs/doctor.md +165 -0
  7. package/docs/launch.md +50 -0
  8. package/docs/lifecycle-cli.md +141 -0
  9. package/docs/lifecycle.md +42 -0
  10. package/docs/migrations/unity.md +288 -0
  11. package/docs/native-provider-format.md +149 -0
  12. package/docs/provider-bundles.md +81 -0
  13. package/docs/provider-resources.md +37 -0
  14. package/docs/release.md +25 -0
  15. package/docs/remove.md +70 -0
  16. package/docs/repair.md +80 -0
  17. package/docs/repositories.md +209 -0
  18. package/docs/repository-manual-recovery.md +102 -0
  19. package/docs/repository-observations.md +29 -0
  20. package/docs/repository-recovery.md +204 -0
  21. package/docs/repository-retention.md +46 -0
  22. package/docs/repository-transport-budgets.md +26 -0
  23. package/docs/retention.md +237 -0
  24. package/docs/source.md +50 -0
  25. package/docs/switch.md +412 -0
  26. package/package.json +39 -0
  27. package/schemas/common.schema.json +251 -0
  28. package/schemas/inventory.schema.json +15 -0
  29. package/schemas/operation.schema.json +286 -0
  30. package/schemas/pipeline.schema.json +317 -0
  31. package/schemas/state.schema.json +302 -0
  32. package/schemas/workspace.schema.json +67 -0
  33. package/src/cli.js +7 -0
  34. package/src/commands/adopt.js +2 -0
  35. package/src/commands/bootstrap-recovery.js +90 -0
  36. package/src/commands/dispatch.js +394 -0
  37. package/src/commands/init.js +84 -0
  38. package/src/commands/launch.js +69 -0
  39. package/src/commands/migration-apply.js +53 -0
  40. package/src/commands/migration.js +50 -0
  41. package/src/commands/repositories.js +61 -0
  42. package/src/commands/repository-abandon.js +16 -0
  43. package/src/commands/repository-ancestors.js +21 -0
  44. package/src/commands/repository-locks.js +77 -0
  45. package/src/contracts/parse.js +57 -0
  46. package/src/contracts/semantic.js +240 -0
  47. package/src/contracts/validate.js +21 -0
  48. package/src/launch/grok.js +20 -0
  49. package/src/migrations/legacy-unity-begin.js +35 -0
  50. package/src/migrations/legacy-unity-compensate.js +82 -0
  51. package/src/migrations/legacy-unity-deactivate.js +60 -0
  52. package/src/migrations/legacy-unity-deactivation-resume-apply.js +62 -0
  53. package/src/migrations/legacy-unity-deactivation-resume.js +81 -0
  54. package/src/migrations/legacy-unity-finalize.js +88 -0
  55. package/src/migrations/legacy-unity-install-recovery.js +76 -0
  56. package/src/migrations/legacy-unity-install-resume.js +60 -0
  57. package/src/migrations/legacy-unity-install.js +88 -0
  58. package/src/migrations/legacy-unity-lease.js +136 -0
  59. package/src/migrations/legacy-unity-preflight.js +67 -0
  60. package/src/migrations/legacy-unity-preview.js +91 -0
  61. package/src/migrations/legacy-unity-resume-apply.js +39 -0
  62. package/src/migrations/legacy-unity-resume.js +41 -0
  63. package/src/migrations/legacy-unity-resumed-evidence.js +88 -0
  64. package/src/migrations/legacy-unity.js +99 -0
  65. package/src/operations/apply.js +576 -0
  66. package/src/operations/backup.js +94 -0
  67. package/src/operations/bootstrap-lock.js +87 -0
  68. package/src/operations/bootstrap-owner-retirement.js +121 -0
  69. package/src/operations/bundle-update.js +54 -0
  70. package/src/operations/config-fields.js +24 -0
  71. package/src/operations/continuation-lifecycle.js +77 -0
  72. package/src/operations/doctor.js +214 -0
  73. package/src/operations/history.js +170 -0
  74. package/src/operations/installer-identity.js +49 -0
  75. package/src/operations/journal.js +142 -0
  76. package/src/operations/lifecycle.js +133 -0
  77. package/src/operations/lineage-guard.js +20 -0
  78. package/src/operations/lock.js +109 -0
  79. package/src/operations/maintenance.js +62 -0
  80. package/src/operations/migration-pending.js +22 -0
  81. package/src/operations/ownership.js +122 -0
  82. package/src/operations/plan.js +278 -0
  83. package/src/operations/reconciliation.js +112 -0
  84. package/src/operations/recovery-lease.js +66 -0
  85. package/src/operations/remove.js +140 -0
  86. package/src/operations/repair.js +188 -0
  87. package/src/operations/repository-abandon.js +192 -0
  88. package/src/operations/repository-ancestors.js +158 -0
  89. package/src/operations/repository-apply.js +122 -0
  90. package/src/operations/repository-authorization.js +48 -0
  91. package/src/operations/repository-bootstrap-continuation.js +190 -0
  92. package/src/operations/repository-bootstrap-reconcile.js +106 -0
  93. package/src/operations/repository-bootstrap-recover.js +114 -0
  94. package/src/operations/repository-bootstrap.js +82 -0
  95. package/src/operations/repository-clone.js +63 -0
  96. package/src/operations/repository-history.js +108 -0
  97. package/src/operations/repository-inputs.js +41 -0
  98. package/src/operations/repository-journal.js +127 -0
  99. package/src/operations/repository-lock-reconcile.js +401 -0
  100. package/src/operations/repository-pending.js +29 -0
  101. package/src/operations/repository-reconcile.js +182 -0
  102. package/src/operations/repository-resumption-approvals.js +77 -0
  103. package/src/operations/repository-retention-apply.js +75 -0
  104. package/src/operations/repository-retention.js +137 -0
  105. package/src/operations/repository-workspace.js +78 -0
  106. package/src/operations/retention-apply.js +133 -0
  107. package/src/operations/retention-combined-scan.js +30 -0
  108. package/src/operations/retention-combined.js +41 -0
  109. package/src/operations/retention-policy.js +65 -0
  110. package/src/operations/retention-receipts.js +126 -0
  111. package/src/operations/retention-scan.js +86 -0
  112. package/src/operations/retention.js +56 -0
  113. package/src/operations/state.js +210 -0
  114. package/src/operations/switch-activate.js +64 -0
  115. package/src/operations/switch-backups.js +29 -0
  116. package/src/operations/switch-continuation-journal.js +94 -0
  117. package/src/operations/switch-continuation-pending.js +60 -0
  118. package/src/operations/switch-continuation-records.js +109 -0
  119. package/src/operations/switch-continuation-recovery.js +91 -0
  120. package/src/operations/switch-continuation-runtime.js +135 -0
  121. package/src/operations/switch-continuation-store.js +120 -0
  122. package/src/operations/switch-continuation.js +76 -0
  123. package/src/operations/switch-execute.js +68 -0
  124. package/src/operations/switch-inspect.js +45 -0
  125. package/src/operations/switch-journal-store.js +109 -0
  126. package/src/operations/switch-journal.js +71 -0
  127. package/src/operations/switch-lifecycle.js +72 -0
  128. package/src/operations/switch-pending.js +43 -0
  129. package/src/operations/switch-preflight.js +73 -0
  130. package/src/operations/switch-prepare.js +75 -0
  131. package/src/operations/switch-records.js +60 -0
  132. package/src/operations/switch-recovery-store.js +74 -0
  133. package/src/operations/switch.js +52 -0
  134. package/src/operations/toml-fields.js +133 -0
  135. package/src/providers/bundles.js +42 -0
  136. package/src/providers/common-entry.js +16 -0
  137. package/src/providers/grok.js +26 -0
  138. package/src/providers/interface.js +25 -0
  139. package/src/providers/kimi.js +26 -0
  140. package/src/providers/native.js +155 -0
  141. package/src/providers/registry.js +10 -0
  142. package/src/providers/shared.js +51 -0
  143. package/src/providers/source.js +30 -0
  144. package/src/source/git.js +303 -0
  145. package/src/source/inventory.js +87 -0
  146. package/src/source/repository-budget.js +18 -0
  147. package/src/source/snapshot.js +37 -0
  148. package/src/workspace/paths.js +54 -0
  149. package/src/workspace/profiles.js +8 -0
  150. package/src/workspace/repositories.js +57 -0
  151. package/src/workspace/repository-inventory.js +77 -0
  152. package/src/workspace/repository-observation.js +38 -0
  153. package/src/workspace/repository-preflight.js +129 -0
  154. package/src/workspace/repository-preview.js +196 -0
  155. package/src/workspace/repository-tree.js +57 -0
  156. package/src/workspace/reserved.js +11 -0
  157. package/src/workspace/resolve.js +53 -0
@@ -0,0 +1,90 @@
1
+ import {absoluteRoot} from '../workspace/paths.js';
2
+ import {contractDigest} from '../contracts/semantic.js';
3
+ import {fail} from '../contracts/parse.js';
4
+ import path from 'node:path';
5
+ import {hostname} from 'node:os';
6
+ import {requestShape} from '../operations/ownership.js';
7
+ import {readRecord} from '../operations/state.js';
8
+ import {inspectRepositoryBootstrap} from '../operations/repository-bootstrap-reconcile.js';
9
+ import {recoverRepositoryBootstrap} from '../operations/repository-bootstrap-recover.js';
10
+ import {inspectBootstrapRecovery,finishBootstrapRecovery} from '../operations/repository-bootstrap-continuation.js';
11
+ import {prepareBootstrapOwnerRetirement,applyBootstrapOwnerRetirement} from '../operations/bootstrap-owner-retirement.js';
12
+
13
+ function originalPreview(wrapper,original){
14
+ const {digest,...body}=original??{};
15
+ if(contractDigest(body)!==digest || original.wrapper!==wrapper)fail('bootstrap-recover.original-binding');
16
+ let preview=original;
17
+ if(original.kind==='prepared-repository-command'){
18
+ requestShape(original,['schemaVersion','kind','command','wrapper','origin','acquired','choices','options','preview','finalization','pipelineActivated','digest'],[],'bootstrap-recover.original');
19
+ if(original.schemaVersion!==1 || !['init','adopt'].includes(original.command) ||
20
+ original.finalization!=='verify-and-retain-history' || original.pipelineActivated!==false ||
21
+ original.options?.command!==original.command || original.options?.manifestPath!==original.origin?.origin?.path)
22
+ fail('bootstrap-recover.original-binding');
23
+ preview=original.preview;
24
+ }
25
+ const {digest:inner,...rest}=preview??{};
26
+ if(preview?.kind!=='repository-preview-candidate' || preview.wrapper!==wrapper ||
27
+ preview.executionAuthorized!==false || contractDigest(rest)!==inner)fail('bootstrap-recover.original-binding');
28
+ return preview;
29
+ }
30
+ export async function prepareBootstrapRecovery(wrapper,original){
31
+ wrapper=absoluteRoot(wrapper);const native=originalPreview(wrapper,original);
32
+ const reconciliation=await inspectRepositoryBootstrap(wrapper,JSON.stringify(native),native.digest);
33
+ let ownerStopped=false;
34
+ if(reconciliation.recordDigests.owner){
35
+ const owner=await readRecord(path.join(reconciliation.directory,'owner.json'));
36
+ if(owner.digest!==reconciliation.recordDigests.owner)fail('bootstrap-recover.original-drift');
37
+ if(owner.value?.host===hostname() && Number.isSafeInteger(owner.value.pid) && owner.value.pid>0){
38
+ try{process.kill(owner.value.pid,0);}catch(e){ownerStopped=e.code==='ESRCH';}
39
+ }
40
+ }
41
+ const ready=ownerStopped && ['before-state-observed','receipt-consistent-lock-retained'].includes(reconciliation.status);
42
+ const body={schemaVersion:1,kind:'bootstrap-recovery-preview',wrapper,original:structuredClone(original),
43
+ reconciliation,ownerStopped,status:ready?'review-only':'blocked',executionAuthorized:false,pipelineActivated:false};
44
+ return {...body,digest:contractDigest(body)};
45
+ }
46
+ export async function applyBootstrapRecovery(wrapper,preview){
47
+ wrapper=absoluteRoot(wrapper);const {digest,...body}=preview??{};
48
+ if(body.kind!=='bootstrap-recovery-preview' || body.wrapper!==wrapper || body.status!=='review-only' ||
49
+ contractDigest(body)!==digest)fail('bootstrap-recover.preview-approval');
50
+ const current=await prepareBootstrapRecovery(wrapper,body.original);
51
+ if(contractDigest(current)!==contractDigest(preview))fail('bootstrap-recover.preview-stale');
52
+ const native=originalPreview(wrapper,current.original);
53
+ return recoverRepositoryBootstrap({wrapper,previewText:JSON.stringify(native),previewDigest:native.digest,
54
+ approval:{decision:'approve',reconciliationDigest:current.reconciliation.digest}});
55
+ }
56
+
57
+ export async function prepareBootstrapContinuation(wrapper,initialDigest) {
58
+ wrapper=absoluteRoot(wrapper);
59
+ const observation=await inspectBootstrapRecovery({wrapper,initialDigest});
60
+ const ready=observation.status==='complete' || (observation.ownerStopped && observation.originalOwnerStopped);
61
+ const body={schemaVersion:1,kind:'bootstrap-continuation-preview',wrapper,initialDigest:observation.initialDigest,
62
+ observation,status:ready?'review-only':'blocked',executionAuthorized:false,pipelineActivated:false};
63
+ return {...body,digest:contractDigest(body)};
64
+ }
65
+ export async function applyBootstrapContinuation(wrapper,initialDigest,preview) {
66
+ wrapper=absoluteRoot(wrapper);const {digest,...body}=preview??{};
67
+ if(body.kind!=='bootstrap-continuation-preview' || body.wrapper!==wrapper || body.status!=='review-only' ||
68
+ (initialDigest!==undefined && initialDigest!==body.initialDigest) || contractDigest(body)!==digest)
69
+ fail('bootstrap-continue.preview-approval');
70
+ const current=await prepareBootstrapContinuation(wrapper,body.initialDigest);
71
+ if(contractDigest(current)!==contractDigest(preview))fail('bootstrap-continue.preview-stale');
72
+ // The native writer takes exclusive ownership and checks this exact fresh digest.
73
+ return finishBootstrapRecovery({wrapper,initialDigest:body.initialDigest,
74
+ approval:{decision:'approve',recoveryDigest:current.observation.digest}});
75
+ }
76
+ export async function runBootstrapContinuation(command,stdout) {
77
+ if(command.action==='recover-bootstrap'){
78
+ const result=command.apply?await applyBootstrapRecovery(command.workspace,(await readRecord(command.previewFile)).value):
79
+ await prepareBootstrapRecovery(command.workspace,(await readRecord(command.bootstrapPreview)).value);
80
+ await stdout(JSON.stringify(result)+'\n');return result.status==='blocked'?1:0;
81
+ }
82
+ if(command.action==='retire-bootstrap') {
83
+ const result=command.apply?await applyBootstrapOwnerRetirement(command.workspace,(await readRecord(command.previewFile)).value):
84
+ await prepareBootstrapOwnerRetirement(command.workspace);
85
+ await stdout(JSON.stringify(result)+'\n');return result.status==='blocked'?1:0;
86
+ }
87
+ const result=command.apply?await applyBootstrapContinuation(command.workspace,command.initialDigest,
88
+ (await readRecord(command.previewFile)).value):await prepareBootstrapContinuation(command.workspace,command.initialDigest);
89
+ await stdout(JSON.stringify(result)+'\n');return result.status==='blocked'?1:0;
90
+ }
@@ -0,0 +1,394 @@
1
+ import {inspectInstallation} from '../operations/doctor.js';
2
+ import {absoluteRoot} from '../workspace/paths.js';
3
+ import {ContractError,fail} from '../contracts/parse.js';
4
+ import {runRepositoryRecovery} from './repositories.js';
5
+ import {runRepositoryLockRecovery} from './repository-locks.js';
6
+ import {runBootstrapContinuation} from './bootstrap-recovery.js';
7
+ import {runRepositoryAbandon} from './repository-abandon.js';
8
+ import {runRepositoryAncestors} from './repository-ancestors.js';
9
+ import {scanRepositoryRetention} from '../operations/repository-retention.js';
10
+ import {applyRepositoryRetention} from '../operations/repository-retention-apply.js';
11
+ import {scanRetention} from '../operations/retention-scan.js';
12
+ import {scanCombinedRetention} from '../operations/retention-combined-scan.js';
13
+ import {scanCleanupReceipts} from '../operations/retention-receipts.js';
14
+ import {applyRetention} from '../operations/retention-apply.js';
15
+ import {acquireWorkspaceLock} from '../operations/lock.js';
16
+ import {readRecord} from '../operations/state.js';
17
+ import {readRetentionPolicy,previewRetentionPolicy,applyRetentionPolicy} from '../operations/retention-policy.js';
18
+ import {prepareLifecycle,applyLifecycle} from '../operations/lifecycle.js';
19
+ import {prepareMaintenance,applyMaintenance} from '../operations/maintenance.js';
20
+ import {prepareSwitchLifecycle,applySwitchLifecycle} from '../operations/switch-lifecycle.js';
21
+ import {prepareContinuationLifecycle,applyContinuationLifecycle} from '../operations/continuation-lifecycle.js';
22
+ import {assertNoRepositoryPending} from '../operations/repository-pending.js';
23
+ import {listRepositoryHistory} from '../operations/repository-history.js';
24
+ import {runRepositoryCommand} from './init.js';
25
+ import {parseMigrationCommand,runMigrationCommand} from './migration.js';
26
+ import {parseLaunch,runLaunch} from './launch.js';
27
+
28
+ export const help=`Workspace Pipeline CLI — development preview
29
+ Usage: workspace-pipeline doctor --workspace <absolute-directory> [--recovery <relative-record>] [--json]
30
+ workspace-pipeline launch grok --workspace <absolute-directory> --executable <absolute-native-executable> [--inspect] [--execute]
31
+ workspace-pipeline <init|adopt|wrap> --workspace <absolute-directory> --choices <absolute-json-file> [--manifest <absolute-file>] [--network]
32
+ workspace-pipeline <init|adopt|wrap> --workspace <absolute-directory> --apply --preview <absolute-json-file>
33
+ workspace-pipeline <setup|update> --workspace <absolute-directory> [--manifest <absolute-file>] [--network]
34
+ workspace-pipeline <setup|update> --workspace <absolute-directory> --apply --preview <absolute-json-file>
35
+ workspace-pipeline <repair|remove> --workspace <absolute-directory> [--providers <comma-separated-ids>] [--bundles <comma-separated-ids>]
36
+ workspace-pipeline <repair|remove> --workspace <absolute-directory> --apply --preview <absolute-json-file>
37
+ workspace-pipeline switch --workspace <absolute-directory> --manifest <absolute-file> [--network]
38
+ workspace-pipeline switch --workspace <absolute-directory> --apply --preview <absolute-json-file>
39
+ workspace-pipeline continue --workspace <absolute-directory> --recovery <relative-record>
40
+ workspace-pipeline continue --workspace <absolute-directory> --apply --preview <absolute-json-file>
41
+ workspace-pipeline logs list --workspace <absolute-directory> [--json]
42
+ workspace-pipeline logs clean --workspace <absolute-directory> --max-age-days <N> --keep-last <N> --max-delete <N>
43
+ workspace-pipeline logs clean --workspace <absolute-directory> --apply --preview <absolute-json-file>
44
+ workspace-pipeline logs policy show --workspace <absolute-directory>
45
+ workspace-pipeline logs policy set --workspace <absolute-directory> --mode <automatic|disabled> --max-age-days <N> --keep-last <N> --max-delete <N>
46
+ workspace-pipeline logs policy disable --workspace <absolute-directory>
47
+ workspace-pipeline logs policy <set|disable> --workspace <absolute-directory> --apply --preview <absolute-json-file>
48
+ workspace-pipeline --help
49
+
50
+ Migration commands (development preview):
51
+ workspace-pipeline migration unity preview --workspace <absolute-directory> --manifest <absolute-file> [--network]
52
+ workspace-pipeline migration unity inspect --workspace <absolute-directory> --recovery <relative-record> --phase <deactivation|installation|recovery|closeout|compensation>
53
+ workspace-pipeline migration unity apply --workspace <absolute-directory> --preview <absolute-file>
54
+ Preview stages Git externally; inspect is read-only/offline. JSON can contain
55
+ private config bytes. Only explicit apply writes or clears a validated pending marker.
56
+ Apply requires the saved installer-bound envelope and accepts no source overrides.
57
+
58
+ Doctor is read-only and prints JSON. Exit 0 means observed configuration ready;
59
+ exit 1 means not ready/incomplete; exit 2 means invalid invocation or unavailable command.
60
+ Runtime, MCP/harness discovery and provider compatibility are NOT verified.
61
+ No source access, automatic repair or lock removal. Read-only/configuration commands never clean history.
62
+ Unfinished repository/bootstrap/recovery operations block ordinary mutations;
63
+ doctor reports their presence without clearing them or claiming recovery is complete.
64
+ Policy commands configure future startup retention; they never run cleanup.
65
+ For combined v2 cleanup/policy previews add BOTH --receipt-max-age-days <N> and
66
+ --keep-receipts <N>. --max-delete is then one shared journal + receipt cap.
67
+ Logs list/clean preview are read-only. Save clean preview JSON, inspect it, then
68
+ use --apply --preview to delete only that exact still-valid selection. Cleanup is
69
+ not recoverable; its receipt path and partial failures are reported. No defaults.
70
+
71
+ Built-in workspace adapters: Codex, Claude, Kimi and Grok (configuration delivery).
72
+ Grok Claude-import suppression requires the scoped launch command, not direct grok.
73
+ Kimi/Grok native session discovery remains separately verified; setup is not runtime certification.
74
+ No native plugin installation, global activation, trust grant or MCP invocation.
75
+ Preview may stage Git source outside the workspace; --network explicitly permits
76
+ remote acquisition. Save the complete prepared JSON privately and inspect it
77
+ before --apply --preview. It may contain configuration secrets. Apply cannot
78
+ select a new source/manifest or download a replacement for missing staged data.
79
+ No native plugins are installed. Source packages cannot supply executable adapters.
80
+ Repair/remove are offline installed-snapshot operations. --providers and --bundles are remove-only,
81
+ preview-only; omission previews full removal. No automatic history cleanup on these commands.
82
+ Switch requires an explicit incoming manifest in preview, preserves two separate
83
+ phases and activates only after both pass. Failure is not rolled back or retried.
84
+ Continue creates a new approved operation from pending evidence; it does not replay old journals.
85
+ Init/adopt (wrap alias) prepare repositories only, not provider configuration.
86
+ Choices explicitly select keep/directory/init/clone/move per repository. Preview
87
+ does not authorize effects; apply verifies its exact saved package and inputs offline.
88
+ Successful apply verifies results and retains pending markers in history.
89
+ Recovery eligibility is checked by native readers.
90
+ Repository recovery: repositories status --workspace <absolute-directory> is read-only.
91
+ repositories recover-locks --workspace <absolute-directory> --journal <journal-uuid>
92
+ previews exact stopped-owner lock retirement/continuation from retained inputs;
93
+ add --apply --preview <absolute-file> only after inspecting the saved preview.
94
+ This does not recover uncertain effects or incomplete bootstrap creation.
95
+ repositories recover-bootstrap --workspace <absolute-directory> --bootstrap-preview <original-file>
96
+ previews first recovery of a stopped, evidenced bootstrap attempt without source access;
97
+ apply with --apply --preview <fresh-recovery-file> (no original override on apply).
98
+ repositories continue-bootstrap --workspace <absolute-directory> [--initial <sha256:digest>]
99
+ previews interrupted recovery from a complete retained request; --initial selects
100
+ completed history explicitly. Apply requires --apply --preview <absolute-file>.
101
+ It does not infer missing requests, recreate wrappers or replay repository effects.
102
+ repositories retire-bootstrap --workspace <absolute-directory> previews archival
103
+ of an owner-only stopped local bootstrap attempt, only while the wrapper is absent.
104
+ Apply requires --apply --preview <absolute-file>; unknown/torn owners stay blocked.
105
+ repositories finalize --workspace <absolute-directory> produces a saved JSON preview;
106
+ repositories finalize --workspace <absolute-directory> --apply --preview <absolute-file>
107
+ finishes only verified completed effects (or an exact existing completion receipt).
108
+ Finalize does not replay repository operations or retire stale locks.
109
+ repositories prepare-parent --workspace <absolute-directory> previews bounded missing parents;
110
+ apply with --apply --preview <file>, then obtain a fresh init/adopt preview.
111
+ For nested parents this argument is a target sentinel, not the canonical wrapper.
112
+ Stop canonical workspace writers first; parent leases do not serialize their locks.
113
+ Use the actual wrapper path for the subsequent fresh init/adopt preview.
114
+ repositories continue-parent --workspace <absolute-directory> --parent-preview <file>
115
+ prepares a fresh continuation preview; apply it with --apply --preview <file>.
116
+ repositories abandon --workspace <absolute-directory> previews preserving observed
117
+ partial effects and abandoning the old request (NOT successful completion).
118
+ repositories continue-abandon --workspace <absolute-directory> --attempt <uuid>
119
+ previews interrupted abandonment; both require --apply --preview <file> to mutate.
120
+ logs clean --repositories --workspace <absolute-directory> --max-age-days <N>
121
+ --keep-last <N> --max-delete <N> previews unreferenced completed repository groups.
122
+ Apply with --repositories --apply --preview <file>; this is a separate cleanup
123
+ domain, never an implicit increase of the ordinary journal deletion budget.
124
+ Execution inputs are retained privately under .pipeline/repository-inputs;
125
+ --json is optional because JSON is the default.
126
+ This package is not ready to replace an existing installation.`;
127
+
128
+ // Parse strictly before any filesystem observation. Never echo unknown arguments
129
+ // (which can contain credentials). Output transport is trusted CLI code.
130
+ export function parseCommand(args) {
131
+ if(!Array.isArray(args) || args.some(a=>typeof a!=='string')) fail('cli.arguments');
132
+ if(args.length===0 || (args.length===1 && ['--help','-h'].includes(args[0])))return {command:'help'};
133
+ if(args[0]==='launch')return parseLaunch(args);
134
+ if(['setup','update','repair','remove','switch','continue'].includes(args[0]))return parseLifecycle(args);
135
+ if(args[0]==='migration')return parseMigrationCommand(args);
136
+ if(['init','adopt','wrap'].includes(args[0]))return parseRepositories(args);
137
+ if(args[0]==='repositories')return parseRepositoryRecovery(args);
138
+ if(args[0]==='logs')return parseLogs(args);
139
+ if(args[0]!=='doctor')fail('cli.command-unavailable');
140
+ const result={command:'doctor'},seen=new Set();
141
+ for(let i=1;i<args.length;i++) {
142
+ const flag=args[i];
143
+ if(!['--workspace','--recovery','--json'].includes(flag) || seen.has(flag))fail('cli.arguments');
144
+ seen.add(flag);
145
+ if(flag==='--json')continue;
146
+ if(i+1>=args.length || args[i+1].startsWith('--'))fail('cli.arguments');
147
+ const value=args[++i];
148
+ if(flag==='--workspace')result.workspace=absoluteRoot(value);
149
+ else result.recoveryPath=value;
150
+ }
151
+ if(!Object.hasOwn(result,'workspace'))fail('cli.workspace-required');
152
+ if(result.recoveryPath!==undefined && !/^\.pipeline\/transactions\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\/recovery\.json$/.test(result.recoveryPath))fail('recovery.path');
153
+ return result;
154
+ }
155
+
156
+ function parseRepositoryRecovery(args) {
157
+ if(!['status','finalize','recover-locks','recover-bootstrap','continue-bootstrap','retire-bootstrap','abandon','continue-abandon','prepare-parent','continue-parent'].includes(args[1]))fail('cli.arguments');
158
+ const result={command:'repositories',action:args[1]},seen=new Set();
159
+ for(let i=2;i<args.length;i++) {
160
+ const flag=args[i];
161
+ if(!['--workspace','--apply','--preview','--json','--journal','--initial','--attempt','--parent-preview','--bootstrap-preview'].includes(flag) || seen.has(flag))fail('cli.arguments');
162
+ seen.add(flag);
163
+ if(flag==='--json')continue;
164
+ if(flag==='--apply'){result.apply=true;continue;}
165
+ const value=args[++i];if(value===undefined || value.startsWith('--'))fail('cli.arguments');
166
+ if(flag==='--bootstrap-preview'){
167
+ if(result.action!=='recover-bootstrap')fail('cli.arguments');result.bootstrapPreview=absoluteRoot(value);continue;
168
+ }
169
+ if(flag==='--parent-preview'){
170
+ if(result.action!=='continue-parent')fail('cli.arguments');result.parentPreview=absoluteRoot(value);continue;
171
+ }
172
+ if(flag==='--attempt'){
173
+ if(result.action!=='continue-abandon' || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value))fail('cli.arguments');
174
+ result.attempt=value;continue;
175
+ }
176
+ if(flag==='--initial') {
177
+ if(result.action!=='continue-bootstrap' || !/^sha256:[a-f0-9]{64}$/.test(value))fail('cli.arguments');
178
+ result.initialDigest=value;continue;
179
+ }
180
+ if(flag==='--journal') {
181
+ if(result.action!=='recover-locks' || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value))fail('cli.arguments');
182
+ result.journalId=value;continue;
183
+ }
184
+ result[flag==='--workspace'?'workspace':'previewFile']=absoluteRoot(value);
185
+ }
186
+ if(!result.workspace)fail('cli.workspace-required');
187
+ if(result.action==='continue-abandon' && !result.attempt)fail('cli.arguments');
188
+ if(result.action==='continue-parent' && (result.apply?Boolean(result.parentPreview):!result.parentPreview))fail('cli.arguments');
189
+ if(result.action==='recover-bootstrap' && (result.apply?Boolean(result.bootstrapPreview):!result.bootstrapPreview))fail('cli.arguments');
190
+ if(result.action==='recover-locks' && !result.journalId)fail('repositories.journal-required');
191
+ if(result.action==='status' && (result.apply || result.previewFile))fail('cli.arguments');
192
+ if(Boolean(result.apply)!==Boolean(result.previewFile))fail('cli.arguments');
193
+ return result;
194
+ }
195
+
196
+ function parseRepositories(args) {
197
+ const result={command:args[0]==='wrap'?'adopt':args[0]},seen=new Set();
198
+ for(let i=1;i<args.length;i++) {
199
+ const flag=args[i];
200
+ if(!['--workspace','--manifest','--choices','--apply','--preview','--network','--json'].includes(flag) || seen.has(flag))fail('cli.arguments');
201
+ seen.add(flag);
202
+ if(flag==='--json')continue;
203
+ if(flag==='--apply'){result.apply=true;continue;}
204
+ if(flag==='--network'){result.network=true;continue;}
205
+ const value=args[++i];if(value===undefined || value.startsWith('--'))fail('cli.arguments');
206
+ result[{'--workspace':'workspace','--manifest':'manifestPath','--choices':'choicesFile','--preview':'previewFile'}[flag]]=absoluteRoot(value);
207
+ }
208
+ if(!result.workspace)fail('cli.workspace-required');
209
+ if(result.apply?(!result.previewFile || result.choicesFile || result.manifestPath || result.network):(!result.choicesFile || result.previewFile))fail('cli.arguments');
210
+ return result;
211
+ }
212
+
213
+ function parseLifecycle(args) {
214
+ const result={command:args[0]},seen=new Set(),maintenance=['repair','remove'].includes(args[0]);
215
+ for(let i=1;i<args.length;i++) {
216
+ const flag=args[i];
217
+ const allowed=['--workspace','--apply','--preview','--json',...(result.command==='continue'?['--recovery']:maintenance?result.command==='remove'?['--providers','--bundles']:[]:['--manifest','--network'])];
218
+ if(!allowed.includes(flag) || seen.has(flag))fail('cli.arguments');
219
+ seen.add(flag);
220
+ if(flag==='--json')continue;
221
+ if(flag==='--apply'){result.apply=true;continue;}
222
+ if(flag==='--network'){result.network=true;continue;}
223
+ const value=args[++i];if(value===undefined || value.startsWith('--'))fail('cli.arguments');
224
+ if(flag==='--recovery') {
225
+ if(!/^\.pipeline\/transactions\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\/recovery\.json$(?![\s\S])/.test(value))fail('recovery.path');
226
+ result.recoveryPath=value;continue;
227
+ }
228
+ if(flag==='--bundles') {
229
+ const ids=value.split(',');
230
+ if(ids.some(id=>!/^[a-z][a-z0-9-]{0,62}$/.test(id)) || new Set(ids).size!==ids.length)fail('cli.arguments');
231
+ result.bundles=ids.sort();continue;
232
+ }
233
+ if(flag==='--providers') {
234
+ const providers=value.split(',');
235
+ if(providers.some(p=>!['codex','claude','kimi','grok'].includes(p)) || new Set(providers).size!==providers.length)fail('cli.arguments');
236
+ result.providers=providers.sort();continue;
237
+ }
238
+ result[{'--workspace':'workspace','--manifest':'manifestPath','--preview':'previewFile'}[flag]]=absoluteRoot(value);
239
+ }
240
+ if(!result.workspace)fail('cli.workspace-required');
241
+ if(result.apply?(!result.previewFile || result.manifestPath || result.network || result.providers || result.bundles):result.previewFile)fail('cli.arguments');
242
+ if(result.command==='switch' && !result.apply && !result.manifestPath)fail('switch.manifest-required');
243
+ if(result.command==='continue' && (result.apply?result.recoveryPath:!result.recoveryPath))fail('continuation.recovery-required');
244
+ return result;
245
+ }
246
+
247
+ async function runLifecycle(command,registry,stdout,stderr) {
248
+ // A trusted embedding/CLI assembly can supply functions; argv and package
249
+ // data cannot. Until real adapters ship, the default command fails before IO.
250
+ if(registry===null || registry===undefined)fail('cli.providers-unavailable');
251
+ await assertNoRepositoryPending(command.workspace);
252
+ const maintenance=['repair','remove'].includes(command.command);
253
+ const prepare=command.command==='continue'?prepareContinuationLifecycle:command.command==='switch'?prepareSwitchLifecycle:maintenance?prepareMaintenance:prepareLifecycle;
254
+ const apply=command.command==='continue'?applyContinuationLifecycle:command.command==='switch'?applySwitchLifecycle:maintenance?applyMaintenance:applyLifecycle;
255
+ if(!command.apply) {
256
+ const input={command:command.command,wrapper:command.workspace};
257
+ if(command.manifestPath)input.manifestPath=command.manifestPath;
258
+ if(command.network)input.network=true;
259
+ if(command.providers)input.providers=command.providers;
260
+ if(command.bundles)input.bundles=command.bundles;
261
+ if(command.recoveryPath)input.recoveryPath=command.recoveryPath;
262
+ await stdout(JSON.stringify(await prepare(input,registry))+'\n');return 0;
263
+ }
264
+ const prepared=(await readRecord(command.previewFile)).value;
265
+ const result=await apply({command:command.command,wrapper:command.workspace,prepared,
266
+ approval:{decision:'approve',preparedDigest:prepared.digest}},registry,
267
+ {report:async event=>stderr(JSON.stringify(event)+'\n')});
268
+ await stdout(JSON.stringify(result)+'\n');
269
+ return (result.status==='ready' || (['remove','continue'].includes(command.command) && result.status==='not-installed')) && result.lockRelease==='released' && !result.outputError?0:1;
270
+ }
271
+
272
+ function parseLogs(args) {
273
+ if(args[1]==='policy')return parsePolicy(args);
274
+ if(!['list','clean'].includes(args[1]))fail('cli.arguments');
275
+ const result={command:'logs',action:args[1]},seen=new Set();
276
+ const numbers={'--max-age-days':'maxAgeDays','--keep-last':'maxJournals','--max-delete':'maxDeletesPerRun'};
277
+ const receiptNumbers={'--receipt-max-age-days':'maxAgeDays','--keep-receipts':'maxReceipts'};
278
+ for(let i=2;i<args.length;i++) {
279
+ const flag=args[i];
280
+ if(!['--workspace','--json','--apply','--preview','--repositories',...Object.keys(numbers),...Object.keys(receiptNumbers)].includes(flag) || seen.has(flag))fail('cli.arguments');
281
+ seen.add(flag);
282
+ if(flag==='--json')continue;
283
+ if(flag==='--repositories'){result.repositories=true;continue;}
284
+ if(flag==='--apply'){result.apply=true;continue;}
285
+ const value=args[++i];if(value===undefined || value.startsWith('--'))fail('cli.arguments');
286
+ if(flag==='--workspace')result.workspace=absoluteRoot(value);
287
+ else if(flag==='--preview')result.previewFile=absoluteRoot(value);
288
+ else {
289
+ if(!/^(0|[1-9][0-9]*)(?![\s\S])/.test(value) || !Number.isSafeInteger(Number(value)))fail('cli.arguments');
290
+ if(Object.hasOwn(receiptNumbers,flag))(result.receiptPolicy??={})[receiptNumbers[flag]]=Number(value);
291
+ else (result.policy??={})[numbers[flag]]=Number(value);
292
+ }
293
+ }
294
+ if(!result.workspace)fail('cli.workspace-required');
295
+ if(result.receiptPolicy && Object.keys(result.receiptPolicy).length!==2)fail('cli.arguments');
296
+ if(result.repositories && (result.action!=='clean' || result.receiptPolicy))fail('cli.arguments');
297
+ if(result.action==='list' && (result.apply || result.previewFile || result.policy || result.receiptPolicy))fail('cli.arguments');
298
+ if(result.action==='clean') {
299
+ if(result.apply) {if(!result.previewFile || result.policy || result.receiptPolicy)fail('cli.arguments');}
300
+ else if(result.previewFile || !result.policy || Object.keys(result.policy).length!==3)fail('cli.arguments');
301
+ }
302
+ return result;
303
+ }
304
+ function combinedPolicy(command) {
305
+ const {maxDeletesPerRun,...journals}=command.policy;
306
+ return {journals,cleanupReceipts:command.receiptPolicy,maxDeletesPerRun};
307
+ }
308
+
309
+ function parsePolicy(args) {
310
+ const action=args[2];if(!['show','set','disable'].includes(action))fail('cli.arguments');
311
+ const rest=args.slice(3),filtered=[];let mode;
312
+ for(let i=0;i<rest.length;i++) {
313
+ if(rest[i]==='--mode') {
314
+ if(mode!==undefined || !['automatic','disabled'].includes(rest[i+1]))fail('cli.arguments');
315
+ mode=rest[++i];
316
+ }else filtered.push(rest[i]);
317
+ }
318
+ // Reuse strict numeric, duplicate, path and apply/preview parsing.
319
+ const parsed=parseLogs(['logs',action==='set'?'clean':filtered.includes('--apply')?'clean':'list',...filtered]);
320
+ if(parsed.repositories)fail('cli.arguments');
321
+ if(action==='set' && (parsed.apply?mode!==undefined:mode===undefined))fail('cli.arguments');
322
+ if(action!=='set' && (mode!==undefined || parsed.policy))fail('cli.arguments');
323
+ if(action==='show' && parsed.apply)fail('cli.arguments');
324
+ return {...parsed,command:'policy',action,mode};
325
+ }
326
+
327
+ async function runPolicy(command,stdout) {
328
+ if(command.action==='show') {
329
+ await stdout(JSON.stringify(await readRetentionPolicy(command.workspace))+'\n');return 0;
330
+ }
331
+ if(!command.apply) {
332
+ const request=command.action==='disable'?{action:'disable'}:{action:'set',mode:command.mode,
333
+ ...command.receiptPolicy?combinedPolicy(command):{journals:command.policy}};
334
+ await stdout(JSON.stringify(await previewRetentionPolicy(command.workspace,request))+'\n');return 0;
335
+ }
336
+ const preview=(await readRecord(command.previewFile)).value;
337
+ if(preview.action!==command.action || preview.workspace!==command.workspace)fail('retention-policy.approval');
338
+ const lock=await acquireWorkspaceLock(command.workspace);
339
+ try {
340
+ const result=await applyRetentionPolicy(lock,preview,{decision:'approve',previewDigest:preview.digest});
341
+ await stdout(JSON.stringify(result)+'\n');return 0;
342
+ }finally{await lock.release();}
343
+ }
344
+
345
+ async function runLogs(command,stdout,stderr) {
346
+ if(command.action==='list') {
347
+ // No retention policy chosen: disable selection and omit policy/plan fields.
348
+ const scan=await scanRetention(command.workspace,{now:Date.now(),policy:{maxAgeDays:Math.floor(Number.MAX_SAFE_INTEGER/86400000),maxJournals:Number.MAX_SAFE_INTEGER,maxDeletesPerRun:0}});
349
+ const receipts=await scanCleanupReceipts(command.workspace),repositories=await listRepositoryHistory(command.workspace);
350
+ const complete=scan.complete&&receipts.complete&&repositories.complete;
351
+ await stdout(JSON.stringify({workspace:scan.workspace,groups:scan.groups,receipts:receipts.records,
352
+ repositories,diagnostics:[...scan.diagnostics,...receipts.diagnostics,...repositories.diagnostics],complete,selectionDisabled:true})+'\n');
353
+ return complete?0:1;
354
+ }
355
+ if(!command.apply) {
356
+ const preview=await (command.repositories?scanRepositoryRetention:command.receiptPolicy?scanCombinedRetention:scanRetention)(command.workspace,
357
+ {now:Date.now(),policy:command.receiptPolicy?combinedPolicy(command):command.policy});
358
+ await stdout(JSON.stringify(preview)+'\n');return preview.complete?0:1;
359
+ }
360
+ const preview=(await readRecord(command.previewFile)).value;
361
+ if(preview.workspace!==command.workspace)fail('retention-apply.approval');
362
+ if(Boolean(command.repositories)!==(preview.kind==='repository-retention-preview'))fail('retention-apply.approval');
363
+ const lock=await acquireWorkspaceLock(command.workspace);
364
+ try {
365
+ const result=await (command.repositories?applyRepositoryRetention:applyRetention)(lock,preview,{decision:'approve',previewDigest:preview.digest},
366
+ {report:async event=>stderr(JSON.stringify(event)+'\n')});
367
+ await stdout(JSON.stringify(result)+'\n');return result.status==='completed'?0:1;
368
+ }finally{await lock.release();}
369
+ }
370
+
371
+ export async function runCli(args,{stdout,stderr,registry=null}) {
372
+ if(typeof stdout!=='function' || typeof stderr!=='function')fail('cli.transport');
373
+ try {
374
+ const command=parseCommand(args);
375
+ if(command.command==='help') {await stdout(help+'\n');return 0;}
376
+ if(command.command==='launch')return await runLaunch(command,stdout,stderr);
377
+ if(command.command==='migration')return await runMigrationCommand(command,stdout,stderr);
378
+ if(command.command==='logs')return await runLogs(command,stdout,stderr);
379
+ if(command.command==='policy')return await runPolicy(command,stdout);
380
+ if(['init','adopt'].includes(command.command))return await runRepositoryCommand(command,stdout,stderr);
381
+ if(command.command==='repositories')return await (['prepare-parent','continue-parent'].includes(command.action)?runRepositoryAncestors:
382
+ ['abandon','continue-abandon'].includes(command.action)?runRepositoryAbandon:
383
+ command.action==='recover-locks'?runRepositoryLockRecovery:
384
+ ['recover-bootstrap','continue-bootstrap','retire-bootstrap'].includes(command.action)?runBootstrapContinuation:runRepositoryRecovery)(command,stdout);
385
+ if(['setup','update','repair','remove','switch','continue'].includes(command.command))return await runLifecycle(command,registry,stdout,stderr);
386
+ const options=command.recoveryPath===undefined?{}:{recoveryPath:command.recoveryPath};
387
+ const result=await inspectInstallation(command.workspace,options);
388
+ await stdout(JSON.stringify(result)+'\n');
389
+ return result.ready?0:1;
390
+ } catch(error) {
391
+ try {await stderr(JSON.stringify({error:error instanceof ContractError?error.code:'cli.io'})+'\n');}catch{}
392
+ return 2;
393
+ }
394
+ }
@@ -0,0 +1,84 @@
1
+ import path from 'node:path';
2
+ import {tmpdir} from 'node:os';
3
+ import {fail} from '../contracts/parse.js';
4
+ import {contractDigest} from '../contracts/semantic.js';
5
+ import {requestShape} from '../operations/ownership.js';
6
+ import {resolveOrigin,verifyPreparedSnapshot,readRecord} from '../operations/state.js';
7
+ import {acquire} from '../source/git.js';
8
+ import {absoluteRoot} from '../workspace/paths.js';
9
+ import {assertNoRepositoryPending} from '../operations/repository-pending.js';
10
+ import {prepareRepositoryPreview} from '../workspace/repository-preview.js';
11
+ import {applyRepositoryWorkspace} from '../operations/repository-workspace.js';
12
+ import {inspectRepositoryReconciliation,finalizeRepositoryOperations} from '../operations/repository-reconcile.js';
13
+
14
+ function verb(command){if(!['init','adopt'].includes(command))fail('repositories.command');}
15
+ function outside(root,candidate) {
16
+ const relative=path.relative(root.toLowerCase(),candidate.toLowerCase());
17
+ if(relative==='' || (!path.isAbsolute(relative) && relative!=='..' && !relative.startsWith('..'+path.sep)))
18
+ fail('repositories.preparation-location');
19
+ }
20
+
21
+ // Repository orchestration only. Provider installation is a separate setup plan;
22
+ // no provider functions, package scripts, hooks or native plugins are loaded.
23
+ export async function prepareRepositoryCommand({command,wrapper,manifestPath,choices,network=false,tempRoot=tmpdir()}) {
24
+ verb(command);wrapper=absoluteRoot(wrapper);tempRoot=absoluteRoot(tempRoot);
25
+ outside(wrapper,tempRoot);await assertNoRepositoryPending(wrapper);
26
+ const origin=await resolveOrigin({wrapper,manifestPath});
27
+ for(const choice of Object.values(choices??{}))if(choice?.action==='move')outside(absoluteRoot(choice.from),tempRoot);
28
+ const acquired=await acquire(origin.manifest.pipeline,{manifestBase:origin.origin.base,tempRoot,network});
29
+ const verified=await verifyPreparedSnapshot(acquired);
30
+ const options={command,manifestPath:origin.origin.path,tempRoot,network};
31
+ const preview=await prepareRepositoryPreview(verified.manifest,origin.manifest,wrapper,choices,options);
32
+ const again=await resolveOrigin({wrapper,manifestPath:origin.origin.path});
33
+ if(contractDigest(again)!==contractDigest(origin))fail('repositories.origin-drift');
34
+ const body={schemaVersion:1,kind:'prepared-repository-command',command,wrapper,origin,acquired,
35
+ choices:structuredClone(choices),options,preview,finalization:'verify-and-retain-history',pipelineActivated:false};
36
+ return {...body,digest:contractDigest(body)};
37
+ }
38
+
39
+ export async function applyRepositoryCommand({command,wrapper,prepared,approval},{report=async()=>{}}={}) {
40
+ verb(command);wrapper=absoluteRoot(wrapper);
41
+ requestShape(prepared,['schemaVersion','kind','command','wrapper','origin','acquired','choices','options','preview','finalization','pipelineActivated','digest'],[],'repositories.prepared');
42
+ const {digest,...body}=prepared;
43
+ if(approval?.decision!=='approve' || approval.preparedDigest!==digest || contractDigest(body)!==digest ||
44
+ prepared.schemaVersion!==1 || prepared.kind!=='prepared-repository-command' || prepared.command!==command ||
45
+ prepared.wrapper!==wrapper || prepared.finalization!=='verify-and-retain-history' || prepared.pipelineActivated!==false)
46
+ fail('repositories.prepared-binding');
47
+ requestShape(prepared.options,['command','manifestPath','tempRoot','network'],[],'repositories.options');
48
+ if(prepared.options.command!==command || prepared.options.manifestPath!==prepared.origin?.origin?.path)
49
+ fail('repositories.prepared-binding');
50
+ outside(wrapper,absoluteRoot(prepared.options.tempRoot));await assertNoRepositoryPending(wrapper);
51
+ const origin=await resolveOrigin({wrapper,manifestPath:prepared.options.manifestPath});
52
+ if(contractDigest(origin)!==contractDigest(prepared.origin) ||
53
+ contractDigest(prepared.acquired.source)!==contractDigest(origin.manifest.pipeline) ||
54
+ prepared.acquired.resolvedSource!==origin.origin.resolvedSource)fail('repositories.origin-drift');
55
+ outside(wrapper,absoluteRoot(prepared.acquired.snapshotPath));
56
+ const verified=await verifyPreparedSnapshot(prepared.acquired);
57
+ const input={pipeline:verified.manifest,workspace:origin.manifest,wrapper,choices:prepared.choices,
58
+ options:prepared.options,previewText:JSON.stringify(prepared.preview),
59
+ approval:{decision:'approve',previewDigest:prepared.preview.digest},onLocation:report};
60
+ const applied=await applyRepositoryWorkspace(input);
61
+ const executionPreview=applied.executionPreview??prepared.preview;
62
+ const previewText=JSON.stringify(executionPreview);
63
+ const checked=await inspectRepositoryReconciliation(wrapper,previewText,executionPreview.digest);
64
+ if(!checked.canFinalize)fail('repositories.finalization-unresolved');
65
+ // Finalization was explicitly part of the approved command; this does not
66
+ // authorize different effects or a provider installation.
67
+ const finalized=await finalizeRepositoryOperations({wrapper,previewText,previewDigest:executionPreview.digest,
68
+ approval:{decision:'approve',reconciliationDigest:checked.digest}});
69
+ return {status:'repositories-prepared',command,wrapper,journal:applied.journal,executionPreview,
70
+ finalization:finalized,pipelineActivated:false,next:'Prepare a separate setup preview for provider configuration.'};
71
+ }
72
+
73
+ export async function runRepositoryCommand(command,stdout,stderr) {
74
+ if(!command.apply) {
75
+ const choices=(await readRecord(command.choicesFile)).value;
76
+ const prepared=await prepareRepositoryCommand({command:command.command,wrapper:command.workspace,
77
+ manifestPath:command.manifestPath,choices,network:command.network??false});
78
+ await stdout(JSON.stringify(prepared)+'\n');return prepared.preview.status==='blocked'?1:0;
79
+ }
80
+ const prepared=(await readRecord(command.previewFile)).value;
81
+ const result=await applyRepositoryCommand({command:command.command,wrapper:command.workspace,prepared,
82
+ approval:{decision:'approve',preparedDigest:prepared.digest}},{report:async event=>stderr(JSON.stringify({kind:'repository-location',...event})+'\n')});
83
+ await stdout(JSON.stringify(result)+'\n');return 0;
84
+ }