@awebai/oats 0.27.1 → 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.
Files changed (39) hide show
  1. package/bin/oats.mjs +185 -26
  2. package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +8 -3
  3. package/capabilities/oats-okf/bin/oats-okf.mjs +33 -28
  4. package/capabilities/oats-okf/injects/okf.md +29 -21
  5. package/capabilities/oats-okf/lib/config.mjs +5 -1
  6. package/capabilities/oats-okf/lib/consult.mjs +500 -0
  7. package/capabilities/oats-okf/lib/inspection.mjs +11 -3
  8. package/capabilities/oats-okf/lib/io.mjs +9 -2
  9. package/capabilities/oats-okf/lib/sources.mjs +15 -53
  10. package/capabilities/oats-okf/lib/stores.mjs +10 -7
  11. package/capabilities/oats-okf/lib/worker.mjs +9 -1
  12. package/capabilities/oats-okf/oats.json +13 -4
  13. package/capabilities/oats-okf/skills/okf/SKILL.md +14 -6
  14. package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +142 -0
  15. package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +86 -0
  16. package/docs/capabilities.md +3 -1
  17. package/docs/design/2026-09-24-phase-d-plan.md +11 -0
  18. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +241 -0
  19. package/docs/design/2026-09-26-okf-knowledge-operations.md +389 -0
  20. package/docs/desktop-cli-api.md +99 -7
  21. package/docs/oats-local.schema.json +2 -1
  22. package/docs/oats-package.schema.json +39 -0
  23. package/docs/packages.md +67 -3
  24. package/docs/release-notes/v0.27.2.md +51 -0
  25. package/docs/release-notes/v0.28.0.md +144 -0
  26. package/docs/schedules.md +99 -1
  27. package/docs/souls-and-instances.md +19 -3
  28. package/docs/workspaces.md +8 -2
  29. package/lib/core.mjs +124 -10
  30. package/lib/instance-inspect.mjs +4 -4
  31. package/lib/instance-resolution.mjs +62 -18
  32. package/lib/materialize.mjs +13 -0
  33. package/lib/packages.mjs +90 -6
  34. package/lib/resolve.mjs +20 -2
  35. package/lib/schedule.mjs +24 -11
  36. package/lib/triggers.mjs +545 -0
  37. package/lib/workspace.mjs +80 -3
  38. package/package-catalog.json +1 -1
  39. 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
- return { workspace, key: wsObs.key, url: wsObs.url, commit: wsObs.commit, observedAt: wsObs.observedAt, local: local ?? null, members, external: externalRows, problems, warnings: [...unmappedLabelWarnings(workspace, members, externalRows, teams), ...privateSoulWarnings([...members.filter((m) => m.confirmed).flatMap((m) => m.souls), ...externalRows.map((e) => e.soul)])] };
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
@@ -3,7 +3,7 @@
3
3
  "packages": {
4
4
  "oats.okf": {
5
5
  "url": "https://github.com/awebai/oats-okf.git",
6
- "ref": "v2.1.5",
6
+ "ref": "v3.0.0",
7
7
  "path": "oats-package"
8
8
  },
9
9
  "oats.aweb": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.27.1",
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",