@awebai/oats 0.24.7 → 0.24.8

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
@@ -30,7 +30,7 @@ import {
30
30
  approveCapability, approveAvailableCapability, updatePackage, removePackage, migrateLegacyLock, applyLegacyLockMigration,
31
31
  packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir, installedCapabilitiesDir, ownedCapabilitiesDir, loadPackageManifestAt,
32
32
  resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, planInstanceResources, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
33
- findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, findInstanceHomes, listCapabilityAgents, workspaceOf,
33
+ findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, findInstanceHomes, listCapabilityAgents, workspaceOf, stopInstanceSession, recomposeInstanceInstructions,
34
34
  ensureRoot, findRoot, findAgent, listAgents, listInstances, listAgentDefs, createAgent as coreCreateAgent,
35
35
  spawnInstance, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, upsertLocalAgent, defaultRepo, RELATIONS, validateLaunchConfig, resolveLaunchSelection, resolveLaunchExecutable, checkLaunchExecutable, missingLaunchEnvRefs, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_RUNTIMES, LAUNCH_RECIPE_VERSION, parseLaunchCommand, resolveYolo, planLaunch, redactLaunchCommand, restartInstanceSession,
36
36
  } from "../lib/core.mjs";
@@ -59,12 +59,16 @@ import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../l
59
59
  import { approveCapturedCapability } from "../lib/artifact-approvals.mjs";
60
60
  import { loadSetupExpertEdition, SETUP_EXPERT, SETUP_CAPABILITIES } from "../lib/setup-expert-source.mjs";
61
61
  import { observeInstanceGit, diffInstanceFile } from "../lib/instance-git.mjs";
62
+ import { planStop, applyStop, planRetire, resolveInstance as resolveInstanceForCli } from "../lib/instance-lifecycle.mjs";
63
+ const await_import_lifecycle = () => ({ resolveInstance: resolveInstanceForCli });
64
+ import { readinessOf, policyOf } from "../lib/readiness.mjs";
65
+ import { readEvents } from "../lib/instance-events.mjs";
62
66
  import { parsePortableSource } from "../lib/source-spec.mjs";
63
67
 
64
68
  const args = process.argv.slice(2);
65
69
  let cmd = args[0];
66
70
  const HELP_WORDS = new Set(["help", "--help", "-h"]);
67
- const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "instance", "operation", "soul", "launch-config", "experimental", "onboard", "init", "inject", "install", "list", "catalog", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
71
+ const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "instance", "operation", "readiness", "soul", "launch-config", "experimental", "onboard", "init", "inject", "install", "list", "catalog", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
68
72
  const flag = (name) => {
69
73
  const i = args.indexOf(`--${name}`);
70
74
  return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
@@ -822,7 +826,7 @@ function homeContexts(home, meta) {
822
826
  * about the soul, not a prompt to infer one. */
823
827
  function soulDeclarations(soulDir) {
824
828
  const file = join(soulDir, "soul.yaml");
825
- const empty = { declarations: { requires: null, defaults: null, knowledge: null, teams: null, resources: null }, provenance: null, problems: [] };
829
+ const empty = { declarations: { requires: null, defaults: null, knowledge: null, teams: null, resources: null, children: null }, provenance: null, problems: [] };
826
830
  if (!existsSync(file)) return empty;
827
831
  let parsed;
828
832
  try { parsed = withConfigFile(file, () => parseYamlNested(readFileSync(file, "utf8"))); }
@@ -830,7 +834,7 @@ function soulDeclarations(soulDir) {
830
834
  const section = (key) => (parsed[key] !== undefined && parsed[key] !== null && typeof parsed[key] === "object") ? parsed[key] : (parsed[key] === undefined ? null : parsed[key]);
831
835
  const provenance = section("provenance");
832
836
  return {
833
- declarations: { requires: section("requires"), defaults: section("defaults"), knowledge: section("knowledge"), teams: section("teams"), resources: section("resources") },
837
+ declarations: { requires: section("requires"), defaults: section("defaults"), knowledge: section("knowledge"), teams: section("teams"), resources: section("resources"), children: section("children") },
834
838
  provenance: provenance && typeof provenance === "object" ? {
835
839
  kind: provenance.kind ?? null, source: provenance.source ?? null, revision: provenance.revision ?? null,
836
840
  path: provenance.path ?? null, workspaceRevision: provenance.workspaceRevision ?? null,
@@ -860,8 +864,12 @@ function soulEntry(soul, root, { capability } = {}) {
860
864
  * invoking process's ambient agents-root override must not redirect them
861
865
  * to its own deployment. */
862
866
  function dropAmbientRoot() { delete process.env.PI_AGENTS_ROOT; }
863
- function inspectCmd() {
864
- const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
867
+ function inspectCmd() { const result = computeInspect(); if (!result) return; if (JSON_MODE) { const { _print, ...data } = result; jsonOk(data); return; } printInspect(result); }
868
+ /** The inspect answer as data — shared by `oats inspect` and `oats readiness`
869
+ * (K5), so the readiness quartet is derived from the SAME capability,
870
+ * activation, trust and soul facts inspect reports, never a second opinion. */
871
+ function computeInspect({ onFail } = {}) {
872
+ const bail = onFail || ((code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg)));
865
873
  dropAmbientRoot();
866
874
  const homeFlag = flag("home");
867
875
  const home = homeFlag === true ? bail("E_BAD_ARGS", "--home needs an absolute instance home") : homeFlag;
@@ -936,7 +944,7 @@ function inspectCmd() {
936
944
  const h = capabilityHealth(p.level, c, rows.capabilities[c.id], rows.packages[p.package]);
937
945
  byId.set(c.id, {
938
946
  id: c.id, package: p.package, version: c.version || null, layer: c.manifest?.layer || null, command: c.manifest?.command || null,
939
- origin: "installed", level: p.level, source: p.source || null, dir: h.dir,
947
+ origin: "installed", level: p.level, source: p.source || null, commit: p.commit ?? rows.packages[p.package]?.commit ?? null, dir: h.dir,
940
948
  health: { status: h.status, code: h.code, detail: h.detail, installed: !!c.installed, locked: true, trusted: c.trusted === true, integrity: c.integrity || null, installedIntegrity: h.integrity ?? null },
941
949
  });
942
950
  }
@@ -1051,7 +1059,12 @@ function inspectCmd() {
1051
1059
  problems: [...(lockError ? [lockError] : []), ...packagedDiagnostics.map((d) => ({ code: d.code, message: d.message, capability: d.capability })),
1052
1060
  ...(meta ? snapshotCaps.filter((c) => !mans[c.id]).map((c) => ({ code: "captured-capability-missing", message: `${c.id} was active when this home was composed but no manifest for it is acquired now`, capability: c.id })) : [])],
1053
1061
  };
1054
- if (JSON_MODE) { jsonOk(result); return; }
1062
+ result._print = { ctx, selectedSoul, home };
1063
+ return result;
1064
+ }
1065
+ function printInspect(result) {
1066
+ const { ctx, selectedSoul, home } = result._print; delete result._print;
1067
+ const { souls, layers, capabilities } = result;
1055
1068
  console.log(`oats inspect — ${shortPath(ctx)}${selectedSoul ? ` soul ${selectedSoul.name}` : ""}${home ? ` home ${shortPath(home)}` : ""}`);
1056
1069
  for (const s of souls) console.log(` soul ${s.name} [${s.kind}${s.capability ? ` ${s.capability}` : ""}] runtime ${s.runtime}${s.model ? ` model ${s.model}` : ""} work ${s.work}${s.editable.fields.length ? "" : " (read-only)"}`);
1057
1070
  for (const l of LAYERS) console.log(` layer ${l}: ${layers[l].id || (layers[l].disabled ? "disabled" : "none")}${layers[l].provenance ? ` (${layers[l].provenance})` : ""}`);
@@ -3116,9 +3129,55 @@ function renderMergeRegion(r) {
3116
3129
  function instanceCmd() {
3117
3130
  const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
3118
3131
  const sub = args[1], name = args[2];
3119
- const usage = "usage: oats instance git <instance> [--home <abs>] [--dir <d>] [--json] | oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json]";
3120
- if (!["git", "diff"].includes(sub) || !name || name.startsWith("--")) return bail("E_BAD_ARGS", usage);
3132
+ const usage = "usage: oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--dir <d>] [--json] | oats instance git <instance> [--home <abs>] [--dir <d>] [--json] | oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json] | oats instance stop <instance> (--plan | --apply --plan-revision <rev> --idempotency-key <key>) [--no-recursive] [--grace-ms <n>] [--home <abs>] [--dir <d>] [--json]";
3133
+ if (!["git", "diff", "stop", "events"].includes(sub) || !name || name.startsWith("--")) return bail("E_BAD_ARGS", usage);
3121
3134
  dropAmbientRoot();
3135
+ if (sub === "events") {
3136
+ // K7: typed producer events, bounded window; nothing inferred.
3137
+ const homeOpt = flag("home"); if (homeOpt === true || (homeOpt !== undefined && !isAbsolute(homeOpt))) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
3138
+ let root; try { root = ensureRoot(dirFlag()); } catch (e) { return bail(e.code || "E_NO_ROOT", e.message); }
3139
+ const limit = flag("limit"); const since = flag("since");
3140
+ if (limit === true || since === true) return bail("E_BAD_ARGS", usage);
3141
+ try {
3142
+ const { resolveInstance } = await_import_lifecycle();
3143
+ let home = homeOpt;
3144
+ if (!home) home = resolveInstance(dirFlag(), root, name).home;
3145
+ const ev = readEvents(home, { ...(limit !== undefined ? { limit: Math.max(1, Math.min(2000, Number(limit) || 200)) } : {}), ...(since ? { since } : {}) });
3146
+ if (JSON_MODE) { jsonOk(ev); return; }
3147
+ console.log(`${ev.instance}: ${ev.returned} of ${ev.count} event(s)${ev.truncated ? " (window truncated)" : ""}${ev.waitingOnYou ? ` — waiting on you since ${ev.waitingOnYou.since} (${ev.waitingOnYou.producer})` : ""}`);
3148
+ for (const e of ev.events) console.log(` ${e.at ?? "?"} ${e.kind.padEnd(20)} ${e.producer}${e.data ? ` ${JSON.stringify(e.data).slice(0, 120)}` : ""}`);
3149
+ return;
3150
+ } catch (e) { return bail(e.code || "E_EVENTS_FAILED", e.message, e.candidates ? { candidates: e.candidates } : undefined); }
3151
+ }
3152
+ if (sub === "stop") {
3153
+ // K3: plan → apply. The plan is what a confirmation shows; apply carries
3154
+ // its revision back and refuses if reality moved.
3155
+ const homeOpt = flag("home");
3156
+ if (homeOpt === true || (homeOpt !== undefined && !isAbsolute(homeOpt))) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
3157
+ let root; try { root = ensureRoot(dirFlag()); } catch (e) { return bail(e.code || "E_NO_ROOT", e.message); }
3158
+ const recursive = !args.includes("--no-recursive");
3159
+ const wantPlan = args.includes("--plan"), wantApply = args.includes("--apply");
3160
+ if (wantPlan === wantApply) return bail("E_BAD_ARGS", "stop needs exactly one of --plan or --apply");
3161
+ try {
3162
+ if (wantPlan) {
3163
+ const plan = planStop(dirFlag(), root, name, { home: homeOpt, recursive });
3164
+ if (JSON_MODE) { jsonOk(plan); return; }
3165
+ console.log(`stop ${name}${recursive ? " (and recorded children)" : ""} — plan ${plan.planRevision}`);
3166
+ for (const t of plan.targets) console.log(` ${" ".repeat(t.depth)}${t.instance}: session ${t.session.state}${t.work.observed ? `, ${t.work.changed} changed / ${t.work.untracked} untracked on ${t.work.branch ?? "detached"}` : ", work not observed"}${t.midTask === true ? " — mid-task" : t.midTask === "unknown" ? " — activity unknown" : ""}`);
3167
+ for (const n of plan.notes) console.log(` note: ${n}`);
3168
+ console.log(`apply with: oats instance stop ${name} --apply --plan-revision ${plan.planRevision} --idempotency-key <key>`);
3169
+ return;
3170
+ }
3171
+ const rev = flag("plan-revision"), key = flag("idempotency-key"), grace = flag("grace-ms");
3172
+ if (rev === true || key === true || grace === true) return bail("E_BAD_ARGS", usage);
3173
+ const receipt = applyStop(dirFlag(), root, name, { home: homeOpt, recursive, planRevision: rev, idempotencyKey: key, ...(grace !== undefined ? { graceMs: Number(grace) } : {}) });
3174
+ if (JSON_MODE) { jsonOk(receipt); return; }
3175
+ for (const r of receipt.results) console.log(` ${r.instance}: ${r.ok ? (r.stopped ? "stopped" : `already ${r.state}`) : `${r.code} — ${r.message}`}`);
3176
+ console.log(receipt.ok ? `stopped${receipt.replayed ? " (replayed receipt)" : ""}; home, work, transcript and launch configuration retained — restart with \`oats session restart\`` : "some targets are still running; nothing was escalated");
3177
+ if (!receipt.ok) process.exit(1);
3178
+ return;
3179
+ } catch (e) { return bail(e.code || "E_LIFECYCLE_FAILED", e.message, e.plan ? { plan: e.plan } : e.candidates ? { candidates: e.candidates } : undefined); }
3180
+ }
3122
3181
  let home = flag("home");
3123
3182
  if (home === true) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
3124
3183
  if (home !== undefined && !isAbsolute(home)) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
@@ -3156,6 +3215,33 @@ function instanceCmd() {
3156
3215
  bail(e.code || "E_GIT_FAILED", e.message, e.observation ? { observation: e.observation } : undefined);
3157
3216
  }
3158
3217
  }
3218
+ /** `oats readiness [--soul <name>] [--home <abs>] [--verify-signatures] [--policy] [--dir <d>] --json` — K5. */
3219
+ function readinessCmd() {
3220
+ const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
3221
+ dropAmbientRoot();
3222
+ const inspect = computeInspect({ onFail: bail });
3223
+ if (!inspect) return;
3224
+ const soul = flag("soul") === true ? null : flag("soul") || inspect.selected?.soul || null;
3225
+ const verify = args.includes("--verify-signatures");
3226
+ let catalog = null; try { catalog = describeOfficialCatalog(); catalog = { packages: Object.fromEntries(catalog.packages.map((p) => [p.package, p])) }; } catch { catalog = null; }
3227
+ const deploymentDir = inspect.scope?.context ?? null;
3228
+ const readiness = readinessOf(inspect, { soul, verifySignatures: verify, catalog, deploymentDir });
3229
+ if (args.includes("--policy")) {
3230
+ const homeOpt = flag("home");
3231
+ let meta = null;
3232
+ if (homeOpt && homeOpt !== true) { try { meta = JSON.parse(readFileSync(join(homeOpt, "instance.json"), "utf8")); } catch (e) { return bail("E_SESSION_UNKNOWN", `${homeOpt}: ${e.message}`); } }
3233
+ readiness.policy = policyOf({ instanceMeta: meta, soul: soul ? inspect.souls.find((s) => s.name === soul) : null }).policy;
3234
+ readiness.notes.push("policy: a lifecycle-authority claim enforced by the spawn route, not an OS sandbox");
3235
+ }
3236
+ if (JSON_MODE) { jsonOk(readiness); return; }
3237
+ console.log(`readiness — ${readiness.subject.kind === "soul" ? `soul ${readiness.subject.name}` : shortPath(readiness.subject.context)}: ${readiness.summary.ready ? "READY" : `${readiness.summary.fail} failing, ${readiness.summary.unknown} unknown of ${readiness.summary.required} required`}`);
3238
+ for (const [name, check] of Object.entries(readiness.checks)) {
3239
+ console.log(` ${name}: ${check.status}`);
3240
+ for (const i of check.items) console.log(` ${i.status.padEnd(14)} ${i.subject}${i.required ? "" : " (optional)"}${i.reason ? ` — ${i.reason}` : ""}${i.signature ? ` · signature ${i.signature.status}${i.signature.signer?.label ? ` by ${i.signature.signer.label}` : ""}` : ""}${i.remedy ? ` → ${i.remedy}` : ""}`);
3241
+ }
3242
+ if (readiness.policy) console.log(` policy: child spawns ${readiness.policy.childSpawns.allowed ? "allowed" : "disabled"} (${readiness.policy.childSpawns.origin.kind}${readiness.policy.childSpawns.enforced ? ", enforced" : ""}); worktrees ${readiness.policy.worktrees.allowed === null ? "unknown" : readiness.policy.worktrees.allowed ? "allowed" : "not in this work mode"}`);
3243
+ for (const n of readiness.notes) console.log(` note: ${n}`);
3244
+ }
3159
3245
  function catalogCmd() {
3160
3246
  const described = describeOfficialCatalog();
3161
3247
  if (JSON_MODE) { jsonOk(described); return; }
@@ -4066,7 +4152,7 @@ function spawnCmd() {
4066
4152
  };
4067
4153
  checkDirectoryOptions(requestedWork); // before a local soul could be upserted
4068
4154
  const name = args[1];
4069
- if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--instructions-file <f>|--def-file <f>] [--no-launch] [--json]");
4155
+ if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>] [--preview] [--base <ref>] [--model <id>|@native-default] [--allow-child-spawns|--no-child-spawns] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--instructions-file <f>|--def-file <f>] [--no-launch] [--json]");
4070
4156
  // Retired boundary flags (maintainer transport ruling): fail LOUDLY before
4071
4157
  // ANY side effect — including root discovery and local-agent upsert (an
4072
4158
  // --instructions-file spawn must not scaffold/overwrite a local soul before
@@ -4184,8 +4270,11 @@ function spawnCmd() {
4184
4270
  } catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message); throw e; }
4185
4271
  let r;
4186
4272
  try {
4273
+ if (args.includes("--allow-child-spawns") && args.includes("--no-child-spawns")) bail("E_BAD_ARGS", "--allow-child-spawns and --no-child-spawns contradict");
4274
+ if (flag("base") === true) bail("E_BAD_ARGS", "--base needs a ref");
4187
4275
  r = spawnInstance(root, agent, {
4188
4276
  purpose: flag("purpose"), task: taskText, taskFile: taskFileFlag, relation, relativeTo, relativeRoot,
4277
+ ...(args.includes("--allow-child-spawns") ? { allowChildSpawns: true } : args.includes("--no-child-spawns") ? { allowChildSpawns: false } : {}),
4189
4278
  // Directory execution uses deployment configuration, not an ambient Git
4190
4279
  // checkout (especially when invoked via --dir from a source instance).
4191
4280
  repo: (requestedWork || agent.work) === "directory"
@@ -4193,7 +4282,13 @@ function spawnCmd() {
4193
4282
  work: requestedWork, workDir, runtime: flag("runtime"), backend, herdrSocket, yolo, model: flag("model"), branch,
4194
4283
  launchConfig: valueFlag("launch-config"),
4195
4284
  launch: !args.includes("--no-launch"),
4285
+ // K6: --preview decides everything and touches nothing; --base <ref>
4286
+ // selects a worktree's start point; --model @native-default is the
4287
+ // explicit "runtime's own default" (distinct from omitting --model).
4288
+ ...(args.includes("--preview") ? { preview: true } : {}),
4289
+ ...(flag("base") !== undefined && flag("base") !== true ? { baseRef: flag("base") } : {}),
4196
4290
  });
4291
+ if (args.includes("--preview")) { if (JSON_MODE) { jsonOk(r); return; } console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""}) runtime ${r.runtime}${r.model ? ` model ${r.model}` : ` (${r.modelSource})`}; nothing was created`); return; }
4197
4292
  } catch (e) {
4198
4293
  // A typed CLI failure keeps ITS OWN code: re-badging an unsafe-config-key
4199
4294
  // (raised by the readers spawn walks) as E_SPAWN_FAILED tells an agent
@@ -4207,6 +4302,8 @@ function spawnCmd() {
4207
4302
  // An unmet declared requirement is a fact about the soul's configuration
4208
4303
  // (with a remedy), not a spawn-mechanism failure: keep its code and details.
4209
4304
  if (e?.code === "E_REQUIREMENT_INACTIVE") { bail(e.code, e.message, { soul: e.soul, capabilities: e.capabilities, context: e.context, remedy: e.remedy }); throw e; }
4305
+ if (e?.code === "E_CHILD_SPAWNS_DISABLED") { bail(e.code, e.message, { parent: e.parent, policy: e.policy }); throw e; }
4306
+ if (["E_BRANCH_EXISTS", "E_BASE_UNKNOWN"].includes(e?.code)) { bail(e.code, e.message); throw e; }
4210
4307
  bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
4211
4308
  }
4212
4309
  // The instance exists from here on: a failed wake save is reported beside
@@ -4243,9 +4340,23 @@ function spawnCmd() {
4243
4340
 
4244
4341
  function retireCmd() {
4245
4342
  const name = args[1];
4246
- if (!name || name.startsWith("--")) die("usage: oats retire <instance> [--home <path>] [--self] [--delete-branch] [--keep-dir] [--force] [--json]");
4343
+ if (!name || name.startsWith("--")) die("usage: oats retire <instance> [--plan] [--plan-revision <rev> --idempotency-key <key>] [--home <path>] [--self] [--discard-worktree] [--delete-branch] [--keep-dir] [--force] [--json]");
4247
4344
  let homeFlag = flag("home");
4248
4345
  if (homeFlag === true) die("--home needs the instance home path");
4346
+ if (args.includes("--plan")) {
4347
+ // K3: what retirement would touch, with the design's defaults — read-only.
4348
+ dropAmbientRoot();
4349
+ let root; try { root = ensureRoot(dirFlag()); } catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_NO_ROOT", e.message) : die(e.message); }
4350
+ try {
4351
+ const plan = planRetire(dirFlag(), root, name, { home: homeFlag });
4352
+ if (args.includes("--json")) { jsonOk(plan); return; }
4353
+ console.log(`retire ${name} — plan ${plan.planRevision}`);
4354
+ console.log(` session ${plan.facts.session.state}; work ${plan.facts.work.observed ? `${plan.facts.work.changed} changed / ${plan.facts.work.untracked} untracked on ${plan.facts.work.branch ?? "detached"}` : `not observed (${plan.facts.work.reason})`}; children ${plan.facts.children.length}; pull request ${plan.facts.pullRequest}`);
4355
+ console.log(` defaults: retain worktree ${plan.defaults.retainWorktree}, delete branch ${plan.defaults.deleteBranch}, stop children ${plan.defaults.stopChildren}`);
4356
+ for (const n of plan.notes) console.log(` note: ${n}`);
4357
+ return;
4358
+ } catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message, e.candidates ? { candidates: e.candidates } : undefined) : die(e.message); }
4359
+ }
4249
4360
  // The calling instance knows its own home: self-retire never needs to
4250
4361
  // disambiguate a same-named twin by hand.
4251
4362
  if (homeFlag === undefined && process.env.OATS_INSTANCE_HOME && (process.env.PI_AGENT_INSTANCE === name || process.env.OATS_INSTANCE === name)) homeFlag = process.env.OATS_INSTANCE_HOME;
@@ -4260,7 +4371,39 @@ function retireCmd() {
4260
4371
  if (hit && resolve(hit.root) !== resolve(root)) { root = hit.root; (args.includes("--json") ? console.error : console.log)(`(cross-repo: instance homes at ${shortPath(root)})`); }
4261
4372
  }
4262
4373
  const retiringHome = homeFlag || findInstanceHome(root, name);
4263
- const r = retireInstance(root, name, { home: homeFlag, self: isSelf, deleteBranch: args.includes("--delete-branch"), keepDir: args.includes("--keep-dir"), force: args.includes("--force") });
4374
+ // K3: a GUI-driven Remove carries the plan revision it showed and an
4375
+ // idempotency key. The revision is revalidated against a fresh plan
4376
+ // (E_PLAN_STALE with that plan attached — re-confirm, never act on the old
4377
+ // one); a retried key replays the recorded receipt instead of retiring twice.
4378
+ const planRev = flag("plan-revision"), idemKey = flag("idempotency-key");
4379
+ if (planRev === true || idemKey === true) die("--plan-revision and --idempotency-key need values");
4380
+ if ((planRev !== undefined) !== (idemKey !== undefined)) die("--plan-revision and --idempotency-key go together");
4381
+ let replayPath = null, childrenStopped = null, expectedBranch;
4382
+ if (planRev !== undefined) {
4383
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(idemKey)) die("--idempotency-key: 1-128 chars of [A-Za-z0-9._:-]");
4384
+ // Replay first: after a successful retire the home is gone, so the receipt
4385
+ // (beside the instances dir, keyed by the idempotency key) is the answer.
4386
+ const replay = (dir) => { const p = join(dir, `.oats-retire-receipt.${idemKey}.json`); if (!existsSync(p)) return false; try { const prior = JSON.parse(readFileSync(p, "utf8")); if (prior.retired !== name) return false; if (args.includes("--json")) jsonOk({ ...prior, replayed: true }); else console.log(`retire ${name}: replayed receipt for key ${idemKey}`); return true; } catch { return false; } };
4387
+ for (const a of listAgents(root)) if (replay(join(a._dir, "instances"))) return;
4388
+ let fresh;
4389
+ try { fresh = planRetire(dirFlag(), root, name, { home: homeFlag }); } catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message) : die(e.message); }
4390
+ replayPath = join(dirname(fresh.home), `.oats-retire-receipt.${idemKey}.json`);
4391
+ if (fresh.planRevision !== planRev) return args.includes("--json") ? jsonFail("E_PLAN_STALE", `the retire plan changed since it was shown (${planRev} → ${fresh.planRevision}); review the fresh plan`, { plan: fresh }) : die(`the retire plan changed since it was shown; re-run oats retire ${name} --plan`);
4392
+ // The plan promised: recorded children are STOPPED first (bounded, never
4393
+ // escalated) and retained. A child still running after the grace refuses
4394
+ // the retirement — nothing is retired, the receipt names the pid.
4395
+ childrenStopped = [];
4396
+ for (const kid of fresh.facts.children) {
4397
+ try { const s = stopInstanceSession(kid.home, {}); childrenStopped.push({ instance: kid.instance, home: kid.home, ok: true, stopped: s.stopped, alreadyIdle: s.alreadyIdle }); }
4398
+ catch (e) { childrenStopped.push({ instance: kid.instance, home: kid.home, ok: false, code: e.code || "E_SESSION_STOP_FAILED", message: e.message, stillRunning: e.receipt?.stillRunning ?? null }); }
4399
+ }
4400
+ const running = childrenStopped.filter((k) => !k.ok);
4401
+ if (running.length) return args.includes("--json") ? jsonFail("E_CHILDREN_RUNNING", `${running.map((k) => k.instance).join(", ")} ${running.length === 1 ? "is" : "are"} still running after a bounded stop; nothing was retired and nothing was escalated`, { childrenStopped, plan: fresh }) : die(`children still running: ${running.map((k) => k.instance).join(", ")}; nothing retired`);
4402
+ expectedBranch = fresh.facts.work.observed ? fresh.facts.work.branch : undefined;
4403
+ }
4404
+ const r = retireInstance(root, name, { home: homeFlag, self: isSelf, deleteBranch: args.includes("--delete-branch"), discardWorktree: args.includes("--discard-worktree"), keepDir: args.includes("--keep-dir"), force: args.includes("--force"), ...(expectedBranch !== undefined ? { expectedBranch } : {}) });
4405
+ if (childrenStopped) r.childrenStopped = childrenStopped;
4406
+ if (replayPath) { r.planRevision = planRev; r.idempotencyKey = idemKey; r.replayed = false; try { writeFileAtomic(replayPath, JSON.stringify(r, null, 2)); } catch { /* receipt is evidence, not authority */ } }
4264
4407
  // A retired home's wake jobs are forgotten (definitions only; nothing is
4265
4408
  // stopped by this); a deferred self-retire keeps them until the home is gone.
4266
4409
  if (retiringHome && r.removedDir !== false && !r.deferred) { try { const gone = removeWakeForHome(scheduleScopeOf(workspaceOf(root)), retiringHome); if (gone.length) r.wakeSchedulesRemoved = gone; } catch (e) { r.warnings = [...(r.warnings || []), `wake schedules not cleaned: ${e.message}`]; } }
@@ -4362,6 +4505,7 @@ async function sessionCmd() {
4362
4505
  return;
4363
4506
  }
4364
4507
  if (args[1] === "inspect") result = inspectInstanceSession(home);
4508
+ else if (args[1] === "recompose") result = recomposeInstanceInstructions(home, { dryRun: args.includes("--dry-run") });
4365
4509
  else if (args[1] === "start" || args[1] === "restart") {
4366
4510
  const bad = (msg) => { throw Object.assign(new Error(msg), { code: "E_BAD_ARGS" }); };
4367
4511
  const model = flag("model");
@@ -4836,7 +4980,7 @@ function versionCmd() {
4836
4980
  // on it (an older CLI without the surface must fail closed with a
4837
4981
  // reason, not an argument error). `features`: kernel abilities a peer
4838
4982
  // must see before relying on them (retire-home: retire --home).
4839
- console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations"], scheduleApi: SCHEDULE_API, operationsApi: 1, capturedDispatchApi: 1, capturedDispatchActions: ["inspect", "compose", "command", "operation", "spawn", "trust"] }));
4983
+ console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "catalog", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "schedule-history", "session-recompose"], instanceGitApi: 1, soulsApi: 1, lifecycleApi: 1, readinessApi: 1, spawnPreviewApi: 1, eventsApi: 1, scheduleHistoryApi: 2, scheduleApi: SCHEDULE_API, operationsApi: 1, capturedDispatchApi: 1, capturedDispatchActions: ["inspect", "compose", "command", "operation", "spawn", "trust"] }));
4840
4984
  return;
4841
4985
  }
4842
4986
  console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
@@ -5278,6 +5422,7 @@ else if (cmd === "config") configCmd();
5278
5422
  else if (cmd === "trust") trust();
5279
5423
  else if (cmd === "list") listCmd();
5280
5424
  else if (cmd === "catalog") catalogCmd();
5425
+ else if (cmd === "readiness") readinessCmd();
5281
5426
  else if (cmd === "instance") instanceCmd();
5282
5427
  else if (cmd === "remove") removeCmd();
5283
5428
  else if (cmd === "migrate") migrateCmd();
@@ -5480,6 +5625,33 @@ Usage:
5480
5625
  oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json]
5481
5626
  bounded diff of one observed file; refuses when
5482
5627
  the tree moved since the observation
5628
+ oats session recompose --home <abs> [--dry-run] [--json]
5629
+ refresh a LIVE home's AGENTS.md from its current
5630
+ canonical soul + context (same composer as spawn);
5631
+ previous text retained; nothing restarted
5632
+ oats instance events <instance> [--limit <n>] [--since <iso>] [--json]
5633
+ typed lifecycle events (spawned, launched, stopped,
5634
+ restarted, retired, worktree-retained…) written by
5635
+ the action that made them true; nothing inferred
5636
+ oats instance stop <instance> --plan [--no-recursive] [--json]
5637
+ what Stop would touch: session state, recorded
5638
+ children, dirty work; a planRevision to apply
5639
+ oats instance stop <instance> --apply --plan-revision <rev> --idempotency-key <key>
5640
+ quiesce (SIGTERM, bounded, never escalated),
5641
+ children first; home/work/launch retained
5642
+ oats readiness [--soul <n>] [--home <abs>] [--verify-signatures] [--policy] [--json]
5643
+ installed | trusted | configured | enrolled, each
5644
+ pass|fail|unknown|not-applicable with items and
5645
+ remedies; signature status per artifact; enforced
5646
+ child-spawn / worktree policy with origins
5647
+ oats retire <instance> --plan [--json] what Remove would touch, with retention defaults
5648
+ oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree] [--delete-branch]
5649
+ with a plan revision: refuses E_PLAN_STALE (fresh plan
5650
+ attached) if facts moved; a repeated key replays
5651
+ retire; a worktree is RETAINED (re-homed under
5652
+ <workspace>/.agents/worktrees/<repo>/<branch>)
5653
+ unless discarded; --delete-branch deletes the
5654
+ worktree's verified branch and implies discard
5483
5655
  oats update <package> [<package>@<ref>] transactional package update: temp fetch,
5484
5656
  [--to <ref>] [--dir <d>] closure validation, diff, lock replace,
5485
5657
  all capability approvals invalidated; a
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Purpose:** the one accurate view of every work stream in the redesign, what is on main, what is in flight, who owns it, and what blocks it. Lead: `oats-expert` (redesign lead). Updated whenever anything merges, is returned, or reality changes. Older per-lane boards are superseded by this file.
4
4
 
5
- **Last update:** 2026-09-21 19:25Z (earlier stamps reading 2026-09-22 were a lead clock error — the work happened 2026-09-21; corrected on Antares's observation) · S8: 1a/1b/3/4/6a/7a MERGED · K1+K4 MERGED · engineer → 2a · lead → P1 Decision, K3, K5; tag 0.24.7 once 2a lands
5
+ **Last update:** 2026-09-22 10:30Z (clock verified with `date -u`; the previous "22:20Z" stamp was a wall-clock error) · **Slice 2b MERGED** (PR65) · **K3 producer pins closed** (PR66 advertisement + guarded Remove; PR67 kernel-owned child stop, confirmed-branch binding, per-key stop replay, ambiguous parentage) · `oats session recompose` seam (in-place instruction refresh) · engineer wiring **2c** on `aed94b74` → 5 → 6b → 7b → 8 · lead: reviews; tag 0.24.8 when 2c lands
6
6
 
7
7
  Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
8
8
 
@@ -17,7 +17,7 @@ Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, n
17
17
  | S5 | Official marketplace = reviewed list in oats repo | ✅ D4 merged · ✅ `oats.framework` 1.1.1 listed (`oats.core`, `oats.setup`, `oats.knowledge-theory` aliases) | M | Desktop view → S8 |
18
18
  | S6 | Five expert souls created in the oats repo (`souls/<name>/`) | ✅ five + `oats-setup-expert` on main, all declaring `oats.core`, exported + imported | M, L, lead | legacy `agents/` cutover after S7 proof |
19
19
  | S7 | Centralised per-soul knowledge in `oats-knowledge` (migration + PR-only learning) | ✅ **MIGRATED — PR #2 merged → main `7148a36`, 58 concepts / 35k words from 399 legacy** (kernel 26, expert 18, desktop 13, assistant 1, market-research chartered empty); option A roster; theory + no-code-teaching rule; two workflow passes (24 agents) + lead review; validator 58/0/0; ownership 14/14 · ⏸ harvest OFF until new souls run from the base (human) | lead | next: legacy `agents/` decommission with the `~/OATS` cutover |
20
- | S8 | Desktop parity with the redesign (Redesign v3, aweb palette, discovery-first control panel) | 🔄 **STARTED 2026-09-22**: existing `oats-desktop-engineer-1` retrofitted with an aweb identity (spawn hook replayed, `capabilityMeta` persisted, session reloaded); redesign artefacts copied into its home; brief sent (f16eed3f): merge main (0.24.5) first, then PR slices — shell/palette/sidebar → right panel follows selection → Souls view → **Capabilities view incl. official catalog (human's required feature; server-side via kernel catalog API, never auto-acquire/auto-trust)** → Knowledge/Tasks adapters → remainder. Lead reviews/merges each slice. **Decisions 2026-09-22:** human's later palette/row instructions supersede the HTML where they conflict; grouping is agent-group (cross-repo clusters), not repo; kernel seam `oats catalog --json` merged (PR46 `524180b7`, ships 0.24.6); the Juan-host fresh-engineer plan is superseded — this instance owns S8. | oats-desktop-engineer-1, lead | per-slice PRs | ✅ **Slice 3 MERGED** (PR54; Souls + Sources on K4 soulsApi 1; 1503/1503) · ✅ **K1 MERGED** (PR53; `oats instance git|diff`, instanceGitApi 1; DTO in docs/desktop-cli-api.md) · ✅ **K4 MERGED** (PR52; `inspect --json` soulsApi 1: declarations/provenance/readiness/sources; onboard records provenance; DTO in docs/desktop-cli-api.md) · ✅ **Slice 7a MERGED** (PR51; frame 07 Active overview on /api/panel; anonymous groups; 1466/1466) · ✅ **Slice 6a MERGED** (PR50; Spawn modal frame 02 on existing seams; launchConfig restored; 1435/1435) · ✅ **Slice 4 MERGED** (PR49; Capabilities view: official catalog via `oats catalog` + classic inventory; 1386/1386; server boundary reviewed) · ✅ **Slice 1b MERGED** (PR48; contextual panel + focus mode; 1238/1238) · ✅ **Slice 1a MERGED `508c4b5f`** (PR45; 273/273 real jsdom; gates green) → v0.24.6. **Plan + seams recorded: [`docs/design/2026-09-22-desktop-parity-seams.md`](2026-09-22-desktop-parity-seams.md)** — slices 1a–8, seams K1–K8/P1, five policy decisions **DECIDED 2026-09-22 under delegation** — see `decisions/desktop-parity-lifecycle-and-policy-decisions.md`.
20
+ | S8 | Desktop parity with the redesign (Redesign v3, aweb palette, discovery-first control panel) | 🔄 **STARTED 2026-09-22**: existing `oats-desktop-engineer-1` retrofitted with an aweb identity (spawn hook replayed, `capabilityMeta` persisted, session reloaded); redesign artefacts copied into its home; brief sent (f16eed3f): merge main (0.24.5) first, then PR slices — shell/palette/sidebar → right panel follows selection → Souls view → **Capabilities view incl. official catalog (human's required feature; server-side via kernel catalog API, never auto-acquire/auto-trust)** → Knowledge/Tasks adapters → remainder. Lead reviews/merges each slice. **Decisions 2026-09-22:** human's later palette/row instructions supersede the HTML where they conflict; grouping is agent-group (cross-repo clusters), not repo; kernel seam `oats catalog --json` merged (PR46 `524180b7`, ships 0.24.6); the Juan-host fresh-engineer plan is superseded — this instance owns S8. | oats-desktop-engineer-1, lead | per-slice PRs | ✅ **Slice 2b MERGED** (PR65; Connections + PR card; 1654/1654) · ✅ **K5+K6 MERGED** (PR63; readiness quartet/signatures/enforced child policy; spawn --preview/--base/@native-default) · ✅ **K7+K8 MERGED** (PR64; typed instance events; schedule recentRuns) · ✅ **K3 MERGED** (PR62; `instance stop --plan|--apply`, `retire --plan`, retention-by-default retire with re-home) · ✅ baseline flakes fixed (PR61) · ✅ **P1 decided + K1 `remote` MERGED** (PR60) · ✅ **Components (frame 10, existing data) MERGED** (PR59; 1606/1606) · ✅ **Slice 2a MERGED** (PR57; Git panel on K1; legacy gitState collector removed) · ✅ **v0.24.7 tagged/published** (tag → `c923d81b`) · ✅ **Slice 3 MERGED** (PR54; Souls + Sources on K4 soulsApi 1; 1503/1503) · ✅ **K1 MERGED** (PR53; `oats instance git|diff`, instanceGitApi 1; DTO in docs/desktop-cli-api.md) · ✅ **K4 MERGED** (PR52; `inspect --json` soulsApi 1: declarations/provenance/readiness/sources; onboard records provenance; DTO in docs/desktop-cli-api.md) · ✅ **Slice 7a MERGED** (PR51; frame 07 Active overview on /api/panel; anonymous groups; 1466/1466) · ✅ **Slice 6a MERGED** (PR50; Spawn modal frame 02 on existing seams; launchConfig restored; 1435/1435) · ✅ **Slice 4 MERGED** (PR49; Capabilities view: official catalog via `oats catalog` + classic inventory; 1386/1386; server boundary reviewed) · ✅ **Slice 1b MERGED** (PR48; contextual panel + focus mode; 1238/1238) · ✅ **Slice 1a MERGED `508c4b5f`** (PR45; 273/273 real jsdom; gates green) → v0.24.6. **Plan + seams recorded: [`docs/design/2026-09-22-desktop-parity-seams.md`](2026-09-22-desktop-parity-seams.md)** — slices 1a–8, seams K1–K8/P1, five policy decisions **DECIDED 2026-09-22 under delegation** — see `decisions/desktop-parity-lifecycle-and-policy-decisions.md`.
21
21
 
22
22
  ## S1 — Knowledge capability contract rework
23
23
  - ✅ Provider-neutral contract, binding wire v1, helper/input contract, retained execution: OATS 0.24.0 + OKF 2.1.0 (f20f8e57) published.
@@ -34,9 +34,9 @@ Envelope `{schemaVersion:1, ok, result|error}` unchanged. Requests address a ser
34
34
  - **K2 review-thread delivery**: explicit, confirmed send of selected threads to the exact home's session input with receipt (`delivered | refused | unknown`); delivered ≠ consumed. Typed producer events for commit / branch-renamed / PR-updated / review-request; **no prose parsing** to infer actions.
35
35
  - **K3 lifecycle plan/apply**: read-only plan (runtime activity, children, worktree dirt, branch + open PRs, retention per artefact, per-option allowed/default/reason, warnings, blockers) → apply with plan revision + idempotency key, revalidated under the lifecycle lock; per-target `completed | retained | partial | unknown`. Today there is **no standalone stop**, and retire removes owned worktrees; the design's default Remove retains worktree/branch/PR.
36
36
  - **K4 souls/sources enumeration** (✅ MERGED PR52 as additive `inspect --json` `soulsApi:1`, not a new command — see docs/desktop-cli-api.md): qualified list of imported editions + authored local souls with identity/source/revision/requirements/declarations/editability; readiness separate from launchability and adoption; no renderer YAML.
37
- - **K5 readiness quartet**: `installed | trusted | configured | enrolled`, each `pass | fail | unknown | not-applicable` with items (subject, requiredness, reason, producer, evidence, remedy); trust separates artifact approval from `signature {verified|unsigned|unknown|invalid, signer}`; policy view returns **enforced** child-spawn/worktree permissions with origins; native config items report labels/scope state, never secrets; unknown ≠ granted.
38
- - **K6 spawn preview/apply** (add, 2026-09-22 from 6a review: an explicit `model: {kind: "native-default"}` request field — today an omitted model inherits the configured/soul model and there is no force-native override; 6a renders that control disabled until K6): kernel returns suggestions, canonical worktree/home/branch/base OID, resolved knowledge refs, enforced child policy, auto-PR policy, readiness, typed field problems; apply revalidates and captures; no Desktop-derived paths or branches.
39
- - **K7 activity feed**: bounded typed events per instance with provenance; "waiting on you" only from a producer that reports it.
37
+ - **K5 readiness quartet** (✅ MERGED PR63 — `oats readiness`): `installed | trusted | configured | enrolled`, each `pass | fail | unknown | not-applicable` with items (subject, requiredness, reason, producer, evidence, remedy); trust separates artifact approval from `signature {verified|unsigned|unknown|invalid, signer}`; policy view returns **enforced** child-spawn/worktree permissions with origins; native config items report labels/scope state, never secrets; unknown ≠ granted.
38
+ - **K6 spawn preview/apply** (✅ MERGED PR63 — `oats spawn --preview`, `--base`, `--model @native-default`) (add, 2026-09-22 from 6a review: an explicit `model: {kind: "native-default"}` request field — today an omitted model inherits the configured/soul model and there is no force-native override; 6a renders that control disabled until K6): kernel returns suggestions, canonical worktree/home/branch/base OID, resolved knowledge refs, enforced child policy, auto-PR policy, readiness, typed field problems; apply revalidates and captures; no Desktop-derived paths or branches.
39
+ - **K7 activity feed** (✅ MERGED PR64 — `oats instance events`): bounded typed events per instance with provenance; "waiting on you" only from a producer that reports it.
40
40
  - **K8 schedule run history** + captured-policy-preserving edit contract; transcript access via the owning CLI/provider.
41
41
 
42
42
  ## Decisions — DECIDED 2026-09-22 (lead, delegated by the human)
@@ -55,4 +55,4 @@ Also to confirm: prototype "Gemini" runtime is illustrative (not an OATS runtime
55
55
 
56
56
  ## Ownership
57
57
 
58
- Lead: K1, K4, K5 (readiness/policy shape), K6 preview/apply plumbing, K7/K8 projections, dispatch contract for additive-capability views — proposed as Decisions where they change contracts, implemented in small PRs otherwise. P1: a new capability (`oats.git`/GitHub backend) — spec'd as a Decision with the human; implementation lane TBD. Desktop engineer: all slices, Desktop IPC review items (detach, open-in-editor).
58
+ Lead: K1, K4, K5 (readiness/policy shape), K6 preview/apply plumbing, K7/K8 projections, dispatch contract for additive-capability views — proposed as Decisions where they change contracts, implemented in small PRs otherwise. P1: **forge connections are ADE/workstation integrations, not capabilities** — **Decision ACCEPTED** (`agents/oats-expert/soul/knowledge/decisions/p1-forge-connection-is-an-ade-integration.md`). Desktop *Connections* surface (GitHub card: status / Connect = `gh auth login --web` in an owned pane / Disconnect), PR card read by the Desktop server via fixed-argv `gh pr view … --json` at the existing guarded boundary, typed states (available / no-pull-request / not-connected / cli-not-installed / unsupported-forge / no-remote / unavailable), remote workspaces refuse; OATS never holds a token. No kernel dispatch contract. Kernel: K1 gains `remote {name,url,host,path}` (lead). Slice **2b** = Connections + PR card (engineer; lead security gate before wiring). Auto-PR: ADE-owned, off, per spawn, after K6. Desktop engineer: all slices, Desktop IPC review items (detach, open-in-editor).