@awebai/oats 0.41.1 → 0.42.1
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 +10 -21
- package/docs/capabilities.md +73 -2
- package/docs/capability-manifest.schema.json +38 -0
- package/docs/desktop-cli-api.md +161 -13
- package/docs/desktop.md +21 -7
- package/docs/execution-targets.md +202 -45
- package/docs/implementation.md +3 -1
- package/docs/release-notes/v0.42.0.md +398 -0
- package/docs/release-notes/v0.42.1.md +110 -0
- package/docs/servers.md +4 -1
- package/docs/souls-and-instances.md +441 -8
- package/injects/instance-boundary.md +4 -3
- package/injects/work-checkout.md +25 -3
- package/injects/work-worktree.md +30 -7
- package/lib/capability-contract.mjs +56 -0
- package/lib/core.mjs +1384 -271
- package/lib/instance-git.mjs +22 -0
- package/lib/instance-lifecycle.mjs +28 -2
- package/lib/launch-preference.mjs +9 -0
- package/lib/login-environment.mjs +212 -0
- package/lib/retire-output.mjs +63 -0
- package/lib/servers.mjs +5 -2
- package/lib/tree-copy.mjs +4 -2
- package/package.json +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -53,6 +53,7 @@ import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_
|
|
|
53
53
|
|
|
54
54
|
import { observeInstanceGit, diffInstanceFile } from "../lib/instance-git.mjs";
|
|
55
55
|
import { planStop, applyStop, planRetire, resolveInstance as resolveInstanceForCli } from "../lib/instance-lifecycle.mjs";
|
|
56
|
+
import { extraWorktreeLines, formatBytes, workRecoveryLines } from "../lib/retire-output.mjs";
|
|
56
57
|
const await_import_lifecycle = () => ({ resolveInstance: resolveInstanceForCli });
|
|
57
58
|
import { homeTarget, soulTarget, isWorkspaceContext, inspectDocument, readinessDocument, policyOf, policySoul, manifestMissingRequires, INSPECT_OPERATIONS_API } from "../lib/instance-inspect.mjs";
|
|
58
59
|
import { readEvents, setWaiting, incarnationOf } from "../lib/instance-events.mjs";
|
|
@@ -155,15 +156,6 @@ const jsonFail = (code, message, details, exit = 1) => { console.log(JSON.string
|
|
|
155
156
|
const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok: true, result, ...envelopeWarnings() })); };
|
|
156
157
|
// Text mode (or a JSON answer printed before the read): the warning goes to stderr, never stdout.
|
|
157
158
|
process.on("exit", () => { const w = runtimeNameWarning(); if (w && !warningDelivered) process.stderr.write(`oats: warning: ${w.message}\n`); });
|
|
158
|
-
const formatBytes = (n) => n < 1024 ? `${n} B` : n < 1024 ** 2 ? `${(n / 1024).toFixed(1)} KiB` : n < 1024 ** 3 ? `${(n / 1024 ** 2).toFixed(1)} MiB` : `${(n / 1024 ** 3).toFixed(1)} GiB`;
|
|
159
|
-
/** A retire recovery's copied outputs (untracked/ignored or directory work), named with their size. */
|
|
160
|
-
function preservedOutputLines(recovery) {
|
|
161
|
-
const outputs = recovery?.outputs;
|
|
162
|
-
if (!outputs?.paths?.length) return [];
|
|
163
|
-
const shown = outputs.paths.slice(0, 8).map((p) => `${p.path} (${formatBytes(p.bytes)})`);
|
|
164
|
-
const more = outputs.paths.length > 8 ? `, and ${outputs.paths.length - 8} more` : "";
|
|
165
|
-
return [` copied outputs: ${shown.join(", ")}${more} — ${formatBytes(outputs.bytes)} in total`];
|
|
166
|
-
}
|
|
167
159
|
|
|
168
160
|
function shortPath(p) {
|
|
169
161
|
if (!p) return p;
|
|
@@ -2506,7 +2498,7 @@ function retireCmd() {
|
|
|
2506
2498
|
const planRev = flag("plan-revision"), idemKey = flag("idempotency-key");
|
|
2507
2499
|
if (planRev === true || idemKey === true) die("--plan-revision and --idempotency-key need values");
|
|
2508
2500
|
if ((planRev !== undefined) !== (idemKey !== undefined)) die("--plan-revision and --idempotency-key go together");
|
|
2509
|
-
let replayPath = null, childrenStopped = null;
|
|
2501
|
+
let replayPath = null, childrenStopped = null, plannedExtraWorktrees;
|
|
2510
2502
|
if (planRev !== undefined) {
|
|
2511
2503
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(idemKey)) die("--idempotency-key: 1-128 chars of [A-Za-z0-9._:-]");
|
|
2512
2504
|
// Replay first: after a successful retire the home is gone, so the receipt
|
|
@@ -2516,6 +2508,9 @@ function retireCmd() {
|
|
|
2516
2508
|
let fresh;
|
|
2517
2509
|
try { fresh = planRetire(dirFlag(), root, name, { home: homeFlag }); } catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message, e.details) : die(e.message); }
|
|
2518
2510
|
replayPath = join(dirname(fresh.home), `.oats-retire-receipt.${idemKey}.json`);
|
|
2511
|
+
// The extra trees the confirmed plan names, as it names them: the retire refuses as stale rather than
|
|
2512
|
+
// move or remove one that no longer reads that way when it gets to them.
|
|
2513
|
+
plannedExtraWorktrees = fresh.facts.extraWorktrees;
|
|
2519
2514
|
if (fresh.planRevision !== planRev) return args.includes("--json") ? jsonFail("E_PLAN_STALE", `the retire plan changed since it was shown (${planRev} → ${fresh.planRevision}); review the fresh plan`, { plan: fresh }) : die(`the retire plan changed since it was shown; re-run oats retire ${name} --plan`);
|
|
2520
2515
|
// The plan promised: recorded children are STOPPED first (bounded, never
|
|
2521
2516
|
// escalated) and retained. A child still running after the grace refuses
|
|
@@ -2529,7 +2524,7 @@ function retireCmd() {
|
|
|
2529
2524
|
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`);
|
|
2530
2525
|
}
|
|
2531
2526
|
let r;
|
|
2532
|
-
try { r = retireInstance(root, name, { home: homeFlag, self: isSelf, discardWorktree: args.includes("--discard-worktree"), keepDir: args.includes("--keep-dir"), force: args.includes("--force") }); }
|
|
2527
|
+
try { r = retireInstance(root, name, { home: homeFlag, self: isSelf, discardWorktree: args.includes("--discard-worktree"), keepDir: args.includes("--keep-dir"), force: args.includes("--force"), ...(plannedExtraWorktrees ? { plannedExtraWorktrees } : {}) }); }
|
|
2533
2528
|
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); }
|
|
2534
2529
|
if (childrenStopped) r.childrenStopped = childrenStopped;
|
|
2535
2530
|
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 */ } }
|
|
@@ -2570,11 +2565,8 @@ function retireCmd() {
|
|
|
2570
2565
|
console.log(`Retired ${r.retired} (agent ${r.agent})${r.worktreeRemoved ? ", worktree removed" : ""}`);
|
|
2571
2566
|
// Preserving work and not saying so leaves the operator believing it is gone,
|
|
2572
2567
|
// which is most of the harm of deleting it. Name the classes and the path.
|
|
2573
|
-
for (const
|
|
2574
|
-
|
|
2575
|
-
console.log(` ${recovery.path}${typeof recovery.bytes === "number" ? ` (${formatBytes(recovery.bytes)})` : ""}`);
|
|
2576
|
-
for (const line of preservedOutputLines(recovery)) console.log(line);
|
|
2577
|
-
}
|
|
2568
|
+
for (const line of workRecoveryLines(r)) console.log(line);
|
|
2569
|
+
for (const line of extraWorktreeLines(r)) console.log(line);
|
|
2578
2570
|
for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
|
|
2579
2571
|
if (isSelf) console.log("This window dies in ~8s — say any goodbyes now.");
|
|
2580
2572
|
}
|
|
@@ -3725,11 +3717,8 @@ async function serverRouteCmd() {
|
|
|
3725
3717
|
if (branch) console.error(branch);
|
|
3726
3718
|
}
|
|
3727
3719
|
console.log(`Retired ${r.retired} on ${id}${r.deferred ? " (deferred completion scheduled there)" : ""}${r.rollbackIncomplete ? " — cleanup INCOMPLETE on the server, home retained there" : ""}`);
|
|
3728
|
-
for (const
|
|
3729
|
-
|
|
3730
|
-
console.log(` ${recovery.path}${typeof recovery.bytes === "number" ? ` (${formatBytes(recovery.bytes)})` : ""}`);
|
|
3731
|
-
for (const line of preservedOutputLines(recovery)) console.log(line);
|
|
3732
|
-
}
|
|
3720
|
+
for (const line of workRecoveryLines(r, { host: target.sshHost })) console.log(line);
|
|
3721
|
+
for (const line of extraWorktreeLines(r, { host: target.sshHost })) console.log(line);
|
|
3733
3722
|
if (r.rollbackIncomplete) {
|
|
3734
3723
|
for (const f of r.rollbackIncomplete) console.error(` ${f}`);
|
|
3735
3724
|
const branch = failedSpawnBranchLine(r, r.rollbackIncomplete, { host: target.sshHost });
|
package/docs/capabilities.md
CHANGED
|
@@ -59,7 +59,8 @@ A self-contained package has an `oats.json`:
|
|
|
59
59
|
"hooks": {
|
|
60
60
|
"spawn": "bin/team-chat-hook.mjs spawn",
|
|
61
61
|
"retire": "bin/team-chat-hook.mjs retire"
|
|
62
|
-
}
|
|
62
|
+
},
|
|
63
|
+
"retirement": { "disposable": { "home": [".team-chat", ".team-chat-id-*"] } }
|
|
63
64
|
}
|
|
64
65
|
```
|
|
65
66
|
|
|
@@ -153,7 +154,77 @@ A self-contained package has an `oats.json`:
|
|
|
153
154
|
and nothing is silently dropped. Without `--force` that state fails closed with
|
|
154
155
|
`E_UNIDENTIFIED_INSTANCE_HOME` rather than deleting whatever credentials the
|
|
155
156
|
directory still holds; `--force` removes it and leaves any external state for
|
|
156
|
-
the operator to clean up by hand.
|
|
157
|
+
the operator to clean up by hand. Home entries declared in
|
|
158
|
+
`retirement.disposable.home` (below) go with the home: recovery holds no
|
|
159
|
+
copy of them.
|
|
160
|
+
- `retirement.disposable` declares what retirement treats as the provider's
|
|
161
|
+
own state rather than the instance's work. It is a map with two optional
|
|
162
|
+
keys, `home` and `work`, each an array of strings.
|
|
163
|
+
- `home`: provider-owned state that is not the instance's work, left in
|
|
164
|
+
place until the home is removed, and not copied to recovery. The one
|
|
165
|
+
exception is the preservation of a failed spawn in directory mode, which
|
|
166
|
+
copies the whole home, declared entries included. Each entry
|
|
167
|
+
names top-level entries of the instance home:
|
|
168
|
+
- an exact hidden name, `^\.[A-Za-z0-9_][A-Za-z0-9._-]*$` (`.team-chat`);
|
|
169
|
+
- or a prefix, `^\.[A-Za-z0-9_][A-Za-z0-9._-]*-\*$` (`.team-chat-id-*`),
|
|
170
|
+
meaning every top-level entry whose name starts with the text before
|
|
171
|
+
`*`.
|
|
172
|
+
|
|
173
|
+
Refused:
|
|
174
|
+
- anything that is not one hidden top-level name (`notes`, `.aw/keys`,
|
|
175
|
+
`.a*`);
|
|
176
|
+
- an exact name that is `.oats`, `.agents` or `.claude`, or that starts
|
|
177
|
+
with `.oats-events`, `.oats-stop`, `.oats-restart`, `.oats-rollback`,
|
|
178
|
+
`.oats-agents-md`, `.oats-start` or `.oats-attachments`: the top-level
|
|
179
|
+
entries the kernel itself writes in an instance home;
|
|
180
|
+
- a prefix that starts with `.oats-`.
|
|
181
|
+
|
|
182
|
+
An exact name such as `.oats-aweb` is allowed: it is a provider's
|
|
183
|
+
directory. Entries are matched by name, without following symlinks: a
|
|
184
|
+
declared entry that is a symlink is left out and never followed.
|
|
185
|
+
|
|
186
|
+
A matching entry is left out of the home fingerprint that retire compares
|
|
187
|
+
with the spawn baseline, of the home copy and of that copy's
|
|
188
|
+
verification, before and after the retire hooks. A change to declared
|
|
189
|
+
entries alone is therefore not "changed instance-home bytes" and causes
|
|
190
|
+
no copy. When a recovery is written, its receipt names what was left out
|
|
191
|
+
in `workRecovery.notCopied` (names and owners only).
|
|
192
|
+
|
|
193
|
+
The declaration is recorded at spawn, with its owner, in the home's
|
|
194
|
+
retirement baseline, and retire reads it from there only: never from the
|
|
195
|
+
module copy in the home, from the capability as it is today, or from
|
|
196
|
+
`instance.json`. A running instance is unaffected until it is respawned,
|
|
197
|
+
and a home spawned before its capability declared the entries gains no
|
|
198
|
+
exclusion from a package update: its home is copied whole. A missing or
|
|
199
|
+
invalid baseline means no exclusions either.
|
|
200
|
+
|
|
201
|
+
Exclusion means "not copied" and nothing more. Nothing is removed early:
|
|
202
|
+
the entries stay in the home until the home is removed, and retire hooks
|
|
203
|
+
still see them. An incomplete cleanup, or a copy or verification that
|
|
204
|
+
fails, keeps the home with them. A capability that declares nothing has
|
|
205
|
+
its home state copied with the rest of the home.
|
|
206
|
+
- `work`: relative roots under `work/` that the capability generates. In
|
|
207
|
+
worktree mode the roots are kept out of the "untracked or ignored
|
|
208
|
+
worktree bytes" class, so untracked or ignored bytes under them alone do
|
|
209
|
+
not cause a recovery. They are still copied when a copy is made. Like
|
|
210
|
+
`home`, the roots are recorded at spawn. The other work modes do not use
|
|
211
|
+
them.
|
|
212
|
+
|
|
213
|
+
A malformed `retirement` is refused wherever the manifest is read: member
|
|
214
|
+
discovery (`E_WORKSPACE_SCHEMA`), package manifests (`E_PACKAGE_MANIFEST`)
|
|
215
|
+
and the kernel loader refuse the same manifest, with a JSON pointer:
|
|
216
|
+
`/retirement` (no `disposable` map, or a key other than `disposable`,
|
|
217
|
+
`home` and `work`), `/retirement/disposable/<scope>` (not an array of
|
|
218
|
+
strings) or `/retirement/disposable/home/<i>` (an entry outside the
|
|
219
|
+
grammar):
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
capability <id> manifest retirement.disposable.home entry "<value>" must name one hidden top-level home entry (".name", or ".prefix-*")
|
|
223
|
+
capability <id> manifest retirement.disposable.home entry "<value>" covers a kernel-owned home entry
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
This kernel reads the field. What retire does with the entries is in
|
|
227
|
+
[souls-and-instances.md](souls-and-instances.md#retire).
|
|
157
228
|
- `requires` declares what must exist before the capability works. Two kinds:
|
|
158
229
|
- a **host command** (`command`), satisfied by a binary on `PATH`;
|
|
159
230
|
- a **harness package** (`harness` + `package`, optionally `marketplace`),
|
|
@@ -293,6 +293,44 @@
|
|
|
293
293
|
"type": "string",
|
|
294
294
|
"pattern": "^[A-Z][A-Z0-9]*_$"
|
|
295
295
|
}
|
|
296
|
+
},
|
|
297
|
+
"retirement": {
|
|
298
|
+
"type": "object",
|
|
299
|
+
"description": "What retirement treats as the provider's own state rather than the instance's work.",
|
|
300
|
+
"required": ["disposable"],
|
|
301
|
+
"properties": {
|
|
302
|
+
"disposable": {
|
|
303
|
+
"type": "object",
|
|
304
|
+
"properties": {
|
|
305
|
+
"home": {
|
|
306
|
+
"type": "array",
|
|
307
|
+
"description": "Provider-owned state that is not the instance's work, left in place until the home is removed, and not copied to recovery (the preservation of a failed spawn in directory mode is the exception: it copies the whole home, declared entries included). Each entry names top-level entries of the instance home: one hidden name (.aw) or a prefix (.aweb-identity-*, every top-level name that starts with the text before the *). Recorded at spawn: a running instance is unaffected until it is respawned.",
|
|
308
|
+
"items": {
|
|
309
|
+
"type": "string",
|
|
310
|
+
"pattern": "^\\.[A-Za-z0-9_][A-Za-z0-9._-]*(?:-\\*)?$",
|
|
311
|
+
"not": {
|
|
312
|
+
"description": "Home entries the kernel owns cannot be declared.",
|
|
313
|
+
"anyOf": [
|
|
314
|
+
{"pattern": "^\\.(?:oats|agents|claude)$"},
|
|
315
|
+
{"pattern": "^\\.oats-(?:events|stop|restart|rollback|agents-md|start|attachments)"},
|
|
316
|
+
{"pattern": "^\\.oats-.*\\*$"}
|
|
317
|
+
]
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
"work": {
|
|
322
|
+
"type": "array",
|
|
323
|
+
"description": "Relative roots under work/ that a worktree instance's capability generates. Their untracked or ignored bytes do not count as the instance's untracked or ignored worktree bytes; they are still copied when a recovery copy is made.",
|
|
324
|
+
"items": {
|
|
325
|
+
"type": "string",
|
|
326
|
+
"minLength": 1
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
"additionalProperties": false
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
"additionalProperties": false
|
|
296
334
|
}
|
|
297
335
|
},
|
|
298
336
|
"additionalProperties": false
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -1923,7 +1923,11 @@ route target:
|
|
|
1923
1923
|
"running":true,"identity":{"alias":"dev-a","address":"acme/dev-a"},"identityAddress":"acme/dev-a",
|
|
1924
1924
|
"teams":[{"label":"default","team":"acme:team"}],"startedAt":"2026-09-29T10:00:00.000Z","createdAt":"2026-09-29T09:58:12.004Z",
|
|
1925
1925
|
"model":"opus","runtimeState":null,"parentInstance":"lead","siblingInstance":null,"relation":"child","relativeTo":"lead",
|
|
1926
|
-
"spawnOrigin":"instance","
|
|
1926
|
+
"spawnOrigin":"instance","work":"worktree","repo":"/srv/team/ws","branch":"agents/dev-a","modelFrom":"soul",
|
|
1927
|
+
"soul":{"repoKey":"github.com/acme/team","commit":"66566512…","current":"9c1e04ab…","status":"moved"},
|
|
1928
|
+
"modules":[{"name":"oats.okf","from":{"kind":"package","package":"oats.okf","version":"2.1.3","commit":"ab897841…","integrity":"sha256-bada35…","repoKey":"github.com/awebai/oats-okf"},
|
|
1929
|
+
"commit":"ab897841…","current":{"commit":"ab897841…","version":"2.1.3"},"status":"current"}],
|
|
1930
|
+
"retirePending":false,"rollbackIncomplete":false,
|
|
1927
1931
|
"savedRoute":false,"addressable":true,"missingRemotely":false}],
|
|
1928
1932
|
"retireFailures":[]}
|
|
1929
1933
|
```
|
|
@@ -1940,9 +1944,16 @@ route target:
|
|
|
1940
1944
|
- **Instance rows** relay the host's own `status --json` row: `identity`,
|
|
1941
1945
|
`identityAddress`, `teams`, `startedAt`, `createdAt`, `model`,
|
|
1942
1946
|
`runtimeState`, `parentInstance`, `siblingInstance`, `relation`,
|
|
1943
|
-
`relativeTo` and `spawnOrigin
|
|
1944
|
-
|
|
1945
|
-
|
|
1947
|
+
`relativeTo` and `spawnOrigin`, and (relayed from 0.42.1) `work`, `repo`,
|
|
1948
|
+
`branch`, `modelFrom`, `soul` and `modules`, are always present, `null`
|
|
1949
|
+
when the host does not supply them (an older host, a fact it never
|
|
1950
|
+
recorded, or a saved route the host no longer lists). Each is the
|
|
1951
|
+
[local row's](#the-roster-oats-status---json) fact of the same name,
|
|
1952
|
+
relayed as the host answered it. Nothing is derived on this side: `repo`
|
|
1953
|
+
is a path on the host, never read here, and `soul` and `modules` are the
|
|
1954
|
+
host's own drift observation (against its own members and lock), not
|
|
1955
|
+
recomputed: `modules` is the drift rows, or the recorded map when the host
|
|
1956
|
+
could not read its workspace, as on a local row.
|
|
1946
1957
|
- **`waitingOnYou`** (0.40.2, [Waiting on you](#waiting-on-you)) is on a row
|
|
1947
1958
|
only when the host's kernel reports it: a row from a host before 0.40.0,
|
|
1948
1959
|
and a saved route the host did not list, have no such key. Absent means
|
|
@@ -2219,6 +2230,15 @@ oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--
|
|
|
2219
2230
|
`--delete-branch` (since 0.41.0: `this self-retire was requested with
|
|
2220
2231
|
--delete-branch by an older OATS; retirement no longer deletes branches, so
|
|
2221
2232
|
the branch and the worktree were left`).
|
|
2233
|
+
- `worktree-retained` (`data: {movedTo, branch, recordedBranch}`) and
|
|
2234
|
+
`worktree-removed` (`data: {branch}`) are written to the workspace log only,
|
|
2235
|
+
by the retire's worktree step. Since 0.42.1 the retire also writes one per
|
|
2236
|
+
extra tree it handled
|
|
2237
|
+
([extra trees at retire](souls-and-instances.md#extra-trees-at-retire)),
|
|
2238
|
+
with `extra: true` and the tree's absolute `path` added:
|
|
2239
|
+
`worktree-retained` `{movedTo, branch, recordedBranch: null, extra: true,
|
|
2240
|
+
path}` and `worktree-removed` `{branch, extra: true, path}`. A row without
|
|
2241
|
+
`extra` is about `work/`.
|
|
2222
2242
|
- **Incarnation.** Each row carries the writing home's `createdAt` (or
|
|
2223
2243
|
`null` for old rows); the top-level `incarnation` is the current home's (or
|
|
2224
2244
|
`null`). Earlier incarnations are returned as this address's history.
|
|
@@ -2438,7 +2458,9 @@ oats retire <instance> --plan [--home <abs>] [--dir <d>] --json
|
|
|
2438
2458
|
"upstream":{"ref":null,"ahead":null,"behind":null},"base":{"ref":null,"ahead":null,"behind":null},"remote":null},
|
|
2439
2459
|
"workMode":"worktree","repo":"/w/one","recordedBranch":"agents/dev-1",
|
|
2440
2460
|
"children":[{"instance":"dev-1-child","agent":"dev","home":"/w/agents/dev/instances/dev-1-child","session":{"state":"shell","present":true,"backend":"tmux","established":true}}],
|
|
2441
|
-
"ambiguous":[],"pullRequest":"unknown"
|
|
2461
|
+
"ambiguous":[],"pullRequest":"unknown",
|
|
2462
|
+
"extraWorktrees":[{"path":"/w/agents/dev/instances/dev-1/.work-docs","repo":"/w/docs","branch":"agents/dev-1-docs","detachedAt":null,
|
|
2463
|
+
"disposition":"retain","movedTo":"/w/.agents/worktrees/docs/agents-dev-1-docs","reason":"…"}]},
|
|
2442
2464
|
"defaults":{"retainWorktree":true,"deleteBranch":false,"stopChildren":true,"retainChildren":true},
|
|
2443
2465
|
"planRevision":"4e5f6a7b8c9d0e1f2a3b4c5d","notes":["the worktree is on feat/x, not the recorded agents/dev-1; …"]}
|
|
2444
2466
|
```
|
|
@@ -2457,10 +2479,54 @@ oats retire <instance> --plan [--home <abs>] [--dir <d>] --json
|
|
|
2457
2479
|
`unestablished`, with a `note` saying why, and retire refuses with
|
|
2458
2480
|
`E_RUNTIME_ENDPOINT_UNKNOWN`, `--force` included. `notes` repeats either
|
|
2459
2481
|
case. Read an unknown `state` as not idle.
|
|
2482
|
+
- `notes` says what a retire would copy to recovery (0.42), in up to two
|
|
2483
|
+
strings read from the home's retirement baseline and the work mode, without
|
|
2484
|
+
hashing anything:
|
|
2485
|
+
|
|
2486
|
+
```text
|
|
2487
|
+
recovery: the home is copied to /w/agents/dev/instances/.oats-retirement/recovery before the home is removed, when it changed since spawn; not copied: .aw, .aweb-identity, .aweb-identity-*, .oats-aweb (oats.aweb)
|
|
2488
|
+
recovery: uncommitted worktree state is copied there too
|
|
2489
|
+
```
|
|
2490
|
+
|
|
2491
|
+
The first is always present. Its `; not copied: …` tail is there only when
|
|
2492
|
+
the baseline records declared home entries
|
|
2493
|
+
(`retirement.disposable.home`): `not copied: <roots> (<capability>)`, the
|
|
2494
|
+
roots as written in the manifest, in the order the baseline stores them
|
|
2495
|
+
(sorted by capability, then root), one group per capability, groups
|
|
2496
|
+
separated by `; `. At most 16 roots are listed, then `, and N more`.
|
|
2497
|
+
The second is the line above in worktree mode, `recovery: work/ is copied
|
|
2498
|
+
there when it is not empty` in directory mode, and absent in checkout,
|
|
2499
|
+
attached and workspace modes. They are strings in `notes`: no other key
|
|
2500
|
+
changes, and `planRevision` is unaffected.
|
|
2501
|
+
- `facts.extraWorktrees` (0.42.1) lists the instance's
|
|
2502
|
+
[extra trees](souls-and-instances.md#extra-trees-at-retire): linked
|
|
2503
|
+
worktrees at `<home>/.work-*` that Git confirms. It is an array, empty when
|
|
2504
|
+
there are none, in every work mode. Each row is `{path, repo, branch,
|
|
2505
|
+
detachedAt, disposition, movedTo, reason}`:
|
|
2506
|
+
- `path`: the tree's absolute path in the home.
|
|
2507
|
+
- `repo`: its repository, the first entry of that repository's `git
|
|
2508
|
+
worktree list` (the main worktree, or the bare repository).
|
|
2509
|
+
- `branch`: the branch its HEAD is on, or `null`. `detachedAt`: the commit
|
|
2510
|
+
when HEAD is detached, else `null`.
|
|
2511
|
+
- `disposition`: `"remove"` (the tree is clean: it would be removed, its
|
|
2512
|
+
branch kept), `"retain"` (it would be moved to `movedTo`, as `work/` is
|
|
2513
|
+
retained) or `"refuse"` (the retire would refuse with
|
|
2514
|
+
`E_WORK_PRESERVATION_FAILED` and keep the home).
|
|
2515
|
+
- `movedTo`: the target for `"retain"`, else `null`.
|
|
2516
|
+
- `reason`: `null` for `"remove"`, why the tree is not clean for
|
|
2517
|
+
`"retain"`, why it is refused for `"refuse"`.
|
|
2518
|
+
|
|
2519
|
+
`notes` also carries one string per tree that says the same. The trees are
|
|
2520
|
+
part of `planRevision`: a tree created, removed, dirtied or cleaned between
|
|
2521
|
+
the plan and the apply, or a change of its disposition or target (another
|
|
2522
|
+
directory taking the `<leaf>-N` it would move to, for example), refuses a
|
|
2523
|
+
guarded apply with `E_PLAN_STALE` before anything runs. A reader that does
|
|
2524
|
+
not know the key can ignore it; the `notes` strings say the same.
|
|
2460
2525
|
|
|
2461
2526
|
Plain `retire` keeps a worktree-mode instance's work: the worktree is moved
|
|
2462
2527
|
(`git worktree move`) to `<deployment>/.agents/worktrees/<repo>/<branch>` (a
|
|
2463
|
-
`-2` suffix if taken; `detached-<oid12>` when detached), state intact.
|
|
2528
|
+
`-2` suffix if taken; `detached-<oid12>` when detached), state intact. An
|
|
2529
|
+
extra tree that is not clean is moved the same way; a clean one is removed.
|
|
2464
2530
|
|
|
2465
2531
|
```text
|
|
2466
2532
|
oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree] [--home <abs>] --json
|
|
@@ -2472,8 +2538,12 @@ A first retire prints the **raw receipt**, not an envelope:
|
|
|
2472
2538
|
{"retired":"dev-1","agent":"dev",
|
|
2473
2539
|
"retention":{"worktree":"retained","movedTo":"/w/.agents/worktrees/one/feat-x","branch":"feat/x","detachedAt":null,"recordedBranch":"agents/dev-1"},
|
|
2474
2540
|
"worktreeRemoved":false,"branchDeleted":false,"removedDir":true,
|
|
2475
|
-
"workRecovery":{"path":"/w
|
|
2476
|
-
"
|
|
2541
|
+
"workRecovery":{"path":"/w/agents/dev/instances/.oats-retirement/recovery/dev-1-AbC123",
|
|
2542
|
+
"classes":["changed instance-home bytes","untracked or ignored worktree bytes"],"bytes":48444211,
|
|
2543
|
+
"home":{"paths":[{"path":".oats/","bytes":874696},{"path":".agents/","bytes":141312},{"path":"notes/","bytes":2048},{"path":"STATE.md","bytes":512}],"bytes":1018568},
|
|
2544
|
+
"outputs":{"paths":[{"path":"scratch/","bytes":1258291},{"path":"note.txt","bytes":12}],"bytes":1258303},
|
|
2545
|
+
"notCopied":[{"scope":"home","path":".aw","owner":"oats.aweb"}],
|
|
2546
|
+
"afterHooks":{"home":true,"work":false}},
|
|
2477
2547
|
"childrenStopped":[{"instance":"dev-1-child","home":"/w/agents/dev/instances/dev-1-child","ok":true,"stopped":false,"alreadyIdle":true}],
|
|
2478
2548
|
"planRevision":"4e5f6a7b8c9d0e1f2a3b4c5d","idempotencyKey":"r1","replayed":false}
|
|
2479
2549
|
```
|
|
@@ -2512,9 +2582,84 @@ A first retire prints the **raw receipt**, not an envelope:
|
|
|
2512
2582
|
retire (hooks run, a recovery copied), and neither says that one happened:
|
|
2513
2583
|
an error here is not proof that nothing happened, nor that a recovery
|
|
2514
2584
|
exists.
|
|
2515
|
-
- `
|
|
2516
|
-
|
|
2517
|
-
|
|
2585
|
+
- `extraWorktrees` (0.42.1): the extra trees the retire handled, present only
|
|
2586
|
+
when it handled at least one. Each row is the plan's row (`{path, repo,
|
|
2587
|
+
branch, detachedAt, disposition, movedTo, reason}`) plus `outcome`:
|
|
2588
|
+
`"removed"` or `"retained"`, with `movedTo` where a retained tree went. The
|
|
2589
|
+
step runs only when the home is removed (not with `--keep-dir`, not when the
|
|
2590
|
+
home is kept for a retry), after the hooks and before the worktree step of
|
|
2591
|
+
`work/`. `--discard-worktree` does not apply to it. A locked tree
|
|
2592
|
+
(`"refuse"` in the plan) stops the retire with `E_WORK_PRESERVATION_FAILED`
|
|
2593
|
+
naming the tree before anything runs (no session stop, no retire hook);
|
|
2594
|
+
a lock that appears during the hooks, or a move or removal Git refuses,
|
|
2595
|
+
stops it at the step, after the hooks. `--force` does not bypass either;
|
|
2596
|
+
the home and `work/` are kept, and trees already handled stay handled. A tree
|
|
2597
|
+
that no longer matches what the applied plan said refuses with
|
|
2598
|
+
`E_PLAN_STALE`, the home kept, rather than be moved or removed unplanned.
|
|
2599
|
+
The key is additive.
|
|
2600
|
+
- `workRecovery`: `{path, classes, bytes, home, outputs?, repoCopy?,
|
|
2601
|
+
notCopied?, afterHooks?}`, present when a recovery was written. One retire
|
|
2602
|
+
writes at most one recovery directory, and `path` is that directory.
|
|
2603
|
+
- `classes` is the union of the classes seen before and after the retire
|
|
2604
|
+
hooks, in first-seen order. `bytes` covers the whole directory,
|
|
2605
|
+
`after-hooks/` included.
|
|
2606
|
+
- `home`, `outputs` and `repoCopy` describe the pre-hook snapshot (the
|
|
2607
|
+
top-level `home/`, `repo/` or `work/`) and are not rewritten by the
|
|
2608
|
+
post-hook pass. So `repoCopy.copied: false` (a home-only snapshot) can
|
|
2609
|
+
appear with `afterHooks.work: true`: the repository copy is then under
|
|
2610
|
+
`after-hooks/repo/` only. A recovery first written after the hooks (the
|
|
2611
|
+
instance was clean before them) has no `after-hooks/`, and those keys
|
|
2612
|
+
describe that one snapshot.
|
|
2613
|
+
- `home`: `{paths: [{path, bytes}], bytes}`. The top-level entries of the
|
|
2614
|
+
`home/` snapshot, largest first, a directory ending in `/`. Always
|
|
2615
|
+
present when a recovery is written.
|
|
2616
|
+
- `outputs`: `{paths: [{path, bytes}], bytes}` names what was copied
|
|
2617
|
+
beyond tracked state, largest first.
|
|
2618
|
+
- `notCopied`: `[{scope, path, owner}]`, sorted by `path`. The home
|
|
2619
|
+
entries that existed at any point of the retire (before or after the
|
|
2620
|
+
hooks) and were left out by a capability's `retirement.disposable.home`
|
|
2621
|
+
declaration ([capabilities.md](capabilities.md#manifest)), by their real
|
|
2622
|
+
names: a prefix that matches several entries lists each one, and a
|
|
2623
|
+
declared entry that does not exist is not listed. `owner` is the
|
|
2624
|
+
declaring capability; when two declare the same entry it is the first in
|
|
2625
|
+
capability-name order. Since 0.42.1 the list also holds each verified
|
|
2626
|
+
extra tree (`.work-<purpose>`), with `owner: "kernel:extra-worktree"`: the
|
|
2627
|
+
retire handles it at its own step, never in the copy. Each entry has
|
|
2628
|
+
exactly these three keys: names and owners only, no sizes, hashes, modes
|
|
2629
|
+
or contents of what was left out. `scope` is always `"home"` in this
|
|
2630
|
+
release. Present only when there is at least one.
|
|
2631
|
+
- `afterHooks`: `{home: boolean, work: boolean}`, saying which parts were
|
|
2632
|
+
copied again under `after-hooks/`: `after-hooks/home/` when a retire hook
|
|
2633
|
+
changed the home (its bytes and permission bits, the kernel's own
|
|
2634
|
+
records included), and `after-hooks/repo/` (worktree mode) or
|
|
2635
|
+
`after-hooks/work/` (directory mode) unless the work is proven unchanged
|
|
2636
|
+
after the hooks: a directory by its bytes and bits, a worktree by its Git
|
|
2637
|
+
state and the bytes and bits of its files
|
|
2638
|
+
([souls and instances](souls-and-instances.md#retire)). A worktree that
|
|
2639
|
+
holds a repository is never proven unchanged: for it `work: true` says
|
|
2640
|
+
that a work copy was made after the hooks, not that a hook changed the
|
|
2641
|
+
work. `work` is also `true` when the pre-hook snapshot held the home
|
|
2642
|
+
only and the work was copied for the first time after the hooks, moved
|
|
2643
|
+
or not, because something beyond the home was there to preserve. The
|
|
2644
|
+
home is not copied again because the work is.
|
|
2645
|
+
Each part is whole and verified, not a delta, but `after-hooks/` is not
|
|
2646
|
+
a complete picture of the instance after the hooks: with `home: false`
|
|
2647
|
+
the recovery's home is the pre-hook one, and it holds the kernel's own
|
|
2648
|
+
records (the event log, the stop and restart receipts, listed in
|
|
2649
|
+
[souls and instances](souls-and-instances.md#retire)) as of then.
|
|
2650
|
+
Present only when that directory was written.
|
|
2651
|
+
- `workRecoveries` is no longer emitted. An older kernel on a server may
|
|
2652
|
+
still send `workRecoveries[]` beside `workRecovery` (one `{path, classes,
|
|
2653
|
+
bytes, outputs?, repoCopy?}` per recovery directory it wrote), so a
|
|
2654
|
+
reader must keep accepting it. A kernel before 0.42 sends no `home`,
|
|
2655
|
+
`notCopied` or `afterHooks`.
|
|
2656
|
+
- `recovery.json` inside the directory stays `version: 1`. It carries
|
|
2657
|
+
`phase` (`"before-hooks"`, then `"complete"` once the post-hook check has
|
|
2658
|
+
concluded), `home` and, when they apply, `notCopied` and `afterHooks`.
|
|
2659
|
+
`phase` is not in the receipt: a receipt is produced only once that check
|
|
2660
|
+
has concluded. A retried retire writes its own recovery and its receipt
|
|
2661
|
+
names only that one
|
|
2662
|
+
([souls-and-instances.md](souls-and-instances.md#retire)).
|
|
2518
2663
|
- When they apply: `rollbackIncomplete` and `retainedHome` (cleanup
|
|
2519
2664
|
incomplete, home kept, exit 1), `forcedIncomplete`, `relinked`,
|
|
2520
2665
|
`capabilityMeta`, `warnings`, `wakeSchedulesRemoved`.
|
|
@@ -2534,8 +2679,11 @@ A first retire prints the **raw receipt**, not an envelope:
|
|
|
2534
2679
|
`idempotencyKey` and `replayed: false`.
|
|
2535
2680
|
|
|
2536
2681
|
Refusals (envelopes): `E_PLAN_STALE`, `E_CHILDREN_RUNNING`,
|
|
2537
|
-
`E_WORK_PRESERVATION_FAILED` (the home is kept; retry or
|
|
2538
|
-
`--discard-worktree`
|
|
2682
|
+
`E_WORK_PRESERVATION_FAILED` (the home is kept; retry, or
|
|
2683
|
+
`--discard-worktree` when it is `work/` that could not be re-homed: it does
|
|
2684
|
+
not apply to an extra tree), `E_WORK_INSPECTION_FAILED` (the home is kept; the
|
|
2685
|
+
message names the entry or the state that could not be read),
|
|
2686
|
+
`E_SESSION_UNKNOWN`, `E_AMBIGUOUS_INSTANCE`,
|
|
2539
2687
|
`E_NO_ROOT`, `E_LIFECYCLE_FAILED`. A recovery whose Git status disagrees with
|
|
2540
2688
|
the source's carries `details: {home, statusDisagreement: {repo, rows: [{path,
|
|
2541
2689
|
source, recovery}], total}}` (the first 10 paths). Usage errors are text on
|
package/docs/desktop.md
CHANGED
|
@@ -77,16 +77,26 @@ Opened from Finder or the Dock, an app inherits launchd's PATH
|
|
|
77
77
|
the CLI's `#!/usr/bin/env node`. At startup the Desktop therefore runs your
|
|
78
78
|
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
|
-
**Choose oats…**, and the
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
**Choose oats…**, and the terminals the Desktop starts all run with your
|
|
81
|
+
shell's PATH. Only PATH is taken from the shell, never the rest of its
|
|
82
|
+
environment. The OATS tmux server is different: when a spawn or a start the
|
|
83
|
+
Desktop runs has to start it, the CLI gives it your whole login environment,
|
|
84
|
+
read from your login shell
|
|
85
|
+
([execution-targets.md](execution-targets.md#the-servers-start-environment)),
|
|
86
|
+
and falls back to the environment the Desktop gave the CLI (with this PATH)
|
|
87
|
+
only when that cannot be read, saying so on stderr. The shell is started with your environment, without what the
|
|
83
88
|
Desktop or its packaging added to its own (on the AppImage, the entries under
|
|
84
89
|
its mount), like every other program the Desktop starts. If the shell fails, times out or prints no PATH, the
|
|
85
90
|
inherited PATH stays: the backend's `/api/cli` reports `pathSource`
|
|
86
91
|
(`login-shell` or `inherited`), `pathError` (why, or `null`) and
|
|
87
92
|
`probePath` (the PATH the probe used), and the reason is logged at startup.
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
An agent's pane takes its tmux session's or server's PATH, whoever opens its
|
|
94
|
+
window, and its harness is looked up there
|
|
95
|
+
([execution-targets.md](execution-targets.md#the-servers-start-environment)):
|
|
96
|
+
a window the Desktop opens on a server that already runs gets that server's
|
|
97
|
+
PATH, not the Desktop's. A tmux server that was already running keeps its own
|
|
98
|
+
environment; restart it if its sessions should get the new PATH, which ends
|
|
99
|
+
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.
|
|
90
100
|
|
|
91
101
|
## Opening a workspace
|
|
92
102
|
|
|
@@ -239,14 +249,18 @@ for a password or key, and never runs anything over ssh itself.
|
|
|
239
249
|
|
|
240
250
|
Every instance a registered server reports shows in its workspace's roster, whoever spawned it. You
|
|
241
251
|
can open its terminal, start, restart, stop and remove it, and read its readiness, activity, Git
|
|
242
|
-
and diffs, as for a local one.
|
|
252
|
+
and diffs, as for a local one. The context panel's Soul tab and its Messaging & Teams list read
|
|
253
|
+
through the server too (`oats inspect --server <id> --home <path>`), and its Work card, "model
|
|
254
|
+
from" line and "older build" chip show the facts the server relays. Only its pull request is not read here, since the forge reads this
|
|
243
255
|
computer's clones. Every command goes to the server by the instance's home (`--server <id> --home
|
|
244
256
|
<path>`), never by a bare name. Stop and Remove show the plan the server makes, and confirm
|
|
245
257
|
against it.
|
|
246
258
|
|
|
247
259
|
A read waits for the server: the view says "Reading from <server>…", and gives up after about
|
|
248
260
|
45 seconds with "Couldn't reach <server>." When the server refuses, you see its code and message;
|
|
249
|
-
nothing is read from this computer in its place.
|
|
261
|
+
nothing is read from this computer in its place. When the server's OATS, or this computer's, lacks
|
|
262
|
+
what a part of the panel needs, that part names the server and says which OATS to update, rather
|
|
263
|
+
than showing nothing. A row that can't be opened says why on the row:
|
|
250
264
|
Herdr no longer supported, gone from <server>, not reachable on <server>, or <server> not reached.
|
|
251
265
|
For an instance a server no longer lists, the reason names the command that removes it from this
|
|
252
266
|
computer (`oats server forget <server> --instance <name>`).
|