@awebai/oats 0.22.3 → 0.22.5

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
@@ -2206,8 +2206,20 @@ function migrateCmd() {
2206
2206
  /** oats update <package> — transactional package update with diff + trust reset. */
2207
2207
  function updatePackageCmd(id) {
2208
2208
  const dir = dirFlag();
2209
+ // --to <selector>: move a catalog-sourced lock to another catalog ref
2210
+ // (tag) through the same transactional update. A lock with an explicit
2211
+ // selector keeps it on a plain update by design; this is the operator's
2212
+ // way to advance it without remove + reinstall.
2213
+ // Two spellings: `oats update <id> <id>@<selector>` (the engine's own spec
2214
+ // form) or `oats update <id> --to <selector>`.
2215
+ const to = flag("to");
2216
+ if (to === true) { cmdFail("E_BAD_ARGS", "--to needs a catalog selector, e.g. --to v1.10.1"); return; }
2217
+ 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; }
2218
+ const positional = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
2219
+ if (positional && to !== undefined) { cmdFail("E_BAD_ARGS", "give either <id>@<selector> or --to <selector>, not both"); return; }
2220
+ const spec = positional || (to !== undefined ? `${id}@${to}` : undefined);
2209
2221
  let r;
2210
- try { r = updatePackage(dir, id); } catch (e) { cmdFail(e.code || "invalid-lock", e.message || e); return; }
2222
+ try { r = updatePackage(dir, id, spec ? { spec } : {}); } catch (e) { cmdFail(e.code || "invalid-lock", e.message || e); return; }
2211
2223
  if (JSON_MODE) { jsonOk(r); return; }
2212
2224
  // A moved package root is reported even when the bytes are identical: the
2213
2225
  // lock now points somewhere else in the repository, and that is exactly the
@@ -3386,7 +3398,13 @@ else if (cmd === "doctor") {
3386
3398
  args.includes("--json") ? doctorJson(doctorDir) : doctor(doctorDir);
3387
3399
  }
3388
3400
  else if (cmd === "use") use();
3389
- else if (cmd === "update") { const t = args[1] && !args[1].startsWith("--") ? args[1] : undefined; t ? updatePackageCmd(t) : updateCmd(); }
3401
+ else if (cmd === "update") {
3402
+ const t = args[1] && !args[1].startsWith("--") ? args[1] : undefined;
3403
+ // A selector without a package must never fall through to the kernel
3404
+ // self-update (a different product) with the flag silently ignored.
3405
+ 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); }
3406
+ t ? updatePackageCmd(t) : updateCmd();
3407
+ }
3390
3408
  else if (cmd === "type") typeCmd();
3391
3409
  else if (cmd === "inject") injectCmd();
3392
3410
  else if (cmd === "install") install();
@@ -3488,9 +3506,10 @@ Usage:
3488
3506
  report under error.details)
3489
3507
  oats list [--dir <d>] [--json] installed packages, exported capabilities,
3490
3508
  scopes, trust state
3491
- oats update <package> [--dir <d>] transactional package update: temp fetch,
3492
- closure validation, diff, lock replace,
3493
- all capability approvals invalidated
3509
+ oats update <package> [<package>@<ref>] transactional package update: temp fetch,
3510
+ [--to <ref>] [--dir <d>] closure validation, diff, lock replace,
3511
+ all capability approvals invalidated; a
3512
+ spec or --to moves a catalog lock to <ref>
3494
3513
  oats remove <package> [--dir <d>] remove a package (refuses while config or
3495
3514
  dependent packages reference it)
3496
3515
  oats migrate [--dry-run] [--dir <d>] map this scope's v1 capability locks to
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "capability": "oats.aweb",
3
3
  "command": "aweb",
4
- "version": "1.10.0",
4
+ "version": "1.10.1",
5
5
  "compatibility": {
6
6
  "oats": ">=0.22.3"
7
7
  },
@@ -42,6 +42,18 @@
42
42
  "why": "an ambient aweb pi extension, if one is installed, must honour AWEB_DELIVERY=session (0.3.10 and later); an older one would open a second event stream beside the wake broker; no extension at all is fine",
43
43
  "install": "pi install npm:@awebai/pi@latest",
44
44
  "ifInstalled": true
45
+ },
46
+ {
47
+ "runtime": "claude",
48
+ "package": "aweb-channel@awebai-marketplace",
49
+ "marketplace": "awebai/claude-plugins",
50
+ "minVersion": "1.7.9",
51
+ "when": {
52
+ "delivery": "session"
53
+ },
54
+ "why": "an ambient aweb-channel plugin for Claude Code, if one is installed, must honour AWEB_DELIVERY=session (1.7.9 and later); an older one starts its own channel server beside the wake broker and can fetch and acknowledge a message before the broker delivers it; no plugin at all is fine",
55
+ "install": "update the aweb-channel plugin from the awebai marketplace in Claude Code",
56
+ "ifInstalled": true
45
57
  }
46
58
  ],
47
59
  "skills": [
@@ -2,5 +2,4 @@ name: memory-harvest
2
2
  kind: capability
3
3
  work: attached
4
4
  runtime: pi
5
- model: github-copilot/gpt-5.5
6
5
  description: Ephemeral OKF service agent that promotes one live instance's pending notes into its soul.
@@ -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
- writeFileSync(nextPath, JSON.stringify(next, null, 2) + "\n");
152
- return { threads, watermarkPath, nextPath, watermark: next, unattributed: (report.unattributed || []).length, problems };
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 harvestSpawnArgs({ slug, parent, repo, work, workDir, branch, model }) {
202
- const args = ["--purpose", slug, "--parent", parent, "--repo", repo, "--work", work];
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, model: harvestModel,
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}`, model: harvestModel,
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, model: harvestModel,
438
+ slug, parent: inst, repo: context, work: "attached", workDir, ...execution,
414
439
  }), task);
415
440
  }
416
- if (JSON_MODE) jsonOk({ harvest: "spawned", instance: r.instance, window: r.tmux?.window || null, ...(recordPlan ? { record: { threads: recordPlan.threads.map((t) => t.thread), ...(recordPlan.unattributed ? { unattributed: recordPlan.unattributed } : {}), ...(recordPlan.problems?.length ? { problems: recordPlan.problems } : {}) } } : {}) });
417
- out({ meta: { harvestSpawn: r.instance, window: r.tmux?.window } });
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.0",
5
- "compatibility": { "oats": ">=0.22.2" },
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
@@ -140,6 +140,33 @@ 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
+
143
170
  ## oats.aweb settings (1.10.0)
144
171
 
145
172
  Set with `oats use oats.aweb --settings <key>=<value>` at a scope, or per
@@ -148,8 +175,11 @@ soul through the binding's `settings:` map.
148
175
  - `delivery: channel | session` (default `channel`). `session` hands
149
176
  notification delivery to the host wake broker: `AWEB_DELIVERY=session` in
150
177
  the launch environment (declared by the manifest), no Claude channel flag,
151
- a pi extension that honours the opt-out (`@awebai/pi` 0.3.10 or later,
152
- enforced as a conditional requirement with a version floor), and a briefing
178
+ a pi extension that honours the opt-out (`@awebai/pi` 0.3.10 or later) and
179
+ a Claude Code channel plugin that does too (`aweb-channel` 1.7.9 or later),
180
+ both enforced as conditional requirements with a version floor when
181
+ installed (an older plugin starts its own channel server beside the broker
182
+ and can fetch a message before the broker delivers it), and a briefing
153
183
  and registration of the home with the host wake broker (`aw wake register`).
154
184
  Until an aw that ships `aw wake` exists (aweb-abil), session mode REFUSES
155
185
  to spawn rather than leave an instance that nothing wakes: it is for broker
@@ -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 specialists and reviewers. Merlin retains
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.
@@ -27,21 +29,28 @@ Every continuing seat must have a supported OATS launch, composition,
27
29
  status, handover and retirement path. Its outstanding work, knowledge and
28
30
  required skills must survive a change of runtime session; identity/address
29
31
  continuity follows the explicit policy for that seat.
30
- Every remembering role must have a tested learning path; reviewers retain
31
- their explicit exclusion from accumulated memory. Config discovery alone
32
- establishes none of this.
33
-
34
- The installed baseline is OATS 0.22.2, including native Pi/Claude/Codex,
35
- tmux/Herdr, shared `yolo`, remote CLI launch/terminals and deferred retirement.
36
- The Mac Desktop app is installed and passed packaged renderer/PTY launch checks.
37
- Official oats.okf 1.5.0 provides record-fed harvesting. A source candidate for
38
- 0.22.3 adds the retained-authority binding, remote Desktop roster/actions and
39
- retirement corrections; that candidate is not yet a published release.
40
-
41
- No standing seat has transferred yet. Cjr's worker pilot has landed reviewed
42
- code and knowledge; ordinary retirement passed using the next-patch candidate.
43
- A real harvester's automatic deferred completion, successor knowledge use and
44
- session-broker delivery remain explicit acceptance checks.
32
+ Every remembering role must have a tested learning path. Preserve each role's
33
+ explicit policy: Cjr reviewers exclude accumulated memory; Themis uses
34
+ reviewed learning. Config discovery alone establishes none of this.
35
+
36
+ The installed CLI baseline is published OATS 0.22.4 on this Mac and `aweb-agents`,
37
+ including native Pi/Claude/Codex, tmux/Herdr, shared `yolo`, remote Desktop
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.
45
54
 
46
55
  ## Scope inventory
47
56
 
@@ -51,13 +60,13 @@ of continuing seats.
51
60
 
52
61
  | Scope | Starting point | Required disposition |
53
62
  | --- | --- | --- |
54
- | `~/awebai/oats` | Live Claude coordinator and Codex lead; managed review workers also running | Oats owns coordinator handover; lead owns lead handover; preserve established identities |
55
- | `~/cjr` | Preparation `5afb3e8b`; developer pilot landed on master `062e2c75`; legacy Merlin and Minerva live | Merlin owns safe handovers; preserve his DID/address; prove automatic harvest completion and successor use |
56
- | `~/awebai/aweb` | Live Claude coordinator and frontend in legacy homes | Oats owns coordinator handover; lead coordinates frontend with aweb after its current work; preserve identities and cover child repositories |
57
- | `~/tsm` | Five live seats: Zeus, Prometeo, Argos, Themis on Claude; Hermes on Codex. No OATS config/souls found | Zeus prepared the five-seat handover plan; begin with Themis at a safe boundary, Zeus last; preserve session-local schedules and production authority |
63
+ | `~/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
+ | `~/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
+ | `~/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 |
58
67
  | `~/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 |
59
68
  | `~/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 |
60
- | `ai.aweb` on `aweb-agents` | Athena last seen 53 days ago; remote legacy home exists in inventory | Aweb and oats own archival inspection; do not resurrect as a continuing seat |
69
+ | `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 |
61
70
  | `~/awebai/demo-aweb/bob` | Live Pi demo | Aweb owns safe stop and archival disposition; it is not an operating-team migration |
62
71
  | `~/.turn-record` | Live Pi capture service under launchd | Retain as infrastructure; qualify record capture separately from standing seats |
63
72
 
@@ -101,7 +110,9 @@ uniquely named so no existing address has to be removed for the experiment.
101
110
 
102
111
  Merlin retains his identity; other continuing seats follow their accepted
103
112
  policy. Oats implemented explicit source-authority binding in oats.aweb 1.10.0,
104
- reviewed and pinned for the next kernel patch. A disposable rehearsal verified
113
+ reviewed and shipped with OATS 0.22.3. Published oats.aweb 1.10.1 additionally
114
+ requires an installed ambient Claude channel to be at least 1.7.9 for session
115
+ delivery, matching the existing Pi extension floor of 0.3.10. A disposable rehearsal verified
105
116
  stable identity/address, existing conversations, heartbeat, exclusive holder
106
117
  refusal, rollback and authority-preserving retirement. Aweb supplied this
107
118
  supported handover:
@@ -247,11 +258,21 @@ the shared rollout record.
247
258
  - TSM's owner plan is `~/tsm/history/2026-09-06-tsm-oats-handover-plan.md`.
248
259
  Preserve the five seats' worktrees, skills and knowledge; re-arm Zeus's
249
260
  session-local schedules. Production credentials remain solely with the
250
- authorized production operator. His plan reserves Zeus's final cutover
251
- for Juan's presence; the other seats can prepare in the meantime.
252
- - BeadHub supplied its retained-identity and source-root brief through aweb
253
- mail. It awaits the reviewed declarative binding and cutover recipe.
254
- Its Stripe-account dependency and production cutover gates remain separate.
261
+ authorized production operator. Zeus remains last. Juan's current direction
262
+ authorizes completing the migration while he is away; the earlier
263
+ presence-only pause does not override it. Actual job and production boundaries
264
+ 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.
267
+ - BeadHub configuration/soul commit `3ee13a8` in `awebai/beadhub-saas`
268
+ (`~/awebai/beadhub/beadhub-saas`) was independently ACKed by lead and
269
+ landed on `main`. Its tracked deployment template is materialized at the canonical
270
+ parent workspace, with trusted official capabilities and a clean actual
271
+ doctor result. The soul retains SaaS Git provenance and uses workspace mode
272
+ as a coordinator across the three canonical repositories. Its legacy holder
273
+ remains live; authenticated harvest-model configuration is installed and
274
+ verified. The published broker service precedes activation. Stripe and
275
+ production gates remain separate.
255
276
  - Docflow supplied its own identity and handover through its terminal. Its
256
277
  backfill and register verification define the safe boundary. Preserve its
257
278
  Claude memory and existing credentials in place; verify filesystem/TCC
@@ -264,6 +285,77 @@ the shared rollout record.
264
285
  the cross-team coordinator identity/contact policy explicitly before
265
286
  relying on those routes; do not silently replace retained identities.
266
287
  - Real remote Claude launch, terminal input and detach survival passed.
267
- Remote Desktop projection and exact-home lifecycle are under independent
268
- review for the next patch. Remote harvester completion remains blocked by
269
- provider authentication, and no standing remote seat is declared migrated.
288
+ 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.
292
+ - The aweb broker candidate `30469e22` ran as a private launchd service against
293
+ published OATS 0.22.3. Real Claude and Codex sessions in tmux and Pi
294
+ in Herdr, with Claude channel 1.7.9 and Pi extension 0.3.10, fetched mail and replied with exact qualification tokens,
295
+ with all GUI viewers closed. Pi also answered a sender-waiting chat. Mail
296
+ sent while the broker was stopped was recovered after restart. After the
297
+ Codex harness stopped, its registration became inactive and a subsequent
298
+ message remained unread. Pause also survived a daemon restart, keeping mail
299
+ unread until resume, after which the agent replied. The candidate does not
300
+ fetch or acknowledge mail.
301
+ Published aw installation and standing-seat acceptance are still separate.
302
+ - Qualification found that tmux output loses tab separators in a minimal
303
+ launchd environment without a UTF-8 locale. Independently reviewed kernel
304
+ fixes `a0b0e14` and `0260ea9` shipped in 0.22.4, adding UTF-8 mode to
305
+ lifecycle/input and viewer calls. The host service also specifies
306
+ `LC_ALL=en_US.UTF-8`. Existing Claude plugin 1.7.8 also consumed mail before
307
+ the broker wake; updating to 1.7.9 eliminated that competing delivery path.
308
+ The deployed oats.aweb 1.10.1 floor now rejects the incompatible version.
309
+
310
+ - Cjr's second and third worker cycles completed on published kernel 0.22.3
311
+ and oats.okf 1.4.1 (note-fed). The
312
+ second promoted three reviewed concepts; the third read them before working,
313
+ produced the normal host-service health reader (`3dfe3acf`) and completed
314
+ its own reviewed harvest. Both harvesters self-retired without operator
315
+ completion. Each worker's ordinary retirement took four seconds and removed
316
+ its temporary alias. A post-merge fixture mismatch was corrected forward in
317
+ `e8a87c71`, with 47 tests green on a clean export. The reader's real service
318
+ check awaits installation of `ai.aweb.wake`.
319
+ - A record-only harvest of the fresh Claude wake-test session completed with
320
+ official oats.okf 1.5.0. It read its 27-turn source window, found no durable
321
+ lesson to promote, advanced the completed watermark and self-retired. The
322
+ first attempt exposed an unnecessary SQLite index write during pure journal
323
+ reads; reviewed fix `f100320` shipped in 0.22.4. The shared capture service
324
+ and its large index were preserved. Completion is proven; this run does not
325
+ claim a knowledge promotion.
326
+ - Docflow's two-commit preparation at `4458097` is independently ACKed: native
327
+ Claude, retained authority, session delivery and 18 valid curated OKF concepts.
328
+ Its owner is integrating and acquiring official packages. The running backfill
329
+ and FY2025 register checks still determine its activation boundary.
330
+ - The tracked aweb coordinator soul at `0a3a9a91` is independently ACKed; the
331
+ frontend soul at `b3985edb` is owner-reviewed and landed. They preserve the
332
+ coordinator's workspace and the frontend's managed primary SaaS worktree plus
333
+ explicitly assigned secondary OSS tree. Lead and Oats coordinator souls are
334
+ also reviewed and landed. Concrete deployment configuration and final
335
+ checkpoints precede each launch.
336
+ - OATS 0.22.4 is published at tag `0260ea9`, with npm packages and six Desktop
337
+ installers. CI retried once after a disappearing Git maintenance lock in a
338
+ fixture; kernel and Desktop gates then passed. A manual reviewed version-bump
339
+ 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
342
+ being represented as the permanent published service.
343
+
344
+ - Cjr's retained declaration/runbook packet is independently ACKed through
345
+ `6dc6efc8` (three commits); its source authority remains in place and operator
346
+ stop-before-start applies to rollback as well as cutover. Its two soul startup
347
+ corrections also landed after independent review at `bd981b3a`. Official
348
+ oats.aweb 1.10.1 and oats.okf 1.5.0 are now installed and trusted for both
349
+ standing souls. These newer pins do not relabel the earlier worker proofs.
350
+ - TSM's Argos packet is integrated at `f20c54bb` after both independent and
351
+ owner ACKs; actual doctor resolves the two retained reviewer seats. BeadHub
352
+ 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
359
+ spec. A hermetic CLI test exercises the version transition, trust reset and
360
+ 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.
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. See
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,40 @@
1
+ # OATS v0.22.4
2
+
3
+ Two fixes found by running the wake broker and the record-fed harvester as
4
+ real host services, the oats.aweb 1.10.1 floor, and the managed lead soul.
5
+
6
+ ## Session commands under a service locale
7
+
8
+ launchd starts services without a UTF-8 locale, and tmux then replaces the
9
+ tab delimiters in its list-panes output with underscores, so every session
10
+ inspection from the wake broker reported an invalid pane. The shared session
11
+ helper now runs tmux in UTF-8 mode (`-u`), which preserves the delimiters
12
+ regardless of the service locale and makes typed input UTF-8-safe.
13
+
14
+ ## Record windows without the search index
15
+
16
+ `recall --thread --json` used to open the record's SQLite search index just
17
+ to extract turn text, and a capture indexing a large session cache could hold
18
+ that database locked for longer than the harvester waits. Window reads now
19
+ come from the journals alone; the search index opens only for indexed
20
+ search, show and reindex.
21
+
22
+ ## Bundled: oats.aweb 1.10.1
23
+
24
+ Session-mode delivery now requires an installed Claude Code channel plugin
25
+ to be `aweb-channel` 1.7.9 or later (as it already required `@awebai/pi`
26
+ 0.3.10): an older plugin starts its own channel server beside the wake
27
+ broker and can fetch a message before the broker delivers it.
28
+
29
+ ## Managed operating-lead soul
30
+
31
+ `agents/lead/soul` is tracked: the operating lead's role instructions and
32
+ two knowledge notes (runtime and messaging ownership; where rollout state
33
+ lives), ready for a retained-identity binding at its cutover.
34
+
35
+ ## Also
36
+
37
+ - `docs/operating-team-migration.md` records the published 0.22.3 rollout and
38
+ the live wake-broker acceptance on Claude, Codex and Pi.
39
+ - oats.okf 1.5.1 (harvest-runtime, promotion exclusions, replan detector) is
40
+ not in this release; it ships when its independent review completes.
@@ -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.
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);
@@ -6,7 +6,9 @@ import { inspectHerdr, inputHerdr, herdrCommand } from "./herdr.mjs";
6
6
 
7
7
  const shells = new Set(["sh", "bash", "zsh", "fish", "dash", "ksh", "login"]);
8
8
  function tmux(target, args, { exec = execFileSync, input } = {}) {
9
- return exec("tmux", ["-S", target.socket, ...args], {
9
+ // launchd does not supply a UTF-8 locale. In that environment tmux replaces
10
+ // the tab delimiters in list-panes output with underscores unless forced.
11
+ return exec("tmux", ["-u", "-S", target.socket, ...args], {
10
12
  encoding: "utf8", input, timeout: 10000, maxBuffer: 1024 * 1024,
11
13
  stdio: ["pipe", "pipe", "pipe"],
12
14
  });
@@ -10,7 +10,9 @@ export function prepareSessionViewer(target, { exec = execFileSync } = {}) {
10
10
  delete env.HERDR_SESSION;
11
11
  return { binary: target.binary, args: ["terminal", "attach", target.terminalId], env, cleanup() {} };
12
12
  }
13
- const run = (args) => exec("tmux", ["-S", target.socket, ...args], {
13
+ // UTF-8 mode (-u) like the shared session helper: a service without a
14
+ // UTF-8 locale (launchd) otherwise mangles tmux output.
15
+ const run = (args) => exec("tmux", ["-u", "-S", target.socket, ...args], {
14
16
  encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "pipe"],
15
17
  }).trim();
16
18
  const viewer = `oatsview-${process.pid}-${randomUUID().slice(0, 8)}`;
@@ -28,7 +30,7 @@ export function prepareSessionViewer(target, { exec = execFileSync } = {}) {
28
30
  run(["bind-key", "-T", "oatsview-locked", "WheelUpPane", "if-shell", "-F", "#{||:#{pane_in_mode},#{mouse_any_flag}}", "send-keys -M", "copy-mode -e; send-keys -M"]);
29
31
  run(["set-option", "-t", viewer, "mouse", "on"]);
30
32
  const env = { ...process.env }; delete env.TMUX;
31
- return { binary: "tmux", args: ["-S", target.socket, "attach-session", "-t", `=${viewer}`], env, cleanup };
33
+ return { binary: "tmux", args: ["-u", "-S", target.socket, "attach-session", "-t", `=${viewer}`], env, cleanup };
32
34
  } catch (e) { cleanup(); throw e; }
33
35
  }
34
36
 
@@ -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.0",
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.0",
10
+ "ref": "v1.10.1",
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",
3
+ "version": "0.22.5",
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",
@@ -23,7 +23,7 @@ import { join } from "node:path";
23
23
  import process from "node:process";
24
24
 
25
25
  import { RecordStore } from "../lib/store.mjs";
26
- import { RecordIndex } from "../lib/index-db.mjs";
26
+ import { extractTurnText, RecordIndex } from "../lib/index-db.mjs";
27
27
 
28
28
  function parseArgs(argv) {
29
29
  const args = { _: [] };
@@ -40,16 +40,19 @@ function parseArgs(argv) {
40
40
  const args = parseArgs(process.argv.slice(2));
41
41
  const root = args.root ?? process.env.TURN_RECORD_ROOT ?? join(homedir(), ".turn-record");
42
42
  const store = new RecordStore(root, {});
43
- const index = new RecordIndex(store);
43
+ let index;
44
+ const getIndex = () => (index ??= new RecordIndex(store));
44
45
 
45
46
  try {
46
47
  if (args.reindex) {
48
+ const index = getIndex();
47
49
  index.rebuild();
48
50
  console.log(JSON.stringify(index.counts()));
49
51
  process.exit(0);
50
52
  }
51
53
 
52
54
  if (args.show) {
55
+ const index = getIndex();
53
56
  const { resolveTurn } = await import("../lib/index-db.mjs");
54
57
  const byId = store.readAll();
55
58
  const turn = resolveTurn(store, index, args.show, byId);
@@ -111,7 +114,7 @@ try {
111
114
  // A window is a bounded read: a consumer that plans one (the OKF
112
115
  // harvester) sizes it with --ids-only first, then reads exactly that.
113
116
  const out = window.map((t) => {
114
- const docs = index.extractText(t);
117
+ const docs = extractTurnText(t);
115
118
  const base = { id: t.id, ts: t.ts, thread: t.thread, kind: t.kind, source: t.provenance?.source ?? null };
116
119
  const full = { ...base, text: docs.map((d) => ({ role: d.role, text: d.text })) };
117
120
  // bytes = what this turn occupies in the pretty-printed --json answer,
@@ -123,6 +126,7 @@ try {
123
126
  process.exit(0);
124
127
  }
125
128
 
129
+ const index = getIndex();
126
130
  const limit = args.limit ? Number(args.limit) : 20;
127
131
  let rows;
128
132
  if (query) {
@@ -164,5 +168,5 @@ try {
164
168
  console.log(` ${r.id}`);
165
169
  }
166
170
  } finally {
167
- index.close();
171
+ index?.close();
168
172
  }
@@ -413,20 +413,7 @@ export class RecordIndex {
413
413
  // Text documents for one turn. Mail/chat: one doc. Session: one doc per
414
414
  // conversational event extracted from the transcript blob.
415
415
  extractText(turn) {
416
- if (turn.kind === "note" && turn.body?.segment) return []; // dedicated role "segment" row
417
- if (turn.kind === "mail" || turn.kind === "chat" || turn.kind === "note") {
418
- const subject = turn.body?.subject ?? "";
419
- const text = turn.body?.text ?? "";
420
- const joined = subject ? subject + "\n" + text : text;
421
- return joined.trim() ? [{ loc: "", role: turn.kind, text: joined }] : [];
422
- }
423
- if (turn.kind === "session" && typeof turn.body?.line === "string") {
424
- const loc = `line:${turn.provenance?.origin?.line ?? 0}`;
425
- return extractSessionTextFor(turn.provenance?.source, Buffer.from(turn.body.line, "utf8")).map(
426
- (d) => ({ ...d, loc }),
427
- );
428
- }
429
- return [];
416
+ return extractTurnText(turn);
430
417
  }
431
418
 
432
419
  // FTS query with optional filters. Returns rows with turn metadata and a
@@ -502,6 +489,24 @@ export class RecordIndex {
502
489
  }
503
490
  }
504
491
 
492
+ // Journal text extraction is independent of the derived search database.
493
+ export function extractTurnText(turn) {
494
+ if (turn.kind === "note" && turn.body?.segment) return []; // dedicated role "segment" row
495
+ if (turn.kind === "mail" || turn.kind === "chat" || turn.kind === "note") {
496
+ const subject = turn.body?.subject ?? "";
497
+ const text = turn.body?.text ?? "";
498
+ const joined = subject ? subject + "\n" + text : text;
499
+ return joined.trim() ? [{ loc: "", role: turn.kind, text: joined }] : [];
500
+ }
501
+ if (turn.kind === "session" && typeof turn.body?.line === "string") {
502
+ const loc = `line:${turn.provenance?.origin?.line ?? 0}`;
503
+ return extractSessionTextFor(turn.provenance?.source, Buffer.from(turn.body.line, "utf8")).map(
504
+ (d) => ({ ...d, loc }),
505
+ );
506
+ }
507
+ return [];
508
+ }
509
+
505
510
  // Claude Code text extraction, re-exported for compatibility; the
506
511
  // per-format extractors live in formats.mjs.
507
512
  export const extractSessionText = extractCcText;