@awebai/oats 0.28.0 → 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 +296 -106
- package/capabilities/oats-okf/bin/oats-okf.mjs +28 -8
- package/capabilities/oats-okf/injects/okf.md +33 -33
- package/capabilities/oats-okf/lib/binding-wire.mjs +4 -1
- package/capabilities/oats-okf/lib/config.mjs +2 -1
- package/capabilities/oats-okf/lib/consult.mjs +1 -5
- 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/okf-validate.mjs +123 -0
- package/capabilities/oats-okf/lib/sources.mjs +28 -3
- package/capabilities/oats-okf/lib/stores.mjs +9 -4
- package/capabilities/oats-okf/lib/worker.mjs +82 -8
- package/capabilities/oats-okf/oats.json +14 -8
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +8 -6
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +1 -1
- 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 -30
- 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/desktop-cli-api.md +257 -11
- 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 +31 -1
- package/docs/official-catalog.md +7 -4
- package/docs/packages.md +11 -5
- package/docs/release-lane.md +1 -1
- package/docs/release-notes/v0.29.0.md +240 -0
- package/docs/schedules.md +133 -5
- package/docs/souls-and-instances.md +4 -6
- package/docs/workspaces.md +11 -2
- package/lib/automations.mjs +369 -0
- package/lib/core.mjs +65 -154
- package/lib/instance-inspect.mjs +12 -4
- package/lib/instance-resolution.mjs +31 -182
- package/lib/materialize.mjs +5 -7
- package/lib/operator-dispatch.mjs +1 -2
- package/lib/packages.mjs +17 -0
- package/lib/remote.mjs +21 -1
- package/lib/resolve.mjs +51 -7
- package/lib/schedule.mjs +211 -41
- package/lib/triggers.mjs +182 -49
- package/lib/workspace.mjs +1 -1
- package/package-catalog.json +6 -4
- package/package.json +1 -1
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +0 -26
- 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
|
|
|
@@ -143,7 +143,7 @@ export function disabledEntry(local, soulEntry) {
|
|
|
143
143
|
export async function liveTeams(home, meta, { remoteOptions, remote } = {}) {
|
|
144
144
|
const recorded = (reason, error) => ({ teams: Array.isArray(meta?.teams) ? meta.teams : null, source: "recorded", reason, ...(error ? { error } : {}) });
|
|
145
145
|
const soul = meta?.workspace?.soul;
|
|
146
|
-
// A
|
|
146
|
+
// A home without a workspace soul record (a pre-0.29 capability agent's) has nothing to re-read.
|
|
147
147
|
if (!soul || typeof soul.repoKey !== "string" || typeof meta.agent !== "string") return recorded("no-workspace-soul");
|
|
148
148
|
// A package soul's labels are pinned with the package: the spawn-time record is its answer.
|
|
149
149
|
if (soul.package && typeof soul.package === "object") return recorded("package-soul");
|
|
@@ -380,18 +380,25 @@ export function resolveMemberClone(prepared, { explicit } = {}) {
|
|
|
380
380
|
if (typeof explicit === "string" && explicit.trim()) return isAbsolute(explicit) ? resolvePath(explicit) : resolvePath(deployment, explicit);
|
|
381
381
|
const key = prepared?.soulEntry?.repoKey;
|
|
382
382
|
if (typeof key !== "string" || !key) return null;
|
|
383
|
-
|
|
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;
|
|
384
391
|
if (clones && typeof clones === "object") {
|
|
385
392
|
for (const [written, value] of Object.entries(clones)) {
|
|
386
393
|
if (!sameRepoKey(canonicalCloneKey(written), key)) continue;
|
|
387
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:" });
|
|
388
395
|
const path = isAbsolute(value) ? resolvePath(value) : resolvePath(deployment, value);
|
|
389
|
-
return verifyMemberClone(path, key, { via: `oats-local.yaml clones: ${written}` });
|
|
396
|
+
return { path: verifyMemberClone(path, key, { via: `oats-local.yaml clones: ${written}` }), rule: "clones" };
|
|
390
397
|
}
|
|
391
398
|
}
|
|
392
399
|
const convention = conventionCloneDir(deployment, key);
|
|
393
|
-
if (!existsSync(convention)) return null;
|
|
394
|
-
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" };
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
/** The clone url of the soul's member as the workspace names it (for remedies). */
|
|
@@ -417,6 +424,17 @@ export function requireMemberClone(prepared, { explicit } = {}) {
|
|
|
417
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>" } });
|
|
418
425
|
}
|
|
419
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
|
+
|
|
420
438
|
/** The async half of a spawn: everything that touches the network. Returns a
|
|
421
439
|
* PREPARED object that `spawnInstance` consumes synchronously. */
|
|
422
440
|
export async function prepareInstance(contextDir, soulName, { spawn = {}, remoteOptions, remote, local: localOverride, discovery: discoveryOverride } = {}) {
|
|
@@ -495,115 +513,9 @@ export async function discoverOrStandalone(local, { lock, deployment, remoteOpti
|
|
|
495
513
|
/** Materialize a prepared resolution into `home`. Called by spawnInstance after
|
|
496
514
|
* the home directory exists and before the harness is launched. */
|
|
497
515
|
export async function materializePrepared(prepared, home) {
|
|
498
|
-
|
|
499
|
-
// A capability agent's module copied from its anchor's verified copy (prepareCapabilityAgent):
|
|
500
|
-
// materialize still checks the copy's digest against the one the anchor recorded.
|
|
501
|
-
const fetch = localModules ? async (ref, commit, dir, dest, options) => {
|
|
502
|
-
const src = localModules[basename(dest)];
|
|
503
|
-
if (!src) return defaultRemote.fetchRemoteTree(ref, commit, dir, dest, options);
|
|
504
|
-
copyLocalModule(src, dest);
|
|
505
|
-
return { digest: defaultRemote.contentDigest(dest, { allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK }) };
|
|
506
|
-
} : undefined;
|
|
507
|
-
return materialize(prepared.resolution, home, { lock: prepared.lock, remoteOptions: prepared.remoteOptions, soulAgentsMd: prepared.soulAgentsMd, soulDir: prepared.soulDir, ...(fetch ? { fetch } : {}) });
|
|
508
|
-
}
|
|
509
|
-
/** Copy a materialized module tree (regular files, directories, the CLAUDE.md alias) — nothing else. */
|
|
510
|
-
function copyLocalModule(src, dest, rel = "") {
|
|
511
|
-
mkdirSync(dest, { recursive: true });
|
|
512
|
-
for (const name of readdirSync(src).sort()) {
|
|
513
|
-
const from = join(src, name), to = join(dest, name), at = rel ? `${rel}/${name}` : name, st = lstatSync(from);
|
|
514
|
-
if (st.isSymbolicLink()) {
|
|
515
|
-
if (!defaultRemote.OATS_ALIAS_SYMLINK(at) || readlinkSync(from) !== "AGENTS.md") throw err("E_REMOTE_TREE_UNSAFE", `${from} is a symlink`, { path: from, why: "symlink" });
|
|
516
|
-
symlinkSync("AGENTS.md", to);
|
|
517
|
-
} else if (st.isDirectory()) copyLocalModule(from, to, at);
|
|
518
|
-
else if (st.isFile()) { copyFileSync(from, to); chmodSync(to, (st.mode & 0o111) ? 0o755 : 0o644); }
|
|
519
|
-
else throw err("E_REMOTE_TREE_UNSAFE", `${from} is not a regular file`, { path: from, why: "device" });
|
|
520
|
-
}
|
|
516
|
+
return materialize(prepared.resolution, home, { lock: prepared.lock, remoteOptions: prepared.remoteOptions, soulAgentsMd: prepared.soulAgentsMd, soulDir: prepared.soulDir });
|
|
521
517
|
}
|
|
522
518
|
|
|
523
|
-
/**
|
|
524
|
-
* The prepared resolution of a CAPABILITY AGENT (a capability's `agents:` soul, e.g.
|
|
525
|
-
* OKF's memory-harvest worker) — lead decision c3 Q1. It composes its providing
|
|
526
|
-
* capability's module and NOTHING else: no workspace defaults, no knowledge,
|
|
527
|
-
* messaging or tasks module, so no provider hook of any slot runs for it, and the
|
|
528
|
-
* providing module's own hooks do not run either (a manifest has no way to declare
|
|
529
|
-
* them for an agent). A knowledge-layer provider's inject is left out: a service
|
|
530
|
-
* agent carries no memory protocol.
|
|
531
|
-
*
|
|
532
|
-
* The module comes from `agent._manifestSource` — the instance home whose module
|
|
533
|
-
* copy declared the agent (the --parent/--relative-to anchor first): its recorded
|
|
534
|
-
* `modules[<cap>]` (copied from that home's verified copy, pinned to the recorded
|
|
535
|
-
* digest) and its recorded `providers[<cap>]` payload. Without such a home (the
|
|
536
|
-
* source instance is gone), `pkg` — the deployment lock's package entry that
|
|
537
|
-
* declares the agent (resolvePackageCapabilityAgent) — supplies the module, and the
|
|
538
|
-
* payload is the manifest defaults ⊕ the workspace's messaging payload (for a
|
|
539
|
-
* messaging-layer provider) ⊕ oats-local.yaml `settings.<cap>`.
|
|
540
|
-
*/
|
|
541
|
-
export async function prepareCapabilityAgent(contextDir, agent, { discovery, pkg = null, remoteOptions, catalog = null, remote = defaultRemote } = {}) {
|
|
542
|
-
const found = loadLocal(contextDir);
|
|
543
|
-
const local = found.local;
|
|
544
|
-
const deployment = dirname(found.path);
|
|
545
|
-
const lock = existsSync(join(deployment, LOCK_FILE)) ? readLock(deployment) : null;
|
|
546
|
-
const view = discovery ?? await discoverOrStandalone(local, { lock, remoteOptions });
|
|
547
|
-
const cap = agent.capability;
|
|
548
|
-
let module, payload, origins, localModules = null;
|
|
549
|
-
const anchor = pkg ? null : agent._manifestSource;
|
|
550
|
-
if (anchor) {
|
|
551
|
-
let meta;
|
|
552
|
-
try { meta = JSON.parse(readFileSync(join(anchor, "instance.json"), "utf8")); } catch { meta = null; }
|
|
553
|
-
const recorded = meta?.modules?.[cap];
|
|
554
|
-
const moduleDir = join(anchor, MODULES_DIR, cap);
|
|
555
|
-
if (!recorded?.from || typeof recorded.digest !== "string" || !existsSync(join(moduleDir, "oats.json"))) {
|
|
556
|
-
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 });
|
|
557
|
-
}
|
|
558
|
-
const manifest = JSON.parse(readFileSync(join(moduleDir, "oats.json"), "utf8"));
|
|
559
|
-
module = { name: cap, from: { ...recorded.from }, manifest, layer: SLOTS.includes(manifest.layer) ? manifest.layer : null, private: manifest.private === true, digest: recorded.digest };
|
|
560
|
-
payload = meta.providers?.[cap] && typeof meta.providers[cap] === "object" ? JSON.parse(JSON.stringify(meta.providers[cap])) : {};
|
|
561
|
-
origins = Object.fromEntries(Object.keys(payload).map((k) => [`/${k.replace(/~/g, "~0").replace(/\//g, "~1")}`, { kind: "anchor", at: `${join(anchor, "instance.json")}#/providers/${cap}` }]));
|
|
562
|
-
localModules = { [cap]: moduleDir };
|
|
563
|
-
} else if (pkg) {
|
|
564
|
-
const manifest = pkg.manifest;
|
|
565
|
-
if (pkg.kind === "member") {
|
|
566
|
-
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 };
|
|
567
|
-
} else {
|
|
568
|
-
const entry = lock?.packages?.[pkg.package];
|
|
569
|
-
if (!entry) throw err("E_PACKAGE_MISSING", `capability agent ${agent.name}: package ${pkg.package} is not in ${LOCK_FILE}`, { capability: cap, package: pkg.package });
|
|
570
|
-
const ref = packageRef(pkg.package, entry, catalog, remote);
|
|
571
|
-
module = {
|
|
572
|
-
name: cap, from: { kind: "package", package: pkg.package, version: entry.version, commit: entry.commit, integrity: entry.integrity, repoKey: remote.parseRepoRef(ref).key },
|
|
573
|
-
manifest, layer: SLOTS.includes(manifest.layer) ? manifest.layer : null, private: manifest.private === true, dir: pkg.capDir,
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
const layers = [{ payload: manifestDefaultsPayload(manifest), origin: { kind: "manifest-default", at: "oats.json#/settings" } }];
|
|
577
|
-
if (module.layer === "messaging" && view?.workspace?.messaging && typeof view.workspace.messaging === "object") {
|
|
578
|
-
const { byTeam, ...base } = view.workspace.messaging;
|
|
579
|
-
layers.push({ payload: base, origin: { kind: "workspace", at: "oats-workspace.yaml#/messaging" } });
|
|
580
|
-
}
|
|
581
|
-
const host = local.settings?.[cap];
|
|
582
|
-
if (host && typeof host === "object") layers.push({ payload: host, origin: { kind: "host", at: `oats-local.yaml#/settings/${cap}` } });
|
|
583
|
-
payload = mergePayload(...layers.map((l) => l.payload));
|
|
584
|
-
origins = payloadOrigins(layers);
|
|
585
|
-
} else {
|
|
586
|
-
throw new TypeError("prepareCapabilityAgent: the agent names no source home, and no package entry was given");
|
|
587
|
-
}
|
|
588
|
-
// The capability's own kernel range, as resolveSoul checks every soul's modules.
|
|
589
|
-
const compat = kernelCompatibility({ ...module.manifest, capability: cap });
|
|
590
|
-
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 });
|
|
591
|
-
for (const { key, value, values } of settingValueProblems(module.manifest, payload)) {
|
|
592
|
-
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" });
|
|
593
|
-
}
|
|
594
|
-
const slots = { knowledge: null, messaging: null, tasks: null };
|
|
595
|
-
if (module.layer) slots[module.layer] = cap;
|
|
596
|
-
if (module.layer === "knowledge") module.inject = false;
|
|
597
|
-
const injects = module.inject !== false && typeof module.manifest.inject === "string" && module.manifest.inject ? [{ module: cap, path: module.manifest.inject }] : [];
|
|
598
|
-
const repoKey = module.from.repoKey ?? `package:${module.from.package}`;
|
|
599
|
-
const soulEntry = { name: agent.name, repoKey, commit: module.from.commit, team: null, path: null, capability: cap };
|
|
600
|
-
const soul = { name: agent.name, repoKey, commit: module.from.commit, team: null, path: null };
|
|
601
|
-
const decl = { resolutionApi: RESOLUTION_API, soul, modules: [module], slots, skills: [], injects };
|
|
602
|
-
const payloads = { [cap]: payload };
|
|
603
|
-
const declRevision = revisionOf(decl), payloadRevision = revisionOf(payloads);
|
|
604
|
-
const resolution = { ...decl, payloads, payloadOrigins: { [cap]: origins }, teams: [], declRevision, payloadRevision, revision: revisionOf({ declRevision, payloadRevision }) };
|
|
605
|
-
return { local, deployment, lock: anchor ? null : lock, discovery: view, soulEntry, resolution, remoteOptions, spawn: { providers: {} }, capabilityAgent: true, ...(localModules ? { localModules } : {}) };
|
|
606
|
-
}
|
|
607
519
|
|
|
608
520
|
/** Turn a Resolution's modules (already materialized under `home`) into the
|
|
609
521
|
* capability rows the kernel's hooks/environment/requirements code consumes.
|
|
@@ -622,7 +534,8 @@ export function toCapabilityRows(resolution, home) {
|
|
|
622
534
|
level: home, origin: m.from.kind === "package" ? `package:${m.from.package}@${m.from.version}` : `member:${m.from.repoKey}@${m.from.commit}`,
|
|
623
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)}`],
|
|
624
536
|
settings: { ...(resolution.payloads?.[m.name] && typeof resolution.payloads[m.name] === "object" ? resolution.payloads[m.name] : {}) },
|
|
625
|
-
|
|
537
|
+
settingsOrigins: { ...(resolution.payloadOrigins?.[m.name] && typeof resolution.payloadOrigins[m.name] === "object" ? resolution.payloadOrigins[m.name] : {}) },
|
|
538
|
+
skills, inject: inject && existsSync(inject) ? inject : undefined,
|
|
626
539
|
skillsDeclared: manifest.skills || [], injectDeclared: manifest.inject,
|
|
627
540
|
// Member capabilities are trusted by membership (decision 2); package
|
|
628
541
|
// capabilities by their declaration in the workspace's packages: (human
|
|
@@ -681,67 +594,3 @@ export const INSTANCE_SKILLS_DIR = SKILLS_DIR;
|
|
|
681
594
|
export const INSTANCE_MODULES_DIR = MODULES_DIR;
|
|
682
595
|
|
|
683
596
|
|
|
684
|
-
/**
|
|
685
|
-
* Workspace model: a capability-defined agent (a package capability's `agents:`
|
|
686
|
-
* soul — OKF's memory-harvest worker, say) resolves from the deployment's LOCK,
|
|
687
|
-
* not from any instance: every locked package's capability manifests are read
|
|
688
|
-
* over the remote; the first `agents/<name>` match has its capability tree fetched
|
|
689
|
-
* into the deployment's module store `<deployment>/.oats/modules/<cap>@<commit>/`
|
|
690
|
-
* (a per-commit cache shared by every such spawn) so the classic capability-agent
|
|
691
|
-
* machinery can read it exactly as it reads a materialized instance module.
|
|
692
|
-
* → { capability, dir, commit, package, version, manifest, rel } | undefined.
|
|
693
|
-
*/
|
|
694
|
-
export async function resolvePackageCapabilityAgent(contextDir, name, { remoteOptions, catalog = null, discovery = undefined } = {}) {
|
|
695
|
-
if (typeof name !== "string" || !name) return undefined;
|
|
696
|
-
const found = loadLocal(contextDir);
|
|
697
|
-
const deployment = found.path ? dirname(found.path) : resolvePath(contextDir);
|
|
698
|
-
const remote = defaultRemote;
|
|
699
|
-
const view = discovery === undefined ? await discoverOrStandalone(found.local, { deployment, remoteOptions }) : discovery;
|
|
700
|
-
// A confirmed member's own (non-private) capability first: membership is its trust
|
|
701
|
-
// decision, exactly as for a soul's `from: <member>` module.
|
|
702
|
-
for (const row of view?.members || []) {
|
|
703
|
-
if (!row?.confirmed && view?.standalone !== true) continue;
|
|
704
|
-
for (const cap of row.capabilities || []) {
|
|
705
|
-
if (cap.private) continue;
|
|
706
|
-
const agents = Array.isArray(cap.manifest?.agents) ? cap.manifest.agents : [];
|
|
707
|
-
const rel = agents.find((a) => typeof a === "string" && basename(a) === name);
|
|
708
|
-
if (!rel) continue;
|
|
709
|
-
const commit = cap.commit ?? row.commit;
|
|
710
|
-
const store = join(deployment, MODULES_DIR);
|
|
711
|
-
const dir = join(store, `${cap.name}@${String(commit).slice(0, 12)}`);
|
|
712
|
-
if (!existsSync(join(dir, "oats.json"))) {
|
|
713
|
-
mkdirSync(store, { recursive: true });
|
|
714
|
-
await fetchRemoteTree(memberRef(view, remote, cap.repoKey), commit, cap.path, dir, { ...(remoteOptions || {}), allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK });
|
|
715
|
-
}
|
|
716
|
-
return { kind: "member", capability: cap.name, dir, capDir: cap.path, commit, repoKey: cap.repoKey, manifest: cap.manifest, rel };
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
if (!existsSync(join(deployment, LOCK_FILE))) return undefined;
|
|
720
|
-
const lock = readLock(deployment);
|
|
721
|
-
// Declaring a package in packages: is the trust decision: a workspace view
|
|
722
|
-
// admits only locked packages the workspace still declares (a stale lock entry
|
|
723
|
-
// is never a capability-agent source). A standalone view's lock holds only what
|
|
724
|
-
// a standalone sync wrote.
|
|
725
|
-
const declared = view?.standalone === true ? null : (view?.workspace?.packages && typeof view.workspace.packages === "object" ? view.workspace.packages : {});
|
|
726
|
-
for (const [id, entry] of Object.entries(lock.packages || {})) {
|
|
727
|
-
if (!entry || typeof entry !== "object") continue;
|
|
728
|
-
if (declared && !Object.hasOwn(declared, id)) continue;
|
|
729
|
-
let ref;
|
|
730
|
-
try { ref = packageRef(id, entry, catalog, remote); } catch { continue; }
|
|
731
|
-
let read;
|
|
732
|
-
try { read = await readPackageManifests(remote, ref, entry.commit, entry.path, { package: id }); } catch { continue; }
|
|
733
|
-
for (const cap of read.capabilities) {
|
|
734
|
-
const agents = Array.isArray(cap.manifest.agents) ? cap.manifest.agents : [];
|
|
735
|
-
const rel = agents.find((a) => typeof a === "string" && basename(a) === name);
|
|
736
|
-
if (!rel) continue;
|
|
737
|
-
const store = join(deployment, MODULES_DIR);
|
|
738
|
-
const dir = join(store, `${cap.name}@${String(entry.commit).slice(0, 12)}`);
|
|
739
|
-
if (!existsSync(join(dir, "oats.json"))) {
|
|
740
|
-
mkdirSync(store, { recursive: true });
|
|
741
|
-
await fetchRemoteTree(ref, entry.commit, cap.dir, dir, { ...(remoteOptions || {}), allowSymlinks: defaultRemote.OATS_ALIAS_SYMLINK });
|
|
742
|
-
}
|
|
743
|
-
return { kind: "package", capability: cap.name, dir, capDir: cap.dir, commit: entry.commit, package: id, version: entry.version, manifest: cap.manifest, rel };
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return undefined;
|
|
747
|
-
}
|
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
|
}
|
|
@@ -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
|
package/lib/packages.mjs
CHANGED
|
@@ -416,6 +416,23 @@ function assertDigest(what, value, details) {
|
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
/** Bind `remoteOptions` (cacheDir, exec, …) into every call of a contract-§1 remote. */
|
|
419
|
+
/** A remote whose reads at a commit are shared for one command (feature desktop-facts): the souls and
|
|
420
|
+
* capabilities listings resolve many souls over the same package manifests and skill listings, and a read at
|
|
421
|
+
* an immutable commit gives the same answer every time. A failed read is not kept (it is retried). */
|
|
422
|
+
export function memoizedRemote(remote) {
|
|
423
|
+
const memo = { ...remote };
|
|
424
|
+
for (const name of ["readRemoteFile", "listRemoteTree"]) {
|
|
425
|
+
if (typeof remote[name] !== "function") continue;
|
|
426
|
+
const cache = new Map();
|
|
427
|
+
memo[name] = (...args) => {
|
|
428
|
+
const key = JSON.stringify(args);
|
|
429
|
+
if (!cache.has(key)) cache.set(key, Promise.resolve(remote[name](...args)).catch((e) => { cache.delete(key); throw e; }));
|
|
430
|
+
return cache.get(key);
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return memo;
|
|
434
|
+
}
|
|
435
|
+
|
|
419
436
|
export function bindRemote(remote, remoteOptions) {
|
|
420
437
|
if (!remoteOptions || Object.keys(remoteOptions).length === 0) return remote;
|
|
421
438
|
const bound = { ...remote };
|
package/lib/remote.mjs
CHANGED
|
@@ -475,7 +475,7 @@ function assertNoCollisions(entries, ref, commit, prefix) {
|
|
|
475
475
|
async function lsTree(repo, spec, { flags = [], path, ref, commit } = {}) {
|
|
476
476
|
try {
|
|
477
477
|
const args = ["ls-tree", "-l", "-z", ...flags, spec];
|
|
478
|
-
if (path !== undefined) args.push("--", path);
|
|
478
|
+
if (path !== undefined) args.push("--", ...[].concat(path));
|
|
479
479
|
const out = await repo.local(args, { maxBuffer: 64 * 1024 * 1024 });
|
|
480
480
|
return parseLsTree(out.stdout);
|
|
481
481
|
} catch (error) {
|
|
@@ -514,6 +514,26 @@ export async function readRemoteFile(refText, commitArg, path, options = {}) {
|
|
|
514
514
|
return { bytes: out.stdout, size: out.stdout.length };
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
/** The Git tree object ids of `dirs` at `commit`, in one listing (feature desktop-facts: a member capability's
|
|
518
|
+
* fingerprint — content-addressed, so the same bytes give the same id; NOT the sha256 content digest a
|
|
519
|
+
* materialized module records). → Map dir → oid, null for a dir that is not a directory there. */
|
|
520
|
+
export async function remoteTreeOids(refText, commitArg, dirs, options = {}) {
|
|
521
|
+
const ref = parseRepoRef(refText, options);
|
|
522
|
+
requireCommit(commitArg);
|
|
523
|
+
const rels = dirs.map((dir) => normalizeTreePath(dir, { allowRoot: false }));
|
|
524
|
+
const { repo, commit } = await ensureCommit(ref, commitArg, options);
|
|
525
|
+
const entries = rels.length ? (await lsTree(repo, commit, { path: rels, ref, commit })) ?? [] : [];
|
|
526
|
+
return new Map(dirs.map((dir, i) => [dir, entries.find((e) => e.path === rels[i] && e.type === "tree")?.oid ?? null]));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** A browsable URL of a repo at a commit (feature desktop-facts): the file's page with `path`, else the tree.
|
|
530
|
+
* Only GitHub keys have one; any other host or a local repo → null. */
|
|
531
|
+
export function browseUrl(key, commit, path = null) {
|
|
532
|
+
if (typeof key !== "string" || !/^github\.com\/[^/]+\/[^/]+$/.test(key) || typeof commit !== "string" || !commit) return null;
|
|
533
|
+
const clean = typeof path === "string" && path ? path.split("/").filter(Boolean).map(encodeURIComponent).join("/") : null;
|
|
534
|
+
return clean ? `https://${key}/blob/${commit}/${clean}` : `https://${key}/tree/${commit}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
517
537
|
/**
|
|
518
538
|
* → [{ path, type: "blob"|"tree", size? }] relative to <dir>, depth-bounded (depth 1 = direct children).
|
|
519
539
|
* Missing dir → []. Symlinks are reported as type "symlink" so callers can skip them.
|
package/lib/resolve.mjs
CHANGED
|
@@ -368,12 +368,16 @@ export function teamsOf(workspace, labels) {
|
|
|
368
368
|
* Two labels that give one capability different entries (`off` vs a location, or two locations) →
|
|
369
369
|
* E_TEAM_CONFLICT { capability, labels: [a, b] }; identical entries are not a conflict, and a capability
|
|
370
370
|
* the soul names itself is not one either (the soul's entry wins over both).
|
|
371
|
+
* `offs` (optional, feature desktop-facts): receives what the soul turned off — { name, reason: "off",
|
|
372
|
+
* overrides } for each capability its own `off` removed from a lower layer, and { name, reason: "slot-none",
|
|
373
|
+
* slot, overrides: "workspace" } for a slot default its `<slot>: none` dropped (`overrides`: fromOfVia vocabulary).
|
|
371
374
|
*/
|
|
372
|
-
export function composeCapabilities(workspace, soulDefinition, { team = null, labels = team === null ? [] : [team] } = {}) {
|
|
375
|
+
export function composeCapabilities(workspace, soulDefinition, { team = null, labels = team === null ? [] : [team], offs = null } = {}) {
|
|
373
376
|
const map = new Map();
|
|
374
377
|
const apply = (entries, via, path) => {
|
|
375
378
|
for (const [name, value] of Object.entries(entries || {})) {
|
|
376
379
|
const choice = choiceOf(value, `${path}/${name}`, via);
|
|
380
|
+
if (choice === "off" && via === "soul" && offs && map.has(name)) offs.push({ name, reason: "off", overrides: fromOfVia(map.get(name).via) });
|
|
377
381
|
if (choice === "off") map.delete(name);
|
|
378
382
|
else map.set(name, { name, from: choice.from, via });
|
|
379
383
|
}
|
|
@@ -381,6 +385,7 @@ export function composeCapabilities(workspace, soulDefinition, { team = null, la
|
|
|
381
385
|
const defaults = isObject(workspace?.defaults) ? workspace.defaults : {};
|
|
382
386
|
for (const slot of SLOTS) {
|
|
383
387
|
const d = defaults[slot];
|
|
388
|
+
if (soulDefinition?.[slot] === "none" && offs && isObject(d)) for (const name of Object.keys(d)) offs.push({ name, reason: "slot-none", slot, overrides: "workspace" });
|
|
384
389
|
if (soulDefinition?.[slot] === "none" || d === "none" || !isObject(d)) continue;
|
|
385
390
|
const names = Object.keys(d);
|
|
386
391
|
if (names.length > 1) throw fail("E_WORKSPACE_SCHEMA", `defaults.${slot} names ${names.length} capabilities; a slot default names at most one`, { path: `/defaults/${slot}`, names });
|
|
@@ -547,6 +552,28 @@ async function enumerateSkills({ remote, ref, commit, dir, manifest, listing, mo
|
|
|
547
552
|
return skills;
|
|
548
553
|
}
|
|
549
554
|
|
|
555
|
+
/** What a capability provides, by name (feature desktop-facts): its skills (enumerated exactly as a spawn
|
|
556
|
+
* would), commands and hooks. `skills` is null when its declared skills cannot be listed (a spawn of it
|
|
557
|
+
* would refuse; the listing does not). */
|
|
558
|
+
export async function capabilityProvides({ ref, commit, dir, manifest, remote: injected, remoteOptions }) {
|
|
559
|
+
const remote = remoteOf({ remote: injected, remoteOptions });
|
|
560
|
+
const keys = (o) => (isObject(o) ? Object.keys(o).sort(byCodepoint) : []);
|
|
561
|
+
let skills;
|
|
562
|
+
try {
|
|
563
|
+
const missing = (raw, why, text) => fail("E_CAPABILITY_MISSING", text, { skill: raw, why });
|
|
564
|
+
skills = (await enumerateSkills({ remote, ref, commit, dir, manifest, moduleName: manifest.capability, missing })).map((x) => x.name).sort(byCodepoint);
|
|
565
|
+
} catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) skills = null; else throw e; }
|
|
566
|
+
return { skills, commands: keys(manifest.commands), hooks: keys(manifest.hooks) };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** The capability manifests of a locked package, read at its locked commit (feature desktop-facts). */
|
|
570
|
+
export async function lockedPackageCapabilities(id, entry, { catalog = null, remote: injected, remoteOptions } = {}) {
|
|
571
|
+
const remote = remoteOf({ remote: injected, remoteOptions });
|
|
572
|
+
const ref = packageRef(id, entry, catalog, remote);
|
|
573
|
+
const { capabilities } = await readPackageManifests(remote, ref, entry.commit, entry.path, { id, version: entry.version, commit: entry.commit });
|
|
574
|
+
return { ref, capabilities };
|
|
575
|
+
}
|
|
576
|
+
|
|
550
577
|
/* ───────────────────────────── resolveSoul ────────────────────────────── */
|
|
551
578
|
|
|
552
579
|
/**
|
|
@@ -577,9 +604,13 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
577
604
|
const soul = { name: soulEntry.name, repoKey: soulEntry.repoKey, commit: soulEntry.commit ?? null, team, path: soulEntry.path ?? null };
|
|
578
605
|
|
|
579
606
|
// Standalone: the soul's own repo only, workspace defaults unknown (decision 10).
|
|
607
|
+
// What the soul turned off (feature desktop-facts): its own `off` over a lower layer, and a `<slot>: none`
|
|
608
|
+
// that dropped a workspace default below. Provenance, like slotsFrom.
|
|
609
|
+
const offs = [];
|
|
580
610
|
const declared = discovery?.standalone === true
|
|
581
|
-
? composeCapabilities(null, { ...definition, capabilities: soulEntry.capabilities ?? definition.capabilities }, { labels })
|
|
582
|
-
: composeCapabilities(workspace, definition, { labels });
|
|
611
|
+
? composeCapabilities(null, { ...definition, capabilities: soulEntry.capabilities ?? definition.capabilities }, { labels, offs })
|
|
612
|
+
: composeCapabilities(workspace, definition, { labels, offs });
|
|
613
|
+
const turnedOff = offs;
|
|
583
614
|
|
|
584
615
|
const modules = [];
|
|
585
616
|
const skills = [];
|
|
@@ -608,7 +639,7 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
608
639
|
const cap = capabilities.find((c) => c.name === name);
|
|
609
640
|
if (!cap) throw fail("E_PACKAGE_INTEGRITY", `${name}: the lock says package ${id} v${entry.version} provides it, but ${entry.path}/oats-package.json at ${short(entry.commit)} does not`, { ...details, listed: capabilities.map((c) => c.name) });
|
|
610
641
|
const layer = layerOf(cap.manifest);
|
|
611
|
-
if (layer && emptied.has(layer) && via !== "soul") continue;
|
|
642
|
+
if (layer && emptied.has(layer) && via !== "soul") { turnedOff.push({ name, reason: "slot-none", slot: layer, overrides: fromOfVia(via) }); continue; }
|
|
612
643
|
module = {
|
|
613
644
|
name, from: { kind: "package", package: id, version: entry.version, commit: entry.commit, integrity: entry.integrity, repoKey: remote.parseRepoRef(ref).key },
|
|
614
645
|
manifest: clone(cap.manifest), layer, private: cap.manifest.private === true, dir: cap.dir,
|
|
@@ -618,7 +649,7 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
618
649
|
} else {
|
|
619
650
|
const { row, cap } = lookupMember(discovery, soul, name, from, via, lock);
|
|
620
651
|
const layer = layerOf(cap.manifest);
|
|
621
|
-
if (layer && emptied.has(layer) && via !== "soul") continue;
|
|
652
|
+
if (layer && emptied.has(layer) && via !== "soul") { turnedOff.push({ name, reason: "slot-none", slot: layer, overrides: fromOfVia(via) }); continue; }
|
|
622
653
|
const ref = memberRef(discovery, remote, cap.repoKey);
|
|
623
654
|
module = {
|
|
624
655
|
name, from: { kind: "member", repoKey: cap.repoKey, commit: cap.commit ?? row.commit },
|
|
@@ -737,6 +768,16 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
737
768
|
throw fail("E_CAPABILITY_INCOMPATIBLE", `${m.name} requires oats ${c.range}; this kernel is ${kernel} — ${remedy}`, { capability: m.name, range: c.range, kernel, from: m.from });
|
|
738
769
|
}
|
|
739
770
|
|
|
771
|
+
// Capability-defined agents (`agents:` in a manifest) were removed in 0.29.0: an agent ships as a
|
|
772
|
+
// soul. A module still declaring them is refused before anything is composed from it.
|
|
773
|
+
for (const m of modules) {
|
|
774
|
+
if (m.manifest?.agents === undefined) continue;
|
|
775
|
+
const agents = Array.isArray(m.manifest.agents) ? m.manifest.agents.filter((a) => typeof a === "string") : [];
|
|
776
|
+
const how = "ship each agent as a soul — a package soul (`souls/<name>/` beside the package's capabilities, spawned as `oats spawn <package>/<name>`) or a member soul (`souls/<name>/` in a member) — and drop `agents:` from the manifest";
|
|
777
|
+
const remedy = m.from.kind === "package" ? `pin a release of ${m.from.package} without \`agents:\`, or ${how}` : `in ${m.from.repoKey}: ${how}`;
|
|
778
|
+
throw fail("E_CAPABILITY_AGENTS_REMOVED", `${m.name} declares \`agents:\` (${agents.join(", ") || "…"}); capability-defined agents were removed in OATS 0.29.0 — ${remedy}`, { capability: m.name, agents, from: m.from });
|
|
779
|
+
}
|
|
780
|
+
|
|
740
781
|
// Compatibility floors (soul.compatibility) are constraints on PACKAGE versions.
|
|
741
782
|
for (const [cap, range] of Object.entries(isObject(definition.compatibility) ? definition.compatibility : {})) {
|
|
742
783
|
const m = modules.find((x) => x.name === cap);
|
|
@@ -768,9 +809,12 @@ export async function resolveSoul(discovery, soulEntry, { local = null, lock = n
|
|
|
768
809
|
// decision 3) is live messaging state, not composition: it stays out of both fingerprints too, so a
|
|
769
810
|
// single-label soul's revision is what it was.
|
|
770
811
|
// `slotsFrom` (where each filled slot's capability came from) is provenance, like payloadOrigins: outside
|
|
771
|
-
// both fingerprints, so the same capability reached another way is not a composition change.
|
|
812
|
+
// both fingerprints, so the same capability reached another way is not a composition change. So are
|
|
813
|
+
// `capabilitiesFrom` (the same per composed capability) and `turnedOff` (feature desktop-facts).
|
|
772
814
|
const teams = teamsOf(discovery?.standalone === true ? null : workspace, labels);
|
|
773
|
-
|
|
815
|
+
const capabilitiesFrom = Object.fromEntries(modules.map((m) => [m.name, fromOfVia(viaOf.get(m.name))]));
|
|
816
|
+
turnedOff.sort((a, b) => byCodepoint(a.name, b.name));
|
|
817
|
+
return deepFreeze({ ...decl, payloads, payloadOrigins: origins, teams, slotsFrom, capabilitiesFrom, turnedOff, declRevision, payloadRevision, revision });
|
|
774
818
|
}
|
|
775
819
|
|
|
776
820
|
/** Where a composed capability came from, as the Desktop names it (`layers.<layer>.from`): the soul's own
|