@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 +45 -18
- package/docs/capabilities.md +44 -3
- package/docs/desktop-cli-api.md +147 -23
- package/docs/desktop.md +5 -4
- package/docs/execution-targets.md +342 -38
- package/docs/implementation.md +85 -11
- package/docs/oats-local.schema.json +2 -2
- package/docs/official-catalog.md +1 -1
- package/docs/packages.md +5 -5
- package/docs/release-lane.md +8 -4
- package/docs/release-notes/v0.40.2.md +120 -0
- package/docs/release-notes/v0.41.0.md +238 -0
- package/docs/schedules.md +40 -11
- package/docs/servers.md +7 -2
- package/docs/souls-and-instances.md +32 -6
- package/docs/workspaces.md +1 -1
- package/lib/core.mjs +579 -177
- package/lib/dir-lock.mjs +7 -4
- package/lib/instance-events.mjs +130 -46
- 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/schedule-command-child.mjs +39 -9
- package/lib/schedule.mjs +57 -28
- package/lib/servers.mjs +23 -9
- package/lib/session-input.mjs +42 -47
- package/package-catalog.json +1 -1
- package/package.json +1 -1
- package/skills/oats-getting-started/SKILL.md +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,
|
|
@@ -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
|
-
|
|
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 || []) {
|
|
@@ -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]
|
|
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]
|
|
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
|
|
|
@@ -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.
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -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?}`.
|
|
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
|
|
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).
|
|
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
|
|
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)
|
|
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] [--
|
|
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
|
|
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.
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
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.
|
|
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. |
|