@awebai/oats 0.40.2 → 0.41.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.
package/bin/oats.mjs CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  capabilityManifests, capabilityTrust, capabilityExecutablePath,
32
32
  officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
33
33
  findInstanceHome, findInstanceHomes, enclosingInstanceHome, logicalCwd, readableInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, recordedKernelBin, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, validateLaunchConfigDefaults, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, withSafeTaskPrompt, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
34
+ FAILED_SPAWN_BRANCH_LEFT, RETIRE_DELETE_BRANCH_REFUSED,
34
35
  } from "../lib/core.mjs";
35
36
  import {
36
37
  writeFileAtomic, LOCK_FILE, readLock, readLockIfPresent, writeLock, resolvePackages, memoizedRemote,
@@ -2458,9 +2459,22 @@ async function spawnCmd() {
2458
2459
  console.log(` attach: ${r.attach}`);
2459
2460
  }
2460
2461
 
2462
+ /** The line naming the branch a FAILED_SPAWN_BRANCH_LEFT item speaks of, on a line of its own: the item names
2463
+ * none. The receipt's `retention.recordedBranch` when the worktree step ran; otherwise the retained home's
2464
+ * recorded branch, read here (`local`), or where to read it on the host. Null when no item asks for it. */
2465
+ function failedSpawnBranchLine(r, items, { local = false, host } = {}) {
2466
+ if (!items?.includes(FAILED_SPAWN_BRANCH_LEFT)) return null;
2467
+ let name = r.retention?.recordedBranch ?? null;
2468
+ if (!name && local && r.retainedHome) { try { name = JSON.parse(readFileSync(join(r.retainedHome, "instance.json"), "utf8")).branch ?? null; } catch { /* said below */ } }
2469
+ if (name) return ` branch: ${name}`;
2470
+ return r.retainedHome ? ` branch: the "branch" recorded in ${join(r.retainedHome, "instance.json")}${host ? ` on ${host}` : ""}` : null;
2471
+ }
2472
+
2461
2473
  function retireCmd() {
2462
2474
  const name = args[1];
2463
- if (!name || name.startsWith("--")) die("usage: oats retire <instance> [--plan] [--plan-revision <rev> --idempotency-key <key>] [--home <path>] [--self] [--discard-worktree] [--delete-branch] [--keep-dir] [--force] [--json]");
2475
+ // Refused before anything else: before --plan, a replay, a plan revision or a recorded child is stopped.
2476
+ if (args.includes("--delete-branch")) return args.includes("--json") ? jsonFail("E_BAD_ARGS", RETIRE_DELETE_BRANCH_REFUSED) : die(RETIRE_DELETE_BRANCH_REFUSED);
2477
+ if (!name || name.startsWith("--")) die("usage: oats retire <instance> [--plan] [--plan-revision <rev> --idempotency-key <key>] [--home <path>] [--self] [--discard-worktree] [--keep-dir] [--force] [--json]");
2464
2478
  let homeFlag = flag("home");
2465
2479
  if (homeFlag === true) die("--home needs the instance home path");
2466
2480
  if (args.includes("--plan")) {
@@ -2471,7 +2485,7 @@ function retireCmd() {
2471
2485
  const plan = planRetire(dirFlag(), root, name, { home: homeFlag });
2472
2486
  if (args.includes("--json")) { jsonOk(plan); return; }
2473
2487
  console.log(`retire ${name} — plan ${plan.planRevision}`);
2474
- console.log(` session ${plan.facts.session.state}; work ${plan.facts.work.observed ? `${plan.facts.work.changed} changed / ${plan.facts.work.untracked} untracked on ${plan.facts.work.branch ?? "detached"}` : `not observed (${plan.facts.work.reason})`}; children ${plan.facts.children.length}; pull request ${plan.facts.pullRequest}`);
2488
+ console.log(` session ${plan.facts.session.state}; work ${plan.facts.work.observed ? `${plan.facts.work.changed} changed / ${plan.facts.work.untracked} untracked on ${plan.facts.work.branch ?? (plan.facts.work.detached ? "detached" : "a ref OATS carries no branch name for")}` : `not observed (${plan.facts.work.reason})`}; children ${plan.facts.children.length}; pull request ${plan.facts.pullRequest}`);
2475
2489
  console.log(` defaults: retain worktree ${plan.defaults.retainWorktree}, delete branch ${plan.defaults.deleteBranch}, stop children ${plan.defaults.stopChildren}`);
2476
2490
  for (const n of plan.notes) console.log(` note: ${n}`);
2477
2491
  return;
@@ -2492,7 +2506,7 @@ function retireCmd() {
2492
2506
  const planRev = flag("plan-revision"), idemKey = flag("idempotency-key");
2493
2507
  if (planRev === true || idemKey === true) die("--plan-revision and --idempotency-key need values");
2494
2508
  if ((planRev !== undefined) !== (idemKey !== undefined)) die("--plan-revision and --idempotency-key go together");
2495
- let replayPath = null, childrenStopped = null, expectedBranch;
2509
+ let replayPath = null, childrenStopped = null;
2496
2510
  if (planRev !== undefined) {
2497
2511
  if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(idemKey)) die("--idempotency-key: 1-128 chars of [A-Za-z0-9._:-]");
2498
2512
  // Replay first: after a successful retire the home is gone, so the receipt
@@ -2513,10 +2527,9 @@ function retireCmd() {
2513
2527
  }
2514
2528
  const running = childrenStopped.filter((k) => !k.ok);
2515
2529
  if (running.length) return args.includes("--json") ? jsonFail("E_CHILDREN_RUNNING", `${running.map((k) => k.instance).join(", ")} ${running.length === 1 ? "is" : "are"} still running after a bounded stop; nothing was retired and nothing was escalated`, { childrenStopped, plan: fresh }) : die(`children still running: ${running.map((k) => k.instance).join(", ")}; nothing retired`);
2516
- expectedBranch = fresh.facts.work.observed ? fresh.facts.work.branch : undefined;
2517
2530
  }
2518
2531
  let r;
2519
- try { r = retireInstance(root, name, { home: homeFlag, self: isSelf, deleteBranch: args.includes("--delete-branch"), discardWorktree: args.includes("--discard-worktree"), keepDir: args.includes("--keep-dir"), force: args.includes("--force"), ...(expectedBranch !== undefined ? { expectedBranch } : {}) }); }
2532
+ try { r = retireInstance(root, name, { home: homeFlag, self: isSelf, discardWorktree: args.includes("--discard-worktree"), keepDir: args.includes("--keep-dir"), force: args.includes("--force") }); }
2520
2533
  catch (e) { if (!e?.code) throw e; return args.includes("--json") ? jsonFail(e.code, e.message, e.candidates ? { ...e.details, candidates: e.candidates } : e.details) : die(e.message); }
2521
2534
  if (childrenStopped) r.childrenStopped = childrenStopped;
2522
2535
  if (replayPath) { r.planRevision = planRev; r.idempotencyKey = idemKey; r.replayed = false; try { writeFileAtomic(replayPath, JSON.stringify(r, null, 2)); } catch { /* receipt is evidence, not authority */ } }
@@ -2539,6 +2552,8 @@ function retireCmd() {
2539
2552
  if (r.forcedIncomplete) {
2540
2553
  console.error(`Removed ${r.retired} under --force with cleanup INCOMPLETE — this external state was NOT cleaned up and is now yours to remove by hand:`);
2541
2554
  for (const f of r.forcedIncomplete) console.error(` ${f}`);
2555
+ const branch = failedSpawnBranchLine(r, r.forcedIncomplete, { local: true });
2556
+ if (branch) console.error(branch);
2542
2557
  }
2543
2558
  if (args.includes("--json")) { console.log(JSON.stringify(r, null, 2)); if (r.rollbackIncomplete) process.exit(1); return; }
2544
2559
  // An unsuccessful cleanup retry must NOT read as a completed retirement: the
@@ -2547,10 +2562,12 @@ function retireCmd() {
2547
2562
  if (r.rollbackIncomplete) {
2548
2563
  console.error(`Cleanup for ${r.retired} is INCOMPLETE — the instance home is retained at ${r.retainedHome} because external state may still exist:`);
2549
2564
  for (const f of r.rollbackIncomplete) console.error(` ${f}`);
2565
+ const branch = failedSpawnBranchLine(r, r.rollbackIncomplete, { local: true });
2566
+ if (branch) console.error(branch);
2550
2567
  console.error(`Fix the cause and re-run \`oats retire ${r.retired}\`; the home holds the state that cleanup needs.`);
2551
2568
  process.exit(1);
2552
2569
  }
2553
- console.log(`Retired ${r.retired} (agent ${r.agent})${r.worktreeRemoved ? ", worktree removed" : ""}${r.branchDeleted ? ", branch deleted" : ""}`);
2570
+ console.log(`Retired ${r.retired} (agent ${r.agent})${r.worktreeRemoved ? ", worktree removed" : ""}`);
2554
2571
  // Preserving work and not saying so leaves the operator believing it is gone,
2555
2572
  // which is most of the harm of deleting it. Name the classes and the path.
2556
2573
  for (const recovery of r.workRecoveries || (r.workRecovery ? [r.workRecovery] : [])) {
@@ -3673,6 +3690,8 @@ async function serverRouteCmd() {
3673
3690
  }
3674
3691
  rest.push(a);
3675
3692
  }
3693
+ // Refused here too, before anything is sent: a host on an older OATS would still delete the branch.
3694
+ if (cmd === "retire" && rest.includes("--delete-branch")) bail("E_BAD_ARGS", RETIRE_DELETE_BRANCH_REFUSED);
3676
3695
  let routed;
3677
3696
  try { routed = routeCommand(id, cmd, rest); }
3678
3697
  catch (e) { bail(e.code || "E_SSH", e.message, e.details); }
@@ -3694,7 +3713,7 @@ async function serverRouteCmd() {
3694
3713
  console.log(` remote home: ${r.home}`);
3695
3714
  console.log(` route snapshot: ${r.snapshot ? shortPath(r.snapshot) : "none (see the warning)"}`);
3696
3715
  for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
3697
- console.log(` attach: ssh -t ${r.target.sshHost} tmux attach -t ${r.tmux?.session || "oats"}`);
3716
+ console.log(` attach: oats session attach --server ${shellQuote(id)} --instance ${shellQuote(r.instance)}`);
3698
3717
  } else if (cmd === "retire") {
3699
3718
  // Everything the local retireCmd tells the operator, for a remote home
3700
3719
  // they cannot see: forced-incomplete state now theirs to remove by hand
@@ -3702,6 +3721,8 @@ async function serverRouteCmd() {
3702
3721
  if (r.forcedIncomplete) {
3703
3722
  console.error(`Removed ${r.retired} on ${id} under --force with cleanup INCOMPLETE — this external state was NOT cleaned up and is now yours to remove by hand on ${target.sshHost}:`);
3704
3723
  for (const f of r.forcedIncomplete) console.error(` ${f}`);
3724
+ const branch = failedSpawnBranchLine(r, r.forcedIncomplete, { host: target.sshHost });
3725
+ if (branch) console.error(branch);
3705
3726
  }
3706
3727
  console.log(`Retired ${r.retired} on ${id}${r.deferred ? " (deferred completion scheduled there)" : ""}${r.rollbackIncomplete ? " — cleanup INCOMPLETE on the server, home retained there" : ""}`);
3707
3728
  for (const recovery of r.workRecoveries || (r.workRecovery ? [r.workRecovery] : [])) {
@@ -3709,7 +3730,13 @@ async function serverRouteCmd() {
3709
3730
  console.log(` ${recovery.path}${typeof recovery.bytes === "number" ? ` (${formatBytes(recovery.bytes)})` : ""}`);
3710
3731
  for (const line of preservedOutputLines(recovery)) console.log(line);
3711
3732
  }
3712
- if (r.rollbackIncomplete) { for (const f of r.rollbackIncomplete) console.error(` ${f}`); console.error(`Fix the cause there and re-run \`oats retire ${r.retired} --server ${id}\`.`); process.exit(1); }
3733
+ if (r.rollbackIncomplete) {
3734
+ for (const f of r.rollbackIncomplete) console.error(` ${f}`);
3735
+ const branch = failedSpawnBranchLine(r, r.rollbackIncomplete, { host: target.sshHost });
3736
+ if (branch) console.error(branch);
3737
+ console.error(`Fix the cause there and re-run \`oats retire ${r.retired} --server ${id}\`.`);
3738
+ process.exit(1);
3739
+ }
3713
3740
  } else {
3714
3741
  console.log(`oats status — server ${id} (ssh ${r.target.sshHost}, workspace ${r.target.workspace})\n`);
3715
3742
  for (const a of r.agents || []) {
@@ -3987,7 +4014,7 @@ Usage:
3987
4014
  decision binds an apply (--expect-decision <rev>);
3988
4015
  --max-age reuses recent heads (preview only)
3989
4016
  oats retire <instance> [--force] retire an instance (window, hooks,
3990
- [--self] [--delete-branch] worktree, home); --self = retire the
4017
+ [--self] worktree, home); --self = retire the
3991
4018
  [--keep-dir] [--json] CALLING instance: the window dies, then
3992
4019
  a detached external retirement runs
3993
4020
  oats inspect [--dir <scope>] [--soul <name> one authoritative JSON answer for a GUI: souls
@@ -4084,13 +4111,13 @@ Usage:
4084
4111
  with origins (captured homes refuse: the
4085
4112
  captured/portable path was removed in 0.26)
4086
4113
  oats retire <instance> --plan [--json] what Remove would touch, with retention defaults
4087
- oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree] [--delete-branch]
4114
+ oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree]
4088
4115
  with a plan revision: refuses E_PLAN_STALE (fresh plan
4089
4116
  attached) if facts moved; a repeated key replays
4090
4117
  retire; a worktree is RETAINED (re-homed under
4091
4118
  <workspace>/.agents/worktrees/<repo>/<branch>)
4092
- unless discarded; --delete-branch deletes the
4093
- worktree's verified branch and implies discard
4119
+ unless discarded; no retire deletes a branch
4120
+ (--delete-branch is refused)
4094
4121
  oats root print this package's install root
4095
4122
  (adapters resolve the kernel from it)
4096
4123
 
@@ -117,7 +117,15 @@ A self-contained package has an `oats.json`:
117
117
  `oats status` reports it as retained state rather than a live instance, and
118
118
  `oats retire <instance>` retries the cleanup — re-running the retire hooks and
119
119
  the worktree removal, verifying both, and verifying (never deleting) the
120
- branch: a branch is deleted only with `--delete-branch`. A retry that still cannot
120
+ branch: no retire deletes a branch. While the branch the failed spawn created
121
+ is still there, the retry stays incomplete with the item `the branch the
122
+ failed spawn created is left: OATS does not delete it. Inspect it and delete
123
+ it with Git if it is not wanted, then retry`. The item names no branch: the
124
+ CLI prints it on the next line, from the receipt's
125
+ `retention.recordedBranch` when the worktree step ran, otherwise from the
126
+ `branch` the retained home's `instance.json` records. While Git cannot show
127
+ the branch gone (a damaged ref, a failed read), the retry stays incomplete
128
+ with `git branch <b>: could not verify whether it still exists (…)`. A retry that still cannot
121
129
  finish keeps the home again, names what is outstanding, and exits nonzero.
122
130
  - The **escape hatch is `oats retire <instance> --force`**, for a home OATS cannot
123
131
  identify at all: no `instance.json` and no **usable** cleanup descriptor. Usable
@@ -1631,8 +1631,8 @@ with `--expect-decision` records the key and decision in `instance.json`.
1631
1631
  ```json
1632
1632
  {"instance":"rm-api","agent":"rm","home":"/w/agents/rm/instances/rm-api","work":"worktree","branch":"agents/rm-api",
1633
1633
  "base":{"ref":"github.com/nw/agents","oid":"66566512…"},"launched":true,"warnings":[],
1634
- "tmux":{"session":"oats-agents","window":"rm-api"},"backend":"tmux","repo":"/w/agents-repo","harness":"pi","model":null,"parent":null,"sibling":null,"relation":null,
1635
- "spawnOrigin":"operator","attach":"tmux attach -t oats-agents","decision":{"instance":"rm-api","revision":"c557d8ec9a272ba1c1739dc3"},"replayed":false,
1634
+ "tmux":{"session":"oats-agents","window":"rm-api","socket":"/tmp/tmux-1000/oats"},"backend":"tmux","repo":"/w/agents-repo","harness":"pi","model":null,"parent":null,"sibling":null,"relation":null,
1635
+ "spawnOrigin":"operator","attach":"tmux -S /tmp/tmux-1000/oats attach -t oats-agents","decision":{"instance":"rm-api","revision":"c557d8ec9a272ba1c1739dc3"},"replayed":false,
1636
1636
  "wake":{"requested":false,"saved":null,"error":null},"launchConfig":null,
1637
1637
  "launch":{"version":2,"harness":"pi","launchConfig":null,"launchConfigSource":null,"executable":"/usr/local/bin/pi","executableDeclared":null,
1638
1638
  "executableResolvedFrom":"PATH","args":[],"env":{},"model":null,"hooks":{"launch":{},"env":{},"contributions":[]},"prompt":{"kind":"task-file","file":"TASK.md"}}}
@@ -1642,10 +1642,21 @@ with `--expect-decision` records the key and decision in `instance.json`.
1642
1642
 
1643
1643
  - Always present: `instance, agent, home, work, branch, base ({ref, oid}
1644
1644
  the new branch started at; `null` without one), launched, warnings
1645
- (array), tmux ({session, window} | null), backend ("tmux"), repo, harness,
1645
+ (array), tmux ({session, window, socket?} | null), backend ("tmux"), repo, harness,
1646
1646
  model, parent,
1647
1647
  sibling, relation, spawnOrigin (operator | instance), attach, launchConfig,
1648
1648
  launch` (the redacted recipe).
1649
+ - `tmux.socket` is the absolute socket of the tmux server the window was
1650
+ created on; a launched spawn has it, a `--no-launch` one does not. It is
1651
+ the OATS tmux server's
1652
+ ([execution-targets.md](execution-targets.md#the-oats-tmux-server)), where
1653
+ earlier kernels recorded the default server's; the shape is unchanged.
1654
+ - `attach` is one string, a command for a person to paste, the same in text
1655
+ and JSON. It is `tmux -S <tmux.socket> attach -t <session>` for a
1656
+ launched spawn (earlier kernels: `tmux attach -t <session>`) and `oats session attach
1657
+ --home <home>` for `--no-launch`. A value is single-quoted only when it
1658
+ holds a character outside `A-Za-z0-9_./:-`. It is not a field to parse:
1659
+ read `tmux` for the target.
1649
1660
  - When they apply: `yolo`, `decision` and
1650
1661
  `replayed` (bound apply), `wake` (keyed apply), `wakeSchedule` and
1651
1662
  `wakeScheduleError` (a requested wake).
@@ -1658,7 +1669,8 @@ Feature `spawn-name`. `--name <slug>` is the exact name, with no prefix.
1658
1669
  - A name that is not a slug (lowercase letters and digits, single dashes),
1659
1670
  equals a soul name, or exceeds 64 characters (derived names included, with
1660
1671
  their suffix) is `E_INSTANCE_NAME_INVALID`.
1661
- - A name any soul's `instances/` holds, or a live tmux window carries, is
1672
+ - A name any soul's `instances/` holds, or a live window of the target
1673
+ session on the OATS tmux server carries, is
1662
1674
  `E_INSTANCE_NAME_TAKEN {instance, home, session?}`; a typed name never gets
1663
1675
  a silent `-2`.
1664
1676
  - The name is part of the decision.
@@ -2185,7 +2197,8 @@ oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--
2185
2197
  after open), and at most its last 4 MiB is read (`"tail"`).
2186
2198
  - **Kinds:** `spawned`, `launched`, `restarted`, `stopped`, `stop-refused`,
2187
2199
  `retire-planned`, `retired`, `worktree-retained`, `worktree-removed`,
2188
- `branch-deleted`, `child-spawn-refused`, `launch-warning` (0.30: a
2200
+ `branch-deleted` (not written since 0.41.0: no retire deletes a branch;
2201
+ older logs hold it), `child-spawn-refused`, `launch-warning` (0.30: a
2189
2202
  `launch` hook's warning at session start/restart, `data: {message}`),
2190
2203
  `recomposed` (from earlier kernels), `waiting` (0.40: a producer's claim,
2191
2204
  [Waiting on you](#waiting-on-you)). `producer` is `kernel`, a capability id,
@@ -2198,6 +2211,14 @@ oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--
2198
2211
  is dated at the receipt's launch time. The boundary is complete per log: a
2199
2212
  log that missed it gets a copy of the same row (same time and data), never
2200
2213
  a second one.
2214
+ - `retired` (`data: {agent, keepDir, self, quarantine, workRecovery, hooks,
2215
+ reason?}`) is written to the workspace log only,
2216
+ `<deployment>/.agents/events/<agent>--<instance>.jsonl`, which outlives the
2217
+ home. `reason` is present only when
2218
+ the retire completed a self-retire an older OATS recorded with
2219
+ `--delete-branch` (since 0.41.0: `this self-retire was requested with
2220
+ --delete-branch by an older OATS; retirement no longer deletes branches, so
2221
+ the branch and the worktree were left`).
2201
2222
  - **Incarnation.** Each row carries the writing home's `createdAt` (or
2202
2223
  `null` for old rows); the top-level `incarnation` is the current home's (or
2203
2224
  `null`). Earlier incarnations are returned as this address's history.
@@ -2442,7 +2463,7 @@ Plain `retire` keeps a worktree-mode instance's work: the worktree is moved
2442
2463
  `-2` suffix if taken; `detached-<oid12>` when detached), state intact.
2443
2464
 
2444
2465
  ```text
2445
- oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree] [--delete-branch] [--home <abs>] --json
2466
+ oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree] [--home <abs>] --json
2446
2467
  ```
2447
2468
 
2448
2469
  A first retire prints the **raw receipt**, not an envelope:
@@ -2458,8 +2479,7 @@ A first retire prints the **raw receipt**, not an envelope:
2458
2479
  ```
2459
2480
 
2460
2481
  - `retention`: `{worktree: "retained" | "removed" | "absent", movedTo?,
2461
- branch, detachedAt?, recordedBranch, branchDeleted?,
2462
- branchDeletionSkipped?: {expected, actual, reason}}`, or `null` when no
2482
+ branch, detachedAt?, recordedBranch}`, or `null` when no
2463
2483
  worktree step ran: a non-worktree mode, or a worktree kept for the retry.
2464
2484
  A retire whose hooks left cleanup outstanding keeps the worktree exactly as
2465
2485
  it was (with `worktreeRemoved: false`) and says why in `rollbackIncomplete`
@@ -2468,14 +2488,30 @@ A first retire prints the **raw receipt**, not an envelope:
2468
2488
  entry is gone is never touched: it is an incomplete item (`git worktree
2469
2489
  <path>: its admin entry is missing; …`), and `--force` refuses it with
2470
2490
  `E_WORK_PRESERVATION_FAILED`.
2471
- - `--discard-worktree` removes the worktree. `--delete-branch` deletes the
2472
- worktree's verified branch (re-verified at deletion time) and implies
2473
- discarding; a mismatch deletes nothing and reports
2474
- `branchDeletionSkipped`. Without `--delete-branch` no retire deletes a
2475
- branch, a retried or `--force`d quarantine included. A failed spawn's
2476
- quarantine that still owes the branch the spawn created stays incomplete
2477
- (`git branch <b>: kept; the failed spawn created it; pass --delete-branch to
2478
- delete it`).
2491
+ - `--discard-worktree` removes the worktree. No retire deletes a branch, a
2492
+ retried or `--force`d quarantine included: `branchDeleted` is always
2493
+ `false`, and `retention.branchDeleted` and `retention.branchDeletionSkipped`
2494
+ are not written. `--delete-branch` is refused with `E_BAD_ARGS` (`oats
2495
+ retire no longer deletes branches: …`) before any effect: before a plan
2496
+ revision is compared and before a recorded child is stopped. A failed
2497
+ spawn's quarantine that still owes the branch the spawn created stays
2498
+ incomplete while the branch is there (`the branch the failed spawn created
2499
+ is left: OATS does not delete it. Inspect it and delete it with Git if it
2500
+ is not wanted, then retry`; the item names no branch:
2501
+ `retention.recordedBranch` has it when the worktree step ran, otherwise the
2502
+ retained home's `instance.json` `branch`) or while
2503
+ Git cannot show it gone (`git branch <b>: could not verify whether it still
2504
+ exists (…)`). Such a home cannot be completed from Desktop: the operator
2505
+ deletes the branch with Git and retries, or uses `--force` from the CLI.
2506
+ - Before the worktree is removed, HEAD is read again: a HEAD that moved since
2507
+ the retire's last inspection stops it with `E_WORK_PRESERVATION_FAILED`
2508
+ (`the worktree's HEAD changed after it was inspected, so the worktree was
2509
+ not removed. The home and the worktree are kept, and so is any recovery the
2510
+ retire wrote; retry the retire.`), and a HEAD that cannot be read with
2511
+ `E_WORK_INSPECTION_FAILED`. Either may follow earlier effects of the same
2512
+ retire (hooks run, a recovery copied), and neither says that one happened:
2513
+ an error here is not proof that nothing happened, nor that a recovery
2514
+ exists.
2479
2515
  - `workRecovery` (or `workRecoveries[]`): `{path, classes, bytes, outputs?,
2480
2516
  repoCopy?}`; `outputs: {paths: [{path, bytes}], bytes}` names what was
2481
2517
  copied beyond tracked state, largest first.
@@ -2562,6 +2598,12 @@ selection flags. See [the start workflow](desktop-instance-start.md).
2562
2598
  instance's events as a `launch-warning` row, `data: {message}`. They are
2563
2599
  advisory: the start went ahead. Earlier kernels omit the field; read a
2564
2600
  missing `warnings` as `[]`.
2601
+ - The kernel adds one warning of its own, in the same array and as
2602
+ the same event: when the start had to create the window again and created
2603
+ it on a tmux server other than the one the home recorded, the line names
2604
+ the instance, the old socket and the new one (each as a JSON string).
2605
+ `target.socket` is then the new socket
2606
+ ([execution-targets.md](execution-targets.md#existing-instances)).
2565
2607
  - A start or restart appends a `launched` event as soon as its session
2566
2608
  exists (0.40, `phase: "start"` or `"restart"`, `startId`), the session
2567
2609
  boundary that voids earlier waiting claims ([Waiting on you](#waiting-on-you)).
package/docs/desktop.md CHANGED
@@ -79,13 +79,14 @@ login shell once (`$SHELL -ilc`, 3 s timeout) and puts its PATH in front of
79
79
  the inherited one, so the CLI probe, every `oats` call, a CLI picked with
80
80
  **Choose oats…**, and the tmux server and terminals the Desktop starts all run
81
81
  with your shell's PATH. Only PATH is taken from the shell, never the rest of
82
- its environment. If the shell fails, times out or prints no PATH, the
82
+ its environment. The shell is started with your environment, without what the
83
+ Desktop or its packaging added to its own (on the AppImage, the entries under
84
+ its mount), like every other program the Desktop starts. If the shell fails, times out or prints no PATH, the
83
85
  inherited PATH stays: the backend's `/api/cli` reports `pathSource`
84
86
  (`login-shell` or `inherited`), `pathError` (why, or `null`) and
85
87
  `probePath` (the PATH the probe used), and the reason is logged at startup.
86
88
  A tmux server that was already running keeps its own environment; restart it
87
- (`tmux kill-server`, which ends its sessions) if its sessions should get the
88
- new PATH.
89
+ if its sessions should get the new PATH, which ends its sessions: `tmux -L oats kill-server` for the OATS tmux server, where instances run, and `tmux kill-server` for your default server, where an instance started by an earlier kernel may still be.
89
90
 
90
91
  ## Opening a workspace
91
92
 
@@ -322,7 +323,7 @@ error in the terminal. Each drop/paste accepts up to 16 files totaling 25 MB.
322
323
  | --- | --- |
323
324
  | "Compatible oats CLI required" card | No CLI, or a version outside the range the card itself states. Copy the card's install command, or **Choose oats…** to point at the right binary; **Retry** re-probes. Spawn is disabled until a compatible CLI is verified. |
324
325
  | Spawn disabled, no card | The probe hasn't settled yet (transient, resolves in ms). If it persists, the backend is unreachable — restart the app. |
325
- | Terminals fail to open ("could not attach") | tmux missing, or no live session for that instance. Install tmux (`tmux -V`); check `tmux ls`. |
326
+ | Terminals fail to open ("could not attach") | tmux missing, or no live session for that instance. Install tmux (`tmux -V`); check `tmux -L oats ls` (the OATS tmux server, where instances run) and `tmux ls` (your default server, where an instance started by an earlier kernel may still be). |
326
327
  | Can't select/copy text in a terminal tab | A plain drag copies on release ([Copy from a terminal](#copy-from-a-terminal)). If nothing reaches the clipboard, the program in the pane has the mouse, or your tmux config sets `set-clipboard off`: hold **Option** (macOS) or **Shift** while dragging, then copy (Cmd+C / right-click → Copy). |
327
328
  | macOS "app is damaged / can't be opened" | Ad-hoc-signed (not notarized) build + quarantine. Right-click → Open, or clear the quarantine attribute (above). If it persists, verify the bundle: `codesign --verify --deep --strict --verbose=2 "/Applications/OATS Desktop.app"` — a non-zero exit means a broken artifact, report it. |
328
329
  | Roster empty | The opened directory isn't an OATS deployment (it needs `oats-local.yaml` and `agents/`). Use the workspace switcher → Add workspace to select the right folder. |