@awebai/oats 0.24.13 → 0.25.1

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 (58) hide show
  1. package/bin/oats.mjs +994 -2837
  2. package/docs/capabilities.md +136 -323
  3. package/docs/configuration.md +68 -533
  4. package/docs/conventions.md +51 -24
  5. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +19 -14
  6. package/docs/design/2026-09-16-portable-migration-evidence.md +2 -0
  7. package/docs/design/2026-09-16-portable-onboarding.md +4 -2
  8. package/docs/design/2026-09-20-redesign-program-board.md +1 -1
  9. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +2 -0
  10. package/docs/design/2026-09-23-simplified-workspace-model.md +711 -0
  11. package/docs/design/2026-09-23-workspace-module-contracts.md +460 -0
  12. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +65 -0
  13. package/docs/design/README.md +20 -8
  14. package/docs/design/operations-contract.md +1 -0
  15. package/docs/design/package-engine-contract.md +1 -1
  16. package/docs/design/package-runtime-api.md +1 -1
  17. package/docs/desktop-cli-api.md +356 -5
  18. package/docs/desktop-succession.md +12 -6
  19. package/docs/desktop.md +9 -4
  20. package/docs/execution-targets.md +16 -4
  21. package/docs/first-team.md +107 -224
  22. package/docs/implementation.md +41 -11
  23. package/docs/integrations.md +50 -47
  24. package/docs/knowledge-capability-authoring.md +10 -4
  25. package/docs/knowledge-migration.md +21 -12
  26. package/docs/knowledge-reference/package-craft.md +11 -3
  27. package/docs/knowledge.md +60 -18
  28. package/docs/layers.md +3 -3
  29. package/docs/migration-from-oas.md +20 -9
  30. package/docs/oats-local.schema.json +50 -0
  31. package/docs/oats-membership.schema.json +23 -0
  32. package/docs/oats-workspace.schema.json +133 -48
  33. package/docs/official-marketplace.md +9 -6
  34. package/docs/packages.md +229 -440
  35. package/docs/rebuild-to-v2.md +347 -0
  36. package/docs/release-notes/v0.25.0.md +99 -0
  37. package/docs/release-notes/v0.25.1.md +94 -0
  38. package/docs/schedules.md +12 -6
  39. package/docs/soul.schema.json +41 -68
  40. package/docs/souls-and-instances.md +175 -108
  41. package/docs/workspace-adoption.md +70 -345
  42. package/docs/workspaces.md +436 -119
  43. package/lib/core.mjs +462 -61
  44. package/lib/instance-resolution.mjs +387 -0
  45. package/lib/materialize.mjs +580 -0
  46. package/lib/operator-dispatch.mjs +117 -0
  47. package/lib/packages.mjs +558 -1269
  48. package/lib/remote.mjs +718 -0
  49. package/lib/resolve.mjs +638 -0
  50. package/lib/schedule.mjs +90 -16
  51. package/lib/workspace.mjs +654 -0
  52. package/package.json +1 -1
  53. package/lib/portable-migration-artifacts.mjs +0 -135
  54. package/lib/portable-migration-evidence.mjs +0 -305
  55. package/lib/portable-migration-store.mjs +0 -199
  56. package/lib/portable-migration.mjs +0 -104
  57. package/lib/portable-onboarding-acceptance.mjs +0 -66
  58. package/lib/setup-expert-source.mjs +0 -100
@@ -1,199 +0,0 @@
1
- /** Immutable, unselectable historical evidence records.
2
- *
3
- * Partial/unknown evidence lives outside `.agents/resolutions` and can never be
4
- * consumed as dispatch authority. Publication re-verifies all inventoried input
5
- * witnesses first and changes no lock, home, schedule, session or approval. */
6
- import { linkSync, lstatSync, mkdirSync, mkdtempSync, opendirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
7
- import { join } from "node:path";
8
- import { ensureStoreIgnore } from "./capability-artifacts.mjs";
9
- import { readPortableBytes } from "./portable-files.mjs";
10
- import { JSON_FORMAT, jsonIntegrity, validateIntegrity } from "./portable-digest.mjs";
11
- import { canonicalJson, compareUtf8, parseStrictJson } from "./portable-values.mjs";
12
- import { portableScope } from "./portable-state.mjs";
13
- import { objectAt, stringAt, versionAt } from "./portable-shape.mjs";
14
- import { validateOrigin, validateResolutionRef } from "./resolution-shape.mjs";
15
- import { validatePortableMigrationInventory, verifyPortableMigrationInventory } from "./portable-migration-evidence.mjs";
16
- import { planPortableMigration } from "./portable-migration.mjs";
17
- import { verifyHistoricalHomeCapabilityCandidate } from "./portable-migration-artifacts.mjs";
18
- import { oatsError } from "./errors.mjs";
19
-
20
- export const RESOLUTION_EVIDENCE_VERSION = 1;
21
- const TARGET_KINDS = new Set(["selection-lock", "instance-home", "scheduled-job"]);
22
- const ACTIONS = new Set(["preserve", "verify", "hold"]);
23
- const exists = (path) => { try { return lstatSync(path); } catch (error) { if (error.code === "ENOENT") return null; throw error; } };
24
-
25
- export function validateResolutionEvidence(value) {
26
- canonicalJson(value);
27
- objectAt(value, ["schemaVersion", "target", "status", "evidence", "knownInputs", "unresolved", "resolution"],
28
- ["schemaVersion", "target", "status", "evidence", "knownInputs", "unresolved", "resolution"]);
29
- versionAt(value.schemaVersion);
30
- objectAt(value.target, ["kind", "id"], ["kind", "id"]);
31
- if (!TARGET_KINDS.has(value.target.kind)) throw oatsError("invalid-evidence", "unsupported migration evidence target kind");
32
- stringAt(value.target.id, "/target/id");
33
- if (!Array.isArray(value.evidence)) throw oatsError("invalid-evidence", "migration evidence origins must be an array");
34
- value.evidence.forEach(validateOrigin);
35
- objectAt(value.knownInputs, ["inventory", "action", "preserve"], ["inventory", "action", "preserve"]);
36
- validateIntegrity(value.knownInputs.inventory, [JSON_FORMAT]);
37
- if (!ACTIONS.has(value.knownInputs.action)) throw oatsError("invalid-evidence", "invalid planned migration action");
38
- canonicalJson(value.knownInputs.preserve);
39
- if (!Array.isArray(value.unresolved)) throw oatsError("invalid-evidence", "migration unresolved inputs must be an array");
40
- for (const item of value.unresolved) {
41
- objectAt(item, ["pointer", "code", "origins", "reason"], ["pointer", "code", "origins", "reason"]);
42
- stringAt(item.pointer, "/unresolved/pointer", { empty: true, pattern: /^(?:\/(?:[^~]|~[01])*)*$/ });
43
- stringAt(item.code, "/unresolved/code"); stringAt(item.reason, "/unresolved/reason");
44
- if (!Array.isArray(item.origins)) throw oatsError("invalid-evidence", "unresolved origins must be an array");
45
- item.origins.forEach(validateOrigin);
46
- }
47
- if (!['reconstructed', 'partial', 'unknown'].includes(value.status)) throw oatsError("invalid-evidence", "invalid migration evidence status");
48
- if (value.status === "reconstructed") {
49
- validateResolutionRef(value.resolution);
50
- if (!value.evidence.length) throw oatsError("resolution-incomplete", "reconstructed evidence requires historical witnesses");
51
- if (value.unresolved.length) throw oatsError("resolution-incomplete", "reconstructed evidence cannot retain unresolved managed inputs");
52
- } else if (value.resolution !== null) {
53
- throw oatsError("resolution-incomplete", "partial or unknown evidence cannot reference a captured resolution");
54
- }
55
- return value;
56
- }
57
-
58
- export function validateResolutionEvidenceRef(value) {
59
- canonicalJson(value, { maxBytes: 1024, maxDepth: 4, maxEntries: 8 });
60
- objectAt(value, ["schemaVersion", "id"], ["schemaVersion", "id"]);
61
- versionAt(value.schemaVersion);
62
- stringAt(value.id, "/id", { pattern: /^sha256-[a-f0-9]{64}$/ });
63
- return value;
64
- }
65
-
66
- function store(scope, create) {
67
- let root = portableScope(scope);
68
- for (const name of [".agents", "resolution-evidence"]) {
69
- root = join(root, name);
70
- if (create) { try { mkdirSync(root, { mode: 0o700 }); } catch (error) { if (error.code !== "EEXIST") throw error; } }
71
- const stat = exists(root);
72
- if (!stat) throw oatsError("evidence-not-found", "resolution evidence store is absent");
73
- if (!stat.isDirectory() || realpathSync(root) !== root) throw oatsError("invalid-evidence", "resolution evidence store component is not a real directory");
74
- }
75
- if (create) ensureStoreIgnore(root);
76
- return root;
77
- }
78
-
79
- function decode(path, reference) {
80
- const bytes = readPortableBytes(path, { missingCode: "evidence-not-found", invalidCode: "invalid-evidence" });
81
- const value = parseStrictJson(bytes);
82
- if (jsonIntegrity(value).value !== reference.id || !bytes.equals(Buffer.from(canonicalJson(value)))) {
83
- throw oatsError("integrity-drift", "resolution evidence bytes differ from their canonical address");
84
- }
85
- validateResolutionEvidence(value);
86
- return value;
87
- }
88
-
89
- export function readResolutionEvidence(scope, reference) {
90
- validateResolutionEvidenceRef(reference);
91
- return decode(join(store(scope, false), `${reference.id}.json`), reference);
92
- }
93
-
94
- /** Bounded diagnostic enumeration of immutable evidence. Staging and managed
95
- * ignore metadata are not records; every other entry must be one valid file. */
96
- export function listResolutionEvidence(scope, { maxEntries = 512, maxRecords = 256, maxBytes = 32 * 1024 * 1024, includeDocuments = false } = {}) {
97
- if (!Number.isSafeInteger(maxEntries) || maxEntries < 1 || maxEntries > 10_000
98
- || !Number.isSafeInteger(maxRecords) || maxRecords < 1 || maxRecords > 10_000
99
- || !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 256 * 1024 * 1024
100
- || typeof includeDocuments !== "boolean") {
101
- throw oatsError("invalid-evidence", "invalid resolution evidence listing options");
102
- }
103
- let root;
104
- try { root = store(scope, false); }
105
- catch (error) { if (error.code === "evidence-not-found") return []; throw error; }
106
- const entries = [];
107
- let visited = 0;
108
- const directory = opendirSync(root);
109
- try {
110
- for (;;) {
111
- const entry = directory.readSync();
112
- if (entry === null) break;
113
- if (++visited > maxEntries) throw oatsError("resource-limit", "resolution evidence visited-entry limit exceeded");
114
- if (entry.name === ".gitignore" || entry.name.startsWith(".evidence-")) continue;
115
- if (entries.length >= maxRecords) throw oatsError("resource-limit", "resolution evidence record limit exceeded");
116
- entries.push(entry);
117
- }
118
- } finally { directory.closeSync(); }
119
- const results = [];
120
- let totalBytes = 0;
121
- for (const entry of entries.sort((a, b) => compareUtf8(a.name, b.name))) {
122
- const match = /^(sha256-[a-f0-9]{64})\.json$/.exec(entry.name);
123
- if (!entry.isFile() || !match) throw oatsError("invalid-evidence", "resolution evidence store contains an unsupported entry");
124
- const reference = { schemaVersion: 1, id: match[1] }, document = decode(join(root, entry.name), reference);
125
- totalBytes += Buffer.byteLength(canonicalJson(document));
126
- if (totalBytes > maxBytes) throw oatsError("resource-limit", "aggregate resolution evidence byte limit exceeded");
127
- results.push({ reference, target: document.target, status: document.status, unresolved: document.unresolved.length,
128
- ...(includeDocuments ? { document } : {}) });
129
- }
130
- return results;
131
- }
132
-
133
- function targetKey(target) { return canonicalJson(target); }
134
- function entryFor(plan, target) {
135
- const wanted = targetKey(target), entry = plan.targets.find((candidate) => targetKey(candidate.target) === wanted);
136
- if (!entry) throw oatsError("invalid-evidence", "migration target is not in the verified inventory plan");
137
- if (entry.status === "reconstructed") throw oatsError("migration-required", "reconstructed publication requires the dedicated historical evidence verifier");
138
- return entry;
139
- }
140
- function publishEvidence(deployment, document) {
141
- validateResolutionEvidence(document);
142
- const reference = { schemaVersion: 1, id: jsonIntegrity(document).value };
143
- const root = store(deployment, true), destination = join(root, `${reference.id}.json`);
144
- if (exists(destination)) { decode(destination, reference); return reference; }
145
- const staging = mkdtempSync(join(root, ".evidence-"));
146
- let failed = false, primary;
147
- try {
148
- const candidate = join(staging, "record.json");
149
- writeFileSync(candidate, canonicalJson(document), { flag: "wx", mode: 0o600 });
150
- decode(candidate, reference);
151
- try { linkSync(candidate, destination); } catch (error) { if (error.code !== "EEXIST") throw error; }
152
- decode(destination, reference);
153
- return reference;
154
- } catch (error) { failed = true; primary = error; throw error; }
155
- finally {
156
- try { rmSync(staging, { recursive: true, force: true }); }
157
- catch (cleanup) {
158
- const error = failed ? new AggregateError([primary, cleanup], "evidence publication and cleanup failed", { cause: primary })
159
- : new Error("evidence staging cleanup failed", { cause: cleanup });
160
- error.code = failed ? primary.code : cleanup.code; error.stagingPath = staging; throw error;
161
- }
162
- }
163
- }
164
-
165
- /** Publish exactly one target from the deterministic plan after re-reading all
166
- * inventory witnesses. Reconstructed records require a later dedicated verifier
167
- * and are deliberately outside this function. */
168
- export function commitPlannedResolutionEvidence(scope, inventory, target, adapters = {}) {
169
- validatePortableMigrationInventory(inventory);
170
- const current = verifyPortableMigrationInventory(scope, inventory, adapters);
171
- const plan = planPortableMigration(current), entry = entryFor(plan, target);
172
- const document = { schemaVersion: RESOLUTION_EVIDENCE_VERSION,
173
- target: entry.target, status: entry.status, evidence: entry.evidence,
174
- knownInputs: { inventory: plan.inventory, action: entry.action, preserve: entry.preserve ?? null },
175
- unresolved: entry.unresolved, resolution: null };
176
- return publishEvidence(current.deployment, document);
177
- }
178
-
179
- /** Persist a narrow verified home→historical capability association as
180
- * partial evidence. Raw legacy source/settings/commands are intentionally not
181
- * copied; their literal document remains addressed by the evidence origins. */
182
- export function commitHistoricalHomeCapabilityEvidence(scope, inventory, request, adapters = {}) {
183
- validatePortableMigrationInventory(inventory);
184
- const candidate = verifyHistoricalHomeCapabilityCandidate(scope, inventory, request, adapters);
185
- const current = verifyPortableMigrationInventory(scope, inventory, adapters), plan = planPortableMigration(current);
186
- const entry = entryFor(plan, { kind: "instance-home", id: candidate.home.path });
187
- const origins = [...entry.evidence, ...candidate.artifact.evidence];
188
- const seen = new Set(), evidence = origins.filter((origin) => { const key = canonicalJson(origin); if (seen.has(key)) return false; seen.add(key); return true; })
189
- .sort((a, b) => compareUtf8(canonicalJson(a), canonicalJson(b)));
190
- const proof = { kind: candidate.artifact.package ? "home-capability-v2" : "home-capability-v1", home: candidate.home.path,
191
- capability: { id: candidate.artifact.capability.id, version: candidate.artifact.capability.row.version,
192
- package: candidate.artifact.package?.id ?? null, path: candidate.artifact.package ? candidate.artifact.capability.row.path : null },
193
- artifact: candidate.artifact.artifact, association: candidate.association,
194
- legacyTrustedClaim: candidate.legacyTrustedClaim, selectionAuthority: candidate.artifact.selectionAuthority,
195
- approvalAuthority: candidate.approvalAuthority, retentionStatus: candidate.artifact.retentionStatus };
196
- const document = { schemaVersion: RESOLUTION_EVIDENCE_VERSION, target: entry.target, status: "partial", evidence,
197
- knownInputs: { inventory: plan.inventory, action: "hold", preserve: proof }, unresolved: entry.unresolved, resolution: null };
198
- return publishEvidence(current.deployment, document);
199
- }
@@ -1,104 +0,0 @@
1
- /** Pure planning over literal historical evidence. No filesystem access, writes,
2
- * provider execution, source resolution, approval transfer or publication. */
3
- import { canonicalJson, compareUtf8 } from "./portable-values.mjs";
4
- import { jsonIntegrity } from "./portable-digest.mjs";
5
- import { validatePortableMigrationInventory } from "./portable-migration-evidence.mjs";
6
- import { oatsError } from "./errors.mjs";
7
-
8
- export const MIGRATION_PLAN_VERSION = 1;
9
- const originFor = (receipt, pointer = "") => receipt.state === "absent" ? [] : [{ kind: "migration-evidence",
10
- document: { kind: "deployment", path: receipt.path, integrity: receipt.integrity }, pointer }];
11
- const unresolved = (code, reason, receipt, pointer = "") => ({ pointer, code, origins: originFor(receipt, pointer), reason });
12
-
13
- function lockPlan(lock) {
14
- const target = { kind: "selection-lock", id: lock.path };
15
- if (lock.state === "absent") return { target, status: "unknown", action: "hold", evidence: [],
16
- unresolved: [unresolved("migration-held", "the historical lock file is absent", lock)] };
17
- const evidence = originFor(lock);
18
- if (lock.state === "invalid" || lock.format === "unknown") return { target, status: "unknown", action: "hold", evidence,
19
- unresolved: [unresolved("invalid-lock", "the literal historical lock is malformed or unsupported", lock)] };
20
- if (lock.format === "portable-v3") return { target, status: "partial", action: "verify", evidence,
21
- unresolved: [unresolved("migration-required", "the current lock still needs its authoritative lock-v3 reader; a lock is not historical dispatch authority", lock)] };
22
- const empty = lock.rowCounts.packages === 0 && lock.rowCounts.capabilities === 0;
23
- return { target, status: empty ? "unknown" : "partial", action: "hold", evidence,
24
- unresolved: [
25
- ...(lock.semanticVerification === "verified" ? [] : [unresolved("migration-required", "the strict legacy lock reader must verify every row without reinterpretation", lock)]),
26
- unresolved("migration-held", "a deployment lock does not prove which revision a particular home or job used", lock),
27
- unresolved("approval-required", "legacy trust is evidence only and cannot grant new-format exact-artifact approval", lock),
28
- ] };
29
- }
30
-
31
- function homePlan(home) {
32
- const target = { kind: "instance-home", id: home.path };
33
- const receipts = [home.document, home.cleanup.document].filter((entry) => entry.state !== "absent");
34
- const evidence = receipts.flatMap((entry) => originFor(entry));
35
- if (!receipts.length) return { target, status: "unknown", action: "hold", evidence: [],
36
- unresolved: [unresolved("migration-held", "the named home has no instance or quarantine metadata", home.document)] };
37
- if (home.executionBinding.state === "valid-shape") return { target, status: "partial", action: "verify", evidence,
38
- preserve: { executionBinding: home.executionBinding.value },
39
- unresolved: [
40
- unresolved("migration-required", "the referenced captured resolution and all retained inputs must be verified before adoption", home.document, "/executionBinding"),
41
- unresolved("approval-required", "current exact-artifact approval and action-specific readiness remain separate", home.document, "/executionBinding"),
42
- ] };
43
- const hasRuntime = home.capabilities.length || home.cleanup.capabilities.length;
44
- const invalid = home.document.state === "invalid" || home.cleanup.document.state === "invalid" || home.executionBinding.state === "invalid";
45
- return { target, status: hasRuntime && !invalid ? "partial" : "unknown", action: "hold", evidence,
46
- unresolved: [
47
- ...(invalid ? [unresolved("invalid-evidence", "historical home metadata is malformed or has an invalid captured binding", home.document)] : []),
48
- unresolved("migration-held", "the home does not carry a verified complete captured resolution", home.document),
49
- unresolved("migration-held", "historical source revision, full managed resources and helper closure are not established by home metadata", home.document),
50
- unresolved("approval-required", "recorded legacy trust cannot authorize a reconstructed owner-exec artifact", home.document),
51
- ...(home.cleanup.document.state !== "absent" ? [unresolved("migration-held", "cleanup obligations must remain retryable and cannot be discarded during migration", home.cleanup.document)] : []),
52
- ] };
53
- }
54
-
55
- function executionCandidates(job) {
56
- return [
57
- ["definition", job.execution],
58
- ["attempt", job.attempt],
59
- ["lastRun", job.lastRun],
60
- ].filter(([, value]) => value.state !== "absent");
61
- }
62
-
63
- function schedulePlan(schedule, job) {
64
- const target = { kind: "scheduled-job", id: `${schedule.path}:${job.id}` };
65
- const evidence = [schedule.definitions, schedule.state].filter((entry) => entry.state !== "absent").flatMap((entry) => originFor(entry));
66
- const executions = executionCandidates(job), valid = executions.filter(([, value]) => value.state === "valid-shape");
67
- const invalid = executions.some(([, value]) => value.state === "invalid") || !!job.problems?.length
68
- || schedule.definitions.state === "invalid" || schedule.state.state === "invalid";
69
- const unresolvedCustody = [job.attempt.state, job.lastRun.state].some((state) => state === "legacy" || state === "invalid");
70
- if (unresolvedCustody) return { target, status: "unknown", action: "hold", evidence,
71
- unresolved: [
72
- ...(invalid ? [unresolved("invalid-evidence", "the admitted or unknown attempt evidence is malformed", schedule.state)] : []),
73
- unresolved("migration-held", "an existing admitted or unknown historical attempt has no valid captured execution; preserve it exactly and require owner reconciliation", schedule.state),
74
- ] };
75
- if (valid.length) {
76
- const preserve = { executions: valid.map(([source, value]) => ({ source, ...value })) };
77
- return { target, status: "partial", action: "preserve", evidence, preserve,
78
- unresolved: [
79
- ...(invalid ? [unresolved("invalid-evidence", "one or more schedule records disagree with the valid captured authority and must remain held", schedule.state)] : []),
80
- unresolved("migration-required", "each referenced resolution and admitted execution must be verified without rebinding", schedule.state),
81
- ] };
82
- }
83
- const hasAttempt = job.attempt.state !== "absent" || job.lastRun.state !== "absent";
84
- return { target, status: "unknown", action: "hold", evidence,
85
- unresolved: [unresolved("migration-held", hasAttempt
86
- ? "the admitted or unknown historical attempt has no valid captured execution; preserve it exactly and require owner reconciliation"
87
- : "the legacy schedule has no captured software authority", schedule.state)] };
88
- }
89
-
90
- /** Produce a deterministic no-write plan. No target can become reconstructed at
91
- * this stage: reconstruction requires the dedicated historical verifier. */
92
- export function planPortableMigration(inventory) {
93
- validatePortableMigrationInventory(inventory);
94
- const targets = [
95
- ...inventory.locks.map(lockPlan),
96
- ...inventory.homes.map(homePlan),
97
- ...inventory.schedules.flatMap((schedule) => schedule.jobs.map((job) => schedulePlan(schedule, job))),
98
- ].sort((a, b) => compareUtf8(`${a.target.kind}:${a.target.id}`, `${b.target.kind}:${b.target.id}`));
99
- if (targets.some((entry) => entry.status === "reconstructed")) throw oatsError("invalid-evidence", "unverified planner output cannot claim reconstruction");
100
- const plan = { schemaVersion: MIGRATION_PLAN_VERSION, inventory: jsonIntegrity(inventory), readyToApply: false,
101
- effects: { writes: false, providerExecution: false, sessionChanges: false, scheduleChanges: false, trustTransfer: false }, targets };
102
- canonicalJson(plan);
103
- return plan;
104
- }
@@ -1,66 +0,0 @@
1
- /** Fresh setup acceptance boundary. Read-only comparison and explicit mutation
2
- * handoff only; all source discovery and request construction remain owned by
3
- * portable-onboarding. */
4
- import { canonicalJson, freezeJson } from "./portable-values.mjs";
5
- import { sameIdentity } from "./portable-identity.mjs";
6
- import { buildFreshPreparationRequest, recheckFreshOnboarding } from "./portable-onboarding.mjs";
7
- import { oatsError } from "./errors.mjs";
8
-
9
- export const PORTABLE_ONBOARDING_ACCEPTANCE_VERSION = 1;
10
-
11
- /** Prove two issued/ready inspections name one unchanged source while retaining
12
- * distinct organization and standalone work/context facts. */
13
- export function compareFreshSourceAcceptance({ organization, standalone }) {
14
- const organizationRequest = buildFreshPreparationRequest(organization), standaloneRequest = buildFreshPreparationRequest(standalone);
15
- if (!organization.workspace || organization.context.kind !== "workspace") throw oatsError("invalid-declaration", "organization acceptance requires an explicit workspace context");
16
- if (standalone.workspace !== null || standalone.context.kind !== "standalone") throw oatsError("invalid-declaration", "standalone acceptance requires an explicit standalone context");
17
- if (standalone.workTarget.git.present) throw oatsError("invalid-declaration", "standalone acceptance work target must be explicitly non-Git");
18
- if (!sameIdentity(organization.source.identity, standalone.source.identity)) throw oatsError("source-identity-change", "acceptance inspections name different source identities");
19
- if (organization.source.revision.commit !== standalone.source.revision.commit
20
- || organization.source.exportPath !== standalone.source.exportPath
21
- || organization.source.definition !== standalone.source.definition) {
22
- throw oatsError("integrity-drift", "acceptance inspections do not name the same unchanged source export");
23
- }
24
- return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_ACCEPTANCE_VERSION, status: "ready",
25
- source: { identity: organization.source.identity, commit: organization.source.revision.commit,
26
- exportPath: organization.source.exportPath, definition: organization.source.definition },
27
- organization: { context: organization.context, workTarget: organization.workTarget,
28
- preparation: organizationRequest.preparation },
29
- standalone: { context: standalone.context, workTarget: standalone.workTarget,
30
- preparation: standaloneRequest.preparation },
31
- effects: { writes: false, preparation: false, approval: false, enrollment: false } });
32
- }
33
-
34
- function approvalRequests(result) {
35
- const requests = [];
36
- for (const selection of Array.isArray(result?.selections) ? result.selections : []) {
37
- for (const capability of Array.isArray(selection.approvalRequired) ? selection.approvalRequired : []) {
38
- requests.push({ capability, artifactSet: selection.artifactSet, request: selection.request });
39
- }
40
- }
41
- return requests;
42
- }
43
-
44
- /** Recheck fresh deployment state immediately before an explicitly supplied
45
- * public preparation function. Missing integration/provisioning remains pending;
46
- * no stale inspection is permission to overwrite managed state. */
47
- export function prepareFreshOnboarding(inspection, options = {}, { prepareCapturedComposition } = {}) {
48
- canonicalJson(options);
49
- const built = buildFreshPreparationRequest(inspection, options);
50
- const preflight = recheckFreshOnboarding(inspection);
51
- const requestedBindings = built.preparation.operator?.bindings ?? null;
52
- if (preflight.deployment.state === "absent") return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_ACCEPTANCE_VERSION,
53
- status: "pending", code: "fresh-deployment-provisioning-required", mutationAttempted: false,
54
- preparation: built.preparation, workTarget: built.workTarget, requestedBindings,
55
- resolution: null, approvalRequests: [], problems: [{ code: "fresh-deployment-provisioning-required", message: "provision the explicit fresh deployment path before preparation" }] });
56
- if (typeof prepareCapturedComposition !== "function") return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_ACCEPTANCE_VERSION,
57
- status: "pending", code: "onboarding-integration-required", mutationAttempted: false,
58
- preparation: built.preparation, workTarget: built.workTarget, requestedBindings,
59
- resolution: null, approvalRequests: [], problems: [{ code: "onboarding-integration-required", message: "the public captured preparation adapter is not available" }] });
60
- const result = prepareCapturedComposition(built.preparation);
61
- if (!result || typeof result !== "object" || Array.isArray(result)) throw oatsError("invalid-resolution", "public preparation returned no structured result");
62
- return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_ACCEPTANCE_VERSION,
63
- status: typeof result.status === "string" ? result.status : "pending", mutationAttempted: true,
64
- preparation: built.preparation, workTarget: built.workTarget, requestedBindings,
65
- resolution: result.resolution ?? null, approvalRequests: approvalRequests(result), problems: Array.isArray(result.problems) ? result.problems : [] });
66
- }
@@ -1,100 +0,0 @@
1
- /** Read the setup edition as data for a CLASSIC local bootstrap copy.
2
- * No provider code, workspace activation, enrollment or captured identity. */
3
- import { lstatSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync } from 'node:fs';
4
- import { dirname, join } from 'node:path';
5
- import { tmpdir } from 'node:os';
6
- import { fileURLToPath } from 'node:url';
7
- import { parsePortableSoul } from './portable-soul.mjs';
8
- import { parseRepositorySource, parsePortableSource } from './source-spec.mjs';
9
- import { createRepositoryTransaction } from './repository-observation.mjs';
10
- import { createWorkspaceDiscovery } from './workspace-discovery.mjs';
11
- import { packageIntegrity } from './core.mjs';
12
- import { oatsError } from './errors.mjs';
13
-
14
- export const SETUP_EXPERT = 'oats-setup-expert';
15
- export const SETUP_CAPABILITIES = Object.freeze(['oats.core', 'oats.setup']);
16
- const EXPORT = `souls/${SETUP_EXPERT}`;
17
- function validateEdition(bytes) {
18
- const { declaration: soul } = parsePortableSoul(bytes);
19
- const required = soul.requires || {}, caps = required.capabilities || {};
20
- if (soul.name !== SETUP_EXPERT || soul.work !== 'directory'
21
- || Object.keys(required).some(key => key !== 'capabilities')
22
- || Object.keys(caps).length !== 2 || SETUP_CAPABILITIES.some(id => caps[id]?.source !== 'repo:oats-package' || Object.keys(caps[id]).some(key => key !== 'source'))
23
- || ['knowledge', 'messaging', 'tasks'].some(slot => soul.defaults?.[slot] !== 'none')
24
- || Object.keys(soul.defaults || {}).some(key => !['knowledge', 'messaging', 'tasks'].includes(key))
25
- || soul.knowledge || soul.teams?.length || soul.resources?.length || soul.yolo === true || soul.backend || soul['launch-config']) {
26
- throw oatsError('needs-configuration', 'classic setup bootstrap needs the provider-independent directory edition with only oats.core/oats.setup; use explicit portable preparation for other requirements');
27
- }
28
- return soul;
29
- }
30
- function repositoryRequest(source) {
31
- if (typeof source !== 'string' || source.includes('#')) throw oatsError('invalid-source', '--workspace needs a Git repository source, optionally @revision, without a package fragment');
32
- try { return { source: parseRepositorySource(source).normalized }; }
33
- catch {
34
- const parsed = parsePortableSource(source);
35
- if (parsed.kind !== 'git') throw oatsError('invalid-source', '--workspace needs an explicit Git repository source');
36
- return { source: `git:${parsed.url}`, revision: parsed.selector };
37
- }
38
- }
39
-
40
- /** `packages["oats.framework"]` from the repository's own `package-catalog.json` at the observed
41
- * revision, or null when the file or entry is absent. Data only; the acquisition still verifies
42
- * bytes, and the edition/package integrity comparison still applies. */
43
- function frameworkCatalogEntry(transaction, observation) {
44
- const file = transaction.readFile(observation, 'package-catalog.json', { optional: true });
45
- if (!file) return null;
46
- let doc;
47
- try { doc = JSON.parse(file.bytes.toString('utf8')); } catch { throw oatsError('invalid-source', 'workspace package-catalog.json is not valid JSON'); }
48
- const entry = doc && typeof doc === 'object' && !Array.isArray(doc) && doc.packages && typeof doc.packages === 'object' && !Array.isArray(doc.packages)
49
- ? doc.packages['oats.framework'] : undefined;
50
- if (entry === undefined) return null;
51
- if (!entry || typeof entry !== 'object' || Array.isArray(entry) || typeof entry.url !== 'string' || typeof entry.ref !== 'string' || (entry.path !== undefined && typeof entry.path !== 'string')) {
52
- throw oatsError('invalid-source', 'workspace package-catalog.json has an invalid oats.framework entry');
53
- }
54
- return { url: entry.url, ref: entry.ref, ...(entry.path === undefined ? {} : { path: entry.path }), revision: observation.source.commit };
55
- }
56
-
57
- export function loadSetupExpertEdition(workspace, repositoryOptions = {}) {
58
- if (workspace === undefined) {
59
- const root = fileURLToPath(new URL(`../${EXPORT}/`, import.meta.url));
60
- return { declaration: validateEdition(readFileSync(join(root, 'soul.yaml'))), instructions: readFileSync(join(root, 'AGENTS.md'), 'utf8'),
61
- source: { kind: 'packaged-definition', captured: false }, packageIntegrity: null, catalogEntry: null };
62
- }
63
- const request = repositoryRequest(workspace), scratch = realpathSync(mkdtempSync(join(tmpdir(), 'oats-setup-source-'))), owned = lstatSync(scratch);
64
- let transaction;
65
- try {
66
- transaction = createRepositoryTransaction({ ...repositoryOptions, directory: scratch, accessContextKey: 'explicit-setup-source' });
67
- const discovery = createWorkspaceDiscovery(transaction), origin = { kind: 'operator', document: { kind: 'operator', id: 'oats-onboard' }, pointer: '/workspace' };
68
- const observed = transaction.observe(request.source, { ...request, origin });
69
- const workspaceDoc = transaction.readFile(observed, 'oats-workspace.yaml', { optional: true });
70
- const view = workspaceDoc ? discovery.readWorkspace({ ...request, origin }) : null;
71
- const reference = view?.parsed.imports.find(item => item.alias === SETUP_EXPERT)
72
- ?? { source: request.source, revision: observed.source.commit, soul: EXPORT, alias: SETUP_EXPERT };
73
- // A selected workspace import never falls back if its exact source fails.
74
- const imported = discovery.importSoul(reference, { origin });
75
- if (reference.adoption) throw oatsError('needs-configuration', 'classic setup copies an edition only; provider adoption values require the portable preparation path');
76
- const declaration = validateEdition(transaction.readFile(imported.observation, imported.definition).bytes);
77
- const projection = join(scratch, 'edition');
78
- transaction.materialize(imported.observation, imported.roots, projection);
79
- const soulRoot = join(projection, dirname(imported.definition)), body = join(soulRoot, 'AGENTS.md'), alias = join(soulRoot, 'CLAUDE.md');
80
- if (!lstatSync(body).isFile() || !lstatSync(alias).isSymbolicLink() || readlinkSync(alias) !== 'AGENTS.md'
81
- || readdirSync(soulRoot).some(name => !['soul.yaml', 'AGENTS.md', 'CLAUDE.md'].includes(name))) {
82
- throw oatsError('source-incomplete', 'classic setup edition must have canonical AGENTS.md/CLAUDE.md and no omitted private skill or knowledge trees');
83
- }
84
- // The workspace publishes the reviewed catalog: read the oats.framework entry at the WORKSPACE's
85
- // observed revision (falling back to the edition's own revision when the source has no workspace
86
- // document), so onboarding acquires the framework the workspace currently names instead of
87
- // whatever the kernel tarball snapshotted at its own tag. The edition/package integrity check
88
- // below still decides whether that acquisition matches the copied edition.
89
- const catalogEntry = frameworkCatalogEntry(transaction, observed) ?? frameworkCatalogEntry(transaction, imported.observation);
90
- return { declaration, instructions: readFileSync(body, 'utf8'), packageIntegrity: packageIntegrity(join(projection, 'oats-package')),
91
- catalogEntry,
92
- source: { kind: 'exported-edition-copy', source: imported.reference.source, revision: imported.observation.source.commit,
93
- path: imported.reference.soul, workspaceRevision: view?.source.commit ?? null, captured: false } };
94
- } finally {
95
- transaction?.close();
96
- const current = lstatSync(scratch);
97
- if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError('source-unavailable', 'setup source scratch ownership changed');
98
- rmSync(scratch, { recursive: true });
99
- }
100
- }