@awebai/oats 0.25.2 → 0.25.3

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Purpose:** the one accurate view of every work stream in the redesign, what is on main, what is in flight, who owns it, and what blocks it. Lead: `oats-expert` (redesign lead). Updated whenever anything merges, is returned, or reality changes. Older per-lane boards are superseded by this file.
4
4
 
5
- **Last update:** 2026-09-23 20:00Z · **0.25.0 + 0.25.1 PUBLISHED** (workspace model A–C + team-review fixes) · Phase D plan next · **Desktop engineer paused by the human** (10B-0 uncommitted foundation preserved; resume is the human's call; 0.24.14 to be cut from a maintenance branch off `e5cdaf95` when its PR lands) · parity pipeline ⏸.
5
+ **Last update:** 2026-09-23 20:00Z · **0.25.0 + 0.25.1 + 0.25.2 PUBLISHED** (workspace model A–C + team-review fixes + operator-rebuild round) · Phase D plan next · **Desktop engineer paused by the human** (10B-0 uncommitted foundation preserved; resume is the human's call; 0.24.14 to be cut from a maintenance branch off `e5cdaf95` when its PR lands) · parity pipeline ⏸.
6
6
 
7
7
  Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
8
8
 
@@ -564,3 +564,23 @@ in `next.clone` like any member that lacks a clone at the convention (the host
564
564
  is a member; a soul that lives in it may need a work clone). Under an explicit
565
565
  `oats-local.yaml` `standalone:` header the next steps say the view is standalone
566
566
  and list only that repo.
567
+
568
+ ### 0.25.3 — `OATS_SOUL_ID` (stable soul identity for providers)
569
+
570
+ The per-commit soul cache (0.25.1, M1) made `realpath(<home>/soul)` change with every
571
+ member commit; a provider that keyed durable state on that path (OKF 2.1.3 `owners.json`)
572
+ refused the next spawn (`E_OWNER`). "Members are latest" and "the owner is a path" cannot
573
+ both hold, so the kernel now hands hooks a **stable identity**:
574
+
575
+ - `OATS_SOUL_ID` in the `spawn` / `retire` / `launch` hook environment: for a workspace soul
576
+ `<repo key>#<soul name>` exactly as the canonical key is spelled (e.g.
577
+ `github.com/awebai/aweb#aweb-protocol-expert`, local fixtures `local//abs/path.git#name`);
578
+ for a classic soul the realpath of `agents/<name>/soul` (today's value — 0.24 deployments
579
+ unchanged). Also recorded as `instance.json.workspace.soul.id`.
580
+ - `OATS_SOUL` is the **content** the home links — for a workspace soul the per-commit
581
+ directory `agents/<name>/souls/<commit12>/`, never the swappable `agents/<name>/soul`
582
+ pointer. Providers read content from `OATS_SOUL` and key state on `OATS_SOUL_ID`.
583
+ - Provider contract (OKF 2.1.4): `owners[owner] = OATS_SOUL_ID ?? realpath(OATS_SOUL ?? home/soul)`;
584
+ a prior row whose value is a path under `agents/<same soul name>/(soul|souls/<commit>)` is
585
+ migrated to the id once, not refused; any other mismatch stays `E_OWNER`.
586
+
@@ -0,0 +1,19 @@
1
+ # OATS v0.25.3 — `OATS_SOUL_ID`
2
+
3
+ Kernel/Pi **0.25.3**. Tag `v0.25.3` → the commit carrying these notes. No API change;
4
+ `features[]` unchanged; one additive hook environment variable and one additive
5
+ `instance.json` field.
6
+
7
+ ## Kernel
8
+
9
+ - **`OATS_SOUL_ID`** — a soul's stable identity for capability hooks (`spawn`,
10
+ `retire`, `launch`): `<repo key>#<soul name>` for a workspace soul, the realpath of
11
+ `agents/<name>/soul` for a classic soul. Recorded as `instance.json.workspace.soul.id`.
12
+ Fixes the consequence of 0.25.1's per-commit soul cache found by the first outsider
13
+ rebuild: OKF 2.1.3 pins a soul's owner to `realpath(home/soul)`, which now changes with
14
+ every member commit → the next spawn of the same soul was refused `E_OWNER`. The provider
15
+ half (owners keyed by `OATS_SOUL_ID`, one-time migration of path pins) ships in OKF 2.1.4.
16
+ - **`OATS_SOUL` is the content the home links** — for a workspace soul the per-commit
17
+ directory, never the swappable `agents/<name>/soul` pointer (spawn and retire hooks agree).
18
+
19
+ Contract: `docs/design/2026-09-23-workspace-module-contracts.md` § "0.25.3 — OATS_SOUL_ID".
package/lib/core.mjs CHANGED
@@ -640,7 +640,7 @@ const LAUNCH_CONFIG_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
640
640
  const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
641
641
  /** Environment the kernel sets for every launch (identity, home, roots) and
642
642
  * its reference aliases: a configuration may not name them. */
643
- export const RESERVED_LAUNCH_ENV = new Set(["OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "OATS_AGENT", "OATS_SOUL", "OATS_ROOT", "OATS_CONTEXT", "OATS_WORKSPACE", "OATS_EVENT", "OATS_SETTINGS", "OATS_CLI_BIN", "PI_AGENT_INSTANCE", "PI_AGENT_HOME", "PI_AGENTS_ROOT"]);
643
+ 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"]);
644
644
  export const LAUNCH_REF_PREFIX = "OATS_LAUNCH_REF_";
645
645
  const reservedLaunchEnv = (n) => RESERVED_LAUNCH_ENV.has(n) || n.startsWith(LAUNCH_REF_PREFIX);
646
646
  export function validateLaunchConfig(name, entry, where) {
@@ -4806,7 +4806,31 @@ function validateHookEnvironment(capabilityID, value, owners, declarations) {
4806
4806
  return accepted;
4807
4807
  }
4808
4808
 
4809
- export function runLifecycleHooks(event, { home, instance, agentName, soulDir, contextDir, workspaceDir, rootDir, resolved, priorMeta = {}, extraEnv = {}, assertRoots }) {
4809
+ /**
4810
+ * A soul's STABLE identity for capability hooks (OATS_SOUL_ID): what a provider
4811
+ * may key durable state on. Workspace soul: `<repo key>#<soul name>` — it does not
4812
+ * change when the member commits (the per-commit soul directory does) or when the
4813
+ * copy moves. Classic soul: the realpath of agents/<name>/soul, i.e. today's value,
4814
+ * so 0.24 deployments are unchanged. Order: an explicit `soulId` (a prepared spawn),
4815
+ * the home's recorded `instance.json.workspace.soul` (retire/launch of a workspace
4816
+ * home), else the path.
4817
+ */
4818
+ export function stableSoulId({ soulId, home, soulDir, agentName } = {}) {
4819
+ if (typeof soulId === "string" && soulId) return soulId;
4820
+ if (home) {
4821
+ try {
4822
+ const meta = JSON.parse(readFileSync(join(home, "instance.json"), "utf8"));
4823
+ const s = meta?.workspace?.soul;
4824
+ if (typeof s?.id === "string" && s.id) return s.id;
4825
+ if (typeof s?.repoKey === "string" && s.repoKey) return `${s.repoKey}#${meta.agent ?? agentName ?? ""}`;
4826
+ } catch { /* not a workspace home */ }
4827
+ }
4828
+ if (soulDir) { try { return realpathSync(soulDir); } catch { return soulDir; } }
4829
+ return "";
4830
+ }
4831
+ export const workspaceSoulId = (repoKey, name) => `${repoKey}#${name}`;
4832
+
4833
+ export function runLifecycleHooks(event, { home, instance, agentName, soulDir, soulId, contextDir, workspaceDir, rootDir, resolved, priorMeta = {}, extraEnv = {}, assertRoots }) {
4810
4834
  const results = { meta: {}, briefs: [], warnings: [], order: [], launch: {}, env: {}, failures: [], contributions: [] };
4811
4835
  const envOwners = new Map();
4812
4836
  const envDeclarations = new Map((resolved.capabilities || []).map((cap) => [cap.id, { names: new Set(cap.environment || []), namespaces: [...(cap.environmentNamespaces || [])] }]));
@@ -4834,7 +4858,7 @@ export function runLifecycleHooks(event, { home, instance, agentName, soulDir, c
4834
4858
  // the package STORE root — do not conflate them.
4835
4859
  OATS_EVENT: event, OATS_INSTANCE: instance, OATS_INSTANCE_HOME: home, OATS_HOME: home, OATS_AGENT: agentName,
4836
4860
  OATS_CAPABILITY: cap.id, OATS_LAYER: cap.layer || "", OATS_ROOT: rootDir || "",
4837
- OATS_SOUL: soulDir || "", OATS_CONTEXT: contextDir, OATS_WORKSPACE: workspaceDir || "", OATS_LEVEL: cap.level || "",
4861
+ OATS_SOUL: soulDir || "", OATS_SOUL_ID: stableSoulId({ soulId, home, soulDir, agentName }), OATS_CONTEXT: contextDir, OATS_WORKSPACE: workspaceDir || "", OATS_LEVEL: cap.level || "",
4838
4862
  OATS_TEAM_NAME: resolved.team?.name || "", OATS_TEAM_ID: resolved.team?.id || "", OATS_TEAM_SCOPE: resolved.team?.scope || "",
4839
4863
  ...extraEnv,
4840
4864
  // Hooks also run through direct core callers (not only bin/oats).
@@ -6938,8 +6962,12 @@ function* spawnBody(root, agent, o = {}) {
6938
6962
  // Capability lifecycle hooks (spawn) — the knowledge integration scaffolds instance
6939
6963
  // memory (STATE.md/log.md/notes/ are OKF conventions, not kernel ones); the
6940
6964
  // messaging integration mints the comms identity. Kernel stays memory-agnostic.
6965
+ const preparedSoulId = o.prepared ? workspaceSoulId(o.prepared.soulEntry.repoKey, o.prepared.soulEntry.name) : undefined;
6966
+ // Hooks read the soul the HOME links (the per-commit directory for a workspace
6967
+ // soul), never the swappable agents/<name>/soul pointer: a provider that pins a
6968
+ // path must pin this instance's content, and OATS_SOUL_ID is what it keys on.
6941
6969
  const hookRes = runLifecycleHooks("spawn", {
6942
- home, instance, agentName: agent.name, soulDir, contextDir: repoAbs,
6970
+ home, instance, agentName: agent.name, soulDir: homeSoulTarget, soulId: preparedSoulId, contextDir: repoAbs,
6943
6971
  workspaceDir: workspaceOf(root), rootDir: root, resolved: resolvedCfg,
6944
6972
  extraEnv: { OATS_TASK: task, OATS_REPO: repoAbs, OATS_BRANCH: branch || "", OATS_WORK: work, OATS_RUNTIME: runtime, OATS_KIND: agent.kind || "persistent" },
6945
6973
  });
@@ -7043,7 +7071,7 @@ function* spawnBody(root, agent, o = {}) {
7043
7071
  catch (e) { return retainDirectory(e); }
7044
7072
  try {
7045
7073
  const comp = runLifecycleHooks("retire", {
7046
- home, instance, agentName: agent.name, soulDir, contextDir: repoAbs,
7074
+ home, instance, agentName: agent.name, soulDir: homeSoulTarget, soulId: preparedSoulId, contextDir: repoAbs,
7047
7075
  workspaceDir: workspaceOf(root), rootDir: root, resolved: resolvedCfg,
7048
7076
  priorMeta: hookRes.meta || {},
7049
7077
  });
@@ -7242,7 +7270,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
7242
7270
  // digests) in instance.json before this metadata is assembled — carry them.
7243
7271
  if (o.prepared) {
7244
7272
  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 */ }
7245
- meta.workspace = { key: o.prepared.discovery?.key ?? null, commit: o.prepared.discovery?.commit ?? null, resolution: o.prepared.resolution.revision, standalone: o.prepared.discovery?.standalone === true, soul: { repoKey: o.prepared.soulEntry.repoKey, commit: o.prepared.soulEntry.commit, team: o.prepared.soulEntry.team ?? null } };
7273
+ meta.workspace = { key: o.prepared.discovery?.key ?? null, commit: o.prepared.discovery?.commit ?? null, resolution: o.prepared.resolution.revision, standalone: o.prepared.discovery?.standalone === true, soul: { id: workspaceSoulId(o.prepared.soulEntry.repoKey, o.prepared.soulEntry.name), repoKey: o.prepared.soulEntry.repoKey, commit: o.prepared.soulEntry.commit, team: o.prepared.soulEntry.team ?? null } };
7246
7274
  }
7247
7275
  const spawnWarnings = warnings;
7248
7276
 
@@ -9366,9 +9394,12 @@ export function retireInstance(root, name, o = {}) {
9366
9394
  const resolved = meta.capabilityRuntime
9367
9395
  ? { capabilities: meta.capabilityRuntime }
9368
9396
  : resolveOatsConfig(meta.repo, found.agent.name);
9397
+ // The soul this HOME links (a workspace home links its per-commit directory;
9398
+ // agents/<name>/soul may since point elsewhere) — the same value spawn's hook saw.
9399
+ const homeSoulLink = (() => { try { return realpathSync(join(found.home, "soul")); } catch { return null; } })();
9369
9400
  hookResults = runLifecycleHooks("retire", {
9370
9401
  home: found.home, instance: name, agentName: found.agent.name,
9371
- soulDir: found.agent._soulDir || join(found.agent._dir, "soul"),
9402
+ soulDir: homeSoulLink || found.agent._soulDir || join(found.agent._dir, "soul"),
9372
9403
  contextDir: meta.repo, workspaceDir: workspaceOf(root), rootDir: root, resolved, priorMeta: meta.capabilityMeta || {},
9373
9404
  });
9374
9405
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.25.2",
3
+ "version": "0.25.3",
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",