@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,69 @@
1
+ import { lstat } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import { fail } from '../contracts/parse.js';
4
+ import { absoluteRoot, resolveChild } from '../workspace/paths.js';
5
+ import { readState } from '../operations/state.js';
6
+ import { inspectInstallation } from '../operations/doctor.js';
7
+ import { grokEnvironment } from '../launch/grok.js';
8
+
9
+ export function parseLaunch(args) {
10
+ if (args[0] !== 'launch' || args[1] !== 'grok') fail('cli.arguments');
11
+ const result = { command: 'launch', provider: 'grok', execute: false, inspect: false }, seen = new Set();
12
+ for (let i = 2; i < args.length; i++) {
13
+ const flag = args[i];
14
+ if (!['--workspace', '--executable', '--execute', '--inspect'].includes(flag) || seen.has(flag)) fail('cli.arguments');
15
+ seen.add(flag);
16
+ if (flag === '--execute') { result.execute = true; continue; }
17
+ if (flag === '--inspect') { result.inspect = true; continue; }
18
+ const value = args[++i];
19
+ if (value === undefined || value.startsWith('--')) fail('cli.arguments');
20
+ result[flag === '--workspace' ? 'workspace' : 'executable'] = absoluteRoot(value);
21
+ }
22
+ if (!result.workspace) fail('cli.workspace-required');
23
+ if (!result.executable) fail('launch.executable-required');
24
+ return result;
25
+ }
26
+
27
+ function spawnNative(executable, args, options) {
28
+ return new Promise((resolve, reject) => {
29
+ const child = spawn(executable, args, { ...options, shell: false, stdio: 'inherit' });
30
+ child.once('error', () => reject(new Error('launch.spawn')));
31
+ child.once('exit', (code, signal) => resolve({ code, signal }));
32
+ });
33
+ }
34
+
35
+ // Dependencies are trusted embedding/test functions, never package data.
36
+ export async function runLaunch(command, stdout, stderr, dependencies = {}) {
37
+ const doctor = dependencies.doctor ?? inspectInstallation;
38
+ const state = dependencies.state ?? (wrapper => readState(resolveChild(wrapper, '.pipeline/state.json')));
39
+ const stat = dependencies.stat ?? lstat;
40
+ const start = dependencies.start ?? spawnNative;
41
+ const platform = dependencies.platform ?? process.platform;
42
+ const parentEnv = dependencies.env ?? process.env;
43
+ const tty = dependencies.tty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
44
+ // No arbitrary harness arguments: --cwd, worktrees or prompt/config overrides
45
+ // must not silently change the verified launch scope.
46
+ const before = await state(command.workspace);
47
+ const report = await doctor(command.workspace);
48
+ const after = await state(command.workspace);
49
+ if (before.digest !== after.digest) fail('launch.state-drift');
50
+ if (!report.ready || !report.pipeline?.providers.includes('grok')) fail('launch.not-ready');
51
+ if (!after.value.active?.providers.includes('grok') || after.value.pending !== null) fail('launch.not-ready');
52
+ if (platform === 'win32' && !/\.exe$/i.test(command.executable)) fail('launch.executable-format');
53
+ const executable = await stat(command.executable);
54
+ if (!executable.isFile() || executable.isSymbolicLink()) fail('launch.executable-format');
55
+ const args = command.inspect ? ['inspect', '--json'] : [];
56
+ const overlay = grokEnvironment({}, platform);
57
+ const preview = { command: 'launch', provider: 'grok', workspace: command.workspace,
58
+ executable: command.executable, args, environmentOverrides: overlay,
59
+ stateHash: after.digest, configuration: 'verified', runtime: 'not-run',
60
+ warning: 'Close pipeline configuration editors/updaters during launch. No ongoing configuration lock or runtime certification. Harness may start configured MCP servers.' };
61
+ if (!command.execute) { await stdout(JSON.stringify(preview) + '\n'); return 0; }
62
+ if (!command.inspect && !tty) fail('launch.terminal-required');
63
+ await stderr(JSON.stringify(preview) + '\n');
64
+ // Fail if a concurrent installer changed state while the launch report was emitted.
65
+ if ((await state(command.workspace)).digest !== after.digest) fail('launch.state-drift');
66
+ const result = await start(command.executable, args, { cwd: command.workspace, env: grokEnvironment(parentEnv, platform) });
67
+ // Process exit is not a statement about model-visible skills or task acceptance.
68
+ return Number.isInteger(result.code) && result.code >= 0 && result.code <= 255 ? result.code : 1;
69
+ }
@@ -0,0 +1,53 @@
1
+ import path from 'node:path';
2
+ import {readRecord} from '../operations/state.js';
3
+ import {verifyMigrationInstaller} from '../operations/installer-identity.js';
4
+ import {beginLegacyUnityMigration} from '../migrations/legacy-unity-begin.js';
5
+ import {applyLegacyUnityDeactivationResume} from '../migrations/legacy-unity-deactivation-resume-apply.js';
6
+ import {applyLegacyUnityInstallResume} from '../migrations/legacy-unity-install-resume.js';
7
+ import {applyLegacyUnityInstallRecovery} from '../migrations/legacy-unity-install-recovery.js';
8
+ import {applyLegacyUnityCloseoutResume} from '../migrations/legacy-unity-resume-apply.js';
9
+ import {applyLegacyUnityCompensation} from '../migrations/legacy-unity-compensate.js';
10
+ import {fail} from '../contracts/parse.js';
11
+
12
+ const writers={
13
+ 'legacy-unity-deactivation-resume-preview':applyLegacyUnityDeactivationResume,
14
+ 'legacy-unity-install-resume-preview':applyLegacyUnityInstallResume,
15
+ 'legacy-unity-install-recovery-preview':applyLegacyUnityInstallRecovery,
16
+ 'legacy-unity-closeout-preview':applyLegacyUnityCloseoutResume,
17
+ 'legacy-unity-compensation-preview':applyLegacyUnityCompensation
18
+ };
19
+ export async function runMigrationApply(command,stdout,stderr){
20
+ const envelope=(await readRecord(command.previewFile)).value;
21
+ const preview=await verifyMigrationInstaller(envelope);
22
+ if(!preview||preview.workspace!==command.workspace||typeof preview.digest!=='string')fail('migration.preview-binding');
23
+ if(preview.kind!=='legacy-unity-preview'&&!Object.hasOwn(writers,preview.kind))fail('migration.preview-binding');
24
+ const approval={decision:'approve',previewDigest:preview.digest};
25
+ await stderr('Applying the exact saved migration preview. Stop harness sessions and config editors. No automatic rollback.\n');
26
+ let result;
27
+ if(preview.kind==='legacy-unity-preview'){
28
+ const begun=await beginLegacyUnityMigration(preview,approval,command.workspace);
29
+ try{
30
+ await stderr(JSON.stringify({recovery:path.resolve(command.workspace,begun.recoveryPath),
31
+ logs:path.resolve(command.workspace,path.dirname(begun.recoveryPath))})+'\n');
32
+ await verifyMigrationInstaller(envelope);
33
+ const phase=await begun.deactivate();
34
+ if(phase.outcome!=='completed')result={...phase,recoveryPath:begun.recoveryPath};
35
+ else{
36
+ await verifyMigrationInstaller(envelope);
37
+ const installed=await begun.install();
38
+ if(installed.installationStatus!=='ready')result={...installed,migrationRecoveryPath:begun.recoveryPath};
39
+ else{
40
+ await verifyMigrationInstaller(envelope);
41
+ result={...await begun.finalize(),migrationRecoveryPath:begun.recoveryPath};
42
+ }
43
+ }
44
+ }finally{await begun.lock.release();}
45
+ }else{
46
+ await stderr(JSON.stringify({recovery:path.resolve(command.workspace,preview.recoveryPath),
47
+ logs:path.resolve(command.workspace,path.dirname(preview.recoveryPath))})+'\n');
48
+ await verifyMigrationInstaller(envelope);
49
+ result=await writers[preview.kind](preview,approval,command.workspace);
50
+ }
51
+ await stdout(JSON.stringify(result)+'\n');
52
+ return ['completed','legacy-restored'].includes(result.status)?0:1;
53
+ }
@@ -0,0 +1,50 @@
1
+ import {absoluteRoot} from '../workspace/paths.js';
2
+ import {fail} from '../contracts/parse.js';
3
+ import {prepareLegacyUnityPreview} from '../migrations/legacy-unity-preview.js';
4
+ import {prepareLegacyUnityDeactivationResume} from '../migrations/legacy-unity-deactivation-resume.js';
5
+ import {prepareLegacyUnityInstallResume} from '../migrations/legacy-unity-install-resume.js';
6
+ import {prepareLegacyUnityInstallRecovery} from '../migrations/legacy-unity-install-recovery.js';
7
+ import {prepareLegacyUnityCloseoutResume} from '../migrations/legacy-unity-resume.js';
8
+ import {prepareLegacyUnityCompensation} from '../migrations/legacy-unity-compensate.js';
9
+ import {bindMigrationInstaller,readInstallerIdentity} from '../operations/installer-identity.js';
10
+ import {contractDigest} from '../contracts/semantic.js';
11
+ import {runMigrationApply} from './migration-apply.js';
12
+ import {mkdtemp} from 'node:fs/promises';
13
+ import {tmpdir} from 'node:os';
14
+ import path from 'node:path';
15
+
16
+ const inspectors={deactivation:prepareLegacyUnityDeactivationResume,installation:prepareLegacyUnityInstallResume,
17
+ recovery:prepareLegacyUnityInstallRecovery,closeout:prepareLegacyUnityCloseoutResume,compensation:prepareLegacyUnityCompensation};
18
+ export function parseMigrationCommand(args){
19
+ if(args[1]!=='unity'||!['preview','inspect','apply'].includes(args[2]))fail('cli.arguments');
20
+ const result={command:'migration',action:args[2]},seen=new Set();
21
+ const allowed=result.action==='apply'?['--workspace','--preview','--json']:result.action==='preview'?['--workspace','--manifest','--network','--json']:['--workspace','--recovery','--phase','--json'];
22
+ for(let i=3;i<args.length;i++){
23
+ const flag=args[i];if(!allowed.includes(flag)||seen.has(flag))fail('cli.arguments');seen.add(flag);
24
+ if(flag==='--json')continue;
25
+ if(flag==='--network'){result.network=true;continue;}
26
+ const value=args[++i];if(value===undefined||value.startsWith('--'))fail('cli.arguments');
27
+ if(flag==='--workspace')result.workspace=absoluteRoot(value);
28
+ if(flag==='--manifest')result.manifestPath=absoluteRoot(value);
29
+ if(flag==='--recovery')result.recoveryPath=value;
30
+ if(flag==='--phase')result.phase=value;
31
+ if(flag==='--preview')result.previewFile=absoluteRoot(value);
32
+ }
33
+ if(!result.workspace)fail('cli.workspace-required');
34
+ if(result.action==='preview'&&!result.manifestPath)fail('cli.arguments');
35
+ if(result.action==='apply'&&!result.previewFile)fail('cli.arguments');
36
+ if(result.action==='inspect'&&(!Object.hasOwn(inspectors,result.phase??'')||typeof result.recoveryPath!=='string'||
37
+ !/^\.pipeline\/migrations\/[a-f0-9-]{36}\/recovery\.json$/.test(result.recoveryPath)))fail('cli.arguments');
38
+ return result;
39
+ }
40
+ export async function runMigrationCommand(command,stdout,stderr){
41
+ if(command.action==='apply')return runMigrationApply(command,stdout,stderr);
42
+ await stderr('Migration preview may contain private configuration bytes. Save it locally; do not publish it. Inspect before explicit apply.\n');
43
+ const before=await readInstallerIdentity();
44
+ const result=command.action==='preview'?await prepareLegacyUnityPreview({wrapper:command.workspace,manifestPath:command.manifestPath,network:command.network===true,
45
+ tempRoot:await mkdtemp(path.join(tmpdir(),'wpc-migration-prepare-'))}):
46
+ await inspectors[command.phase](command.workspace,command.recoveryPath);
47
+ const bound=await bindMigrationInstaller(result);
48
+ if(contractDigest(before)!==contractDigest(bound.installer))fail('installer.changed');
49
+ await stdout(JSON.stringify(bound)+'\n');return result.status==='blocked'?1:0;
50
+ }
@@ -0,0 +1,61 @@
1
+ import path from 'node:path';
2
+ import {lstat} from 'node:fs/promises';
3
+ import {absoluteRoot,resolveChild} from '../workspace/paths.js';
4
+ import {readRecord} from '../operations/state.js';
5
+ import {readRepositoryInputs} from '../operations/repository-inputs.js';
6
+ import {contractDigest} from '../contracts/semantic.js';
7
+ import {fail} from '../contracts/parse.js';
8
+ import {bootstrapLockDirectory} from '../operations/bootstrap-lock.js';
9
+ import {withRecoveryLease,assertRecoveryLeaseHeld} from '../operations/recovery-lease.js';
10
+ import {inspectRepositoryReconciliation,finalizeRepositoryOperations,resumeRepositoryFinalization} from '../operations/repository-reconcile.js';
11
+
12
+ async function exists(p){try{await lstat(p);return true;}catch(e){if(e.code==='ENOENT')return false;throw e;}}
13
+ export async function prepareRepositoryFinalization(wrapper) {
14
+ wrapper=absoluteRoot(wrapper);
15
+ const markerPath=resolveChild(wrapper,'.pipeline/repository-operation.json');
16
+ const bootstrap=bootstrapLockDirectory(wrapper),blockers=[];
17
+ for(const p of [bootstrap,bootstrap+'.recovery',bootstrap+'.recovery-resume',resolveChild(wrapper,'.pipeline/lock')])
18
+ if(await exists(p))blockers.push({code:'repositories.recovery-lock-present',path:p});
19
+ if(!await exists(markerPath))return {status:'no-pending-marker',wrapper,blockers,executionAuthorized:false,
20
+ completionVerified:false,pipelineActivated:false};
21
+ const marker=await readRecord(markerPath);
22
+ const input=await readRepositoryInputs(wrapper,marker.value.journal,marker.value.previewDigest);
23
+ const checked=await inspectRepositoryReconciliation(wrapper,input.previewText,input.previewDigest);
24
+ if(!checked.canFinalize)blockers.push({code:'repositories.effects-unresolved'});
25
+ const receiptPath=resolveChild(wrapper,'.pipeline/repository-completions/'+path.basename(checked.journal)+'.json');
26
+ const receipt=await exists(receiptPath)?await readRecord(receiptPath):null;
27
+ if(receipt && contractDigest(receipt.value)!==contractDigest({schemaVersion:1,status:'repository-effects-verified',
28
+ reconciliation:checked,pipelineActivated:false}))fail('repositories.recovery-receipt');
29
+ if((await readRecord(markerPath)).digest!==marker.digest ||
30
+ (await readRecord(input.path)).digest!==input.recordDigest)fail('repositories.recovery-drift');
31
+ const body={schemaVersion:1,kind:'repository-finalization-preview',wrapper,
32
+ status:blockers.length?'blocked':'review-only',blockers,action:receipt?'resume-finalization':'finalize',
33
+ inputDigest:input.recordDigest,markerDigest:marker.digest,reconciliation:checked,
34
+ receiptDigest:receipt?.digest??null,executionAuthorized:false,pipelineActivated:false};
35
+ return {...body,digest:contractDigest(body)};
36
+ }
37
+ export async function applyRepositoryFinalization(wrapper,preview) {
38
+ wrapper=absoluteRoot(wrapper);
39
+ const {digest,...body}=preview??{};
40
+ if(body.kind!=='repository-finalization-preview' || body.wrapper!==wrapper ||
41
+ body.status!=='review-only' || contractDigest(body)!==digest)fail('repositories.recovery-approval');
42
+ return withRecoveryLease(wrapper,async lease=>{
43
+ assertRecoveryLeaseHeld(lease);
44
+ const fresh=await prepareRepositoryFinalization(wrapper);
45
+ if(contractDigest(fresh)!==contractDigest(preview))fail('repositories.recovery-stale');
46
+ const input=await readRepositoryInputs(wrapper,fresh.reconciliation.journal,fresh.reconciliation.previewDigest);
47
+ const run=fresh.action==='resume-finalization'?resumeRepositoryFinalization:finalizeRepositoryOperations;
48
+ return run({wrapper,lease,previewText:input.previewText,previewDigest:input.previewDigest,
49
+ approval:{decision:'approve',reconciliationDigest:fresh.reconciliation.digest}});
50
+ });
51
+ }
52
+ export async function runRepositoryRecovery(command,stdout) {
53
+ if(command.apply) {
54
+ const preview=(await readRecord(command.previewFile)).value;
55
+ const result=await applyRepositoryFinalization(command.workspace,preview);
56
+ await stdout(JSON.stringify(result)+'\n');return 0;
57
+ }
58
+ const result=await prepareRepositoryFinalization(command.workspace);
59
+ await stdout(JSON.stringify(result)+'\n');
60
+ return result.status==='review-only' || (result.status==='no-pending-marker' && !result.blockers.length)?0:1;
61
+ }
@@ -0,0 +1,16 @@
1
+ import {prepareRepositoryAbandon,inspectRepositoryAbandon,applyRepositoryAbandon} from '../operations/repository-abandon.js';
2
+ import {readRecord} from '../operations/state.js';
3
+ import {fail} from '../contracts/parse.js';
4
+
5
+ export async function runRepositoryAbandon(command,stdout){
6
+ const continuation=command.action==='continue-abandon';
7
+ if(command.apply){
8
+ const preview=(await readRecord(command.previewFile)).value;
9
+ if(continuation && preview.attempt!==command.attempt)fail('repository-abandon.attempt');
10
+ const result=await applyRepositoryAbandon(command.workspace,preview,{continuation});
11
+ await stdout(JSON.stringify(result)+'\n');return 0;
12
+ }
13
+ const result=continuation?await inspectRepositoryAbandon(command.workspace,command.attempt):
14
+ await prepareRepositoryAbandon(command.workspace);
15
+ await stdout(JSON.stringify(result)+'\n');return 0;
16
+ }
@@ -0,0 +1,21 @@
1
+ import {prepareRepositoryAncestors,applyRepositoryAncestors,inspectRepositoryAncestors,finishRepositoryAncestors} from '../operations/repository-ancestors.js';
2
+ import {readRecord} from '../operations/state.js';
3
+ import {contractDigest} from '../contracts/semantic.js';
4
+ import {fail} from '../contracts/parse.js';
5
+
6
+ export async function runRepositoryAncestors(c,stdout){
7
+ let result;
8
+ if(c.action==='prepare-parent'){
9
+ if(c.apply){const p=(await readRecord(c.previewFile)).value;if(p.wrapper!==c.workspace)fail('ancestors.workspace');result=await applyRepositoryAncestors(p);}
10
+ else result=await prepareRepositoryAncestors(c.workspace);
11
+ }else if(!c.apply){
12
+ const p=(await readRecord(c.parentPreview)).value;if(p.wrapper!==c.workspace)fail('ancestors.workspace');
13
+ const observation=await inspectRepositoryAncestors(p),body={kind:'repository-parent-continuation-preview',wrapper:c.workspace,parent:p,observation,executionAuthorized:false};
14
+ result={...body,digest:contractDigest(body)};
15
+ }else{
16
+ const p=(await readRecord(c.previewFile)).value,{digest,...body}=p;
17
+ if(p.kind!=='repository-parent-continuation-preview' || p.wrapper!==c.workspace || p.parent?.wrapper!==c.workspace || contractDigest(body)!==digest)fail('ancestors.approval');
18
+ result=await finishRepositoryAncestors(p.parent,p.observation?.digest);
19
+ }
20
+ await stdout(JSON.stringify(result)+'\n');return 0;
21
+ }
@@ -0,0 +1,77 @@
1
+ import path from 'node:path';
2
+ import {lstat} from 'node:fs/promises';
3
+ import {absoluteRoot,resolveChild} from '../workspace/paths.js';
4
+ import {fail} from '../contracts/parse.js';
5
+ import {contractDigest} from '../contracts/semantic.js';
6
+ import {readRecord} from '../operations/state.js';
7
+ import {readRepositoryInputs} from '../operations/repository-inputs.js';
8
+ import {bootstrapLockDirectory} from '../operations/bootstrap-lock.js';
9
+ import {inspectRepositoryReconciliation,verifyRepositoryCompletion} from '../operations/repository-reconcile.js';
10
+ import {inspectRepositoryLocks,inspectRepositoryLockRecovery,inspectRepositoryLockResumption,
11
+ recoverRepositoryLocks,finishRepositoryLockRecovery} from '../operations/repository-lock-reconcile.js';
12
+
13
+ const uuid=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
14
+ async function exists(filename){try{await lstat(filename);return true;}catch(e){if(e.code==='ENOENT')return false;throw e;}}
15
+
16
+ // Explicit journal selection: never guess the newest operation from timestamps,
17
+ // UUID ordering or equal desired-state hashes. All recovery uses retained inputs.
18
+ async function prepare(wrapper,journalId) {
19
+ wrapper=absoluteRoot(wrapper);if(!uuid.test(journalId??''))fail('repositories.journal-required');
20
+ const journal='.pipeline/repository-journals/'+journalId;
21
+ const raw=await readRecord(resolveChild(wrapper,'.pipeline/repository-inputs/'+journalId+'.json'));
22
+ const input=await readRepositoryInputs(wrapper,journal,raw.value?.preview?.digest);
23
+ const args={wrapper,journal,previewText:input.previewText,previewDigest:input.previewDigest};
24
+ if(await exists(resolveChild(wrapper,'.pipeline/repository-operation.json'))) {
25
+ const subject=await inspectRepositoryReconciliation(wrapper,input.previewText,input.previewDigest);
26
+ if(subject.journal!==journal || !subject.canFinalize)fail('repositories.effects-unresolved');
27
+ args.reconciliationDigest=subject.digest;
28
+ } else {
29
+ const receipt=await readRecord(resolveChild(wrapper,'.pipeline/repository-completions/'+journalId+'.json'));
30
+ args.reconciliationDigest=receipt.value?.reconciliation?.digest;
31
+ await verifyRepositoryCompletion(args);
32
+ }
33
+ const base=bootstrapLockDirectory(wrapper);let action,observation;
34
+ if(await exists(base+'.recovery-resume')) {
35
+ const approval=await readRecord(path.join(base+'.recovery-resume','approval.json'));
36
+ args.lockDigest=approval.value?.observation?.lockDigest;
37
+ args.recoveryDigest=approval.value?.observation?.digest;
38
+ action='continue';observation=await inspectRepositoryLockResumption(args);
39
+ } else if(await exists(base+'.recovery')) {
40
+ const owner=await readRecord(path.join(base+'.recovery','owner.json'));
41
+ args.lockDigest=owner.value?.lockDigest;
42
+ action='resume';observation=await inspectRepositoryLockRecovery(args);
43
+ } else {
44
+ action='retire';observation=await inspectRepositoryLocks(args);
45
+ }
46
+ const stopped=action==='retire'?observation.ownerLiveness==='local-pid-absent':
47
+ action==='resume'?observation.recoveryOwnerLiveness==='local-pid-absent' &&
48
+ observation.locations.every(l=>l.ownerLiveness==='local-pid-absent'):
49
+ observation.resumptionOwnerLiveness==='local-pid-absent' &&
50
+ observation.current.recoveryOwnerLiveness==='local-pid-absent' &&
51
+ observation.current.locations.every(l=>l.ownerLiveness==='local-pid-absent');
52
+ if((await readRecord(input.path)).digest!==input.recordDigest)fail('repositories.recovery-drift');
53
+ const body={schemaVersion:1,kind:'repository-lock-recovery-preview',wrapper,journalId,action,
54
+ inputDigest:input.recordDigest,observation,status:stopped?'review-only':'blocked',
55
+ blockers:stopped?[]:[{code:'repositories.recovery-owner-unconfirmed'}],executionAuthorized:false,pipelineActivated:false};
56
+ return {preview:{...body,digest:contractDigest(body)},args};
57
+ }
58
+ export async function prepareRepositoryLockRecovery(wrapper,journalId){return (await prepare(wrapper,journalId)).preview;}
59
+
60
+ export async function applyRepositoryLockRecovery(wrapper,journalId,preview) {
61
+ wrapper=absoluteRoot(wrapper);const {digest,...body}=preview??{};
62
+ if(body.kind!=='repository-lock-recovery-preview' || body.wrapper!==wrapper || body.journalId!==journalId ||
63
+ body.status!=='review-only' || contractDigest(body)!==digest)fail('repositories.recovery-approval');
64
+ const fresh=await prepare(wrapper,journalId);
65
+ if(contractDigest(fresh.preview)!==contractDigest(preview))fail('repositories.recovery-stale');
66
+ // Native writers acquire the kernel lease and independently recompute the exact
67
+ // approved observation before mutation. Do not acquire a competing nested lease.
68
+ const key={retire:'lockDigest',resume:'recoveryDigest',continue:'resumptionDigest'}[body.action];
69
+ const run=body.action==='retire'?recoverRepositoryLocks:finishRepositoryLockRecovery;
70
+ return run({...fresh.args,approval:{decision:'approve',[key]:fresh.preview.observation.digest}});
71
+ }
72
+
73
+ export async function runRepositoryLockRecovery(command,stdout) {
74
+ const result=command.apply?await applyRepositoryLockRecovery(command.workspace,command.journalId,
75
+ (await readRecord(command.previewFile)).value):await prepareRepositoryLockRecovery(command.workspace,command.journalId);
76
+ await stdout(JSON.stringify(result)+'\n');return result.status==='blocked'?1:0;
77
+ }
@@ -0,0 +1,57 @@
1
+ import { parseAllDocuments, isMap, isSeq, isScalar, isAlias } from 'yaml';
2
+
3
+ export class ContractError extends Error {
4
+ constructor(code) { super(code); this.name = 'ContractError'; this.code = code; }
5
+ }
6
+ export const fail = code => { throw new ContractError(code); };
7
+ export const MAX_INPUT_BYTES = 2 * 1024 * 1024;
8
+
9
+ // Do not expose parser messages: they can contain source values and credentials.
10
+ export function parse(text, format = 'yaml') {
11
+ if (typeof text !== 'string' || !['json', 'yaml'].includes(format)) fail('parse.input');
12
+ if (Buffer.byteLength(text, 'utf8') > MAX_INPUT_BYTES) fail('parse.size');
13
+ if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
14
+ if (format === 'json') {
15
+ try { JSON.parse(text); } catch { fail('parse.syntax'); }
16
+ }
17
+ let docs;
18
+ try {
19
+ docs = parseAllDocuments(text, { version: '1.2', schema: 'core', uniqueKeys: true,
20
+ strict: true, prettyErrors: false, merge: false, logLevel: 'silent' });
21
+ } catch { fail('parse.syntax'); }
22
+ if (docs.length !== 1) fail('parse.documents');
23
+ const doc = docs[0];
24
+ if (doc.errors.length || doc.warnings.length) fail('parse.syntax');
25
+ if (doc.directives.yaml.explicit && doc.directives.yaml.version !== '1.2') fail('parse.version');
26
+ let count = 0;
27
+ function inspect(node, depth) {
28
+ if (++count > 50000 || depth > 64) fail('parse.complexity');
29
+ if (node === null) return;
30
+ if (isAlias(node) || node.anchor) fail('parse.alias');
31
+ if (node.tag) fail('parse.tag');
32
+ }
33
+ function convert(node, depth) {
34
+ inspect(node, depth);
35
+ if (node === null) return null;
36
+ if (isMap(node)) {
37
+ const result = Object.create(null), keys = new Set();
38
+ for (const pair of node.items) {
39
+ inspect(pair.key, depth + 1);
40
+ if (!isScalar(pair.key) || typeof pair.key.value !== 'string') fail('parse.key');
41
+ const key = pair.key.value;
42
+ if (keys.has(key)) fail('parse.duplicate');
43
+ if (['__proto__', 'constructor', 'prototype', '<<'].includes(key)) fail('parse.key');
44
+ keys.add(key);
45
+ result[key] = convert(pair.value, depth + 1);
46
+ }
47
+ return result;
48
+ }
49
+ if (isSeq(node)) return node.items.map(n => convert(n, depth + 1));
50
+ if (!isScalar(node)) fail('parse.type');
51
+ const v = node.value;
52
+ if (typeof v === 'number' && (!Number.isFinite(v) || (Number.isInteger(v) && !Number.isSafeInteger(v)))) fail('parse.number');
53
+ if (v !== null && !['string', 'boolean', 'number'].includes(typeof v)) fail('parse.type');
54
+ return v;
55
+ }
56
+ return convert(doc.contents, 0);
57
+ }