@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
@@ -54,6 +54,18 @@ something to say. The one warning so far is `deprecated-runtime-name` (see
54
54
  --json`, whose document is not an envelope, carries the same `warnings` beside
55
55
  its `problems`. In text mode the warning is one `oats: warning: …` line on stderr.
56
56
 
57
+ ## Flags
58
+
59
+ A kernel command reads `--flag=value` exactly as `--flag value`, with the
60
+ same validation (0.27.3+; before, the inline form was silently ignored, so
61
+ `--harness=claude` spawned the default harness). The value is everything after
62
+ the first `=`. An empty `--flag=` is `E_BAD_ARGS` ("`--flag=` needs a value"),
63
+ and so is a value on a switch: `--yolo=false` is refused and never turns
64
+ yolo on. A capability command's own flags belong to its provider. They are
65
+ forwarded exactly as typed; the kernel reads only its dispatch flag (`--soul`)
66
+ in either form. There is no feature string: a caller that must work with
67
+ older kernels uses the spaced form.
68
+
57
69
  ## The harness rename (feature `harness`, OATS 0.27.0)
58
70
 
59
71
  What starts an instance (pi, claude or codex) is its **harness**. 0.27.0 renames
@@ -667,7 +679,7 @@ unchecked, echoed a stored `definition.id` without checking it, and named a
667
679
  ```
668
680
 
669
681
  **Exact shapes (API 3):**
670
- - `schedule list --json` → `result = {scope, scheduleApi: 2, scheduleHistoryApi: 3, integrity, schedules: Entry[], scheduler}`.
682
+ - `schedule list --json` → `result = {scope, scheduleApi: 2, scheduleHistoryApi: 3, integrity, schedules: Entry[], triggers: {count, command: "oats trigger list"}, scheduler}` (`triggers`, 0.28.0: the trigger definitions this listing leaves out).
671
683
  - `schedule show <id> --json` → `result = {schedule: Entry}` (one level of nesting; `integrity` is NOT on `show` — it is a scope fact reported by `list`).
672
684
  - `Entry` (readable) = definition fields (`id, kind, home, message|operation, cron, enabled, …`) + `{scope, scheduleApi: 2, scheduleHistoryApi: 3, executionStatus, nextRun: ISO|null, lastRun: Run|null, history, recentRuns: Run[], running: boolean, attempt?, pendingWake?}`.
673
685
  - `Entry` (unreadable, `list` only) = `{id, scope, scheduleApi: 2, scheduleHistoryApi: 3, unreadable: {code, message}, history: {status: "corrupt", stored: null, truncated: false}, recentRuns: []}` — no definition fields.
@@ -953,6 +965,12 @@ location. The receipt says so:
953
965
  discarding the worktree (a checked-out branch cannot be deleted).
954
966
  - A failed move keeps the home and refuses `E_WORK_PRESERVATION_FAILED` —
955
967
  nothing is lost; retry or pass `--discard-worktree`.
968
+ - When a recovery's Git status disagrees with the source's (0.27.2),
969
+ `E_WORK_PRESERVATION_FAILED` carries `details: {home, statusDisagreement:
970
+ {repo, rows: [{path, source, recovery}], total}}`. `repo` is `.` or a nested
971
+ repository's path. `source`/`recovery` are the porcelain `XY` codes, or
972
+ `null` where that side has no row. `rows` holds the first 10 paths, sorted,
973
+ and `total` counts all of them. The message names the same rows.
956
974
  - Non-worktree modes report `retention: null`. Quarantine/rollback paths keep
957
975
  their removal semantics.
958
976
  - A recovery (`workRecovery`, `workRecoveries[]`) is `{path, classes, bytes,
@@ -1109,8 +1127,8 @@ gate that on the absence of `packages-no-approval`.
1109
1127
  "souls":["tools-expert"],"capabilities":["acme-tools-dev"],"publishes":{"package":"acme.tools","version":"0.4.0"}},
1110
1128
  {"key":"github.com/acme/billing","name":"billing","commit":"<oid>","confirmed":false,"status":"no-backlink","detail":"github.com/acme/billing@… has no oats-membership.yaml","team":null,
1111
1129
  "souls":[],"capabilities":[],"publishes":null}],
1112
- "packages":[{"id":"oats.okf","version":"2.1.3","source":"catalog:oats.okf","commit":"<oid>","integrity":"sha256-…","capabilities":["oats.okf"]},
1113
- {"id":"acme.tools","version":"0.4.0","source":"git:github.com/acme/tools@v0.4.0","commit":"<oid>","integrity":"sha256-…","capabilities":["acme-deploy","acme-lint"]}],
1130
+ "packages":[{"id":"oats.okf","version":"2.1.3","source":"catalog:oats.okf","commit":"<oid>","integrity":"sha256-…","capabilities":["oats.okf"],"souls":[]},
1131
+ {"id":"acme.tools","version":"0.4.0","source":"git:github.com/acme/tools@v0.4.0","commit":"<oid>","integrity":"sha256-…","capabilities":["acme-deploy","acme-lint"],"souls":["release-reviewer"]}],
1114
1132
  "changes":[{"id":"acme.tools","from":null,"to":"0.4.0","commit":"<oid>"}],
1115
1133
  "problems":[]}
1116
1134
  ```
@@ -1119,6 +1137,8 @@ gate that on the absence of `packages-no-approval`.
1119
1137
  `backlink-elsewhere` | `cannot-read`; `detail` explains an unconfirmed row.
1120
1138
  `publishes` reports a member's `oats-package/` (informational — its
1121
1139
  capabilities are **not** in `capabilities[]`; the non-collapse rule).
1140
+ - `packages[].souls` (feature `package-souls`, 0.28.0): the names of the
1141
+ package souls the lock records for that package (`[]` when none).
1122
1142
  - `changes[]`: `from` = previously locked version or `null`; `to` = `null` when
1123
1143
  the package was dropped from `packages:` and from the lock.
1124
1144
  - `problems[]`: `{ code, path, message, repoKey? }` — per-item discovery
@@ -1201,10 +1221,23 @@ or `"unassigned"`.
1201
1221
  "souls":[
1202
1222
  {"name":"release-manager","origin":"member github.com/acme/agents @ 3f2a9c1e","kind":"member","repoKey":"github.com/acme/agents","commit":"<oid>",
1203
1223
  "team":"engineering","private":false,"path":"souls/release-manager","work":"worktree","description":"Cuts, verifies and announces releases."},
1204
- {"name":"security-reviewer","origin":"external github.com/oss-collective/experts @ 9c4e1f2a","kind":"external",…}],
1224
+ {"name":"security-reviewer","origin":"external github.com/oss-collective/experts @ 9c4e1f2a","kind":"external",…},
1225
+ {"name":"release-reviewer","qualifiedName":"acme.tools/release-reviewer","origin":"package acme.tools v0.4.0","kind":"package","package":"acme.tools","version":"0.4.0",
1226
+ "repoKey":"github.com/acme/tools","commit":"<oid>","team":"engineering","labels":["engineering"],"private":false,"path":"oats-package/souls/release-reviewer","work":"directory","description":"…"}],
1205
1227
  "problems":[]}
1206
1228
  ```
1207
1229
 
1230
+ **Package souls** (feature `package-souls`, 0.28.0): a row with `kind:
1231
+ "package"` is a soul a locked package ships (listed only while the workspace
1232
+ declares the package). It carries `package`, `version` and `qualifiedName`
1233
+ (`<package>/<soul>`); spawn it by `qualifiedName` (the bare `name` works when
1234
+ it is unique). The Souls page shows it as "from package <id> <version>".
1235
+ Its instances home under `agents/<package>--<soul>/` (`.` in the package id
1236
+ becomes `-`), and that directory is the agent `name` in `oats status --json`
1237
+ (`agents[].name`, e.g. `oats-okf--knowledge-maintainer`). A
1238
+ problem about a package soul carries `package` (and `repoKey: null`); its
1239
+ `path` is `package:<id>:<path in the repo>`.
1240
+
1208
1241
  Package capabilities of declared-but-unsynced packages are absent until `sync`.
1209
1242
  (`oats souls --json` keeps `soulsApi: 1` in 0.26.0: its shape is unchanged.
1210
1243
  The probe's `soulsApi` is **2** because it tracks the `oats inspect --json`
@@ -1255,9 +1288,13 @@ between preview and apply is `E_DECISION_STALE`):
1255
1288
  key**: legal only at the top level of the workspace file's `messaging:`;
1256
1289
  anywhere else in any payload layer (soul, `oats-local.yaml` `settings`,
1257
1290
  `--provider`, at any depth) it is `E_WORKSPACE_SCHEMA { reason: "reserved-key", path, key }`.
1258
- - Soul lookup: `E_SOUL_UNKNOWN { name, members[] }` (not among confirmed
1259
- members or externals), `E_SOUL_AMBIGUOUS { name, repos[] }` (name it
1260
- `<repo>/<soul>`). Resolution errors keep their codes (`E_NOT_A_MEMBER`,
1291
+ - Soul lookup: `E_SOUL_UNKNOWN { name, members[], packages[] }` (not among
1292
+ confirmed members, externals or package souls), `E_SOUL_AMBIGUOUS { name,
1293
+ repos[], qualified[] }` (name one of `qualified`: `<member>/<soul>` or
1294
+ `<package>/<soul>`), `E_SOUL_DISABLED { name, qualifiedName, entry }` (the
1295
+ soul is in `oats-local.yaml` `souls.disabled`). A package soul whose fetched
1296
+ content does not match the lock is `E_PACKAGE_INTEGRITY { why:
1297
+ "soul-digest", package, soul, locked, observed }`. Resolution errors keep their codes (`E_NOT_A_MEMBER`,
1261
1298
  `E_MEMBERSHIP_UNCONFIRMED { repoKey, reason }`, `E_CAPABILITY_MISSING { hint? }`,
1262
1299
  `E_CAPABILITY_PRIVATE`, `E_PACKAGE_MISSING`, `E_PACKAGE_INTEGRITY { why: "capabilities", listed, locked }`,
1263
1300
  `E_SLOT_CONFLICT { slot, modules[], reason? }`, `E_SKILL_DUPLICATE { name, modules[] }`,
@@ -1280,6 +1317,7 @@ Written by materialization inside the spawn transaction; read back by
1280
1317
  "providers":{"acme-release-tooling":{},"oats.okf":{"owns":"release-manager","reads":["platform-engineer"],"state-dir":"/Users/ana/.oats/okf"}},
1281
1318
  "workspace":{"key":"github.com/acme/agents","name":"acme","deployment":"/Users/ana/acme-workspace","commit":"<oid>","resolution":"<24 hex>","standalone":false,
1282
1319
  "soul":{"repoKey":"github.com/acme/agents","commit":"<oid>","team":"engineering"}},
1320
+ "…":"a package soul's workspace.soul also records package: {id, version, commit, digest, path}, and its id is package:<id>#<soul>",
1283
1321
  "capabilities":[{"id":"oats.okf","capability":"oats.okf","origin":"package:oats.okf@2.1.3","…":"one row per module"}]}
1284
1322
  ```
1285
1323
 
@@ -1335,6 +1373,60 @@ top-level `workspace` reachability field:
1335
1373
  `oats-local.yaml` has no `workspace` field and `modules` as recorded.
1336
1374
  - Text mode prints `modules: <cap> from <member|package …> @ <7-char>` lines
1337
1375
  for non-current modules (`--verbose` for all).
1376
+ - `instances[].soul` is the soul source's drift: `{ repoKey, commit, current,
1377
+ status, reason? }`. For a **package soul** (feature `package-souls`) it also
1378
+ carries `package`, `version` and `currentVersion`; `moved` means the package
1379
+ pin moved (another version/commit is locked now), `missing` has `reason`
1380
+ `package-absent` (no longer locked or declared) or `soul-absent` (the locked
1381
+ package no longer ships it). Text: `soul: <name> from package <id> v<version>
1382
+ @ <7-char> [package moved since (now v<version> @ <7-char>)]`.
1383
+
1384
+ ### Triggers (feature `triggers`, OATS 0.28.0) — `oats trigger … --json` → `triggerApi: 1`
1385
+
1386
+ Event-driven spawns of a deployment ([schedules.md#triggers](schedules.md#triggers)).
1387
+ Definitions live in `oats-schedules.json` (`kind: "trigger"`); `oats schedule list`
1388
+ does not show them.
1389
+
1390
+ ```json
1391
+ {"triggerApi":1,"scope":"/abs/deployment","triggers":[
1392
+ {"id":"okf-harvest-review","enabled":true,"kind":"trigger",
1393
+ "on":{"source":"github.pull_request","repo":"github.com/acme/knowledge","events":["opened","reopened","ready_for_review"],"labels":["okf-harvest"],"base":"main","poll":"2m"},
1394
+ "spawn":{"soul":"oats.okf/knowledge-maintainer","purpose":"review-pr-{number}","task":"…","teams":["okf"],"harness":"claude","model":"opus"},
1395
+ "concurrency":{"max":2,"perKey":1},"template":{"package":"oats.okf","version":"4.0.0","commit":"<oid>","template":"harvest-review"},
1396
+ "triggerApi":1,"scope":"/abs/deployment","createdAt":"<iso>","updatedAt":"<iso>"}]}
1397
+ ```
1398
+
1399
+ - `list` → the document above; `show <id>`, `add`, `enable`, `disable` →
1400
+ `{ trigger }` (one row); a stored definition that no longer validates carries
1401
+ `invalid: { code, message }`. `remove <id>` → `{ removed, live: [instance] }`.
1402
+ - `status [<id>]` → `{ triggerApi, scope, triggers: [{ id, enabled, repo, soul,
1403
+ lastPoll: { at, ok, prs, matching } | { at, ok: false, error } | null,
1404
+ nextPollAt, pending: [{ key, event, number, url, observedAt }], fired: [{ key,
1405
+ at, instance, home, event, number }] (newest 50), firedTotal, live: [{
1406
+ instance, home, repo, number, event }], lastError: { at, code, message, key? } | null }] }`.
1407
+ - `test <id>` → `{ triggerApi, id, ok, gh: { ok, account, credentialSource:
1408
+ "keyring" | "config" | "env:<VAR>" | "unknown" | null, reachesHostTimer:
1409
+ boolean | null, note, detail }, repo: { key,
1410
+ readable, fullName, permissions: { push, maintain, admin }, canMerge } |
1411
+ { key, readable: false, error }, soul: { resolves, name, agent, messaging } |
1412
+ { resolves: false, name, error }, teams: { requested, undeclared | null,
1413
+ messaging }, wouldFire: [{ key, event, number, url, held? }], pollError?, problems:
1414
+ [string], warnings: [string], spawned: false }`. It writes nothing. `ok`
1415
+ counts `problems` only; a credential the host timer cannot reach
1416
+ (`reachesHostTimer: false`) is a warning.
1417
+ - `oats schedule list --json` gains `triggers: { count, command: "oats trigger
1418
+ list" }`: the triggers it does not list.
1419
+ - The tick's `considered[]` gains trigger rows `{ workspace, trigger, action,
1420
+ … }` with `action` `not-due`, `poll-failed`, `polled` (`prs`, `matching`;
1421
+ nothing to fire), `held`, `fired` (`key`,
1422
+ `instance`, `home`), `spawn-failed` (`key`, `code`, `error`), `would-fire`
1423
+ (`--dry-run`) or `invalid`.
1424
+ - A triggered instance's `instance.json.trigger` is `{ id, key, source, repo,
1425
+ number, url, event, headSha, observedAt, eventFile }`; the event file is
1426
+ `OATS_TRIGGER_EVENT_FILE`.
1427
+ - Errors: `E_TRIGGER_INVALID { field }`, `E_TRIGGER_EXISTS`,
1428
+ `E_TRIGGER_UNKNOWN`, `E_BAD_ARGS` (`missing` / `parameters` for a template),
1429
+ `E_PACKAGE_MISSING`, `E_PACKAGE_MANIFEST`, `E_LOCAL_MISSING`.
1338
1430
 
1339
1431
  ### Eligible teams (feature `teams`, OATS 0.26.0)
1340
1432
 
@@ -69,9 +69,10 @@
69
69
  "additionalProperties": false,
70
70
  "properties": {
71
71
  "disabled": {
72
+ "description": "Souls not run on this machine (a spawn is E_SOUL_DISABLED): a bare soul name disables every soul of that name; a qualified name disables one — <package>/<soul> for a package soul, <member name>/<soul> for a member's.",
72
73
  "type": "array",
73
74
  "uniqueItems": true,
74
- "items": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }
75
+ "items": { "type": "string", "pattern": "^(?:[a-z0-9][a-z0-9._-]*/)?[a-z0-9]+(?:-[a-z0-9]+)*$" }
75
76
  }
76
77
  }
77
78
  }
@@ -53,6 +53,45 @@
53
53
  },
54
54
  "uniqueItems": true
55
55
  },
56
+ "souls": {
57
+ "description": "Package souls (OATS 0.28.0): package-relative soul directories, each an ordinary soul (soul.yaml + AGENTS.md, optional skills/). A soul's name is its directory's. They are versioned and locked with the package (oats-lock.json records each soul's name, path and digest), listed with their package origin, and spawned by the qualified name <package>/<soul> (or the bare name when unique). `from: here` in a package soul is its own package at the locked commit.",
58
+ "type": "array",
59
+ "items": {
60
+ "type": "string",
61
+ "minLength": 1,
62
+ "pattern": "^[^/].*$",
63
+ "not": {
64
+ "pattern": "(^|/)\\.\\.(/|$)"
65
+ }
66
+ },
67
+ "uniqueItems": true
68
+ },
69
+ "triggers": {
70
+ "description": "Trigger templates (OATS 0.28.0): `oats trigger add --from <package>:<id> [--set <name>=<value>]…` instantiates one from the locked package. Each file is { parameters: { <name>: { path, required?, default?, description? } }, definition: { …a trigger definition… } }; see docs/schedules.md#triggers.",
71
+ "type": "array",
72
+ "items": {
73
+ "type": "object",
74
+ "required": [
75
+ "id",
76
+ "file"
77
+ ],
78
+ "additionalProperties": false,
79
+ "properties": {
80
+ "id": {
81
+ "type": "string",
82
+ "pattern": "^[a-z0-9][a-z0-9._-]*$"
83
+ },
84
+ "file": {
85
+ "type": "string",
86
+ "minLength": 1,
87
+ "pattern": "^[^/].*$",
88
+ "not": {
89
+ "pattern": "(^|/)\\.\\.(/|$)"
90
+ }
91
+ }
92
+ }
93
+ }
94
+ },
56
95
  "configTemplates": {
57
96
  "description": "Named config TEMPLATES: complete reference oats-config.yaml files of the classic line, whose adoption verb is removed under the workspace model. A template is a recommended starting point that becomes ordinary local policy on adoption — adopters may change every copied setting, and package updates never rewrite an adopted config. Templates must stay portable: no secret, credential, account, machine path or provider-local ID. Installation applies none of them. This is the canonical spelling; a manifest may not carry both this and the legacy `configs`.",
58
97
  "type": "object",
package/docs/packages.md CHANGED
@@ -27,7 +27,9 @@ A Git repository **contains** a package at `oats-package/`:
27
27
  ```
28
28
 
29
29
  `oats-package.json` must declare `package` and `capabilities` (a list of
30
- directories relative to the package root, each holding an `oats.json`). A
30
+ directories relative to the package root, each holding an `oats.json`). It may
31
+ also declare `souls` (0.28.0): soul directories the package ships, see
32
+ [Package souls](#package-souls). A
31
33
  directory entry need not equal the capability's name
32
34
  (`capabilities/oats-okf` → capability `oats.okf`). A package declaring one
33
35
  capability name twice, a listed directory without a manifest, or a manifest
@@ -73,7 +75,7 @@ members:
73
75
  - git:github.com/acme/platform
74
76
  packages:
75
77
  oats.framework: v1.1.3
76
- oats.okf: v2.1.5
78
+ oats.okf: v3.0.0
77
79
  oats.aweb: v1.14.2
78
80
  teams:
79
81
  global: { description: Org-wide }
@@ -172,7 +174,8 @@ same workspace commit hold identical locks.
172
174
  "version": "0.4.0",
173
175
  "commit": "47f4b81660e4cc9701d373088de52462762585a3",
174
176
  "integrity": "sha256-4cd126a7…",
175
- "capabilities": ["acme-deploy", "acme-lint"]
177
+ "capabilities": ["acme-deploy", "acme-lint"],
178
+ "souls": [{ "name": "release-reviewer", "path": "souls/release-reviewer", "digest": "sha256-9a0f…" }]
176
179
  }
177
180
  }
178
181
  }
@@ -187,6 +190,7 @@ same workspace commit hold identical locks.
187
190
  | `commit` | full 40-hex OID the version resolved to |
188
191
  | `integrity` | `sha256-<hex>` content digest of the package tree at `path` |
189
192
  | `capabilities` | the capability names the package provides (sorted) — what `from: package` looks up |
193
+ | `souls` | the package souls (0.28.0), sorted by name: `name`, `path` (inside the package) and `digest` (`sha256-<hex>` of the soul directory); absent when the package ships none |
190
194
 
191
195
  A capability provided by **two** locked packages is ambiguous and fails
192
196
  closed (`E_PACKAGE_MISSING { ambiguous: [ids] }`): keep one of them in
@@ -220,6 +224,66 @@ against what the fetch reported; skills are copied to
220
224
  `packages:` and syncing affects **only new spawns**; `oats status` shows a
221
225
  running instance's package module as `moved` once the lock points elsewhere.
222
226
 
227
+ ## Package souls
228
+
229
+ A package may ship **souls** as well as capabilities (0.28.0). One pin in
230
+ `packages:` then versions both: nothing drifts, unlike an `external:` soul's
231
+ commit pin.
232
+
233
+ ```
234
+ oats-package/
235
+ ├── oats-package.json # { …, "capabilities": ["capabilities/acme-review"], "souls": ["souls/release-reviewer"] }
236
+ ├── capabilities/acme-review/oats.json
237
+ └── souls/release-reviewer/
238
+ ├── soul.yaml # an ordinary soul: name = the directory's name
239
+ ├── AGENTS.md
240
+ └── skills/…
241
+ ```
242
+
243
+ - **Locked.** `oats sync` records each soul's `name`, `path` and `digest` in
244
+ the lock entry. A soul without `soul.yaml` or `AGENTS.md`, or whose
245
+ directory is not a soul name, is `E_PACKAGE_MANIFEST`. On a later sync at
246
+ the same version the souls must still match (`E_PACKAGE_INTEGRITY { why:
247
+ "souls" }`); a lock written before 0.28.0 has its `souls` filled in.
248
+ - **Listed.** `oats souls` lists a package soul with `kind: "package"`,
249
+ `package`, `version`, `qualifiedName` and `origin: "package <id>
250
+ v<version>"`; `oats sync` / `oats workspace status` list each package's
251
+ `souls`. Only packages the workspace still declares are listed.
252
+ - **Named.** The qualified name is `<package>/<soul>` (`oats.okf/knowledge-maintainer`).
253
+ A bare name works when it is unique across member, external and package
254
+ souls; otherwise `E_SOUL_AMBIGUOUS` names each qualified form
255
+ (`details.qualified`). A member soul's qualified form is `<member name>/<soul>`.
256
+ - **Resolved** like any soul: the workspace and team defaults apply, `off` and
257
+ `<slot>: none` work, every `team:` label must be declared (`E_TEAM_UNKNOWN`
258
+ in discovery), and `from: here` means **this package** at the locked commit
259
+ (a capability it does not provide is `E_CAPABILITY_MISSING`).
260
+ - **Spawned** at the locked commit: the soul is fetched into the per-commit
261
+ soul cache and its digest must equal the lock's (`E_PACKAGE_INTEGRITY
262
+ { why: "soul-digest" }`). A package soul homes in its own agent directory,
263
+ `agents/<package>--<soul>/` (the package id with `.` as `-`:
264
+ `agents/oats-okf--knowledge-maintainer/`), which is also its agent name
265
+ (`OATS_AGENT`, the `oats status` row); its instances are named from it
266
+ (`oats-okf-knowledge-maintainer-<purpose>`). A soul name never holds `--`,
267
+ so a member soul of the same bare name keeps its own `agents/<soul>/`.
268
+ Two packages whose ids sanitise alike (`a.b`, `a-b`) and that ship a
269
+ same-named soul would share a directory: both are listed with an
270
+ `E_SOUL_AMBIGUOUS` problem, and spawning either is `E_SOUL_AMBIGUOUS
271
+ { agentDir, qualified }` — keep one of the packages.
272
+ `instance.json.workspace.soul` records `name`, `qualifiedName`, `package:
273
+ { id, version, commit, digest, path }` and the soul id `package:<id>#<soul>`;
274
+ in `oats status` the soul is `moved` once the package pin moves.
275
+ - **Disabled** by `oats-local.yaml` `souls.disabled` by its qualified or bare
276
+ name (`E_SOUL_DISABLED` at spawn).
277
+ - **Trusted** as the package's capabilities are: declaring the package is the
278
+ trust decision.
279
+
280
+ ## Trigger templates
281
+
282
+ A package may also ship **trigger templates** (0.28.0): `triggers: [{ id,
283
+ file }]` in `oats-package.json`, each file `{ parameters, definition }`.
284
+ `oats trigger add --from <package>:<id> --set <name>=<value>` instantiates one
285
+ at the locked commit; see [schedules.md#triggers](schedules.md#triggers).
286
+
223
287
  ## Compatibility floors
224
288
 
225
289
  A soul may state floors on package versions — constraints, not sources:
@@ -0,0 +1,51 @@
1
+ # OATS 0.27.2
2
+
3
+ ## Fixed
4
+
5
+ - **Retiring a worktree instance whose repository excludes paths locally.**
6
+ Retire refused with `E_WORK_PRESERVATION_FAILED` ("recovered Git
7
+ index/status disagreed with the source") when the repository ignored a path
8
+ only through its local rules: the common Git directory's `info/exclude`, or
9
+ a `core.excludesFile` set in the repository's config. The recovery clone had
10
+ neither, so such a path was ignored (`!!`) in the source but untracked, or
11
+ absent for an empty directory, in the clone. Preservation runs before the
12
+ retire hooks and `--force` does not bypass it, so the instance could not be
13
+ retired and its identities were never revoked. The recovery now carries
14
+ those patterns into its own `.git/info/exclude` (nested repositories too),
15
+ and `recovery.json` lists them under `excludes`
16
+ (`[{ repo, kind, path }]`).
17
+ When a recovery's status still disagrees, the refusal now **names the
18
+ differing rows** from both sides, for example
19
+ `.scratch/ (source !!, recovery absent)`: the first 10, then
20
+ `; and N more`. Under `--json` they are in
21
+ `details.statusDisagreement` (`{repo, rows: [{path, source, recovery}],
22
+ total}`; see [desktop-cli-api.md](../desktop-cli-api.md)).
23
+ **On an older kernel:** remove the disposable paths that
24
+ `git -C <home>/work status --porcelain --ignored=matching` lists as `!!`
25
+ before retiring.
26
+
27
+ - **Retiring a worktree instance whose repository changes how status reads
28
+ its files.** The recovery clone probed its own settings and lacked the
29
+ common Git directory's `info/attributes`. A repository with, for example,
30
+ `core.fileMode=false` and a mode-only change was clean in the source but
31
+ ` M` in the clone, so retire refused. The recovery now takes the source's
32
+ `core.fileMode`, `core.ignoreCase`, `core.precomposeUnicode`,
33
+ `core.symlinks`, `core.autocrlf` and `core.eol` (set, or unset where the
34
+ source leaves them unset) and its `info/attributes`, nested repositories
35
+ included. `recovery.json` lists them under `statusConfig`
36
+ (`[{ repo, kind: "config", key, value } | { repo, kind: "info/attributes", path }]`).
37
+
38
+ ## Known limitations
39
+
40
+ - The bundled oats.okf is still 2.1.5 (its harvest answers one `deprecated-runtime-name` warning per spawn; harmless). oats.okf 2.1.6 follows in a later patch.
41
+
42
+ - **A `core.attributesFile` or filter driver set in the repository's own
43
+ config is not carried** into a recovery. Merging its patterns would change
44
+ their precedence, and a pointer would tie the recovery to a host file. Such
45
+ a repository can still make retire refuse, and the refusal names the rows.
46
+ Workaround: commit or restore the files it re-reads before retiring.
47
+
48
+ ## Documentation
49
+
50
+ - `oats retire --force` does **not** skip work preservation
51
+ ([souls-and-instances.md](../souls-and-instances.md#retire)).
@@ -0,0 +1,144 @@
1
+ # OATS 0.28.0
2
+
3
+ Package souls, triggers, okf 3.0.0 remote knowledge consult, and the Workspace v4 Desktop.
4
+
5
+ ## Added
6
+
7
+ - **Package souls** (feature `package-souls`). A package may ship souls as
8
+ well as capabilities: `souls: ["souls/<name>", …]` in
9
+ `oats-package/oats-package.json`, each an ordinary soul directory
10
+ (`soul.yaml`, `AGENTS.md`, `skills/`). One pin in the workspace's
11
+ `packages:` versions both, so a package soul never drifts the way an
12
+ `external:` commit pin can. See [packages.md](../packages.md#package-souls).
13
+ - `oats sync` locks each soul's `name`, `path` and `digest` in the package's
14
+ lock entry and checks them again on the next sync
15
+ (`E_PACKAGE_INTEGRITY { why: "souls" }`). A soul without `soul.yaml` or
16
+ `AGENTS.md` is `E_PACKAGE_MANIFEST`. A lock written by an earlier kernel
17
+ has its `souls` filled in on the next sync.
18
+ - `oats souls` lists them with `kind: "package"`, `package`, `version` and
19
+ `qualifiedName`; `oats sync` and `oats workspace status` list each
20
+ package's `souls`.
21
+ - Spawn one by its qualified name `<package>/<soul>`
22
+ (`oats spawn oats.okf/knowledge-maintainer`), or by its bare name when that
23
+ is unique. A bare name shared by several souls is `E_SOUL_AMBIGUOUS`, which
24
+ now names each qualified form (`details.qualified`); a member soul's is
25
+ `<member name>/<soul>`.
26
+ - A package soul resolves like any soul: workspace and team defaults apply,
27
+ `off` and `<slot>: none` work, its `team:` labels must be declared, and
28
+ `from: here` means its own package at the locked commit.
29
+ - The spawn fetches the soul at the locked commit and verifies it against the
30
+ lock's digest (`E_PACKAGE_INTEGRITY { why: "soul-digest" }`). It homes in
31
+ its own agent directory `agents/<package>--<soul>/`
32
+ (`agents/oats-okf--knowledge-maintainer/`), its agent name in `oats status`
33
+ and `OATS_AGENT`, so a member soul of the same bare name never shares its
34
+ directory or roster row. `instance.json.workspace.soul` records `name`,
35
+ `qualifiedName` and `package: { id, version, commit, digest, path }`, and
36
+ the soul id is `package:<id>#<soul>`. `oats status` shows the soul as
37
+ `moved` once the package pin moves. Two packages whose ids differ only by
38
+ `.` and `-` (`a.b`, `a-b`) and that ship a same-named soul would share an
39
+ agent directory: that is an `E_SOUL_AMBIGUOUS` problem in `oats sync` and
40
+ `oats souls`, and neither spawns.
41
+
42
+ - **Triggers** (feature `triggers`): event-driven spawns. A trigger says
43
+ "when a GitHub pull request event matches, spawn a new instance of this soul
44
+ with this task, in these teams". It is stored beside the schedules
45
+ (`kind: "trigger"` in `oats-schedules.json`) and evaluated by the same host
46
+ tick; there is no daemon or webhook. See
47
+ [schedules.md#triggers](../schedules.md#triggers).
48
+ - The source `github.pull_request` is polled with the host's `gh` at the
49
+ trigger's `poll` interval (at least 1m). Events: `opened`, `reopened`,
50
+ `ready_for_review`, `labeled`, `synchronize`, filtered by `labels` and
51
+ `base`.
52
+ - Each event fires once: its key is recorded only after a successful spawn,
53
+ so a failed spawn is retried on the next poll. Delivery is at least once: a
54
+ tick that dies between the spawn and recording its key spawns the event
55
+ again (held while the first instance is live, under the default `perKey: 1`). `concurrency.max` and
56
+ `perKey` bound the live instances of the trigger and of one PR.
57
+ - The purpose and task are templated from `{repo} {number} {url} {event}
58
+ {headSha} {trigger}` only; a PR's title and body never reach the task.
59
+ `spawn.teams` becomes the messaging capability's `join=`. The instance gets
60
+ the event as `OATS_TRIGGER_EVENT_FILE` (`<home>/.oats/trigger-event.json`)
61
+ and records `instance.json.trigger`.
62
+ - `oats trigger add (--file | --from <package>:<template> --set …) | list |
63
+ show | enable | disable | remove | test | status`, all with `--json`.
64
+ `oats trigger test` checks gh auth and where its credential comes from
65
+ (warning when the host timer cannot reach it, such as a shell-only
66
+ `GH_TOKEN`), the repository and your push/maintain/admin permissions on it,
67
+ the soul, the teams, and what would fire now, and spawns nothing.
68
+ `oats schedule list` counts the triggers it does not list.
69
+ - A package may ship trigger templates (`triggers: [{ id, file }]` in
70
+ `oats-package.json`).
71
+ - `oats spawn --trigger-event <file>` is how a trigger hands the event to the
72
+ spawn.
73
+
74
+ ## Fixed
75
+
76
+ - **`--flag=value` is read.** Every kernel command ignored the inline form
77
+ silently: `oats spawn dev --harness=claude` spawned the default harness,
78
+ and `--name=x`, `--runtime=x`, `--dir=x` and `--server=x` were dropped the
79
+ same way. A kernel command now reads `--flag=value` exactly as
80
+ `--flag value`, with the same validation. The value is everything after the
81
+ first `=`. Two inline forms are refused with `E_BAD_ARGS`: an empty
82
+ `--flag=`, and a value on a switch. `--yolo=false` is refused and never
83
+ turns yolo on. A capability command's own flags are its provider's: they
84
+ are forwarded exactly as typed, and the kernel reads only its dispatch flag
85
+ (`--soul`) in either form. `oats capture`, `recall` and `setup` parse their
86
+ own arguments, as before. See
87
+ [desktop-cli-api.md § Flags](../desktop-cli-api.md#flags).
88
+ **On an older kernel:** use the spaced form.
89
+
90
+ - **`oats-local.yaml` `souls.disabled` is enforced at spawn.** It was documented
91
+ as "not run on this machine", but only counted souls in the `oats sync`
92
+ report. A listed soul is now refused with `E_SOUL_DISABLED { name,
93
+ qualifiedName, entry }`. An entry is a bare name (every soul of that name) or
94
+ a qualified name (`oats.okf/knowledge-harvester`, `<member>/<soul>`).
95
+
96
+ ## oats.okf 3.0.0 (catalog pin and bundled mirror)
97
+
98
+ The official catalog now pins `oats.okf` to `v3.0.0` (tag object `ad2349c7`,
99
+ commit `76f7ccdb`). The copy bundled in this package is its byte mirror. The
100
+ package declares `oats >=0.26.0`.
101
+
102
+ - **Knowledge is consulted remotely.** An instance reads its soul's knowledge
103
+ at the accepted state through new commands: `oats okf bases`, `index`,
104
+ `cat --base <alias> <path>`, `ls`, `links` and `search`. A Git base is served
105
+ from one host-wide partial clone per base, and a read refetches the accepted
106
+ branch once the cached commit is older than the `consult-max-age` setting
107
+ (seconds; default 300; `0` refetches on every read). `--fresh` always
108
+ refetches. If the fetch fails, the read is served from the cache, with
109
+ `stale: true` and the reason in its receipt.
110
+ A directory base is read in place. The new `okf-consultation` skill and the
111
+ okf injection teach instances to run `oats okf index` at the start of every
112
+ task.
113
+ - **BREAKING: no `./knowledge/` in homes.** A spawn records the accepted
114
+ resolution and materializes no `./knowledge/` directory or view. Anything
115
+ that read `<home>/knowledge/bases/<alias>/…` reads
116
+ `oats okf cat --base <alias> <path>` instead.
117
+ - **BREAKING: `oats okf refresh` is `E_REMOVED`.** There are no per-instance
118
+ views to refresh.
119
+ - **`oats okf read --path` still works** in 3.0.0, as an alias of `cat`.
120
+ - **Older homes:** a `./knowledge/` a 2.x spawn left in a home is not touched.
121
+ 3.0.0 ignores it, and `oats okf inspect` reports it as
122
+ `legacy-local-view`.
123
+ - The memory-harvest worker soul no longer ships a `CLAUDE.md -> AGENTS.md`
124
+ symlink (npm drops symlinks, and the kernel composes each home's
125
+ `CLAUDE.md`). The mirror check (`scripts/check-okf-mirror.mjs`) no longer
126
+ requires one; any symlink a future release ships is mirrored and verified
127
+ as before.
128
+
129
+ ## Desktop
130
+
131
+ - **Workspace v4 redesign** (#206): the Workspace area (Setup, Souls, Capabilities, the soul and capability pages) is rebuilt to the Workspace v4 design. Facts the kernel does not report yet are left out rather than guessed; the Setup screen no longer names kernel files.
132
+ - **Soul team labels** reach the Souls grid (#208).
133
+
134
+ ## Tests and CI
135
+
136
+ - A home's resolved view (`resolvedFromHome`) carrying the slot rows its
137
+ spawn recorded (`layers`, since 0.26.0 layers-from) is now pinned. Before,
138
+ no test caught reverting it to `{}`.
139
+ - CI runs the suite in six parallel shards (`node --test --test-shard`), plus a gate job with the old check name. Main runs no longer cancel each other.
140
+
141
+ ## Upgrading
142
+
143
+ - **okf 3.0.0 is breaking for anything that read `<home>/knowledge/`.** Use `oats okf cat`. Existing homes keep their old view, unused.
144
+ - **Package souls and triggers are additive.** Nothing changes until a package declares `souls:`/`triggers:`, or you add a trigger.