@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
|
@@ -421,7 +421,8 @@ model or GitHub: processing continues after the home is gone.
|
|
|
421
421
|
|
|
422
422
|
Before any retire hook runs, retire preserves the instance's uncommitted and
|
|
423
423
|
unmerged work: a verified recovery under `.oats-retirement/recovery/`, named in
|
|
424
|
-
the summary.
|
|
424
|
+
the summary. One retire writes at most one recovery directory. A worktree
|
|
425
|
+
recovery is a standalone clone that carries the
|
|
425
426
|
repository's local exclude rules (`info/exclude`, a configured
|
|
426
427
|
`core.excludesFile`), its `info/attributes` and the settings that change what
|
|
427
428
|
status reports (`core.fileMode`, `core.ignoreCase`, …), so its Git status
|
|
@@ -431,6 +432,315 @@ home. **`--force` does not skip work preservation.** It forces only past a
|
|
|
431
432
|
missing or unusable cleanup marker and past incomplete hook cleanup
|
|
432
433
|
([capabilities.md](capabilities.md)).
|
|
433
434
|
|
|
435
|
+
The recovery holds the state before the retire hooks at its top level, and
|
|
436
|
+
what the hooks changed under `after-hooks/`:
|
|
437
|
+
|
|
438
|
+
```text
|
|
439
|
+
<recovery>/
|
|
440
|
+
recovery.json
|
|
441
|
+
home/ the home before the retire hooks
|
|
442
|
+
repo/ or work/ the work before the retire hooks (worktree, directory)
|
|
443
|
+
after-hooks/
|
|
444
|
+
home/ the home again, only if a hook changed home bytes
|
|
445
|
+
repo/ or work/ the work again, unless it is proven unchanged
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
After the hooks, retire copies again each part the hooks moved. The home
|
|
449
|
+
moved when its bytes did. The work is copied again unless it is proven
|
|
450
|
+
unchanged. A Git status with the same rows is not that proof: a hook can
|
|
451
|
+
rewrite a file that was already modified, and the row stays the same. In
|
|
452
|
+
directory mode the work state is the bytes of `work/`, with the permission
|
|
453
|
+
bits of each entry and of `work/` itself. In worktree mode it
|
|
454
|
+
is what a work copy of the worktree holds, each file read where the copy
|
|
455
|
+
reads it:
|
|
456
|
+
|
|
457
|
+
- its Git status, the ref its HEAD is on and the commit, and, when the copy
|
|
458
|
+
is made detached, the branch the repository's own HEAD is on;
|
|
459
|
+
- its index: the entries (what `git ls-files -s` lists, with the
|
|
460
|
+
skip-worktree and assume-unchanged marks), the resolve-undo records (what
|
|
461
|
+
`git ls-files --resolve-undo` lists) and the index file's permission bits;
|
|
462
|
+
- what the copy takes from the Git directories as files, each with its bytes
|
|
463
|
+
and its permission bits: the state of an operation in progress (a merge, a
|
|
464
|
+
rebase, a cherry-pick, a revert, a bisect), `info/attributes` and the
|
|
465
|
+
stash's log;
|
|
466
|
+
- its tags and its stash;
|
|
467
|
+
- its exclude rules (`core.excludesFile` with the file the copy reads for it,
|
|
468
|
+
and `info/exclude`) and the settings that change what `git status` reports
|
|
469
|
+
(`core.fileMode`, `core.ignoreCase`, `core.precomposeUnicode`,
|
|
470
|
+
`core.symlinks`, `core.autocrlf`, `core.eol`);
|
|
471
|
+
- the bytes and the permission bits of its files, Git metadata left out.
|
|
472
|
+
|
|
473
|
+
The rule is that the work is unchanged only if everything its copy would
|
|
474
|
+
carry is equal byte for byte, and anything that cannot be compared exactly
|
|
475
|
+
counts as changed. Two things bound it, and neither is a claim of byte
|
|
476
|
+
equality:
|
|
477
|
+
|
|
478
|
+
- **The index's derived data, a deliberate semantic exception.** The index
|
|
479
|
+
file also holds a cache of each file's stat data, which a read-only Git
|
|
480
|
+
command rewrites, and extensions derived from its entries. It is compared
|
|
481
|
+
by what it holds (its entries, its resolve-undo records and its mode), not
|
|
482
|
+
by its bytes.
|
|
483
|
+
- **The shared repository, a custody boundary.** The repository the worktree
|
|
484
|
+
belongs to stays where it is: its objects, its other branches and the
|
|
485
|
+
settings a clone of it is served under (a shallow boundary, grafts, hidden
|
|
486
|
+
refs) are not compared. That holds because no retire removes that
|
|
487
|
+
repository or deletes a branch, and a commit of the worktree that no ref
|
|
488
|
+
reaches is preserved before the worktree is removed.
|
|
489
|
+
|
|
490
|
+
The permission bits of the home directory and of the worktree directory
|
|
491
|
+
themselves are not compared: their copies are made in directories the copier
|
|
492
|
+
creates, which do not carry them.
|
|
493
|
+
|
|
494
|
+
Every part of that state is compared as its bytes, never as decoded text. A
|
|
495
|
+
ref name, a path in the index or in the status, a file name or the target of
|
|
496
|
+
a symbolic link need not be valid UTF-8, and two states that differ only in
|
|
497
|
+
such bytes are two states. This is what the proof compares, not what a copy
|
|
498
|
+
can hold: a recovery cannot hold a file whose name is not valid UTF-8. A
|
|
499
|
+
copy of a home or a worktree that has one fails, and the retire refuses with
|
|
500
|
+
nothing lost. A clean worktree that has one, with only the home to preserve,
|
|
501
|
+
needs no work copy and retires.
|
|
502
|
+
|
|
503
|
+
A worktree's tags, stash, exclude rules and settings are kept by the
|
|
504
|
+
repository it belongs to. They are part of the state because a work copy
|
|
505
|
+
carries them, so a tag or a stash made in that repository while the retire
|
|
506
|
+
hooks run adds a copy attempt. That repository's other branches are
|
|
507
|
+
not part of the state: they outlive the worktree, and a recovery does not
|
|
508
|
+
hold them. The one exception is a worktree whose copy is made detached (its
|
|
509
|
+
`HEAD` is detached, on a ref that is not a branch, or on a branch whose name
|
|
510
|
+
is not UTF-8): its copy holds the branch the `HEAD` of the repository it is
|
|
511
|
+
cloned from is on, so that branch is part of the state. When the retire removes the worktree, whether a ref that outlives
|
|
512
|
+
it reaches the commit `HEAD` is at is part of the state too: a hook that
|
|
513
|
+
deletes the one ref that reached it adds a copy attempt, though the
|
|
514
|
+
files, the status and `HEAD` did not move.
|
|
515
|
+
|
|
516
|
+
**A worktree that holds a repository is not provable.** A directory under
|
|
517
|
+
the worktree that holds a `.git` entry of any kind, a dangling symbolic link
|
|
518
|
+
included, makes the worktree not provable. One whose `.git` is a directory, a
|
|
519
|
+
file or a link that leads to one is a repository: one made by `git init` or
|
|
520
|
+
a clone, a submodule, or a linked worktree of another repository placed in
|
|
521
|
+
the work. One whose `.git` is a dangling symbolic link, or could not be
|
|
522
|
+
tested, is not read as a repository and counts all the same: the copy
|
|
523
|
+
carries such a link as a link, and no comparison reads it. A repository's
|
|
524
|
+
state is not compared:
|
|
525
|
+
the retire hooks run between the two copies and can change it in ways no read
|
|
526
|
+
of its state covers (its configuration, its objects, what a clone of it is
|
|
527
|
+
shown). So the work of such a worktree is in the pre-hook snapshot and is
|
|
528
|
+
copied again after the hooks, whatever they did. `afterHooks.work` is then
|
|
529
|
+
`true` on a successful completion: it says that a work copy was made after
|
|
530
|
+
the hooks, not that a hook changed the work. The copy after the hooks can
|
|
531
|
+
fail where the one before did not, because a hook changed the nested
|
|
532
|
+
repository; the retire then refuses with `E_WORK_PRESERVATION_FAILED` after
|
|
533
|
+
the hooks, and the home, the work and the recovery written before the hooks
|
|
534
|
+
are kept.
|
|
535
|
+
|
|
536
|
+
A worktree with a `.git` entry under it that OATS cannot read as a
|
|
537
|
+
repository (a dangling symbolic link, or a `.git` it cannot test) is never
|
|
538
|
+
home-only, and no class is added for it. So a change to the home alone can
|
|
539
|
+
now cause a work-copy attempt before the hooks that OATS 0.41 did not make,
|
|
540
|
+
and any failure of that attempt can refuse the retirement: it refuses with
|
|
541
|
+
`E_WORK_PRESERVATION_FAILED` before any retire hook runs, with no recovery
|
|
542
|
+
written and the home and the work kept. What the retire did before the copy
|
|
543
|
+
stays done, and the refusal says so: a launched instance's session has been
|
|
544
|
+
stopped by then.
|
|
545
|
+
|
|
546
|
+
Copied again is not "nothing is lost": a nested repository with its own Git
|
|
547
|
+
directory is removed with the worktree, and its copy is a clone. What the copy of a nested repository
|
|
548
|
+
holds of each kind:
|
|
549
|
+
|
|
550
|
+
| Of the nested repository | The copy holds |
|
|
551
|
+
|---|---|
|
|
552
|
+
| the branch `HEAD` is on | the branch and its commits |
|
|
553
|
+
| every other branch | its commits, without the branch name: `git fsck --unreachable` in the copy lists them |
|
|
554
|
+
| tags | the tags and what they name |
|
|
555
|
+
| the stash | its latest entry and the stash's log; the commits of older entries are not there |
|
|
556
|
+
| remote-tracking refs, notes, any other namespace | nothing beyond what a branch or a tag reaches |
|
|
557
|
+
| a repository inside it | its files, its Git directory included, as plain files |
|
|
558
|
+
|
|
559
|
+
A commit the copy holds without a name is lost to `git gc` in the copy.
|
|
560
|
+
`git fsck --unreachable` in the copy lists such commits, and
|
|
561
|
+
`git branch <name> <commit>` there gives one a name again. What the copier
|
|
562
|
+
cannot carry at all (a file whose name is not valid UTF-8, an entry that is
|
|
563
|
+
not a file, a directory or a symbolic link) refuses the copy, here as
|
|
564
|
+
anywhere.
|
|
565
|
+
|
|
566
|
+
**A worktree that cannot be proven unchanged.** Two things make a worktree
|
|
567
|
+
not provable:
|
|
568
|
+
|
|
569
|
+
- a repository under it (above);
|
|
570
|
+
- a read of the state that fails, while `git status` works: one of the Git
|
|
571
|
+
commands the list above is read with, or one of the files it is read from
|
|
572
|
+
(`info/attributes`, the stash's log, the index, an exclude file, the
|
|
573
|
+
operation state). A worktree whose path has a line feed in it is such a
|
|
574
|
+
case: Git prints its directories over more than one line. A read that
|
|
575
|
+
fails is never taken for "not set" or for "unchanged", and neither is a
|
|
576
|
+
file or directory that the retire cannot test for (no permission, for
|
|
577
|
+
example): only one that is not there is absent.
|
|
578
|
+
|
|
579
|
+
A worktree that is not provable always has its work in the pre-hook
|
|
580
|
+
snapshot, also when only the home has something to preserve, and the work is
|
|
581
|
+
copied again after the hooks whenever there is something to preserve. Its
|
|
582
|
+
recovery is therefore larger. With nothing to preserve it retires like any
|
|
583
|
+
other. The copy reads what the proof reads, so where the proof failed the
|
|
584
|
+
copy may fail too: the retire then refuses with `E_WORK_PRESERVATION_FAILED`
|
|
585
|
+
before any retire hook runs. No recovery was written and nothing was
|
|
586
|
+
deleted; the retire has already stopped the session of a launched instance
|
|
587
|
+
by then, the instance is not retired, and its home and work are kept.
|
|
588
|
+
|
|
589
|
+
A retire hook can leave the worktree in that state too. The snapshot before
|
|
590
|
+
the hooks was then taken of a provable worktree, and may hold the home only.
|
|
591
|
+
After the hooks the work is copied under `after-hooks/`, whether or not
|
|
592
|
+
anything in it moved. When that copy cannot be made, the retire refuses with
|
|
593
|
+
`E_WORK_PRESERVATION_FAILED` after the hooks have run, and the home, the
|
|
594
|
+
work and the recovery written before the hooks are all kept.
|
|
595
|
+
|
|
596
|
+
Every refusal of the copy made before the hooks, whatever its cause, ends by
|
|
597
|
+
saying what the retire has done by then: no retire hook has run, no recovery
|
|
598
|
+
was written and nothing was deleted, the instance is not retired, its home
|
|
599
|
+
and work are kept, and its session has been stopped, or this retire stopped
|
|
600
|
+
no session. A refusal after the hooks does not say that.
|
|
601
|
+
|
|
602
|
+
A snapshot that holds the home only (the work had nothing to preserve
|
|
603
|
+
before the hooks) has no work copy to stand for it. It gets the work under
|
|
604
|
+
`after-hooks/` when the hooks moved it, and also when they did not but
|
|
605
|
+
something beyond the home is there to preserve after them, such as a
|
|
606
|
+
retirement baseline that is gone.
|
|
607
|
+
|
|
608
|
+
The Git state is read at every inspection. When there is something to
|
|
609
|
+
preserve before the hooks, the files are read once before the recovery is
|
|
610
|
+
written, and a pre-hook copy of the work is verified against that read. They
|
|
611
|
+
are read at most once more after the hooks: to prove the work unchanged when
|
|
612
|
+
the Git state did not move, or to verify the copy when it did. With nothing
|
|
613
|
+
preserved before the hooks, nothing is compared: a
|
|
614
|
+
recovery is written after them whenever there is something to preserve.
|
|
615
|
+
|
|
616
|
+
A worktree whose `git status` fails refuses the retire with
|
|
617
|
+
`E_WORK_INSPECTION_FAILED` at the first inspection: no recovery was written,
|
|
618
|
+
nothing was deleted, and the retire has not stopped the instance's session.
|
|
619
|
+
A directory in place of `info/exclude`, or of the file `core.excludesFile`
|
|
620
|
+
names, is such a case: Git itself refuses to use it. Any other read of the
|
|
621
|
+
state that fails does not refuse here: it makes the worktree not provable,
|
|
622
|
+
as described above.
|
|
623
|
+
|
|
624
|
+
**A socket, a FIFO or a device file in the worktree.** An entry that is not
|
|
625
|
+
a file, a directory or a symbolic link has no bytes to read or to copy, and
|
|
626
|
+
Git prints no status row for it, so a worktree that holds one can read as
|
|
627
|
+
clean. Such an entry refuses the retire, at one of three points:
|
|
628
|
+
|
|
629
|
+
- **at the first inspection**, when the entry is where that inspection reads:
|
|
630
|
+
inside a directory that Git reports as ignored whole (unless a capability
|
|
631
|
+
declared it a disposable work root), in the `work/` of a directory
|
|
632
|
+
instance, or in the home itself. The code is `E_WORK_INSPECTION_FAILED`.
|
|
633
|
+
The message names the entry and says what to do, and ends there. The first
|
|
634
|
+
inspection runs before the retire stops the session: the session is still
|
|
635
|
+
running, no recovery was written and nothing was deleted;
|
|
636
|
+
- **before the hooks**, when the entry is anywhere else in a worktree and
|
|
637
|
+
there is something to preserve, with `E_WORK_INSPECTION_FAILED`: no hook
|
|
638
|
+
has run, no recovery was written and nothing was deleted, however often the
|
|
639
|
+
retire is retried. The message names the entry and says what to do: safely
|
|
640
|
+
stop the process or resource that owns it, or move the entry elsewhere,
|
|
641
|
+
then retry. The entry may be a live endpoint, so deleting it is not the
|
|
642
|
+
advice. The retire has already stopped the session of a launched instance
|
|
643
|
+
by then: the instance's session is stopped, the instance is not retired,
|
|
644
|
+
and its home is kept. The message says so, and says that this retire
|
|
645
|
+
stopped no session when the instance had none to stop. To continue, deal
|
|
646
|
+
with the entry and run `oats retire <instance>` again, or start the session
|
|
647
|
+
again in the same home with `oats session start --home <abs>`. A
|
|
648
|
+
self-retire (`--self`) is completed by its detached completion, which stops
|
|
649
|
+
the session and refuses in the same way; the refusal is recorded beside the
|
|
650
|
+
home, as described below. After a refused self-retire only `oats retire
|
|
651
|
+
<instance>` continues: `oats session start` refuses while the self-retire's
|
|
652
|
+
pending marker is there, and a retire clears it. Of the refusals at this
|
|
653
|
+
point, only that of `--self --keep-dir`, which stays in the calling
|
|
654
|
+
process, comes with the session still running;
|
|
655
|
+
- **after the hooks**, when a retire hook left the entry behind: the hooks
|
|
656
|
+
have run, and the home, the work and the recovery written before the hooks
|
|
657
|
+
are all kept. The code is `E_WORK_INSPECTION_FAILED` when the Git state is
|
|
658
|
+
as it was, and `E_WORK_PRESERVATION_FAILED` when the hook also moved the
|
|
659
|
+
Git state, because the copy then meets the entry before the files are
|
|
660
|
+
read; that message names the entry too.
|
|
661
|
+
|
|
662
|
+
A worktree that cannot be proven unchanged is copied without the read, so
|
|
663
|
+
there the refusal comes from the copy, as `E_WORK_PRESERVATION_FAILED`.
|
|
664
|
+
|
|
665
|
+
**A file in the worktree that cannot be read.** The same read refuses a file
|
|
666
|
+
it cannot read: one without read permission, or one over 2 GiB, which a
|
|
667
|
+
single read cannot take. Two kinds of file are read although they are nothing
|
|
668
|
+
to preserve: a tracked file that is unchanged, and a file under a work root
|
|
669
|
+
that a capability declared disposable (`retirement.disposable.work`). So a
|
|
670
|
+
retire that has only the home to preserve refuses for such a file, with
|
|
671
|
+
`E_WORK_INSPECTION_FAILED`, before any retire hook runs, with or without
|
|
672
|
+
`--force`: no recovery was written and nothing was deleted. As for a socket
|
|
673
|
+
or a FIFO refused before the hooks, the retire has already stopped the
|
|
674
|
+
session of a launched instance by then, the instance is not retired, and its
|
|
675
|
+
home is kept. The message is `could not read the worktree at <work>:
|
|
676
|
+
<reason>`. For a file without read permission the reason names the file. For
|
|
677
|
+
a file over 2 GiB it gives the size, and `find <work> -type f -size
|
|
678
|
+
+2147483647c` finds the file. To continue, move the file out of the worktree
|
|
679
|
+
or make it readable, then run `oats retire <instance>` again. With nothing to
|
|
680
|
+
preserve, the files are not read and the retire goes through.
|
|
681
|
+
|
|
682
|
+
The home is not copied again because the work is, and the work is not copied
|
|
683
|
+
again because the home is. The home's comparison after the hooks holds every
|
|
684
|
+
entry the home copy carries, as its bytes and permission bits, the kernel's
|
|
685
|
+
own records included: `.oats-events.jsonl`, `.oats-stop.json`,
|
|
686
|
+
`.oats-stop-receipt.json` and `.oats-stop-receipt.*.json`,
|
|
687
|
+
`.oats-restart.json`, `.oats-agents-md.*.previous`, `.claude/settings.json`,
|
|
688
|
+
and every field of `instance.json`. A hook that writes one of them has the
|
|
689
|
+
home copied again. Those records are left out only of the comparison with the
|
|
690
|
+
spawn baseline, which decides whether the home has anything to preserve at
|
|
691
|
+
all. When a hook moved only the work, the recovery holds them as of the
|
|
692
|
+
pre-hook snapshot. The retire's own events are written to the workspace log
|
|
693
|
+
(`<deployment>/.agents/events/`), never to the home's. A home copy is
|
|
694
|
+
verified against the digest the baseline uses, which passes over those
|
|
695
|
+
records: an inherited limit, not an exact verification of the whole home.
|
|
696
|
+
A work copy's verification passes over every entry named `.git`: a dangling
|
|
697
|
+
`.git` symbolic link under the worktree is copied as a link and not verified.
|
|
698
|
+
|
|
699
|
+
Each part under `after-hooks/` is whole and verified, not a delta, and it is
|
|
700
|
+
verified before the worktree step and before the home is removed.
|
|
701
|
+
`after-hooks/` is not a complete picture of the instance after the hooks: its
|
|
702
|
+
`home/` exists only when the home's own bytes moved, and a part that is not
|
|
703
|
+
there is the one in the pre-hook snapshot. Nothing in
|
|
704
|
+
the pre-hook `home/`, `repo/` or `work/` is rewritten. If that copy fails or
|
|
705
|
+
cannot be verified, retire refuses with `E_WORK_PRESERVATION_FAILED`, keeps
|
|
706
|
+
the home and leaves the pre-hook recovery intact. An instance with nothing to
|
|
707
|
+
preserve before the hooks, and something after them, gets its one recovery
|
|
708
|
+
then: the hooks' bytes are under `home/`, `repo/` or `work/`, and there is no
|
|
709
|
+
`after-hooks/`.
|
|
710
|
+
|
|
711
|
+
The summary prints the recovery once. The path and the `after-hooks/` line
|
|
712
|
+
are how to find both snapshots; each line after the path appears only when
|
|
713
|
+
it applies:
|
|
714
|
+
|
|
715
|
+
```text
|
|
716
|
+
Retired dev-1 (agent dev)
|
|
717
|
+
Work that was not committed has been preserved: changed instance-home bytes, untracked or ignored worktree bytes
|
|
718
|
+
/w/agents/dev/instances/.oats-retirement/recovery/dev-1-AbC123 (46.2 MiB)
|
|
719
|
+
copied from the home: .oats/ (854.2 KiB), .agents/ (138.0 KiB), notes/ (2.0 KiB), STATE.md (512 B) — 994.7 KiB in total
|
|
720
|
+
copied outputs: scratch/ (1.2 MiB), note.txt (12 B) — 1.2 MiB in total
|
|
721
|
+
not copied: .aw, .oats-aweb (oats.aweb)
|
|
722
|
+
after the retire hooks: home copied again under after-hooks/
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
`recovery.json` records the recovery's `phase`. It is `"before-hooks"` when the
|
|
726
|
+
pre-hook snapshot is written, and `"complete"` once the post-hook check has
|
|
727
|
+
concluded, together with `afterHooks: { home, work }` when `after-hooks/` was
|
|
728
|
+
written. `after-hooks/` counts only when `recovery.json` lists it. A retried
|
|
729
|
+
retire, after incomplete cleanup or after a refused or interrupted attempt,
|
|
730
|
+
starts over: it writes its own recovery, its receipt names only that one, and
|
|
731
|
+
it never reads, amends or deletes an earlier one. A directory that an earlier
|
|
732
|
+
attempt left at `before-hooks` is not corrupt: it is a complete, verified
|
|
733
|
+
pre-hook snapshot.
|
|
734
|
+
|
|
735
|
+
Home entries that a capability declared in `retirement.disposable.home`
|
|
736
|
+
([capabilities.md](capabilities.md#manifest)) are provider-owned state, not
|
|
737
|
+
the instance's work, and are not copied: neither before nor after the hooks.
|
|
738
|
+
They stay in the home until the home is removed, retire hooks still see them,
|
|
739
|
+
and a home kept for a retry keeps them. The summary lists the ones that
|
|
740
|
+
exist under "not copied", by name, with the declaring capability. The
|
|
741
|
+
declaration is recorded at spawn: a home spawned before its capability
|
|
742
|
+
declared them gains no exclusion from a package update, and is copied whole.
|
|
743
|
+
|
|
434
744
|
Retire stops the harness through the home's session receipt. A home spawned
|
|
435
745
|
before 0.25.9 has none. It retires only when its session is observably gone:
|
|
436
746
|
instance.json records no launch, or the recorded tmux server is not running,
|
|
@@ -499,6 +809,76 @@ commit yet cannot be read that way: a retire that would remove it, or that
|
|
|
499
809
|
has work of it to copy, refuses with `E_WORK_INSPECTION_FAILED` and removes
|
|
500
810
|
nothing.
|
|
501
811
|
|
|
812
|
+
#### Extra trees at retire
|
|
813
|
+
|
|
814
|
+
An instance can hold [extra trees](#extra-trees) in its home beside `work/`.
|
|
815
|
+
Retire handles them itself, so the home removal never deletes their work.
|
|
816
|
+
|
|
817
|
+
An **extra tree** is a top-level entry of the home named `.work-*` that is a
|
|
818
|
+
real directory (not a symbolic link), whose `.git` is a regular file, and that
|
|
819
|
+
Git confirms is a registered linked worktree of some repository: its Git
|
|
820
|
+
directory differs from its common directory, its top level is the entry
|
|
821
|
+
itself, and that repository's `git worktree list` names it. Its repository is
|
|
822
|
+
the first entry of that list (the main worktree, or the bare repository).
|
|
823
|
+
Anything named `.work-*` that does not verify (a plain directory, an orphaned
|
|
824
|
+
`.git` file, a symbolic link, a nested full clone), or that is a worktree of a
|
|
825
|
+
repository inside the home itself, is ordinary home bytes, and the home's
|
|
826
|
+
recovery copies it as before (with that repository). This holds in every work mode,
|
|
827
|
+
`directory` included.
|
|
828
|
+
|
|
829
|
+
A verified extra tree is not part of the home's recovery bytes: it does not
|
|
830
|
+
count as changed instance-home bytes, it is not copied, and the recovery's
|
|
831
|
+
`notCopied` lists it as `{scope: "home", path: ".work-<purpose>", owner:
|
|
832
|
+
"kernel:extra-worktree"}`.
|
|
833
|
+
|
|
834
|
+
The extra-tree step runs only when the home is going to be removed: not with
|
|
835
|
+
`--keep-dir`, and not when the retire keeps the home for a retry. That is the
|
|
836
|
+
condition of the `work/` worktree step (nothing outstanding, or `--force`).
|
|
837
|
+
It runs after the retire hooks and before the `work/` step, so a refusal
|
|
838
|
+
leaves `work/` untouched. For each tree:
|
|
839
|
+
|
|
840
|
+
- **A clean tree is removed.** Clean means that `git status`, ignored and
|
|
841
|
+
untracked files included, is empty; no merge, rebase, cherry-pick, revert,
|
|
842
|
+
bisect or sequencer operation is in progress; and its HEAD commit is
|
|
843
|
+
reached by a ref of its repository (the tree's own HEAD, reflog and
|
|
844
|
+
`refs/worktree/` refs do not count: they go with its admin entry). The retire runs `git worktree remove` (without
|
|
845
|
+
`--force`) and `git worktree prune`, and verifies that the tree is gone from
|
|
846
|
+
`git worktree list`. Every Git command of this step runs helper-free (no
|
|
847
|
+
fsmonitor, hooks or external diff the repository's configuration names). Its branch is never deleted: a commit on the branch that
|
|
848
|
+
was not pushed stays in the clone, on that branch.
|
|
849
|
+
- **Any other tree is re-homed**, as `work/` is by default: `git worktree
|
|
850
|
+
move` to `<deployment>/.agents/worktrees/<repo>/<leaf>`, where `<leaf>` is
|
|
851
|
+
the branch name with characters outside `A-Za-z0-9._-` replaced by `-`, or
|
|
852
|
+
`detached-<12 hex>` when HEAD is detached. A target that exists gets `-2`,
|
|
853
|
+
`-3`, and so on. A tree whose HEAD cannot be read is re-homed too.
|
|
854
|
+
- **A tree the retire cannot handle refuses it.** A locked tree (`git worktree
|
|
855
|
+
lock`) refuses before anything runs: a retire that would remove the home
|
|
856
|
+
finds the lock in its first inspection, before the session is stopped and
|
|
857
|
+
before any retire hook, and stops with "nothing was run or removed". A lock
|
|
858
|
+
that appears while the hooks run is refused at the step, before any tree is
|
|
859
|
+
touched. A move or a removal that Git refuses (a tree with submodules, for
|
|
860
|
+
example), or a removal that cannot be verified, refuses at the step, after
|
|
861
|
+
the hooks. Each refusal is `E_WORK_PRESERVATION_FAILED` naming the tree,
|
|
862
|
+
and the home is kept. At the step, trees already handled in that pass stay
|
|
863
|
+
handled, and the message says what was done. `--force` does not bypass it:
|
|
864
|
+
it forces past hook cleanup, not past local work.
|
|
865
|
+
|
|
866
|
+
`--discard-worktree` applies to `work/` only: a tree that is not clean is
|
|
867
|
+
always re-homed. The retire writes one workspace event per handled tree
|
|
868
|
+
(`worktree-removed` or `worktree-retained`, with `extra: true` and the tree's
|
|
869
|
+
`path`), and its summary prints one line per tree: removed, or re-homed to
|
|
870
|
+
the new path.
|
|
871
|
+
|
|
872
|
+
`oats retire <instance> --plan` lists the trees and what the retire would do
|
|
873
|
+
with each, and they are part of the plan's revision: a tree created, removed,
|
|
874
|
+
dirtied or cleaned between the plan and a guarded apply, or a new target for
|
|
875
|
+
it, refuses the apply with `E_PLAN_STALE` before anything runs. The retire
|
|
876
|
+
checks the trees again at the step itself and refuses with `E_PLAN_STALE`,
|
|
877
|
+
keeping the home, rather than move or remove a tree in a way the plan did
|
|
878
|
+
not say. The retire hooks have run by then, and no tree was moved or
|
|
879
|
+
removed. The fields are in
|
|
880
|
+
[the CLI API](desktop-cli-api.md#retire).
|
|
881
|
+
|
|
502
882
|
## Work modes
|
|
503
883
|
|
|
504
884
|
A work mode decides what `./work` points at and what discipline the agent must
|
|
@@ -514,7 +894,8 @@ instructions state first (`injects/instance-boundary.md`):
|
|
|
514
894
|
target another one deliberately).
|
|
515
895
|
- `<instance-home>/work` — the repository or workspace view — is where
|
|
516
896
|
repository reading, editing, building, testing, git and commits happen, to the
|
|
517
|
-
extent the mode below permits.
|
|
897
|
+
extent the mode below permits. In `worktree` and `checkout` mode, the
|
|
898
|
+
instance's [extra trees](#extra-trees) in the home serve the same purpose.
|
|
518
899
|
- The home has no soul link: the composed `AGENTS.md` already carries the
|
|
519
900
|
soul's instructions, and `instance.json` `soulDir` records the (read-only,
|
|
520
901
|
per-commit) soul directory every hook and dispatched command receives as
|
|
@@ -553,10 +934,12 @@ Use this for agents that will edit code or docs independently.
|
|
|
553
934
|
Rules:
|
|
554
935
|
|
|
555
936
|
- Build, test, and commit from `work/`, on your own branch.
|
|
556
|
-
- Never
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
937
|
+
- Never work in a shared checkout (the repo's main checkout, or any clone
|
|
938
|
+
others use): do not edit, commit or switch branches there. Against a clone,
|
|
939
|
+
run only `git worktree add` and `git worktree remove`, as in
|
|
940
|
+
[extra trees](#extra-trees).
|
|
941
|
+
- Everything you change happens in `work/` or in your extra trees.
|
|
942
|
+
- Leave your branch and the worktree list clean when your task closes.
|
|
560
943
|
|
|
561
944
|
### `checkout` — shared current branch
|
|
562
945
|
|
|
@@ -570,7 +953,10 @@ Rules:
|
|
|
570
953
|
|
|
571
954
|
- Stay on the currently checked-out branch.
|
|
572
955
|
- Do not switch branches unless explicitly asked.
|
|
573
|
-
-
|
|
956
|
+
- No destructive git operations (`reset --hard`, rebase, force-push, checkout
|
|
957
|
+
of another branch) unless the human explicitly asks.
|
|
958
|
+
- Work that needs its own branch goes in an [extra tree](#extra-trees), not
|
|
959
|
+
in `work/`.
|
|
574
960
|
|
|
575
961
|
### `attached` — another instance's tree
|
|
576
962
|
|
|
@@ -595,7 +981,10 @@ used. No implicit fallback changes the other modes.
|
|
|
595
981
|
`--work-dir` and `--branch` are rejected. Canonical instructions, skill
|
|
596
982
|
composition, provider trust and harness preflight still apply. Retirement preserves nonempty work in verified recovery storage beside the
|
|
597
983
|
home (`workRecovery.path/work`) before deleting it, including files created by
|
|
598
|
-
hooks; directory work has no disposable-root exemptions.
|
|
984
|
+
hooks; directory work has no disposable-root exemptions. A retire hook's later
|
|
985
|
+
change to work is in the same recovery, under
|
|
986
|
+
`workRecovery.path/after-hooks/work` (under `workRecovery.path/work` when
|
|
987
|
+
nothing needed preserving before the hooks). The work-root cannot be
|
|
599
988
|
exchanged for a symlink. Recovery does not replace the worker's delivery protocol.
|
|
600
989
|
|
|
601
990
|
### `workspace` — cross-repo coordinator
|
|
@@ -622,6 +1011,50 @@ Rules:
|
|
|
622
1011
|
|
|
623
1012
|
The instance records no branch: the workspace is not a Git tree.
|
|
624
1013
|
|
|
1014
|
+
### Extra trees
|
|
1015
|
+
|
|
1016
|
+
An instance in `worktree` or `checkout` mode can create extra trees: linked
|
|
1017
|
+
Git worktrees in its home, beside `work/`. It does so when the work needs
|
|
1018
|
+
another branch, or another repository of the deployment (one task that
|
|
1019
|
+
touches several repositories). The `worktree` and `checkout` briefings give
|
|
1020
|
+
the command; the `workspace`, `directory` and `attached` briefings do not.
|
|
1021
|
+
There is no `oats` command for it.
|
|
1022
|
+
|
|
1023
|
+
`<clone>` is any clone of the deployment (`oats-local.yaml` `clones:`, or
|
|
1024
|
+
`<deployment>/<repo>`), `origin` is its remote for that repository, and
|
|
1025
|
+
`<base>` is the remote branch the work starts from (the branch itself, to
|
|
1026
|
+
rework an existing one):
|
|
1027
|
+
|
|
1028
|
+
```bash
|
|
1029
|
+
git -C <clone> worktree add --detach "$OATS_INSTANCE_HOME/.work-<purpose>"
|
|
1030
|
+
git -C "$OATS_INSTANCE_HOME/.work-<purpose>" fetch --refmap= origin <base>
|
|
1031
|
+
git -C "$OATS_INSTANCE_HOME/.work-<purpose>" switch -c <branch> FETCH_HEAD
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
- The tree starts from the remote's current state, never from a local branch
|
|
1035
|
+
of the clone, which may be stale.
|
|
1036
|
+
- Creating it moves none of the clone's refs. The fetch runs inside the new
|
|
1037
|
+
linked tree, which has its own `FETCH_HEAD`, and `--refmap=` keeps it from
|
|
1038
|
+
updating remote-tracking refs. The clone's `FETCH_HEAD`, branches and work
|
|
1039
|
+
tree are not touched, so creation does not race with others who use the
|
|
1040
|
+
clone. The fetched objects go to the repository's shared object store.
|
|
1041
|
+
- `git switch` needs Git 2.23 or later.
|
|
1042
|
+
- `<branch>` follows the repository's own naming rules, else
|
|
1043
|
+
`agents/<instance>-<purpose>`. If that branch already exists in the clone,
|
|
1044
|
+
`switch -c` refuses: use `<instance>/<branch>`. Never `-C` or `-B`, which
|
|
1045
|
+
reset a branch someone else may own.
|
|
1046
|
+
- The tree has no upstream. Push with `git push origin HEAD:<remote-branch>`
|
|
1047
|
+
(`<base>` when reworking an existing branch). A push does update the
|
|
1048
|
+
clone's `refs/remotes/origin/<remote-branch>`, as any push does.
|
|
1049
|
+
- Before the task closes, merge each extra tree into the PR branch, or push
|
|
1050
|
+
its branch and name it in the hand-back; then
|
|
1051
|
+
`git -C <clone> worktree remove "$OATS_INSTANCE_HOME/.work-<purpose>"`.
|
|
1052
|
+
|
|
1053
|
+
`$OATS_INSTANCE_HOME` is set in every harness session OATS launches (Claude
|
|
1054
|
+
Code, Codex, pi), not only in hooks. Retirement removes a clean extra tree
|
|
1055
|
+
and re-homes one that holds work, but the briefing tells the agent not to
|
|
1056
|
+
rely on it: see [extra trees at retire](#extra-trees-at-retire).
|
|
1057
|
+
|
|
625
1058
|
## Agents root
|
|
626
1059
|
|
|
627
1060
|
Instance homes live under the deployment's agents root,
|
|
@@ -26,9 +26,10 @@ root, and not the work tree. Anything that says "your home" means this directory
|
|
|
26
26
|
**`<instance-home>/work` is your repository or workspace view** — whatever your
|
|
27
27
|
work mode grants you of the code.
|
|
28
28
|
|
|
29
|
-
- **Repository work happens there
|
|
30
|
-
|
|
31
|
-
or from your
|
|
29
|
+
- **Repository work happens there, or in the extra trees your mode block
|
|
30
|
+
grants, and nowhere else**: reading, editing, building, testing, git and
|
|
31
|
+
commits, on repository content. Never from the main checkout or from your
|
|
32
|
+
home root, beyond what your mode block names.
|
|
32
33
|
- **What your mode permits is the mode block's call**, immediately below. Some
|
|
33
34
|
modes are read-only, some share a tree with others, and that block is the
|
|
34
35
|
authority on which operations are yours to perform.
|
package/injects/work-checkout.md
CHANGED
|
@@ -7,6 +7,28 @@ in the same tree as the human and possibly other agents.
|
|
|
7
7
|
explicitly asked.**
|
|
8
8
|
- No destructive git operations (reset --hard, rebase, force-push, checkout
|
|
9
9
|
of another branch) without an explicit human instruction.
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
- Work that needs its own branch goes in an extra tree, not in `work/`.
|
|
11
|
+
|
|
12
|
+
### Extra trees
|
|
13
|
+
|
|
14
|
+
When the work needs another branch, or another repository of this deployment,
|
|
15
|
+
create an extra tree in your home. `<clone>` is any clone of this deployment
|
|
16
|
+
(`oats-local.yaml` `clones:`, or `<deployment>/<repo>`); `origin` is its remote
|
|
17
|
+
for that repository; `<base>` is the remote branch the work starts from (the
|
|
18
|
+
branch itself, when you rework an existing one):
|
|
19
|
+
|
|
20
|
+
git -C <clone> worktree add --detach "$OATS_INSTANCE_HOME/.work-<purpose>"
|
|
21
|
+
git -C "$OATS_INSTANCE_HOME/.work-<purpose>" fetch --refmap= origin <base>
|
|
22
|
+
git -C "$OATS_INSTANCE_HOME/.work-<purpose>" switch -c <branch> FETCH_HEAD
|
|
23
|
+
|
|
24
|
+
- This starts from the remote's current state and moves none of the clone's
|
|
25
|
+
refs. Never start from a local branch of the clone, which may be stale.
|
|
26
|
+
- Name `<branch>` by the repository's own rules, else `agents/<instance>-<purpose>`.
|
|
27
|
+
If it already exists in that clone, `switch -c` refuses: use `<instance>/<branch>`.
|
|
28
|
+
Never `-C`/`-B`, which reset a branch someone else may own.
|
|
29
|
+
- The tree has no upstream: push with `git push origin HEAD:<remote-branch>`
|
|
30
|
+
(`<base>` when you rework an existing branch).
|
|
31
|
+
- Before your task closes, merge each extra tree into your PR branch, or push its
|
|
32
|
+
branch and name it in your hand-back; then
|
|
33
|
+
`git -C <clone> worktree remove "$OATS_INSTANCE_HOME/.work-<purpose>"`.
|
|
34
|
+
Retirement keeps a tree that still holds uncommitted work, but don't rely on it.
|
package/injects/work-worktree.md
CHANGED
|
@@ -3,11 +3,34 @@
|
|
|
3
3
|
Your `./work` is a **git worktree on your own branch** — a full checkout that is
|
|
4
4
|
yours alone: build, test and commit there, on your branch.
|
|
5
5
|
|
|
6
|
-
- **Never
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
your human spawns another instance.
|
|
6
|
+
- **Never work in a shared checkout** (the repo's main checkout, or any clone
|
|
7
|
+
others use): don't edit, commit or switch branches there. Against a clone you
|
|
8
|
+
run only `worktree add`/`remove` below. If unsure, `pwd`.
|
|
9
|
+
- Everything you change happens in `work/` or your extra trees — **including your
|
|
10
|
+
own soul** when it lives in this repo: soul edits are branch changes, reviewed
|
|
11
|
+
and merged like code.
|
|
13
12
|
- Leave your branch and the worktree list clean when your task closes.
|
|
13
|
+
|
|
14
|
+
### Extra trees
|
|
15
|
+
|
|
16
|
+
When the work needs another branch, or another repository of this deployment,
|
|
17
|
+
create an extra tree in your home. `<clone>` is any clone of this deployment
|
|
18
|
+
(`oats-local.yaml` `clones:`, or `<deployment>/<repo>`); `origin` is its remote
|
|
19
|
+
for that repository; `<base>` is the remote branch the work starts from (the
|
|
20
|
+
branch itself, when you rework an existing one):
|
|
21
|
+
|
|
22
|
+
git -C <clone> worktree add --detach "$OATS_INSTANCE_HOME/.work-<purpose>"
|
|
23
|
+
git -C "$OATS_INSTANCE_HOME/.work-<purpose>" fetch --refmap= origin <base>
|
|
24
|
+
git -C "$OATS_INSTANCE_HOME/.work-<purpose>" switch -c <branch> FETCH_HEAD
|
|
25
|
+
|
|
26
|
+
- This starts from the remote's current state and moves none of the clone's
|
|
27
|
+
refs. Never start from a local branch of the clone, which may be stale.
|
|
28
|
+
- Name `<branch>` by the repository's own rules, else `agents/<instance>-<purpose>`.
|
|
29
|
+
If it already exists in that clone, `switch -c` refuses: use `<instance>/<branch>`.
|
|
30
|
+
Never `-C`/`-B`, which reset a branch someone else may own.
|
|
31
|
+
- The tree has no upstream: push with `git push origin HEAD:<remote-branch>`
|
|
32
|
+
(`<base>` when you rework an existing branch).
|
|
33
|
+
- Before your task closes, merge each extra tree into your PR branch, or push its
|
|
34
|
+
branch and name it in your hand-back; then
|
|
35
|
+
`git -C <clone> worktree remove "$OATS_INSTANCE_HOME/.work-<purpose>"`.
|
|
36
|
+
Retirement keeps a tree that still holds uncommitted work, but don't rely on it.
|