@awebai/oats 0.40.1 → 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,
@@ -490,7 +491,7 @@ function finishOperation({ r, bail, address, provider, op, argFlags, cwd, home,
490
491
  // (a partial receipt such as result.instance of something it launched
491
492
  // before failing, and any details it gave) travels in error.details so a
492
493
  // scheduler can keep an unconfirmed outcome and reconcile that target.
493
- if (!envelope.ok) bail(envelope.error?.code || "E_OPERATION_FAILED", `${address}: ${envelope.error?.message || "failed"}`, { exit: r.status, envelope, ...(reportsRetainedEffectsText(envelope.error?.message) ? { unconfirmed: true } : {}) });
494
+ if (!envelope.ok) bail(envelope.error?.code || "E_OPERATION_FAILED", `${address}: ${envelope.error?.message || "failed"}`, { exit: r.status, envelope, ...(envelope.error?.details?.unconfirmed === true || reportsRetainedEffectsText(envelope.error?.message) ? { unconfirmed: true } : {}) });
494
495
  if (r.status !== 0) bail("E_OPERATION_RESULT", `${address} (${provider.capability} ${op.command}) answered ok but exited ${r.status}; the receipt is not trusted and its effects are unconfirmed${stderr ? `: ${stderr.slice(0, 400)}` : ""}`, observed(envelope));
495
496
  const result = envelope.result && typeof envelope.result === "object" ? envelope.result : {};
496
497
  if (op.kind === "view") {
@@ -2413,8 +2414,8 @@ async function spawnCmd() {
2413
2414
  if (e?.code === "E_PLACEMENT_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
2414
2415
  if (e?.code === "E_INSTANCE_NAME_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home ?? null, ...(e.session ? { session: e.session } : {}) }); throw e; }
2415
2416
  if (e?.code === "E_INSTANCE_NAME_INVALID") { bail(e.code, e.message, e.details); throw e; }
2416
- if (e?.code === "E_SPAWN_INCOMPLETE") { bail(e.code, e.message, { instance: e.instance, home: e.home, launched: e.launched }); throw e; }
2417
- bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
2417
+ if (e?.code === "E_SPAWN_INCOMPLETE") { bail(e.code, e.message, { ...e.details, instance: e.instance, home: e.home, launched: e.launched }); throw e; }
2418
+ bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e, e.details?.unconfirmed === true ? e.details : undefined); throw e;
2418
2419
  }
2419
2420
  // The instance exists from here on: a failed wake save is reported beside
2420
2421
  // the full receipt, never hidden, and never causes a second spawn.
@@ -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 || []) {
@@ -3818,7 +3845,7 @@ else if (cmd === "session") await sessionCmd();
3818
3845
  else if (cmd === "schedule") await scheduleCmd();
3819
3846
  else if (cmd === "trigger") await triggerCmd();
3820
3847
  else if (cmd === "automations") await automationsCmd();
3821
- else if (cmd === "spawn") { try { await spawnCmd(); } catch (e) { if (TYPED_CLI_FAILURES.has(e?.code)) throw e; if (JSON_MODE) jsonFail("E_SPAWN_FAILED", e.message || e); throw e; } }
3848
+ else if (cmd === "spawn") { try { await spawnCmd(); } catch (e) { if (TYPED_CLI_FAILURES.has(e?.code)) throw e; if (JSON_MODE) jsonFail("E_SPAWN_FAILED", e.message || e, e.details?.unconfirmed === true ? e.details : undefined); throw e; } }
3822
3849
  else if (cmd === "retire") retireCmd();
3823
3850
  else if (cmd === "capture" || cmd === "recall" || cmd === "setup") await recordCmd(cmd);
3824
3851
  else if (cmd === "experimental") await experimentalCmd();
@@ -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
@@ -4062,7 +4089,7 @@ Usage:
4062
4089
  typed lifecycle events (spawned, launched, stopped,
4063
4090
  restarted, retired, worktree-retained…) written by
4064
4091
  the action that made them true; nothing inferred
4065
- oats instance waiting <set|clear> --producer <id> [--reason permission|question|attention] [--message <text>] [--home <abs>] [--json]
4092
+ oats instance waiting <set|clear> --producer <id> [--reason permission|question|attention] [--message <text>] [--home <abs>] [--dir <d>] [--json]
4066
4093
  a producer's claim that the instance needs input
4067
4094
  from a human; appended only on change; display only
4068
4095
  oats instance attention [--message <text>] [--clear] [--json]
@@ -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
 
@@ -4135,7 +4162,7 @@ Layers: ${LAYERS.join(", ")}. Workspace model v2: docs/design/2026-09-23-workspa
4135
4162
  // Same two renderings as every other typed failure: one envelope on stdout in
4136
4163
  // --json mode, one `oats: <message>` line on stderr otherwise. The message
4137
4164
  // already names the offending file — the readers re-raise it with one.
4138
- if (JSON_MODE) jsonFail(e.code, e.message);
4165
+ if (JSON_MODE) jsonFail(e.code, e.message, e.details?.unconfirmed === true ? e.details : undefined);
4139
4166
  die(e.message);
4140
4167
  } finally {
4141
4168
  // The command's read session: every `git cat-file --batch` child ends before the process does.
@@ -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
@@ -544,6 +552,18 @@ runs `bin/claude-waiting.sh` from the home's module copy, which calls
544
552
  | `PostToolUseFailure` | `*` | clear, unless a subagent made the call |
545
553
  | `UserPromptSubmit`, `Stop`, `SessionEnd` | none | clear, not debounced (a turn boundary) |
546
554
 
555
+ **One home per settings file.** The launch hook bakes the home it writes the
556
+ settings for into every command, as its real path (oats.core 2.4.1). The
557
+ script acts only when the session's `$OATS_INSTANCE_HOME` names that same
558
+ home, through any spelling (a symlinked deployment resolves to the same
559
+ path). A Claude process that loads one home's settings while carrying
560
+ another instance's environment (a nested `claude -p`, a `claude -p` started
561
+ with its working directory in another home, a pane that inherited the
562
+ variables) does nothing at all: no CLI call and no marker write, for either
563
+ home. Before 2.4.1 it set and cleared the claim of the home its environment
564
+ named, and its `Stop` and `SessionEnd` clears could erase that instance's
565
+ real claim.
566
+
547
567
  Claude Code shows an AskUserQuestion through its permission dialog, so that
548
568
  dialog's own `permission_prompt` follows the question's set: the script keeps
549
569
  the current reason in its marker, and a permission prompt never relabels an
@@ -597,7 +617,8 @@ clears it.
597
617
  after the hook began, the worst case is about 4 s, well under Claude's 5 s
598
618
  hook timeout.
599
619
  - **Debounce.** The script keeps private state outside the home, in a file
600
- per home: `<dir>/<first 16 hex of sha256(home)>.claude`, where `<dir>`
620
+ per home: `<dir>/<first 16 hex of sha256(home)>.claude` (the home's real
621
+ path, so each spelling of a home has the same file), where `<dir>`
601
622
  is per user: `$XDG_RUNTIME_DIR/oats-waiting` when that is set and
602
623
  absolute, else `$TMPDIR/oats-waiting-<uid>` when `TMPDIR` is absolute,
603
624
  else `/tmp/oats-waiting-<uid>`. The spawn and launch
@@ -715,6 +736,11 @@ again" permission rules there.
715
736
  - A `Stop` or `UserPromptSubmit` always clears, even one the main thread
716
737
  produces while a subagent's prompt is open (a background subagent's
717
738
  completion is submitted as a prompt).
739
+ - The one-home rule separates homes, not two sessions of one home: a second
740
+ Claude process started inside the same home with that home's own
741
+ `$OATS_INSTANCE_HOME` (a `claude -p` the agent runs there) still matches,
742
+ so its `Stop` and `SessionEnd` clears can erase the home's own live
743
+ `oats.core` claim ([#557](https://github.com/awebai/oats/issues/557)).
718
744
  - A Claude instance spawned before the upgrade gets the emitter only when
719
745
  it is respawned. Its launch hook comes from its recorded module copy.
720
746
 
@@ -752,7 +778,22 @@ with no provider or a home operation without `--home`
752
778
  The provider must exit 0 with exactly one JSON envelope on stdout. Otherwise
753
779
  the outcome is **unconfirmed**: `E_OPERATION_TIMEOUT` (after 240 s) or
754
780
  `E_OPERATION_RESULT`, with `error.details { unconfirmed: true, exit, envelope?, stderr? }`.
755
- A provider's own `ok: false` is relayed with its code. A schedule of kind
781
+ A provider's own `ok: false` is relayed with its code and full envelope. When
782
+ the provider sets `error.details.unconfirmed: true` (the boolean), the operation
783
+ wrapper also sets its outer `error.details.unconfirmed: true`. Providers should
784
+ set that marker when dispatched effects or their compensation cannot be
785
+ confirmed, and preserve it through wrappers. Producers should leave ordinary
786
+ refusals and fully compensated failures unmarked: naming a home or retained
787
+ evidence is not itself uncertainty. The operation wrapper and scheduler still
788
+ apply their existing message-based compatibility checks during this additive
789
+ migration, including for copied providers in older homes; their text-based
790
+ false positives are not removed by this change.
791
+
792
+ The kernel marks incomplete keyed spawns (`E_SPAWN_INCOMPLETE`) and spawn
793
+ failures whose rollback cannot finish with the same field, through the CLI.
794
+ A completed rollback remains an unmarked failure. This adds structural evidence;
795
+ it does not remove text fallbacks or change scheduler slot and retry rules.
796
+ A schedule of kind
756
797
  `operation` runs the same command ([schedules.md](schedules.md)). The JSON
757
798
  shapes are in [desktop-cli-api.md](desktop-cli-api.md#inspect-readiness-and-operation-run-on-the-workspace-model-operationsapi-2-soulsapi-2-readinessapi-2-oats-0260).
758
799
 
@@ -543,7 +543,10 @@ oats operation run <layer>:<name> (--home <abs> | --soul <name> [--dir <d>]) [--
543
543
  home operation without `--home`), `E_CAPABILITY_REQUIRES`, `E_BAD_ARGS`
544
544
  (undeclared or missing `--arg`), `E_CAPABILITY_BROKEN`. A provider's `ok:
545
545
  false` is relayed with its code and `details: {exit, envelope,
546
- unconfirmed?}`. `E_OPERATION_TIMEOUT` (240 s) and `E_OPERATION_RESULT` are
546
+ unconfirmed?}`. A literal provider `error.details.unconfirmed: true` is
547
+ promoted to the outer details while its full envelope stays nested. Existing
548
+ retained-effect text checks remain for compatibility during migration.
549
+ `E_OPERATION_TIMEOUT` (240 s) and `E_OPERATION_RESULT` are
547
550
  unconfirmed outcomes: `details: {exit, signal, unconfirmed: true,
548
551
  envelope?, stderr?, cleanup?}`.
549
552
 
@@ -1617,7 +1620,7 @@ with `--expect-decision` records the key and decision in `instance.json`.
1617
1620
  `E_IDEMPOTENCY_CONFLICT {instance, home}`.
1618
1621
  - `spawnCompleted` is `false` until launch, lineage and events are done; a
1619
1622
  retry of an unfinished spawn is `E_SPAWN_INCOMPLETE {instance, home,
1620
- launched}` (recover through the session surface).
1623
+ launched, unconfirmed: true}` (recover through the session surface).
1621
1624
  - The key lives in the home. Mint it on the first confirmation and keep it
1622
1625
  for that intent's retries.
1623
1626
  - `wake: {requested, saved, error}` is recorded and replayed; `saved: null`
@@ -1628,8 +1631,8 @@ with `--expect-decision` records the key and decision in `instance.json`.
1628
1631
  ```json
1629
1632
  {"instance":"rm-api","agent":"rm","home":"/w/agents/rm/instances/rm-api","work":"worktree","branch":"agents/rm-api",
1630
1633
  "base":{"ref":"github.com/nw/agents","oid":"66566512…"},"launched":true,"warnings":[],
1631
- "tmux":{"session":"oats-agents","window":"rm-api"},"backend":"tmux","repo":"/w/agents-repo","harness":"pi","model":null,"parent":null,"sibling":null,"relation":null,
1632
- "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,
1633
1636
  "wake":{"requested":false,"saved":null,"error":null},"launchConfig":null,
1634
1637
  "launch":{"version":2,"harness":"pi","launchConfig":null,"launchConfigSource":null,"executable":"/usr/local/bin/pi","executableDeclared":null,
1635
1638
  "executableResolvedFrom":"PATH","args":[],"env":{},"model":null,"hooks":{"launch":{},"env":{},"contributions":[]},"prompt":{"kind":"task-file","file":"TASK.md"}}}
@@ -1639,10 +1642,21 @@ with `--expect-decision` records the key and decision in `instance.json`.
1639
1642
 
1640
1643
  - Always present: `instance, agent, home, work, branch, base ({ref, oid}
1641
1644
  the new branch started at; `null` without one), launched, warnings
1642
- (array), tmux ({session, window} | null), backend ("tmux"), repo, harness,
1645
+ (array), tmux ({session, window, socket?} | null), backend ("tmux"), repo, harness,
1643
1646
  model, parent,
1644
1647
  sibling, relation, spawnOrigin (operator | instance), attach, launchConfig,
1645
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.
1646
1660
  - When they apply: `yolo`, `decision` and
1647
1661
  `replayed` (bound apply), `wake` (keyed apply), `wakeSchedule` and
1648
1662
  `wakeScheduleError` (a requested wake).
@@ -1655,7 +1669,8 @@ Feature `spawn-name`. `--name <slug>` is the exact name, with no prefix.
1655
1669
  - A name that is not a slug (lowercase letters and digits, single dashes),
1656
1670
  equals a soul name, or exceeds 64 characters (derived names included, with
1657
1671
  their suffix) is `E_INSTANCE_NAME_INVALID`.
1658
- - 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
1659
1674
  `E_INSTANCE_NAME_TAKEN {instance, home, session?}`; a typed name never gets
1660
1675
  a silent `-2`.
1661
1676
  - The name is part of the decision.
@@ -1686,11 +1701,17 @@ Feature `spawn-name`. `--name <slug>` is the exact name, with no prefix.
1686
1701
  | `E_INSTANCE_NAME_INVALID`, `E_INSTANCE_NAME_TAKEN` | see above | |
1687
1702
  | `E_DECISION_STALE` | `{decision}` | |
1688
1703
  | `E_PLACEMENT_TAKEN`, `E_IDEMPOTENCY_CONFLICT` | `{instance, home}` | |
1689
- | `E_SPAWN_INCOMPLETE` | `{instance, home, launched}` | |
1704
+ | `E_SPAWN_INCOMPLETE` | `{instance, home, launched, unconfirmed: true}` | |
1690
1705
  | `E_LAUNCH_*`, `E_MODEL_UNKNOWN`, `E_UNSUPPORTED_HARNESS` | | the launch selection is refused |
1691
1706
  | `E_LAUNCH_SHIM` | | the home's `oats` (`<home>/.oats/bin/oats`) cannot be written; the spawn is rolled back |
1692
1707
  | `E_SCHEDULE_INVALID` | | a bad wake (`--wake-json`, `--wake-file`, `--wake-*`) |
1693
- | `E_SPAWN_FAILED` | | anything else |
1708
+ | `E_SPAWN_FAILED` | `{unconfirmed: true}` when compensation cannot finish | anything else |
1709
+
1710
+ Spawn failure envelopes carry `error.details.unconfirmed: true` when an existing
1711
+ keyed spawn is incomplete (`E_SPAWN_INCOMPLETE`) or compensation cannot confirm
1712
+ cleanup. The keyed-spawn details retain `instance`, `home` and `launched`.
1713
+ Confirmed completed compensation does not set this marker. This is an additive
1714
+ producer migration; existing text-based compatibility checks remain.
1694
1715
 
1695
1716
  ## `instance.json` and the roster
1696
1717
 
@@ -1922,6 +1943,15 @@ route target:
1922
1943
  `relativeTo` and `spawnOrigin` are always present, `null` when the host
1923
1944
  does not supply them (a host before 0.31, a fact it never recorded, or a
1924
1945
  saved route the host no longer lists). Nothing is derived on this side.
1946
+ - **`waitingOnYou`** (0.40.2, [Waiting on you](#waiting-on-you)) is on a row
1947
+ only when the host's kernel reports it: a row from a host before 0.40.0,
1948
+ and a saved route the host did not list, have no such key. Absent means
1949
+ "not reported", which is not `null` ("no claim"). When present it is `null`
1950
+ or `{since, producer, reason, message}`, passed through the kernel's read
1951
+ rule again on this side: a value that is not a claim (no valid `since` or
1952
+ `producer`) is `null`, and an unknown `reason` or an invalid `message` is
1953
+ `null` inside a claim that still counts. As on a local row, the host
1954
+ reports a claim only for a running instance.
1925
1955
  - **`addressable`** (0.31): `true` for every row the host reports. Routed
1926
1956
  session and lifecycle commands reach it by `--home`, or by name when the
1927
1957
  name is unique on the host ([addressing](servers.md#run-there); a shared
@@ -2160,12 +2190,15 @@ oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--
2160
2190
 
2161
2191
  - **Sources.** `home` is `<home>/.oats-events.jsonl`; `workspace` is
2162
2192
  `<deployment>/.agents/events/<agent>--<instance>.jsonl` (it survives the
2163
- home). Each is `{path, status: "ok" | "absent" | "refused" | "tail",
2193
+ home). The deployment is the one the home's spawn recorded, when that
2194
+ directory really holds the home at `agents/<agent>/instances/<instance>`;
2195
+ otherwise it is the fourth ancestor of the home as it was addressed. Each is `{path, status: "ok" | "absent" | "refused" | "tail",
2164
2196
  bytes}`. Only a regular file is opened (no symlinks, same device and inode
2165
2197
  after open), and at most its last 4 MiB is read (`"tail"`).
2166
2198
  - **Kinds:** `spawned`, `launched`, `restarted`, `stopped`, `stop-refused`,
2167
2199
  `retire-planned`, `retired`, `worktree-retained`, `worktree-removed`,
2168
- `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
2169
2202
  `launch` hook's warning at session start/restart, `data: {message}`),
2170
2203
  `recomposed` (from earlier kernels), `waiting` (0.40: a producer's claim,
2171
2204
  [Waiting on you](#waiting-on-you)). `producer` is `kernel`, a capability id,
@@ -2178,16 +2211,47 @@ oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--
2178
2211
  is dated at the receipt's launch time. The boundary is complete per log: a
2179
2212
  log that missed it gets a copy of the same row (same time and data), never
2180
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`).
2181
2222
  - **Incarnation.** Each row carries the writing home's `createdAt` (or
2182
2223
  `null` for old rows); the top-level `incarnation` is the current home's (or
2183
2224
  `null`). Earlier incarnations are returned as this address's history.
2184
2225
  - **Address.** `--home` must be a home of `<instance>` (`E_HOME_MISMATCH`).
2226
+ A home has one address in storage, its real path (since 0.40.2): rows are
2227
+ written and matched under it, whatever spelling a writer or reader used (a
2228
+ deployment reached through a symlink, a symlinked agents root). The answer
2229
+ keeps the spelling it was asked in: the top-level `home` and every
2230
+ returned row's `home` are the home as the caller addressed it (`--home`,
2231
+ or the home found under `--dir`), the same string a status row carries.
2185
2232
  Rows for another address are dropped and counted in
2186
2233
  `integrity.foreignRows`; torn or invalid lines are counted in
2187
2234
  `integrity.unreadableRows`. A row present in both logs is returned once;
2188
2235
  identical rows repeated within one log (a set, a clear and the same set in
2189
2236
  one millisecond) are all returned, as many as the log holding the most
2190
2237
  copies has.
2238
+ - **Rows from before 0.40.2**, in a deployment addressed through a symlink
2239
+ only. A stored `home` is never rewritten, and never matched under another
2240
+ spelling. A row an earlier kernel wrote under the lexical spelling (a
2241
+ spawn's rows, and the claims of a session that was spawned and never
2242
+ restarted) is foreign after the upgrade, and counted in
2243
+ `integrity.foreignRows`. What that means for a claim:
2244
+ - A claim that was live under the lexical spelling stops showing. Nothing
2245
+ brings that row back: the claim shows again only when a producer makes it
2246
+ anew (the next permission prompt or question, the agent's next
2247
+ `oats instance attention`). A restart starts a new session with no
2248
+ claim, as always.
2249
+ - A claim that stayed set because the restart that should have voided it
2250
+ was recorded under the real path (#583) is gone.
2251
+ - The rows a started or restarted session wrote under the real path, which
2252
+ `oats status --dir <symlink>` could not see, are read now. They cannot
2253
+ surface a stale claim: such a session wrote its clears and its session
2254
+ boundaries under the real path too, so that history is complete.
2191
2255
  - **Window.** `count` is the rows after `--since`; `returned` the window
2192
2256
  (`--limit`, default 200, 1–2000); `truncated` means rows were cut or a
2193
2257
  source was a tail. `lastEvent` is `{kind, at, producer, incarnation}` of
@@ -2251,14 +2315,17 @@ oats instance attention [--message <text>] [--clear] --json
2251
2315
  message (a hand-edited log) reads as `null`, and the claim still counts.
2252
2316
  A stored `reason` outside `permission`, `question`, `attention` reads as
2253
2317
  `null` too, and a row whose `producer` is neither `kernel` nor a valid
2254
- producer id is no claim at all.
2318
+ producer id, or whose `at` is not a date, is no claim at all.
2255
2319
  - **Idempotent.** The verb reads the producer's live claim first and appends
2256
2320
  only on a change: a `set` whose reason or message differs from the live
2257
2321
  positive claim appends (`changed: true`), an identical one does not; a
2258
2322
  `clear` appends only over a live positive claim. The answer is
2259
2323
  `{eventsApi, instance, home, producer, changed, waitingOnYou}`, where
2260
2324
  `waitingOnYou` is that producer's resulting claim (`null` when it holds
2261
- none). Concurrent writers append whole lines; the latest row decides.
2325
+ none) and `home` is the home as the caller addressed it (`--home`,
2326
+ `$OATS_INSTANCE_HOME` or the enclosing home); the row is stored under the
2327
+ real path, so a set and a clear through different spellings of one home
2328
+ meet. Concurrent writers append whole lines; the latest row decides.
2262
2329
  Each log is judged on its own, and success means both took the row: a
2263
2330
  write either log refused is `E_EVENTS_FAILED` naming it, and the next call
2264
2331
  (a retry) appends again to repair it.
@@ -2396,7 +2463,7 @@ Plain `retire` keeps a worktree-mode instance's work: the worktree is moved
2396
2463
  `-2` suffix if taken; `detached-<oid12>` when detached), state intact.
2397
2464
 
2398
2465
  ```text
2399
- 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
2400
2467
  ```
2401
2468
 
2402
2469
  A first retire prints the **raw receipt**, not an envelope:
@@ -2412,8 +2479,7 @@ A first retire prints the **raw receipt**, not an envelope:
2412
2479
  ```
2413
2480
 
2414
2481
  - `retention`: `{worktree: "retained" | "removed" | "absent", movedTo?,
2415
- branch, detachedAt?, recordedBranch, branchDeleted?,
2416
- branchDeletionSkipped?: {expected, actual, reason}}`, or `null` when no
2482
+ branch, detachedAt?, recordedBranch}`, or `null` when no
2417
2483
  worktree step ran: a non-worktree mode, or a worktree kept for the retry.
2418
2484
  A retire whose hooks left cleanup outstanding keeps the worktree exactly as
2419
2485
  it was (with `worktreeRemoved: false`) and says why in `rollbackIncomplete`
@@ -2422,14 +2488,30 @@ A first retire prints the **raw receipt**, not an envelope:
2422
2488
  entry is gone is never touched: it is an incomplete item (`git worktree
2423
2489
  <path>: its admin entry is missing; …`), and `--force` refuses it with
2424
2490
  `E_WORK_PRESERVATION_FAILED`.
2425
- - `--discard-worktree` removes the worktree. `--delete-branch` deletes the
2426
- worktree's verified branch (re-verified at deletion time) and implies
2427
- discarding; a mismatch deletes nothing and reports
2428
- `branchDeletionSkipped`. Without `--delete-branch` no retire deletes a
2429
- branch, a retried or `--force`d quarantine included. A failed spawn's
2430
- quarantine that still owes the branch the spawn created stays incomplete
2431
- (`git branch <b>: kept; the failed spawn created it; pass --delete-branch to
2432
- 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.
2433
2515
  - `workRecovery` (or `workRecoveries[]`): `{path, classes, bytes, outputs?,
2434
2516
  repoCopy?}`; `outputs: {paths: [{path, bytes}], bytes}` names what was
2435
2517
  copied beyond tracked state, largest first.
@@ -2461,6 +2543,42 @@ stderr, not envelopes.
2461
2543
 
2462
2544
  ## Sessions and launch configurations
2463
2545
 
2546
+ ### Input
2547
+
2548
+ ```text
2549
+ oats session input --home <abs> [--text-file <path>] --json
2550
+ ```
2551
+
2552
+ Input bytes come from stdin or the named file. The existing version-1 success
2553
+ answer is `{schemaVersion: 1, ok: true, result: {home, backend: "tmux",
2554
+ present: true, state, paneId, submitted: true, verified}}`. Session input runs
2555
+ on the execution host, including when the wake broker invokes it there;
2556
+ `--server` is not supported for input. The adapter sends one literal
2557
+ bracketed paste and one Enter after the existing input/authority/target checks.
2558
+
2559
+ `submitted` means terminal-operation success, **not model acceptance or
2560
+ processing**. `verified` is display observation only: `true` means a bounded
2561
+ look changed, possibly because of unrelated output or a dialog; `false` means
2562
+ unchanged, unreadable or exhausted observation. False never authorizes retry
2563
+ and is not proof of a pending draft or absence of effects. No `reason` is
2564
+ emitted; the `enter-not-taken` result from 0.39.4 is removed.
2565
+
2566
+ Read-only settling before Enter shares one monotonic 2-second budget starting
2567
+ when paste returns; up to two post-Enter looks share a 1-second budget. Probe
2568
+ timeouts and sleeps use the remaining budget. Observation failures produce
2569
+ `verified: false`, not input errors or extra keys. The original paste/key
2570
+ command timeouts and `E_SESSION_INPUT_FAILED` errors remain; the observation
2571
+ budgets do not bound those commands, failed buffer cleanup or OS scheduling.
2572
+ No busy-pane submission or exactly-once guarantee is provided. Generic command
2573
+ errors can still be uncertain after partial effects.
2574
+
2575
+ The Desktop terminal's authorized PTY writes are a separate stream; they do not
2576
+ consume this `verified` field. The Pi bridge does not interpret this result.
2577
+ Scheduler wake still records a nonthrowing input operation as delivered without
2578
+ adding acceptance/history fields. Actual broker acknowledgement and retry
2579
+ policy require their own consumer qualification; this result is not a native
2580
+ harness receipt. See [execution targets](execution-targets.md).
2581
+
2464
2582
  ### Start and restart
2465
2583
 
2466
2584
  ```text
@@ -2480,6 +2598,12 @@ selection flags. See [the start workflow](desktop-instance-start.md).
2480
2598
  instance's events as a `launch-warning` row, `data: {message}`. They are
2481
2599
  advisory: the start went ahead. Earlier kernels omit the field; read a
2482
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)).
2483
2607
  - A start or restart appends a `launched` event as soon as its session
2484
2608
  exists (0.40, `phase: "start"` or `"restart"`, `startId`), the session
2485
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. |