@awebai/oats 0.24.7 → 0.24.8

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.
@@ -53,8 +53,9 @@ declares**, parsed by the kernel — a consumer never parses YAML and never
53
53
  infers a field that is not there:
54
54
 
55
55
  - `soulsApi: 1`
56
- - `declarations: { requires, defaults, knowledge, teams, resources }` — each
57
- the declared object, or `null` when the section is absent.
56
+ - `declarations: { requires, defaults, knowledge, teams, resources, children }` — each
57
+ the declared object, or `null` when the section is absent (`children`
58
+ since 0.24.8: `{spawn: boolean}`, see readiness policy).
58
59
  - `provenance: { kind, source, revision, path, workspaceRevision } | null` —
59
60
  where this soul copy came from, as recorded by the kernel when it created
60
61
  it (`oats onboard` records `packaged-definition` or
@@ -105,6 +106,7 @@ un-materialized tree → `E_NO_WORKTREE`.
105
106
  "recorded":{"branch":"feat/x","repo":"/abs/repo","drift":true},
106
107
  "upstream":{"ref":"origin/feat/y","ahead":1,"behind":0},
107
108
  "base":{"ref":"origin/main","source":"origin/HEAD","mergeBase":"<oid>","ahead":2,"behind":0},
109
+ "remote":{"name":"origin","url":"git@github.com:acme/one.git","host":"github.com","path":"acme/one","source":"branch-upstream|origin"},
108
110
  "summary":{"changed":1,"renamed":1,"copied":0,"unmerged":0,"untracked":1},
109
111
  "files":[{"id":"<24 hex>","kind":"renamed","xy":"R.","submodule":false,"score":"R100","path":"src/new.txt","origPath":"src/old.txt"}],
110
112
  "notes":[]}
@@ -119,6 +121,11 @@ un-materialized tree → `E_NO_WORKTREE`.
119
121
  unmerged | untracked. Ignored files are not listed.
120
122
  - `files[].id` is **opaque**, minted under (`revision`, `indexRevision`). It is
121
123
  the only way to ask for a diff.
124
+ - `remote` (0.24.8+): the branch's configured remote (`source: branch-upstream`),
125
+ else `origin`, else `null` — never invented. `host`/`path` are **parsed** from
126
+ the URL (ssh/https forms; `.git` stripped) so an ADE can choose a forge backend
127
+ and a `owner/repo` **without running Git**; a local path has `host: null`. No
128
+ network, no forge knowledge in the kernel.
122
129
 
123
130
  `oats instance diff <instance> --file <id> --revision <rev> [--index-revision <idx>] --json`
124
131
  returns a bounded unified diff:
@@ -147,7 +154,303 @@ returns a bounded unified diff:
147
154
  render a diff against a tree that is not the one on screen. A path in
148
155
  `--file` is `E_BAD_ARGS`.
149
156
 
150
- No GitHub/PR data here: that is a capability seam (see the `oats.git` decision).
157
+ No forge (PR/checks/reviews) data here: forge connections are an ADE/workstation integration (P1 decision), read by the Desktop server through the forge's own CLI; the kernel only reports the instance's `remote` so the ADE can pick a backend.
158
+
159
+ ## Instance events (`oats instance events`, `eventsApi: 1`, OATS 0.24.8+) — K7
160
+
161
+ Typed lifecycle events per instance, **written by the kernel action that made
162
+ them true**, with the receipt it produced. Nothing is inferred from
163
+ transcripts, TASK/STATE files or prose. Append-only, two logs: `<home>/.oats-events.jsonl`
164
+ and `<workspace>/.agents/events/<agent>--<instance>.jsonl` (survives the
165
+ home's removal, so a retired instance's `retired` event is still readable).
166
+
167
+ `oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--dir <d>] --json`
168
+
169
+ ```json
170
+ {"eventsApi":1,"instance":"dev-1","home":"/abs/home","count":7,"returned":7,"truncated":false,
171
+ "events":[{"eventsApi":1,"at":"<iso>","instance":"dev-1","home":"/abs/home","producer":"kernel","kind":"spawned","data":{"agent":"dev","work":"worktree","branch":"agents/dev-1","runtime":"claude","model":null,"parentInstance":null,"relation":null,"launched":true}},
172
+ {"…":"launched | restarted | stopped | stop-refused | retire-planned | worktree-retained | worktree-removed | branch-deleted | retired | child-spawn-refused"}],
173
+ "lastEvent":{"kind":"stopped","at":"<iso>","producer":"kernel"},
174
+ "waitingOnYou":null,
175
+ "notes":["…"]}
176
+ ```
177
+
178
+ - `kind` is a closed set (unknown kinds are refused at write). `producer` is
179
+ `kernel` for lifecycle facts; a capability may append its own events with
180
+ its id as producer (the write API is `appendEvent`, not the renderer).
181
+ - **`waitingOnYou` is `null` unless a producer reported it** (`data.waitingOnYou:
182
+ true` with a `reason`). `null` means *unknown*, not "not waiting". Today no
183
+ kernel path claims it; the Active overview keeps rendering unknown until a
184
+ producer (a messaging or review capability) does.
185
+ - Window is bounded (`--limit`, default 200; `truncated` says so). A torn line
186
+ appears as `kind: "unreadable"` rather than vanishing.
187
+
188
+ ## Schedule run history (`scheduleApi: 2`, OATS 0.24.8+) — K8
189
+
190
+ `oats schedule show|list --json` entries gain **`recentRuns`**: the last 50
191
+ settled runs (newest first) — every `lastRun` the scheduler recorded once its
192
+ outcome settled (`ended | stopped | blocked | invalid | delivered | skipped |
193
+ unknown …`, never `active`/`starting`), exactly as the producer wrote it,
194
+ deduplicated per run. Where the run launched or targeted an instance, a
195
+ `transcript: {instance, home, kind: "session"}` pointer says which home's
196
+ session to open (the existing `oats session` surface); the kernel does not
197
+ copy transcripts. `nextRun`/`lastRun`/`executionStatus` are unchanged. The
198
+ Schedules view (frame 08) renders `recentRuns` as the recent-runs list and the
199
+ transcript pointer as the handoff; captured-policy definitions are preserved
200
+ as they are (definition fields are untouched by this addition).
201
+
202
+ ## Spawn preview (`oats spawn … --preview`, `spawnPreviewApi: 1`, OATS 0.24.8+)
203
+
204
+ The Spawn modal's fields are backed by the kernel's own decision, taken **before
205
+ any side effect**: `oats spawn <agent> [same flags as a real spawn] --preview --json`
206
+ runs every preflight a spawn runs (placement, composition, resources,
207
+ executable, runtime packages, child-spawn policy) and returns what the spawn
208
+ *would* do — then returns without creating a home, branch or worktree.
209
+
210
+ ```json
211
+ {"spawnPreviewApi":1,"preview":true,"agent":"dev","kind":"persistent","instance":"dev-fix-login","home":"/abs/agents/dev/instances/dev-fix-login",
212
+ "repo":"/abs/repo","work":"worktree","runtime":"claude","model":"opus","modelSource":"soul","launchConfig":null,"yolo":false,"backend":"tmux",
213
+ "branch":"agents/dev-fix-login","base":{"ref":"HEAD","oid":"<oid>"},"worktree":"/abs/agents/dev/instances/dev-fix-login/work",
214
+ "relation":null,"parentInstance":null,"policy":{"childSpawns":{"allowed":true,"origin":{"kind":"default","detail":"…"}}},
215
+ "executable":"/abs/bin/claude","capabilities":["oats.core"],"skills":["oats-operate","oats-souls"],"task":"…"}
216
+ ```
217
+
218
+ - **Name / work area**: `instance` is the canonical name (`<agent>-<purpose>`,
219
+ de-duplicated with `-2`, `-3`…); `home` and `worktree` are the canonical
220
+ paths. The renderer never derives paths.
221
+ - **Branch / base** (worktree mode): `branch` defaults to `agents/<instance>`
222
+ (`--branch <name>` overrides; validated); `base` is `--base <ref>` resolved
223
+ to its commit oid (default `HEAD`). `E_BRANCH_EXISTS` and `E_BASE_UNKNOWN`
224
+ are refused in preview and in apply, before anything exists. Apply creates
225
+ the worktree **from that exact oid**.
226
+ - **Model**: `model`/`modelSource` are the resolved selection. Omitting
227
+ `--model` **inherits** the launch configuration's or soul's preference;
228
+ `--model @native-default` is the explicit "use the runtime's own default"
229
+ (`modelSource: "native default (explicit)"`). These are different requests
230
+ and the UI must not relabel one as the other.
231
+ - **Policy**: `policy.childSpawns` is what this instance will record (soul
232
+ declaration / spawn option / default), enforced later by the spawn route for
233
+ its children (see readiness).
234
+ - **Apply** = the same command without `--preview`; the same inputs yield the
235
+ same decisions (instance, branch, base oid). If the world moved between
236
+ preview and apply (name taken, branch created, base gone) the apply refuses
237
+ with the same typed codes — the preview is a statement, not a reservation.
238
+ - Not in preview (later K6 follow-ups): attach-knowledge refs from the
239
+ knowledge provider (05 excluded, attach stays), auto-PR intent (ADE-owned,
240
+ P1).
241
+
242
+ ## Readiness quartet, signatures, enforced policy (`oats readiness`, `readinessApi: 1`, OATS 0.24.8+)
243
+
244
+ `oats readiness [--soul <name>] [--home <abs>] [--verify-signatures] [--policy] [--dir <d>] --json`
245
+ is the first-run readiness view (frame 09) and the Capabilities readiness rows
246
+ (frame 04). Every fact is derived from the **same** data `oats inspect` reports
247
+ — never a second opinion — and rolled into four checks:
248
+
249
+ ```json
250
+ {"readinessApi":1,"subject":{"kind":"soul","name":"dev"},"at":"<iso>",
251
+ "checks":{
252
+ "installed": {"status":"pass","items":[{"subject":"oats.core","status":"pass","required":true,"reason":null,"producer":"oats list","evidence":{"version":"1.1.3","integrity":"sha256-…","origin":"installed"},"remedy":null}]},
253
+ "trusted": {"status":"fail","items":[{"subject":"oats.core","status":"fail","required":true,"reason":"executable surface not approved","producer":"artifact approval","evidence":{"integrity":"sha256-…"},"remedy":"oats trust oats.core",
254
+ "signature":{"status":"unknown","signer":null,"reason":"signature verification needs a network fetch; pass --verify-signatures"}}]},
255
+ "configured":{"status":"pass","items":[{"subject":"oats.core activation","status":"pass","required":true,"producer":"oats-config.yaml","evidence":{"target":"declared","level":"/abs"},"remedy":null}]},
256
+ "enrolled": {"status":"not-applicable","items":[{"subject":"workspace membership","status":"not-applicable","required":false,"producer":"oats.yaml","reason":"standalone deployment: no workspace declared in oats.yaml"}]}},
257
+ "summary":{"ready":false,"required":3,"pass":2,"fail":1,"unknown":0},
258
+ "notes":["…"]}
259
+ ```
260
+
261
+ - Each check is `pass | fail | unknown | not-applicable`; items carry
262
+ `subject, status, required, reason, producer, evidence, remedy`. **"Ready" is
263
+ `summary.ready`**: every *required* item passes (or is not-applicable) and
264
+ there is at least one required item — never inferred from an empty set.
265
+ - **`installed`**: artifact present, locked, integrity matches. **`trusted`**:
266
+ executable approval of the exact artifact (`oats trust`). Separately,
267
+ `signature {status: verified | unsigned | unknown | invalid | not-applicable,
268
+ signer: {id, label} | null, reason}` — the source commit's **verified Git
269
+ signature**, named signer or nothing. It is `unknown` unless
270
+ `--verify-signatures` (a network fetch of that one commit; `git log %G?`);
271
+ a catalog URL, repository owner or byte hash is never a signer. Render
272
+ "Trusted · signed by <label>" only for `verified`.
273
+ - **`configured`**: activation for the subject, runtime-package requirements
274
+ (`missingRequires`), runtime-settings problems. **`enrolled`**: workspace
275
+ **member admission** (decision §3) — `not-applicable` for a standalone
276
+ deployment (no `workspace:` in `oats.yaml`), `unknown` until admission is
277
+ verified against the workspace observation, `pass`/`fail` when it is. Never
278
+ login, never team registration; "Skip" leaves it not-applicable, never pass.
279
+ - Subject: `--soul <name>` scopes required items to the soul's declared
280
+ requirements; without it, to the scope's active capabilities.
281
+
282
+ `--policy` adds the **enforced** policy view with origins:
283
+
284
+ ```json
285
+ "policy":{"childSpawns":{"allowed":false,"enforced":true,"origin":{"kind":"soul","detail":"children.spawn: false in soul.yaml"}},
286
+ "worktrees":{"allowed":true,"mode":"worktree","enforced":true,"origin":{"kind":"work-mode","detail":"work: worktree"}}}
287
+ ```
288
+
289
+ `childSpawns` is **enforced by the spawn route**: `soul.yaml` may declare
290
+ `children: {spawn: false}`; `oats spawn --allow-child-spawns | --no-child-spawns`
291
+ overrides per spawn; the result is recorded in `instance.json`
292
+ `policy.childSpawns {allowed, origin}`. A spawn with `--parent <p>` (or
293
+ `--relation child --relative-to <p>`) under a parent whose recorded policy is
294
+ off refuses **`E_CHILD_SPAWNS_DISABLED`** (`details.parent`, `details.policy`)
295
+ before anything is created. Absent policy (pre-0.24.8 instances) = allowed,
296
+ reported as `origin.kind: "default"`. With `--home <abs>` the policy is the
297
+ instance's recorded (enforced) one; with only `--soul` it is the declaration
298
+ (`enforced: false`). It is a lifecycle-authority claim, not an OS sandbox —
299
+ the UI says so.
300
+
301
+ ## Lifecycle plans — Stop and Remove (`lifecycleApi: 1`, OATS 0.24.8+)
302
+
303
+ The Desktop's Stop and Remove confirmations render **plans**: a read-only
304
+ statement of what the action would touch, with the facts a human needs, and a
305
+ `planRevision` hashed from the facts that make the action safe. Apply carries
306
+ the revision back; if reality moved, apply **refuses with the fresh plan**
307
+ (`E_PLAN_STALE`, `details.plan`) instead of acting on a world the human did
308
+ not see. An `idempotencyKey` makes a retried apply return the first receipt.
309
+
310
+ ### `oats instance stop <instance> --plan [--no-recursive] [--home <abs>] [--dir <d>] --json`
311
+
312
+ ```json
313
+ {"lifecycleApi":1,"action":"stop","instance":"dev-1","home":"/abs/home","recursive":true,"at":"<iso>",
314
+ "targets":[{"instance":"dev-1-child","agent":"dev","home":"/abs/child","depth":1,"workMode":"worktree","launched":true,
315
+ "session":{"state":"unknown","present":true,"backend":"tmux","established":true},
316
+ "work":{"observed":true,"revision":"<oid>","branch":"feat/x","detached":false,"drift":false,"changed":2,"untracked":1,"upstream":{"ref":null,"ahead":null,"behind":null},"base":{"ref":"origin/main","ahead":1,"behind":0},"remote":{"host":"github.com","path":"acme/one"}},
317
+ "retiring":false,"stopPending":false,"midTask":true}],
318
+ "skipped":[],"planRevision":"<24 hex>","notes":[]}
319
+ ```
320
+
321
+ - `targets` are the instance's **recorded descendants deepest-first, then the
322
+ instance** (recorded parentage — `parentInstance` — is the only relation the
323
+ kernel knows). `--no-recursive` lists them under `skipped` instead.
324
+ - `session.state` is the backend's word: `shell`/`stopped`/`not-launched` are
325
+ idle; `unknown` means a non-shell process is running whose identity tmux
326
+ cannot name (the ordinary state of a launched harness). If the state **could
327
+ not be established**, `established:false`, `present:null`,
328
+ `state:"unestablished"`, with a `reason` — render that as unknown, never as
329
+ idle.
330
+ - `work` is K1's observation (`observed:false` with a `reason` when there is no
331
+ work tree or it cannot be read — not "clean").
332
+ - `midTask` is **reported** activity: `true` (running session or dirty work),
333
+ `false` (established idle and observed clean), or `"unknown"`.
334
+
335
+ ### `oats instance stop <instance> --apply --plan-revision <rev> --idempotency-key <key> [--no-recursive] [--grace-ms <n>] --json`
336
+
337
+ Quiesces each target (SIGTERM to the harness processes, bounded wait, **never
338
+ escalated**), children first, under a per-home stop marker; retains home, work
339
+ tree, transcript and launch configuration so `oats session restart` brings the
340
+ instance back. Refuses `E_PLAN_STALE` (fresh plan attached),
341
+ `E_INSTANCE_RETIRING`, `E_LIFECYCLE_BUSY`.
342
+
343
+ ```json
344
+ {"lifecycleApi":1,"action":"stop","instance":"dev-1","home":"/abs/home","idempotencyKey":"k","planRevision":"<rev>","at":"<iso>",
345
+ "ok":false,"results":[{"instance":"dev-1-child","home":"/abs/child","ok":false,"code":"E_SESSION_STOP_FAILED","message":"…still running after 1500 ms; nothing was escalated","stillRunning":[4242]},
346
+ {"instance":"dev-1","home":"/abs/home","ok":true,"stopped":true,"alreadyIdle":false,"state":"shell"}],
347
+ "retained":["home","work","transcript","launch"],"replayed":false}
348
+ ```
349
+
350
+ `ok:false` means at least one target is still running; the receipt says which
351
+ pid. Nothing was killed harder. A replay (`replayed:true`) is the recorded
352
+ receipt for that key, not a second action.
353
+
354
+ ### `oats retire <instance> --plan [--home <abs>] [--dir <d>] --json`
355
+
356
+ What Remove would touch, with the design's defaults. Read-only.
357
+
358
+ ```json
359
+ {"lifecycleApi":1,"action":"retire","instance":"dev-1","home":"/abs/home","at":"<iso>",
360
+ "facts":{"session":{…},"work":{…K1 summary…},"workMode":"worktree","repo":"/abs/repo","recordedBranch":"agents/dev-1",
361
+ "children":[{"instance":"dev-1-child","agent":"dev","home":"/abs/child","session":{…}}],"pullRequest":"unknown"},
362
+ "defaults":{"retainWorktree":true,"deleteBranch":false,"stopChildren":true,"retainChildren":true},
363
+ "planRevision":"<24 hex>","notes":["the worktree is on feat/x, not the recorded agents/dev-1; branch actions use the worktree's branch", "…"]}
364
+ ```
365
+
366
+ `pullRequest` is **always `"unknown"` from the kernel**: forge facts belong to
367
+ the ADE's connection (P1). Branch actions use the **worktree's** branch
368
+ (`facts.work.branch`), never `recordedBranch`.
369
+
370
+ ### `oats retire <instance> [--discard-worktree] [--delete-branch] --json` — retention is the default (K3b)
371
+
372
+ Plain `retire` now **retains** a worktree-mode instance's work: the worktree
373
+ cannot stay under the removed home, so it is **re-homed** with
374
+ `git worktree move` to `<workspace>/.agents/worktrees/<repo>/<branch>` (a
375
+ `-2`, `-3` suffix if taken; detached → `detached-<oid12>`), with staged,
376
+ unstaged and untracked state intact, and the repository knows the new
377
+ location. The receipt says so:
378
+
379
+ ```json
380
+ {"retired":"dev-1","retention":{"worktree":"retained","movedTo":"/ws/.agents/worktrees/repo/feat-x","branch":"feat/x","detachedAt":null,"recordedBranch":"agents/dev-1"},
381
+ "worktreeRemoved":false,"branchDeleted":false, "workRecovery":{…}}
382
+ ```
383
+
384
+ - `--discard-worktree` restores removal (`retention.worktree: "removed"`).
385
+ - `--delete-branch` deletes the **worktree's verified branch**
386
+ (`retention.branchDeleted`), never the recorded spawn name, and implies
387
+ discarding the worktree (a checked-out branch cannot be deleted).
388
+ - A failed move keeps the home and refuses `E_WORK_PRESERVATION_FAILED` —
389
+ nothing is lost; retry or pass `--discard-worktree`.
390
+ - Non-worktree modes report `retention: null`. Quarantine/rollback paths keep
391
+ their removal semantics.
392
+ - The Remove dialog's "also delete worktree / branch" checkboxes map to these
393
+ two flags; the kernel never touches a PR.
394
+ - **Guarded apply** (what a GUI sends): `oats retire <i> --plan-revision <rev>
395
+ --idempotency-key <key> [--discard-worktree] [--delete-branch] --json`. The
396
+ revision is revalidated against a fresh plan first — facts moved →
397
+ `E_PLAN_STALE` with `details.plan` (re-render, re-confirm; nothing retired);
398
+ a repeated key **replays** the recorded receipt (`replayed: true`, JSON-v1
399
+ envelope) instead of retiring twice. A first retire prints its raw receipt
400
+ (pre-existing shape) with `planRevision`/`idempotencyKey`/`replayed:false`
401
+ added. Mint the key server-side per confirmation intent and keep it for that
402
+ intent's retries.
403
+ - **Children first, kernel-owned.** The plan's `facts.children` are stopped
404
+ by the kernel before retirement (bounded SIGTERM, never escalated) and
405
+ retained; the receipt lists `childrenStopped[]`. A child still running
406
+ after the grace **refuses the whole retirement** — `E_CHILDREN_RUNNING`
407
+ with `details.childrenStopped` (pids) and `details.plan`; nothing retired.
408
+ - **Branch deletion is bound to the confirmed branch.** The kernel re-verifies
409
+ the worktree's branch at the moment of deletion, after hooks (which may
410
+ mutate the tree); a mismatch deletes nothing and reports
411
+ `retention.branchDeletionSkipped {expected, actual, reason}`.
412
+ - **Ambiguous parentage is reported, never acted on.** Recorded parentage is
413
+ a bare name; if a child's parent name resolves to several homes under the
414
+ root, that child appears under `ambiguous[]` — `plan.ambiguous` on a stop
415
+ plan, `plan.facts.ambiguous` on a retire plan — with the reason, and is
416
+ excluded from `targets`/`children`.
417
+ - **Stop replay horizon**: stop receipts are stored **per idempotency key**
418
+ (`<home>/.oats-stop-receipt.<key>.json`); any earlier key replays its own
419
+ receipt for as long as the home exists. Retire receipts live beside the
420
+ instances directory and replay after the home is gone.
421
+
422
+ ### Feature advertisement — gate every new command on the probe
423
+
424
+ `oats version --json` `features` now lists: `catalog`, `instance-git`,
425
+ `instance-git-remote`, `souls-declarations`, `lifecycle-plans`,
426
+ `retire-retention`, `readiness`, `spawn-preview`, `instance-events`,
427
+ `schedule-history`, and carries the API integers (`instanceGitApi`, `soulsApi`,
428
+ `lifecycleApi`, `readinessApi`, `spawnPreviewApi`, `eventsApi`,
429
+ `scheduleHistoryApi`). **Gate on these, never on a version string and never by
430
+ optimistic invocation**: an older CLI ignores an unknown `--plan` on `retire`
431
+ and *retires*. Absent feature → the view is unavailable.
432
+
433
+ ## Instruction refresh (`oats session recompose`, feature `session-recompose`, OATS 0.24.8+)
434
+
435
+ A live instance's composed `AGENTS.md` is generated at spawn and outranks any
436
+ mail or tracked file *in the running context*. When a soul changes (a role or
437
+ budget amendment) and a respawn is not possible or wanted, an operator
438
+ refreshes the home in place:
439
+
440
+ - `oats session recompose --home <abs> [--dry-run] --json` → `{home, instance,
441
+ agent, soulDir, contextDir, changed, dryRun, blocks[{source,file}], previous,
442
+ note}`. Same composer spawn used, the home's own `soul` link and recorded
443
+ context/work mode. `changed:false` is a no-op (no receipt). On change the
444
+ prior text is retained as `previous` (`<home>/.oats-agents-md.<stamp>.previous`),
445
+ `instance.json` gains `instructions[]`/`recomposedAt`, and a `recomposed`
446
+ event is appended.
447
+ - **Nothing is signalled or restarted** — the harness re-reads on its own
448
+ schedule; the receipt's `note` says so. Refuses a retiring home
449
+ (`E_INSTANCE_RETIRING`), captured incarnations and capability-defined souls
450
+ (`E_UNSUPPORTED_MODE`: those are refreshed by a new resolution / package).
451
+ - Gate on `features.includes("session-recompose")`. It is an **operator
452
+ action** (the human or the instance's parent), never something a Desktop
453
+ poll or an agent runs on itself.
151
454
 
152
455
  ## Mutations exposed to Desktop v1
153
456
 
@@ -1,6 +1,6 @@
1
1
  # OATS v0.24.7 — instance Git observation, soul declarations, and no hollow agents
2
2
 
3
- Kernel/Pi/Desktop **0.24.7**. Two additive read-only contracts for the Desktop
3
+ Kernel/Pi/Desktop **0.24.7**. Tag `v0.24.7` → commit `c923d81b` (this notes commit; the last code change is `912aaee0`, PR #57). The npm tarball's `gitHead` is `c923d81b`; the version-bump commit lands after the tag (PR #58). A from-the-tag verifier should expect `gitHead == c923d81b`. Two additive read-only contracts for the Desktop
4
4
  (`instanceGitApi: 1`, `soulsApi: 1`), one spawn refusal that closes a
5
5
  first-team-path defect, and four Desktop parity slices.
6
6
 
@@ -53,7 +53,7 @@ first-team-path defect, and four Desktop parity slices.
53
53
  which ran Git against every instance tree on every roster poll without helper
54
54
  controls and substituted healthy zeros on failure — is **removed**. Desktop
55
55
  Git reads are the K1 route only. GitHub/PR card is *unavailable* pending the
56
- `oats.git` decision.
56
+ P1 decision: forge connections are an ADE/workstation integration, not a capability (working names `oats.git`/`oats.forge` retired).
57
57
  - **3** Souls + Sources on K4: declarations, recorded provenance (`null` renders
58
58
  *Unrecorded*, never *Local*), "Not declared" vs "Not reported" distinguished,
59
59
  sources-installed ≠ Ready.
@@ -0,0 +1,107 @@
1
+ # OATS v0.24.8 — lifecycle plans, readiness, spawn preview, instance events, and guarded Stop/Remove in the Desktop
2
+
3
+ Kernel/Pi/Desktop **0.24.8**. Tag `v0.24.8` → the commit carrying these notes
4
+ (the last code change is `508fda04`, PR #69). The npm tarball's `gitHead` is
5
+ the tagged commit; the version-bump commit lands after the tag. All new
6
+ contracts are **advertised**: consumers gate on `oats version --json`
7
+ `features[]` names and the API integers below — never on the version number,
8
+ never by optimistic invocation.
9
+
10
+ ## Kernel — the remaining Desktop-parity seams (K3, K5, K6, K7, K8)
11
+
12
+ - **Lifecycle plans** (K3, `lifecycleApi: 1`, feature `lifecycle-plans`):
13
+ `oats instance stop <i> --plan|--apply` and `oats retire <i> --plan`, then the
14
+ **guarded apply** `oats retire <i> --plan-revision <rev> --idempotency-key <key>
15
+ [--discard-worktree] [--delete-branch]`. A plan states producer facts (ordered
16
+ targets deepest-first, session/work facts, recorded children, `midTask:
17
+ "unknown"` never inferred) under a 24-hex `planRevision`; apply re-reads and
18
+ refuses `E_PLAN_STALE` (fresh plan attached) when facts moved. Keys replay
19
+ their own receipt instead of acting twice — stop receipts are stored **per
20
+ key**, retire receipts survive the home's removal.
21
+ - **Retention by default** (feature `retire-retention`): a plain retire
22
+ re-homes the worktree to `<workspace>/.agents/worktrees/<repo>/<branch>`
23
+ instead of deleting it; `--discard-worktree` removes; `--delete-branch`
24
+ deletes the branch the tree is **verified** to be on and implies discard.
25
+ Receipt `retention {worktree: retained|removed|absent, movedTo, branch,
26
+ recordedBranch, branchDeleted?, branchDeletionSkipped?}`; a failed move is
27
+ `E_WORK_PRESERVATION_FAILED` with the home kept.
28
+ - **Children first, kernel-owned.** A guarded retire stops the plan's recorded
29
+ children (bounded SIGTERM, never escalated) and retains them; a child still
30
+ running refuses the whole retirement — `E_CHILDREN_RUNNING`, nothing retired.
31
+ - **Branch deletion is bound to the confirmed branch**: re-verified at deletion,
32
+ after hooks (which may mutate the tree); a mismatch deletes nothing and
33
+ reports `branchDeletionSkipped {expected, actual, reason}`.
34
+ - **Ambiguous parentage is reported, never acted on**: parent edges are bare
35
+ names; one that resolves to several homes appears under `ambiguous[]`
36
+ (`plan.ambiguous` on stop, `plan.facts.ambiguous` on retire).
37
+ - A pre-plan CLI does not understand `--plan` and would retire on it; that is
38
+ exactly why the feature is advertised — consumers must gate.
39
+ - **Readiness** (K5, `readinessApi: 1`): `oats readiness [--soul] [--home]
40
+ [--verify-signatures] [--policy] --json` — the quartet
41
+ installed · trusted · configured · enrolled, each `pass|fail|unknown|not-applicable`
42
+ with items `{subject, required, reason, producer, evidence, remedy}`;
43
+ `summary.ready` is never vacuous. Signatures are `unknown` unless
44
+ `--verify-signatures` (an explicit one-commit fetch; `verified|unsigned|invalid`
45
+ with the signer named). Enrolled = workspace-member admission.
46
+ - **Enforced child policy** (K5): soul `children: {spawn: bool}`, spawn
47
+ `--allow-child-spawns|--no-child-spawns`, recorded as `instance.json`
48
+ `policy.childSpawns {allowed, origin}`; a spawn with `--parent` under an off
49
+ policy refuses `E_CHILD_SPAWNS_DISABLED`. `inspect --json` souls carry
50
+ `declarations.children`; capability rows carry `commit`.
51
+ - **Spawn preview** (K6, `spawnPreviewApi: 1`): `oats spawn … --preview --json`
52
+ states the decision — instance name, home, worktree, branch, base `{ref, oid}`,
53
+ runtime/model/launch config, work mode, composition sources, policy — without
54
+ creating anything. `--base <ref>` picks the base; `@native-default` names the
55
+ runtime's own default explicitly.
56
+ - **Instance events** (K7, `eventsApi: 1`, feature `instance-events`):
57
+ `oats instance events <i> [--limit] [--since] --json` — typed, producer-
58
+ attributed lifecycle events (spawned, launched, restarted, stopped,
59
+ stop-refused, retire-planned, retired, worktree-retained, branch-deleted,
60
+ child-spawn-refused, recomposed) from the home's log and the workspace's
61
+ retained copy; truncation and unreadable lines are visible, never smoothed.
62
+ - **Schedule run history** (K8, `scheduleApi: 2`, feature `schedule-history`):
63
+ schedules report `recentRuns` — settled runs only, producer order, ≤ 50.
64
+ - **Instruction refresh** (feature `session-recompose`): `oats session recompose
65
+ --home <abs> [--dry-run] --json` recomposes a **live** home's `AGENTS.md`
66
+ from its current soul with the same composer spawn used; previous text is
67
+ retained beside it, `instance.json` and a `recomposed` event record it,
68
+ nothing is restarted. An operator action for when a role changes and a
69
+ respawn is not possible or wanted.
70
+ - Remote workspaces: `oats instance git|diff` reach a remote instance through
71
+ the negotiated route (K1 remote), refusing rather than guessing where no
72
+ route exists.
73
+
74
+ ## Desktop (parity slices 2b and 2c)
75
+
76
+ - **2b — Connections.** Settings → Connections with an inline "Connect
77
+ GitHub" (device flow through `gh`, credential custody stays with `gh`; the
78
+ auth pane is a closed key set with redaction). The Git & GitHub panel gains an
79
+ observation-bound PR card: informational, correlated to the exact K1
80
+ revision/branch/remote, never authority. Forge connections are an **ADE
81
+ integration**, not a capability.
82
+ - **2c — Stop and Remove.** The instance menu offers *Stop…* and *Remove
83
+ instance…*; both open a plan-backed confirmation showing the kernel's exact
84
+ targets, skipped/ambiguous children, session and work facts, the real branch
85
+ (recorded-branch drift noted separately) and retained-by-default choices.
86
+ One guarded local route; the plan is read from the CLI, each confirmation
87
+ gets an opaque reference, the idempotency key is minted on the first
88
+ confirmation and kept for that intent's retries, one apply at a time; lost
89
+ responses are shown as **unknown outcome**, never "no effect". Stale plans
90
+ require a new confirmation; a refused child stop names the child. The old
91
+ unguarded retire route now answers `E_PLAN_REQUIRED`.
92
+ - Everything above is **unavailable** (not hidden, not guessed) when the
93
+ installed CLI does not advertise the corresponding feature.
94
+
95
+ ## Also
96
+
97
+ - Lessons recorded: consumers gate on advertised features, never on
98
+ optimistic invocation; a declaration that suppresses a default must be
99
+ enforced; read-only Git observation is a security boundary.
100
+ - Verification budget: one full gate per change is PR CI; locally only the
101
+ affected suites. Desktop-only changes run the Desktop suites plus the
102
+ focused suite.
103
+
104
+ ## Upgrade
105
+
106
+ `npm i -g @awebai/oats@0.24.8`, then `oats doctor`. Desktops pinned to an
107
+ older CLI keep working; new controls appear as the CLI's advertised features do.