@awebai/oats 0.27.0 → 0.27.2
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/capabilities/oats-aweb/bin/oats-aweb.mjs +272 -72
- package/capabilities/oats-aweb/injects/aweb.md +9 -1
- package/capabilities/oats-aweb/lib/binding-wire.mjs +42 -11
- package/capabilities/oats-aweb/oats.json +47 -5
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +79 -301
- package/docs/capabilities.md +3 -1
- package/docs/desktop-cli-api.md +6 -0
- package/docs/packages.md +1 -1
- package/docs/release-notes/v0.27.1.md +29 -0
- package/docs/release-notes/v0.27.2.md +51 -0
- package/docs/souls-and-instances.md +12 -0
- package/lib/core.mjs +102 -6
- package/package-catalog.json +1 -1
- package/package.json +1 -1
package/docs/desktop-cli-api.md
CHANGED
|
@@ -953,6 +953,12 @@ location. The receipt says so:
|
|
|
953
953
|
discarding the worktree (a checked-out branch cannot be deleted).
|
|
954
954
|
- A failed move keeps the home and refuses `E_WORK_PRESERVATION_FAILED` —
|
|
955
955
|
nothing is lost; retry or pass `--discard-worktree`.
|
|
956
|
+
- When a recovery's Git status disagrees with the source's (0.27.2),
|
|
957
|
+
`E_WORK_PRESERVATION_FAILED` carries `details: {home, statusDisagreement:
|
|
958
|
+
{repo, rows: [{path, source, recovery}], total}}`. `repo` is `.` or a nested
|
|
959
|
+
repository's path. `source`/`recovery` are the porcelain `XY` codes, or
|
|
960
|
+
`null` where that side has no row. `rows` holds the first 10 paths, sorted,
|
|
961
|
+
and `total` counts all of them. The message names the same rows.
|
|
956
962
|
- Non-worktree modes report `retention: null`. Quarantine/rollback paths keep
|
|
957
963
|
their removal semantics.
|
|
958
964
|
- A recovery (`workRecovery`, `workRecoveries[]`) is `{path, classes, bytes,
|
package/docs/packages.md
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# OATS 0.27.1
|
|
2
|
+
|
|
3
|
+
## Changed
|
|
4
|
+
|
|
5
|
+
- **oats.aweb 1.14.2 is bundled and pinned** (was 1.13.1): **joining teams
|
|
6
|
+
beyond the personal one now works.** An instance joins the eligible teams it
|
|
7
|
+
is given at spawn (`--provider oats.aweb join=<labels>`) or later with
|
|
8
|
+
`oats aweb join <labels>` / the Desktop's Teams controls, and leaves with
|
|
9
|
+
`oats aweb leave <labels>`. Each joined team gets its own identity under the
|
|
10
|
+
instance home. Joined teams **poll** (their mail is read between tasks); live
|
|
11
|
+
receive for joined teams is planned for oats.aweb 1.15. Joining needs
|
|
12
|
+
**aw 1.36.12 or later** (older aw answers `E_TEAM_AW_FLOOR`; the primary
|
|
13
|
+
identity still mints). A leave removes the local identity only after aweb
|
|
14
|
+
confirms the membership is released, so a failed leave can be retried.
|
|
15
|
+
Retire leaves every joined team before releasing the primary identity.
|
|
16
|
+
- The framework workspace (`oats-workspace.yaml`) pins oats.aweb v1.14.2.
|
|
17
|
+
- Skip oats.aweb 1.14.0 and 1.14.1 (tagged, never pinned by an OATS release): their binding check answered a `teams` key that OATS rejects, so `oats readiness` showed messaging unknown; 1.14.2 fixes it, and 1.14.0's joins were refused by aw.
|
|
18
|
+
|
|
19
|
+
## Known limitations
|
|
20
|
+
|
|
21
|
+
- The bundled oats.okf is still 2.1.5: its harvest worker spawns with `--runtime`, so each harvest answers one `deprecated-runtime-name` warning on 0.27.x. It's harmless; oats.okf 2.1.6 passes `--harness`.
|
|
22
|
+
|
|
23
|
+
- A per-workspace personal team still needs oats.aweb 1.15 (the aweb service
|
|
24
|
+
and CLI already carry personal enrollment); until then "personal" is the
|
|
25
|
+
person's active aweb team.
|
|
26
|
+
|
|
27
|
+
## Upgrading from 0.27.0
|
|
28
|
+
|
|
29
|
+
- To join teams: upgrade aw to 1.36.12 or later (and restart the host's wake daemon), then `oats sync` so the deployment's lock takes oats.aweb 1.14.2. Existing instances keep their primary identity; they join teams with `oats aweb join <labels>`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# OATS 0.27.2
|
|
2
|
+
|
|
3
|
+
## Fixed
|
|
4
|
+
|
|
5
|
+
- **Retiring a worktree instance whose repository excludes paths locally.**
|
|
6
|
+
Retire refused with `E_WORK_PRESERVATION_FAILED` ("recovered Git
|
|
7
|
+
index/status disagreed with the source") when the repository ignored a path
|
|
8
|
+
only through its local rules: the common Git directory's `info/exclude`, or
|
|
9
|
+
a `core.excludesFile` set in the repository's config. The recovery clone had
|
|
10
|
+
neither, so such a path was ignored (`!!`) in the source but untracked, or
|
|
11
|
+
absent for an empty directory, in the clone. Preservation runs before the
|
|
12
|
+
retire hooks and `--force` does not bypass it, so the instance could not be
|
|
13
|
+
retired and its identities were never revoked. The recovery now carries
|
|
14
|
+
those patterns into its own `.git/info/exclude` (nested repositories too),
|
|
15
|
+
and `recovery.json` lists them under `excludes`
|
|
16
|
+
(`[{ repo, kind, path }]`).
|
|
17
|
+
When a recovery's status still disagrees, the refusal now **names the
|
|
18
|
+
differing rows** from both sides, for example
|
|
19
|
+
`.scratch/ (source !!, recovery absent)`: the first 10, then
|
|
20
|
+
`; and N more`. Under `--json` they are in
|
|
21
|
+
`details.statusDisagreement` (`{repo, rows: [{path, source, recovery}],
|
|
22
|
+
total}`; see [desktop-cli-api.md](../desktop-cli-api.md)).
|
|
23
|
+
**On an older kernel:** remove the disposable paths that
|
|
24
|
+
`git -C <home>/work status --porcelain --ignored=matching` lists as `!!`
|
|
25
|
+
before retiring.
|
|
26
|
+
|
|
27
|
+
- **Retiring a worktree instance whose repository changes how status reads
|
|
28
|
+
its files.** The recovery clone probed its own settings and lacked the
|
|
29
|
+
common Git directory's `info/attributes`. A repository with, for example,
|
|
30
|
+
`core.fileMode=false` and a mode-only change was clean in the source but
|
|
31
|
+
` M` in the clone, so retire refused. The recovery now takes the source's
|
|
32
|
+
`core.fileMode`, `core.ignoreCase`, `core.precomposeUnicode`,
|
|
33
|
+
`core.symlinks`, `core.autocrlf` and `core.eol` (set, or unset where the
|
|
34
|
+
source leaves them unset) and its `info/attributes`, nested repositories
|
|
35
|
+
included. `recovery.json` lists them under `statusConfig`
|
|
36
|
+
(`[{ repo, kind: "config", key, value } | { repo, kind: "info/attributes", path }]`).
|
|
37
|
+
|
|
38
|
+
## Known limitations
|
|
39
|
+
|
|
40
|
+
- The bundled oats.okf is still 2.1.5 (its harvest answers one `deprecated-runtime-name` warning per spawn; harmless). oats.okf 2.1.6 follows in a later patch.
|
|
41
|
+
|
|
42
|
+
- **A `core.attributesFile` or filter driver set in the repository's own
|
|
43
|
+
config is not carried** into a recovery. Merging its patterns would change
|
|
44
|
+
their precedence, and a pointer would tie the recovery to a host file. Such
|
|
45
|
+
a repository can still make retire refuse, and the refusal names the rows.
|
|
46
|
+
Workaround: commit or restore the files it re-reads before retiring.
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
- `oats retire --force` does **not** skip work preservation
|
|
51
|
+
([souls-and-instances.md](../souls-and-instances.md#retire)).
|
|
@@ -302,6 +302,18 @@ uncertified capture retains the home for retry. Successful retirement enqueues
|
|
|
302
302
|
evidence but never waits for a model or GitHub: independent processing and
|
|
303
303
|
source-targeted inspection continue after the home disappears.
|
|
304
304
|
|
|
305
|
+
Before any retire hook runs, retire preserves the instance's uncommitted and
|
|
306
|
+
unmerged work: a verified recovery under `.oats-retirement/recovery/`, named in
|
|
307
|
+
the summary. A worktree recovery is a standalone clone that carries the
|
|
308
|
+
repository's local exclude rules (`info/exclude`, a configured
|
|
309
|
+
`core.excludesFile`), its `info/attributes` and the settings that change what
|
|
310
|
+
status reports (`core.fileMode`, `core.ignoreCase`, …), so its Git status
|
|
311
|
+
matches the source's. A recovery that
|
|
312
|
+
cannot be verified refuses with `E_WORK_PRESERVATION_FAILED` and keeps the
|
|
313
|
+
home. **`--force` does not skip work preservation.** It forces only past a
|
|
314
|
+
missing or unusable cleanup marker and past incomplete hook cleanup
|
|
315
|
+
([capabilities.md](capabilities.md)).
|
|
316
|
+
|
|
305
317
|
`oats retire <instance> --self` lets an instance retire itself when the human
|
|
306
318
|
or briefing says it is done. A live harness cannot give a stable final
|
|
307
319
|
inspection of its own work, so the calling process inspects, runs, and removes
|
package/lib/core.mjs
CHANGED
|
@@ -4092,6 +4092,36 @@ function worktreeStatus(repo) {
|
|
|
4092
4092
|
}
|
|
4093
4093
|
}
|
|
4094
4094
|
|
|
4095
|
+
/** `git status --porcelain=v1 -z` as Map<path, XY>; a rename/copy row names its source (`R ← old`). */
|
|
4096
|
+
function statusRows(z) {
|
|
4097
|
+
const rows = new Map(), parts = z.split("\0");
|
|
4098
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4099
|
+
const row = parts[i];
|
|
4100
|
+
if (!row) continue;
|
|
4101
|
+
const xy = row.slice(0, 2), path = row.slice(3);
|
|
4102
|
+
rows.set(path, xy[0] === "R" || xy[0] === "C" ? `${xy} ← ${parts[++i]}` : xy);
|
|
4103
|
+
}
|
|
4104
|
+
return rows;
|
|
4105
|
+
}
|
|
4106
|
+
const STATUS_DISAGREEMENT_CAP = 10;
|
|
4107
|
+
/** Where two statuses disagree, by path: { rows: [{ path, source, recovery }] (null = absent; sorted, the
|
|
4108
|
+
* first `cap`), total }. What E_WORK_PRESERVATION_FAILED shows, so an operator sees `!! .scratch/` against
|
|
4109
|
+
* an absent row directly. */
|
|
4110
|
+
export function statusDisagreement(sourceStatus, recoveredStatus, cap = STATUS_DISAGREEMENT_CAP) {
|
|
4111
|
+
const a = statusRows(sourceStatus), b = statusRows(recoveredStatus);
|
|
4112
|
+
const paths = [...new Set([...a.keys(), ...b.keys()])].filter((p) => a.get(p) !== b.get(p)).sort();
|
|
4113
|
+
return { rows: paths.slice(0, cap).map((path) => ({ path, source: a.get(path) ?? null, recovery: b.get(path) ?? null })), total: paths.length };
|
|
4114
|
+
}
|
|
4115
|
+
/** Refuse a recovery whose Git status is not the source's, naming the differing rows (capped). */
|
|
4116
|
+
function assertStatusAgrees(source, recovered, what, repo) {
|
|
4117
|
+
const sourceStatus = worktreeStatus(source), recoveredStatus = worktreeStatus(recovered);
|
|
4118
|
+
if (sourceStatus === recoveredStatus) return;
|
|
4119
|
+
const diff = statusDisagreement(sourceStatus, recoveredStatus);
|
|
4120
|
+
const shown = diff.rows.map((r) => `${r.path} (source ${r.source ?? "absent"}, recovery ${r.recovery ?? "absent"})`).join("; ");
|
|
4121
|
+
const more = diff.total > diff.rows.length ? `; and ${diff.total - diff.rows.length} more` : "";
|
|
4122
|
+
throw Object.assign(new Error(`${what}${shown ? `: ${shown}${more}` : ""}`), { statusDisagreement: { repo, ...diff } });
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4095
4125
|
function generatedWorkFingerprint(work, status, disposableRoots = []) {
|
|
4096
4126
|
const owned = (path) => disposableRoots.some((root) => path === root || path.startsWith(`${root}${sep}`));
|
|
4097
4127
|
const paths = status.split("\0").filter(Boolean)
|
|
@@ -4971,6 +5001,64 @@ function restoreStandaloneGitState(sourceWork, recoveredRepo) {
|
|
|
4971
5001
|
}
|
|
4972
5002
|
}
|
|
4973
5003
|
|
|
5004
|
+
/** Give a recovery clone the source's effective exclude rules, so the status comparison sees the same
|
|
5005
|
+
* ignored paths. A fresh clone has neither the common dir's info/exclude nor a repository-configured
|
|
5006
|
+
* core.excludesFile, so a path excluded only there is `!!` in the source and `??` in the clone. Both are
|
|
5007
|
+
* written into the clone's own info/exclude (self-contained): core.excludesFile first, then
|
|
5008
|
+
* info/exclude, which keeps Git's precedence (a later pattern wins, and info/exclude outranks
|
|
5009
|
+
* core.excludesFile). → the sources carried, [{ kind, path }]. */
|
|
5010
|
+
function carryExcludes(sourceWork, recoveredRepo) {
|
|
5011
|
+
const git = (...args) => execFileSync("git", ["-C", sourceWork, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER }).trim();
|
|
5012
|
+
const carried = [], parts = [];
|
|
5013
|
+
// A file with no pattern line (Git's template info/exclude is comments only) changes nothing: not carried.
|
|
5014
|
+
const carry = (kind, file) => {
|
|
5015
|
+
if (!existsSync(file)) return;
|
|
5016
|
+
const text = readFileSync(file, "utf8");
|
|
5017
|
+
if (!text.split("\n").some((line) => line.trim() && !line.startsWith("#"))) return;
|
|
5018
|
+
parts.push(text); carried.push({ kind, path: file });
|
|
5019
|
+
};
|
|
5020
|
+
let excludesFile = "";
|
|
5021
|
+
try { excludesFile = git("config", "--path", "--get", "core.excludesFile"); } catch { /* unset: Git's default applies to both repositories alike */ }
|
|
5022
|
+
if (excludesFile) {
|
|
5023
|
+
carry("core.excludesFile", resolve(git("rev-parse", "--show-toplevel"), excludesFile));
|
|
5024
|
+
}
|
|
5025
|
+
carry("info/exclude", join(git("rev-parse", "--path-format=absolute", "--git-common-dir"), "info", "exclude"));
|
|
5026
|
+
if (!carried.length) return carried;
|
|
5027
|
+
mkdirSync(join(recoveredRepo, ".git", "info"), { recursive: true });
|
|
5028
|
+
writeFileSync(join(recoveredRepo, ".git", "info", "exclude"), parts.map((t) => (t.endsWith("\n") ? t : `${t}\n`)).join(""));
|
|
5029
|
+
return carried;
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
/** Repository settings that change what `git status` reports for the same bytes and index. */
|
|
5033
|
+
const STATUS_CONFIG = ["core.fileMode", "core.ignoreCase", "core.precomposeUnicode", "core.symlinks", "core.autocrlf", "core.eol"];
|
|
5034
|
+
/** Give a recovery clone the source's status-affecting settings, so the status comparison judges the same
|
|
5035
|
+
* bytes the same way. A fresh clone probes its own core.fileMode/ignoreCase/… and lacks the common dir's
|
|
5036
|
+
* info/attributes, so e.g. core.fileMode=false with a mode-only change is clean in the source and ` M` in
|
|
5037
|
+
* the clone. Each key whose effective value differs is set (or, unset in the source, unset) in the
|
|
5038
|
+
* clone's local config; info/attributes is copied to the clone's (the same precedence). → what was
|
|
5039
|
+
* carried: [{ kind: "config", key, value } | { kind: "info/attributes", path }]. */
|
|
5040
|
+
function carryStatusConfig(sourceWork, recoveredRepo) {
|
|
5041
|
+
const get = (repo, key) => {
|
|
5042
|
+
try { return execFileSync("git", ["-C", repo, "config", "--get", key], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER }).replace(/\n$/, ""); }
|
|
5043
|
+
catch (e) { if (e.status === 1) return null; throw e; }
|
|
5044
|
+
};
|
|
5045
|
+
const carried = [];
|
|
5046
|
+
for (const key of STATUS_CONFIG) {
|
|
5047
|
+
const value = get(sourceWork, key);
|
|
5048
|
+
if (value === get(recoveredRepo, key)) continue;
|
|
5049
|
+
execFileSync("git", ["-C", recoveredRepo, "config", "--local", ...(value === null ? ["--unset-all", key] : [key, value])], { stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER });
|
|
5050
|
+
carried.push({ kind: "config", key, value });
|
|
5051
|
+
}
|
|
5052
|
+
const common = execFileSync("git", ["-C", sourceWork, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER }).trim();
|
|
5053
|
+
const attributes = join(common, "info", "attributes");
|
|
5054
|
+
if (existsSync(attributes)) {
|
|
5055
|
+
mkdirSync(join(recoveredRepo, ".git", "info"), { recursive: true });
|
|
5056
|
+
copyFileSync(attributes, join(recoveredRepo, ".git", "info", "attributes"));
|
|
5057
|
+
carried.push({ kind: "info/attributes", path: attributes });
|
|
5058
|
+
}
|
|
5059
|
+
return carried;
|
|
5060
|
+
}
|
|
5061
|
+
|
|
4974
5062
|
function detachRecoveryClone(source, recovered) {
|
|
4975
5063
|
let stash;
|
|
4976
5064
|
try { stash = execFileSync("git", ["-C", source, "rev-parse", "--verify", "--quiet", "refs/stash"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER }).trim(); }
|
|
@@ -4989,6 +5077,7 @@ function detachRecoveryClone(source, recovered) {
|
|
|
4989
5077
|
}
|
|
4990
5078
|
|
|
4991
5079
|
function materializeNestedRepositories(sourceWork, recoveredRepo) {
|
|
5080
|
+
const excludes = [], statusConfig = [];
|
|
4992
5081
|
for (const source of nestedGitRoots(sourceWork)) {
|
|
4993
5082
|
const rel = relative(sourceWork, source);
|
|
4994
5083
|
const dest = join(recoveredRepo, rel);
|
|
@@ -4998,6 +5087,8 @@ function materializeNestedRepositories(sourceWork, recoveredRepo) {
|
|
|
4998
5087
|
execFileSync("git", ["-C", dest, "checkout", "--quiet", head], { maxBuffer: GIT_MAX_BUFFER });
|
|
4999
5088
|
detachRecoveryClone(source, dest);
|
|
5000
5089
|
restoreStandaloneGitState(source, dest);
|
|
5090
|
+
for (const c of carryExcludes(source, dest)) excludes.push({ repo: rel, ...c });
|
|
5091
|
+
for (const c of carryStatusConfig(source, dest)) statusConfig.push({ repo: rel, ...c });
|
|
5001
5092
|
for (const e of readdirSync(source, { withFileTypes: true })) {
|
|
5002
5093
|
if (e.name === ".git") continue;
|
|
5003
5094
|
const target = join(dest, e.name);
|
|
@@ -5005,8 +5096,9 @@ function materializeNestedRepositories(sourceWork, recoveredRepo) {
|
|
|
5005
5096
|
copyTreeSafe(join(source, e.name), target);
|
|
5006
5097
|
}
|
|
5007
5098
|
if (existsSync(join(dest, ".git", "objects", "info", "alternates"))) throw new Error(`nested recovery ${rel} depends on object alternates`);
|
|
5008
|
-
|
|
5099
|
+
assertStatusAgrees(source, dest, `nested recovery ${rel} Git state disagreed with source`, rel);
|
|
5009
5100
|
}
|
|
5101
|
+
return { excludes, statusConfig };
|
|
5010
5102
|
}
|
|
5011
5103
|
|
|
5012
5104
|
/** Bytes a path holds, never following a symlink (a link counts as its own entry). */
|
|
@@ -5061,7 +5153,7 @@ function preserveRetirementWork(observation, meta, instance) {
|
|
|
5061
5153
|
if (fingerprintTree(observation.home, { excludeRoot: new Set(["work"]), instanceHome: true }) !== fingerprintTree(recoveredHome, { instanceHome: true })) {
|
|
5062
5154
|
throw new Error("home recovery verification disagreed with the source");
|
|
5063
5155
|
}
|
|
5064
|
-
let branchDrift;
|
|
5156
|
+
let branchDrift, excludes, statusConfig;
|
|
5065
5157
|
if (!homeOnly && meta.work === "worktree" && meta.repo && meta.branch && (existsSync(observation.work) || observation.branchExists)) {
|
|
5066
5158
|
const recoveredRepo = join(staging, "repo");
|
|
5067
5159
|
// The branch is derived from the worktree while it exists: an instance
|
|
@@ -5080,18 +5172,21 @@ function preserveRetirementWork(observation, meta, instance) {
|
|
|
5080
5172
|
detachRecoveryClone(sourceGitContext, recoveredRepo);
|
|
5081
5173
|
if (existsSync(observation.work)) {
|
|
5082
5174
|
restoreStandaloneGitState(observation.work, recoveredRepo);
|
|
5175
|
+
excludes = carryExcludes(observation.work, recoveredRepo).map((c) => ({ repo: ".", ...c }));
|
|
5176
|
+
statusConfig = carryStatusConfig(observation.work, recoveredRepo).map((c) => ({ repo: ".", ...c }));
|
|
5083
5177
|
for (const e of readdirSync(observation.work, { withFileTypes: true })) {
|
|
5084
5178
|
if (e.name === ".git") continue;
|
|
5085
5179
|
const dest = join(recoveredRepo, e.name);
|
|
5086
5180
|
rmSync(dest, { recursive: true, force: true });
|
|
5087
5181
|
copyTreeSafe(join(observation.work, e.name), dest);
|
|
5088
5182
|
}
|
|
5089
|
-
materializeNestedRepositories(observation.work, recoveredRepo);
|
|
5183
|
+
const nested = materializeNestedRepositories(observation.work, recoveredRepo);
|
|
5184
|
+
excludes.push(...nested.excludes); statusConfig.push(...nested.statusConfig);
|
|
5090
5185
|
}
|
|
5091
5186
|
if (existsSync(join(recoveredRepo, ".git", "objects", "info", "alternates"))) throw new Error("recovery clone depends on object alternates");
|
|
5092
5187
|
if (existsSync(observation.work)) {
|
|
5093
5188
|
if (fingerprintTree(observation.work, { excludeRoot: new Set([".git"]), excludeGitMetadata: true }) !== fingerprintTree(recoveredRepo, { excludeRoot: new Set([".git"]), excludeGitMetadata: true })) throw new Error("worktree recovery verification disagreed with the source");
|
|
5094
|
-
|
|
5189
|
+
assertStatusAgrees(observation.work, recoveredRepo, "recovered Git index/status disagreed with the source", ".");
|
|
5095
5190
|
}
|
|
5096
5191
|
const recoveredHead = execFileSync("git", ["-C", recoveredRepo, "rev-parse", "HEAD"], { encoding: "utf8" , maxBuffer: GIT_MAX_BUFFER }).trim();
|
|
5097
5192
|
const sourceHead = ref.branch === null ? ref.oid
|
|
@@ -5109,14 +5204,15 @@ function preserveRetirementWork(observation, meta, instance) {
|
|
|
5109
5204
|
// What the copy cost: the untracked/ignored (or directory) outputs it carries, named.
|
|
5110
5205
|
const workCopied = observation.directory ? !!observation.directoryFingerprint : !homeOnly && meta.work === "worktree" && existsSync(observation.work);
|
|
5111
5206
|
const outputs = workCopied ? preservedOutputs(observation.work, observation.directory) : undefined;
|
|
5112
|
-
writeFileSync(join(staging, "recovery.json"), JSON.stringify({ version: 1, instance, classes: observation.classes, sourceHome: observation.home, createdAt: new Date().toISOString(), ...(repoCopy ? { repoCopy } : {}), ...(branchDrift ? { branchDrift } : {}), ...(outputs ? { outputs } : {}) }, null, 2) + "\n", { mode: 0o600 });
|
|
5207
|
+
writeFileSync(join(staging, "recovery.json"), JSON.stringify({ version: 1, instance, classes: observation.classes, sourceHome: observation.home, createdAt: new Date().toISOString(), ...(repoCopy ? { repoCopy } : {}), ...(branchDrift ? { branchDrift } : {}), ...(excludes?.length ? { excludes } : {}), ...(statusConfig?.length ? { statusConfig } : {}), ...(outputs ? { outputs } : {}) }, null, 2) + "\n", { mode: 0o600 });
|
|
5113
5208
|
const bytes = treeBytes(staging);
|
|
5114
5209
|
mkdirSync(dirname(recovery), { recursive: true });
|
|
5115
5210
|
renameSync(staging, recovery);
|
|
5116
5211
|
return { path: recovery, classes: observation.classes, bytes, ...(outputs ? { outputs } : {}), ...(repoCopy ? { repoCopy } : {}) };
|
|
5117
5212
|
} catch (e) {
|
|
5118
5213
|
rmSync(staging, { recursive: true, force: true });
|
|
5119
|
-
|
|
5214
|
+
const details = e.statusDisagreement ? { home: observation.home, statusDisagreement: e.statusDisagreement } : undefined;
|
|
5215
|
+
throw Object.assign(oatsError("E_WORK_PRESERVATION_FAILED", `retirement work remains at ${observation.home}; recovery could not be verified: ${e.message}`, details), details ? { details } : {});
|
|
5120
5216
|
}
|
|
5121
5217
|
}
|
|
5122
5218
|
|
package/package-catalog.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.2",
|
|
4
4
|
"description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|