@awebai/oats 0.22.5 → 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 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
- entry = existing && existing !== "none" && existing.capability === manifest.capability ? existing : { capability: manifest.capability };
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
- // A layer entry with no explicit targets is implicitly global — materialize that
717
- // before narrowing, so adding a soul/type binding doesn't silently drop everyone else.
718
- if (manifest.layer && entry.global === undefined && !entry["agent-types"] && !entry.souls) entry.global = true;
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
@@ -2957,6 +2961,17 @@ function capabilityCommand() {
2957
2961
  if (!trust.trusted) bail("E_CAPABILITY_BLOCKED", `${m.capability} executable command is blocked: ${trust.reason}`);
2958
2962
  const sub = args[1];
2959
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
+ }
2960
2975
  // Distinguish an ABSENT key from a declared-but-invalid value: a manifest
2961
2976
  // entry of "" / 0 / false / null is a broken capability, not an unknown
2962
2977
  // command (it is listed in cmds).
@@ -3391,6 +3406,13 @@ function serverRouteCmd() {
3391
3406
  // blame` pointing at the commit that last changed each command.
3392
3407
  const TYPED_CLI_FAILURES = new Set(["unsafe-config-key", "unsafe-config-value"]);
3393
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); }
3394
3416
  if (flag("server") !== undefined && ["spawn", "retire", "status", "session", "okf"].includes(cmd)) serverRouteCmd();
3395
3417
  else if (cmd === "server") serverCmd();
3396
3418
  else if (cmd === "doctor") {
@@ -3435,7 +3457,29 @@ else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && capabilityComma
3435
3457
  // text must NOT contaminate stdout — still one envelope object, nonzero exit.
3436
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`);
3437
3459
  else {
3438
- console.log(`oats — Open Agent Team Specification
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
3439
3483
 
3440
3484
  Usage:
3441
3485
  oats version [--json] kernel version; --json emits the
@@ -3588,8 +3632,7 @@ The turn record (core — every conversation captured, searchable, replicated):
3588
3632
  oats <namespace> <command> [args…] run an operational command only when its
3589
3633
  capability is active (e.g. oats okf harvest)
3590
3634
 
3591
- Layers: ${LAYERS.join(", ")}. Level detection: ~ → laptop, .git → repo, else workspace.`);
3592
- process.exit(cmd && !HELP_WORDS.has(cmd) ? 1 : 0);
3635
+ Layers: ${LAYERS.join(", ")}. Level detection: ~ → laptop, .git → repo, else workspace.`;
3593
3636
  }
3594
3637
  } catch (e) {
3595
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
- raw = parseSecretJson(run(["aw", "team", "join", inv.token, "--name", instance, "--json"], home, 45000, { secrets: [inv.token], secretSafe: true }), "aw team join");
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
- const meta = JSON.parse(process.env.OATS_META || "{}");
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "capability": "oats.aweb",
3
3
  "command": "aweb",
4
- "version": "1.10.1",
4
+ "version": "1.10.3",
5
5
  "compatibility": {
6
6
  "oats": ">=0.22.3"
7
7
  },
@@ -167,6 +167,26 @@ start a second harvester while the first one's home exists. The check uses the
167
167
  existing prepared watermark file and does not treat a successful spawn as
168
168
  completed learning.
169
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
+
170
190
  ## oats.aweb settings (1.10.0)
171
191
 
172
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
- Planning record, 2026-09-05. Juan asked lead to discuss the migration with
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.
@@ -33,24 +33,75 @@ Every remembering role must have a tested learning path. Preserve each role's
33
33
  explicit policy: Cjr reviewers exclude accumulated memory; Themis uses
34
34
  reviewed learning. Config discovery alone establishes none of this.
35
35
 
36
- The installed CLI baseline is published OATS 0.22.4 on this Mac and `aweb-agents`,
36
+ The installed CLI baseline is published OATS 0.22.5 on this Mac and `aweb-agents`,
37
37
  including native Pi/Claude/Codex, tmux/Herdr, shared `yolo`, remote Desktop
38
38
  roster/actions, retained-authority binding and corrected deferred retirement.
39
- The installed Mac Desktop 0.22.4 passed published ZIP checksum, strict deep
40
- codesign, packaged renderer and PTY launch checks; the previous 0.22.3 app
41
- is preserved for rollback. Official oats.okf 1.5.0 provides record-fed harvesting; each
42
- deployment must select its authenticated provider. Version 1.5.1, adding
43
- harvest-runtime selection and detection of unadvanced record plans, remains
44
- under independent review and is not yet published.
45
-
46
- No standing seat has transferred yet. Cjr's workers have landed reviewed
47
- code and knowledge. Two ordinary cycles on published 0.22.3 completed automatic
48
- harvester retirement, independent knowledge review, integration and home-only
49
- worker retirement. A successor read promoted indexed knowledge at startup and
50
- identified how it shaped its implementation; both acceptance checks passed.
51
- The reviewed aweb broker candidate passed
52
- real harness delivery; installation of its published release as the normal
53
- host service remains a prerequisite for standing cutover.
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.
54
105
 
55
106
  ## Scope inventory
56
107
 
@@ -63,14 +114,14 @@ of continuing seats.
63
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 |
64
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 |
65
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 |
66
- | `~/tsm` | Five live seats: Zeus, Prometeo, Argos, Themis on Claude; Hermes on Codex. Themis and Argos config/souls integrated, official capabilities installed and trusted | Zeus prepared the five-seat handover plan; begin with Themis at a safe boundary, Zeus last; preserve session-local schedules and production authority |
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 |
67
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 |
68
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 |
69
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 |
70
121
  | `~/awebai/demo-aweb/bob` | Live Pi demo | Aweb owns safe stop and archival disposition; it is not an operating-team migration |
71
- | `~/.turn-record` | Live Pi capture service under launchd | Retain as infrastructure; qualify record capture separately from standing seats |
122
+ | `~/.turn-record` | Capture and experimental mind services paused after memory incident | Preserve records; review and measure resource fixes before resuming |
72
123
 
73
- The live inventory above was checked on 2026-09-05 using harness process
124
+ The starting inventory above was checked on 2026-09-05 using harness process
74
125
  working directories and exact custom tmux sockets, without interrupting them.
75
126
  TSM uses its aweb tmux socket, BeadHub the awebai socket, and Docflow the
76
127
  main socket. Lead delivered explicitly attributed coordination messages to
@@ -143,17 +194,18 @@ stops the runtime before releasing capabilities; status remains read-only.
143
194
  Cjr archived its local harvester override and uses official oats.okf 1.5.0.
144
195
  Its authenticated Pi model is `openai-codex/gpt-5.5`. The remote qualification
145
196
  host's equivalent provider login fails refresh with `invalid_refresh_token`;
146
- spawning that harvester is not successful learning. Both failed test sessions
147
- were retired normally. Do not copy rotating login tokens from another host.
148
- An explicit `harvest-runtime` setting is planned in oats.okf 1.5.1 so an
149
- already authenticated Claude or Codex runtime can do the same work.
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.
150
202
 
151
203
  ### Finish temporary identity retirement
152
204
 
153
205
  The aweb owner must resolve the remote lifecycle defect tracked under
154
206
  `aweb-aaum.6`; oats coordinates package integration. The leaked release identities are a reproduction; reconcile the exact
155
207
  owner-side list before naming or deleting them. Alias-release fixes are in
156
- aweb source; production same-alias join/delete/rejoin acceptance is pending. Independently verify coordination cleanup,
208
+ aweb source; production same-alias join/delete/rejoin acceptance passed on published aw 1.36.1. Independently verify coordination cleanup,
157
209
  claims and certificate state. Admin cleanup is a recovery procedure, not
158
210
  proof of automatic retirement. This gates temporary-worker completion;
159
211
  adopted standing executions instead must preserve their durable identity.
@@ -245,7 +297,7 @@ substitute a green `doctor`, a roster row or a successful hook report for
245
297
  the corresponding live check. Keep credentials and private case data out of
246
298
  the shared rollout record.
247
299
 
248
- ## Latest operating evidence (2026-09-05)
300
+ ## Earlier operating evidence (2026-09-05)
249
301
 
250
302
  - Cjr's pilot landed five useful task commits and eleven promoted concepts
251
303
  from four harvests, with independent code and knowledge reviews. Ordinary
@@ -262,8 +314,10 @@ the shared rollout record.
262
314
  authorizes completing the migration while he is away; the earlier
263
315
  presence-only pause does not override it. Actual job and production boundaries
264
316
  remain. Themis's ten-commit packet landed at `46cf2083`; its installed
265
- oats.aweb 1.10.1 and oats.okf 1.5.0 passed the actual doctor. The other three
266
- specialist briefs are being prepared at their work checkpoints.
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.
267
321
  - BeadHub configuration/soul commit `3ee13a8` in `awebai/beadhub-saas`
268
322
  (`~/awebai/beadhub/beadhub-saas`) was independently ACKed by lead and
269
323
  landed on `main`. Its tracked deployment template is materialized at the canonical
@@ -286,9 +340,9 @@ the shared rollout record.
286
340
  relying on those routes; do not silently replace retained identities.
287
341
  - Real remote Claude launch, terminal input and detach survival passed.
288
342
  Remote Desktop projection and exact-home lifecycle shipped in 0.22.3; the
289
- installed remote CLI serves the real registered roster. Remote harvester
290
- completion remains blocked by provider authentication, and no standing
291
- 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.
292
346
  - The aweb broker candidate `30469e22` ran as a private launchd service against
293
347
  published OATS 0.22.3. Real Claude and Codex sessions in tmux and Pi
294
348
  in Herdr, with Claude channel 1.7.9 and Pi extension 0.3.10, fetched mail and replied with exact qualification tokens,
@@ -325,7 +379,7 @@ the shared rollout record.
325
379
  claim a knowledge promotion.
326
380
  - Docflow's two-commit preparation at `4458097` is independently ACKed: native
327
381
  Claude, retained authority, session delivery and 18 valid curated OKF concepts.
328
- Its owner is integrating and acquiring official packages. The running backfill
382
+ Its role and package preparation is integrated. The running backfill
329
383
  and FY2025 register checks still determine its activation boundary.
330
384
  - The tracked aweb coordinator soul at `0a3a9a91` is independently ACKed; the
331
385
  frontend soul at `b3985edb` is owner-reviewed and landed. They preserve the
@@ -337,8 +391,13 @@ the shared rollout record.
337
391
  installers. CI retried once after a disappearing Git maintenance lock in a
338
392
  fixture; kernel and Desktop gates then passed. A manual reviewed version-bump
339
393
  PR completed the bot's permission-blocked post-publication step. Published
340
- npm JavaScript bytes match the tag. Aweb 1.36.1 publication is still pending
341
- a gateway build dependency download; the private candidate broker is not
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
342
401
  being represented as the permanent published service.
343
402
 
344
403
  - Cjr's retained declaration/runbook packet is independently ACKed through
@@ -350,12 +409,35 @@ the shared rollout record.
350
409
  - TSM's Argos packet is integrated at `f20c54bb` after both independent and
351
410
  owner ACKs; actual doctor resolves the two retained reviewer seats. BeadHub
352
411
  and Themis supplied final private startup briefings and idle checkpoints.
353
- Hermes and Prometeo were explicitly woken to read unseen followups and prepare
354
- their continuation briefs; their legacy channels had not delivered those
355
- requests reliably.
356
- - A locked-selector update gap found during actual team setup is corrected
357
- on main at `d95018e` (two independently reviewed commits). The next kernel
358
- patch supports `oats update <package> --to <ref>` or a positional catalog
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
359
418
  spec. A hermetic CLI test exercises the version transition, trust reset and
360
419
  invalid arguments, including refusal to enter the kernel self-updater when
361
- a package is missing. This change is not yet part of installed 0.22.4.
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.
@@ -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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "oats.aweb": {
9
9
  "url": "https://github.com/awebai/oats-aweb.git",
10
- "ref": "v1.10.1",
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.5",
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
- if (out.appended > 0 && !args["no-index"]) {
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
- A dropped hook is recovered by any later pass (capture, or capture --watch).`);
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
- for (const { format, dir } of dirs.values()) {
249
- appended += captureSessions(store, { owner, roots: [dir], format, ignore }).appended;
250
- }
251
- if (appended > 0 && !args["no-index"]) {
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
+ }