@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
package/lib/core.mjs
CHANGED
|
@@ -517,7 +517,7 @@ const LAUNCH_CONFIG_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
|
517
517
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
518
518
|
/** Environment the kernel sets for every launch (identity, home, roots) and
|
|
519
519
|
* its reference aliases: a configuration may not name them. */
|
|
520
|
-
export const RESERVED_LAUNCH_ENV = new Set(["OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "OATS_AGENT", "OATS_SOUL", "OATS_SOUL_ID", "OATS_ROOT", "OATS_CONTEXT", "OATS_WORKSPACE", "OATS_EVENT", "OATS_SETTINGS", "OATS_CLI_BIN", "PI_AGENT_INSTANCE", "PI_AGENT_HOME", "PI_AGENTS_ROOT"]);
|
|
520
|
+
export const RESERVED_LAUNCH_ENV = new Set(["OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "OATS_AGENT", "OATS_SOUL", "OATS_SOUL_ID", "OATS_ROOT", "OATS_CONTEXT", "OATS_WORKSPACE", "OATS_EVENT", "OATS_SETTINGS", "OATS_SETTINGS_ORIGINS", "OATS_CLI_BIN", "PI_AGENT_INSTANCE", "PI_AGENT_HOME", "PI_AGENTS_ROOT"]);
|
|
521
521
|
export const LAUNCH_REF_PREFIX = "OATS_LAUNCH_REF_";
|
|
522
522
|
const reservedLaunchEnv = (n) => RESERVED_LAUNCH_ENV.has(n) || n.startsWith(LAUNCH_REF_PREFIX);
|
|
523
523
|
export function validateLaunchConfig(name, entry, where) {
|
|
@@ -680,7 +680,9 @@ function loadManifestAt(idir, origin, { strict = false } = {}) {
|
|
|
680
680
|
if (targetFields.length) throw new Error(`capability ${id} manifest cannot declare config-owned targets: ${targetFields.join(", ")}`);
|
|
681
681
|
if (m.layer && !LAYERS.includes(m.layer)) throw new Error(`capability ${id} declares unknown layer "${m.layer}"`);
|
|
682
682
|
if (m.command && !/^[a-z0-9][a-z0-9-]*$/.test(m.command)) throw new Error(`capability ${id} has invalid command namespace "${m.command}"`);
|
|
683
|
-
|
|
683
|
+
// `agents:` (capability-defined agents) was removed in 0.29.0: resolution refuses a module that
|
|
684
|
+
// declares it (E_CAPABILITY_AGENTS_REMOVED). A copy already recorded in a home still loads here,
|
|
685
|
+
// so that home launches and retires; nothing reads the key.
|
|
684
686
|
validateManifestOperations(m, id);
|
|
685
687
|
validateBindingInterface(m);
|
|
686
688
|
return { ...m, _dir: idir, _origin: origin };
|
|
@@ -917,33 +919,6 @@ function manifestPath(manifest, rel) {
|
|
|
917
919
|
}
|
|
918
920
|
/** Resolve an executable declared by a manifest through the same artifact boundary as hooks. */
|
|
919
921
|
export function capabilityExecutablePath(manifest, rel) { return manifestPath(manifest, rel); }
|
|
920
|
-
function assertCapabilityTreeContained(manifest, tree, resource = "skill") {
|
|
921
|
-
const artifact = realpathSync(manifest._dir);
|
|
922
|
-
const visited = new Set();
|
|
923
|
-
const assertInside = (target, path) => {
|
|
924
|
-
const fromArtifact = relative(artifact, target);
|
|
925
|
-
if (fromArtifact === ".." || fromArtifact.startsWith(`..${sep}`) || isAbsolute(fromArtifact)) {
|
|
926
|
-
throw new Error(`capability ${manifest.capability} ${resource} path escapes its integrity boundary: ${relative(manifest._dir, path)}`);
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
|
-
const walk = (dir) => {
|
|
930
|
-
const realDir = realpathSync(dir);
|
|
931
|
-
assertInside(realDir, dir);
|
|
932
|
-
if (visited.has(realDir)) return;
|
|
933
|
-
visited.add(realDir);
|
|
934
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
935
|
-
const path = join(dir, entry.name);
|
|
936
|
-
const target = realpathSync(path); // also rejects broken symlinks
|
|
937
|
-
assertInside(target, path);
|
|
938
|
-
if (entry.isSymbolicLink()) {
|
|
939
|
-
// Recurse through contained directory links: descendants may carry a
|
|
940
|
-
// second symlink that escapes the package boundary.
|
|
941
|
-
if (lstatSync(target).isDirectory()) walk(target);
|
|
942
|
-
} else if (entry.isDirectory()) walk(path);
|
|
943
|
-
}
|
|
944
|
-
};
|
|
945
|
-
walk(tree);
|
|
946
|
-
}
|
|
947
922
|
/** Packaged default injection for a capability or work mode (undefined if none shipped). */
|
|
948
923
|
export function packagedInject(name, startDir) {
|
|
949
924
|
const m = capabilityManifest(name, startDir);
|
|
@@ -1014,7 +989,6 @@ export function composeInstanceAgentsMd(soulDir, contextDir, soulName, workMode,
|
|
|
1014
989
|
const workModeInject = packagedInject(`work-${workMode || "checkout"}`);
|
|
1015
990
|
if (workModeInject && existsSync(workModeInject)) wanted.push([`work-mode:${workMode || "checkout"}`, workModeInject]);
|
|
1016
991
|
for (const cap of resolved.capabilities) {
|
|
1017
|
-
if (kind === "capability" && cap.layer === "knowledge") continue; // ephemeral: no memory protocol
|
|
1018
992
|
if (cap.inject && existsSync(cap.inject)) wanted.push([`capability:${cap.id}`, cap.inject]);
|
|
1019
993
|
}
|
|
1020
994
|
const blocks = wanted.map(([source, file]) => ({ source, file, content: readFileSync(file, "utf8").trim() }));
|
|
@@ -1136,20 +1110,12 @@ export function planInstanceResources({ resolved, soulDir, agent, contextDir, co
|
|
|
1136
1110
|
if (typeof declared !== "string" || !declared) continue;
|
|
1137
1111
|
expected.push({ type: "skill-tree", source: cap.id, declared, path: undefined, entries: [], deferred: "materialize", origin: cap.origin, module: cap.id });
|
|
1138
1112
|
}
|
|
1139
|
-
|
|
1140
|
-
if (cap.injectDeclared && !intentionallyDroppedPlanned) expected.push({ type: "injection", source: cap.id, declared: cap.injectDeclared, path: undefined, deferred: "materialize", origin: cap.origin, module: cap.id });
|
|
1113
|
+
if (cap.injectDeclared) expected.push({ type: "injection", source: cap.id, declared: cap.injectDeclared, path: undefined, deferred: "materialize", origin: cap.origin, module: cap.id });
|
|
1141
1114
|
continue;
|
|
1142
1115
|
}
|
|
1143
1116
|
for (const s of cap.skillsDeclared || []) {
|
|
1144
1117
|
add({ type: "skill-tree", source: cap.id, declared: s.declared, path: s.path, origin: cap.origin, level: cap.level });
|
|
1145
1118
|
}
|
|
1146
|
-
// Capability agents are ephemeral and deliberately get no memory protocol,
|
|
1147
|
-
// so composeInstanceAgentsMd drops knowledge-layer injections for them.
|
|
1148
|
-
// The expected set MUST apply the same rule or it reports an intentional
|
|
1149
|
-
// omission as an incomplete composition. (Coupled to the matching `continue`
|
|
1150
|
-
// in composeInstanceAgentsMd — change both together.)
|
|
1151
|
-
const intentionallyDropped = agent?.kind === "capability" && cap.layer === "knowledge";
|
|
1152
|
-
if (intentionallyDropped) continue;
|
|
1153
1119
|
// A capability that declares `inject:` must produce it.
|
|
1154
1120
|
if (cap.injectDeclared && cap.inject === undefined) {
|
|
1155
1121
|
add({ type: "injection", source: cap.id, declared: cap.injectDeclared, path: undefined, origin: cap.origin, level: cap.level });
|
|
@@ -1501,6 +1467,11 @@ export function stableSoulId({ soulId, home, soulDir, agentName } = {}) {
|
|
|
1501
1467
|
return "";
|
|
1502
1468
|
}
|
|
1503
1469
|
export const workspaceSoulId = (repoKey, name) => `${repoKey}#${name}`;
|
|
1470
|
+
/** A triggered instance's event, inside its home's .oats/ (lib/triggers.mjs). */
|
|
1471
|
+
export const TRIGGER_EVENT_FILE = "trigger-event.json";
|
|
1472
|
+
/** A prepared soul entry's id: `<repoKey>#<name>` for a member or external soul, `package:<id>#<name>`
|
|
1473
|
+
* for a package soul (stable across the package's versions and independent of its repo). */
|
|
1474
|
+
const preparedSoulIdOf = (entry) => workspaceSoulId(typeof entry.package === "string" ? `package:${entry.package}` : entry.repoKey, entry.name);
|
|
1504
1475
|
/** The soul directory an instance incarnates, as spawn recorded it (instance.json
|
|
1505
1476
|
* `soulDir`): a workspace soul's per-commit copy (agents/<soul>/souls/<commit12>) or
|
|
1506
1477
|
* the read-only soul inside a capability package. It is what every classic
|
|
@@ -1584,6 +1555,10 @@ export function runLifecycleHooks(event, { home, instance, agentName, soulDir, s
|
|
|
1584
1555
|
// a caller's extraEnv: those may point at a different executable.
|
|
1585
1556
|
OATS_CLI_BIN: realpathSync(join(PKG_ROOT, "bin", "oats.mjs")),
|
|
1586
1557
|
OATS_SETTINGS: JSON.stringify(cap.settings || {}),
|
|
1558
|
+
// Where each leaf of OATS_SETTINGS came from (JSON pointer → { kind, at }; kind is
|
|
1559
|
+
// manifest-default | workspace | soul | host | spawn | anchor), so a provider can tell a
|
|
1560
|
+
// soul-set value from a host-set one. {} when the home recorded none (spawned before 0.29).
|
|
1561
|
+
OATS_SETTINGS_ORIGINS: JSON.stringify(cap.settingsOrigins || {}),
|
|
1587
1562
|
OATS_META: JSON.stringify(priorMeta[cap.id] || {}),
|
|
1588
1563
|
},
|
|
1589
1564
|
}).trim();
|
|
@@ -1600,7 +1575,7 @@ export function runLifecycleHooks(event, { home, instance, agentName, soulDir, s
|
|
|
1600
1575
|
// A launch hook's run is recorded even when its answer is empty: an
|
|
1601
1576
|
// empty answer replaces what the provider contributed before.
|
|
1602
1577
|
if (event === "launch" || (o.launch && typeof o.launch === "object" && Object.keys(o.launch).length) || (o.env && typeof o.env === "object" && Object.keys(o.env).length)) {
|
|
1603
|
-
results.contributions.push({ capability: cap.id, layer: cap.layer || null, level: cap.level || null, settings: { ...(cap.settings || {}) }, trust: { trusted: !!cap.trust?.trusted, integrity: cap.trust?.integrity || null }, launch: o.launch && typeof o.launch === "object" ? { ...o.launch } : {}, env: o.env && typeof o.env === "object" ? Object.keys(o.env).sort() : [] });
|
|
1578
|
+
results.contributions.push({ capability: cap.id, layer: cap.layer || null, level: cap.level || null, settings: { ...(cap.settings || {}) }, settingsOrigins: { ...(cap.settingsOrigins || {}) }, trust: { trusted: !!cap.trust?.trusted, integrity: cap.trust?.integrity || null }, launch: o.launch && typeof o.launch === "object" ? { ...o.launch } : {}, env: o.env && typeof o.env === "object" ? Object.keys(o.env).sort() : [] });
|
|
1604
1579
|
}
|
|
1605
1580
|
if (o.env !== undefined) {
|
|
1606
1581
|
if (event !== "spawn" && event !== "launch") throw new HookEnvironmentContractError(`${cap.id} hook env is supported only for spawn and launch, not ${event}`);
|
|
@@ -1672,7 +1647,10 @@ function readSoul(agentDir, soulDir = soulOf(agentDir)) {
|
|
|
1672
1647
|
}
|
|
1673
1648
|
const soul = soulHarnessField(stripInternalAnnotations(parsed), p);
|
|
1674
1649
|
soul._dir = agentDir;
|
|
1675
|
-
soul
|
|
1650
|
+
// A package soul homes at <package>--<soul> (lib/workspace.mjs packageSoulAgentName): that
|
|
1651
|
+
// directory, not the soul.yaml name, is its agent name, so its instances never share a
|
|
1652
|
+
// member soul's name or roster row.
|
|
1653
|
+
soul.name = basename(agentDir).includes("--") ? basename(agentDir) : (soul.name || basename(agentDir));
|
|
1676
1654
|
return soul;
|
|
1677
1655
|
}
|
|
1678
1656
|
export function findAgent(root, name) {
|
|
@@ -1686,91 +1664,6 @@ export function findAgentAt(root, name, soulDir) {
|
|
|
1686
1664
|
return agent;
|
|
1687
1665
|
}
|
|
1688
1666
|
|
|
1689
|
-
/** Canonical capability-defined agents: a manifest's `agents: ["agents/reviewer"]`
|
|
1690
|
-
* entries are package-relative soul directories (soul.yaml + AGENTS.md directly
|
|
1691
|
-
* inside). They resolve when the capability is ACTIVE in the
|
|
1692
|
-
* context; the soul stays read-only in the package (fresh identity every spawn —
|
|
1693
|
-
* no long-term memory), while instances home under <root>/<name>/instances/ (the
|
|
1694
|
-
* agent dir holds only instances/). */
|
|
1695
|
-
function capabilityAgentMetadata(manifest, rel) {
|
|
1696
|
-
const soulDir = manifestPath(manifest, rel);
|
|
1697
|
-
const soulFile = manifestPath(manifest, join(rel, "soul.yaml"));
|
|
1698
|
-
if (!soulDir || !soulFile) return undefined;
|
|
1699
|
-
// Read only contained identity metadata to decide whether this provider owns
|
|
1700
|
-
// the requested name. Full tree containment + trust happen after a match.
|
|
1701
|
-
const soul = soulHarnessField(stripInternalAnnotations(withConfigFile(soulFile, () => parseYamlFlat(readFileSync(soulFile, "utf8")))), soulFile);
|
|
1702
|
-
return { soulDir, soul, name: soul.name || basename(soulDir) };
|
|
1703
|
-
}
|
|
1704
|
-
/**
|
|
1705
|
-
* Workspace model: a capability-defined agent (a package's `agents:` soul, e.g.
|
|
1706
|
-
* OKF's memory-harvest worker) resolves from a MATERIALIZED module — the copy the
|
|
1707
|
-
* requesting instance already carries under <home>/.oats/modules/<cap>/. Looks in
|
|
1708
|
-
* `anchorHome` first (the --parent / --relative-to instance), then every instance
|
|
1709
|
-
* home under `root`; the first module declaring an agent of that name wins.
|
|
1710
|
-
* → the same shape findCapabilityAgent returns, plus `_manifestSource` (the home
|
|
1711
|
-
* whose modules supplied it) so skill/inject lookups read that copy.
|
|
1712
|
-
*/
|
|
1713
|
-
export function findModuleCapabilityAgent(root, name, { anchorHome = null } = {}) {
|
|
1714
|
-
if (typeof name !== "string" || !name) return undefined;
|
|
1715
|
-
const homes = [];
|
|
1716
|
-
if (anchorHome) homes.push(anchorHome);
|
|
1717
|
-
if (root && existsSync(root)) {
|
|
1718
|
-
for (const a of readdirSync(root, { withFileTypes: true })) {
|
|
1719
|
-
if (!a.isDirectory() || a.name.startsWith(".")) continue;
|
|
1720
|
-
const inst = join(root, a.name, "instances");
|
|
1721
|
-
if (!existsSync(inst)) continue;
|
|
1722
|
-
for (const i of readdirSync(inst, { withFileTypes: true })) if (i.isDirectory() && !i.name.startsWith(".")) homes.push(join(inst, i.name));
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
const seen = new Set();
|
|
1726
|
-
const failures = [];
|
|
1727
|
-
for (const home of homes) {
|
|
1728
|
-
const real = (() => { try { return realpathSync(home); } catch { return null; } })();
|
|
1729
|
-
if (!real || seen.has(real)) continue;
|
|
1730
|
-
seen.add(real);
|
|
1731
|
-
const modules = instanceModulesRoot(real);
|
|
1732
|
-
if (!modules) continue;
|
|
1733
|
-
for (const [id, manifest] of Object.entries(capabilityManifests(real))) {
|
|
1734
|
-
for (const rel of manifest?.agents || []) {
|
|
1735
|
-
let meta;
|
|
1736
|
-
try { meta = capabilityAgentMetadata(manifest, rel); } catch { continue; }
|
|
1737
|
-
if (!meta || meta.name !== name) continue;
|
|
1738
|
-
try {
|
|
1739
|
-
assertCapabilityTreeContained(manifest, meta.soulDir, "agent");
|
|
1740
|
-
return {
|
|
1741
|
-
...meta.soul, name,
|
|
1742
|
-
kind: "capability", capability: id,
|
|
1743
|
-
_dir: join(root, name),
|
|
1744
|
-
_soulDir: meta.soulDir,
|
|
1745
|
-
_manifestSource: real,
|
|
1746
|
-
_module: manifest._module,
|
|
1747
|
-
};
|
|
1748
|
-
} catch (e) { failures.push(e); }
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
if (failures.length) throw failures[0];
|
|
1753
|
-
return undefined;
|
|
1754
|
-
}
|
|
1755
|
-
/**
|
|
1756
|
-
* A capability-defined agent from ONE capability directory (a package tree the
|
|
1757
|
-
* workspace resolver fetched into the deployment's module store). The manifest is
|
|
1758
|
-
* read from `dir`, trusted by construction (the workspace declares the package; the lock pins it), and
|
|
1759
|
-
* the agent record points its skill/inject lookups at that store entry.
|
|
1760
|
-
*/
|
|
1761
|
-
export function capabilityAgentFromDir(dir, name, root, { module = null } = {}) {
|
|
1762
|
-
const manifest = loadManifestAt(dir, `module:${dir}`);
|
|
1763
|
-
if (!manifest) return undefined;
|
|
1764
|
-
manifest._module = module;
|
|
1765
|
-
for (const rel of manifest.agents || []) {
|
|
1766
|
-
let meta;
|
|
1767
|
-
try { meta = capabilityAgentMetadata(manifest, rel); } catch { continue; }
|
|
1768
|
-
if (!meta || meta.name !== name) continue;
|
|
1769
|
-
assertCapabilityTreeContained(manifest, meta.soulDir, "agent");
|
|
1770
|
-
return { ...meta.soul, name, kind: "capability", capability: manifest.capability, _dir: join(root, name), _soulDir: meta.soulDir, _manifestSource: dir, _manifest: manifest, _module: module };
|
|
1771
|
-
}
|
|
1772
|
-
return undefined;
|
|
1773
|
-
}
|
|
1774
1667
|
export function listAgents(root) {
|
|
1775
1668
|
const agents = [];
|
|
1776
1669
|
const scan = (base, kind) => {
|
|
@@ -1898,11 +1791,20 @@ const nameTooLong = (name, remedy) => oatsError("E_INSTANCE_NAME_INVALID", `inst
|
|
|
1898
1791
|
* final name, suffix included, is capped at MAX_INSTANCE_NAME. */
|
|
1899
1792
|
function nextInstanceName(root, agent, purpose, prepared) {
|
|
1900
1793
|
const n = deriveInstanceName(root, agent, purpose, prepared);
|
|
1901
|
-
if (n.length
|
|
1902
|
-
|
|
1903
|
-
|
|
1794
|
+
if (n.length <= MAX_INSTANCE_NAME) return n;
|
|
1795
|
+
if (!purpose) throw nameTooLong(n, `the soul name "${agent.name}" is too long to derive an instance name from; pass a shorter --name`);
|
|
1796
|
+
// Say the budget: the name is <agent name>-<purpose>[-<n>], and a package soul's agent name
|
|
1797
|
+
// carries its package id (oats-okf-knowledge-maintainer-…), so its purposes are short.
|
|
1798
|
+
const p = slug(purpose), prefix = `${slug(agent.name)}-`, suffix = n.length - prefix.length - p.length;
|
|
1799
|
+
const budget = Math.max(0, MAX_INSTANCE_NAME - prefix.length - suffix);
|
|
1800
|
+
throw Object.assign(nameTooLong(n, `the purpose ${JSON.stringify(p)} is ${p.length} characters; ${agent.name}'s instances are named ${prefix}<purpose>${suffix > 0 ? "-<n>" : ""}, which leaves at most ${budget} for the purpose — shorten it, or pass --name`), { details: { prefix, purpose: p, maxPurpose: budget } });
|
|
1801
|
+
}
|
|
1802
|
+
// The stem is the agent name AS A SLUG: a package soul's agent name holds `--`
|
|
1803
|
+
// (acme-pkg--keeper) and its instances are acme-pkg-keeper-<purpose>, so the name
|
|
1804
|
+
// that is length-checked and de-duplicated here is the one the home gets.
|
|
1904
1805
|
function deriveInstanceName(root, agent, purpose, prepared) {
|
|
1905
|
-
const
|
|
1806
|
+
const stem = slug(agent.name);
|
|
1807
|
+
const base = purpose ? `${stem}-${slug(purpose)}` : undefined;
|
|
1906
1808
|
const instancesDir = join(agent._dir, "instances");
|
|
1907
1809
|
const own = existsSync(instancesDir) ? readdirSync(instancesDir) : [];
|
|
1908
1810
|
const instances = deploymentInstanceHomes(root), souls = deploymentSoulNames(root, prepared);
|
|
@@ -1913,7 +1815,7 @@ function deriveInstanceName(root, agent, purpose, prepared) {
|
|
|
1913
1815
|
return n;
|
|
1914
1816
|
}
|
|
1915
1817
|
let i = own.length + 1, n;
|
|
1916
|
-
do { n = `${
|
|
1818
|
+
do { n = `${stem}-${i++}`; } while (taken(n));
|
|
1917
1819
|
return n;
|
|
1918
1820
|
}
|
|
1919
1821
|
|
|
@@ -1973,6 +1875,19 @@ export function resolveClaudeBinary(contextDir) {
|
|
|
1973
1875
|
* claude takes aliases/bare claude-* ids only; nothing usable → "" (claude default).
|
|
1974
1876
|
* codex: translate openai/openai-codex entries to native ids, otherwise use its default.
|
|
1975
1877
|
* Probe failures: first entry wins (pi errors loudly at launch). */
|
|
1878
|
+
/** instance.json `modelFrom` (feature desktop-facts): where the model a home runs came from — "soul" (the
|
|
1879
|
+
* soul's preference), "spawn" / "start" (an explicit --model then), "launch-config", "harness-default" (the
|
|
1880
|
+
* harness's native model). A recorded model keeps `prior`. From resolveLaunchSelection's `modelSource`. */
|
|
1881
|
+
export function modelFromOf(modelSource, { at = "spawn", prior = null } = {}) {
|
|
1882
|
+
if (typeof modelSource !== "string") return prior ?? null;
|
|
1883
|
+
if (modelSource === "explicit") return at;
|
|
1884
|
+
if (modelSource === "soul default") return "soul";
|
|
1885
|
+
if (modelSource.startsWith("launch-config ")) return "launch-config";
|
|
1886
|
+
if (modelSource === "recorded") return prior ?? null;
|
|
1887
|
+
if (modelSource.startsWith("native default")) return "harness-default";
|
|
1888
|
+
return prior ?? null;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1976
1891
|
export function resolveModelPreference(model, harness = "pi") {
|
|
1977
1892
|
const prefs = String(model || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1978
1893
|
if (harness === "codex") {
|
|
@@ -2545,7 +2460,7 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
2545
2460
|
if (o.name !== undefined) instance = explicitInstanceName(o.name);
|
|
2546
2461
|
else {
|
|
2547
2462
|
instance = o.instance || nextInstanceName(root, agent, o.purpose, o.prepared);
|
|
2548
|
-
if (!instance.startsWith(agent.name)) instance = `${agent.name}-${slug(instance)}`;
|
|
2463
|
+
if (!instance.startsWith(agent.name) && !instance.startsWith(slug(agent.name))) instance = `${agent.name}-${slug(instance)}`;
|
|
2549
2464
|
instance = slug(instance);
|
|
2550
2465
|
if (instance.length > MAX_INSTANCE_NAME) throw nameTooLong(instance, "pass a shorter instance name");
|
|
2551
2466
|
}
|
|
@@ -2807,7 +2722,9 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
2807
2722
|
}
|
|
2808
2723
|
|
|
2809
2724
|
const home = join(agent._dir, "instances", instance);
|
|
2810
|
-
|
|
2725
|
+
// A home that exists already is the same refusal as losing the exclusive mkdir below (a
|
|
2726
|
+
// concurrent apply that got there first, or anything else): E_PLACEMENT_TAKEN, nothing created.
|
|
2727
|
+
if (existsSync(home)) throw Object.assign(oatsError("E_PLACEMENT_TAKEN", `${instance} already exists at ${home}; nothing was created by this call`), { instance, home });
|
|
2811
2728
|
// AUTHORITATIVE placement check, on the DESTINATION rather than on lexical
|
|
2812
2729
|
// paths, immediately before the first side effect. The earlier root/agent-dir
|
|
2813
2730
|
// checks are lexical and can be walked around by a symlink anywhere along the
|
|
@@ -2849,7 +2766,7 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
2849
2766
|
// reads the soul, config chain and capability content), so resolving it here
|
|
2850
2767
|
// lets every "declared but missing" failure happen with zero side effects to
|
|
2851
2768
|
// roll back — no home, no worktree, no identity, no tmux window.
|
|
2852
|
-
//
|
|
2769
|
+
// A workspace soul carries _soulDir (its per-commit soul cache: findAgentAt).
|
|
2853
2770
|
const soulDir = agent._soulDir || soulOf(agent._dir);
|
|
2854
2771
|
const composition = composeInstanceAgentsMd(soulDir, repoAbs, agent.name, work, agent.kind, o.prepared);
|
|
2855
2772
|
const resolvedCfg = composition.resolved;
|
|
@@ -3033,13 +2950,7 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
3033
2950
|
materializeOutcome = yield () => (o.materialize ?? materializePreparedDefault)({ ...o.prepared, soulAgentsMd: composition.text, soulDir }, home);
|
|
3034
2951
|
const rows = (typeof o.prepared.toCapabilityRows === "function" ? o.prepared.toCapabilityRows : toCapabilityRows)(o.prepared.resolution, home);
|
|
3035
2952
|
if (!Array.isArray(rows)) throw new Error("toCapabilityRows returned no rows");
|
|
3036
|
-
|
|
3037
|
-
// (lead decision c3 Q1: a harvester never registers as a knowledge source or
|
|
3038
|
-
// gets a messaging identity). Its rows keep everything else.
|
|
3039
|
-
for (const row of rows) {
|
|
3040
|
-
if (agent.kind === "capability") { row.hooks = {}; row.requiredHooks = []; }
|
|
3041
|
-
else row.hooks = materializedHookCommands(row, home);
|
|
3042
|
-
}
|
|
2953
|
+
for (const row of rows) row.hooks = materializedHookCommands(row, home);
|
|
3043
2954
|
resolvedCfg.capabilities = rows;
|
|
3044
2955
|
// S1: the capability blocks materialize appended are part of the composed
|
|
3045
2956
|
// instructions. Read the markers back from the AGENTS.md that was WRITTEN
|
|
@@ -3270,14 +3181,22 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
3270
3181
|
// Capability lifecycle hooks (spawn) — the knowledge integration scaffolds instance
|
|
3271
3182
|
// memory (STATE.md/log.md/notes/ are OKF conventions, not kernel ones); the
|
|
3272
3183
|
// messaging integration mints the comms identity. Kernel stays memory-agnostic.
|
|
3273
|
-
const preparedSoulId = o.prepared ?
|
|
3184
|
+
const preparedSoulId = o.prepared ? preparedSoulIdOf(o.prepared.soulEntry) : undefined;
|
|
3185
|
+
// A trigger's event (lib/triggers.mjs): a private copy in the home, named to hooks and the harness
|
|
3186
|
+
// as OATS_TRIGGER_EVENT_FILE. Its PR title/body are never in the task: the soul reads them from here.
|
|
3187
|
+
let triggerEventFile = null;
|
|
3188
|
+
if (o.triggerEvent && typeof o.triggerEvent === "object") {
|
|
3189
|
+
triggerEventFile = join(home, ".oats", TRIGGER_EVENT_FILE);
|
|
3190
|
+
mkdirSync(dirname(triggerEventFile), { recursive: true });
|
|
3191
|
+
writeFileSync(triggerEventFile, JSON.stringify(o.triggerEvent, null, 2) + "\n", { mode: 0o600 });
|
|
3192
|
+
}
|
|
3274
3193
|
// Hooks read the soul the HOME links (the per-commit directory for a workspace
|
|
3275
3194
|
// soul), never the swappable agents/<name>/soul pointer: a provider that pins a
|
|
3276
3195
|
// path must pin this instance's content, and OATS_SOUL_ID is what it keys on.
|
|
3277
3196
|
const hookRes = runLifecycleHooks("spawn", {
|
|
3278
3197
|
home, instance, agentName: agent.name, soulDir: homeSoulTarget, soulId: preparedSoulId, contextDir: repoAbs,
|
|
3279
3198
|
workspaceDir: workspaceOf(root), rootDir: root, resolved: resolvedCfg,
|
|
3280
|
-
extraEnv: { OATS_TASK: task, OATS_REPO: repoAbs, OATS_BRANCH: branch || "", OATS_WORK: work, OATS_HARNESS: harness, OATS_RUNTIME: harness, OATS_KIND: agent.kind || "persistent" },
|
|
3199
|
+
extraEnv: { OATS_TASK: task, OATS_REPO: repoAbs, OATS_BRANCH: branch || "", OATS_WORK: work, OATS_HARNESS: harness, OATS_RUNTIME: harness, OATS_KIND: agent.kind || "persistent", ...(triggerEventFile ? { OATS_TRIGGER_EVENT_FILE: triggerEventFile } : {}) },
|
|
3281
3200
|
});
|
|
3282
3201
|
warnings.push(...hookRes.warnings);
|
|
3283
3202
|
// Which capability hooks RAN (in order) and how each ended — recorded on the
|
|
@@ -3498,6 +3417,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3498
3417
|
hooks: { launch: { ...hookRes.launch }, env: { ...hookRes.env }, contributions: hookRes.contributions || [] },
|
|
3499
3418
|
prompt: LAUNCH_PROMPT,
|
|
3500
3419
|
};
|
|
3420
|
+
if (triggerEventFile) recipe.env.OATS_TRIGGER_EVENT_FILE = triggerEventFile;
|
|
3501
3421
|
const cmdline = renderLaunchRecipe(recipe, { home, instance });
|
|
3502
3422
|
|
|
3503
3423
|
// Module skills as materialize landed them (.agents/skills/<module>/<skill>/),
|
|
@@ -3506,7 +3426,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3506
3426
|
const moduleSkills = (materializeOutcome?.skills || []).map((row) => ({ name: row.name, source: `module:${row.module}`, from: join(home, row.from) }));
|
|
3507
3427
|
const meta = {
|
|
3508
3428
|
agent: agent.name, kind: agent.kind || "persistent", instance, home, soulDir: homeSoulTarget,
|
|
3509
|
-
repo: repoAbs, work, branch, harness, model: model || undefined,
|
|
3429
|
+
repo: repoAbs, work, branch, harness, model: model || undefined, modelFrom: modelFromOf(launchSelection.modelSource, { at: "spawn" }) ?? undefined,
|
|
3510
3430
|
...(yolo !== undefined ? { yolo } : {}),
|
|
3511
3431
|
parentInstance: parentInstance && parentInstance !== instance ? parentInstance : undefined,
|
|
3512
3432
|
siblingInstance: siblingInstance && siblingInstance !== instance ? siblingInstance : undefined,
|
|
@@ -3514,6 +3434,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3514
3434
|
relativeTo: relation ? relativeTo : undefined,
|
|
3515
3435
|
spawnOrigin: relation || (parentInstance && parentInstance !== instance) ? "instance" : "operator",
|
|
3516
3436
|
policy: { childSpawns: ownChildPolicy },
|
|
3437
|
+
...(triggerEventFile ? { trigger: { id: o.triggerEvent.trigger, key: o.triggerEvent.key ?? null, source: o.triggerEvent.source, repo: o.triggerEvent.repo, number: o.triggerEvent.number, url: o.triggerEvent.url ?? null, event: o.triggerEvent.event, headSha: o.triggerEvent.headSha ?? null, observedAt: o.triggerEvent.observedAt ?? null, eventFile: triggerEventFile } } : {}),
|
|
3517
3438
|
// K6c: a decision-bound spawn records what bound it, so a retry with the
|
|
3518
3439
|
// same key replays this receipt instead of spawning again.
|
|
3519
3440
|
// The FULL bound decision (placement + effective), exactly as the fence
|
|
@@ -3523,7 +3444,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3523
3444
|
capabilityMeta: Object.keys(hookRes.meta).length ? hookRes.meta : undefined,
|
|
3524
3445
|
capabilities: resolvedCfg.capabilities.map((cap) => ({
|
|
3525
3446
|
id: cap.id, layer: cap.layer, command: cap.command, origin: cap.origin, level: cap.level,
|
|
3526
|
-
settings: cap.settings, provenance: cap.provenance, skills: cap.skills || [],
|
|
3447
|
+
settings: cap.settings, settingsOrigins: cap.settingsOrigins ?? {}, provenance: cap.provenance, skills: cap.skills || [],
|
|
3527
3448
|
hooks: Object.keys(cap.hooks || {}), trusted: !!cap.trust?.trusted,
|
|
3528
3449
|
...(cap.environment?.length ? { environment: [...cap.environment] } : {}),
|
|
3529
3450
|
...(cap.environmentNamespaces?.length ? { environmentNamespaces: [...cap.environmentNamespaces] } : {}),
|
|
@@ -3568,7 +3489,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3568
3489
|
},
|
|
3569
3490
|
},
|
|
3570
3491
|
capabilityRuntime: resolvedCfg.capabilities.map((cap) => ({
|
|
3571
|
-
id: cap.id, layer: cap.layer, level: cap.level, settings: cap.settings,
|
|
3492
|
+
id: cap.id, layer: cap.layer, level: cap.level, settings: cap.settings, settingsOrigins: cap.settingsOrigins ?? {},
|
|
3572
3493
|
hooks: cap.hooks, requiredHooks: cap.requiredHooks, environment: cap.environment, environmentNamespaces: cap.environmentNamespaces,
|
|
3573
3494
|
missingRequires: cap.missingRequires, trust: cap.trust,
|
|
3574
3495
|
executable: cap.executable,
|
|
@@ -3582,7 +3503,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3582
3503
|
try { const prior = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); if (prior.modules) meta.modules = prior.modules; if (prior.providers) meta.providers = prior.providers; } catch { /* materialize wrote it; absent means nothing to carry */ }
|
|
3583
3504
|
// M5/3a: the workspace's name and the deployment directory are recorded, so a home
|
|
3584
3505
|
// answers them (inspect/operation run --home, OATS_WORKSPACE_NAME) without discovery.
|
|
3585
|
-
meta.workspace = { key: o.prepared.discovery?.key ?? null, name: o.prepared.discovery?.workspace?.name ?? null, deployment: o.prepared.deployment ?? null, commit: o.prepared.discovery?.commit ?? null, resolution: o.prepared.resolution.revision, standalone: o.prepared.discovery?.standalone === true, soul: { id:
|
|
3506
|
+
meta.workspace = { key: o.prepared.discovery?.key ?? null, name: o.prepared.discovery?.workspace?.name ?? null, deployment: o.prepared.deployment ?? null, commit: o.prepared.discovery?.commit ?? null, resolution: o.prepared.resolution.revision, standalone: o.prepared.discovery?.standalone === true, soul: { id: preparedSoulIdOf(o.prepared.soulEntry), repoKey: o.prepared.soulEntry.repoKey, commit: o.prepared.soulEntry.commit, team: o.prepared.soulEntry.team ?? null, labels: [...(o.prepared.soulEntry.labels ?? (o.prepared.soulEntry.team ? [o.prepared.soulEntry.team] : []))], ...(typeof o.prepared.soulEntry.package === "string" ? { name: o.prepared.soulEntry.name, qualifiedName: o.prepared.soulEntry.qualifiedName, package: { id: o.prepared.soulEntry.package, version: o.prepared.soulEntry.version, commit: o.prepared.soulEntry.commit, digest: o.prepared.soulEntry.digest, path: o.prepared.soulEntry.path } } : {}) }, layers: layerRows(o.prepared.resolution) };
|
|
3586
3507
|
// Teams contract (decision 6): the eligible teams at spawn, recorded as EVIDENCE beside
|
|
3587
3508
|
// `providers` (never inside that capability-keyed map). A home's hooks and operations get
|
|
3588
3509
|
// the LIVE set (liveTeams); this is what they fall back to when discovery cannot answer.
|
|
@@ -3730,6 +3651,12 @@ export function listInstances(root, tmuxSession = DEFAULT_TMUX_SESSION) {
|
|
|
3730
3651
|
catch (error) { liveness = { running: null, runtimeState: "unreachable", runtimeError: error.message }; }
|
|
3731
3652
|
}
|
|
3732
3653
|
const identity = servedIdentityOf(meta);
|
|
3654
|
+
// Feature desktop-facts: `startedAt` — the last session start or restart (the receipt's restarts[]),
|
|
3655
|
+
// else the spawn's own launch (createdAt), else null (never launched); `identityAddress` — the
|
|
3656
|
+
// messaging identity's address (or alias) its capability recorded, null otherwise; `modelFrom` — null
|
|
3657
|
+
// for a home spawned before it was recorded.
|
|
3658
|
+
const lastStart = Array.isArray(meta.restarts) && meta.restarts.length ? meta.restarts[meta.restarts.length - 1]?.startedAt ?? null : null;
|
|
3659
|
+
const facts = { startedAt: lastStart ?? (meta.launched ? meta.createdAt ?? null : null), identityAddress: identity ? identity.address ?? identity.alias ?? null : null, modelFrom: meta.modelFrom ?? null };
|
|
3733
3660
|
// The home IS the directory enumerated here, and the instance its name:
|
|
3734
3661
|
// a file inside it cannot relocate or rename itself in the roster (every
|
|
3735
3662
|
// consumer acting on `home` — retire, inspect --home, the Desktop's file
|
|
@@ -3739,7 +3666,7 @@ export function listInstances(root, tmuxSession = DEFAULT_TMUX_SESSION) {
|
|
|
3739
3666
|
...(meta.home !== undefined && meta.home !== home ? { recordedHome: meta.home } : {}),
|
|
3740
3667
|
...(meta.instance !== undefined && meta.instance !== e.name ? { recordedInstance: meta.instance } : {}),
|
|
3741
3668
|
};
|
|
3742
|
-
return { ...meta, ...claims, home, instance: e.name, ...(identity ? { identity } : {}), ...(meta.launch && typeof meta.launch === "object" ? { launch: redactLaunchRecipe(meta.launch) } : {}), ...(typeof meta.command === "string" ? { command: redactLaunchCommand(meta.command) } : {}), ...liveness, ...(rollbackIncomplete ? { rollbackIncomplete } : {}), ...(retirePending ? { retirePending } : {}) };
|
|
3669
|
+
return { ...meta, ...claims, home, instance: e.name, ...facts, ...(identity ? { identity } : {}), ...(meta.launch && typeof meta.launch === "object" ? { launch: redactLaunchRecipe(meta.launch) } : {}), ...(typeof meta.command === "string" ? { command: redactLaunchCommand(meta.command) } : {}), ...liveness, ...(rollbackIncomplete ? { rollbackIncomplete } : {}), ...(retirePending ? { retirePending } : {}) };
|
|
3743
3670
|
|
|
3744
3671
|
});
|
|
3745
3672
|
};
|
|
@@ -3926,7 +3853,7 @@ function quarantineInstanceHome({ home, instance, agent, soulDir, soulId, incomp
|
|
|
3926
3853
|
...(sessionTarget ? { sessionTarget } : {}),
|
|
3927
3854
|
outstanding: { hooks: [...outstandingHooks], git: [...outstandingGit], ...(directoryPreservation ? { directory: true } : {}) },
|
|
3928
3855
|
capabilityRuntime: (resolvedCfg.capabilities || []).map((cap) => ({
|
|
3929
|
-
id: cap.id, layer: cap.layer, level: cap.level, settings: cap.settings,
|
|
3856
|
+
id: cap.id, layer: cap.layer, level: cap.level, settings: cap.settings, settingsOrigins: cap.settingsOrigins ?? {},
|
|
3930
3857
|
hooks: cap.hooks, requiredHooks: cap.requiredHooks, environment: cap.environment, environmentNamespaces: cap.environmentNamespaces,
|
|
3931
3858
|
missingRequires: cap.missingRequires,
|
|
3932
3859
|
trust: cap.trust, executable: cap.executable,
|
|
@@ -4523,7 +4450,9 @@ export function capturedProviders(meta, frozen) {
|
|
|
4523
4450
|
return ids.map((id) => {
|
|
4524
4451
|
const contribution = contributions.find((c) => c.capability === id) || null;
|
|
4525
4452
|
const binding = bindings.find((b) => b.id === id) || null;
|
|
4526
|
-
|
|
4453
|
+
// The origins travel with the settings they describe (the contribution's, else the binding's).
|
|
4454
|
+
const settingsOrigins = (contribution?.settings ? contribution.settingsOrigins : binding?.settingsOrigins) ?? {};
|
|
4455
|
+
return { id, contribution, binding, settings: contribution?.settings ?? binding?.settings ?? {}, settingsOrigins };
|
|
4527
4456
|
});
|
|
4528
4457
|
}
|
|
4529
4458
|
/** Capability contributions for a start of an existing home: the recorded
|
|
@@ -4551,7 +4480,7 @@ export function prepareLaunchHooks({ frozen, harness, resolvedCfg, home, meta, c
|
|
|
4551
4480
|
const trust = capabilityTrust(manifest);
|
|
4552
4481
|
if (!trust.trusted) throw oatsError("E_LAUNCH_PREPARATION", `${p.id} was part of this home's launch at spawn but is no longer trusted in the scope (${trust.reason || "not trusted"}); respawn the instance; nothing was stopped`);
|
|
4553
4482
|
const hooks = manifestHookCommands(manifest);
|
|
4554
|
-
if (hooks.launch) withLaunchHook.push({ id: p.id, capability: p.id, manifest, layer: p.contribution?.layer ?? p.binding?.layer ?? manifest.layer ?? null, level: p.contribution?.level ?? p.binding?.level ?? null, settings: p.settings, hooks, trust, environment: [...(manifest.environment || [])], environmentNamespaces: [...(manifest.environmentNamespaces || [])], missingRequires: [] });
|
|
4483
|
+
if (hooks.launch) withLaunchHook.push({ id: p.id, capability: p.id, manifest, layer: p.contribution?.layer ?? p.binding?.layer ?? manifest.layer ?? null, level: p.contribution?.level ?? p.binding?.level ?? null, settings: p.settings, settingsOrigins: p.settingsOrigins, hooks, trust, environment: [...(manifest.environment || [])], environmentNamespaces: [...(manifest.environmentNamespaces || [])], missingRequires: [] });
|
|
4555
4484
|
}
|
|
4556
4485
|
if (withLaunchHook.length) {
|
|
4557
4486
|
const res = runLifecycleHooks("launch", { assertRoots, home, instance: meta.instance, agentName: meta.agent, soulDir: instanceSoulDir(home, meta), contextDir: ctx, rootDir: dirname(dirname(dirname(home))), resolved: { ...(resolvedCfg || {}), capabilities: withLaunchHook }, priorMeta: meta.capabilityMeta || {}, extraEnv: { OATS_HARNESS: harness, OATS_PREVIOUS_HARNESS: frozen.harness || "", OATS_RUNTIME: harness, OATS_PREVIOUS_RUNTIME: frozen.harness || "", ...extraEnv } });
|
|
@@ -4653,7 +4582,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4653
4582
|
// The independent receipt first (retire and session consult it), then the
|
|
4654
4583
|
// mutable metadata; both tmp+rename. A failure between them is what the
|
|
4655
4584
|
// pending receipt exists for.
|
|
4656
|
-
const record = (meta, { id, backend, target, model, command, startedAt, reused, launch, harness: newHarness, yolo: newYolo, stop, nativeRecordId, hookMeta }, clearPending = true) => {
|
|
4585
|
+
const record = (meta, { id, backend, target, model, command, startedAt, reused, launch, harness: newHarness, yolo: newYolo, stop, nativeRecordId, hookMeta, modelFrom }, clearPending = true) => {
|
|
4657
4586
|
checkRoots();
|
|
4658
4587
|
const baselinePath = retirementBaselinePath(realHome);
|
|
4659
4588
|
let baseline;
|
|
@@ -4667,7 +4596,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4667
4596
|
const recorded = meta.startId === id;
|
|
4668
4597
|
const restarts = (Array.isArray(meta.restarts) ? meta.restarts : []).slice(recorded ? -20 : -19);
|
|
4669
4598
|
if (!recorded) restarts.push({ startedAt, model: model ?? null, reused });
|
|
4670
|
-
const next = { ...meta, model, command, launched: true, startId: id, restarts, restartCount: (meta.restartCount || 0) + (recorded ? 0 : 1),
|
|
4599
|
+
const next = { ...meta, model, command, launched: true, startId: id, restarts, restartCount: (meta.restartCount || 0) + (recorded ? 0 : 1), ...(modelFrom ? { modelFrom } : {}),
|
|
4671
4600
|
...(launch ? { launch } : {}), ...(newHarness ? { harness: newHarness } : {}), ...(newYolo !== undefined ? { yolo: newYolo } : {}),
|
|
4672
4601
|
// Launch-hook meta lands per capability over the spawn's record; a hook
|
|
4673
4602
|
// that answered without meta keeps its previous entry (retire reads it).
|
|
@@ -4752,7 +4681,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4752
4681
|
// preflight happens here, before anything is observed or stopped.
|
|
4753
4682
|
const selected = o.launchConfig !== undefined || o.harness !== undefined || o.yolo !== undefined;
|
|
4754
4683
|
const hasRecipe = meta.launch && typeof meta.launch === "object";
|
|
4755
|
-
let launchPlan = null;
|
|
4684
|
+
let launchPlan = null, explicitModelFrom = null;
|
|
4756
4685
|
if (selected || hasRecipe) {
|
|
4757
4686
|
// Every start of a home with a recipe (ordinary, model-only, or under a
|
|
4758
4687
|
// selection) goes through the one planner: recipe shape, the recorded
|
|
@@ -4765,13 +4694,13 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4765
4694
|
const resolvedCfg = resolvedFromHome(realHome, meta, { teams: o.teams, teamsSource: o.teamsSource });
|
|
4766
4695
|
let agent; try { agent = findAgent(dirname(dirname(dirname(realHome))), meta.agent); } catch { agent = undefined; }
|
|
4767
4696
|
const plan = planLaunch({ home: realHome, instance: meta.instance, meta, contextDir: context, agentLike: agent || { harness: meta.harness, model: meta.model, yolo: meta.yolo }, selection: { launchConfig: o.launchConfig, harness: o.harness, model: o.model, yolo: o.yolo }, resolvedCfg, env: o.env || process.env, assertRoots: checkRoots });
|
|
4768
|
-
launchPlan = { recipe: plan.recipe, command: plan.command, harness: plan.harness, model: plan.model, yolo: plan.yolo, ...(plan.hookMeta ? { hookMeta: plan.hookMeta } : {}) };
|
|
4697
|
+
launchPlan = { recipe: plan.recipe, command: plan.command, harness: plan.harness, model: plan.model, yolo: plan.yolo, modelFrom: modelFromOf(plan.modelSource, { at: "start", prior: meta.modelFrom ?? null }), ...(plan.hookMeta ? { hookMeta: plan.hookMeta } : {}) };
|
|
4769
4698
|
command = launchPlan.command; model = launchPlan.model;
|
|
4770
4699
|
} else if (o.model !== undefined && o.model !== null && String(o.model).trim() !== "") {
|
|
4771
4700
|
const resolved = resolveModelPreference(String(o.model), harness);
|
|
4772
4701
|
if (!resolved) throw oatsError("E_MODEL_UNKNOWN", `model preference ${JSON.stringify(o.model)} has no entry usable by harness ${harness}; give a ${harness} model id`);
|
|
4773
4702
|
command = withLaunchModel(command, resolved);
|
|
4774
|
-
model = resolved;
|
|
4703
|
+
model = resolved; explicitModelFrom = "start";
|
|
4775
4704
|
} else parseLaunchCommand(command);
|
|
4776
4705
|
// References recorded for this home must resolve on this host on every
|
|
4777
4706
|
// start path, and the source variables go to the pane, not the command.
|
|
@@ -4781,8 +4710,8 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4781
4710
|
const paneEnvFlags = paneEnv.flatMap((r) => ["-e", `${r.name}=${r.value}`]);
|
|
4782
4711
|
const paneEnvExports = paneEnv.map((r) => `export ${r.name}=${shq(r.value)}; `).join("");
|
|
4783
4712
|
checkRoots(); // launch hooks/preparation have run; no backend has been observed
|
|
4784
|
-
const planExtra = launchPlan ? { launch: launchPlan.recipe, harness: launchPlan.harness, yolo: launchPlan.yolo,
|
|
4785
|
-
...(launchPlan.hookMeta ? { hookMeta: launchPlan.hookMeta } : {}) } : {};
|
|
4713
|
+
const planExtra = launchPlan ? { launch: launchPlan.recipe, harness: launchPlan.harness, yolo: launchPlan.yolo, ...(launchPlan.modelFrom ? { modelFrom: launchPlan.modelFrom } : {}),
|
|
4714
|
+
...(launchPlan.hookMeta ? { hookMeta: launchPlan.hookMeta } : {}) } : (explicitModelFrom ? { modelFrom: explicitModelFrom } : {});
|
|
4786
4715
|
let target = receipt.target;
|
|
4787
4716
|
let state = { present: false, state: "not-launched" };
|
|
4788
4717
|
let serverGone = false;
|
package/lib/instance-inspect.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { accessSync, constants as fsConstants, existsSync, readFileSync, realpat
|
|
|
15
15
|
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { capabilityManifests, instanceSoulDir, manifestOperations, parseYamlNested, servedIdentityOf, teamEnv, upgradeHomeMeta, withConfigFile } from "./core.mjs";
|
|
18
|
-
import { discoverOrStandalone, findSoulEntry, liveTeams, prepareInstance } from "./instance-resolution.mjs";
|
|
18
|
+
import { agentDirOf, discoverOrStandalone, findSoulEntry, liveTeams, prepareInstance } from "./instance-resolution.mjs";
|
|
19
19
|
import { declaredSettings } from "./capability-contract.mjs";
|
|
20
20
|
import { kernelCompatibility, teamLabelsOf, teamsOf } from "./resolve.mjs";
|
|
21
21
|
import { loadLocal } from "./workspace.mjs";
|
|
@@ -94,7 +94,7 @@ export async function homeTarget(home, meta, { remoteOptions, discover = true, l
|
|
|
94
94
|
// The derived deployment exactly — never an oats-local.yaml found further up.
|
|
95
95
|
const found = loadLocal(deployment);
|
|
96
96
|
if (real(dirname(found.path)) !== real(deployment)) throw Object.assign(new Error(`${deployment} has no oats-local.yaml`), { code: "E_HOME_MISMATCH" });
|
|
97
|
-
discovery = await discoverOrStandalone(found.local, { remoteOptions });
|
|
97
|
+
discovery = await discoverOrStandalone(found.local, { deployment, remoteOptions });
|
|
98
98
|
}
|
|
99
99
|
catch (e) { discoveryError = { code: e.code || "E_REMOTE_UNREADABLE", message: e.message }; }
|
|
100
100
|
}
|
|
@@ -120,7 +120,9 @@ export async function homeTarget(home, meta, { remoteOptions, discover = true, l
|
|
|
120
120
|
soul: { name: meta.agent, repoKey: ws.soul?.repoKey ?? null, commit: ws.soul?.commit ?? null, team: ws.soul?.team ?? null, external, path: null, soulDir, definition, problems: [...problems, ...loadProblems] },
|
|
121
121
|
workspace: { key: ws.key ?? null, name: typeof ws.name === "string" ? ws.name : discovery?.workspace?.name ?? null, deployment, commit: ws.commit ?? null, standalone: ws.standalone === true },
|
|
122
122
|
modules: Object.keys(modules).sort().map((name) => ({ name, from: modules[name]?.from ?? null, manifest: manifests[name] ?? null, dir: join(realHome, ".oats", "modules", name) })),
|
|
123
|
-
payloads: obj(meta.providers) ? meta.providers : {},
|
|
123
|
+
payloads: obj(meta.providers) ? meta.providers : {},
|
|
124
|
+
payloadOrigins: Object.fromEntries((Array.isArray(meta.capabilities) ? meta.capabilities : []).filter((c) => c && typeof c.id === "string").map((c) => [c.id, obj(c.settingsOrigins) ? c.settingsOrigins : {}])),
|
|
125
|
+
slots, slotsFrom: Object.fromEntries(LAYERS.map((l) => [l, obj(ws.layers?.[l]) ? ws.layers[l].from ?? null : null])), discovery, discoveryError, resolutionError: null,
|
|
124
126
|
lock: null, prepared: null,
|
|
125
127
|
};
|
|
126
128
|
}
|
|
@@ -140,13 +142,13 @@ export async function soulTarget(contextDir, soul, { remoteOptions } = {}) {
|
|
|
140
142
|
let discovery = prepared?.discovery ?? null, soulEntry = prepared?.soulEntry ?? null;
|
|
141
143
|
if (!prepared) {
|
|
142
144
|
// Still name the soul (and its member) when its resolution is refused.
|
|
143
|
-
discovery = await discoverOrStandalone(found.local, { remoteOptions });
|
|
145
|
+
discovery = await discoverOrStandalone(found.local, { deployment, remoteOptions });
|
|
144
146
|
soulEntry = findSoulEntry(discovery, soul);
|
|
145
147
|
}
|
|
146
148
|
const res = prepared?.resolution;
|
|
147
149
|
// A refused resolution still names its eligible teams: the labels and the workspace are known.
|
|
148
150
|
const teams = res?.teams ?? teamsOf(discovery?.standalone === true ? null : discovery?.workspace ?? null, teamLabelsOf(soulEntry));
|
|
149
|
-
const cached = soulEntry?.commit ? join(deployment, "agents", soulEntry
|
|
151
|
+
const cached = soulEntry?.commit ? join(deployment, "agents", agentDirOf(soulEntry), "souls", String(soulEntry.commit).slice(0, 12)) : null;
|
|
150
152
|
return {
|
|
151
153
|
kind: "soul", home: null, meta: null, deployment, agentsRoot: join(deployment, "agents"), teams, teamsSource: "live",
|
|
152
154
|
subject: { kind: "soul", soul: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null, team: soulEntry.team ?? null },
|
|
@@ -154,7 +156,8 @@ export async function soulTarget(contextDir, soul, { remoteOptions } = {}) {
|
|
|
154
156
|
soulDir: cached && existsSync(join(cached, "soul.yaml")) ? cached : null, definition: soulEntry.definition ?? null, problems: [] },
|
|
155
157
|
workspace: { key: discovery?.key ?? null, name: discovery?.workspace?.name ?? null, deployment, commit: discovery?.commit ?? null, standalone: discovery?.standalone === true },
|
|
156
158
|
modules: (res?.modules || []).map((m) => ({ name: m.name, from: m.from ?? null, manifest: m.manifest ?? null, dir: null, module: m })).sort((a, b) => a.name.localeCompare(b.name)),
|
|
157
|
-
payloads: obj(res?.payloads) ? res.payloads : {}, slots: obj(res?.slots) ? res.slots : Object.fromEntries(LAYERS.map((l) => [l, null])), slotsFrom: obj(res?.slotsFrom) ? res.slotsFrom : {},
|
|
159
|
+
payloads: obj(res?.payloads) ? res.payloads : {}, payloadOrigins: obj(res?.payloadOrigins) ? res.payloadOrigins : {}, slots: obj(res?.slots) ? res.slots : Object.fromEntries(LAYERS.map((l) => [l, null])), slotsFrom: obj(res?.slotsFrom) ? res.slotsFrom : {},
|
|
160
|
+
capabilitiesFrom: obj(res?.capabilitiesFrom) ? res.capabilitiesFrom : {}, turnedOff: Array.isArray(res?.turnedOff) ? res.turnedOff : [],
|
|
158
161
|
discovery, discoveryError: null, resolutionError, lock: prepared?.lock ?? null, prepared,
|
|
159
162
|
};
|
|
160
163
|
}
|
|
@@ -184,7 +187,9 @@ function capabilityRows(t) {
|
|
|
184
187
|
else if (op.context === "home" && !t.home) reason = "needs a running home (--home)";
|
|
185
188
|
return { ...op, argv: [m.command ?? null, op.command], available: !reason, reason };
|
|
186
189
|
});
|
|
187
|
-
|
|
190
|
+
// composedFrom (feature desktop-facts): where the soul's composition took it from — "workspace" |
|
|
191
|
+
// "team:<label>" | "soul" (layers-from vocabulary); null for a home (not recorded at spawn). `from` is the module's origin.
|
|
192
|
+
return { id: name, version: m.version ?? null, layer: m.layer ?? null, command: m.command ?? null, from, composedFrom: t.capabilitiesFrom?.[name] ?? null, dir,
|
|
188
193
|
settings: obj(t.payloads[name]) ? t.payloads[name] : {}, declares: declaredSettings(m), compatibility: compatibilityRow(name, m), missingRequires: missing, operations };
|
|
189
194
|
});
|
|
190
195
|
}
|
|
@@ -207,6 +212,9 @@ export function inspectDocument(t, { kernel }) {
|
|
|
207
212
|
return {
|
|
208
213
|
operationsApi: INSPECT_OPERATIONS_API, kernel, subject: t.subject, workspace: t.workspace,
|
|
209
214
|
souls: [soulRow(t)], layers, capabilities,
|
|
215
|
+
// The capabilities the soul turned off (feature desktop-facts): its `off` over a workspace/team entry
|
|
216
|
+
// (reason "off"), or a `<slot>: none` that dropped a workspace default (reason "slot-none", slot).
|
|
217
|
+
capabilitiesOff: (t.turnedOff || []).map(({ name, reason, slot, overrides }) => ({ id: name, off: true, from: "soul", reason, ...(slot ? { slot } : {}), overrides })),
|
|
210
218
|
// Teams contract item 7 (feature `teams`): the ELIGIBLE teams, one per soul label in order —
|
|
211
219
|
// live for a home (`teamsSource: "recorded"` when its workspace could not be read now).
|
|
212
220
|
teams: t.teams ?? null, teamsSource: t.teamsSource ?? null,
|
|
@@ -234,7 +242,7 @@ const item = (subject, status, { required = true, reason = null, producer = "ker
|
|
|
234
242
|
function providerEnv(t, capability, settings) {
|
|
235
243
|
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^(OATS_|OAS_|PI_)/.test(k)));
|
|
236
244
|
Object.assign(env, teamEnv({ workspace: { key: t.workspace.key, name: t.workspace.name, deployment: t.deployment, team: t.soul.team, slots: t.slots }, payloads: t.payloads, teams: t.teams, teamsSource: t.teamsSource }));
|
|
237
|
-
return Object.assign(env, { OATS_CAPABILITY: capability, OATS_SETTINGS: JSON.stringify(settings), OATS_CLI_BIN: CLI_BIN, OATS_WORKSPACE: t.deployment,
|
|
245
|
+
return Object.assign(env, { OATS_CAPABILITY: capability, OATS_SETTINGS: JSON.stringify(settings), OATS_SETTINGS_ORIGINS: JSON.stringify(t.payloadOrigins?.[capability] ?? {}), OATS_CLI_BIN: CLI_BIN, OATS_WORKSPACE: t.deployment,
|
|
238
246
|
...(t.home ? { OATS_INSTANCE: t.meta.instance, OATS_INSTANCE_HOME: t.home } : {}), OATS_AGENT: t.soul.name, ...(t.soul.soulDir ? { OATS_SOUL: t.soul.soulDir } : {}) });
|
|
239
247
|
}
|
|
240
248
|
/** An executable inside a module directory: both sides realpath'd (a symlink out of
|