@awebai/oats 0.22.4 → 0.22.6
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 +74 -12
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +51 -2
- package/capabilities/oats-aweb/oats.json +1 -1
- package/capabilities/oats-okf/agents/memory-harvest/soul.yaml +0 -1
- package/capabilities/oats-okf/bin/oats-okf.mjs +57 -16
- package/capabilities/oats-okf/oats.json +12 -2
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +3 -0
- package/docs/integrations.md +47 -0
- package/docs/operating-team-migration.md +181 -35
- package/docs/packages.md +5 -1
- package/docs/release-notes/v0.22.5.md +36 -0
- package/docs/release-notes/v0.22.6.md +53 -0
- package/lib/core.mjs +2 -0
- package/package-catalog.json +2 -2
- package/package.json +1 -1
- package/packages/record/bin/capture.mjs +44 -14
- package/packages/record/lib/capture-lock.mjs +64 -0
package/bin/oats.mjs
CHANGED
|
@@ -674,7 +674,8 @@ function use() {
|
|
|
674
674
|
let entry;
|
|
675
675
|
if (manifest.layer) {
|
|
676
676
|
const existing = caps.layers[manifest.layer];
|
|
677
|
-
|
|
677
|
+
var entryExisted = !!(existing && existing !== "none" && existing.capability === manifest.capability);
|
|
678
|
+
entry = entryExisted ? existing : { capability: manifest.capability };
|
|
678
679
|
if (existing && existing !== "none" && existing.capability !== manifest.capability && enabled) {
|
|
679
680
|
die(`fundamental layer ${manifest.layer} already binds ${existing.capability} at this level — disable it first`);
|
|
680
681
|
}
|
|
@@ -713,9 +714,12 @@ function use() {
|
|
|
713
714
|
}
|
|
714
715
|
if (targetKind === "global") entry.global = enabled;
|
|
715
716
|
else {
|
|
716
|
-
//
|
|
717
|
-
// before narrowing, so adding a soul/type binding
|
|
718
|
-
|
|
717
|
+
// An EXISTING layer entry with no explicit targets is implicitly global:
|
|
718
|
+
// materialize that before narrowing, so adding a soul/type binding does not
|
|
719
|
+
// silently drop everyone else. An entry this command just created (the
|
|
720
|
+
// layer was `none` or another capability) has no implicit global to keep:
|
|
721
|
+
// a targeted first binding is written as global: false, explicitly.
|
|
722
|
+
if (manifest.layer && entry.global === undefined && !entry["agent-types"] && !entry.souls) entry.global = entryExisted;
|
|
719
723
|
entry[targetKind] = entry[targetKind] && typeof entry[targetKind] === "object" ? entry[targetKind] : {};
|
|
720
724
|
// Same write-side refusal, and for the same two reasons: `--soul
|
|
721
725
|
// __proto__` was swallowed by the inherited setter and reported as
|
|
@@ -2206,8 +2210,20 @@ function migrateCmd() {
|
|
|
2206
2210
|
/** oats update <package> — transactional package update with diff + trust reset. */
|
|
2207
2211
|
function updatePackageCmd(id) {
|
|
2208
2212
|
const dir = dirFlag();
|
|
2213
|
+
// --to <selector>: move a catalog-sourced lock to another catalog ref
|
|
2214
|
+
// (tag) through the same transactional update. A lock with an explicit
|
|
2215
|
+
// selector keeps it on a plain update by design; this is the operator's
|
|
2216
|
+
// way to advance it without remove + reinstall.
|
|
2217
|
+
// Two spellings: `oats update <id> <id>@<selector>` (the engine's own spec
|
|
2218
|
+
// form) or `oats update <id> --to <selector>`.
|
|
2219
|
+
const to = flag("to");
|
|
2220
|
+
if (to === true) { cmdFail("E_BAD_ARGS", "--to needs a catalog selector, e.g. --to v1.10.1"); return; }
|
|
2221
|
+
if (to !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(to)) { cmdFail("E_BAD_ARGS", `--to selector ${JSON.stringify(to)} is not a catalog ref`); return; }
|
|
2222
|
+
const positional = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
|
|
2223
|
+
if (positional && to !== undefined) { cmdFail("E_BAD_ARGS", "give either <id>@<selector> or --to <selector>, not both"); return; }
|
|
2224
|
+
const spec = positional || (to !== undefined ? `${id}@${to}` : undefined);
|
|
2209
2225
|
let r;
|
|
2210
|
-
try { r = updatePackage(dir, id); } catch (e) { cmdFail(e.code || "invalid-lock", e.message || e); return; }
|
|
2226
|
+
try { r = updatePackage(dir, id, spec ? { spec } : {}); } catch (e) { cmdFail(e.code || "invalid-lock", e.message || e); return; }
|
|
2211
2227
|
if (JSON_MODE) { jsonOk(r); return; }
|
|
2212
2228
|
// A moved package root is reported even when the bytes are identical: the
|
|
2213
2229
|
// lock now points somewhere else in the repository, and that is exactly the
|
|
@@ -2945,6 +2961,17 @@ function capabilityCommand() {
|
|
|
2945
2961
|
if (!trust.trusted) bail("E_CAPABILITY_BLOCKED", `${m.capability} executable command is blocked: ${trust.reason}`);
|
|
2946
2962
|
const sub = args[1];
|
|
2947
2963
|
const cmds = Object.keys(m.commands);
|
|
2964
|
+
// `oats <ns> --help` and `oats <ns> <cmd> --help` answer from the manifest
|
|
2965
|
+
// and never run the executable: the command's own help, if it has one,
|
|
2966
|
+
// is not worth a side effect (harvest --help once spawned a harvester).
|
|
2967
|
+
if (HELP_WORDS.has(sub) || args.slice(2).some((a) => a === "--help" || a === "-h")) {
|
|
2968
|
+
const known = sub && !HELP_WORDS.has(sub) && Object.prototype.hasOwnProperty.call(m.commands, sub);
|
|
2969
|
+
if (JSON_MODE) { jsonOk({ capability: m.capability, namespace: cmd, command: known ? sub : null, commands: cmds, description: m.description || null, help: "printed from the manifest; the executable was not run" }); return; }
|
|
2970
|
+
console.log(`oats ${cmd}${known ? ` ${sub}` : ""} — ${m.capability}${m.description ? `: ${m.description}` : ""}`);
|
|
2971
|
+
console.log(` commands: ${cmds.join(", ") || "(none)"}`);
|
|
2972
|
+
console.log(` (help printed from the manifest; the executable was not run)`);
|
|
2973
|
+
process.exit(0);
|
|
2974
|
+
}
|
|
2948
2975
|
// Distinguish an ABSENT key from a declared-but-invalid value: a manifest
|
|
2949
2976
|
// entry of "" / 0 / false / null is a broken capability, not an unknown
|
|
2950
2977
|
// command (it is listed in cmds).
|
|
@@ -3379,6 +3406,13 @@ function serverRouteCmd() {
|
|
|
3379
3406
|
// blame` pointing at the commit that last changed each command.
|
|
3380
3407
|
const TYPED_CLI_FAILURES = new Set(["unsafe-config-key", "unsafe-config-value"]);
|
|
3381
3408
|
try {
|
|
3409
|
+
// `--help`/`-h` anywhere after a kernel command prints that command's usage
|
|
3410
|
+
// and exits 0 BEFORE any dispatch: a fresh operator inspects --help before
|
|
3411
|
+
// using a command, and `install --help` once ran the bare restore while
|
|
3412
|
+
// `okf harvest --help` spawned a harvester (BeadHub, 2026-09-05).
|
|
3413
|
+
const KERNEL_COMMANDS = new Set(["capture", "config", "create", "doctor", "experimental", "init", "inject", "install", "list", "migrate", "pane", "recall", "remove", "retire", "root", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
|
|
3414
|
+
const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
|
|
3415
|
+
if (cmd && KERNEL_COMMANDS.has(cmd) && wantsHelp) { if (JSON_MODE) { jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); process.exit(0); } usageFor(cmd); process.exit(0); }
|
|
3382
3416
|
if (flag("server") !== undefined && ["spawn", "retire", "status", "session", "okf"].includes(cmd)) serverRouteCmd();
|
|
3383
3417
|
else if (cmd === "server") serverCmd();
|
|
3384
3418
|
else if (cmd === "doctor") {
|
|
@@ -3386,7 +3420,13 @@ else if (cmd === "doctor") {
|
|
|
3386
3420
|
args.includes("--json") ? doctorJson(doctorDir) : doctor(doctorDir);
|
|
3387
3421
|
}
|
|
3388
3422
|
else if (cmd === "use") use();
|
|
3389
|
-
else if (cmd === "update") {
|
|
3423
|
+
else if (cmd === "update") {
|
|
3424
|
+
const t = args[1] && !args[1].startsWith("--") ? args[1] : undefined;
|
|
3425
|
+
// A selector without a package must never fall through to the kernel
|
|
3426
|
+
// self-update (a different product) with the flag silently ignored.
|
|
3427
|
+
if (!t && flag("to") !== undefined) { cmdFail("E_BAD_ARGS", "oats update --to needs a package: oats update <package> --to <ref> (or <package> <package>@<ref>)"); process.exit(1); }
|
|
3428
|
+
t ? updatePackageCmd(t) : updateCmd();
|
|
3429
|
+
}
|
|
3390
3430
|
else if (cmd === "type") typeCmd();
|
|
3391
3431
|
else if (cmd === "inject") injectCmd();
|
|
3392
3432
|
else if (cmd === "install") install();
|
|
@@ -3417,7 +3457,29 @@ else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && capabilityComma
|
|
|
3417
3457
|
// text must NOT contaminate stdout — still one envelope object, nonzero exit.
|
|
3418
3458
|
else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && JSON_MODE) jsonFail("E_UNKNOWN_COMMAND", `unknown command "${cmd}" — no kernel subcommand or active capability namespace matches`);
|
|
3419
3459
|
else {
|
|
3420
|
-
console.log(
|
|
3460
|
+
console.log(usageText());
|
|
3461
|
+
process.exit(cmd && !HELP_WORDS.has(cmd) ? 1 : 0);
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
/** The usage lines for one kernel command (its `oats <cmd> ...` lines and
|
|
3465
|
+
* their indented continuations), or the whole usage when none match. */
|
|
3466
|
+
function usageLinesFor(name) {
|
|
3467
|
+
const out = [];
|
|
3468
|
+
let inside = false;
|
|
3469
|
+
for (const line of usageText().split("\n")) {
|
|
3470
|
+
if (new RegExp(`^ oats ${name}(\\s|$)`).test(line)) { out.push(line); inside = true; continue; }
|
|
3471
|
+
if (inside && /^ {6}/.test(line) && !/^ oats /.test(line)) { out.push(line); continue; }
|
|
3472
|
+
inside = false;
|
|
3473
|
+
}
|
|
3474
|
+
return out;
|
|
3475
|
+
}
|
|
3476
|
+
function usageFor(name) {
|
|
3477
|
+
const out = usageLinesFor(name);
|
|
3478
|
+
console.log(out.length ? `Usage:\n${out.join("\n")}` : usageText());
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
function usageText() {
|
|
3482
|
+
return `oats — Open Agent Team Specification
|
|
3421
3483
|
|
|
3422
3484
|
Usage:
|
|
3423
3485
|
oats version [--json] kernel version; --json emits the
|
|
@@ -3488,9 +3550,10 @@ Usage:
|
|
|
3488
3550
|
report under error.details)
|
|
3489
3551
|
oats list [--dir <d>] [--json] installed packages, exported capabilities,
|
|
3490
3552
|
scopes, trust state
|
|
3491
|
-
oats update <package> [
|
|
3492
|
-
|
|
3493
|
-
all capability approvals invalidated
|
|
3553
|
+
oats update <package> [<package>@<ref>] transactional package update: temp fetch,
|
|
3554
|
+
[--to <ref>] [--dir <d>] closure validation, diff, lock replace,
|
|
3555
|
+
all capability approvals invalidated; a
|
|
3556
|
+
spec or --to moves a catalog lock to <ref>
|
|
3494
3557
|
oats remove <package> [--dir <d>] remove a package (refuses while config or
|
|
3495
3558
|
dependent packages reference it)
|
|
3496
3559
|
oats migrate [--dry-run] [--dir <d>] map this scope's v1 capability locks to
|
|
@@ -3569,8 +3632,7 @@ The turn record (core — every conversation captured, searchable, replicated):
|
|
|
3569
3632
|
oats <namespace> <command> [args…] run an operational command only when its
|
|
3570
3633
|
capability is active (e.g. oats okf harvest)
|
|
3571
3634
|
|
|
3572
|
-
Layers: ${LAYERS.join(", ")}. Level detection: ~ → laptop, .git → repo, else workspace
|
|
3573
|
-
process.exit(cmd && !HELP_WORDS.has(cmd) ? 1 : 0);
|
|
3635
|
+
Layers: ${LAYERS.join(", ")}. Level detection: ~ → laptop, .git → repo, else workspace.`;
|
|
3574
3636
|
}
|
|
3575
3637
|
} catch (e) {
|
|
3576
3638
|
if (!TYPED_CLI_FAILURES.has(e?.code)) throw e;
|
|
@@ -85,6 +85,17 @@ const parseSecretJson = (text, what) => {
|
|
|
85
85
|
* `command -v`, which is a SHELL BUILTIN — spawning it as a program depends on
|
|
86
86
|
* a /usr/bin/command binary that many systems do not ship, and its absence
|
|
87
87
|
* would read as "aw is missing" on every such host. */
|
|
88
|
+
/** The installed aw's version from `aw version` ("aw 1.36.1 ..."), or
|
|
89
|
+
* undefined when it cannot be read; compared as numeric triples. */
|
|
90
|
+
function awAtLeast(floor) {
|
|
91
|
+
let v;
|
|
92
|
+
try { v = /aw\s+v?(\d+)\.(\d+)\.(\d+)/.exec(run(["aw", "version"], undefined, 10000)); } catch { return false; }
|
|
93
|
+
if (!v) return false;
|
|
94
|
+
const a = v.slice(1, 4).map(Number), b = floor.split(".").map(Number);
|
|
95
|
+
for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] > b[i]; }
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
function onPath(cmd) {
|
|
89
100
|
for (const dir of String(process.env.PATH || "").split(delimiter)) {
|
|
90
101
|
if (!dir) continue;
|
|
@@ -195,6 +206,17 @@ function wakeDeregister(instanceHome) {
|
|
|
195
206
|
try { run(["aw", "wake", "deregister", "--home", instanceHome], instanceHome, 60000); return true; } catch { return false; }
|
|
196
207
|
}
|
|
197
208
|
const seatLockPath = (source) => join(dirname(source), ".aw-retained-seat.json");
|
|
209
|
+
/** The alias a home's .aw/workspace.yaml records under memberships (indented),
|
|
210
|
+
* or undefined. Read only when the hook has no alias of its own. */
|
|
211
|
+
const workspaceAliasOf = (homeDir) => {
|
|
212
|
+
try { const m = readFileSync(join(homeDir, ".aw", "workspace.yaml"), "utf8").match(/^\s*alias:\s*["']?([a-z0-9][a-z0-9._-]{0,127})["']?\s*$/mi); return m ? m[1] : undefined; }
|
|
213
|
+
catch { return undefined; }
|
|
214
|
+
};
|
|
215
|
+
/** A join that the CLI reported as failed (or that this hook killed on
|
|
216
|
+
* timeout) may still have completed server-side: the home then holds a
|
|
217
|
+
* signing key, a team certificate and a workspace binding. */
|
|
218
|
+
const joinedLate = (homeDir) => existsSync(join(homeDir, ".aw", "signing.key")) && existsSync(join(homeDir, ".aw", "team-certs")) && !!workspaceAliasOf(homeDir);
|
|
219
|
+
const JOIN_TIMEOUT_MS = Number(process.env.OATS_AWEB_JOIN_TIMEOUT_MS) > 0 ? Number(process.env.OATS_AWEB_JOIN_TIMEOUT_MS) : 120000;
|
|
198
220
|
const yamlScalar = (text, key) => {
|
|
199
221
|
const m = String(text).match(new RegExp(`^${key}:\\s*["']?([^"'\\n#]+)["']?\\s*$`, "m"));
|
|
200
222
|
return m ? m[1].trim() : undefined;
|
|
@@ -349,8 +371,18 @@ if (event === "spawn") {
|
|
|
349
371
|
if (!inv?.token || typeof inv.token !== "string") fatal("aw team invite returned no usable token, so no identity could be minted");
|
|
350
372
|
let raw;
|
|
351
373
|
try {
|
|
352
|
-
|
|
374
|
+
// 120 s: a join on a slow or flapping link is slow, not broken; a killed
|
|
375
|
+
// join that completed server-side is caught below.
|
|
376
|
+
raw = parseSecretJson(run(["aw", "team", "join", inv.token, "--name", instance, "--json"], home, JOIN_TIMEOUT_MS, { secrets: [inv.token], secretSafe: true }), "aw team join");
|
|
353
377
|
} catch (e) {
|
|
378
|
+
// The join may have completed after the CLI was killed or reported a
|
|
379
|
+
// failure: if the home now holds a bound identity, that identity EXISTS
|
|
380
|
+
// and must be reported so compensation retires it instead of orphaning it.
|
|
381
|
+
if (joinedLate(home)) {
|
|
382
|
+
const late = workspaceAliasOf(home);
|
|
383
|
+
minted = { team, alias: late };
|
|
384
|
+
fatal(`aw team join was reported failed (${e.message || e}) but the home now holds a bound identity "${late}" on ${team}; reported for compensation so it is retired, not orphaned`, minted);
|
|
385
|
+
}
|
|
354
386
|
// A retired alias keeps its certificate until aweb-abim ships, so a
|
|
355
387
|
// re-spawn under the same name is refused by AWID. Say that, and the
|
|
356
388
|
// remedy, instead of relaying a bare join error.
|
|
@@ -413,7 +445,7 @@ if (event === "spawn") {
|
|
|
413
445
|
fatal(`identity minting failed: ${e.message || e}`, minted);
|
|
414
446
|
}
|
|
415
447
|
} else if (event === "retire") {
|
|
416
|
-
|
|
448
|
+
let meta = JSON.parse(process.env.OATS_META || "{}");
|
|
417
449
|
// A retained seat: release the lock and leave the identity alone. Never
|
|
418
450
|
// aw workspace delete (it would soft-delete the standing identity's row)
|
|
419
451
|
// and never team retire; the source .aw stays until a human removes it.
|
|
@@ -426,6 +458,10 @@ if (event === "spawn") {
|
|
|
426
458
|
// undo, which is completion. An alias WITH no local `.aw` is the opposite —
|
|
427
459
|
// the remote record exists and its key is gone, so the self-delete cannot be
|
|
428
460
|
// authenticated and the cleanup is incomplete, not vacuous (reviewer-602627c).
|
|
461
|
+
// A home whose spawn hook could not report its alias (a join killed on
|
|
462
|
+
// timeout that completed anyway) still carries the alias in its workspace
|
|
463
|
+
// binding: use it rather than leaving the workspace orphaned.
|
|
464
|
+
if (!meta.alias) { const late = workspaceAliasOf(home); if (late) meta = { ...meta, alias: late, aliasFromHome: true }; }
|
|
429
465
|
if (!meta.alias) out({ meta: { retired: false, reason: "nothing-to-delete" } });
|
|
430
466
|
if (!existsSync(join(home, ".aw"))) {
|
|
431
467
|
out({ meta: { retired: false, reason: "no-local-identity-key" }, warning: `oats-aweb: alias "${meta.alias}" was minted but ${join(home, ".aw")} is gone, so the remote record cannot be self-deleted and will linger until stale` }, 1);
|
|
@@ -433,6 +469,19 @@ if (event === "spawn") {
|
|
|
433
469
|
try {
|
|
434
470
|
// Self-delete from inside the home, authenticated by its own key — a remote
|
|
435
471
|
// delete would 409 until the server marks the workspace stale.
|
|
472
|
+
// aw 1.36.1 (aweb-abim) revokes the member's certificate on delete and
|
|
473
|
+
// says so: `--json` prints alias_released true|false with a reason, and
|
|
474
|
+
// a released alias may be reused by a later spawn. An older aw cannot
|
|
475
|
+
// revoke, so the alias stays unusable and the report says that instead.
|
|
476
|
+
if (awAtLeast("1.36.1")) {
|
|
477
|
+
const raw = run(["aw", "workspace", "delete", meta.alias, "--json"], home);
|
|
478
|
+
let doc; try { doc = JSON.parse(raw); } catch { doc = undefined; }
|
|
479
|
+
const released = doc?.alias_released === true;
|
|
480
|
+
// aw 1.36.1 prints the cause as alias_released_reason (workspace.go,
|
|
481
|
+
// workspace_self_retire.go); `reason` is tolerated for a later rename.
|
|
482
|
+
const reason = typeof doc?.alias_released_reason === "string" ? doc.alias_released_reason : typeof doc?.reason === "string" ? doc.reason : (doc ? "unstated" : "no JSON answer");
|
|
483
|
+
out({ meta: { retired: true, aliasReusable: released, aliasReason: reason }, ...(released ? {} : { warning: `oats-aweb: workspace "${meta.alias}" deleted but its alias was not released (${reason}); spawn successors with a fresh --purpose until it is` }) });
|
|
484
|
+
}
|
|
436
485
|
run(["aw", "workspace", "delete", meta.alias], home);
|
|
437
486
|
// Honest: the workspace row is deleted, but a hosted local member cannot
|
|
438
487
|
// revoke its own AWID certificate (aweb-abim), so the alias is NOT
|
|
@@ -52,10 +52,6 @@ catch (e) {
|
|
|
52
52
|
if (JSON_MODE) jsonFail("E_HARVEST_FAILED", `malformed OATS_SETTINGS: ${e.message || e}`);
|
|
53
53
|
process.stderr.write(`oats-okf: malformed OATS_SETTINGS (ignoring): ${e.message || e}\n`);
|
|
54
54
|
}
|
|
55
|
-
/** Model for the memory-harvest agent — promotion judgment is cheap-but-good
|
|
56
|
-
* work; default gpt-5.5, overridable via okf settings { "harvest-model": ... }. */
|
|
57
|
-
const DEFAULT_HARVEST_MODEL = "github-copilot/gpt-5.5";
|
|
58
|
-
|
|
59
55
|
function runtimeError(code, message) {
|
|
60
56
|
return Object.assign(new Error(message), { code });
|
|
61
57
|
}
|
|
@@ -148,8 +144,19 @@ function planRecordHarvest(instanceHome) {
|
|
|
148
144
|
// The exact next watermark is written beside the current one by the
|
|
149
145
|
// package; the harvester's delivery is a rename, nothing retyped.
|
|
150
146
|
const nextPath = join(instanceHome, RECORD_WATERMARK.replace(/\.json$/, ".next.json"));
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
const windows = threads.map(({ thread, afterTurnId, untilTurnId }) => ({ thread, afterTurnId, untilTurnId }));
|
|
148
|
+
let pending;
|
|
149
|
+
try { pending = JSON.parse(readFileSync(nextPath, "utf8")).pendingHarvest; } catch { /* no prior plan */ }
|
|
150
|
+
const sameWindows = pending?.instance && JSON.stringify(pending.windows) === JSON.stringify(windows);
|
|
151
|
+
const warnings = sameWindows ? [
|
|
152
|
+
`previous harvester ${pending.instance} did not advance the watermark for ${windows.map((w) => `${w.thread} (${w.afterTurnId || "start"} -> ${w.untilTurnId})`).join(", ")}; inspect its outcome and use oats okf harvest --from-record --force to retry`,
|
|
153
|
+
] : [];
|
|
154
|
+
// Reuse the existing prepared watermark; no extra journal. Stamp it only
|
|
155
|
+
// after a successful spawn, and never overwrite a stalled plan on a skip.
|
|
156
|
+
if (sameWindows && !process.argv.includes("--force")) return { stalled: true, warnings };
|
|
157
|
+
const prepared = JSON.stringify(next, null, 2) + "\n";
|
|
158
|
+
writeFileSync(nextPath, prepared);
|
|
159
|
+
return { threads, windows, watermarkPath, nextPath, watermark: next, prepared, unattributed: (report.unattributed || []).length, problems, warnings };
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
/** Briefing block for record-fed candidates, appended to the harvest task. */
|
|
@@ -198,11 +205,27 @@ async function spawnHarvester(spawnArgs, task) {
|
|
|
198
205
|
}
|
|
199
206
|
}
|
|
200
207
|
|
|
201
|
-
function
|
|
202
|
-
const
|
|
208
|
+
function harvestRuntime() {
|
|
209
|
+
const runtime = settings["harvest-runtime"] ?? "pi";
|
|
210
|
+
if (!["pi", "claude", "codex"].includes(runtime)) {
|
|
211
|
+
throw runtimeError("E_HARVEST_SETTINGS", "harvest-runtime must be pi, claude or codex");
|
|
212
|
+
}
|
|
213
|
+
const configured = settings["harvest-model"];
|
|
214
|
+
if (configured != null && (typeof configured !== "string" || !configured.trim())) {
|
|
215
|
+
throw runtimeError("E_HARVEST_SETTINGS", "harvest-model must be a nonempty model name");
|
|
216
|
+
}
|
|
217
|
+
const model = configured?.trim() || undefined;
|
|
218
|
+
if (runtime !== "pi" && model?.includes("/")) {
|
|
219
|
+
throw runtimeError("E_HARVEST_SETTINGS", `harvest-model for ${runtime} must be a native model name, without a Pi provider/ prefix`);
|
|
220
|
+
}
|
|
221
|
+
return { runtime, model };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function harvestSpawnArgs({ slug, parent, repo, work, workDir, branch, runtime, model }) {
|
|
225
|
+
const args = ["--purpose", slug, "--parent", parent, "--repo", repo, "--work", work, "--runtime", runtime];
|
|
203
226
|
if (workDir) args.push("--work-dir", workDir);
|
|
204
227
|
if (branch) args.push("--branch", branch);
|
|
205
|
-
args.push("--model", model);
|
|
228
|
+
if (model) args.push("--model", model);
|
|
206
229
|
return args;
|
|
207
230
|
}
|
|
208
231
|
|
|
@@ -331,7 +354,7 @@ _(the single next action — keep this current; a fresh session on any model res
|
|
|
331
354
|
}
|
|
332
355
|
}
|
|
333
356
|
const notesDir = join(home, "notes");
|
|
334
|
-
const skip = (why) => (JSON_MODE ? jsonOk({ harvest: "skipped", reason: why }) : out({ meta: { harvestSpawn: "skipped", why } }));
|
|
357
|
+
const skip = (why, warnings = []) => (JSON_MODE ? jsonOk({ harvest: "skipped", reason: why, ...(warnings.length ? { warnings } : {}) }) : out({ meta: { harvestSpawn: "skipped", why }, ...(warnings.length ? { warnings } : {}) }));
|
|
335
358
|
if (String(agName).startsWith("memory-harvest")) skip("self (loop guard)");
|
|
336
359
|
const notes = existsSync(notesDir) ? readdirSync(notesDir).filter((f) => f.endsWith(".md")) : [];
|
|
337
360
|
|
|
@@ -358,6 +381,7 @@ _(the single next action — keep this current; a fresh session on any model res
|
|
|
358
381
|
// write few notes). --from-record asks for the record even with notes.
|
|
359
382
|
// Planned only now, after every skip above: a capture pass is a real
|
|
360
383
|
// write and index, and "calling it too often is safe" must stay true.
|
|
384
|
+
const execution = harvestRuntime();
|
|
361
385
|
let recordPlan = null;
|
|
362
386
|
if (notes.length === 0 || process.argv.includes("--from-record")) {
|
|
363
387
|
let planned = null;
|
|
@@ -366,13 +390,14 @@ _(the single next action — keep this current; a fresh session on any model res
|
|
|
366
390
|
process.stderr.write(`oats-okf: record unavailable${notes.length ? ", harvesting notes only" : ""}: ${planned.unavailable}\n`);
|
|
367
391
|
if (notes.length === 0) skip("no pending notes");
|
|
368
392
|
} else recordPlan = planned;
|
|
393
|
+
for (const warning of recordPlan?.warnings || []) process.stderr.write(`oats-okf: record: ${warning}\n`);
|
|
394
|
+
if (recordPlan?.stalled) skip("previous harvester did not advance the watermark", recordPlan.warnings);
|
|
369
395
|
for (const line of recordPlan?.problems || []) process.stderr.write(`oats-okf: record: ${line}\n`);
|
|
370
396
|
if (recordPlan?.unattributed) process.stderr.write(`oats-okf: record: ${recordPlan.unattributed} session file(s) carry no working directory and cannot be attributed to any home (oats capture --home <home> lists them)\n`);
|
|
371
397
|
if (notes.length === 0 && !recordPlan) skip("no pending notes");
|
|
372
398
|
}
|
|
373
399
|
// Effective command settings are injected by capability dispatch. No
|
|
374
400
|
// resolved-config read crosses the public package boundary.
|
|
375
|
-
const harvestModel = settings["harvest-model"] || DEFAULT_HARVEST_MODEL;
|
|
376
401
|
const workDir = realpathSync(join(home, "work"));
|
|
377
402
|
const realSoul = realpathSync(sDir);
|
|
378
403
|
const harvName = `memory-harvest-${slug}`;
|
|
@@ -386,7 +411,7 @@ _(the single next action — keep this current; a fresh session on any model res
|
|
|
386
411
|
// version. It must not touch the owner's work tree.
|
|
387
412
|
const task = `Harvest the pending notes of live LOCAL-SOUL instance "${inst}" (agent "${agName}") into its soul — by direct edits, no commit.\n\n- Source notes: ${notes.length ? `${notesDir} (${notes.join(", ")})` : "none pending"}\n- Soul knowledge bundle to update: ${join(realSoul, "knowledge")}\n- Soul skills dir (for procedure-shaped notes): ${join(realSoul, "skills")}\n- This soul is LOCAL (uncommitted, gitignored): edit those soul files IN PLACE. Do NOT run git commit — not for the soul, and not in ./work (the shared tree belongs to the working instance; leave it untouched).\n- Follow your memory-harvest skill for everything else: promote/merge/drop each note, knowledge vs skill routing, index + log discipline, validate the bundle, DELETE processed notes from the source notes/ dir.\n${recordBrief(recordPlan, packageRuntimeCli())}\n- Then run \`oats retire ${harvName} --self\`.`;
|
|
388
413
|
r = await spawnHarvester(harvestSpawnArgs({
|
|
389
|
-
slug, parent: inst, repo: context, work: "attached", workDir,
|
|
414
|
+
slug, parent: inst, repo: context, work: "attached", workDir, ...execution,
|
|
390
415
|
}), task);
|
|
391
416
|
} else if ((process.env.OATS_WORK || meta.work) === "workspace") {
|
|
392
417
|
// WORKSPACE-MODE instance: ./work is the whole workspace, not a git repo —
|
|
@@ -399,7 +424,7 @@ _(the single next action — keep this current; a fresh session on any model res
|
|
|
399
424
|
const task = `Harvest the pending notes of live WORKSPACE-MODE instance "${inst}" (agent "${agName}") into its soul — delivered as a PR.\n\n- Source notes: ${notes.length ? `${notesDir} (${notes.join(", ")})` : "none pending"}\n- Your ./work is a dedicated worktree of the soul's home repo (${soulRepo}), branch memory-harvest/${slug}.\n- Soul knowledge bundle to update: ./work/${join(relSoul, "knowledge")}\n- Soul skills dir (for procedure-shaped notes): ./work/${join(relSoul, "skills")}\n- Follow your memory-harvest skill: promote/merge/drop each note, knowledge vs skill routing, index + log discipline, validate the bundle, DELETE processed notes from the source notes/ dir, and commit once (prefixed "memory-harvest:") if anything changed.${recordBrief(recordPlan, packageRuntimeCli())}\n- If you changed anything: push the branch and open a PR (\`git push -u origin memory-harvest/${slug}\` then \`gh pr create --fill\`). Do NOT merge it; the humans/owners of ${soulRepo} review soul changes. If gh is unavailable, push the branch and report the compare URL. A harvest that promoted nothing has nothing to commit, push or open; that is a completed harvest, not a failed one.\n- Finally run \`oats retire ${harvName} --self\` (keep the branch: --self only).`;
|
|
400
425
|
r = await spawnHarvester(harvestSpawnArgs({
|
|
401
426
|
slug, parent: inst, repo: soulRepo, work: "worktree",
|
|
402
|
-
branch: `memory-harvest/${slug}`,
|
|
427
|
+
branch: `memory-harvest/${slug}`, ...execution,
|
|
403
428
|
}), task);
|
|
404
429
|
} else {
|
|
405
430
|
// Repo-resident souls: write to the soul AS SEEN FROM THE WORK TREE, so the
|
|
@@ -410,11 +435,27 @@ _(the single next action — keep this current; a fresh session on any model res
|
|
|
410
435
|
: realSoul;
|
|
411
436
|
const task = `Harvest the pending notes of live instance "${inst}" (agent "${agName}") into its soul.\n\n- Source notes: ${notes.length ? `${notesDir} (${notes.join(", ")})` : "none pending"}\n- Soul knowledge bundle to update: ${join(soulTarget, "knowledge")}\n- Soul skills dir (for procedure-shaped notes): ${join(soulTarget, "skills")}\n- You are ATTACHED to the instance's work tree (./work) — commit your promotions there as a single commit, prefixed "memory-harvest:".\n- Follow your memory-harvest skill: promote/merge/drop each note, knowledge vs skill routing, index + log discipline, validate the bundle, DELETE processed notes from the source notes/ dir (so they are not re-harvested).${recordBrief(recordPlan, packageRuntimeCli())}\n- Commit if you changed anything (a harvest that promoted nothing has nothing to commit), then run \`oats retire ${harvName} --self\`.`;
|
|
412
437
|
r = await spawnHarvester(harvestSpawnArgs({
|
|
413
|
-
slug, parent: inst, repo: context, work: "attached", workDir,
|
|
438
|
+
slug, parent: inst, repo: context, work: "attached", workDir, ...execution,
|
|
414
439
|
}), task);
|
|
415
440
|
}
|
|
416
|
-
if (
|
|
417
|
-
|
|
441
|
+
if (recordPlan) {
|
|
442
|
+
try {
|
|
443
|
+
// A fast harvester may already have renamed the prepared file. Never
|
|
444
|
+
// recreate that consumed plan or replace a different plan's contents.
|
|
445
|
+
if (readFileSync(recordPlan.nextPath, "utf8") === recordPlan.prepared) {
|
|
446
|
+
writeFileSync(recordPlan.nextPath, JSON.stringify({ ...recordPlan.watermark, pendingHarvest: { instance: r.instance, windows: recordPlan.windows } }, null, 2) + "\n");
|
|
447
|
+
}
|
|
448
|
+
} catch (e) {
|
|
449
|
+
if (e.code !== "ENOENT") {
|
|
450
|
+
const warning = `harvester ${r.instance} spawned but its retry marker could not be recorded: ${e.message}`;
|
|
451
|
+
recordPlan.warnings.push(warning);
|
|
452
|
+
process.stderr.write(`oats-okf: ${warning}\n`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const warnings = recordPlan?.warnings?.length ? { warnings: recordPlan.warnings } : {};
|
|
457
|
+
if (JSON_MODE) jsonOk({ harvest: "spawned", instance: r.instance, window: r.tmux?.window || null, ...warnings, ...(recordPlan ? { record: { threads: recordPlan.threads.map((t) => t.thread), ...(recordPlan.unattributed ? { unattributed: recordPlan.unattributed } : {}), ...(recordPlan.problems?.length ? { problems: recordPlan.problems } : {}) } } : {}) });
|
|
458
|
+
out({ meta: { harvestSpawn: r.instance, window: r.tmux?.window }, ...warnings });
|
|
418
459
|
} catch (e) {
|
|
419
460
|
if (JSON_MODE) jsonFail(e.code || "E_HARVEST_FAILED", `harvest spawn failed (notes are safe on disk): ${e.message || e}`);
|
|
420
461
|
warn(`harvest spawn failed (notes are safe on disk): ${e.message || e}`);
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"capability": "oats.okf",
|
|
3
3
|
"command": "okf",
|
|
4
|
-
"version": "1.5.
|
|
5
|
-
"compatibility": { "oats": ">=0.22.
|
|
4
|
+
"version": "1.5.1",
|
|
5
|
+
"compatibility": { "oats": ">=0.22.3" },
|
|
6
6
|
"layer": "knowledge",
|
|
7
7
|
"description": "Knowledge layer via OKF: soul bundles, instance memory (STATE.md/log.md/notes/), continuous post-commit harvest into the soul (commit, PR, or direct-edit for local souls), craft + memory skills, validator.",
|
|
8
8
|
"requires": [],
|
|
9
|
+
"settings": {
|
|
10
|
+
"harvest-runtime": {
|
|
11
|
+
"default": "pi",
|
|
12
|
+
"values": ["pi", "claude", "codex"],
|
|
13
|
+
"description": "Harness for the memory harvester, independent of the source instance's runtime."
|
|
14
|
+
},
|
|
15
|
+
"harvest-model": {
|
|
16
|
+
"description": "Optional model pin for the selected harvest runtime, for example to use a cheaper model: a Pi provider/model or a native Claude/Codex model. When omitted, each harness uses its configured default."
|
|
17
|
+
}
|
|
18
|
+
},
|
|
9
19
|
"agents": [
|
|
10
20
|
"agents/memory-harvest"
|
|
11
21
|
],
|
|
@@ -92,6 +92,9 @@ is how what they learned reaches the soul.
|
|
|
92
92
|
one-line title, the claim, and its provenance as the turn ids it came from.
|
|
93
93
|
A candidate is something the instance learned or decided, stated in the
|
|
94
94
|
turns, not something you infer it should have learned.
|
|
95
|
+
- Never promote a secret or credential, however it appears in the record.
|
|
96
|
+
- Never promote third-party message content verbatim. A lesson may be about a
|
|
97
|
+
received message; unverified sender content is not soul knowledge by transcription.
|
|
95
98
|
- Then judge every candidate exactly as a note: promote, merge, or drop
|
|
96
99
|
against the same bar. Expect most to drop: session trivia, tool noise,
|
|
97
100
|
restated repo facts and task-scoped decisions all fail it. Promoted
|
package/docs/integrations.md
CHANGED
|
@@ -140,6 +140,53 @@ Test an integration as a capability package: acquire, lock, trust, activate,
|
|
|
140
140
|
spawn, retire, with the golden fixtures as the behavior oracle for the kernel
|
|
141
141
|
side.
|
|
142
142
|
|
|
143
|
+
## oats.okf harvest settings (1.5.1)
|
|
144
|
+
|
|
145
|
+
The harvester can use a different harness from the source instance. Select one
|
|
146
|
+
that is installed and authenticated on the host where the harvest runs:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
oats use oats.okf --settings harvest-runtime=claude
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `harvest-runtime: pi | claude | codex` defaults to `pi`.
|
|
153
|
+
- `harvest-model` is an optional pin, for example to use a cheaper model.
|
|
154
|
+
When omitted, each harness uses its configured default. Pi accepts
|
|
155
|
+
provider/model patterns. Claude and Codex require a native model name
|
|
156
|
+
(for example `sonnet` or `gpt-5.5`), without a Pi provider prefix.
|
|
157
|
+
|
|
158
|
+
These settings apply to note and record harvests, including deferred retirement
|
|
159
|
+
and remote harvests. For a remote instance, configure its host's knowledge
|
|
160
|
+
binding; the local viewer does not supply its own provider credentials.
|
|
161
|
+
|
|
162
|
+
If a record harvester was spawned but did not advance its watermark, planning
|
|
163
|
+
the same windows again warns with that instance and the boundary IDs and skips
|
|
164
|
+
another spawn. Inspect the previous attempt first. `oats okf harvest
|
|
165
|
+
--from-record --force` retries those windows explicitly; it still refuses to
|
|
166
|
+
start a second harvester while the first one's home exists. The check uses the
|
|
167
|
+
existing prepared watermark file and does not treat a successful spawn as
|
|
168
|
+
completed learning.
|
|
169
|
+
|
|
170
|
+
## oats.aweb late joins (1.10.3)
|
|
171
|
+
|
|
172
|
+
`aw team join` at spawn gets 120 s (a slow link is slow, not broken). If the
|
|
173
|
+
join is reported failed or is killed on timeout but the home then holds a
|
|
174
|
+
bound identity (signing key, team certificate, workspace alias), the hook
|
|
175
|
+
reports that alias in its meta so the kernel's compensation retires it
|
|
176
|
+
instead of orphaning it. The retire hook likewise reads the alias from the
|
|
177
|
+
home's `.aw/workspace.yaml` when its meta carries none.
|
|
178
|
+
|
|
179
|
+
## oats.aweb retire report (1.10.2)
|
|
180
|
+
|
|
181
|
+
On a host whose installed `aw` is 1.36.1 or later, the retire hook deletes
|
|
182
|
+
the workspace with `aw workspace delete <alias> --json` and reports what the
|
|
183
|
+
platform answered: `meta.aliasReusable` is true when `alias_released` is
|
|
184
|
+
true (the certificate was revoked and a later spawn may reuse the slug),
|
|
185
|
+
false otherwise with `meta.aliasReason` carrying the platform's reason and a
|
|
186
|
+
warning naming the fresh-purpose remedy. On an older `aw` the pre-1.36.1
|
|
187
|
+
report stands (`aliasReusable: false`, warning naming aweb-abim), because
|
|
188
|
+
that CLI cannot revoke the certificate.
|
|
189
|
+
|
|
143
190
|
## oats.aweb settings (1.10.0)
|
|
144
191
|
|
|
145
192
|
Set with `oats use oats.aweb --settings <key>=<value>` at a scope, or per
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Full operating-team migration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Operating record, updated 2026-09-06. Juan asked lead to discuss the migration with
|
|
4
4
|
Merlin and plan for all teams on this machine to be managed by OATS, with
|
|
5
5
|
harvesting fully working. This expands the earlier release/configuration
|
|
6
6
|
rollout. It does not describe an already completed migration.
|
|
@@ -9,7 +9,9 @@ The cjr runbook is owned by Merlin at
|
|
|
9
9
|
`~/cjr/agents/docs/2026-09-05-oats-migration.md`. This document records the
|
|
10
10
|
shared framework work and the wider rollout.
|
|
11
11
|
|
|
12
|
-
Fresh identities are authorized for
|
|
12
|
+
Fresh identities are authorized for continuing seats as well as specialists and
|
|
13
|
+
reviewers. Preserve the accepted retained-identity choice where it is useful.
|
|
14
|
+
Merlin retains
|
|
13
15
|
both `cjr.aweb.ai/merlin` and his existing durable DID. Aweb clarified that
|
|
14
16
|
re-minting the same address changes identity and breaks continuity; the supported
|
|
15
17
|
path is an explicit transfer of his existing authority with one live process.
|
|
@@ -31,21 +33,75 @@ Every remembering role must have a tested learning path. Preserve each role's
|
|
|
31
33
|
explicit policy: Cjr reviewers exclude accumulated memory; Themis uses
|
|
32
34
|
reviewed learning. Config discovery alone establishes none of this.
|
|
33
35
|
|
|
34
|
-
The installed baseline is published OATS 0.22.
|
|
36
|
+
The installed CLI baseline is published OATS 0.22.5 on this Mac and `aweb-agents`,
|
|
35
37
|
including native Pi/Claude/Codex, tmux/Herdr, shared `yolo`, remote Desktop
|
|
36
38
|
roster/actions, retained-authority binding and corrected deferred retirement.
|
|
37
|
-
The
|
|
38
|
-
launch checks
|
|
39
|
-
|
|
40
|
-
harvest-runtime selection and detection of unadvanced record
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
The installed Mac Desktop 0.22.5 passed published ZIP checksum, strict deep
|
|
40
|
+
codesign, packaged renderer and PTY launch checks; the previous 0.22.4 app
|
|
41
|
+
is preserved for rollback. Official oats.okf 1.5.1 is published after independent
|
|
42
|
+
review, adding harvest-runtime selection and detection of unadvanced record
|
|
43
|
+
plans. Each deployment selects an authenticated harness; without an explicit
|
|
44
|
+
harvest-model, that harness uses its own configured default. Some prepared
|
|
45
|
+
teams still use the compatible 1.5.0 package; preserve the exact versions of
|
|
46
|
+
each earlier qualification.
|
|
47
|
+
|
|
48
|
+
BeadHub, Minerva and Merlin now have managed standing executions with retained
|
|
49
|
+
identities. BeadHub and Minerva passed separate mail/chat checks; Merlin verified
|
|
50
|
+
his identity and preserved claims, then received and replied to Minerva's real
|
|
51
|
+
mail through the host wake path. This is not completion of all teams: frontend
|
|
52
|
+
and Themis encountered failed setup, Docflow still has a running backfill, and
|
|
53
|
+
the coordinator handovers remain outstanding. See the current status below;
|
|
54
|
+
older evidence records keep the version and outcome of each earlier check.
|
|
55
|
+
|
|
56
|
+
## Current status and operating limits (2026-09-06)
|
|
57
|
+
|
|
58
|
+
Juan requires completion without exhausting the machine again. **Do not disturb
|
|
59
|
+
TSM until its deployment is finished.** No TSM runtime, configuration, identity,
|
|
60
|
+
or handover operation is authorized during that boundary. Wait for Zeus's
|
|
61
|
+
explicit deployment-complete report; earlier cutover sequencing below is
|
|
62
|
+
superseded by this condition.
|
|
63
|
+
|
|
64
|
+
The first broad rollout produced overlapping record-capture processes: each
|
|
65
|
+
could index the large local store, with individual processes exceeding 2 GB
|
|
66
|
+
RSS. The capture watcher was about 1.7 GB. The experimental mind follow service
|
|
67
|
+
also consumed substantial CPU/memory and launched model runs. These are observed
|
|
68
|
+
contributors, not a complete accounting of the reported GUI memory incident;
|
|
69
|
+
no OATS Desktop process remained when lead took the incident snapshot.
|
|
70
|
+
|
|
71
|
+
Lead stopped the capture watcher and residual capture passes, disabled their
|
|
72
|
+
exact Claude hooks, and stopped the experimental mind follow service. Settings,
|
|
73
|
+
service definitions, raw records and learning state are preserved. GUI launch
|
|
74
|
+
is paused. Resume with one bounded operation at a time, checking memory between
|
|
75
|
+
launches; declining swap alone does not prove sustained stability. Capture stays
|
|
76
|
+
disabled until its concurrency fix is independently reviewed and measured. The
|
|
77
|
+
first proposed lock was rejected because age-based stealing and initialization
|
|
78
|
+
races could still permit overlapping passes.
|
|
79
|
+
|
|
80
|
+
| Scope | Verified state | Next boundary |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| Host services | Published aw 1.36.1 installed; normal launchd wake service on Mac and enabled user service on `aweb-agents`; private broker stopped | Investigate repeated reconnect hints and reported read timing without assuming the broker acknowledged mail |
|
|
83
|
+
| BeadHub | `beadhub-seat`, retained DID/address and claims; native Codex; independent mail/chat; first reviewed knowledge PR merged at `70c839e` | Repeat harvest exposed a retained merged-branch collision; operator updated the linked soul and removed only the verified merged branch; next cycle waits for a bounded launch slot |
|
|
84
|
+
| Cjr | `accountant-minerva` and `coordinator-merlin` live on retained identities; old holders stopped first; claims preserved; real delivery and reviewed learning recorded | Complete the existing log worker's fresh review, one reviewer at a time; no financial authority changes |
|
|
85
|
+
| Aweb | Coordinator remains live; old frontend stopped; replacement failed during a timed-out join that completed server-side | Supported cleanup of the retained failed home/orphan binding, then one successor with independent delivery checks |
|
|
86
|
+
| TSM | Prepared souls and owner checkpoints; Themis setup failed before the current hold | **No migration work until Zeus reports deployment finished**; re-inventory with its owner afterwards |
|
|
87
|
+
| Docflow | Legacy seat and actual mail backfill remain running | Finish backfill and register checks; owner restores mail-ingest afterwards; accountant-sync remains unloaded under its separate export fence |
|
|
88
|
+
| Oats/lead | Existing coordinators remain active | Last handovers, with actual stop receipts and all unresolved work carried forward |
|
|
89
|
+
| Remote qualification | Published host service delivered native Claude mail/chat through Herdr; corrected knowledge independently reviewed; source retired with `aliasReusable: true` | Earlier separate fresh-reader cycle passed; latest corrected wake-specific retrieval is still pending |
|
|
90
|
+
|
|
91
|
+
Published aw 1.36.1 is tagged at `bfdb20886080e4ffe1f02b266f6116d12bd100fd`.
|
|
92
|
+
All 46 release targets have passing results for that source: targets 1–41 in
|
|
93
|
+
one run, followed by an explicitly accepted continuation of 42–46 after a Go
|
|
94
|
+
download failure. This was not one atomic run. Evidence is archived under
|
|
95
|
+
`~/awebai/bookshelf/records/2026-09-05-aw-1.36.1-split-gate/`.
|
|
96
|
+
Production same-alias join/delete/rejoin passed, and official oats.aweb 1.10.2
|
|
97
|
+
reports the released alias result truthfully. Retained standing-seat retirement
|
|
98
|
+
must still preserve authority.
|
|
99
|
+
|
|
100
|
+
Desktop 0.22.5 has six validated team roots saved as workspace suggestions,
|
|
101
|
+
not six running GUI instances. It starts with one workspace and can add others.
|
|
102
|
+
It is currently closed; visual QA and sustained multi-workspace memory behavior
|
|
103
|
+
are not claimed. Native remote Pi authentication and remote Codex remain
|
|
104
|
+
unqualified; the accepted remote harness is Claude.
|
|
49
105
|
|
|
50
106
|
## Scope inventory
|
|
51
107
|
|
|
@@ -55,17 +111,17 @@ of continuing seats.
|
|
|
55
111
|
|
|
56
112
|
| Scope | Starting point | Required disposition |
|
|
57
113
|
| --- | --- | --- |
|
|
58
|
-
| `~/awebai/oats` | Live Claude coordinator and Codex lead; managed review workers also running | Oats owns coordinator handover; lead owns lead handover;
|
|
59
|
-
| `~/cjr` | Preparation `5afb3e8b`; developer pilot landed on master `062e2c75`; legacy Merlin and Minerva live | Merlin owns safe handovers; preserve his DID/address;
|
|
60
|
-
| `~/awebai/aweb` | Live Claude coordinator and frontend in legacy homes | Oats owns coordinator handover; lead coordinates frontend with aweb after its current work;
|
|
61
|
-
| `~/tsm` | Five live seats: Zeus, Prometeo, Argos, Themis on Claude; Hermes on Codex.
|
|
114
|
+
| `~/awebai/oats` | Live Claude coordinator and Codex lead; managed review workers also running | Oats owns coordinator handover; lead owns lead handover; follow the explicit fresh or retained identity choice |
|
|
115
|
+
| `~/cjr` | Preparation `5afb3e8b`; developer pilot landed on master `062e2c75`; legacy Merlin and Minerva live | Merlin owns safe handovers; preserve his DID/address; automatic harvest completion and successor use are proven; prepare standing seats |
|
|
116
|
+
| `~/awebai/aweb` | Live Claude coordinator and frontend in legacy homes | Oats owns coordinator handover; lead coordinates frontend with aweb after its current work; handover task responsibility and cover child repositories |
|
|
117
|
+
| `~/tsm` | Five live seats: Zeus, Prometeo, Argos, Themis on Claude; Hermes on Codex. All five souls integrated, official capabilities installed and trusted, owner checkpoints prepared | Paused by Juan until deployment complete; owner rechecks all seats afterwards; preserve schedules and production authority |
|
|
62
118
|
| `~/prj/beadhub-all` | Live Codex session, despite stale offline roster | Beadhub accepted preparation and is at a safe boundary; retain its global identity, native Codex and separate canonical code roots under `~/awebai/beadhub`; billing remains separately gated |
|
|
63
119
|
| `~/prj/docflow` | Live Claude seat identified itself as local `juan.aweb.ai/alice` on `docflow:juan.aweb.ai` | Owner Juan; finish running mail backfill and register checks before transfer; retain identity, memory and Minerva route; accountant-sync remains deliberately unloaded |
|
|
64
120
|
| `ai.aweb` on `aweb-agents` | Aweb confirms Athena intentionally inactive; remote legacy home retained | Aweb and oats own archival inspection; do not resurrect as a continuing seat |
|
|
65
121
|
| `~/awebai/demo-aweb/bob` | Live Pi demo | Aweb owns safe stop and archival disposition; it is not an operating-team migration |
|
|
66
|
-
| `~/.turn-record` |
|
|
122
|
+
| `~/.turn-record` | Capture and experimental mind services paused after memory incident | Preserve records; review and measure resource fixes before resuming |
|
|
67
123
|
|
|
68
|
-
The
|
|
124
|
+
The starting inventory above was checked on 2026-09-05 using harness process
|
|
69
125
|
working directories and exact custom tmux sockets, without interrupting them.
|
|
70
126
|
TSM uses its aweb tmux socket, BeadHub the awebai socket, and Docflow the
|
|
71
127
|
main socket. Lead delivered explicitly attributed coordination messages to
|
|
@@ -138,17 +194,18 @@ stops the runtime before releasing capabilities; status remains read-only.
|
|
|
138
194
|
Cjr archived its local harvester override and uses official oats.okf 1.5.0.
|
|
139
195
|
Its authenticated Pi model is `openai-codex/gpt-5.5`. The remote qualification
|
|
140
196
|
host's equivalent provider login fails refresh with `invalid_refresh_token`;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
197
|
+
both failed Pi tests were retired normally. No rotating login tokens were copied.
|
|
198
|
+
Official oats.okf 1.5.1 now selects the already authenticated native Claude
|
|
199
|
+
runtime on that host. A real note-fed harvest promoted an operational lesson,
|
|
200
|
+
self-retired, and a fresh successor retrieved and used the lesson. This proves
|
|
201
|
+
that alternative harness path; it does not claim the Pi login was repaired.
|
|
145
202
|
|
|
146
203
|
### Finish temporary identity retirement
|
|
147
204
|
|
|
148
205
|
The aweb owner must resolve the remote lifecycle defect tracked under
|
|
149
206
|
`aweb-aaum.6`; oats coordinates package integration. The leaked release identities are a reproduction; reconcile the exact
|
|
150
207
|
owner-side list before naming or deleting them. Alias-release fixes are in
|
|
151
|
-
aweb source; production same-alias join/delete/rejoin acceptance
|
|
208
|
+
aweb source; production same-alias join/delete/rejoin acceptance passed on published aw 1.36.1. Independently verify coordination cleanup,
|
|
152
209
|
claims and certificate state. Admin cleanup is a recovery procedure, not
|
|
153
210
|
proof of automatic retirement. This gates temporary-worker completion;
|
|
154
211
|
adopted standing executions instead must preserve their durable identity.
|
|
@@ -240,7 +297,7 @@ substitute a green `doctor`, a roster row or a successful hook report for
|
|
|
240
297
|
the corresponding live check. Keep credentials and private case data out of
|
|
241
298
|
the shared rollout record.
|
|
242
299
|
|
|
243
|
-
##
|
|
300
|
+
## Earlier operating evidence (2026-09-05)
|
|
244
301
|
|
|
245
302
|
- Cjr's pilot landed five useful task commits and eleven promoted concepts
|
|
246
303
|
from four harvests, with independent code and knowledge reviews. Ordinary
|
|
@@ -253,16 +310,23 @@ the shared rollout record.
|
|
|
253
310
|
- TSM's owner plan is `~/tsm/history/2026-09-06-tsm-oats-handover-plan.md`.
|
|
254
311
|
Preserve the five seats' worktrees, skills and knowledge; re-arm Zeus's
|
|
255
312
|
session-local schedules. Production credentials remain solely with the
|
|
256
|
-
authorized production operator.
|
|
257
|
-
|
|
313
|
+
authorized production operator. Zeus remains last. Juan's current direction
|
|
314
|
+
authorizes completing the migration while he is away; the earlier
|
|
315
|
+
presence-only pause does not override it. Actual job and production boundaries
|
|
316
|
+
remain. Themis's ten-commit packet landed at `46cf2083`; its installed
|
|
317
|
+
oats.aweb 1.10.1 and oats.okf 1.5.0 passed the actual doctor. All five souls
|
|
318
|
+
are now integrated through Prometeo's `103e534c`, with owner-approved private
|
|
319
|
+
startup briefs and checkpoints. Those checkpoints must be refreshed at the
|
|
320
|
+
actual stop; Zeus's deployment and scheduled-job boundaries remain binding.
|
|
258
321
|
- BeadHub configuration/soul commit `3ee13a8` in `awebai/beadhub-saas`
|
|
259
322
|
(`~/awebai/beadhub/beadhub-saas`) was independently ACKed by lead and
|
|
260
323
|
landed on `main`. Its tracked deployment template is materialized at the canonical
|
|
261
324
|
parent workspace, with trusted official capabilities and a clean actual
|
|
262
325
|
doctor result. The soul retains SaaS Git provenance and uses workspace mode
|
|
263
326
|
as a coordinator across the three canonical repositories. Its legacy holder
|
|
264
|
-
remains live; authenticated harvest-model configuration
|
|
265
|
-
broker service
|
|
327
|
+
remains live; authenticated harvest-model configuration is installed and
|
|
328
|
+
verified. The published broker service precedes activation. Stripe and
|
|
329
|
+
production gates remain separate.
|
|
266
330
|
- Docflow supplied its own identity and handover through its terminal. Its
|
|
267
331
|
backfill and register verification define the safe boundary. Preserve its
|
|
268
332
|
Claude memory and existing credentials in place; verify filesystem/TCC
|
|
@@ -276,9 +340,9 @@ the shared rollout record.
|
|
|
276
340
|
relying on those routes; do not silently replace retained identities.
|
|
277
341
|
- Real remote Claude launch, terminal input and detach survival passed.
|
|
278
342
|
Remote Desktop projection and exact-home lifecycle shipped in 0.22.3; the
|
|
279
|
-
installed remote CLI serves the real registered roster.
|
|
280
|
-
|
|
281
|
-
remote seat is declared migrated.
|
|
343
|
+
installed remote CLI serves the real registered roster. Native Claude
|
|
344
|
+
harvesting subsequently completed with official oats.okf 1.5.1, as recorded
|
|
345
|
+
below. No standing remote seat is declared migrated.
|
|
282
346
|
- The aweb broker candidate `30469e22` ran as a private launchd service against
|
|
283
347
|
published OATS 0.22.3. Real Claude and Codex sessions in tmux and Pi
|
|
284
348
|
in Herdr, with Claude channel 1.7.9 and Pi extension 0.3.10, fetched mail and replied with exact qualification tokens,
|
|
@@ -291,7 +355,89 @@ the shared rollout record.
|
|
|
291
355
|
Published aw installation and standing-seat acceptance are still separate.
|
|
292
356
|
- Qualification found that tmux output loses tab separators in a minimal
|
|
293
357
|
launchd environment without a UTF-8 locale. Independently reviewed kernel
|
|
294
|
-
|
|
358
|
+
fixes `a0b0e14` and `0260ea9` shipped in 0.22.4, adding UTF-8 mode to
|
|
359
|
+
lifecycle/input and viewer calls. The host service also specifies
|
|
295
360
|
`LC_ALL=en_US.UTF-8`. Existing Claude plugin 1.7.8 also consumed mail before
|
|
296
361
|
the broker wake; updating to 1.7.9 eliminated that competing delivery path.
|
|
297
362
|
The deployed oats.aweb 1.10.1 floor now rejects the incompatible version.
|
|
363
|
+
|
|
364
|
+
- Cjr's second and third worker cycles completed on published kernel 0.22.3
|
|
365
|
+
and oats.okf 1.4.1 (note-fed). The
|
|
366
|
+
second promoted three reviewed concepts; the third read them before working,
|
|
367
|
+
produced the normal host-service health reader (`3dfe3acf`) and completed
|
|
368
|
+
its own reviewed harvest. Both harvesters self-retired without operator
|
|
369
|
+
completion. Each worker's ordinary retirement took four seconds and removed
|
|
370
|
+
its temporary alias. A post-merge fixture mismatch was corrected forward in
|
|
371
|
+
`e8a87c71`, with 47 tests green on a clean export. The reader's real service
|
|
372
|
+
check awaits installation of `ai.aweb.wake`.
|
|
373
|
+
- A record-only harvest of the fresh Claude wake-test session completed with
|
|
374
|
+
official oats.okf 1.5.0. It read its 27-turn source window, found no durable
|
|
375
|
+
lesson to promote, advanced the completed watermark and self-retired. The
|
|
376
|
+
first attempt exposed an unnecessary SQLite index write during pure journal
|
|
377
|
+
reads; reviewed fix `f100320` shipped in 0.22.4. The shared capture service
|
|
378
|
+
and its large index were preserved. Completion is proven; this run does not
|
|
379
|
+
claim a knowledge promotion.
|
|
380
|
+
- Docflow's two-commit preparation at `4458097` is independently ACKed: native
|
|
381
|
+
Claude, retained authority, session delivery and 18 valid curated OKF concepts.
|
|
382
|
+
Its role and package preparation is integrated. The running backfill
|
|
383
|
+
and FY2025 register checks still determine its activation boundary.
|
|
384
|
+
- The tracked aweb coordinator soul at `0a3a9a91` is independently ACKed; the
|
|
385
|
+
frontend soul at `b3985edb` is owner-reviewed and landed. They preserve the
|
|
386
|
+
coordinator's workspace and the frontend's managed primary SaaS worktree plus
|
|
387
|
+
explicitly assigned secondary OSS tree. Lead and Oats coordinator souls are
|
|
388
|
+
also reviewed and landed. Concrete deployment configuration and final
|
|
389
|
+
checkpoints precede each launch.
|
|
390
|
+
- OATS 0.22.4 is published at tag `0260ea9`, with npm packages and six Desktop
|
|
391
|
+
installers. CI retried once after a disappearing Git maintenance lock in a
|
|
392
|
+
fixture; kernel and Desktop gates then passed. A manual reviewed version-bump
|
|
393
|
+
PR completed the bot's permission-blocked post-publication step. Published
|
|
394
|
+
npm JavaScript bytes match the tag. OATS 0.22.5 subsequently shipped the
|
|
395
|
+
package-selector CLI and oats.okf 1.5.1 pin; its manual version-bump PR #5
|
|
396
|
+
completed at `91ef541`. Aweb 1.36.1 remains unpublished: candidate `bfdb2088`
|
|
397
|
+
passed targets 1–41, then a Go dependency download failed in target 42.
|
|
398
|
+
Its coordinator explicitly accepted completing targets 42–46 on that same
|
|
399
|
+
candidate and environment, preserving both logs and recording the loss of
|
|
400
|
+
single-run atomicity. The remainder is running; the private broker is not
|
|
401
|
+
being represented as the permanent published service.
|
|
402
|
+
|
|
403
|
+
- Cjr's retained declaration/runbook packet is independently ACKed through
|
|
404
|
+
`6dc6efc8` (three commits); its source authority remains in place and operator
|
|
405
|
+
stop-before-start applies to rollback as well as cutover. Its two soul startup
|
|
406
|
+
corrections also landed after independent review at `bd981b3a`. Official
|
|
407
|
+
oats.aweb 1.10.1 and oats.okf 1.5.0 are now installed and trusted for both
|
|
408
|
+
standing souls. These newer pins do not relabel the earlier worker proofs.
|
|
409
|
+
- TSM's Argos packet is integrated at `f20c54bb` after both independent and
|
|
410
|
+
owner ACKs; actual doctor resolves the two retained reviewer seats. BeadHub
|
|
411
|
+
and Themis supplied final private startup briefings and idle checkpoints.
|
|
412
|
+
Hermes and Prometeo were explicitly woken to read unseen followups; their
|
|
413
|
+
legacy channels had not delivered those requests reliably. Both subsequently
|
|
414
|
+
approved their souls and final handover checkpoints.
|
|
415
|
+
- A locked-selector update gap found during actual team setup was corrected
|
|
416
|
+
at `d95018e` (two independently reviewed commits) and shipped in 0.22.5.
|
|
417
|
+
It supports `oats update <package> --to <ref>` or a positional catalog
|
|
418
|
+
spec. A hermetic CLI test exercises the version transition, trust reset and
|
|
419
|
+
invalid arguments, including refusal to enter the kernel self-updater when
|
|
420
|
+
a package is missing. The installed remote 0.22.5 CLI accepted the selector
|
|
421
|
+
and preserved the already-correct 1.5.1 lock and its trust.
|
|
422
|
+
|
|
423
|
+
- Herdr 0.8.2 is installed on both hosts. Published OATS 0.22.4 launched a
|
|
424
|
+
real remote Claude session through the saved server route; a separate
|
|
425
|
+
session-input request elicited an actual reply. The source and its successor
|
|
426
|
+
then completed native Claude harvesting and knowledge use with official OKF
|
|
427
|
+
1.5.1. The harvester consumed its source note, promoted one PATH-resolution
|
|
428
|
+
lesson, passed strict validation, and self-retired. A fresh instance read the
|
|
429
|
+
indexed lesson before checking every resolved tool path and comparing runtime
|
|
430
|
+
and login shells. Both source instances were retired normally with their
|
|
431
|
+
changed home files preserved. This is local-soul promotion and retrieval,
|
|
432
|
+
not a repository PR cycle or remote broker-wake acceptance.
|
|
433
|
+
- The remote host now has shared `yolo: true`, updated Pi installations,
|
|
434
|
+
Claude channel 1.7.9, and the published OATS 0.22.5 capture hooks and enabled
|
|
435
|
+
user service. Its existing record-owner name was preserved and the initial
|
|
436
|
+
capture/index pass completed. The Mac capture service was left in place.
|
|
437
|
+
- Coordinator scope is explicit: Oats and Aweb retain their global identities
|
|
438
|
+
through the rehearsed authority-transfer path. Fresh local lead/frontend
|
|
439
|
+
identities relay cross-team requests through a verified global coordinator;
|
|
440
|
+
an alias containing a domain does not itself establish global reach. The
|
|
441
|
+
final briefs preserve conversations or hand off unresolved threads according
|
|
442
|
+
to that identity choice, require an actual old-process stop, and distinguish
|
|
443
|
+
startup context from a separate incoming-message wake check.
|
package/docs/packages.md
CHANGED
|
@@ -30,7 +30,11 @@ default. Only the selected subtree is installed and hashed, so repository docs,
|
|
|
30
30
|
CI configuration, owner souls, and sibling packages stay outside the package's
|
|
31
31
|
payload and integrity. One repository may ship several packages at different
|
|
32
32
|
paths. The lock pins the selected root in its own `path` field, and only an
|
|
33
|
-
explicit `oats update <package>` may move it.
|
|
33
|
+
explicit `oats update <package>` may move it. A catalog lock with an explicit
|
|
34
|
+
selector (`catalog:oats.aweb@v1.8.0`) keeps that selector on a plain update;
|
|
35
|
+
to advance it to another published ref, give the spec or `--to`:
|
|
36
|
+
`oats update oats.aweb oats.aweb@v1.10.1` or `oats update oats.aweb --to v1.10.1`
|
|
37
|
+
(same transactional path, approvals invalidated, then `oats trust`). See
|
|
34
38
|
[`design/package-engine-contract.md` §1.1](design/package-engine-contract.md).
|
|
35
39
|
|
|
36
40
|
Ground truth for the contract: [`oats-package.schema.json`](oats-package.schema.json),
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# OATS v0.22.5
|
|
2
|
+
|
|
3
|
+
A small release: the knowledge package 1.5.1 pin, a way to move a pinned
|
|
4
|
+
package lock from the CLI, and the operating records from the first team
|
|
5
|
+
rollouts.
|
|
6
|
+
|
|
7
|
+
## Bundled: oats.okf 1.5.1
|
|
8
|
+
|
|
9
|
+
`harvest-runtime` chooses the harness that runs the memory harvester (pi,
|
|
10
|
+
claude or codex; default pi), so a host authenticated for one harness need
|
|
11
|
+
not authenticate another. No harness gets a vendor default: with no
|
|
12
|
+
`harvest-model` set, the harvester inherits the harness's own configured
|
|
13
|
+
model; `harvest-model` remains the way to pin a cheaper one. The promotion
|
|
14
|
+
instructions exclude secrets, credentials and verbatim third-party message
|
|
15
|
+
content from promotion. `okf harvest` warns instead of respawning when a
|
|
16
|
+
plan's window boundaries did not move since the previous harvester was
|
|
17
|
+
spawned.
|
|
18
|
+
|
|
19
|
+
## Move a pinned package lock
|
|
20
|
+
|
|
21
|
+
`oats update <package> <package>@<ref>` or `oats update <package> --to <ref>`
|
|
22
|
+
moves a catalog-sourced lock to another published ref through the same
|
|
23
|
+
transactional update (closure validation, lock replace, approvals
|
|
24
|
+
invalidated, then `oats trust`). A plain `oats update <package>` keeps an
|
|
25
|
+
explicit selector, as before, and `--to` without a package is refused rather
|
|
26
|
+
than falling through to the kernel self-update (`oats update` with no
|
|
27
|
+
package).
|
|
28
|
+
|
|
29
|
+
## Also
|
|
30
|
+
|
|
31
|
+
- The managed oats coordinator soul (`agents/oats-coordinator/soul`) is
|
|
32
|
+
tracked, with playbooks for the release lane, payload publication and
|
|
33
|
+
review routing.
|
|
34
|
+
- `docs/operating-team-migration.md` records the installed 0.22.4 rollout,
|
|
35
|
+
the completed record-fed and note-fed harvest cycles, and the standing
|
|
36
|
+
seats prepared for cutover.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# OATS v0.22.6
|
|
2
|
+
|
|
3
|
+
A resource-safety patch after the first full team rollout exhausted the
|
|
4
|
+
machine: record capture runs one pass per root, the Desktop app runs once,
|
|
5
|
+
and `--help` never executes a command. Plus the oats.aweb 1.10.3 pin.
|
|
6
|
+
|
|
7
|
+
## Record capture: one pass per root
|
|
8
|
+
|
|
9
|
+
Hook-triggered capture passes, one per agent event across every live agent,
|
|
10
|
+
used to overlap, each opening the multi-gigabyte search index. A capture pass
|
|
11
|
+
now takes a per-root lock directory (`<root>/.capture.lock`). A second pass
|
|
12
|
+
finding it skips at once and the next pass catches up, since reconciliation
|
|
13
|
+
is idempotent. The lock is never stolen: a live, dead, unknowable or
|
|
14
|
+
still-initializing holder all refuse the pass, and the refusal names the
|
|
15
|
+
holder's pid, start time and liveness together with the exact operator
|
|
16
|
+
recovery (verify the pid is gone, remove the lock directory with the printed
|
|
17
|
+
shell-safe command, rerun). A killed pass therefore needs one explicit
|
|
18
|
+
cleanup, which is the accepted trade against any automatic reclaim protocol.
|
|
19
|
+
|
|
20
|
+
An indexing-enabled pass reconciles the index even when it appended nothing,
|
|
21
|
+
so turns left by append-only passes (`--sessions-only --no-index --quiet`,
|
|
22
|
+
the recommended hook form from `capture --install-hint`) become searchable at
|
|
23
|
+
the next plain pass.
|
|
24
|
+
|
|
25
|
+
## Desktop runs once
|
|
26
|
+
|
|
27
|
+
A repeated launch of OATS Desktop no longer starts a second backend, viewer
|
|
28
|
+
set and window. The secondary process exits before any startup work, and the
|
|
29
|
+
running app restores and focuses its existing window (waiting for its own
|
|
30
|
+
startup to finish first). Switching workspaces stays with the validated
|
|
31
|
+
switcher inside the running window.
|
|
32
|
+
|
|
33
|
+
## `--help` never executes
|
|
34
|
+
|
|
35
|
+
`oats <command> --help` (and `-h`) prints usage for every kernel builtin and,
|
|
36
|
+
for capability commands, answers from the manifest without running the
|
|
37
|
+
capability executable. Previously several commands ran with `--help` as an
|
|
38
|
+
argument.
|
|
39
|
+
|
|
40
|
+
## Bundled: oats.aweb 1.10.3
|
|
41
|
+
|
|
42
|
+
The join wait after spawn is 120 s (`OATS_AWEB_JOIN_TIMEOUT_MS`), a join that
|
|
43
|
+
completes server-side after the client timed out is recovered instead of
|
|
44
|
+
leaving a bound alias behind, retirement reads the alias from the home's
|
|
45
|
+
`.aw/workspace.yaml`, and on aw 1.36.1 `aliasReusable` reflects the server's
|
|
46
|
+
`alias_released_reason` truthfully (1.10.2).
|
|
47
|
+
|
|
48
|
+
## Also
|
|
49
|
+
|
|
50
|
+
- `oats use` from no binding writes a targeted binding with `global: false`.
|
|
51
|
+
- `docs/operating-team-migration.md` records the live rollout, the memory
|
|
52
|
+
incident and its mitigation, the serial launch constraint and the TSM
|
|
53
|
+
deployment hold.
|
package/lib/core.mjs
CHANGED
|
@@ -3343,6 +3343,8 @@ export function updatePackage(startDir, packageId, opts = {}) {
|
|
|
3343
3343
|
// the user's own selection, so it is re-appended and stays sticky across
|
|
3344
3344
|
// updates; a catalog entry OWNS its path, so an update deliberately re-reads it
|
|
3345
3345
|
// and may adopt a moved root (reported below).
|
|
3346
|
+
if (opts.spec && src.kind !== "catalog") throw oatsError("invalid-source", `package "${packageId}" is locked from a ${src.kind} source; a selector (--to) applies to catalog-sourced packages only (its source is ${entry.source})`);
|
|
3347
|
+
if (opts.spec && parsePackageSource(opts.spec).id !== src.id) throw oatsError("invalid-source", `selector spec "${opts.spec}" names a different catalog package than the lock's ${src.id}`);
|
|
3346
3348
|
const spec = opts.spec || (src.kind === "catalog" ? (src.selector ? `${src.id}@${src.selector}` : src.id)
|
|
3347
3349
|
: src.kind === "git" ? `${src.ref && !/^[0-9a-f]{40}$/.test(src.ref) ? `${src.url}@${src.ref}` : src.url}#${entry.path}`
|
|
3348
3350
|
: src.path);
|
package/package-catalog.json
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"packages": {
|
|
3
3
|
"oats.okf": {
|
|
4
4
|
"url": "https://github.com/awebai/oats-okf.git",
|
|
5
|
-
"ref": "v1.5.
|
|
5
|
+
"ref": "v1.5.1",
|
|
6
6
|
"path": "oats-package"
|
|
7
7
|
},
|
|
8
8
|
"oats.aweb": {
|
|
9
9
|
"url": "https://github.com/awebai/oats-aweb.git",
|
|
10
|
-
"ref": "v1.10.
|
|
10
|
+
"ref": "v1.10.3",
|
|
11
11
|
"path": "oats-package"
|
|
12
12
|
},
|
|
13
13
|
"oats.jira": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.6",
|
|
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",
|
|
@@ -24,6 +24,7 @@ import { SESSION_FORMATS } from "../lib/formats.mjs";
|
|
|
24
24
|
import { captureAwLogs, defaultCommLogDir } from "../lib/capture-aw.mjs";
|
|
25
25
|
import { RecordIndex } from "../lib/index-db.mjs";
|
|
26
26
|
import { IgnoreError, ignoreFilePath, loadIgnore } from "../lib/ignore.mjs";
|
|
27
|
+
import { acquireCaptureLock } from "../lib/capture-lock.mjs";
|
|
27
28
|
|
|
28
29
|
// Fail closed but actionably: an unreadable ignore file must stop capture,
|
|
29
30
|
// as one clear line naming the file — never an uncaught stack trace.
|
|
@@ -157,7 +158,23 @@ function log(...parts) {
|
|
|
157
158
|
if (!quiet) console.log(...parts);
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
/** Take the root's single-run lock, or say who holds it. A hook-triggered
|
|
162
|
+
* pass that finds it held exits 0: the holder's pass, or the next one,
|
|
163
|
+
* reconciles the same sessions. */
|
|
164
|
+
function withCaptureLock(fn) {
|
|
165
|
+
const lock = acquireCaptureLock(root);
|
|
166
|
+
if (lock.held) {
|
|
167
|
+
// Never quiet: a stale lock after a killed pass needs the operator, and
|
|
168
|
+
// the line says exactly what to check and what to remove.
|
|
169
|
+
const line = `capture: another pass holds ${root}: ${lock.held.recovery}; skipping this pass`;
|
|
170
|
+
if (lock.held.liveness === "alive") log(line); else console.error(line);
|
|
171
|
+
return { appended: 0, skipped: true };
|
|
172
|
+
}
|
|
173
|
+
try { return fn(); } finally { lock.release(); }
|
|
174
|
+
}
|
|
175
|
+
|
|
160
176
|
function pass() {
|
|
177
|
+
return withCaptureLock(() => {
|
|
161
178
|
const out = { appended: 0 };
|
|
162
179
|
const ignore = loadIgnoreOrExit(root);
|
|
163
180
|
if (!args["aw-only"]) {
|
|
@@ -192,7 +209,11 @@ function pass() {
|
|
|
192
209
|
const ignored = awIgnored ? `, ${awIgnored} ignored` : "";
|
|
193
210
|
log(`aw-logs: ${awFiles} files, ${awEntries} entries, ${awAppended} new, ${awFailed} failed${ignored}`);
|
|
194
211
|
}
|
|
195
|
-
|
|
212
|
+
// Index whenever this pass may index, not only when THIS pass appended:
|
|
213
|
+
// an append-only pass (--no-index, the hook form) leaves turns behind
|
|
214
|
+
// that the next indexing pass must pick up. index.update() walks
|
|
215
|
+
// per-stream cursors, so a pass with nothing new is cheap.
|
|
216
|
+
if (!args["no-index"]) {
|
|
196
217
|
const index = new RecordIndex(store);
|
|
197
218
|
try {
|
|
198
219
|
index.update();
|
|
@@ -202,6 +223,7 @@ function pass() {
|
|
|
202
223
|
}
|
|
203
224
|
}
|
|
204
225
|
return out;
|
|
226
|
+
});
|
|
205
227
|
}
|
|
206
228
|
|
|
207
229
|
if (args["install-hint"]) {
|
|
@@ -210,12 +232,15 @@ if (args["install-hint"]) {
|
|
|
210
232
|
|
|
211
233
|
{
|
|
212
234
|
"hooks": {
|
|
213
|
-
"Stop": [{"hooks": [{"type": "command", "command": "node ${self} --sessions-only --quiet"}]}],
|
|
214
|
-
"SessionEnd": [{"hooks": [{"type": "command", "command": "node ${self} --sessions-only --quiet"}]}]
|
|
235
|
+
"Stop": [{"hooks": [{"type": "command", "command": "node ${self} --sessions-only --no-index --quiet"}]}],
|
|
236
|
+
"SessionEnd": [{"hooks": [{"type": "command", "command": "node ${self} --sessions-only --no-index --quiet"}]}]
|
|
215
237
|
}
|
|
216
238
|
}
|
|
217
239
|
|
|
218
|
-
|
|
240
|
+
Hook passes append turns only (--no-index); the search index is updated by
|
|
241
|
+
capture --watch (every 15 minutes and on change) or by a plain capture pass.
|
|
242
|
+
One pass runs per record root at a time: a hook pass that finds another
|
|
243
|
+
running exits at once, and a dropped hook is recovered by any later pass.`);
|
|
219
244
|
process.exit(0);
|
|
220
245
|
}
|
|
221
246
|
|
|
@@ -245,17 +270,22 @@ if (args.home) {
|
|
|
245
270
|
const dirs = new Map(); // one capture pass per (format, directory)
|
|
246
271
|
for (const s of found) dirs.set(`${s.source}\0${dirname(s.path)}`, { format: s.source, dir: dirname(s.path) });
|
|
247
272
|
let appended = 0;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const index = new RecordIndex(store);
|
|
253
|
-
try {
|
|
254
|
-
index.update();
|
|
255
|
-
} finally {
|
|
256
|
-
index.close();
|
|
273
|
+
const homePass = withCaptureLock(() => {
|
|
274
|
+
let n = 0;
|
|
275
|
+
for (const { format, dir } of dirs.values()) {
|
|
276
|
+
n += captureSessions(store, { owner, roots: [dir], format, ignore }).appended;
|
|
257
277
|
}
|
|
258
|
-
|
|
278
|
+
if (!args["no-index"]) { // same as pass(): an earlier append-only pass may have left unindexed turns
|
|
279
|
+
const index = new RecordIndex(store);
|
|
280
|
+
try {
|
|
281
|
+
index.update();
|
|
282
|
+
} finally {
|
|
283
|
+
index.close();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { appended: n };
|
|
287
|
+
});
|
|
288
|
+
appended = homePass.appended;
|
|
259
289
|
// A tombstoned turn is hidden everywhere; a boundary naming one would be
|
|
260
290
|
// refused by recall, so boundaries come from the visible turns only.
|
|
261
291
|
const claims = store.tombstoneClaims();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// One capture pass per record root at a time. Hook-triggered passes (one per
|
|
2
|
+
// agent event, across every live agent) used to overlap, each opening the
|
|
3
|
+
// multi-gigabyte search index; a second pass finding the lock exits at once
|
|
4
|
+
// and the next pass catches up, since reconciliation is idempotent.
|
|
5
|
+
//
|
|
6
|
+
// The lock is a DIRECTORY: mkdir is atomic and a directory is never
|
|
7
|
+
// observable half-created. The owner record (pid, start time) is written
|
|
8
|
+
// inside it after the mkdir. Nothing here ever steals a lock: any existing
|
|
9
|
+
// lock, live, dead, unknowable or still initializing, refuses the pass and
|
|
10
|
+
// names the holder and the operator recovery. A stale lock after a killed
|
|
11
|
+
// pass is removed by the operator once the pid is verified gone; the
|
|
12
|
+
// message says exactly that. (A reclaim protocol was reviewed and rejected:
|
|
13
|
+
// rename is not compare-and-swap, and stealing from a stalled live
|
|
14
|
+
// initializer under memory pressure is the failure we are preventing.)
|
|
15
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
export function captureLockPath(root) { return join(root, ".capture.lock"); }
|
|
19
|
+
|
|
20
|
+
/** "alive" | "dead" | "unknown" for an owner pid ("unknown" = exists but not signalable). */
|
|
21
|
+
export function holderLiveness(pid) {
|
|
22
|
+
if (!Number.isInteger(pid) || pid <= 0) return "unknown";
|
|
23
|
+
try { process.kill(pid, 0); return "alive"; } catch (e) { return e.code === "EPERM" ? "unknown" : "dead"; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readOwner(dir) {
|
|
27
|
+
try { return JSON.parse(readFileSync(join(dir, "owner.json"), "utf8")); } catch { return undefined; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Single-quote shell escaping: safe to paste whatever the path contains. */
|
|
31
|
+
export function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'"; }
|
|
32
|
+
|
|
33
|
+
/** The operator's recovery line for a lock that is not ours. */
|
|
34
|
+
export function recoveryInstruction(dir, owner, liveness) {
|
|
35
|
+
const remove = `rm -r -- ${shellQuote(dir)}`;
|
|
36
|
+
if (!owner?.pid) return `${dir} is held by a pass that has not written its owner record yet (initializing, or killed before it could); stop capture triggers (hooks, launchd), verify no capture process is running (pgrep -f capture.mjs), then remove the lock with: ${remove} and rerun`;
|
|
37
|
+
const who = `pid ${owner.pid} (started ${owner.startedAt || "?"}, now ${liveness})`;
|
|
38
|
+
if (liveness === "alive") return `${dir} is held by ${who}; let it finish, the next pass catches up`;
|
|
39
|
+
return `${dir} is held by ${who}; if that process is gone (ps -p ${owner.pid}), remove the lock with: ${remove} and rerun`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Try to take the root's capture lock. Returns { path, release } when
|
|
43
|
+
* taken, or { path, held: { pid, startedAt, liveness, recovery } } when any
|
|
44
|
+
* lock exists. Never removes a lock it did not create. */
|
|
45
|
+
export function acquireCaptureLock(root, { now = Date.now, pid = process.pid, liveness = holderLiveness } = {}) {
|
|
46
|
+
const dir = captureLockPath(root);
|
|
47
|
+
mkdirSync(root, { recursive: true }); // the store creates the root lazily; the lock may come first
|
|
48
|
+
try {
|
|
49
|
+
mkdirSync(dir);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
if (e.code !== "EEXIST") throw e;
|
|
52
|
+
const owner = readOwner(dir);
|
|
53
|
+
const live = owner ? (owner.pid === pid ? "alive" : liveness(owner.pid)) : "unknown";
|
|
54
|
+
return { path: dir, held: { pid: owner?.pid, startedAt: owner?.startedAt, liveness: live, recovery: recoveryInstruction(dir, owner, live) } };
|
|
55
|
+
}
|
|
56
|
+
writeFileSync(join(dir, "owner.json"), JSON.stringify({ pid, startedAt: new Date(now()).toISOString() }));
|
|
57
|
+
return {
|
|
58
|
+
path: dir,
|
|
59
|
+
release: () => {
|
|
60
|
+
const cur = readOwner(dir);
|
|
61
|
+
if (cur && cur.pid === pid) { try { rmSync(dir, { recursive: true, force: true }); } catch { /* already gone */ } }
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|