@awebai/oats 0.25.8 → 0.26.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 (185) hide show
  1. package/README.md +8 -6
  2. package/bin/oats.mjs +576 -1714
  3. package/capabilities/oats-authoring/oats-package.json +2 -2
  4. package/capabilities/oats-authoring/oats.json +2 -2
  5. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
  6. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
  7. package/capabilities/oats-aweb/bin/oats-aweb.mjs +334 -72
  8. package/capabilities/oats-aweb/injects/aweb.md +7 -2
  9. package/capabilities/oats-aweb/lib/binding-wire.mjs +107 -5
  10. package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
  11. package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
  12. package/capabilities/oats-aweb/oats.json +14 -4
  13. package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
  14. package/capabilities/oats-jira/oats.json +2 -2
  15. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
  16. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
  17. package/capabilities/oats-linear/oats.json +2 -2
  18. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
  19. package/capabilities/oats-okf/bin/oats-okf.mjs +1 -1
  20. package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
  21. package/capabilities/oats-okf/lib/migration.mjs +2 -2
  22. package/capabilities/oats-okf/lib/sources.mjs +5 -4
  23. package/capabilities/oats-okf/lib/stores.mjs +40 -9
  24. package/capabilities/oats-okf/lib/worker.mjs +3 -3
  25. package/capabilities/oats-okf/oats.json +1 -1
  26. package/capabilities/oats-review/oats.json +3 -2
  27. package/docs/capabilities.md +218 -47
  28. package/docs/capability-manifest.schema.json +13 -4
  29. package/docs/configuration.md +17 -5
  30. package/docs/conventions.md +16 -26
  31. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
  32. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
  33. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
  34. package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
  35. package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
  36. package/docs/design/2026-09-20-redesign-program-board.md +2 -2
  37. package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
  38. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
  39. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +54 -10
  40. package/docs/design/2026-09-24-phase-d-plan.md +77 -0
  41. package/docs/design/2026-09-25-teams-contract.md +226 -0
  42. package/docs/design/README.md +3 -3
  43. package/docs/design/launch-configurations.md +20 -16
  44. package/docs/design/operations-contract.md +27 -10
  45. package/docs/desktop-cli-api.md +546 -264
  46. package/docs/desktop-instance-start.md +1 -1
  47. package/docs/desktop.md +7 -13
  48. package/docs/execution-targets.md +16 -18
  49. package/docs/first-team.md +14 -17
  50. package/docs/implementation.md +28 -59
  51. package/docs/integrations.md +88 -32
  52. package/docs/knowledge-capability-authoring.md +1 -1
  53. package/docs/knowledge-reference/package-craft.md +10 -8
  54. package/docs/knowledge-theory.md +1 -1
  55. package/docs/knowledge.md +10 -11
  56. package/docs/layers.md +16 -17
  57. package/docs/oats-local.schema.json +29 -1
  58. package/docs/oats-membership.schema.json +5 -3
  59. package/docs/oats-package.schema.json +2 -2
  60. package/docs/oats-workspace.schema.json +1 -1
  61. package/docs/{official-marketplace.md → official-catalog.md} +15 -16
  62. package/docs/packages.md +75 -52
  63. package/docs/release-notes/v0.22.0.md +1 -1
  64. package/docs/release-notes/v0.23.1.md +1 -1
  65. package/docs/release-notes/v0.25.9.md +23 -0
  66. package/docs/release-notes/v0.26.0.md +670 -0
  67. package/docs/schedules.md +48 -126
  68. package/docs/soul.schema.json +11 -4
  69. package/docs/souls-and-instances.md +56 -43
  70. package/docs/workspaces.md +80 -58
  71. package/injects/instance-boundary.md +1 -1
  72. package/injects/work-attached.md +1 -1
  73. package/injects/work-workspace.md +2 -2
  74. package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
  75. package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
  76. package/lib/capability-contract.mjs +110 -0
  77. package/lib/config-data.mjs +2 -2
  78. package/lib/core.mjs +700 -4824
  79. package/lib/digest.mjs +12 -0
  80. package/lib/instance-inspect.mjs +396 -0
  81. package/lib/instance-lifecycle.mjs +3 -4
  82. package/lib/instance-resolution.mjs +212 -26
  83. package/lib/instruction-composition.mjs +0 -20
  84. package/lib/materialize.mjs +6 -4
  85. package/lib/operator-dispatch.mjs +33 -13
  86. package/lib/packages.mjs +25 -190
  87. package/lib/provider-binding.mjs +4 -2
  88. package/lib/provider-reasons.mjs +3 -68
  89. package/lib/resolve.mjs +204 -68
  90. package/lib/schedule.mjs +97 -272
  91. package/lib/servers.mjs +13 -13
  92. package/lib/{portable-shape.mjs → shape.mjs} +4 -3
  93. package/lib/tree-copy.mjs +44 -0
  94. package/lib/workspace.mjs +125 -20
  95. package/package-catalog.json +7 -7
  96. package/package.json +1 -1
  97. package/skills/integration-authoring/SKILL.md +48 -40
  98. package/skills/oats-getting-started/SKILL.md +105 -110
  99. package/skills/oats-support/SKILL.md +2 -2
  100. package/skills/soul-craft/SKILL.md +13 -6
  101. package/bin/oats-pi-sdk-host.mjs +0 -17
  102. package/docs/2026-09-03-architecture-proposal.md +0 -642
  103. package/docs/artifact-approvals.schema.json +0 -7
  104. package/docs/captured-invocation-context.schema.json +0 -7
  105. package/docs/captured-resolution.schema.json +0 -7
  106. package/docs/design/package-engine-contract.md +0 -813
  107. package/docs/design/package-runtime-api.md +0 -588
  108. package/docs/desktop-succession.md +0 -57
  109. package/docs/execution-capsule.schema.json +0 -108
  110. package/docs/first-team-demo.md +0 -92
  111. package/docs/knowledge-migration.md +0 -147
  112. package/docs/migration-from-oas.md +0 -103
  113. package/docs/oats-config.schema.json +0 -172
  114. package/docs/oats-lock-v3.schema.json +0 -7
  115. package/docs/oats-lock.schema.json +0 -175
  116. package/docs/operating-team-migration.md +0 -470
  117. package/docs/portable.schema.json +0 -2512
  118. package/docs/provider-check-input.schema.json +0 -7
  119. package/docs/rebuild-to-v2.md +0 -511
  120. package/docs/workspace-adoption.md +0 -74
  121. package/injects/framework-workspace.md +0 -7
  122. package/injects/local-soul.md +0 -19
  123. package/injects/oats-portable.md +0 -20
  124. package/injects/oats.md +0 -11
  125. package/injects/portable-instance-boundary.md +0 -39
  126. package/injects/portable-work-directory.md +0 -29
  127. package/lib/artifact-approvals.mjs +0 -120
  128. package/lib/artifact-tree.mjs +0 -141
  129. package/lib/capability-artifacts.mjs +0 -179
  130. package/lib/capability-execution.mjs +0 -15
  131. package/lib/capability-inputs.mjs +0 -39
  132. package/lib/capability-provenance.mjs +0 -231
  133. package/lib/captured-action-shape.mjs +0 -21
  134. package/lib/captured-admission-shape.mjs +0 -20
  135. package/lib/captured-binding-file.mjs +0 -36
  136. package/lib/captured-dispatch.mjs +0 -66
  137. package/lib/captured-instance-index.mjs +0 -277
  138. package/lib/captured-invocation-context.mjs +0 -130
  139. package/lib/captured-launch-request.mjs +0 -66
  140. package/lib/captured-operation-process.mjs +0 -15
  141. package/lib/captured-pi-custody.mjs +0 -29
  142. package/lib/captured-pi-host.mjs +0 -167
  143. package/lib/captured-pi-outcome.mjs +0 -172
  144. package/lib/captured-resolutions.mjs +0 -275
  145. package/lib/captured-scaffold.mjs +0 -87
  146. package/lib/captured-selector.mjs +0 -28
  147. package/lib/captured-session-backend.mjs +0 -52
  148. package/lib/captured-source-receipt-file.mjs +0 -72
  149. package/lib/helper-injection-policy.mjs +0 -104
  150. package/lib/legacy-lock-codec.mjs +0 -106
  151. package/lib/manifest-settings.mjs +0 -84
  152. package/lib/package-closure.mjs +0 -48
  153. package/lib/package-materialization.mjs +0 -83
  154. package/lib/pi-sdk-host.mjs +0 -229
  155. package/lib/portable-artifacts.mjs +0 -115
  156. package/lib/portable-choices.mjs +0 -82
  157. package/lib/portable-composition.mjs +0 -136
  158. package/lib/portable-digest.mjs +0 -105
  159. package/lib/portable-identity.mjs +0 -40
  160. package/lib/portable-lock.mjs +0 -117
  161. package/lib/portable-onboarding-request.mjs +0 -49
  162. package/lib/portable-onboarding.mjs +0 -256
  163. package/lib/portable-package-preparation.mjs +0 -188
  164. package/lib/portable-policy.mjs +0 -44
  165. package/lib/portable-soul.mjs +0 -42
  166. package/lib/portable-state.mjs +0 -80
  167. package/lib/prepare-composition.mjs +0 -170
  168. package/lib/prepared-bindings.mjs +0 -92
  169. package/lib/prepared-resources.mjs +0 -127
  170. package/lib/provider-binding-broker.mjs +0 -65
  171. package/lib/provider-binding-wire.mjs +0 -116
  172. package/lib/readiness.mjs +0 -225
  173. package/lib/repository-observation.mjs +0 -226
  174. package/lib/resolution-shape.mjs +0 -393
  175. package/lib/schedule-capsule.mjs +0 -206
  176. package/lib/soul-constraints.mjs +0 -40
  177. package/lib/source-projection.mjs +0 -84
  178. package/lib/source-spec.mjs +0 -189
  179. package/lib/workspace-definition.mjs +0 -126
  180. package/lib/workspace-discovery.mjs +0 -146
  181. package/skills/oats/SKILL.md +0 -162
  182. package/skills/oats-config/SKILL.md +0 -164
  183. package/skills/oats-packages/SKILL.md +0 -184
  184. package/skills/oats-portable/SKILL.md +0 -115
  185. package/skills/oats-portable-artifacts/SKILL.md +0 -63
package/lib/readiness.mjs DELETED
@@ -1,225 +0,0 @@
1
- /** K5 — readiness quartet, signature status and enforced policy, as DATA.
2
- *
3
- * `installed | trusted | configured | enrolled`, each `pass | fail | unknown |
4
- * not-applicable` with items a human can act on: subject, whether it is
5
- * required, the reason, the producer of the fact, evidence, remedy. Every fact
6
- * is derived from what inspect already reports (capability inventory, health,
7
- * executable approval, activation, runtime-package requirements, soul
8
- * declarations) — never a second opinion. Unknown is unknown; "Ready" is the
9
- * consumer's word and only when every required check passes. */
10
- import { spawnSync } from "node:child_process";
11
- import { killGroup } from "./process-group.mjs";
12
- import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
13
- import { tmpdir } from "node:os";
14
- import { join } from "node:path";
15
- import { parseYamlNested } from "./core.mjs";
16
-
17
- export const READINESS_API = 1;
18
-
19
- function roll(items, { required = (i) => i.required !== false } = {}) {
20
- const req = items.filter(required);
21
- if (!items.length) return "not-applicable";
22
- if (req.some((i) => i.status === "fail")) return "fail";
23
- if (req.some((i) => i.status === "unknown")) return "unknown";
24
- if (req.every((i) => i.status === "pass" || i.status === "not-applicable")) return req.length ? "pass" : "not-applicable";
25
- return "unknown";
26
- }
27
- const item = (subject, status, { required = true, reason = null, producer = "kernel", evidence = null, remedy = null, ...rest } = {}) =>
28
- ({ subject, status, required, reason, producer, evidence, remedy, ...rest });
29
-
30
- /** Verified Git signature of a source commit, named signer or nothing.
31
- * Requires network (fetch) — only when the caller asks (`verify: true`);
32
- * otherwise `unknown` with the reason. Never a URL, owner or hash as signer. */
33
- export const SIGNATURE_FAILURES = Object.freeze(["transport-not-allowed", "fetch-failed", "fetch-timeout", "budget-exhausted", "verifier-failed", "verifier-timeout", "cannot-check", "no-source"]);
34
- const ALLOWED_TRANSPORTS = /^(https:\/\/|ssh:\/\/|git@[^/:]+:)/;
35
- /** Verified Git signature of a source commit, named signer or nothing.
36
- * Requires network (fetch) — only when the caller asks (`verify: true`);
37
- * otherwise `unknown` with the reason. Never a URL, owner or hash as signer.
38
- * Bounded custody: one total budget per call (`budgetMs`, default 60s) shared
39
- * by fetch and verify; each Git child runs in its own process group and is
40
- * killed with the group on timeout; the scratch repository is removed on every
41
- * exit including signals; Git reads NO global/system config and cannot prompt;
42
- * only https/ssh transports are fetched. Failures carry a closed `failure`
43
- * code — never stderr. */
44
- /** One verification budget for a whole readiness read: every signatureOf call
45
- * in that read draws from it, so a target with N capabilities is bounded by the
46
- * TOTAL (default 120 s), not N × per-call. */
47
- export function verificationBudget(totalMs = 120000) {
48
- const started = Date.now();
49
- return { totalMs, remaining: () => totalMs - (Date.now() - started) };
50
- }
51
- const scratchDirs = new Set();
52
- let signalCleanupInstalled = false;
53
- function installSignalCleanup() {
54
- if (signalCleanupInstalled) return; signalCleanupInstalled = true;
55
- const sweep = () => { for (const d of scratchDirs) { try { rmSync(d, { recursive: true, force: true }); } catch { /* best effort */ } } scratchDirs.clear(); };
56
- process.once("exit", sweep);
57
- for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.once(sig, () => { sweep(); process.exit(128 + (sig === "SIGINT" ? 2 : sig === "SIGTERM" ? 15 : 1)); });
58
- }
59
- export function signatureOf({ url, commit }, { verify = false, budgetMs = 60000, budget = null } = {}) {
60
- if (!url || !commit || commit === "local") return { status: "not-applicable", signer: null, reason: commit === "local" ? "path-installed capability has no source commit" : "no source recorded", failure: null };
61
- if (!verify) return { status: "unknown", signer: null, reason: "signature verification needs a network fetch; pass --verify-signatures", failure: null };
62
- if (!ALLOWED_TRANSPORTS.test(url)) return { status: "unknown", signer: null, reason: "source transport is not https or ssh; not fetched", failure: { code: "transport-not-allowed" } };
63
- const shared = budget ?? verificationBudget(budgetMs);
64
- if (shared.remaining() <= 0) return { status: "unknown", signer: null, reason: "the verification budget was exhausted", failure: { code: "budget-exhausted" } };
65
- installSignalCleanup();
66
- const dir = mkdtempSync(join(tmpdir(), "oats-sig-")); scratchDirs.add(dir);
67
- const cleanup = () => { scratchDirs.delete(dir); try { rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ } };
68
- const env = { PATH: process.env.PATH ?? "", HOME: dir, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_ASKPASS: "/bin/false", SSH_ASKPASS: "/bin/false", GIT_SSH_COMMAND: "ssh -o BatchMode=yes", LC_ALL: "C" };
69
- const git = (args, stage) => {
70
- const left = shared.remaining();
71
- if (left <= 0) throw Object.assign(new Error("budget"), { failure: "budget-exhausted" });
72
- const child = spawnSync("git", ["-C", dir, "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "-c", "protocol.allow=never", "-c", "protocol.https.allow=always", "-c", "protocol.ssh.allow=always", ...args],
73
- { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: left, killSignal: "SIGKILL", detached: true, env, shell: false });
74
- if (child.error?.code === "ETIMEDOUT" || child.signal === "SIGKILL") { killGroup(child); throw Object.assign(new Error(stage), { failure: `${stage}-timeout` }); }
75
- if (child.error || child.status !== 0) { killGroup(child); throw Object.assign(new Error(stage), { failure: `${stage}-failed` }); }
76
- return child.stdout;
77
- };
78
- try {
79
- git(["init", "-q"], "verifier");
80
- git(["fetch", "-q", "--depth", "1", url, commit], "fetch");
81
- // %G? : G good, B bad, U good-untrusted, X expired, Y expired key, R revoked, E cannot check, N none
82
- const out = git(["log", "-1", "--format=%G?%x00%GS%x00%GK%x00%GF", commit], "verifier").trim();
83
- const [code, signerName, keyId, fingerprint] = out.split("\0");
84
- if (code === "N") return { status: "unsigned", signer: null, reason: "commit carries no signature", failure: null };
85
- if (code === "G") return { status: "verified", signer: { id: fingerprint || keyId || null, label: signerName || null }, reason: null, failure: null };
86
- if (code === "U") return { status: "verified", signer: { id: fingerprint || keyId || null, label: signerName || null }, reason: "good signature from a key not marked trusted in the local keyring", trust: "untrusted-key", failure: null };
87
- if (code === "E") return { status: "unknown", signer: null, reason: "signature present but cannot be checked (missing public key)", failure: { code: "cannot-check" } };
88
- return { status: "invalid", signer: null, reason: { B: "bad signature", X: "good signature that has expired", Y: "good signature made by an expired key", R: "good signature made by a revoked key" }[code] || "verifier reported an unrecognised state", failure: null };
89
- } catch (e) {
90
- const code = SIGNATURE_FAILURES.includes(e.failure) ? e.failure : "verifier-failed";
91
- const reasons = { "fetch-failed": "the source could not be fetched", "fetch-timeout": "the fetch exceeded the verification budget", "budget-exhausted": "the verification budget was exhausted", "verifier-failed": "the local verifier failed", "verifier-timeout": "the local verifier exceeded the verification budget" };
92
- return { status: "unknown", signer: null, reason: reasons[code], failure: { code } };
93
- } finally { cleanup(); }
94
- }
95
-
96
- function sourceOfCapability(cap, catalog) {
97
- // cap.source is e.g. "catalog:oats.framework" or "git:https://…@ref#path"; the
98
- // installed record's package commit is what we would verify.
99
- const src = String(cap.source || "");
100
- if (src.startsWith("git:")) { const m = /^git:([^@#]+)(?:@([^#]+))?/.exec(src); return { url: m?.[1] ?? null, commit: cap.commit ?? m?.[2] ?? null }; }
101
- if (src.startsWith("catalog:") && catalog) { const entry = catalog.packages?.[src.slice(8)]; return { url: entry?.url ?? null, commit: cap.commit ?? null }; }
102
- return { url: null, commit: cap.commit ?? null };
103
- }
104
-
105
- /** The quartet for a scope or one soul, from an inspect result. */
106
- export function readinessOf(inspect, { soul = null, verifySignatures = false, catalog = null, deploymentDir = null, memberDocument = null, selector = null, verificationBudgetMs = 120000 } = {}) {
107
- const caps = inspect.capabilities || [];
108
- const budget = verifySignatures ? verificationBudget(verificationBudgetMs) : null;
109
- const soulEntry = soul ? (inspect.souls || []).find((s) => s.name === soul) : null;
110
- const declaredRequires = soulEntry?.declarations?.requires?.capabilities ? Object.keys(soulEntry.declarations.requires.capabilities) : null;
111
- const required = new Set(declaredRequires ?? caps.filter((c) => c.activation?.enabled).map((c) => c.id));
112
- const declaredForSoul = (c) => !!c.activation?.declaredAt?.some((d) => (d.targets || []).includes(`soul:${soul}`));
113
- const relevant = soul ? caps.filter((c) => required.has(c.id) || declaredForSoul(c)) : caps;
114
- // Typed linkage for consumers (frame-level per-capability rows): WHICH
115
- // capability, at which config level/scope, and WHY it is in this quartet.
116
- const capabilityOf = (c) => ({ id: c.id, level: c.activation?.level ?? null, scope: c.activation?.target ?? null });
117
- const originOf = (c) => declaredRequires?.includes(c.id) ? { kind: "requires", target: `soul:${soul}` }
118
- : soul && declaredForSoul(c) ? { kind: "declares", target: `soul:${soul}` }
119
- : c.activation?.enabled ? { kind: "default", target: c.activation?.target ?? "global" } : { kind: "inventory", target: null };
120
- const typed = (c) => ({ capability: capabilityOf(c), origin: originOf(c) });
121
-
122
- // installed — the artifact's bytes are present and locked with matching integrity
123
- const installed = relevant.map((c) => {
124
- const ok = c.health?.installed === true && (c.health.integrity == null || c.health.installedIntegrity == null || c.health.integrity === c.health.installedIntegrity);
125
- return item(c.id, ok ? "pass" : c.health?.installed === false ? "fail" : "unknown", { required: required.has(c.id), producer: "oats list",
126
- reason: ok ? null : c.health?.installed === false ? "not acquired" : c.health?.code || "integrity drift", evidence: { version: c.version ?? null, integrity: c.health?.integrity ?? null, origin: c.origin ?? null },
127
- remedy: ok ? null : `oats install ${c.package || c.id}`, ...typed(c) });
128
- });
129
- for (const id of required) if (!caps.some((c) => c.id === id)) installed.push(item(id, "fail", { producer: "soul declaration", reason: "declared by the soul but not in the inventory", remedy: `oats install <package providing ${id}>`,
130
- capability: { id, level: null, scope: null }, origin: { kind: "requires", target: `soul:${soul}` } }));
131
-
132
- // trusted — executable approval of the exact artifact; signature separately
133
- const trusted = relevant.map((c) => {
134
- // The inspect row states whether the manifest has anything trust approves
135
- // (commands/hooks/launch env). No surface → trust is not-applicable, however
136
- // the lock records it. Older rows without the flag fall back to the old heuristic.
137
- const executable = typeof c.health?.executableSurface === "boolean" ? c.health.executableSurface
138
- : !!(c.operations?.length || c.health?.code === "untrusted-surface");
139
- const approved = c.health?.trusted === true;
140
- const sig = signatureOf(sourceOfCapability(c, catalog), { verify: verifySignatures, budget });
141
- return item(c.id, !executable ? "not-applicable" : approved ? "pass" : c.health?.trusted === false ? "fail" : "unknown", { required: required.has(c.id), producer: "artifact approval",
142
- reason: !executable ? "no executable surface" : approved ? null : "executable surface not approved", evidence: { integrity: c.health?.integrity ?? null }, remedy: approved || !executable ? null : `oats trust ${c.id}`,
143
- signature: sig, ...typed(c) });
144
- });
145
-
146
- // configured — EFFECTIVE activation for the subject (a declaration at the
147
- // soul is not activation: `enabled` is the resolved verdict for this subject,
148
- // and a declared-but-disabled binding is a fail that says so) + runtime
149
- // package requirements + layer readiness problems
150
- const configured = [];
151
- for (const c of relevant) {
152
- const active = c.activation?.enabled === true;
153
- const declaredOnly = !active && soul && declaredForSoul(c);
154
- if (required.has(c.id)) configured.push(item(`${c.id} activation`, active ? "pass" : "fail", { producer: "oats-config.yaml",
155
- reason: active ? null : declaredOnly ? `declared for soul ${soul} but disabled${c.activation?.reason ? ` (${c.activation.reason})` : ""}` : `not active for ${soul ? `soul ${soul}` : "this scope"}`,
156
- evidence: { target: c.activation?.target ?? null, level: c.activation?.level ?? null, source: c.activation?.source ?? null }, remedy: active ? null : `oats use ${c.id}${soul ? ` --soul ${soul}` : ""}`, ...typed(c) }));
157
- for (const miss of c.missingRequires || []) configured.push(item(`${c.id} requires ${miss.command}`, "fail", { producer: "capability manifest", reason: miss.why || "required command not on PATH", remedy: miss.install || null, ...typed(c) }));
158
- }
159
- for (const p of inspect.problems || []) if (/runtime package|DISABLED|extension/i.test(p.message || "")) configured.push(item(p.capability || p.code, "fail", { producer: "runtime settings", reason: p.message, remedy: null }));
160
- if (soulEntry && soulEntry.readiness?.status === "undeclared") configured.push(item(`${soul} declarations`, "not-applicable", { required: false, producer: "soul.yaml", reason: "no requirements declared" }));
161
-
162
- // enrolled — workspace member admission (decision §3): not-applicable for
163
- // standalone; pass/fail when this repository declares a workspace.
164
- const enrolled = [];
165
- const member = memberDocument ?? readMemberDocument(deploymentDir);
166
- if (!member) enrolled.push(item("workspace membership", "not-applicable", { required: false, producer: "oats.yaml", reason: "standalone deployment: no workspace declared in oats.yaml" }));
167
- else if (member.unreadable) enrolled.push(item("workspace membership", "unknown", { producer: "oats.yaml", reason: "workspace member document is unreadable; membership cannot be stated", evidence: { file: member.file ?? null }, remedy: "repair oats.yaml (valid YAML) and re-run" }));
168
- else if (!member.workspace?.source) enrolled.push(item("workspace membership", "not-applicable", { required: false, producer: "oats.yaml", reason: "oats.yaml declares exports but no workspace backlink" }));
169
- else enrolled.push(item(`member of ${member.workspace.source}`, member.admitted === true ? "pass" : member.admitted === false ? "fail" : "unknown", { producer: "workspace discovery",
170
- reason: member.admitted === true ? null : member.admitted === false ? "this repository is not admitted in the workspace's members" : "reciprocal admission not observed: this CLI reads the backlink but does not yet fetch the workspace's members (declared, not verified)",
171
- evidence: { workspace: member.workspace.source, revision: member.workspace.revision ?? null }, remedy: member.admitted === true ? null : "ask the workspace maintainer to admit this repository (oats-workspace.yaml members) — enrolment is admission, not login" }));
172
-
173
- const checks = { installed: { status: roll(installed), items: installed }, trusted: { status: roll(trusted), items: trusted }, configured: { status: roll(configured), items: configured }, enrolled: { status: roll(enrolled), items: enrolled } };
174
- const requiredStatuses = Object.values(checks).flatMap((c) => c.items.filter((i) => i.required).map((i) => i.status));
175
- // Per-capability grouping of the same items (no second observation): each
176
- // capability's four verdicts, ready only if all its REQUIRED items pass.
177
- const byCapability = [...new Set(Object.values(checks).flatMap((c) => c.items.map((i) => i.capability?.id).filter(Boolean)))].sort().map((id) => {
178
- const of = (name) => checks[name].items.filter((i) => i.capability?.id === id);
179
- const statuses = Object.keys(checks).flatMap((name) => of(name).filter((i) => i.required).map((i) => i.status));
180
- const any = Object.keys(checks).flatMap((name) => of(name)).find(Boolean);
181
- return { capability: any?.capability ?? { id, level: null, scope: null }, origin: any?.origin ?? null, required: statuses.length > 0,
182
- checks: Object.fromEntries(Object.keys(checks).map((name) => [name, of(name).length ? roll(of(name)) : "not-applicable"])),
183
- ownReady: statuses.length > 0 && statuses.every((s) => s === "pass" || s === "not-applicable") };
184
- });
185
- // Items that belong to no capability (workspace membership, soul declarations)
186
- // are SUBJECT-level: a capability's own verdict never overrides them, and a
187
- // row must not read "ready" while the subject is blocked by one of them.
188
- const subjectBlockers = Object.entries(checks).flatMap(([name, c]) => c.items.filter((i) => i.required && !i.capability && i.status !== "pass" && i.status !== "not-applicable").map((i) => ({ check: name, subject: i.subject, status: i.status })));
189
- for (const g of byCapability) g.ready = g.ownReady && subjectBlockers.length === 0;
190
- return { readinessApi: READINESS_API, subject: { ...(soul ? { kind: "soul", name: soul } : { kind: "scope", context: inspect.scope?.context ?? null }), ...(selector ? { selector } : {}) }, at: new Date().toISOString(),
191
- checks, summary: { ready: requiredStatuses.length > 0 && requiredStatuses.every((s) => s === "pass" || s === "not-applicable"), required: requiredStatuses.length,
192
- pass: requiredStatuses.filter((s) => s === "pass").length, fail: requiredStatuses.filter((s) => s === "fail").length, unknown: requiredStatuses.filter((s) => s === "unknown").length, byCapability, subjectBlockers },
193
- notes: [
194
- ...(verifySignatures ? [] : ["signatures are unknown until --verify-signatures (network fetch)"]),
195
- "ready means every REQUIRED check passes; it is never inferred from an empty set",
196
- "enrolment is workspace member admission, not native login or team registration",
197
- ] };
198
- }
199
-
200
- function readMemberDocument(deploymentDir) {
201
- if (!deploymentDir) return null;
202
- const file = join(deploymentDir, "oats.yaml");
203
- if (!existsSync(file)) return null;
204
- try {
205
- const doc = parseYamlNested(readFileSync(file, "utf8"));
206
- // The lenient parser never throws: a `workspace:` that is present but not a
207
- // mapping is a document we cannot read a membership from — unreadable, not absent.
208
- if (doc.workspace !== undefined && (doc.workspace === null || typeof doc.workspace !== "object")) return { workspace: null, admitted: null, unreadable: true, file };
209
- return { workspace: doc.workspace && typeof doc.workspace === "object" ? doc.workspace : null, admitted: null, exports: doc.exports ?? null };
210
- } catch { return { workspace: null, admitted: null, unreadable: true, file }; }
211
- }
212
-
213
- /** Enforced policy for an instance (from its recorded metadata) or a soul
214
- * (its declaration + default), with origins. Advisory/unknown stays unknown. */
215
- export function policyOf({ instanceMeta = null, soul = null } = {}) {
216
- const child = instanceMeta?.policy?.childSpawns
217
- ? { allowed: instanceMeta.policy.childSpawns.allowed === true, enforced: true, origin: instanceMeta.policy.childSpawns.origin ?? { kind: "recorded" } }
218
- : instanceMeta ? { allowed: true, enforced: true, origin: { kind: "default", detail: "no recorded policy: children allowed (pre-0.24.8 instance)" } }
219
- : soul?.declarations?.children && typeof soul.declarations.children.spawn === "boolean"
220
- ? { allowed: soul.declarations.children.spawn, enforced: false, origin: { kind: "soul", detail: "children.spawn in soul.yaml; enforced once an instance records it" } }
221
- : { allowed: true, enforced: false, origin: { kind: "default", detail: "no declaration: children allowed" } };
222
- const worktree = instanceMeta ? { allowed: instanceMeta.work === "worktree" || instanceMeta.work === "checkout", mode: instanceMeta.work ?? null, enforced: true, origin: { kind: "work-mode", detail: `work: ${instanceMeta.work}` } }
223
- : soul ? { allowed: ["worktree", "checkout"].includes(soul.work), mode: soul.work ?? null, enforced: false, origin: { kind: "soul", detail: `work: ${soul.work}` } } : { allowed: null, mode: null, enforced: false, origin: { kind: "unknown" } };
224
- return { readinessApi: READINESS_API, policy: { childSpawns: child, worktrees: worktree }, notes: ["a lifecycle-authority claim enforced by the spawn route, not an OS sandbox"] };
225
- }
@@ -1,226 +0,0 @@
1
- /** Exact, transaction-scoped repository observations. Bare Git reads never check
2
- * out or execute source content. Cache lifetime/auth context is one transaction. */
3
- import { execFileSync } from "node:child_process";
4
- import { lstatSync, mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
5
- import { isAbsolute, join } from "node:path";
6
- import { canonicalJson, decodeUtf8, freezeJson, parseStrictJson } from "./portable-values.mjs";
7
- import { bytesIntegrity } from "./portable-digest.mjs";
8
- import { parseRepositorySource, portablePath, revisionSelector } from "./source-spec.mjs";
9
- import { sameIdentity, validateRepositoryIdentity } from "./portable-identity.mjs";
10
- import { validateOrigin } from "./resolution-shape.mjs";
11
- import { createSourceProjection, validateProjectionPath } from "./source-projection.mjs";
12
- export { validateProjectionPath } from "./source-projection.mjs";
13
- import { oatsError } from "./errors.mjs";
14
-
15
- function githubCoordinates(url) {
16
- let host, path;
17
- if (url.includes("://")) { const parsed = new URL(url); host = parsed.hostname; path = parsed.pathname.slice(1); }
18
- else { const match = /^[^@]+@([^:]+):(.+)$/.exec(url); if (!match) return null; [, host, path] = match; }
19
- if (host.toLowerCase() !== "github.com") return null;
20
- const parts = path.replace(/\.git$/, "").split("/");
21
- if (parts.length !== 2 || parts.some((part) => !/^[A-Za-z0-9_.-]+$/.test(part))) throw oatsError("source-identity-unresolved", "GitHub repository locator cannot be qualified");
22
- return { host: "github.com", owner: parts[0], name: parts[1] };
23
- }
24
-
25
- /** Native host credentials only; no token extraction, copying or fallback after
26
- * a GitHub identity lookup fails. Unsupported hosts use explicit remote identity. */
27
- export function readHostingIdentity(repository, { environment = process.env, expectedIdentity } = {}) {
28
- if (expectedIdentity?.kind === "canonical-remote") return { identity: { kind: "canonical-remote", remote: repository.normalized } };
29
- const coordinates = githubCoordinates(repository.url);
30
- if (!coordinates) return { identity: { kind: "canonical-remote", remote: repository.normalized } };
31
- let metadata;
32
- try {
33
- const output = execFileSync("gh", ["api", "--hostname", coordinates.host, `repos/${coordinates.owner}/${coordinates.name}`,
34
- "--jq", "{id, full_name, default_branch}"], { env: environment, timeout: 30_000, maxBuffer: 64 * 1024, stdio: ["ignore", "pipe", "pipe"] });
35
- metadata = parseStrictJson(output);
36
- } catch { throw oatsError("source-identity-unresolved", "repository hosting identity is unavailable in the selected access context"); }
37
- if (!Number.isSafeInteger(metadata.id) || metadata.id < 1 || typeof metadata.full_name !== "string") throw oatsError("source-identity-unresolved", "hosting response lacks a stable repository identity");
38
- if (metadata.full_name.toLowerCase() !== `${coordinates.owner}/${coordinates.name}`.toLowerCase()) throw oatsError("source-identity-change", "repository redirect or rename requires an explicit identity mapping");
39
- revisionSelector(metadata.default_branch);
40
- return { identity: { kind: "provider-repository", provider: "github", host: coordinates.host, id: String(metadata.id) }, defaultBranch: metadata.default_branch };
41
- }
42
-
43
- function gitEnvironment(environment) {
44
- const out = { ...environment };
45
- // Keep native credential/SSH facilities, but never an invoking repository's
46
- // object/index/config injection. Host HOME/config credentials remain host-owned.
47
- const retained = new Set(["GIT_ASKPASS", "GIT_SSH", "GIT_SSH_COMMAND", "GIT_SSH_VARIANT", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_CONFIG_NOSYSTEM"]);
48
- for (const key of Object.keys(out)) if (key.startsWith("GIT_") && !retained.has(key)) delete out[key];
49
- out.GIT_TERMINAL_PROMPT = "0"; out.GIT_LITERAL_PATHSPECS = "1"; out.GIT_NO_REPLACE_OBJECTS = "1";
50
- return out;
51
- }
52
-
53
- export function createRepositoryTransaction({ directory, accessContextKey, environment = process.env, identityReader = readHostingIdentity, allowLocalGit = false,
54
- maxRepositories = 32, maxEntries = 10_000, maxBytes = 256 * 1024 * 1024 } = {}) {
55
- if (typeof directory !== "string" || !isAbsolute(directory) || typeof accessContextKey !== "string" || !accessContextKey) throw oatsError("invalid-source", "repository observation requires an explicit scratch directory and access context");
56
- canonicalJson(accessContextKey, { maxBytes: 8192 });
57
- for (const value of [maxRepositories, maxEntries, maxBytes]) if (!Number.isSafeInteger(value) || value < 1) throw oatsError("resource-limit", "invalid repository observation budget");
58
- const parent = realpathSync(directory);
59
- if (!lstatSync(parent).isDirectory()) throw oatsError("invalid-source", "repository observation scratch root is not a directory");
60
- const root = mkdtempSync(join(parent, ".repository-transaction-")), owned = lstatSync(root), hooks = join(root, "empty-hooks");
61
- mkdirSync(hooks, { mode: 0o700 });
62
- const env = gitEnvironment(environment), memo = new Map(), snapshots = new Map(), hosts = new Map(), defaults = new Map(), handles = new WeakMap(), blobs = new Map();
63
- let closed = false, readBytes = 0, entriesRead = 0;
64
- const active = () => { if (closed) throw oatsError("source-unavailable", "repository transaction is closed"); };
65
- const git = (args, limit = 1024 * 1024) => {
66
- active();
67
- try { return execFileSync("git", ["-c", `core.hooksPath=${hooks}`, "-c", "protocol.ext.allow=never", "-c", `protocol.file.allow=${allowLocalGit ? "always" : "never"}`,
68
- "-c", "http.followRedirects=false", ...args], { cwd: root, env, timeout: 60_000, maxBuffer: limit, stdio: ["ignore", "pipe", "pipe"] }); }
69
- catch (error) { throw oatsError(error.code === "ENOBUFS" ? "resource-limit" : "source-unavailable", "repository read failed in the selected access context"); }
70
- };
71
- const hostFor = (repository, expectedIdentity) => {
72
- if (!hosts.has(repository.normalized)) {
73
- if (hosts.size >= maxRepositories * 4) throw oatsError("resource-limit", "repository identity count exceeded");
74
- try {
75
- const response = identityReader(repository, { environment: env, expectedIdentity });
76
- validateRepositoryIdentity(response.identity);
77
- if (response.defaultBranch !== undefined) revisionSelector(response.defaultBranch);
78
- hosts.set(repository.normalized, { value: freezeJson({ identity: structuredClone(response.identity),
79
- ...(response.defaultBranch === undefined ? {} : { defaultBranch: response.defaultBranch }) }) });
80
- } catch (error) { hosts.set(repository.normalized, { error }); }
81
- }
82
- const hosted = hosts.get(repository.normalized);
83
- if (hosted.error) throw hosted.error;
84
- const host = hosted.value;
85
- if (host.identity.kind === "canonical-remote" && host.identity.remote !== repository.normalized) throw oatsError("source-identity-change", "canonical repository identity differs from its source");
86
- if (expectedIdentity && !sameIdentity(host.identity, expectedIdentity)) throw oatsError("source-identity-change", "repository hosting identity changed");
87
- const identityKey = canonicalJson(host.identity);
88
- if (host.defaultBranch !== undefined && !defaults.has(identityKey)) defaults.set(identityKey, { selector: host.defaultBranch });
89
- return host;
90
- };
91
- const dataFor = (observation) => {
92
- active(); const data = handles.get(observation);
93
- if (!data) throw oatsError("invalid-source", "repository observation was not issued by this transaction");
94
- return data;
95
- };
96
- const list = (data, paths, recursive) => {
97
- const args = ["-C", data.directory, "ls-tree", "-z", "--full-tree", ...(recursive ? ["-r"] : []), data.commit];
98
- if (!paths.includes(".")) args.push("--", ...paths);
99
- const rows = decodeUtf8(git(args, 16 * 1024 * 1024)).split("\0").filter(Boolean);
100
- if ((entriesRead += rows.length) > maxEntries) throw oatsError("resource-limit", "repository entry budget exceeded");
101
- return rows.map((row) => {
102
- const at = row.indexOf("\t"), header = /^([0-7]{6}) (blob|tree|commit) ([a-f0-9]{40})$/.exec(row.slice(0, at)), path = row.slice(at + 1);
103
- if (!header || at < 0) throw oatsError("invalid-source", "repository returned a malformed tree entry");
104
- portablePath(path);
105
- if (path.split("/").some((part) => part.toLowerCase() === ".git")) throw oatsError("resource-not-contained", "source projection cannot include Git administrative files");
106
- if (!paths.some((root) => root === "." || path === root || path.startsWith(`${root}/`))) throw oatsError("resource-not-contained", "repository returned an unselected source path");
107
- return { mode: header[1], type: header[2], object: header[3], path };
108
- });
109
- };
110
- const blob = (data, entry, limit) => {
111
- if (entry.type !== "blob") throw oatsError("source-incomplete", "selected source contains an unsupported submodule or object kind");
112
- const key = `${data.directory}:${entry.object}`;
113
- if (!blobs.has(key)) {
114
- const sizeText = decodeUtf8(git(["-C", data.directory, "cat-file", "-s", entry.object], 1024)).trim();
115
- const size = Number(sizeText);
116
- if (!/^[0-9]+$/.test(sizeText) || !Number.isSafeInteger(size) || size > limit || readBytes + size > maxBytes) throw oatsError("resource-limit", "repository content byte budget exceeded");
117
- const bytes = git(["-C", data.directory, "cat-file", "blob", entry.object], size + 1024);
118
- if (bytes.length !== size) throw oatsError("integrity-drift", "repository blob changed during reading");
119
- readBytes += size; blobs.set(key, bytes);
120
- }
121
- const bytes = blobs.get(key);
122
- if (bytes.length > limit) throw oatsError("resource-limit", "repository descriptor byte limit exceeded");
123
- return Buffer.from(bytes);
124
- };
125
-
126
- return {
127
- identify(source, { expectedIdentity } = {}) {
128
- active();
129
- if (expectedIdentity !== undefined) validateRepositoryIdentity(expectedIdentity);
130
- const repository = parseRepositorySource(source), host = hostFor(repository, expectedIdentity);
131
- return Object.freeze({ identity: host.identity, source: repository.normalized, accessContextKey });
132
- },
133
- observe(source, { revision, origin, expectedIdentity } = {}) {
134
- active(); validateOrigin(origin);
135
- if (expectedIdentity !== undefined) validateRepositoryIdentity(expectedIdentity);
136
- // Local working snapshots use path: custody, not portable file: identities.
137
- // allowLocalGit only enables an explicit host/test transport mapping.
138
- const repository = parseRepositorySource(source);
139
- if (revision !== undefined) revisionSelector(revision);
140
- const key = canonicalJson({ source: repository.normalized, selector: revision ?? null });
141
- if (!memo.has(key)) {
142
- if (memo.size >= maxRepositories * 4) throw oatsError("resource-limit", "repository request count exceeded");
143
- // Cache failures too: a single transaction never re-observes a moving
144
- // request because its first attempt was inconvenient or inaccessible.
145
- const slot = {}; memo.set(key, slot);
146
- try {
147
- const host = hostFor(repository, expectedIdentity), identityKey = canonicalJson(host.identity);
148
- let selector = revision ?? defaults.get(identityKey)?.selector, exactHead = revision === undefined ? defaults.get(identityKey)?.exactHead : undefined;
149
- if (selector === undefined) {
150
- const head = decodeUtf8(git(["ls-remote", "--symref", "--", repository.url, "HEAD"], 64 * 1024));
151
- const symbolic = /^ref: refs\/heads\/(.+)\tHEAD$/m.exec(head), resolved = /^([a-f0-9]{40})\tHEAD$/m.exec(head);
152
- if (!symbolic || !resolved) throw oatsError("source-identity-unresolved", "repository default branch is unavailable");
153
- selector = symbolic[1]; exactHead = resolved[1];
154
- defaults.set(identityKey, { selector, exactHead });
155
- }
156
- revisionSelector(selector);
157
- const snapshotKey = canonicalJson({ identity: host.identity, selector });
158
- if (snapshots.has(snapshotKey)) {
159
- const existing = snapshots.get(snapshotKey);
160
- if (existing.error) throw existing.error;
161
- slot.data = existing.data;
162
- } else {
163
- if (snapshots.size >= maxRepositories) throw oatsError("resource-limit", "repository snapshot count exceeded");
164
- const directory = join(root, `repository-${snapshots.size}.git`);
165
- snapshots.set(snapshotKey, slot);
166
- // One selected shallow snapshot, not all repository branches/history.
167
- // Servers may decline filtering; budgets below bound selected reads,
168
- // not the transport's pack size. Git/host resource limits still apply.
169
- git(["init", "--quiet", "--bare", directory]);
170
- git(["-C", directory, "remote", "add", "origin", repository.url]);
171
- git(["-C", directory, "fetch", "--quiet", "--depth=1", "--filter=blob:none", "--no-tags", "--", "origin", exactHead ?? selector], 4 * 1024 * 1024);
172
- const commit = decodeUtf8(git(["-C", directory, "rev-parse", "--verify", "--quiet", "--end-of-options", "FETCH_HEAD^{commit}"], 1024)).trim();
173
- if (exactHead && commit !== exactHead) throw oatsError("integrity-drift", "fetched default branch differs from its exact observation");
174
- if (!/^[a-f0-9]{40}$/.test(commit)) throw oatsError("source-identity-unresolved", "repository selector did not resolve to an exact commit");
175
- slot.data = { directory, identity: Object.freeze({ ...host.identity }), selector, commit };
176
- }
177
- } catch (error) { slot.error = error; throw error; }
178
- }
179
- const slot = memo.get(key);
180
- if (slot.error) throw slot.error;
181
- const data = { ...slot.data, remote: repository.normalized };
182
- if (expectedIdentity && !sameIdentity(data.identity, expectedIdentity)) throw oatsError("source-identity-change", "cached repository identity differs from the request");
183
- const observation = Object.freeze({ schemaVersion: 1, source: Object.freeze({ identity: data.identity, remote: data.remote,
184
- selector: data.selector, commit: data.commit, provenance: freezeJson([structuredClone(origin)]) }), accessContextKey });
185
- handles.set(observation, data);
186
- return observation;
187
- },
188
- readFile(observation, path, { optional = false } = {}) {
189
- portablePath(path); const data = dataFor(observation), rows = list(data, [path], false);
190
- if (!rows.length && optional) return null;
191
- if (rows.length !== 1 || rows[0].path !== path || !["100644", "100755"].includes(rows[0].mode)) throw oatsError("export-not-found", "repository descriptor is absent or is not a regular file");
192
- const bytes = blob(data, rows[0], 1024 * 1024);
193
- return { bytes, origin: { kind: "source", source: data.remote, revision: data.commit, path, integrity: bytesIntegrity(bytes) } };
194
- },
195
- materialize(observation, roots, destination) {
196
- const data = dataFor(observation);
197
- canonicalJson(roots);
198
- if (!Array.isArray(roots) || !roots.length || roots.length > 1024 || new Set(roots).size !== roots.length) throw oatsError("invalid-source", "source projection needs a bounded unique root set");
199
- roots.forEach((path) => portablePath(path, { allowRoot: true }));
200
- if (typeof destination !== "string" || !isAbsolute(destination)) throw oatsError("invalid-source", "source projection destination must be explicit");
201
- const rows = list(data, roots, true);
202
- for (const path of roots) if (!rows.some((row) => path === "." || row.path === path || row.path.startsWith(`${path}/`))) throw oatsError("export-not-found", "required source projection root is absent");
203
- for (const entry of rows) validateProjectionPath(entry.path);
204
- const expected = new Map(rows.map((entry) => [entry.path, entry]));
205
- if (expected.size !== rows.length) throw oatsError("invalid-source", "source tree has duplicate paths");
206
- const projection = createSourceProjection(destination);
207
- try {
208
- for (const entry of rows) {
209
- const bytes = blob(data, entry, maxBytes);
210
- if (entry.mode === "120000") projection.link(entry.path, decodeUtf8(bytes, "source link target"));
211
- else if (["100644", "100755"].includes(entry.mode)) projection.file(entry.path, bytes, entry.mode === "100755" ? 0o755 : 0o644);
212
- else throw oatsError("source-incomplete", "source object mode cannot be retained");
213
- }
214
- projection.finish();
215
- return { directory: destination, roots: [...roots], observation };
216
- } catch (error) { error.stagingPath = destination; throw error; }
217
- },
218
- close() {
219
- if (closed) return;
220
- const current = lstatSync(root);
221
- if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError("source-unavailable", "repository transaction cleanup lost ownership");
222
- closed = true;
223
- rmSync(root, { recursive: true });
224
- },
225
- };
226
- }