@awebai/oats 0.27.2 → 0.28.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 +185 -26
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +8 -3
- package/capabilities/oats-okf/bin/oats-okf.mjs +33 -28
- package/capabilities/oats-okf/injects/okf.md +29 -21
- package/capabilities/oats-okf/lib/config.mjs +5 -1
- package/capabilities/oats-okf/lib/consult.mjs +500 -0
- package/capabilities/oats-okf/lib/inspection.mjs +11 -3
- package/capabilities/oats-okf/lib/io.mjs +9 -2
- package/capabilities/oats-okf/lib/sources.mjs +15 -53
- package/capabilities/oats-okf/lib/stores.mjs +10 -7
- package/capabilities/oats-okf/lib/worker.mjs +9 -1
- package/capabilities/oats-okf/oats.json +13 -4
- package/capabilities/oats-okf/skills/okf/SKILL.md +14 -6
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +142 -0
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +86 -0
- 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 +93 -7
- package/docs/oats-local.schema.json +2 -1
- package/docs/oats-package.schema.json +39 -0
- package/docs/packages.md +67 -3
- package/docs/release-notes/v0.28.0.md +144 -0
- package/docs/schedules.md +99 -1
- package/docs/souls-and-instances.md +7 -3
- package/docs/workspaces.md +8 -2
- package/lib/core.mjs +22 -4
- package/lib/instance-inspect.mjs +4 -4
- package/lib/instance-resolution.mjs +62 -18
- package/lib/materialize.mjs +13 -0
- package/lib/packages.mjs +90 -6
- package/lib/resolve.mjs +20 -2
- package/lib/schedule.mjs +24 -11
- package/lib/triggers.mjs +545 -0
- package/lib/workspace.mjs +80 -3
- package/package-catalog.json +1 -1
- package/package.json +1 -1
package/lib/workspace.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
15
15
|
import { parseConfigData } from "./config-data.mjs";
|
|
16
16
|
import { oatsError } from "./errors.mjs";
|
|
17
17
|
import * as defaultRemote from "./remote.mjs";
|
|
18
|
-
import { bindRemote, classifyPackageValue } from "./packages.mjs";
|
|
18
|
+
import { bindRemote, classifyPackageValue, lockedPackageRef } from "./packages.mjs";
|
|
19
19
|
import { manifestContractProblems } from "./capability-contract.mjs";
|
|
20
20
|
|
|
21
21
|
/* ───────────────────────────── errors ─────────────────────────────────── */
|
|
@@ -612,6 +612,75 @@ export async function discoverRepo(ref, { at, remote: injected, remoteOptions }
|
|
|
612
612
|
return { key: obs.key, commit: obs.commit, membership, ...items, problems: [...problems, ...items.problems] };
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
/** The agents-root directory (and agent name) of a package soul: `<package>--<soul>`, the package id
|
|
616
|
+
* with every character outside [a-z0-9-] as `-` (`oats.okf/knowledge-maintainer` →
|
|
617
|
+
* `oats-okf--knowledge-maintainer`). A soul name is a slug and never holds `--`, so this can never be
|
|
618
|
+
* a member soul's directory; instances are named by the slug of it (`oats-okf-knowledge-maintainer-<purpose>`). */
|
|
619
|
+
export const PACKAGE_SOUL_SEPARATOR = "--";
|
|
620
|
+
export function packageSoulAgentName(id, soul) { return `${String(id).replace(/[^a-z0-9-]/g, "-")}${PACKAGE_SOUL_SEPARATOR}${soul}`; }
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Package souls (0.28.0): the souls the lock records for each package the workspace STILL declares,
|
|
624
|
+
* read at the locked commit. A package soul is listed like a member soul, with its package origin
|
|
625
|
+
* (`package`, `version`) and its qualified name `<package>/<soul>`; its labels are its soul.yaml's
|
|
626
|
+
* own `team` (a package has no membership default) and each must be declared (E_TEAM_UNKNOWN).
|
|
627
|
+
* Nothing here trusts the lock's digests — a spawn verifies the fetched soul against them.
|
|
628
|
+
* `lock` null (or no workspace) → nothing. → { souls: [SoulEntry], problems }
|
|
629
|
+
*/
|
|
630
|
+
export async function discoverPackageSouls(workspace, lock, { remote: injected, remoteOptions } = {}) {
|
|
631
|
+
const souls = [], problems = [];
|
|
632
|
+
if (!isObject(lock?.packages) || !isObject(workspace)) return { souls, problems };
|
|
633
|
+
const remote = remoteOf({ remote: injected, remoteOptions });
|
|
634
|
+
const declared = isObject(workspace.packages) ? workspace.packages : {};
|
|
635
|
+
const teams = isObject(workspace.teams) ? Object.keys(workspace.teams) : [];
|
|
636
|
+
for (const id of Object.keys(lock.packages).sort()) {
|
|
637
|
+
const entry = lock.packages[id];
|
|
638
|
+
if (!Array.isArray(entry?.souls) || entry.souls.length === 0 || !Object.hasOwn(declared, id)) continue;
|
|
639
|
+
const at = (rel) => `package:${id}:${rel}`;
|
|
640
|
+
const problem = (code, path, message) => problems.push({ code, repoKey: null, package: id, path: at(path), message });
|
|
641
|
+
const ref = lockedPackageRef(entry);
|
|
642
|
+
if (!ref) { problem("E_PACKAGE_MISSING", entry.path, `the lock records no url for ${id} — run \`oats sync\``); continue; }
|
|
643
|
+
let key;
|
|
644
|
+
try { key = remote.parseRepoRef(ref).key; }
|
|
645
|
+
catch (e) { if (e?.code === "E_REPO_REF") { problem("E_REPO_REF", entry.path, e.message); continue; } throw e; }
|
|
646
|
+
for (const locked of entry.souls) {
|
|
647
|
+
const path = `${entry.path}/${locked.path}`;
|
|
648
|
+
const file = `${path}/soul.yaml`;
|
|
649
|
+
let bytes;
|
|
650
|
+
try { ({ bytes } = await remote.readRemoteFile(ref, entry.commit, file)); }
|
|
651
|
+
catch (e) {
|
|
652
|
+
if (typeof e?.code === "string" && e.code.startsWith("E_REMOTE_")) { problem(e.code, file, e.message); continue; }
|
|
653
|
+
throw e;
|
|
654
|
+
}
|
|
655
|
+
const read = readDeclaration("soul", bytes, { kind: "soul", repoKey: key, commit: entry.commit, path: file });
|
|
656
|
+
if (read.problems) { for (const p of read.problems) problem("E_WORKSPACE_SCHEMA", `${file}#${p.path}`, `${p.message}${schemaHint("soul", read.value)}`); continue; }
|
|
657
|
+
const definition = read.value;
|
|
658
|
+
if (definition.name !== locked.name) { problem("E_WORKSPACE_SCHEMA", `${file}#/name`, `soul name ${show(definition.name)} does not match its directory ${show(locked.name)}`); continue; }
|
|
659
|
+
const labels = labelList(definition.team) ?? [];
|
|
660
|
+
for (const [label, where] of labelPaths(definition, labels, file)) {
|
|
661
|
+
if (!teams.includes(label)) problems.push({ code: "E_TEAM_UNKNOWN", repoKey: null, package: id, path: at(where), message: `team ${show(label)} is not declared in the workspace's teams (${teams.join(", ") || "none"}) — declare it in oats-workspace.yaml teams: to spawn ${id}/${locked.name}` });
|
|
662
|
+
}
|
|
663
|
+
souls.push({
|
|
664
|
+
name: locked.name, path, repoKey: key, ref, commit: entry.commit, team: labels[0] ?? null, labels, private: false, definition,
|
|
665
|
+
package: id, version: entry.version, qualifiedName: `${id}/${locked.name}`, agentName: packageSoulAgentName(id, locked.name), digest: locked.digest,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Two packages whose ids differ only where the directory name sanitises (`a.b`, `a-b`) and that
|
|
670
|
+
// ship a same-named soul would share one agent directory: both are listed, neither is spawnable.
|
|
671
|
+
const byDir = new Map();
|
|
672
|
+
for (const s of souls) byDir.set(s.agentName, [...(byDir.get(s.agentName) || []), s]);
|
|
673
|
+
for (const [dir, group] of byDir) {
|
|
674
|
+
if (group.length < 2) continue;
|
|
675
|
+
const qualified = group.map((s) => s.qualifiedName).sort();
|
|
676
|
+
for (const s of group) {
|
|
677
|
+
s.collides = qualified;
|
|
678
|
+
problems.push({ code: "E_SOUL_AMBIGUOUS", repoKey: null, package: s.package, path: `package:${s.package}:${s.path}/soul.yaml`, message: `package souls ${qualified.join(" and ")} would share the agent directory agents/${dir}/ — keep one of the packages in packages:` });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return { souls, problems };
|
|
682
|
+
}
|
|
683
|
+
|
|
615
684
|
/**
|
|
616
685
|
* The whole picture in one access context.
|
|
617
686
|
* → { workspace, members: [{ key, commit, confirmed, reason?, team, souls, capabilities, publishes }], external: [...],
|
|
@@ -619,7 +688,7 @@ export async function discoverRepo(ref, { at, remote: injected, remoteOptions }
|
|
|
619
688
|
* `publishes` = { package, version } when the member carries oats-package/oats-package.json (informational:
|
|
620
689
|
* the package's capabilities are NOT member capabilities — non-collapse rule), else null.
|
|
621
690
|
*/
|
|
622
|
-
export async function discoverWorkspace(ref, { at, local, remote: injected, remoteOptions } = {}) {
|
|
691
|
+
export async function discoverWorkspace(ref, { at, local, lock = null, remote: injected, remoteOptions } = {}) {
|
|
623
692
|
const remote = remoteOf({ remote: injected, remoteOptions });
|
|
624
693
|
const wsObs = await observeWorkspace(ref, { at, remote });
|
|
625
694
|
const workspace = wsObs.workspace;
|
|
@@ -671,7 +740,15 @@ export async function discoverWorkspace(ref, { at, local, remote: injected, remo
|
|
|
671
740
|
for (const label of labels) if (!teams.includes(label)) pushProblem("E_TEAM_UNKNOWN", `${file}#/team`, `team ${show(label)} is not declared in the workspace's teams`);
|
|
672
741
|
externalRows.push({ source: entry.source, key, commit, soul: { name: read.value.name, path: entry.soul, repoKey: key, commit, team: labels[0] ?? null, labels, private: false, definition: read.value } });
|
|
673
742
|
}
|
|
674
|
-
|
|
743
|
+
const pkg = await discoverPackageSouls(workspace, lock, { remote });
|
|
744
|
+
problems.push(...pkg.problems);
|
|
745
|
+
return { workspace, key: wsObs.key, url: wsObs.url, commit: wsObs.commit, observedAt: wsObs.observedAt, local: local ?? null, members, external: externalRows, packageSouls: pkg.souls, problems, warnings: workspaceWarnings(workspace, members, externalRows, pkg.souls, teams) };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** The discovery's warnings: unmapped team labels and ignored `private:`, over every listed soul. */
|
|
749
|
+
export function workspaceWarnings(workspace, members, externalRows, packageSouls, teams = isObject(workspace?.teams) ? Object.keys(workspace.teams) : []) {
|
|
750
|
+
const extra = (packageSouls || []).map((soul) => ({ soul }));
|
|
751
|
+
return [...unmappedLabelWarnings(workspace, members, [...externalRows, ...extra], teams), ...privateSoulWarnings([...members.filter((m) => m.confirmed).flatMap((m) => m.souls), ...externalRows.map((e) => e.soul), ...(packageSouls || [])])];
|
|
675
752
|
}
|
|
676
753
|
|
|
677
754
|
/** Teams contract decision 5: a soul label the workspace declares in `teams:` but does not map in
|
package/package-catalog.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|