@awebai/oats 0.25.2 → 0.25.4
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/docs/design/2026-09-20-redesign-program-board.md +1 -1
- package/docs/design/2026-09-23-workspace-module-contracts.md +20 -0
- package/docs/rebuild-to-v2.md +7 -0
- package/docs/release-notes/v0.25.3.md +19 -0
- package/docs/release-notes/v0.25.4.md +21 -0
- package/lib/core.mjs +49 -8
- package/package.json +1 -1
|
@@ -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
|
|
5
|
+
**Last update:** 2026-09-23 20:00Z · **0.25.0–0.25.3 PUBLISHED** (workspace model A–C; team-review fixes; operator-rebuild round; OATS_SOUL_ID) · **OKF 2.1.4 pending human GO** (owner pin by id, clone/timeout, retire-schedule seam; Antares writes the PRs) · 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
|
+
|
package/docs/rebuild-to-v2.md
CHANGED
|
@@ -337,6 +337,13 @@ type a digest. An `--approve` that names an id or version the resolution does
|
|
|
337
337
|
not contain is an error, not a silent skip; an entry the flags do not cover
|
|
338
338
|
stays unapproved (exit `2`, as above).
|
|
339
339
|
|
|
340
|
+
`<version>` is the value `sync --json` reports as `approvalNeeded[].version`,
|
|
341
|
+
which is what the lock records as the package's `version`. For a **catalog**
|
|
342
|
+
package that is the published version (`oats.okf@2.1.4`). For a **git** source
|
|
343
|
+
pinned by commit (`git:github.com/awebai/oats-okf@<oid>`) it is the **full
|
|
344
|
+
commit OID**, not the `git:` reference and not a tag name — copy it from the
|
|
345
|
+
`approvalNeeded` line rather than from your workspace file.
|
|
346
|
+
|
|
340
347
|
## 7b. OKF 2: start a FRESH `state-dir` — do not re-point the old one
|
|
341
348
|
|
|
342
349
|
OKF 2 pins each knowledge **owner** to a soul by path: at source registration
|
|
@@ -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".
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# OATS 0.25.4
|
|
2
|
+
|
|
3
|
+
Patch release: one kernel fix found by an operator rehearsal on 0.25.3.
|
|
4
|
+
|
|
5
|
+
## Fixed
|
|
6
|
+
|
|
7
|
+
- **Quarantine retry never re-ran the owed retire hook on a workspace-mode
|
|
8
|
+
home.** When a required spawn hook fails and its compensation is incomplete,
|
|
9
|
+
the kernel retains the home with a cleanup descriptor
|
|
10
|
+
(`.oats-rollback-incomplete.json`). Module materialization had already
|
|
11
|
+
written a pre-hook `instance.json` stub (`{ modules, providers,
|
|
12
|
+
resolutionRevision }`), and `oats retire` trusted that stub over the
|
|
13
|
+
descriptor — so `meta.repo` was undefined, the retry reported *"retire hook
|
|
14
|
+
<cap>: did not run on this retry"* and *"cleanup descriptor lost its context
|
|
15
|
+
repo"* on every attempt, and only `--force` could remove the home (leaving
|
|
16
|
+
the provider's external state to be cleaned by hand). The retry now reads
|
|
17
|
+
the cleanup descriptor whenever the live record is not a complete spawn
|
|
18
|
+
record; the owed hook runs, the home stays while it fails, and is removed
|
|
19
|
+
once it succeeds. Regression test in `test/spawn-workspace.test.mjs`.
|
|
20
|
+
|
|
21
|
+
No contract changes. `oats version --json` is unchanged apart from the version.
|
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
|
-
|
|
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
|
|
|
@@ -9273,7 +9301,17 @@ export function retireInstance(root, name, o = {}) {
|
|
|
9273
9301
|
} catch { markerUnusable = true; }
|
|
9274
9302
|
}
|
|
9275
9303
|
const liveMeta = existsSync(metaPath) ? JSON.parse(readFileSync(metaPath, "utf8")) : undefined;
|
|
9276
|
-
|
|
9304
|
+
// A quarantined home may carry an instance.json that is NOT the spawn record:
|
|
9305
|
+
// module materialization writes a pre-hook stub ({ modules, providers,
|
|
9306
|
+
// resolutionRevision }) before the spawn hooks run, and a required-hook
|
|
9307
|
+
// failure retains the home with that stub in place. Trusting it over the
|
|
9308
|
+
// cleanup descriptor left meta.repo undefined, so the retry never re-ran the
|
|
9309
|
+
// owed retire hook and reported "cleanup descriptor lost its context repo"
|
|
9310
|
+
// on every attempt — only --force could clear the home (found by an operator
|
|
9311
|
+
// rehearsal on 0.25.3). The descriptor is written FOR this retry; a live
|
|
9312
|
+
// record only wins when it is a complete spawn record (repo + work present).
|
|
9313
|
+
const liveIsSpawnRecord = !!liveMeta && nonEmptyString(liveMeta.repo) && WORK_MODES.includes(liveMeta.work);
|
|
9314
|
+
const meta = liveIsSpawnRecord || !quarantine ? (liveMeta || quarantine?.cleanup || {}) : { ...liveMeta, ...quarantine.cleanup };
|
|
9277
9315
|
// Compensation metadata is the SPAWN's, from whichever record survived — never
|
|
9278
9316
|
// a failed retry's report, which says nothing about what still needs undoing.
|
|
9279
9317
|
if (!liveMeta?.capabilityMeta && quarantine?.cleanup?.capabilityMeta) meta.capabilityMeta = quarantine.cleanup.capabilityMeta;
|
|
@@ -9366,9 +9404,12 @@ export function retireInstance(root, name, o = {}) {
|
|
|
9366
9404
|
const resolved = meta.capabilityRuntime
|
|
9367
9405
|
? { capabilities: meta.capabilityRuntime }
|
|
9368
9406
|
: resolveOatsConfig(meta.repo, found.agent.name);
|
|
9407
|
+
// The soul this HOME links (a workspace home links its per-commit directory;
|
|
9408
|
+
// agents/<name>/soul may since point elsewhere) — the same value spawn's hook saw.
|
|
9409
|
+
const homeSoulLink = (() => { try { return realpathSync(join(found.home, "soul")); } catch { return null; } })();
|
|
9369
9410
|
hookResults = runLifecycleHooks("retire", {
|
|
9370
9411
|
home: found.home, instance: name, agentName: found.agent.name,
|
|
9371
|
-
soulDir: found.agent._soulDir || join(found.agent._dir, "soul"),
|
|
9412
|
+
soulDir: homeSoulLink || found.agent._soulDir || join(found.agent._dir, "soul"),
|
|
9372
9413
|
contextDir: meta.repo, workspaceDir: workspaceOf(root), rootDir: root, resolved, priorMeta: meta.capabilityMeta || {},
|
|
9373
9414
|
});
|
|
9374
9415
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.4",
|
|
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",
|