owlrunkit 0.15.0 → 0.16.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 (76) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +274 -66
  3. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +223 -117
  4. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  5. package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +2 -2
  6. package/package.json +2 -2
  7. package/packages/attest/distribution-manifest-v1.json +10 -0
  8. package/packages/attest/src/quick.mjs +10 -0
  9. package/registry-release-policy-v1.json +8 -2
  10. package/sbom.spdx.json +5 -5
  11. package/scripts/runkit-contract/assurance-router.mjs +119 -0
  12. package/scripts/runkit-contract/core-contract.mjs +250 -115
  13. package/scripts/runkit-contract/core-successor.mjs +2043 -0
  14. package/scripts/runkit-contract/deployment-workflow.mjs +2040 -0
  15. package/scripts/runkit-contract/finalize.mjs +242 -21
  16. package/scripts/runkit-contract/fleet-discovery.mjs +456 -0
  17. package/scripts/runkit-contract/formal-workflow.mjs +850 -0
  18. package/scripts/runkit-contract/human-status.mjs +553 -0
  19. package/scripts/runkit-contract/inspect-presentation.mjs +25 -8
  20. package/scripts/runkit-contract/install-codex-skill.mjs +73 -30
  21. package/scripts/runkit-contract/lease-lifecycle.mjs +8 -2
  22. package/scripts/runkit-contract/lifecycle-orchestration.mjs +135 -12
  23. package/scripts/runkit-contract/onboarding-doctor.mjs +43 -90
  24. package/scripts/runkit-contract/owner-authority-trust.mjs +230 -0
  25. package/scripts/runkit-contract/owner-deployment-decision.mjs +385 -0
  26. package/scripts/runkit-contract/profile-onboarding.mjs +841 -13
  27. package/scripts/runkit-contract/project-cli-resolver.mjs +701 -0
  28. package/scripts/runkit-contract/project-control-parser.mjs +830 -0
  29. package/scripts/runkit-contract/project-control-state.mjs +28 -0
  30. package/scripts/runkit-contract/provenance-common.mjs +19 -2
  31. package/scripts/runkit-contract/ready-for-commit.mjs +330 -18
  32. package/scripts/runkit-contract/remote-deployment.mjs +1596 -0
  33. package/scripts/runkit-contract/remote-process-adapter.mjs +177 -0
  34. package/scripts/runkit-contract/runkit-bootstrap.mjs +131 -0
  35. package/scripts/runkit-contract/runkit-cli.mjs +1097 -21
  36. package/scripts/runkit-contract/schemas/assurance-route-v1.schema.json +26 -0
  37. package/scripts/runkit-contract/schemas/core-successor-batch-receipt-v1.schema.json +126 -0
  38. package/scripts/runkit-contract/schemas/core-successor-plan-v1.schema.json +155 -0
  39. package/scripts/runkit-contract/schemas/core-successor-project-success-receipt-v1.schema.json +72 -0
  40. package/scripts/runkit-contract/schemas/deployment-execute-request-v1.schema.json +63 -0
  41. package/scripts/runkit-contract/schemas/deployment-lineage-v1.schema.json +292 -0
  42. package/scripts/runkit-contract/schemas/deployment-prepare-receipt-v1.schema.json +140 -0
  43. package/scripts/runkit-contract/schemas/fleet-discovery-v1.schema.json +31 -0
  44. package/scripts/runkit-contract/schemas/fleet-manifest-v1.schema.json +20 -0
  45. package/scripts/runkit-contract/schemas/fleet-root-registry-v1.schema.json +34 -0
  46. package/scripts/runkit-contract/schemas/human-status-v1.schema.json +102 -0
  47. package/scripts/runkit-contract/schemas/owner-authority-trust-v1.schema.json +66 -0
  48. package/scripts/runkit-contract/schemas/owner-deployment-authority-v1.schema.json +46 -0
  49. package/scripts/runkit-contract/schemas/owner-deployment-authority-v2.schema.json +94 -0
  50. package/scripts/runkit-contract/schemas/owner-deployment-decision-v1.schema.json +102 -0
  51. package/scripts/runkit-contract/schemas/owner-migration-authority-v1.schema.json +33 -0
  52. package/scripts/runkit-contract/schemas/owner-migration-authority-v2.schema.json +67 -0
  53. package/scripts/runkit-contract/schemas/profile-apply-receipt-v1.schema.json +32 -0
  54. package/scripts/runkit-contract/schemas/profile-detection-v2.schema.json +145 -0
  55. package/scripts/runkit-contract/schemas/ready-for-commit-v2.schema.json +37 -0
  56. package/scripts/runkit-contract/schemas/remote-deployment-manifest-v1.schema.json +379 -0
  57. package/scripts/runkit-contract/schemas/remote-deployment-result-v1.schema.json +153 -0
  58. package/scripts/runkit-contract/schemas/remote-deployment-stage-journal-v1.schema.json +183 -0
  59. package/scripts/runkit-contract/schemas/remote-target-v1.schema.json +32 -0
  60. package/scripts/runkit-contract/schemas/source-candidate-v2.schema.json +152 -0
  61. package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +13 -0
  62. package/scripts/runkit-contract/snapshot.mjs +1 -1
  63. package/scripts/runkit-contract/source-candidate.mjs +1382 -0
  64. package/scripts/runkit-contract/ssh-remote-adapter.mjs +1184 -0
  65. package/scripts/runkit-contract/sync-core016-release-identity.mjs +161 -0
  66. package/scripts/runkit-contract/templates/assurance-request.json +13 -0
  67. package/scripts/runkit-contract/templates/core-successor-owner-authority.json +15 -0
  68. package/scripts/runkit-contract/templates/deployment-execute-request.json +39 -0
  69. package/scripts/runkit-contract/templates/deployment-owner-authority.json +23 -0
  70. package/scripts/runkit-contract/templates/deployment-owner-decision.json +26 -0
  71. package/scripts/runkit-contract/templates/fleet-manifest.json +8 -0
  72. package/scripts/runkit-contract/templates/fleet-root-registry.json +8 -0
  73. package/scripts/runkit-contract/templates/owner-authority-trust.json +16 -0
  74. package/scripts/runkit-contract/templates/ready-for-commit-request.json +2 -2
  75. package/scripts/runkit-contract/templates/remote-deployment-manifest.json +108 -0
  76. package/scripts/runkit-contract/verification-receipt-gate.mjs +28 -0
@@ -8,7 +8,6 @@ import {
8
8
  mkdtemp,
9
9
  readFile,
10
10
  readdir,
11
- realpath,
12
11
  rename,
13
12
  rm,
14
13
  writeFile,
@@ -18,10 +17,11 @@ import { pathToFileURL } from "node:url";
18
17
 
19
18
  import {
20
19
  initializeProjectRunKit,
21
- inspectProjectUpgradeSafety,
22
20
  isDirectExecution,
23
21
  validateProjectConfigV2,
24
22
  } from "./core-contract.mjs";
23
+ import { discoverFleet } from "./fleet-discovery.mjs";
24
+ import { inspectProjectControlState } from "./project-control-state.mjs";
25
25
 
26
26
  const MANIFEST_FILE = ".owlcoda-install-manifest.json";
27
27
 
@@ -193,21 +193,39 @@ export async function inspectInstalledSkill({ targetRoot } = {}) {
193
193
  };
194
194
  }
195
195
 
196
- async function inspectFleetForSkillChange(workspaceRoots) {
197
- if (!Array.isArray(workspaceRoots) || workspaceRoots.length === 0) {
198
- throw new Error("skill_update_fleet_preflight_required");
199
- }
200
- const roots = [];
201
- for (const workspaceRoot of workspaceRoots) {
202
- if (typeof workspaceRoot !== "string" || workspaceRoot.length === 0) {
203
- throw new Error("skill_update_fleet_workspace_invalid");
196
+ export async function inspectFleetForSkillChange({
197
+ workspaceRoots = null,
198
+ fleetManifestPath = null,
199
+ fleetRoots = null,
200
+ fleetRegistryPath = null,
201
+ } = {}) {
202
+ let discovery;
203
+ try {
204
+ discovery = await discoverFleet({
205
+ workspaceRoots,
206
+ fleetManifestPath,
207
+ fleetRoots,
208
+ fleetRegistryPath,
209
+ });
210
+ } catch (error) {
211
+ if (
212
+ workspaceRoots === null
213
+ && fleetManifestPath === null
214
+ && fleetRoots === null
215
+ && error?.message === "fleet_registry_missing"
216
+ ) {
217
+ throw new Error("skill_update_fleet_registry_missing");
204
218
  }
205
- roots.push(await realpath(workspaceRoot));
219
+ throw error;
220
+ }
221
+ if (!discovery.complete) {
222
+ throw new Error(
223
+ `skill_update_fleet_discovery_incomplete:${discovery.unreachableRoots.join(",")}`,
224
+ );
206
225
  }
207
- const fleet = [...new Set(roots)].sort();
208
226
  const projects = [];
209
- for (const workspaceRoot of fleet) {
210
- const safety = inspectProjectUpgradeSafety({ workspaceRoot });
227
+ for (const workspaceRoot of discovery.workspaceRoots) {
228
+ const safety = inspectProjectControlState({ workspaceRoot }).upgradeSafety;
211
229
  if (safety.status !== "safe") {
212
230
  const blockers = [
213
231
  ...safety.activeRunIds,
@@ -266,6 +284,9 @@ export async function installCodexSkill({
266
284
  archiveRoot,
267
285
  expectedUnmanagedManifestSha256 = null,
268
286
  workspaceRoots = null,
287
+ fleetManifestPath = null,
288
+ fleetRoots = null,
289
+ fleetRegistryPath = null,
269
290
  } = {}) {
270
291
  if (typeof repositoryRoot !== "string" || typeof targetRoot !== "string" || typeof archiveRoot !== "string") {
271
292
  throw new Error("repositoryRoot, targetRoot, and archiveRoot are required");
@@ -300,7 +321,12 @@ export async function installCodexSkill({
300
321
  }
301
322
  return { status: "unchanged", manifestSha256: desired.manifestSha256, previousInstallation: null };
302
323
  }
303
- const projects = await inspectFleetForSkillChange(workspaceRoots);
324
+ const projects = await inspectFleetForSkillChange({
325
+ workspaceRoots,
326
+ fleetManifestPath,
327
+ fleetRoots,
328
+ fleetRegistryPath,
329
+ });
304
330
  await mkdir(archiveRoot, { recursive: true });
305
331
  const archivePath = await nextAvailablePath(
306
332
  archiveRoot,
@@ -438,7 +464,7 @@ export async function restoreCodexSkill({
438
464
  throw new Error("skill_rollback_prior_identity_mismatch");
439
465
  }
440
466
  const workspaceRoots = receipt.projects.map((project) => project.workspaceRoot);
441
- const currentProjects = await inspectFleetForSkillChange(workspaceRoots);
467
+ const currentProjects = await inspectFleetForSkillChange({ workspaceRoots });
442
468
  const receiptProjectByRoot = new Map(
443
469
  receipt.projects.map((project) => [project.workspaceRoot, project]),
444
470
  );
@@ -516,17 +542,22 @@ export async function recoverCodexSkillForActiveFleet({
516
542
  targetRoot,
517
543
  archiveRoot,
518
544
  archivedInstallationPath,
519
- workspaceRoots,
545
+ workspaceRoots = null,
546
+ fleetManifestPath = null,
547
+ fleetRoots = null,
520
548
  } = {}) {
521
549
  if (
522
550
  typeof targetRoot !== "string"
523
551
  || typeof archiveRoot !== "string"
524
552
  || typeof archivedInstallationPath !== "string"
525
- || !Array.isArray(workspaceRoots)
526
- || workspaceRoots.length === 0
553
+ || (
554
+ (!Array.isArray(workspaceRoots) || workspaceRoots.length === 0)
555
+ && (typeof fleetManifestPath !== "string" || fleetManifestPath.length === 0)
556
+ && (!Array.isArray(fleetRoots) || fleetRoots.length === 0)
557
+ )
527
558
  ) {
528
559
  throw new Error(
529
- "targetRoot, archiveRoot, archivedInstallationPath, and workspaceRoots are required",
560
+ "targetRoot, archiveRoot, archivedInstallationPath, and a fleet source are required",
530
561
  );
531
562
  }
532
563
  const resolvedTarget = path.resolve(targetRoot);
@@ -553,14 +584,19 @@ export async function recoverCodexSkillForActiveFleet({
553
584
  if (!archivedCore || typeof archivedCore.coreManifestSha256 !== "string") {
554
585
  throw new Error("skill_recovery_archived_core_identity_missing");
555
586
  }
556
- const fleetRoots = [];
557
- for (const workspaceRoot of workspaceRoots) {
558
- fleetRoots.push(await realpath(workspaceRoot));
587
+ const discovery = await discoverFleet({
588
+ workspaceRoots,
589
+ fleetManifestPath,
590
+ fleetRoots,
591
+ });
592
+ if (!discovery.complete) {
593
+ throw new Error(
594
+ `skill_recovery_fleet_discovery_incomplete:${discovery.unreachableRoots.join(",")}`,
595
+ );
559
596
  }
560
- const fleet = [...new Set(fleetRoots)].sort();
561
597
  const projects = [];
562
598
  let activeRunCount = 0;
563
- for (const workspaceRoot of fleet) {
599
+ for (const workspaceRoot of discovery.workspaceRoots) {
564
600
  const configPath = path.join(
565
601
  workspaceRoot,
566
602
  ".owlcoda/runkit/config.json",
@@ -572,7 +608,10 @@ export async function recoverCodexSkillForActiveFleet({
572
608
  `skill_recovery_config_core_mismatch:${workspaceRoot}`,
573
609
  );
574
610
  }
575
- const safety = inspectProjectUpgradeSafety({ workspaceRoot });
611
+ const safety = inspectProjectControlState({
612
+ workspaceRoot,
613
+ currentCore: archivedCore,
614
+ }).upgradeSafety;
576
615
  if (safety.issues.length > 0) {
577
616
  throw new Error(
578
617
  `skill_recovery_control_state_invalid:${workspaceRoot}:${safety.issues.join(",")}`,
@@ -665,9 +704,9 @@ function parseOptions(values) {
665
704
  const value = values[index + 1];
666
705
  if (!flag?.startsWith("--") || value === undefined) throw new Error("Options must be --name value pairs");
667
706
  const name = flag.slice(2);
668
- if (name === "workspace") {
669
- options.workspace ??= [];
670
- options.workspace.push(value);
707
+ if (name === "workspace" || name === "fleet-root") {
708
+ options[name] ??= [];
709
+ options[name].push(value);
671
710
  } else {
672
711
  options[name] = value;
673
712
  }
@@ -686,6 +725,8 @@ export async function runCli(argv = process.argv.slice(2)) {
686
725
  archiveRoot: options.archive,
687
726
  expectedUnmanagedManifestSha256: options["expected-unmanaged"] ?? null,
688
727
  workspaceRoots: options.workspace ?? null,
728
+ fleetManifestPath: options["fleet-manifest"] ?? null,
729
+ fleetRoots: options["fleet-root"] ?? null,
689
730
  });
690
731
  return { exitCode: 0, ...output };
691
732
  }
@@ -706,7 +747,9 @@ export async function runCli(argv = process.argv.slice(2)) {
706
747
  targetRoot: options.target,
707
748
  archiveRoot: options.archive,
708
749
  archivedInstallationPath: options["archived-installation"],
709
- workspaceRoots: options.workspace,
750
+ workspaceRoots: options.workspace ?? null,
751
+ fleetManifestPath: options["fleet-manifest"] ?? null,
752
+ fleetRoots: options["fleet-root"] ?? null,
710
753
  });
711
754
  return { exitCode: 0, ...output };
712
755
  }
@@ -196,9 +196,15 @@ function acquireControlLock(workspaceRoot) {
196
196
  export function withControlTransaction(workspaceRoot, operation) {
197
197
  const release = acquireControlLock(workspaceRoot);
198
198
  try {
199
- return operation();
200
- } finally {
199
+ const result = operation();
200
+ if (result && typeof result.then === "function") {
201
+ return Promise.resolve(result).finally(release);
202
+ }
203
+ release();
204
+ return result;
205
+ } catch (error) {
201
206
  release();
207
+ throw error;
202
208
  }
203
209
  }
204
210
 
@@ -30,6 +30,7 @@ import {
30
30
  verifySnapshotSourceBinding,
31
31
  } from "./snapshot.mjs";
32
32
  import { verifyDeliveryPacket } from "./source-fingerprint.mjs";
33
+ import { verifySourceCandidateV2 } from "./source-candidate.mjs";
33
34
  import { validateVerificationReceiptGate } from "./verification-receipt-gate.mjs";
34
35
 
35
36
  const LOCKFILES = [
@@ -53,6 +54,12 @@ function entryExists(filePath) {
53
54
  }
54
55
  }
55
56
 
57
+ function sortedFileMap(value) {
58
+ return Object.fromEntries(
59
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)),
60
+ );
61
+ }
62
+
56
63
  function within(root, candidate) {
57
64
  const relative = path.relative(root, candidate);
58
65
  return relative === ""
@@ -479,11 +486,84 @@ export function activeAcceptedGate({ workspaceRoot, runId }) {
479
486
  }
480
487
  }
481
488
  const distinctMatchingPackets = collapseByteIdenticalDeliveryCandidates(matchingPackets);
482
- if (distinctMatchingPackets.length !== 1
483
- || distinctMatchingPackets[0].sourceGate.status !== "valid") {
484
- throw new Error("Accepted finish requires one fresh delivery packet matching the active verification gate; source drift remains unresolved.");
489
+ let matchingCandidate = null;
490
+ if (distinctMatchingPackets.length === 0) {
491
+ const candidateRoot = path.join(executionRoot, "source-candidates");
492
+ projectTruthDirectory({
493
+ workspaceRoot,
494
+ executionRoot,
495
+ directoryPath: candidateRoot,
496
+ label: "Source candidate directory",
497
+ });
498
+ const candidates = [];
499
+ for (const entry of readdirSync(candidateRoot, { withFileTypes: true })) {
500
+ if (!entry.name.endsWith(".json")) continue;
501
+ if (entry.isSymbolicLink() || !entry.isFile()) {
502
+ throw new Error(`Source candidate must be a regular file: ${entry.name}`);
503
+ }
504
+ const candidatePath = path.join(candidateRoot, entry.name);
505
+ const candidate = readJson(regularFile(
506
+ candidatePath,
507
+ `Source candidate ${entry.name}`,
508
+ ));
509
+ if (
510
+ candidate.runId === runId
511
+ && candidate.sourceFingerprint?.sha256
512
+ === validated.active.receipt.sourceFingerprint
513
+ ) {
514
+ const sourceGate = verifySourceCandidateV2({
515
+ workspaceRoot,
516
+ candidatePath: relativeToWorkspace(workspaceRoot, candidatePath),
517
+ });
518
+ if (sourceGate.status === "valid") {
519
+ candidates.push({ candidatePath, candidate, sourceGate });
520
+ }
521
+ }
522
+ }
523
+ const manifestFingerprints = new Set(
524
+ candidates.map(({ candidate }) => candidate.sourceManifest.sha256),
525
+ );
526
+ if (manifestFingerprints.size === 1) {
527
+ [matchingCandidate] = candidates;
528
+ }
529
+ }
530
+ if (
531
+ matchingCandidate === null
532
+ && (
533
+ distinctMatchingPackets.length !== 1
534
+ || distinctMatchingPackets[0].sourceGate.status !== "valid"
535
+ )
536
+ ) {
537
+ throw new Error("Accepted finish requires one fresh source binding matching the active verification gate; source drift remains unresolved.");
485
538
  }
486
539
  const [matchingPacket] = distinctMatchingPackets;
540
+ const declaredSourceArtifact = validated.active.receipt.sourceArtifact;
541
+ if (declaredSourceArtifact !== undefined) {
542
+ const selectedPath = matchingCandidate === null
543
+ ? matchingPacket.packetPath
544
+ : matchingCandidate.candidatePath;
545
+ const expectedSourceArtifact = {
546
+ kind: matchingCandidate === null
547
+ ? "delivery_packet_v1"
548
+ : "source_candidate_v2",
549
+ runId,
550
+ path: relativeToWorkspace(workspaceRoot, selectedPath),
551
+ sha256: sha256(readFileSync(selectedPath)),
552
+ sourceFingerprint: validated.active.receipt.sourceFingerprint,
553
+ };
554
+ if (
555
+ declaredSourceArtifact.kind !== expectedSourceArtifact.kind
556
+ || declaredSourceArtifact.runId !== expectedSourceArtifact.runId
557
+ || declaredSourceArtifact.path !== expectedSourceArtifact.path
558
+ || declaredSourceArtifact.sha256 !== expectedSourceArtifact.sha256
559
+ || declaredSourceArtifact.sourceFingerprint
560
+ !== expectedSourceArtifact.sourceFingerprint
561
+ ) {
562
+ throw new Error(
563
+ "Accepted finish requires the active receipt sourceArtifact to match the selected source bytes.",
564
+ );
565
+ }
566
+ }
487
567
  const snapshotsRoot = path.join(executionRoot, "snapshots");
488
568
  const evidenceRoot = path.join(executionRoot, "snapshot-evidence");
489
569
  projectTruthDirectory({ workspaceRoot, executionRoot, directoryPath: snapshotsRoot, label: "Snapshot directory" });
@@ -492,14 +572,45 @@ export function activeAcceptedGate({ workspaceRoot, runId }) {
492
572
  const snapshotId = safeIdentifier(commandReceipt.id, "active snapshot id");
493
573
  const snapshotPath = path.join(snapshotsRoot, `${snapshotId}.json`);
494
574
  const snapshot = readJson(regularFile(snapshotPath, `Active snapshot ${snapshotId}`));
495
- const sourceBinding = verifySnapshotSourceBinding({
496
- snapshot,
497
- expectedFiles: matchingPacket.packet.changedFiles?.files
498
- ?? matchingPacket.packet.changedFiles?.wholeFileSha256,
499
- expectedFingerprint: validated.active.receipt.sourceFingerprint,
500
- });
501
- if (!sourceBinding.valid) {
502
- throw new Error(`Accepted finish requires snapshot source bound to the DeliveryPacket: ${snapshotId}; ${sourceBinding.issues.join("; ")}`);
575
+ if (matchingCandidate !== null) {
576
+ const expectedFiles = Object.fromEntries(
577
+ matchingCandidate.candidate.sourceManifest.entries
578
+ .filter((entry) => entry.operation !== "deleted")
579
+ .map((entry) => [entry.path, entry.sha256])
580
+ .sort(([left], [right]) => left.localeCompare(right)),
581
+ );
582
+ const beforeFiles = sortedFileMap(
583
+ snapshot.repositoryBefore?.selectedFiles ?? {},
584
+ );
585
+ const afterFiles = sortedFileMap(
586
+ snapshot.repositoryAfter?.selectedFiles ?? {},
587
+ );
588
+ const expectedInputs = Object.entries(expectedFiles)
589
+ .map(([id, hash]) => ({ id, sha256: hash }));
590
+ const actualInputs = [
591
+ ...(snapshot.command?.evidence?.materialInputs ?? []),
592
+ ].sort((left, right) => String(left?.id).localeCompare(String(right?.id)));
593
+ if (
594
+ snapshot.repositoryBefore?.head
595
+ !== matchingCandidate.candidate.baseline.head
596
+ || snapshot.repositoryAfter?.head
597
+ !== matchingCandidate.candidate.baseline.head
598
+ || JSON.stringify(beforeFiles) !== JSON.stringify(expectedFiles)
599
+ || JSON.stringify(afterFiles) !== JSON.stringify(expectedFiles)
600
+ || JSON.stringify(actualInputs) !== JSON.stringify(expectedInputs)
601
+ ) {
602
+ throw new Error(`Accepted finish requires snapshot source bound to SourceCandidate V2: ${snapshotId}`);
603
+ }
604
+ } else {
605
+ const sourceBinding = verifySnapshotSourceBinding({
606
+ snapshot,
607
+ expectedFiles: matchingPacket.packet.changedFiles?.files
608
+ ?? matchingPacket.packet.changedFiles?.wholeFileSha256,
609
+ expectedFingerprint: validated.active.receipt.sourceFingerprint,
610
+ });
611
+ if (!sourceBinding.valid) {
612
+ throw new Error(`Accepted finish requires snapshot source bound to the DeliveryPacket: ${snapshotId}; ${sourceBinding.issues.join("; ")}`);
613
+ }
503
614
  }
504
615
  const expectedStdoutPath = relativeToWorkspace(workspaceRoot, path.join(evidenceRoot, `${snapshotId}.stdout`));
505
616
  const expectedStderrPath = relativeToWorkspace(workspaceRoot, path.join(evidenceRoot, `${snapshotId}.stderr`));
@@ -523,7 +634,19 @@ export function activeAcceptedGate({ workspaceRoot, runId }) {
523
634
  status: "valid",
524
635
  gateInputPath,
525
636
  gateInputRelativePath: relativeToWorkspace(workspaceRoot, gateInputPath),
526
- deliveryPacketPath: relativeToWorkspace(workspaceRoot, matchingPacket.packetPath),
637
+ ...(matchingCandidate === null
638
+ ? {
639
+ deliveryPacketPath: relativeToWorkspace(
640
+ workspaceRoot,
641
+ matchingPacket.packetPath,
642
+ ),
643
+ }
644
+ : {
645
+ sourceCandidatePath: relativeToWorkspace(
646
+ workspaceRoot,
647
+ matchingCandidate.candidatePath,
648
+ ),
649
+ }),
527
650
  activeReceiptSha256: gate.activeReceiptSha256,
528
651
  authorizationGranted: false,
529
652
  };
@@ -18,6 +18,8 @@ import {
18
18
  isCanonicalSha512Integrity,
19
19
  validateStoredRegistryAdoptionEvidence,
20
20
  } from "./registry-adoption-gate.mjs";
21
+ import { resolveProjectCli } from "./project-cli-resolver.mjs";
22
+ import { inspectProjectControlState } from "./project-control-state.mjs";
21
23
 
22
24
  const PACKAGE_NAME = "owlrunkit";
23
25
  const EXPECTED_VERSION = CORE_VERSION;
@@ -294,95 +296,25 @@ function strictDescendant(root, candidate) {
294
296
  && !path.isAbsolute(relative);
295
297
  }
296
298
 
297
- function assertPathWithoutSymlinks(root, target, { targetType }) {
298
- const realRoot = realpathSync(root);
299
- const relative = path.relative(realRoot, target);
300
- if (!strictDescendant(realRoot, target)) {
301
- throw new Error("installed_package_path_invalid");
302
- }
303
- let current = realRoot;
304
- for (const segment of relative.split(path.sep)) {
305
- current = path.join(current, segment);
306
- const stat = lstatSync(current);
307
- if (stat.isSymbolicLink()) throw new Error("installed_package_symlink_rejected");
308
- }
309
- const finalStat = lstatSync(target);
310
- if (
311
- (targetType === "file" && !finalStat.isFile())
312
- || (targetType === "directory" && !finalStat.isDirectory())
313
- ) {
314
- throw new Error("installed_package_path_invalid");
315
- }
316
- const realTarget = realpathSync(target);
317
- if (!strictDescendant(realRoot, realTarget)) {
318
- throw new Error("installed_package_path_invalid");
319
- }
320
- }
321
-
322
- function dependencyDeclarations(manifest) {
323
- return ["dependencies", "devDependencies", "optionalDependencies"]
324
- .filter((section) => Object.hasOwn(manifest[section] ?? {}, PACKAGE_NAME))
325
- .map((section) => ({ section, spec: manifest[section][PACKAGE_NAME] }));
326
- }
327
-
328
299
  export function readLocalInstallBinding({ workspaceRoot } = {}) {
329
- const root = resolveBoundedWorkspaceRoot(workspaceRoot);
330
- const manifestPath = path.join(root, "package.json");
331
- const lockPath = path.join(root, "package-lock.json");
332
- const installedRoot = path.join(root, "node_modules", PACKAGE_NAME);
333
- const installedManifestPath = path.join(installedRoot, "package.json");
334
- try {
335
- for (const [target, targetType] of [
336
- [installedRoot, "directory"],
337
- ]) {
338
- assertPathWithoutSymlinks(root, target, { targetType });
339
- }
340
- const manifest = readWorkspaceJsonBounded(root, "package.json");
341
- const lock = readWorkspaceJsonBounded(root, "package-lock.json");
342
- const installedManifest = readWorkspaceJsonBounded(
343
- root,
344
- path.join("node_modules", PACKAGE_NAME, "package.json"),
345
- );
346
- const declarations = dependencyDeclarations(manifest);
347
- if (declarations.length !== 1) {
348
- return {
349
- status: "mismatch",
350
- issueCodes: ["registry_install_binding_ambiguous"],
351
- };
352
- }
353
- const requestedSpec = declarations[0].spec;
354
- const lockBinding = lock.packages?.[`node_modules/${PACKAGE_NAME}`];
355
- if (
356
- requestedSpec !== EXPECTED_VERSION
357
- || lockBinding?.version !== EXPECTED_VERSION
358
- || installedManifest.name !== PACKAGE_NAME
359
- || installedManifest.version !== EXPECTED_VERSION
360
- || typeof lockBinding.resolved !== "string"
361
- || typeof lockBinding.integrity !== "string"
362
- ) {
363
- return {
364
- status: "mismatch",
365
- issueCodes: ["registry_install_binding_mismatch"],
366
- };
367
- }
368
- return {
369
- status: "bound",
370
- requestedSpec: `${PACKAGE_NAME}@${requestedSpec}`,
371
- packageName: PACKAGE_NAME,
372
- version: EXPECTED_VERSION,
373
- resolved: lockBinding.resolved,
374
- integrity: lockBinding.integrity,
375
- };
376
- } catch (error) {
377
- return {
378
- status: "mismatch",
379
- issueCodes: [
380
- error instanceof Error && error.message.startsWith("installed_package_")
381
- ? error.message
382
- : "registry_install_binding_mismatch",
383
- ],
384
- };
385
- }
300
+ const binding = resolveProjectCli({
301
+ workspaceRoot,
302
+ expectedVersion: EXPECTED_VERSION,
303
+ requireEntrypoint: false,
304
+ });
305
+ if (binding.status === "bound") return binding;
306
+ const issue = binding.issueCodes?.[0];
307
+ const issueCode = issue === "project_cli_dependency_ambiguous"
308
+ ? "registry_install_binding_ambiguous"
309
+ : issue === "project_cli_symlink_rejected"
310
+ ? "installed_package_symlink_rejected"
311
+ : issue === "project_cli_path_invalid"
312
+ ? "installed_package_path_invalid"
313
+ : "registry_install_binding_mismatch";
314
+ return {
315
+ status: "mismatch",
316
+ issueCodes: [issueCode],
317
+ };
386
318
  }
387
319
 
388
320
  export async function readRegistryExact({
@@ -694,6 +626,10 @@ export async function runDoctor({
694
626
  } = {}) {
695
627
  const root = resolveBoundedWorkspaceRoot(workspaceRoot);
696
628
  const core = currentCoreIdentity();
629
+ const lifecycle = inspectProjectControlState({
630
+ workspaceRoot: root,
631
+ currentCore: core,
632
+ });
697
633
  const installed = readLocalInstallBinding({ workspaceRoot: root });
698
634
  const registry = registryClient
699
635
  ? await registryClient.readExact({ packageName: PACKAGE_NAME, version: EXPECTED_VERSION })
@@ -720,7 +656,12 @@ export async function runDoctor({
720
656
  },
721
657
  config,
722
658
  installedPackage: installed.status === "bound"
723
- ? { status: "ok", version: installed.version }
659
+ ? {
660
+ status: "ok",
661
+ version: installed.version,
662
+ cliStatus: installed.cliStatus,
663
+ ...(installed.cliPath ? { cliPath: installed.cliPath } : {}),
664
+ }
724
665
  : { status: "mismatch" },
725
666
  registry: registry.status === "registry_verified"
726
667
  ? { status: "ok", version: registry.version }
@@ -732,6 +673,12 @@ export async function runDoctor({
732
673
  detected: readProjectScripts(root),
733
674
  },
734
675
  runtimeIsolation,
676
+ lifecycle: {
677
+ status: lifecycle.status,
678
+ activeRunIds: lifecycle.activeRunIds,
679
+ activeLeaseIds: lifecycle.activeLeaseIds,
680
+ recovery: lifecycle.recovery,
681
+ },
735
682
  };
736
683
  const issueCodes = [...(registry.issueCodes ?? [])];
737
684
  if (!nodeOk) issueCodes.push("node_version_unsupported");
@@ -740,6 +687,7 @@ export async function runDoctor({
740
687
  if (config.status === "invalid") issueCodes.push("runkit_config_invalid");
741
688
  if (profiles.status === "missing") issueCodes.push("profiles_missing");
742
689
  if (profiles.status === "invalid") issueCodes.push("profiles_invalid");
690
+ if (lifecycle.status === "invalid") issueCodes.push("runkit_control_state_invalid");
743
691
  if (installed.status !== "bound") {
744
692
  issueCodes.push(...(installed.issueCodes ?? ["registry_install_binding_mismatch"]));
745
693
  }
@@ -761,7 +709,9 @@ export async function runDoctor({
761
709
  ...(profiles.launcherWarnings ?? []),
762
710
  ].sort();
763
711
  const nextAllowedAction = uniqueIssues.length === 0
764
- ? (profiles.launcherWarnings ?? []).length > 0
712
+ ? lifecycle.recovery.nextAllowedAction !== "plan_new_execution"
713
+ ? lifecycle.recovery.nextAllowedAction
714
+ : (profiles.launcherWarnings ?? []).length > 0
765
715
  ? "review_profile_launchers"
766
716
  : adoption.status === "ok"
767
717
  ? "plan_new_execution"
@@ -774,6 +724,8 @@ export async function runDoctor({
774
724
  ? "run_init_or_repair_config"
775
725
  : uniqueIssues.some((code) => code.startsWith("profiles_"))
776
726
  ? "detect_or_repair_profiles"
727
+ : uniqueIssues.includes("runkit_control_state_invalid")
728
+ ? "repair_execution_artifacts"
777
729
  : "repair_registry_binding";
778
730
  return {
779
731
  schemaVersion: "OwlCodaRunKitDoctorReportV1",
@@ -795,6 +747,7 @@ export function formatDoctorHuman(report) {
795
747
  `Registry: ${report.checks.registry.status}`,
796
748
  `Profiles: ${report.checks.profiles.status}`,
797
749
  `Adoption: ${report.checks.adoption.status}`,
750
+ `Lifecycle: ${report.checks.lifecycle.status}`,
798
751
  ...(report.issueCodes.length > 0 ? [`Issues: ${report.issueCodes.join(", ")}`] : []),
799
752
  ...(report.warningCodes.length > 0 ? [`Warnings: ${report.warningCodes.join(", ")}`] : []),
800
753
  `Next: ${report.nextAllowedAction === "adopt_current_release"