@awebai/oats 0.27.2 → 0.29.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.
- package/bin/oats.mjs +445 -96
- package/capabilities/oats-okf/bin/oats-okf.mjs +55 -30
- package/capabilities/oats-okf/injects/okf.md +36 -28
- package/capabilities/oats-okf/lib/binding-wire.mjs +4 -1
- package/capabilities/oats-okf/lib/config.mjs +6 -1
- package/capabilities/oats-okf/lib/consult.mjs +496 -0
- package/capabilities/oats-okf/lib/harvest-status.mjs +88 -0
- package/capabilities/oats-okf/lib/harvest-switch.mjs +81 -0
- package/capabilities/oats-okf/lib/inspection.mjs +11 -3
- package/capabilities/oats-okf/lib/io.mjs +9 -2
- package/capabilities/oats-okf/lib/okf-validate.mjs +123 -0
- package/capabilities/oats-okf/lib/sources.mjs +42 -55
- package/capabilities/oats-okf/lib/stores.mjs +19 -11
- package/capabilities/oats-okf/lib/worker.mjs +90 -8
- package/capabilities/oats-okf/oats.json +24 -9
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +144 -0
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +86 -0
- package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +104 -0
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +140 -0
- package/capabilities/oats-okf-harvest/injects/harvester.md +12 -0
- package/capabilities/oats-okf-harvest/oats.json +26 -0
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +168 -0
- package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +192 -0
- package/capabilities/{oats-okf/skills/okf → oats-okf-harvest/skills/okf-authoring}/SKILL.md +15 -22
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +149 -0
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +12 -0
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +45 -0
- package/capabilities/oats-okf-maintenance/oats.json +21 -0
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +144 -0
- package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +192 -0
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +151 -0
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +123 -0
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +146 -0
- package/capabilities/oats-review/injects/review.md +3 -2
- package/capabilities/oats-review/oats.json +3 -4
- package/docs/capabilities.md +41 -9
- package/docs/capability-manifest.schema.json +0 -7
- package/docs/design/2026-09-24-phase-d-plan.md +11 -0
- package/docs/design/2026-09-26-desktop-design-brief-architecture.md +241 -0
- package/docs/design/2026-09-26-okf-knowledge-operations.md +389 -0
- package/docs/desktop-cli-api.md +342 -10
- package/docs/implementation.md +1 -1
- package/docs/knowledge-capability-authoring.md +8 -2
- package/docs/knowledge-reference/package-craft.md +8 -5
- package/docs/knowledge.md +101 -0
- package/docs/oats-local.schema.json +33 -2
- package/docs/oats-package.schema.json +39 -0
- package/docs/official-catalog.md +7 -4
- package/docs/packages.md +76 -6
- package/docs/release-lane.md +1 -1
- package/docs/release-notes/v0.28.0.md +144 -0
- package/docs/release-notes/v0.29.0.md +240 -0
- package/docs/schedules.md +230 -4
- package/docs/souls-and-instances.md +11 -9
- package/docs/workspaces.md +18 -3
- package/lib/automations.mjs +369 -0
- package/lib/core.mjs +87 -158
- package/lib/instance-inspect.mjs +16 -8
- package/lib/instance-resolution.mjs +90 -197
- package/lib/materialize.mjs +18 -7
- package/lib/operator-dispatch.mjs +1 -2
- package/lib/packages.mjs +107 -6
- package/lib/remote.mjs +21 -1
- package/lib/resolve.mjs +71 -9
- package/lib/schedule.mjs +228 -45
- package/lib/triggers.mjs +678 -0
- package/lib/workspace.mjs +81 -4
- package/package-catalog.json +6 -4
- package/package.json +1 -1
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +0 -21
- package/capabilities/oats-okf/agents/memory-harvest/soul.yaml +0 -5
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +0 -285
- package/capabilities/oats-review/agents/reviewer/AGENTS.md +0 -53
- package/capabilities/oats-review/agents/reviewer/soul.yaml +0 -6
- /package/capabilities/{oats-okf/skills/okf → oats-okf-harvest/skills/okf-authoring}/scripts/okf-validate.mjs +0 -0
|
@@ -16,19 +16,19 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Nothing here reads `oats-config.yaml`, an installed-capability directory or a
|
|
18
18
|
* per-soul `source:` — those do not exist in this model. */
|
|
19
|
-
import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync
|
|
20
|
-
import { join, resolve as resolvePath, dirname,
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
|
|
20
|
+
import { join, resolve as resolvePath, dirname, relative, isAbsolute, sep } from "node:path";
|
|
21
21
|
import { oatsError } from "./errors.mjs";
|
|
22
22
|
import { loadLocal, discoverWorkspace, discoverRepo, standaloneRepo, observeWorkspace, observeSoulLabels } from "./workspace.mjs";
|
|
23
|
-
import { resolveSoul,
|
|
24
|
-
import { declaredSettings
|
|
23
|
+
import { resolveSoul, teamsOf, kernelCompatibility } from "./resolve.mjs";
|
|
24
|
+
import { declaredSettings } from "./capability-contract.mjs";
|
|
25
25
|
import { materialize, MODULES_DIR, SKILLS_DIR } from "./materialize.mjs";
|
|
26
26
|
import { fetchRemoteTree } from "./remote.mjs";
|
|
27
27
|
import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync, symlinkSync, statSync } from "node:fs";
|
|
28
28
|
import { tmpdir } from "node:os";
|
|
29
29
|
import { spawnSync } from "node:child_process";
|
|
30
30
|
import { randomBytes } from "node:crypto";
|
|
31
|
-
import { readLock, LOCK_FILE,
|
|
31
|
+
import { readLock, LOCK_FILE, SOUL_ALIAS_SYMLINK } from "./packages.mjs";
|
|
32
32
|
import * as defaultRemote from "./remote.mjs";
|
|
33
33
|
import { parseRepoRef } from "./remote.mjs";
|
|
34
34
|
|
|
@@ -77,30 +77,58 @@ export function parseProviderFlags(pairs) {
|
|
|
77
77
|
return JSON.parse(JSON.stringify(out));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
*
|
|
80
|
+
/** The qualified name of a discovered soul: `<package>/<soul>` for a package soul,
|
|
81
|
+
* `<member name>/<soul>` for a member's, `<source repo name>/<soul>` for an external one. */
|
|
82
|
+
export function qualifiedSoulName(entry) {
|
|
83
|
+
if (typeof entry?.package === "string") return `${entry.package}/${entry.name}`;
|
|
84
|
+
return `${memberNameOf(entry?.repoKey ?? "")}/${entry?.name}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Find the soul named `name` in a discovery: confirmed members, external souls and package
|
|
88
|
+
* souls. A bare name must be unique across all three (else E_SOUL_AMBIGUOUS naming each
|
|
89
|
+
* qualified form); `<repo>/<soul>` names a member (its key, a key suffix or its member name)
|
|
90
|
+
* or an external source, `<package>/<soul>` a package soul. */
|
|
82
91
|
export function findSoulEntry(discovery, name) {
|
|
83
92
|
const [repoPart, soulPart] = name.includes("/") && !name.startsWith("/") ? [name.slice(0, name.lastIndexOf("/")), name.slice(name.lastIndexOf("/") + 1)] : [null, name];
|
|
84
93
|
const hits = [];
|
|
85
94
|
// A standalone view's one row is the repo's own (unconfirmed by definition — the
|
|
86
95
|
// workspace could not be read); resolveSoul admits exactly that case.
|
|
87
96
|
const standaloneOwn = discovery.standalone === true ? discovery.key : null;
|
|
97
|
+
const memberMatches = (key) => !repoPart || key === repoPart || key.endsWith(repoPart) || memberNameOf(key) === repoPart;
|
|
88
98
|
for (const m of discovery.members || []) {
|
|
89
99
|
if (!m.confirmed && m.key !== standaloneOwn) continue;
|
|
90
100
|
for (const s of m.souls || []) {
|
|
91
|
-
if (s.name !== soulPart) continue;
|
|
92
|
-
if (repoPart && !m.key.endsWith(repoPart) && m.key !== repoPart) continue;
|
|
101
|
+
if (s.name !== soulPart || !memberMatches(m.key)) continue;
|
|
93
102
|
hits.push({ ...s, repoKey: m.key, memberCommit: m.commit, external: false });
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
105
|
for (const x of discovery.external || []) {
|
|
97
106
|
if (x.soul?.name === soulPart && (!repoPart || (x.source && String(x.source).includes(repoPart)))) hits.push({ ...x.soul, repoKey: x.soul.repoKey ?? parseRepoRef(x.source.replace(/@.*$/, "")).key, commit: x.commit, external: true });
|
|
98
107
|
}
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
for (const s of discovery.packageSouls || []) {
|
|
109
|
+
if (s.name === soulPart && (!repoPart || repoPart === s.package)) hits.push({ ...s, external: false });
|
|
110
|
+
}
|
|
111
|
+
if (hits.length === 0) throw err("E_SOUL_UNKNOWN", `no soul ${JSON.stringify(name)} among the confirmed members, external souls or package souls of this workspace`, { name, members: (discovery.members || []).filter((m) => m.confirmed).map((m) => m.key), packages: [...new Set((discovery.packageSouls || []).map((s) => s.package))].sort() });
|
|
112
|
+
if (hits.length > 1) {
|
|
113
|
+
const qualified = hits.map(qualifiedSoulName);
|
|
114
|
+
throw err("E_SOUL_AMBIGUOUS", `soul ${JSON.stringify(name)} is ${hits.length} souls (${qualified.join(", ")}); name one of them`, { name, repos: hits.map((h) => h.repoKey), qualified });
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(hits[0].collides)) throw err("E_SOUL_AMBIGUOUS", `package souls ${hits[0].collides.join(" and ")} would share the agent directory agents/${hits[0].agentName}/ — keep one of the packages in the workspace's packages:`, { name, agentDir: hits[0].agentName, qualified: hits[0].collides });
|
|
101
117
|
return hits[0];
|
|
102
118
|
}
|
|
103
119
|
|
|
120
|
+
/** The agents-root directory of a discovered soul: a package soul's is `<package>--<soul>`
|
|
121
|
+
* (packageSoulAgentName), every other soul's is its name. */
|
|
122
|
+
export function agentDirOf(entry) { return typeof entry?.agentName === "string" ? entry.agentName : entry?.name; }
|
|
123
|
+
|
|
124
|
+
/** `oats-local.yaml` `souls.disabled` (not run on this machine): an entry names a soul by its
|
|
125
|
+
* bare name (every soul of that name) or its qualified name. → the matching entry or null. */
|
|
126
|
+
export function disabledEntry(local, soulEntry) {
|
|
127
|
+
const list = Array.isArray(local?.souls?.disabled) ? local.souls.disabled : [];
|
|
128
|
+
const qualified = qualifiedSoulName(soulEntry);
|
|
129
|
+
return list.find((d) => d === soulEntry.name || d === qualified) ?? null;
|
|
130
|
+
}
|
|
131
|
+
|
|
104
132
|
/**
|
|
105
133
|
* A home's LIVE eligible teams (teams contract 2026-09-25, decision 6). Teams are messaging
|
|
106
134
|
* state, not frozen composition: the soul's labels are read from its repository NOW and each
|
|
@@ -115,8 +143,10 @@ export function findSoulEntry(discovery, name) {
|
|
|
115
143
|
export async function liveTeams(home, meta, { remoteOptions, remote } = {}) {
|
|
116
144
|
const recorded = (reason, error) => ({ teams: Array.isArray(meta?.teams) ? meta.teams : null, source: "recorded", reason, ...(error ? { error } : {}) });
|
|
117
145
|
const soul = meta?.workspace?.soul;
|
|
118
|
-
// A
|
|
146
|
+
// A home without a workspace soul record (a pre-0.29 capability agent's) has nothing to re-read.
|
|
119
147
|
if (!soul || typeof soul.repoKey !== "string" || typeof meta.agent !== "string") return recorded("no-workspace-soul");
|
|
148
|
+
// A package soul's labels are pinned with the package: the spawn-time record is its answer.
|
|
149
|
+
if (soul.package && typeof soul.package === "object") return recorded("package-soul");
|
|
120
150
|
const deployment = dirname(dirname(dirname(dirname(resolvePath(home)))));
|
|
121
151
|
try {
|
|
122
152
|
const found = loadLocal(deployment);
|
|
@@ -162,9 +192,13 @@ function swapSoulPointer(soulDir, target) {
|
|
|
162
192
|
/** Fetch a prepared soul's source tree (soul.yaml + AGENTS.md, CLAUDE.md → AGENTS.md) into `dest`. */
|
|
163
193
|
async function fetchSoulSource(prepared, dest) {
|
|
164
194
|
const e = prepared.soulEntry;
|
|
165
|
-
const ref = e.repoKey.startsWith("local/") ? e.repoKey.slice("local/".length) : `git:${e.repoKey}`;
|
|
195
|
+
const ref = typeof e.ref === "string" ? e.ref : e.repoKey.startsWith("local/") ? e.repoKey.slice("local/".length) : `git:${e.repoKey}`;
|
|
166
196
|
// A soul's CLAUDE.md → AGENTS.md alias is the one symlink a soul source may carry.
|
|
167
|
-
await fetchRemoteTree(ref, e.commit, e.path, dest, { ...(prepared.remoteOptions || {}), allowSymlinks:
|
|
197
|
+
const { digest } = await fetchRemoteTree(ref, e.commit, e.path, dest, { ...(prepared.remoteOptions || {}), allowSymlinks: SOUL_ALIAS_SYMLINK });
|
|
198
|
+
// A package soul is what the lock recorded at sync: the same bytes at the locked commit.
|
|
199
|
+
if (typeof e.package === "string" && digest !== e.digest) {
|
|
200
|
+
throw err("E_PACKAGE_INTEGRITY", `package soul ${e.qualifiedName} v${e.version} @ ${String(e.commit).slice(0, 12)}: content digest ${digest} does not match the locked ${e.digest} — run \`oats sync\` (a lock that was edited: remove the entry and sync again)`, { package: e.package, soul: e.name, version: e.version, commit: e.commit, why: "soul-digest", locked: e.digest, observed: digest });
|
|
201
|
+
}
|
|
168
202
|
if (!existsSync(join(dest, "soul.yaml")) || !existsSync(join(dest, "AGENTS.md"))) throw err("E_SOUL_INCOMPLETE", `soul ${e.name} at ${e.repoKey}@${String(e.commit).slice(0, 12)} lacks soul.yaml or AGENTS.md`, { repoKey: e.repoKey, commit: e.commit, path: e.path });
|
|
169
203
|
if (!existsSync(join(dest, "CLAUDE.md"))) symlinkSync("AGENTS.md", join(dest, "CLAUDE.md"));
|
|
170
204
|
}
|
|
@@ -176,7 +210,7 @@ async function fetchSoulSource(prepared, dest) {
|
|
|
176
210
|
* that temporary copy; `fetched` says whether the preview had to fetch. */
|
|
177
211
|
export async function previewWorkspaceSoul(prepared, agentsRoot) {
|
|
178
212
|
const e = prepared.soulEntry;
|
|
179
|
-
const commitDir = join(agentsRoot, e
|
|
213
|
+
const commitDir = join(agentsRoot, agentDirOf(e), SOULS_DIR, commit12(e.commit));
|
|
180
214
|
if (existsSync(join(commitDir, "soul.yaml")) && existsSync(join(commitDir, "AGENTS.md"))) return { soulDir: realpathSync(commitDir), fetched: false, cleanup: () => {} };
|
|
181
215
|
const tmp = realpathSync(mkdtempSync(join(tmpdir(), "oats-preview-soul-")));
|
|
182
216
|
const cleanup = () => rmSync(tmp, { recursive: true, force: true });
|
|
@@ -202,7 +236,7 @@ export async function previewWorkspaceSoul(prepared, agentsRoot) {
|
|
|
202
236
|
* Returns the PER-COMMIT directory (what a home should link). */
|
|
203
237
|
export async function ensureWorkspaceSoul(prepared, agentsRoot) {
|
|
204
238
|
const e = prepared.soulEntry;
|
|
205
|
-
const agentDir = join(agentsRoot, e
|
|
239
|
+
const agentDir = join(agentsRoot, agentDirOf(e)); const soulDir = join(agentDir, "soul");
|
|
206
240
|
const soulsDir = join(agentDir, SOULS_DIR);
|
|
207
241
|
const stamp = join(agentDir, SOUL_SOURCE_STAMP);
|
|
208
242
|
const readStamp = () => { try { return JSON.parse(readFileSync(stamp, "utf8")); } catch { return null; } };
|
|
@@ -248,7 +282,8 @@ export async function ensureWorkspaceSoul(prepared, agentsRoot) {
|
|
|
248
282
|
if (soulPointerTarget(soulDir) !== target) swapSoulPointer(soulDir, target);
|
|
249
283
|
const cur = readStamp();
|
|
250
284
|
if (!cur || cur.repoKey !== e.repoKey || cur.commit !== e.commit || cur.path !== e.path) {
|
|
251
|
-
|
|
285
|
+
const pkg = typeof e.package === "string" ? { package: e.package, version: e.version } : {};
|
|
286
|
+
writeFileSync(stamp, JSON.stringify({ repoKey: e.repoKey, commit: e.commit, path: e.path, ...pkg, fetchedAt: new Date().toISOString() }, null, 2) + "\n");
|
|
252
287
|
}
|
|
253
288
|
return target;
|
|
254
289
|
}
|
|
@@ -345,18 +380,25 @@ export function resolveMemberClone(prepared, { explicit } = {}) {
|
|
|
345
380
|
if (typeof explicit === "string" && explicit.trim()) return isAbsolute(explicit) ? resolvePath(explicit) : resolvePath(deployment, explicit);
|
|
346
381
|
const key = prepared?.soulEntry?.repoKey;
|
|
347
382
|
if (typeof key !== "string" || !key) return null;
|
|
348
|
-
|
|
383
|
+
return memberCloneOf(deployment, prepared?.local, key).path;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Steps 2–4 of resolveMemberClone for one member, naming the rule that found it (feature desktop-facts):
|
|
387
|
+
* → { path, rule: "clones" | "convention" } | { path: null, rule: null }; a named or conventional path that
|
|
388
|
+
* is not the member's clone is E_CLONE_MISMATCH, as for a spawn. */
|
|
389
|
+
export function memberCloneOf(deployment, local, key) {
|
|
390
|
+
const clones = local?.clones;
|
|
349
391
|
if (clones && typeof clones === "object") {
|
|
350
392
|
for (const [written, value] of Object.entries(clones)) {
|
|
351
393
|
if (!sameRepoKey(canonicalCloneKey(written), key)) continue;
|
|
352
394
|
if (typeof value !== "string" || !value.trim()) throw err("E_CLONE_MISMATCH", `oats-local.yaml clones: ${written} must be a path`, { path: value, expected: key, found: null, via: "oats-local.yaml clones:" });
|
|
353
395
|
const path = isAbsolute(value) ? resolvePath(value) : resolvePath(deployment, value);
|
|
354
|
-
return verifyMemberClone(path, key, { via: `oats-local.yaml clones: ${written}` });
|
|
396
|
+
return { path: verifyMemberClone(path, key, { via: `oats-local.yaml clones: ${written}` }), rule: "clones" };
|
|
355
397
|
}
|
|
356
398
|
}
|
|
357
399
|
const convention = conventionCloneDir(deployment, key);
|
|
358
|
-
if (!existsSync(convention)) return null;
|
|
359
|
-
return verifyMemberClone(convention, key, { via: "convention path" });
|
|
400
|
+
if (!existsSync(convention)) return { path: null, rule: null };
|
|
401
|
+
return { path: verifyMemberClone(convention, key, { via: "convention path" }), rule: "convention" };
|
|
360
402
|
}
|
|
361
403
|
|
|
362
404
|
/** The clone url of the soul's member as the workspace names it (for remedies). */
|
|
@@ -382,6 +424,17 @@ export function requireMemberClone(prepared, { explicit } = {}) {
|
|
|
382
424
|
throw err("E_CLONE_MISSING", `soul ${soul} works in a ${work} of ${key}, and this machine has no clone of it — either \`git clone ${url} ${convention}\` (the convention: <deployment>/<member name>) or point oats-local.yaml at an existing clone: \`clones: { ${key}: <abs path> }\`; for a one-off pass --repo <path>`, { soul, repoKey: key, work, deployment, convention, url, remedies: { clone: `git clone ${url} ${convention}`, local: { clones: { [key]: "<abs path>" } }, flag: "--repo <path>" } });
|
|
383
425
|
}
|
|
384
426
|
|
|
427
|
+
/** Would a spawn of this discovered soul be refused before it touches this machine (feature desktop-facts)?
|
|
428
|
+
* The same checks prepareInstance makes — souls.disabled, then the soul's resolution (team conflicts, slot
|
|
429
|
+
* conflicts, missing or private capabilities, compatibility …) — without spawning anything.
|
|
430
|
+
* → { spawnable: true, problem: null } | { spawnable: false, problem: { code, message } } */
|
|
431
|
+
export async function soulSpawnability(local, discovery, lock, soulEntry, { remoteOptions, remote } = {}) {
|
|
432
|
+
const disabled = disabledEntry(local, soulEntry);
|
|
433
|
+
if (disabled !== null) return { spawnable: false, problem: { code: "E_SOUL_DISABLED", message: `disabled on this machine (oats-local.yaml souls.disabled: ${disabled})` } };
|
|
434
|
+
try { await resolveSoul(discovery, soulEntry, { local, lock, spawn: {}, remoteOptions, remote }); return { spawnable: true, problem: null }; }
|
|
435
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return { spawnable: false, problem: { code: e.code, message: e.message } }; throw e; }
|
|
436
|
+
}
|
|
437
|
+
|
|
385
438
|
/** The async half of a spawn: everything that touches the network. Returns a
|
|
386
439
|
* PREPARED object that `spawnInstance` consumes synchronously. */
|
|
387
440
|
export async function prepareInstance(contextDir, soulName, { spawn = {}, remoteOptions, remote, local: localOverride, discovery: discoveryOverride } = {}) {
|
|
@@ -389,8 +442,10 @@ export async function prepareInstance(contextDir, soulName, { spawn = {}, remote
|
|
|
389
442
|
const local = found.local;
|
|
390
443
|
const deployment = found.path ? dirname(found.path) : resolvePath(contextDir);
|
|
391
444
|
const lock = existsSync(join(deployment, LOCK_FILE)) ? readLock(deployment) : null;
|
|
392
|
-
const discovery = discoveryOverride ?? await discoverOrStandalone(local, { remoteOptions, remote });
|
|
445
|
+
const discovery = discoveryOverride ?? await discoverOrStandalone(local, { lock, remoteOptions, remote });
|
|
393
446
|
const soulEntry = findSoulEntry(discovery, soulName);
|
|
447
|
+
const disabled = disabledEntry(local, soulEntry);
|
|
448
|
+
if (disabled !== null) throw err("E_SOUL_DISABLED", `soul ${qualifiedSoulName(soulEntry)} is disabled on this machine (oats-local.yaml souls.disabled: ${disabled}) — remove it from that list to spawn it here`, { name: soulEntry.name, qualifiedName: qualifiedSoulName(soulEntry), entry: disabled });
|
|
394
449
|
const resolution = await resolveSoul(discovery, soulEntry, { local, lock, spawn, remoteOptions, remote });
|
|
395
450
|
return { local, deployment, lock, discovery, soulEntry, resolution, remoteOptions, spawn };
|
|
396
451
|
}
|
|
@@ -402,6 +457,11 @@ export async function prepareInstance(contextDir, soulName, { spawn = {}, remote
|
|
|
402
457
|
const ACCESS_REASONS = new Set(["auth", "not-found"]);
|
|
403
458
|
const isAccessFailure = (e) => e?.code === "E_REMOTE_UNREADABLE" && ACCESS_REASONS.has(e?.details?.reason ?? e?.provenance?.reason);
|
|
404
459
|
|
|
460
|
+
/** The deployment's lock, or null when it has none (an unreadable lock is E_LOCK_SCHEMA). */
|
|
461
|
+
export function deploymentLock(deployment) {
|
|
462
|
+
return existsSync(join(deployment, LOCK_FILE)) ? readLock(deployment) : null;
|
|
463
|
+
}
|
|
464
|
+
|
|
405
465
|
/** Decision 10: a standalone view is a MEMBER whose workspace cannot be read — the
|
|
406
466
|
* repo must declare that workspace (oats-membership.yaml). A lone repo with no
|
|
407
467
|
* backlink is not a member of anything and never becomes a capability source. */
|
|
@@ -424,21 +484,23 @@ function requireMembership(repo, ref) {
|
|
|
424
484
|
* readable in this access context (auth / not-found).
|
|
425
485
|
* A transient failure reading the host (network, timeout) is rethrown as-is.
|
|
426
486
|
*/
|
|
427
|
-
export async function discoverOrStandalone(local, { remoteOptions, remote } = {}) {
|
|
487
|
+
export async function discoverOrStandalone(local, { lock, deployment, remoteOptions, remote } = {}) {
|
|
428
488
|
const ro = { remoteOptions, remote };
|
|
489
|
+
// Package souls are listed from the deployment's lock (the `lock` given, else `<deployment>/oats-lock.json`).
|
|
490
|
+
if (lock === undefined) lock = typeof deployment === "string" ? deploymentLock(deployment) : null;
|
|
429
491
|
if (typeof local.standalone === "string" && local.standalone) {
|
|
430
492
|
const repo = await discoverRepo(local.standalone, ro);
|
|
431
493
|
requireMembership(repo, local.standalone);
|
|
432
494
|
return { ...standaloneRepo(local.standalone, repo.commit, repo, { remote }), standaloneReason: "explicit" };
|
|
433
495
|
}
|
|
434
|
-
try { return await discoverWorkspace(local.workspace, { local, ...ro }); }
|
|
496
|
+
try { return await discoverWorkspace(local.workspace, { local, lock, ...ro }); }
|
|
435
497
|
catch (e) {
|
|
436
498
|
if (e?.code !== "E_WORKSPACE_SCHEMA" || e?.details?.notAHost !== true) throw e;
|
|
437
499
|
// The ref is a repository without oats-workspace.yaml: is it a member whose
|
|
438
500
|
// workspace we cannot read? Then the standalone view is what the operator gets.
|
|
439
501
|
const repo = await discoverRepo(local.workspace, ro);
|
|
440
502
|
if (!repo.membership) throw e;
|
|
441
|
-
try { return await discoverWorkspace(repo.membership.workspace, { local, ...ro }); }
|
|
503
|
+
try { return await discoverWorkspace(repo.membership.workspace, { local, lock, ...ro }); }
|
|
442
504
|
catch (inner) {
|
|
443
505
|
// Only an ACCESS failure on the host means "standalone"; anything else
|
|
444
506
|
// (network, timeout, a broken workspace file) is the caller's to see.
|
|
@@ -451,115 +513,9 @@ export async function discoverOrStandalone(local, { remoteOptions, remote } = {}
|
|
|
451
513
|
/** Materialize a prepared resolution into `home`. Called by spawnInstance after
|
|
452
514
|
* the home directory exists and before the harness is launched. */
|
|
453
515
|
export async function materializePrepared(prepared, home) {
|
|
454
|
-
|
|
455
|
-
// A capability agent's module copied from its anchor's verified copy (prepareCapabilityAgent):
|
|
456
|
-
// materialize still checks the copy's digest against the one the anchor recorded.
|
|
457
|
-
const fetch = localModules ? async (ref, commit, dir, dest, options) => {
|
|
458
|
-
const src = localModules[basename(dest)];
|
|
459
|
-
if (!src) return defaultRemote.fetchRemoteTree(ref, commit, dir, dest, options);
|
|
460
|
-
copyLocalModule(src, dest);
|
|
461
|
-
return { digest: defaultRemote.contentDigest(dest, { allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK }) };
|
|
462
|
-
} : undefined;
|
|
463
|
-
return materialize(prepared.resolution, home, { lock: prepared.lock, remoteOptions: prepared.remoteOptions, soulAgentsMd: prepared.soulAgentsMd, soulDir: prepared.soulDir, ...(fetch ? { fetch } : {}) });
|
|
464
|
-
}
|
|
465
|
-
/** Copy a materialized module tree (regular files, directories, the CLAUDE.md alias) — nothing else. */
|
|
466
|
-
function copyLocalModule(src, dest, rel = "") {
|
|
467
|
-
mkdirSync(dest, { recursive: true });
|
|
468
|
-
for (const name of readdirSync(src).sort()) {
|
|
469
|
-
const from = join(src, name), to = join(dest, name), at = rel ? `${rel}/${name}` : name, st = lstatSync(from);
|
|
470
|
-
if (st.isSymbolicLink()) {
|
|
471
|
-
if (!defaultRemote.OATS_ALIAS_SYMLINK(at) || readlinkSync(from) !== "AGENTS.md") throw err("E_REMOTE_TREE_UNSAFE", `${from} is a symlink`, { path: from, why: "symlink" });
|
|
472
|
-
symlinkSync("AGENTS.md", to);
|
|
473
|
-
} else if (st.isDirectory()) copyLocalModule(from, to, at);
|
|
474
|
-
else if (st.isFile()) { copyFileSync(from, to); chmodSync(to, (st.mode & 0o111) ? 0o755 : 0o644); }
|
|
475
|
-
else throw err("E_REMOTE_TREE_UNSAFE", `${from} is not a regular file`, { path: from, why: "device" });
|
|
476
|
-
}
|
|
516
|
+
return materialize(prepared.resolution, home, { lock: prepared.lock, remoteOptions: prepared.remoteOptions, soulAgentsMd: prepared.soulAgentsMd, soulDir: prepared.soulDir });
|
|
477
517
|
}
|
|
478
518
|
|
|
479
|
-
/**
|
|
480
|
-
* The prepared resolution of a CAPABILITY AGENT (a capability's `agents:` soul, e.g.
|
|
481
|
-
* OKF's memory-harvest worker) — lead decision c3 Q1. It composes its providing
|
|
482
|
-
* capability's module and NOTHING else: no workspace defaults, no knowledge,
|
|
483
|
-
* messaging or tasks module, so no provider hook of any slot runs for it, and the
|
|
484
|
-
* providing module's own hooks do not run either (a manifest has no way to declare
|
|
485
|
-
* them for an agent). A knowledge-layer provider's inject is left out: a service
|
|
486
|
-
* agent carries no memory protocol.
|
|
487
|
-
*
|
|
488
|
-
* The module comes from `agent._manifestSource` — the instance home whose module
|
|
489
|
-
* copy declared the agent (the --parent/--relative-to anchor first): its recorded
|
|
490
|
-
* `modules[<cap>]` (copied from that home's verified copy, pinned to the recorded
|
|
491
|
-
* digest) and its recorded `providers[<cap>]` payload. Without such a home (the
|
|
492
|
-
* source instance is gone), `pkg` — the deployment lock's package entry that
|
|
493
|
-
* declares the agent (resolvePackageCapabilityAgent) — supplies the module, and the
|
|
494
|
-
* payload is the manifest defaults ⊕ the workspace's messaging payload (for a
|
|
495
|
-
* messaging-layer provider) ⊕ oats-local.yaml `settings.<cap>`.
|
|
496
|
-
*/
|
|
497
|
-
export async function prepareCapabilityAgent(contextDir, agent, { discovery, pkg = null, remoteOptions, catalog = null, remote = defaultRemote } = {}) {
|
|
498
|
-
const found = loadLocal(contextDir);
|
|
499
|
-
const local = found.local;
|
|
500
|
-
const deployment = dirname(found.path);
|
|
501
|
-
const lock = existsSync(join(deployment, LOCK_FILE)) ? readLock(deployment) : null;
|
|
502
|
-
const view = discovery ?? await discoverOrStandalone(local, { remoteOptions });
|
|
503
|
-
const cap = agent.capability;
|
|
504
|
-
let module, payload, origins, localModules = null;
|
|
505
|
-
const anchor = pkg ? null : agent._manifestSource;
|
|
506
|
-
if (anchor) {
|
|
507
|
-
let meta;
|
|
508
|
-
try { meta = JSON.parse(readFileSync(join(anchor, "instance.json"), "utf8")); } catch { meta = null; }
|
|
509
|
-
const recorded = meta?.modules?.[cap];
|
|
510
|
-
const moduleDir = join(anchor, MODULES_DIR, cap);
|
|
511
|
-
if (!recorded?.from || typeof recorded.digest !== "string" || !existsSync(join(moduleDir, "oats.json"))) {
|
|
512
|
-
throw err("E_CAPABILITY_BROKEN", `capability agent ${agent.name}: ${anchor} does not record a verified copy of ${cap} (instance.json modules.${cap} with a digest, and ${join(MODULES_DIR, cap)}); respawn that instance`, { capability: cap, anchor });
|
|
513
|
-
}
|
|
514
|
-
const manifest = JSON.parse(readFileSync(join(moduleDir, "oats.json"), "utf8"));
|
|
515
|
-
module = { name: cap, from: { ...recorded.from }, manifest, layer: SLOTS.includes(manifest.layer) ? manifest.layer : null, private: manifest.private === true, digest: recorded.digest };
|
|
516
|
-
payload = meta.providers?.[cap] && typeof meta.providers[cap] === "object" ? JSON.parse(JSON.stringify(meta.providers[cap])) : {};
|
|
517
|
-
origins = Object.fromEntries(Object.keys(payload).map((k) => [`/${k.replace(/~/g, "~0").replace(/\//g, "~1")}`, { kind: "anchor", at: `${join(anchor, "instance.json")}#/providers/${cap}` }]));
|
|
518
|
-
localModules = { [cap]: moduleDir };
|
|
519
|
-
} else if (pkg) {
|
|
520
|
-
const manifest = pkg.manifest;
|
|
521
|
-
if (pkg.kind === "member") {
|
|
522
|
-
module = { name: cap, from: { kind: "member", repoKey: pkg.repoKey, commit: pkg.commit }, manifest, layer: SLOTS.includes(manifest.layer) ? manifest.layer : null, private: false, dir: pkg.capDir };
|
|
523
|
-
} else {
|
|
524
|
-
const entry = lock?.packages?.[pkg.package];
|
|
525
|
-
if (!entry) throw err("E_PACKAGE_MISSING", `capability agent ${agent.name}: package ${pkg.package} is not in ${LOCK_FILE}`, { capability: cap, package: pkg.package });
|
|
526
|
-
const ref = packageRef(pkg.package, entry, catalog, remote);
|
|
527
|
-
module = {
|
|
528
|
-
name: cap, from: { kind: "package", package: pkg.package, version: entry.version, commit: entry.commit, integrity: entry.integrity, repoKey: remote.parseRepoRef(ref).key },
|
|
529
|
-
manifest, layer: SLOTS.includes(manifest.layer) ? manifest.layer : null, private: manifest.private === true, dir: pkg.capDir,
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
const layers = [{ payload: manifestDefaultsPayload(manifest), origin: { kind: "manifest-default", at: "oats.json#/settings" } }];
|
|
533
|
-
if (module.layer === "messaging" && view?.workspace?.messaging && typeof view.workspace.messaging === "object") {
|
|
534
|
-
const { byTeam, ...base } = view.workspace.messaging;
|
|
535
|
-
layers.push({ payload: base, origin: { kind: "workspace", at: "oats-workspace.yaml#/messaging" } });
|
|
536
|
-
}
|
|
537
|
-
const host = local.settings?.[cap];
|
|
538
|
-
if (host && typeof host === "object") layers.push({ payload: host, origin: { kind: "host", at: `oats-local.yaml#/settings/${cap}` } });
|
|
539
|
-
payload = mergePayload(...layers.map((l) => l.payload));
|
|
540
|
-
origins = payloadOrigins(layers);
|
|
541
|
-
} else {
|
|
542
|
-
throw new TypeError("prepareCapabilityAgent: the agent names no source home, and no package entry was given");
|
|
543
|
-
}
|
|
544
|
-
// The capability's own kernel range, as resolveSoul checks every soul's modules.
|
|
545
|
-
const compat = kernelCompatibility({ ...module.manifest, capability: cap });
|
|
546
|
-
if (!compat.ok) throw err("E_CAPABILITY_INCOMPATIBLE", `capability agent ${agent.name}: ${cap} requires oats ${compat.range}; this kernel is ${compat.kernel}`, { capability: cap, range: compat.range, kernel: compat.kernel, from: module.from });
|
|
547
|
-
for (const { key, value, values } of settingValueProblems(module.manifest, payload)) {
|
|
548
|
-
throw err("E_WORKSPACE_SCHEMA", `${cap}: setting ${JSON.stringify(key)} is ${JSON.stringify(value)}, not one of ${values.map((v) => JSON.stringify(v)).join(", ")}`, { capability: cap, key, value, values, reason: "setting-value" });
|
|
549
|
-
}
|
|
550
|
-
const slots = { knowledge: null, messaging: null, tasks: null };
|
|
551
|
-
if (module.layer) slots[module.layer] = cap;
|
|
552
|
-
if (module.layer === "knowledge") module.inject = false;
|
|
553
|
-
const injects = module.inject !== false && typeof module.manifest.inject === "string" && module.manifest.inject ? [{ module: cap, path: module.manifest.inject }] : [];
|
|
554
|
-
const repoKey = module.from.repoKey ?? `package:${module.from.package}`;
|
|
555
|
-
const soulEntry = { name: agent.name, repoKey, commit: module.from.commit, team: null, path: null, capability: cap };
|
|
556
|
-
const soul = { name: agent.name, repoKey, commit: module.from.commit, team: null, path: null };
|
|
557
|
-
const decl = { resolutionApi: RESOLUTION_API, soul, modules: [module], slots, skills: [], injects };
|
|
558
|
-
const payloads = { [cap]: payload };
|
|
559
|
-
const declRevision = revisionOf(decl), payloadRevision = revisionOf(payloads);
|
|
560
|
-
const resolution = { ...decl, payloads, payloadOrigins: { [cap]: origins }, teams: [], declRevision, payloadRevision, revision: revisionOf({ declRevision, payloadRevision }) };
|
|
561
|
-
return { local, deployment, lock: anchor ? null : lock, discovery: view, soulEntry, resolution, remoteOptions, spawn: { providers: {} }, capabilityAgent: true, ...(localModules ? { localModules } : {}) };
|
|
562
|
-
}
|
|
563
519
|
|
|
564
520
|
/** Turn a Resolution's modules (already materialized under `home`) into the
|
|
565
521
|
* capability rows the kernel's hooks/environment/requirements code consumes.
|
|
@@ -578,7 +534,8 @@ export function toCapabilityRows(resolution, home) {
|
|
|
578
534
|
level: home, origin: m.from.kind === "package" ? `package:${m.from.package}@${m.from.version}` : `member:${m.from.repoKey}@${m.from.commit}`,
|
|
579
535
|
provenance: [m.from.kind === "package" ? `package ${m.from.package} v${m.from.version}` : `member ${m.from.repoKey} @ ${String(m.from.commit).slice(0, 12)}`],
|
|
580
536
|
settings: { ...(resolution.payloads?.[m.name] && typeof resolution.payloads[m.name] === "object" ? resolution.payloads[m.name] : {}) },
|
|
581
|
-
|
|
537
|
+
settingsOrigins: { ...(resolution.payloadOrigins?.[m.name] && typeof resolution.payloadOrigins[m.name] === "object" ? resolution.payloadOrigins[m.name] : {}) },
|
|
538
|
+
skills, inject: inject && existsSync(inject) ? inject : undefined,
|
|
582
539
|
skillsDeclared: manifest.skills || [], injectDeclared: manifest.inject,
|
|
583
540
|
// Member capabilities are trusted by membership (decision 2); package
|
|
584
541
|
// capabilities by their declaration in the workspace's packages: (human
|
|
@@ -637,67 +594,3 @@ export const INSTANCE_SKILLS_DIR = SKILLS_DIR;
|
|
|
637
594
|
export const INSTANCE_MODULES_DIR = MODULES_DIR;
|
|
638
595
|
|
|
639
596
|
|
|
640
|
-
/**
|
|
641
|
-
* Workspace model: a capability-defined agent (a package capability's `agents:`
|
|
642
|
-
* soul — OKF's memory-harvest worker, say) resolves from the deployment's LOCK,
|
|
643
|
-
* not from any instance: every locked package's capability manifests are read
|
|
644
|
-
* over the remote; the first `agents/<name>` match has its capability tree fetched
|
|
645
|
-
* into the deployment's module store `<deployment>/.oats/modules/<cap>@<commit>/`
|
|
646
|
-
* (a per-commit cache shared by every such spawn) so the classic capability-agent
|
|
647
|
-
* machinery can read it exactly as it reads a materialized instance module.
|
|
648
|
-
* → { capability, dir, commit, package, version, manifest, rel } | undefined.
|
|
649
|
-
*/
|
|
650
|
-
export async function resolvePackageCapabilityAgent(contextDir, name, { remoteOptions, catalog = null, discovery = undefined } = {}) {
|
|
651
|
-
if (typeof name !== "string" || !name) return undefined;
|
|
652
|
-
const found = loadLocal(contextDir);
|
|
653
|
-
const deployment = found.path ? dirname(found.path) : resolvePath(contextDir);
|
|
654
|
-
const remote = defaultRemote;
|
|
655
|
-
const view = discovery === undefined ? await discoverOrStandalone(found.local, { remoteOptions }) : discovery;
|
|
656
|
-
// A confirmed member's own (non-private) capability first: membership is its trust
|
|
657
|
-
// decision, exactly as for a soul's `from: <member>` module.
|
|
658
|
-
for (const row of view?.members || []) {
|
|
659
|
-
if (!row?.confirmed && view?.standalone !== true) continue;
|
|
660
|
-
for (const cap of row.capabilities || []) {
|
|
661
|
-
if (cap.private) continue;
|
|
662
|
-
const agents = Array.isArray(cap.manifest?.agents) ? cap.manifest.agents : [];
|
|
663
|
-
const rel = agents.find((a) => typeof a === "string" && basename(a) === name);
|
|
664
|
-
if (!rel) continue;
|
|
665
|
-
const commit = cap.commit ?? row.commit;
|
|
666
|
-
const store = join(deployment, MODULES_DIR);
|
|
667
|
-
const dir = join(store, `${cap.name}@${String(commit).slice(0, 12)}`);
|
|
668
|
-
if (!existsSync(join(dir, "oats.json"))) {
|
|
669
|
-
mkdirSync(store, { recursive: true });
|
|
670
|
-
await fetchRemoteTree(memberRef(view, remote, cap.repoKey), commit, cap.path, dir, { ...(remoteOptions || {}), allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK });
|
|
671
|
-
}
|
|
672
|
-
return { kind: "member", capability: cap.name, dir, capDir: cap.path, commit, repoKey: cap.repoKey, manifest: cap.manifest, rel };
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
if (!existsSync(join(deployment, LOCK_FILE))) return undefined;
|
|
676
|
-
const lock = readLock(deployment);
|
|
677
|
-
// Declaring a package in packages: is the trust decision: a workspace view
|
|
678
|
-
// admits only locked packages the workspace still declares (a stale lock entry
|
|
679
|
-
// is never a capability-agent source). A standalone view's lock holds only what
|
|
680
|
-
// a standalone sync wrote.
|
|
681
|
-
const declared = view?.standalone === true ? null : (view?.workspace?.packages && typeof view.workspace.packages === "object" ? view.workspace.packages : {});
|
|
682
|
-
for (const [id, entry] of Object.entries(lock.packages || {})) {
|
|
683
|
-
if (!entry || typeof entry !== "object") continue;
|
|
684
|
-
if (declared && !Object.hasOwn(declared, id)) continue;
|
|
685
|
-
let ref;
|
|
686
|
-
try { ref = packageRef(id, entry, catalog, remote); } catch { continue; }
|
|
687
|
-
let read;
|
|
688
|
-
try { read = await readPackageManifests(remote, ref, entry.commit, entry.path, { package: id }); } catch { continue; }
|
|
689
|
-
for (const cap of read.capabilities) {
|
|
690
|
-
const agents = Array.isArray(cap.manifest.agents) ? cap.manifest.agents : [];
|
|
691
|
-
const rel = agents.find((a) => typeof a === "string" && basename(a) === name);
|
|
692
|
-
if (!rel) continue;
|
|
693
|
-
const store = join(deployment, MODULES_DIR);
|
|
694
|
-
const dir = join(store, `${cap.name}@${String(entry.commit).slice(0, 12)}`);
|
|
695
|
-
if (!existsSync(join(dir, "oats.json"))) {
|
|
696
|
-
mkdirSync(store, { recursive: true });
|
|
697
|
-
await fetchRemoteTree(ref, entry.commit, cap.dir, dir, { ...(remoteOptions || {}), allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK });
|
|
698
|
-
}
|
|
699
|
-
return { kind: "package", capability: cap.name, dir, capDir: cap.dir, commit: entry.commit, package: id, version: entry.version, manifest: cap.manifest, rel };
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
return undefined;
|
|
703
|
-
}
|
package/lib/materialize.mjs
CHANGED
|
@@ -240,11 +240,8 @@ function moduleSkills(resolution, module, moduleDir) {
|
|
|
240
240
|
return rows;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
/** The inject files a module contributes → [relative path]. resolution.injects rows first, else manifest.inject
|
|
244
|
-
* none for a module row marked `inject: false` (a capability agent's knowledge-layer provider carries no
|
|
245
|
-
* memory protocol: lib/instance-resolution.mjs#prepareCapabilityAgent). */
|
|
243
|
+
/** The inject files a module contributes → [relative path]. resolution.injects rows first, else manifest.inject. */
|
|
246
244
|
function moduleInjects(resolution, module, moduleDir) {
|
|
247
|
-
if (module.inject === false) return [];
|
|
248
245
|
const declared = (resolution.injects || []).filter((i) => i && i.module === module.name);
|
|
249
246
|
const rels = declared.length
|
|
250
247
|
? declared.map((i) => modulePath(i.path, module.name, "inject"))
|
|
@@ -566,9 +563,10 @@ export function driftOf(instanceJson, discovery, { lock } = {}) {
|
|
|
566
563
|
rows.push({ module: name, from: clone(from), recorded, current: null, status: "missing", reason: member ? (member.reason || "unconfirmed") : "unconfirmed" });
|
|
567
564
|
continue;
|
|
568
565
|
}
|
|
569
|
-
const
|
|
570
|
-
if (!
|
|
571
|
-
|
|
566
|
+
const cap = Array.isArray(member.capabilities) ? member.capabilities.find((c) => c && c.name === name) : null;
|
|
567
|
+
if (!cap) { rows.push({ module: name, from: clone(from), recorded, current: { commit: member.commit }, status: "missing", reason: "capability-absent" }); continue; }
|
|
568
|
+
// `version` (feature desktop-facts): the member capability's manifest version now, beside the commit.
|
|
569
|
+
rows.push({ module: name, from: clone(from), recorded, current: { commit: member.commit, version: typeof cap.manifest?.version === "string" ? cap.manifest.version : null }, status: member.commit === commit ? "current" : "moved" });
|
|
572
570
|
}
|
|
573
571
|
return rows;
|
|
574
572
|
}
|
|
@@ -584,12 +582,25 @@ export function driftOf(instanceJson, discovery, { lock } = {}) {
|
|
|
584
582
|
* (reason "unconfirmed" / the row's reason). A standalone view's own member row is unconfirmed
|
|
585
583
|
* by construction (reason "cannot-read") yet carries the repo's current commit — it is compared,
|
|
586
584
|
* not reported missing. Returns null for a home without a recorded workspace soul (classic).
|
|
585
|
+
* A PACKAGE soul (`workspace.soul.package`) is compared with the discovery's package souls: the
|
|
586
|
+
* pin moved → "moved"; the package no longer locked/declared → "missing" ("package-absent"); the
|
|
587
|
+
* soul no longer shipped by the package → "missing" ("soul-absent").
|
|
587
588
|
*/
|
|
588
589
|
export function soulDriftOf(instanceJson, discovery) {
|
|
589
590
|
const soul = plainObject(instanceJson?.workspace) && plainObject(instanceJson.workspace.soul) ? instanceJson.workspace.soul : null;
|
|
590
591
|
if (!soul || typeof soul.repoKey !== "string") return null;
|
|
591
592
|
const name = typeof instanceJson.agent === "string" ? instanceJson.agent : (typeof instanceJson.soul === "string" ? instanceJson.soul : null);
|
|
592
593
|
const commit = typeof soul.commit === "string" ? soul.commit : null;
|
|
594
|
+
if (plainObject(soul.package) && typeof soul.package.id === "string") {
|
|
595
|
+
// A package soul: "moved" means the package pin moved (another version/commit is locked now).
|
|
596
|
+
const pkg = { package: soul.package.id, version: soul.package.version ?? null };
|
|
597
|
+
const soulName = typeof soul.name === "string" ? soul.name : name;
|
|
598
|
+
const base = { name: soulName, repoKey: soul.repoKey, commit, team: soul.team ?? null, ...pkg };
|
|
599
|
+
const listed = Array.isArray(discovery?.packageSouls) ? discovery.packageSouls.filter((s) => s && s.package === soul.package.id) : [];
|
|
600
|
+
const now = listed.find((s) => s.name === soulName) || null;
|
|
601
|
+
if (!now) return { ...base, current: null, status: "missing", reason: listed.length ? "soul-absent" : "package-absent" };
|
|
602
|
+
return { ...base, current: { commit: now.commit, version: now.version }, status: now.commit === commit ? "current" : "moved" };
|
|
603
|
+
}
|
|
593
604
|
const members = Array.isArray(discovery?.members) ? discovery.members : [];
|
|
594
605
|
const member = members.find((m) => m && m.key === soul.repoKey) || null;
|
|
595
606
|
const standaloneOwn = discovery?.standalone === true && member && member.key === discovery.key && typeof member.commit === "string";
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* `prepareInstance(dir, soul)` → the soul's Resolution — find the module whose
|
|
7
7
|
* manifest `command` is the namespace, fetch that capability into the
|
|
8
8
|
* deployment's per-commit module store `<deployment>/.oats/modules/<cap>@<commit12>/`
|
|
9
|
-
*
|
|
10
|
-
* lib/instance-resolution.mjs#resolvePackageCapabilityAgent) and dispatch to that
|
|
9
|
+
* and dispatch to that
|
|
11
10
|
* copy with the soul's merged payload as `OATS_SETTINGS`.
|
|
12
11
|
*
|
|
13
12
|
* Never "the newest instance's copy" (an instance is not an authority for the
|