@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 +39 -12
- package/docs/capabilities.md +9 -1
- package/docs/desktop-cli-api.md +58 -16
- package/docs/desktop.md +5 -4
- package/docs/execution-targets.md +312 -9
- package/docs/implementation.md +37 -6
- package/docs/release-notes/v0.41.0.md +238 -0
- package/docs/servers.md +4 -1
- package/docs/souls-and-instances.md +32 -6
- package/lib/core.mjs +552 -165
- package/lib/instance-git.mjs +113 -4
- package/lib/instance-lifecycle.mjs +3 -2
- package/lib/packages.mjs +1 -1
- package/lib/resolve.mjs +1 -1
- package/lib/servers.mjs +16 -8
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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,
|
|
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" : ""}
|
|
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:
|
|
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) {
|
|
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]
|
|
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]
|
|
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;
|
|
4093
|
-
|
|
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
|
|
package/docs/capabilities.md
CHANGED
|
@@ -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
|
|
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
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -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
|
|
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
|
|
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] [--
|
|
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
|
|
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.
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
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.
|
|
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
|
-
|
|
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. |
|