@awebai/oats 0.28.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/oats.mjs +298 -107
- 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 +277 -15
- 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/release-notes/v0.29.1.md +79 -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-git.mjs +30 -1
- 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
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 });
|
|
@@ -1589,6 +1555,10 @@ export function runLifecycleHooks(event, { home, instance, agentName, soulDir, s
|
|
|
1589
1555
|
// a caller's extraEnv: those may point at a different executable.
|
|
1590
1556
|
OATS_CLI_BIN: realpathSync(join(PKG_ROOT, "bin", "oats.mjs")),
|
|
1591
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 || {}),
|
|
1592
1562
|
OATS_META: JSON.stringify(priorMeta[cap.id] || {}),
|
|
1593
1563
|
},
|
|
1594
1564
|
}).trim();
|
|
@@ -1605,7 +1575,7 @@ export function runLifecycleHooks(event, { home, instance, agentName, soulDir, s
|
|
|
1605
1575
|
// A launch hook's run is recorded even when its answer is empty: an
|
|
1606
1576
|
// empty answer replaces what the provider contributed before.
|
|
1607
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)) {
|
|
1608
|
-
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() : [] });
|
|
1609
1579
|
}
|
|
1610
1580
|
if (o.env !== undefined) {
|
|
1611
1581
|
if (event !== "spawn" && event !== "launch") throw new HookEnvironmentContractError(`${cap.id} hook env is supported only for spawn and launch, not ${event}`);
|
|
@@ -1694,91 +1664,6 @@ export function findAgentAt(root, name, soulDir) {
|
|
|
1694
1664
|
return agent;
|
|
1695
1665
|
}
|
|
1696
1666
|
|
|
1697
|
-
/** Canonical capability-defined agents: a manifest's `agents: ["agents/reviewer"]`
|
|
1698
|
-
* entries are package-relative soul directories (soul.yaml + AGENTS.md directly
|
|
1699
|
-
* inside). They resolve when the capability is ACTIVE in the
|
|
1700
|
-
* context; the soul stays read-only in the package (fresh identity every spawn —
|
|
1701
|
-
* no long-term memory), while instances home under <root>/<name>/instances/ (the
|
|
1702
|
-
* agent dir holds only instances/). */
|
|
1703
|
-
function capabilityAgentMetadata(manifest, rel) {
|
|
1704
|
-
const soulDir = manifestPath(manifest, rel);
|
|
1705
|
-
const soulFile = manifestPath(manifest, join(rel, "soul.yaml"));
|
|
1706
|
-
if (!soulDir || !soulFile) return undefined;
|
|
1707
|
-
// Read only contained identity metadata to decide whether this provider owns
|
|
1708
|
-
// the requested name. Full tree containment + trust happen after a match.
|
|
1709
|
-
const soul = soulHarnessField(stripInternalAnnotations(withConfigFile(soulFile, () => parseYamlFlat(readFileSync(soulFile, "utf8")))), soulFile);
|
|
1710
|
-
return { soulDir, soul, name: soul.name || basename(soulDir) };
|
|
1711
|
-
}
|
|
1712
|
-
/**
|
|
1713
|
-
* Workspace model: a capability-defined agent (a package's `agents:` soul, e.g.
|
|
1714
|
-
* OKF's memory-harvest worker) resolves from a MATERIALIZED module — the copy the
|
|
1715
|
-
* requesting instance already carries under <home>/.oats/modules/<cap>/. Looks in
|
|
1716
|
-
* `anchorHome` first (the --parent / --relative-to instance), then every instance
|
|
1717
|
-
* home under `root`; the first module declaring an agent of that name wins.
|
|
1718
|
-
* → the same shape findCapabilityAgent returns, plus `_manifestSource` (the home
|
|
1719
|
-
* whose modules supplied it) so skill/inject lookups read that copy.
|
|
1720
|
-
*/
|
|
1721
|
-
export function findModuleCapabilityAgent(root, name, { anchorHome = null } = {}) {
|
|
1722
|
-
if (typeof name !== "string" || !name) return undefined;
|
|
1723
|
-
const homes = [];
|
|
1724
|
-
if (anchorHome) homes.push(anchorHome);
|
|
1725
|
-
if (root && existsSync(root)) {
|
|
1726
|
-
for (const a of readdirSync(root, { withFileTypes: true })) {
|
|
1727
|
-
if (!a.isDirectory() || a.name.startsWith(".")) continue;
|
|
1728
|
-
const inst = join(root, a.name, "instances");
|
|
1729
|
-
if (!existsSync(inst)) continue;
|
|
1730
|
-
for (const i of readdirSync(inst, { withFileTypes: true })) if (i.isDirectory() && !i.name.startsWith(".")) homes.push(join(inst, i.name));
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
const seen = new Set();
|
|
1734
|
-
const failures = [];
|
|
1735
|
-
for (const home of homes) {
|
|
1736
|
-
const real = (() => { try { return realpathSync(home); } catch { return null; } })();
|
|
1737
|
-
if (!real || seen.has(real)) continue;
|
|
1738
|
-
seen.add(real);
|
|
1739
|
-
const modules = instanceModulesRoot(real);
|
|
1740
|
-
if (!modules) continue;
|
|
1741
|
-
for (const [id, manifest] of Object.entries(capabilityManifests(real))) {
|
|
1742
|
-
for (const rel of manifest?.agents || []) {
|
|
1743
|
-
let meta;
|
|
1744
|
-
try { meta = capabilityAgentMetadata(manifest, rel); } catch { continue; }
|
|
1745
|
-
if (!meta || meta.name !== name) continue;
|
|
1746
|
-
try {
|
|
1747
|
-
assertCapabilityTreeContained(manifest, meta.soulDir, "agent");
|
|
1748
|
-
return {
|
|
1749
|
-
...meta.soul, name,
|
|
1750
|
-
kind: "capability", capability: id,
|
|
1751
|
-
_dir: join(root, name),
|
|
1752
|
-
_soulDir: meta.soulDir,
|
|
1753
|
-
_manifestSource: real,
|
|
1754
|
-
_module: manifest._module,
|
|
1755
|
-
};
|
|
1756
|
-
} catch (e) { failures.push(e); }
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
if (failures.length) throw failures[0];
|
|
1761
|
-
return undefined;
|
|
1762
|
-
}
|
|
1763
|
-
/**
|
|
1764
|
-
* A capability-defined agent from ONE capability directory (a package tree the
|
|
1765
|
-
* workspace resolver fetched into the deployment's module store). The manifest is
|
|
1766
|
-
* read from `dir`, trusted by construction (the workspace declares the package; the lock pins it), and
|
|
1767
|
-
* the agent record points its skill/inject lookups at that store entry.
|
|
1768
|
-
*/
|
|
1769
|
-
export function capabilityAgentFromDir(dir, name, root, { module = null } = {}) {
|
|
1770
|
-
const manifest = loadManifestAt(dir, `module:${dir}`);
|
|
1771
|
-
if (!manifest) return undefined;
|
|
1772
|
-
manifest._module = module;
|
|
1773
|
-
for (const rel of manifest.agents || []) {
|
|
1774
|
-
let meta;
|
|
1775
|
-
try { meta = capabilityAgentMetadata(manifest, rel); } catch { continue; }
|
|
1776
|
-
if (!meta || meta.name !== name) continue;
|
|
1777
|
-
assertCapabilityTreeContained(manifest, meta.soulDir, "agent");
|
|
1778
|
-
return { ...meta.soul, name, kind: "capability", capability: manifest.capability, _dir: join(root, name), _soulDir: meta.soulDir, _manifestSource: dir, _manifest: manifest, _module: module };
|
|
1779
|
-
}
|
|
1780
|
-
return undefined;
|
|
1781
|
-
}
|
|
1782
1667
|
export function listAgents(root) {
|
|
1783
1668
|
const agents = [];
|
|
1784
1669
|
const scan = (base, kind) => {
|
|
@@ -1906,11 +1791,20 @@ const nameTooLong = (name, remedy) => oatsError("E_INSTANCE_NAME_INVALID", `inst
|
|
|
1906
1791
|
* final name, suffix included, is capped at MAX_INSTANCE_NAME. */
|
|
1907
1792
|
function nextInstanceName(root, agent, purpose, prepared) {
|
|
1908
1793
|
const n = deriveInstanceName(root, agent, purpose, prepared);
|
|
1909
|
-
if (n.length
|
|
1910
|
-
|
|
1911
|
-
|
|
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.
|
|
1912
1805
|
function deriveInstanceName(root, agent, purpose, prepared) {
|
|
1913
|
-
const
|
|
1806
|
+
const stem = slug(agent.name);
|
|
1807
|
+
const base = purpose ? `${stem}-${slug(purpose)}` : undefined;
|
|
1914
1808
|
const instancesDir = join(agent._dir, "instances");
|
|
1915
1809
|
const own = existsSync(instancesDir) ? readdirSync(instancesDir) : [];
|
|
1916
1810
|
const instances = deploymentInstanceHomes(root), souls = deploymentSoulNames(root, prepared);
|
|
@@ -1921,7 +1815,7 @@ function deriveInstanceName(root, agent, purpose, prepared) {
|
|
|
1921
1815
|
return n;
|
|
1922
1816
|
}
|
|
1923
1817
|
let i = own.length + 1, n;
|
|
1924
|
-
do { n = `${
|
|
1818
|
+
do { n = `${stem}-${i++}`; } while (taken(n));
|
|
1925
1819
|
return n;
|
|
1926
1820
|
}
|
|
1927
1821
|
|
|
@@ -1981,6 +1875,19 @@ export function resolveClaudeBinary(contextDir) {
|
|
|
1981
1875
|
* claude takes aliases/bare claude-* ids only; nothing usable → "" (claude default).
|
|
1982
1876
|
* codex: translate openai/openai-codex entries to native ids, otherwise use its default.
|
|
1983
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
|
+
|
|
1984
1891
|
export function resolveModelPreference(model, harness = "pi") {
|
|
1985
1892
|
const prefs = String(model || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1986
1893
|
if (harness === "codex") {
|
|
@@ -2553,7 +2460,7 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
2553
2460
|
if (o.name !== undefined) instance = explicitInstanceName(o.name);
|
|
2554
2461
|
else {
|
|
2555
2462
|
instance = o.instance || nextInstanceName(root, agent, o.purpose, o.prepared);
|
|
2556
|
-
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)}`;
|
|
2557
2464
|
instance = slug(instance);
|
|
2558
2465
|
if (instance.length > MAX_INSTANCE_NAME) throw nameTooLong(instance, "pass a shorter instance name");
|
|
2559
2466
|
}
|
|
@@ -2815,7 +2722,9 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
2815
2722
|
}
|
|
2816
2723
|
|
|
2817
2724
|
const home = join(agent._dir, "instances", instance);
|
|
2818
|
-
|
|
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 });
|
|
2819
2728
|
// AUTHORITATIVE placement check, on the DESTINATION rather than on lexical
|
|
2820
2729
|
// paths, immediately before the first side effect. The earlier root/agent-dir
|
|
2821
2730
|
// checks are lexical and can be walked around by a symlink anywhere along the
|
|
@@ -2857,7 +2766,7 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
2857
2766
|
// reads the soul, config chain and capability content), so resolving it here
|
|
2858
2767
|
// lets every "declared but missing" failure happen with zero side effects to
|
|
2859
2768
|
// roll back — no home, no worktree, no identity, no tmux window.
|
|
2860
|
-
//
|
|
2769
|
+
// A workspace soul carries _soulDir (its per-commit soul cache: findAgentAt).
|
|
2861
2770
|
const soulDir = agent._soulDir || soulOf(agent._dir);
|
|
2862
2771
|
const composition = composeInstanceAgentsMd(soulDir, repoAbs, agent.name, work, agent.kind, o.prepared);
|
|
2863
2772
|
const resolvedCfg = composition.resolved;
|
|
@@ -3041,13 +2950,7 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
3041
2950
|
materializeOutcome = yield () => (o.materialize ?? materializePreparedDefault)({ ...o.prepared, soulAgentsMd: composition.text, soulDir }, home);
|
|
3042
2951
|
const rows = (typeof o.prepared.toCapabilityRows === "function" ? o.prepared.toCapabilityRows : toCapabilityRows)(o.prepared.resolution, home);
|
|
3043
2952
|
if (!Array.isArray(rows)) throw new Error("toCapabilityRows returned no rows");
|
|
3044
|
-
|
|
3045
|
-
// (lead decision c3 Q1: a harvester never registers as a knowledge source or
|
|
3046
|
-
// gets a messaging identity). Its rows keep everything else.
|
|
3047
|
-
for (const row of rows) {
|
|
3048
|
-
if (agent.kind === "capability") { row.hooks = {}; row.requiredHooks = []; }
|
|
3049
|
-
else row.hooks = materializedHookCommands(row, home);
|
|
3050
|
-
}
|
|
2953
|
+
for (const row of rows) row.hooks = materializedHookCommands(row, home);
|
|
3051
2954
|
resolvedCfg.capabilities = rows;
|
|
3052
2955
|
// S1: the capability blocks materialize appended are part of the composed
|
|
3053
2956
|
// instructions. Read the markers back from the AGENTS.md that was WRITTEN
|
|
@@ -3523,7 +3426,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3523
3426
|
const moduleSkills = (materializeOutcome?.skills || []).map((row) => ({ name: row.name, source: `module:${row.module}`, from: join(home, row.from) }));
|
|
3524
3427
|
const meta = {
|
|
3525
3428
|
agent: agent.name, kind: agent.kind || "persistent", instance, home, soulDir: homeSoulTarget,
|
|
3526
|
-
repo: repoAbs, work, branch, harness, model: model || undefined,
|
|
3429
|
+
repo: repoAbs, work, branch, harness, model: model || undefined, modelFrom: modelFromOf(launchSelection.modelSource, { at: "spawn" }) ?? undefined,
|
|
3527
3430
|
...(yolo !== undefined ? { yolo } : {}),
|
|
3528
3431
|
parentInstance: parentInstance && parentInstance !== instance ? parentInstance : undefined,
|
|
3529
3432
|
siblingInstance: siblingInstance && siblingInstance !== instance ? siblingInstance : undefined,
|
|
@@ -3541,7 +3444,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3541
3444
|
capabilityMeta: Object.keys(hookRes.meta).length ? hookRes.meta : undefined,
|
|
3542
3445
|
capabilities: resolvedCfg.capabilities.map((cap) => ({
|
|
3543
3446
|
id: cap.id, layer: cap.layer, command: cap.command, origin: cap.origin, level: cap.level,
|
|
3544
|
-
settings: cap.settings, provenance: cap.provenance, skills: cap.skills || [],
|
|
3447
|
+
settings: cap.settings, settingsOrigins: cap.settingsOrigins ?? {}, provenance: cap.provenance, skills: cap.skills || [],
|
|
3545
3448
|
hooks: Object.keys(cap.hooks || {}), trusted: !!cap.trust?.trusted,
|
|
3546
3449
|
...(cap.environment?.length ? { environment: [...cap.environment] } : {}),
|
|
3547
3450
|
...(cap.environmentNamespaces?.length ? { environmentNamespaces: [...cap.environmentNamespaces] } : {}),
|
|
@@ -3586,7 +3489,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3586
3489
|
},
|
|
3587
3490
|
},
|
|
3588
3491
|
capabilityRuntime: resolvedCfg.capabilities.map((cap) => ({
|
|
3589
|
-
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 ?? {},
|
|
3590
3493
|
hooks: cap.hooks, requiredHooks: cap.requiredHooks, environment: cap.environment, environmentNamespaces: cap.environmentNamespaces,
|
|
3591
3494
|
missingRequires: cap.missingRequires, trust: cap.trust,
|
|
3592
3495
|
executable: cap.executable,
|
|
@@ -3748,6 +3651,12 @@ export function listInstances(root, tmuxSession = DEFAULT_TMUX_SESSION) {
|
|
|
3748
3651
|
catch (error) { liveness = { running: null, runtimeState: "unreachable", runtimeError: error.message }; }
|
|
3749
3652
|
}
|
|
3750
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 };
|
|
3751
3660
|
// The home IS the directory enumerated here, and the instance its name:
|
|
3752
3661
|
// a file inside it cannot relocate or rename itself in the roster (every
|
|
3753
3662
|
// consumer acting on `home` — retire, inspect --home, the Desktop's file
|
|
@@ -3757,7 +3666,7 @@ export function listInstances(root, tmuxSession = DEFAULT_TMUX_SESSION) {
|
|
|
3757
3666
|
...(meta.home !== undefined && meta.home !== home ? { recordedHome: meta.home } : {}),
|
|
3758
3667
|
...(meta.instance !== undefined && meta.instance !== e.name ? { recordedInstance: meta.instance } : {}),
|
|
3759
3668
|
};
|
|
3760
|
-
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 } : {}) };
|
|
3761
3670
|
|
|
3762
3671
|
});
|
|
3763
3672
|
};
|
|
@@ -3944,7 +3853,7 @@ function quarantineInstanceHome({ home, instance, agent, soulDir, soulId, incomp
|
|
|
3944
3853
|
...(sessionTarget ? { sessionTarget } : {}),
|
|
3945
3854
|
outstanding: { hooks: [...outstandingHooks], git: [...outstandingGit], ...(directoryPreservation ? { directory: true } : {}) },
|
|
3946
3855
|
capabilityRuntime: (resolvedCfg.capabilities || []).map((cap) => ({
|
|
3947
|
-
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 ?? {},
|
|
3948
3857
|
hooks: cap.hooks, requiredHooks: cap.requiredHooks, environment: cap.environment, environmentNamespaces: cap.environmentNamespaces,
|
|
3949
3858
|
missingRequires: cap.missingRequires,
|
|
3950
3859
|
trust: cap.trust, executable: cap.executable,
|
|
@@ -4541,7 +4450,9 @@ export function capturedProviders(meta, frozen) {
|
|
|
4541
4450
|
return ids.map((id) => {
|
|
4542
4451
|
const contribution = contributions.find((c) => c.capability === id) || null;
|
|
4543
4452
|
const binding = bindings.find((b) => b.id === id) || null;
|
|
4544
|
-
|
|
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 };
|
|
4545
4456
|
});
|
|
4546
4457
|
}
|
|
4547
4458
|
/** Capability contributions for a start of an existing home: the recorded
|
|
@@ -4569,7 +4480,7 @@ export function prepareLaunchHooks({ frozen, harness, resolvedCfg, home, meta, c
|
|
|
4569
4480
|
const trust = capabilityTrust(manifest);
|
|
4570
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`);
|
|
4571
4482
|
const hooks = manifestHookCommands(manifest);
|
|
4572
|
-
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: [] });
|
|
4573
4484
|
}
|
|
4574
4485
|
if (withLaunchHook.length) {
|
|
4575
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 } });
|
|
@@ -4671,7 +4582,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4671
4582
|
// The independent receipt first (retire and session consult it), then the
|
|
4672
4583
|
// mutable metadata; both tmp+rename. A failure between them is what the
|
|
4673
4584
|
// pending receipt exists for.
|
|
4674
|
-
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) => {
|
|
4675
4586
|
checkRoots();
|
|
4676
4587
|
const baselinePath = retirementBaselinePath(realHome);
|
|
4677
4588
|
let baseline;
|
|
@@ -4685,7 +4596,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4685
4596
|
const recorded = meta.startId === id;
|
|
4686
4597
|
const restarts = (Array.isArray(meta.restarts) ? meta.restarts : []).slice(recorded ? -20 : -19);
|
|
4687
4598
|
if (!recorded) restarts.push({ startedAt, model: model ?? null, reused });
|
|
4688
|
-
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 } : {}),
|
|
4689
4600
|
...(launch ? { launch } : {}), ...(newHarness ? { harness: newHarness } : {}), ...(newYolo !== undefined ? { yolo: newYolo } : {}),
|
|
4690
4601
|
// Launch-hook meta lands per capability over the spawn's record; a hook
|
|
4691
4602
|
// that answered without meta keeps its previous entry (retire reads it).
|
|
@@ -4770,7 +4681,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4770
4681
|
// preflight happens here, before anything is observed or stopped.
|
|
4771
4682
|
const selected = o.launchConfig !== undefined || o.harness !== undefined || o.yolo !== undefined;
|
|
4772
4683
|
const hasRecipe = meta.launch && typeof meta.launch === "object";
|
|
4773
|
-
let launchPlan = null;
|
|
4684
|
+
let launchPlan = null, explicitModelFrom = null;
|
|
4774
4685
|
if (selected || hasRecipe) {
|
|
4775
4686
|
// Every start of a home with a recipe (ordinary, model-only, or under a
|
|
4776
4687
|
// selection) goes through the one planner: recipe shape, the recorded
|
|
@@ -4783,13 +4694,13 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4783
4694
|
const resolvedCfg = resolvedFromHome(realHome, meta, { teams: o.teams, teamsSource: o.teamsSource });
|
|
4784
4695
|
let agent; try { agent = findAgent(dirname(dirname(dirname(realHome))), meta.agent); } catch { agent = undefined; }
|
|
4785
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 });
|
|
4786
|
-
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 } : {}) };
|
|
4787
4698
|
command = launchPlan.command; model = launchPlan.model;
|
|
4788
4699
|
} else if (o.model !== undefined && o.model !== null && String(o.model).trim() !== "") {
|
|
4789
4700
|
const resolved = resolveModelPreference(String(o.model), harness);
|
|
4790
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`);
|
|
4791
4702
|
command = withLaunchModel(command, resolved);
|
|
4792
|
-
model = resolved;
|
|
4703
|
+
model = resolved; explicitModelFrom = "start";
|
|
4793
4704
|
} else parseLaunchCommand(command);
|
|
4794
4705
|
// References recorded for this home must resolve on this host on every
|
|
4795
4706
|
// start path, and the source variables go to the pane, not the command.
|
|
@@ -4799,8 +4710,8 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4799
4710
|
const paneEnvFlags = paneEnv.flatMap((r) => ["-e", `${r.name}=${r.value}`]);
|
|
4800
4711
|
const paneEnvExports = paneEnv.map((r) => `export ${r.name}=${shq(r.value)}; `).join("");
|
|
4801
4712
|
checkRoots(); // launch hooks/preparation have run; no backend has been observed
|
|
4802
|
-
const planExtra = launchPlan ? { launch: launchPlan.recipe, harness: launchPlan.harness, yolo: launchPlan.yolo,
|
|
4803
|
-
...(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 } : {});
|
|
4804
4715
|
let target = receipt.target;
|
|
4805
4716
|
let state = { present: false, state: "not-launched" };
|
|
4806
4717
|
let serverGone = false;
|
package/lib/instance-git.mjs
CHANGED
|
@@ -138,6 +138,33 @@ function remoteOf(work, branch) {
|
|
|
138
138
|
if (!url) return null;
|
|
139
139
|
return { name, url, ...parseRemoteUrl(url), source: configured && configured !== "." ? "branch-upstream" : "origin" };
|
|
140
140
|
}
|
|
141
|
+
/** Per-file line counts (0.29.1), from ONE `git diff <captured HEAD> --numstat -z -M`: the working tree
|
|
142
|
+
* against the observed commit, staged and unstaged combined. That is the same baseline as the status letters
|
|
143
|
+
* and `oats instance diff`. An unborn tree counts against the empty tree (`hash-object` without -w writes nothing).
|
|
144
|
+
* → Map path → { additions, deletions, binary }, keyed by the NEW path for a rename; null when git failed.
|
|
145
|
+
* -z records: "A\tD\tpath\0", or "A\tD\t\0old\0new\0" for a rename; "-\t-" is a binary file. */
|
|
146
|
+
function lineCounts(work, headOid) {
|
|
147
|
+
const against = headOid ?? trim(git(work, ["hash-object", "-t", "tree", "/dev/null"], { allowFail: true }));
|
|
148
|
+
if (!against) return null;
|
|
149
|
+
const raw = git(work, ["diff", against, "--numstat", "-z", "-M", "--"], { allowFail: true });
|
|
150
|
+
if (raw === null) return null;
|
|
151
|
+
const counts = new Map(), recs = raw.split("\0");
|
|
152
|
+
for (let i = 0; i < recs.length;) {
|
|
153
|
+
const m = /^(-|\d+)\t(-|\d+)\t([\s\S]*)$/.exec(recs[i++]);
|
|
154
|
+
if (!m) continue;
|
|
155
|
+
let path = m[3];
|
|
156
|
+
if (path === "") { i++; path = recs[i++]; } // a rename: skip the old path, key the new one
|
|
157
|
+
if (typeof path !== "string" || !path) continue;
|
|
158
|
+
counts.set(path, m[1] === "-" ? { additions: null, deletions: null, binary: true } : { additions: Number(m[1]), deletions: Number(m[2]), binary: false });
|
|
159
|
+
}
|
|
160
|
+
return counts;
|
|
161
|
+
}
|
|
162
|
+
/** An entry's counts: untracked (no baseline; contents are not read) and submodules have none (all null). */
|
|
163
|
+
function countsOf(counts, entry) {
|
|
164
|
+
if (entry.kind === "untracked" || entry.submodule) return { additions: null, deletions: null, binary: null };
|
|
165
|
+
return counts?.get(entry.path) ?? { additions: null, deletions: null, binary: null };
|
|
166
|
+
}
|
|
167
|
+
|
|
141
168
|
function fileId(revision, indexOid, entry) {
|
|
142
169
|
return createHash("sha256").update(`${revision}\0${indexOid}\0${entry.kind}\0${entry.path}\0${entry.origPath ?? ""}`).digest("hex").slice(0, 24);
|
|
143
170
|
}
|
|
@@ -164,7 +191,8 @@ export function observeInstanceGit(home) {
|
|
|
164
191
|
const counts = mergeBase ? countRange(work, `${base.ref}...HEAD`) : null;
|
|
165
192
|
baseComparison = { ref: base.ref, source: base.source, mergeBase, ahead: counts ? counts.right : null, behind: counts ? counts.left : null };
|
|
166
193
|
}
|
|
167
|
-
const
|
|
194
|
+
const counts = lineCounts(work, headOid);
|
|
195
|
+
const files = entries.filter((e) => e.kind !== "ignored").map((e) => ({ id: fileId(revision, indexOid, e), ...e, ...countsOf(counts, e) }));
|
|
168
196
|
const summary = { changed: 0, renamed: 0, copied: 0, unmerged: 0, untracked: 0 };
|
|
169
197
|
for (const f of files) summary[f.kind]++;
|
|
170
198
|
return {
|
|
@@ -178,6 +206,7 @@ export function observeInstanceGit(home) {
|
|
|
178
206
|
notes: [
|
|
179
207
|
...(upstream.ref === null ? ["no upstream configured: upstream ahead/behind are unknown, not zero"] : []),
|
|
180
208
|
...(baseComparison.ref === null ? ["no default branch found (origin/HEAD, origin/main, origin/master, main, master): base comparison unknown"] : []),
|
|
209
|
+
...(counts === null ? ["line counts unavailable (git diff --numstat failed): additions/deletions are unknown, not zero"] : []),
|
|
181
210
|
],
|
|
182
211
|
};
|
|
183
212
|
}
|
package/lib/instance-inspect.mjs
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|