@awebai/oats 0.24.6 → 0.24.7

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, listCapabilityAgents, workspaceOf,
33
+ findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, findInstanceHomes, listCapabilityAgents, workspaceOf,
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";
@@ -58,12 +58,13 @@ import { portableScope } from "../lib/portable-state.mjs";
58
58
  import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../lib/captured-operation-process.mjs";
59
59
  import { approveCapturedCapability } from "../lib/artifact-approvals.mjs";
60
60
  import { loadSetupExpertEdition, SETUP_EXPERT, SETUP_CAPABILITIES } from "../lib/setup-expert-source.mjs";
61
+ import { observeInstanceGit, diffInstanceFile } from "../lib/instance-git.mjs";
61
62
  import { parsePortableSource } from "../lib/source-spec.mjs";
62
63
 
63
64
  const args = process.argv.slice(2);
64
65
  let cmd = args[0];
65
66
  const HELP_WORDS = new Set(["help", "--help", "-h"]);
66
- const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "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"]);
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"]);
67
68
  const flag = (name) => {
68
69
  const i = args.indexOf(`--${name}`);
69
70
  return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
@@ -815,11 +816,36 @@ function homeContexts(home, meta) {
815
816
  out.push(dirname(agentsRootOfHome(realOrResolved(home))));
816
817
  return out;
817
818
  }
819
+ /** A soul's own declared requirements/defaults/provenance, read from its
820
+ * soul.yaml through the kernel's parser (never guessed, never re-parsed by a
821
+ * consumer). Absent sections are null: an unrecorded provenance is a fact
822
+ * about the soul, not a prompt to infer one. */
823
+ function soulDeclarations(soulDir) {
824
+ const file = join(soulDir, "soul.yaml");
825
+ const empty = { declarations: { requires: null, defaults: null, knowledge: null, teams: null, resources: null }, provenance: null, problems: [] };
826
+ if (!existsSync(file)) return empty;
827
+ let parsed;
828
+ try { parsed = withConfigFile(file, () => parseYamlNested(readFileSync(file, "utf8"))); }
829
+ catch (e) { return { ...empty, problems: [{ code: "soul-declarations-unreadable", message: e.message }] }; }
830
+ const section = (key) => (parsed[key] !== undefined && parsed[key] !== null && typeof parsed[key] === "object") ? parsed[key] : (parsed[key] === undefined ? null : parsed[key]);
831
+ const provenance = section("provenance");
832
+ return {
833
+ declarations: { requires: section("requires"), defaults: section("defaults"), knowledge: section("knowledge"), teams: section("teams"), resources: section("resources") },
834
+ provenance: provenance && typeof provenance === "object" ? {
835
+ kind: provenance.kind ?? null, source: provenance.source ?? null, revision: provenance.revision ?? null,
836
+ path: provenance.path ?? null, workspaceRevision: provenance.workspaceRevision ?? null,
837
+ } : null,
838
+ problems: [],
839
+ };
840
+ }
818
841
  function soulEntry(soul, root, { capability } = {}) {
819
842
  const dir = soul._dir || soul.soulDir;
820
843
  const soulDir = capability ? soul.soulDir : join(dir, "soul");
821
844
  const packaged = !!capability;
845
+ const declared = soulDeclarations(soulDir);
822
846
  return {
847
+ soulsApi: 1, declarations: declared.declarations, provenance: declared.provenance,
848
+ declarationProblems: declared.problems,
823
849
  name: soul.name, kind: packaged ? "capability" : (soul.kind || "persistent"), capability: capability || null,
824
850
  type: soul.type ?? null, description: soul.description ?? null, repo: soul.repo ?? null, work: soul.work || "checkout",
825
851
  runtime: soul.runtime || "pi", model: soul.model ?? null, yolo: soul.yolo === true || soul.yolo === "true" ? true : soul.yolo === false || soul.yolo === "false" ? false : null, launchConfig: soul["launch-config"] ?? null, backend: soul.backend ?? null,
@@ -996,11 +1022,32 @@ function inspectCmd() {
996
1022
  instructions: { ...readTextCapped(join(home, "AGENTS.md")), sources: meta.instructions || [] }, drift,
997
1023
  };
998
1024
  }
1025
+ // K4: the deployment's portable source context and each soul's declared
1026
+ // requirements joined against the capability inventory in this same payload.
1027
+ // Readiness is about the soul's declared sources, kept apart from
1028
+ // launchability (spawn) and adoption (prepare); unobservable = null.
1029
+ const capabilityById = new Map(capabilities.map((c) => [c.id, c]));
1030
+ for (const s of souls) {
1031
+ const required = s.declarations?.requires?.capabilities;
1032
+ const requirements = required && typeof required === "object" ? Object.entries(required).map(([id, spec]) => {
1033
+ const cap = capabilityById.get(id) || null;
1034
+ return { capability: id, source: spec && typeof spec === "object" ? spec.source ?? null : null,
1035
+ installed: cap ? cap.health?.installed ?? null : false, approved: cap ? cap.health?.trusted ?? null : null,
1036
+ active: cap ? !!cap.activation?.enabled : null, version: cap?.version ?? null };
1037
+ }) : null;
1038
+ s.readiness = { source: s.provenance ? "recorded" : "unrecorded", requirements,
1039
+ status: requirements === null ? "undeclared" : requirements.every((q) => q.installed === true) ? "sources-installed" : requirements.some((q) => q.installed === false) ? "sources-missing" : "unknown" };
1040
+ }
1041
+ const sourceKey = (p) => JSON.stringify([p.source, p.revision, p.path]);
1042
+ const sourceItems = [...new Map(souls.filter((s) => s.provenance?.source).map((s) => [sourceKey(s.provenance), { ...s.provenance, souls: [] }])).values()];
1043
+ for (const s of souls) if (s.provenance?.source) sourceItems.find((i) => sourceKey(i) === sourceKey(s.provenance)).souls.push(s.name);
1044
+ const sources = { soulsApi: 1, kind: sourceItems.length ? "recorded-provenance" : "none-recorded", items: sourceItems,
1045
+ note: sourceItems.length ? null : "no soul in this scope records a portable source address" };
999
1046
  const result = {
1000
1047
  operationsApi: 1, kernel: OATS_VERSION,
1001
1048
  scope: { context: ctx, requestedContext: requestedContext === ctx ? null : requestedContext, workspace: roots.length ? workspaceOf(roots[0]) : ctx, team: r.team || null, chain: chain.map((c) => ({ file: c._file, level: c._level, levelKind: levelOf(c._level) })), agentsRoots: roots },
1002
1049
  selected: { soul: selectedSoul?.name || null, agentsRoot: selectedSoul?.agentsRoot || null, home: home || null, source: meta ? "snapshot" : "config" },
1003
- souls, layers, capabilities, knowledge, snapshot, currentConfig,
1050
+ souls, sources, layers, capabilities, knowledge, snapshot, currentConfig,
1004
1051
  problems: [...(lockError ? [lockError] : []), ...packagedDiagnostics.map((d) => ({ code: d.code, message: d.message, capability: d.capability })),
1005
1052
  ...(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 })) : [])],
1006
1053
  };
@@ -3062,6 +3109,53 @@ function renderMergeRegion(r) {
3062
3109
  /** `oats catalog [--json]` — the effective official package catalog, read-only.
3063
3110
  * Identity/discovery for consumers that cannot import the kernel (Desktop):
3064
3111
  * never acquires, never trusts, never fetches. */
3112
+ /** `oats instance <git|diff> <instance>` — K1: read-only Git observation of one
3113
+ * instance's work tree. The instance is addressed qualified: an explicit
3114
+ * --home, or a name under the --dir scope (team roots included) that resolves
3115
+ * to exactly one home; several homes refuse with every candidate named. */
3116
+ function instanceCmd() {
3117
+ const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
3118
+ 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);
3121
+ dropAmbientRoot();
3122
+ let home = flag("home");
3123
+ if (home === true) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
3124
+ if (home !== undefined && !isAbsolute(home)) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
3125
+ if (home === undefined) {
3126
+ let root;
3127
+ try { root = ensureRoot(dirFlag()); } catch (e) { return bail(e.code || "E_NO_ROOT", e.message); }
3128
+ let r; try { r = resolveOatsConfig(dirFlag()); } catch (e) { return bail(e.code || "E_CONFIG_BROKEN", e.message); }
3129
+ const roots = [...new Set([root, ...(r.team ? teamAgentRoots(r.team.scope) : [])].map((p) => realOrResolved(p)))];
3130
+ const candidates = [];
3131
+ for (const rt of roots) for (const hit of findInstanceHomes(rt, name)) candidates.push({ root: rt, agent: hit.agent?.name ?? null, home: hit.home });
3132
+ if (!candidates.length) return bail("E_SESSION_UNKNOWN", `no instance ${JSON.stringify(name)} under ${roots.join(", ")}`);
3133
+ if (candidates.length > 1) return bail("E_AMBIGUOUS_INSTANCE", `instance ${JSON.stringify(name)} has ${candidates.length} homes; pass --home <abs>`, { candidates });
3134
+ home = candidates[0].home;
3135
+ } else if (basename(home) !== name) return bail("E_HOME_MISMATCH", `--home ${home} is not the home of instance ${JSON.stringify(name)}`);
3136
+ try {
3137
+ if (sub === "git") {
3138
+ const observed = observeInstanceGit(home);
3139
+ if (JSON_MODE) { jsonOk(observed); return; }
3140
+ const o = observed.observation;
3141
+ console.log(`${observed.instance} — ${shortPath(o.worktree)} @ ${o.branch ?? (o.detached ? `detached ${o.revision.slice(0, 12)}` : "unborn")}`);
3142
+ console.log(` upstream: ${observed.upstream.ref ? `${observed.upstream.ref} +${observed.upstream.ahead} -${observed.upstream.behind}` : "none (ahead/behind unknown)"}`);
3143
+ console.log(` base: ${observed.base.ref ? `${observed.base.ref} +${observed.base.ahead} -${observed.base.behind} (merge-base ${observed.base.mergeBase?.slice(0, 12)})` : "unknown"}`);
3144
+ console.log(` files: ${observed.files.length} (${Object.entries(observed.summary).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(", ") || "clean"})`);
3145
+ for (const f of observed.files) console.log(` ${f.xy} ${f.origPath ? `${f.origPath} -> ` : ""}${f.path} [${f.id}]`);
3146
+ for (const n of observed.notes) console.log(` note: ${n}`);
3147
+ return;
3148
+ }
3149
+ const fileId = flag("file"), revision = flag("revision"), indexRevision = flag("index-revision");
3150
+ if (fileId === true || revision === true || indexRevision === true) return bail("E_BAD_ARGS", usage);
3151
+ const d = diffInstanceFile(home, { fileId, revision, indexRevision });
3152
+ if (JSON_MODE) { jsonOk(d); return; }
3153
+ console.log(`${d.file.origPath ? `${d.file.origPath} -> ` : ""}${d.file.path} (${d.file.kind}, against ${d.against})${d.binary ? " [binary]" : ""}${d.truncated ? ` [truncated at ${d.limit} bytes]` : ""}`);
3154
+ if (!d.binary) process.stdout.write(d.patch);
3155
+ } catch (e) {
3156
+ bail(e.code || "E_GIT_FAILED", e.message, e.observation ? { observation: e.observation } : undefined);
3157
+ }
3158
+ }
3065
3159
  function catalogCmd() {
3066
3160
  const described = describeOfficialCatalog();
3067
3161
  if (JSON_MODE) { jsonOk(described); return; }
@@ -3960,7 +4054,7 @@ function statusTeam() {
3960
4054
 
3961
4055
  function spawnCmd() {
3962
4056
  // JSON mode: contract envelope, stable error codes, stderr-only progress.
3963
- const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
4057
+ const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
3964
4058
  const note = (msg) => (JSON_MODE ? console.error(msg) : console.log(msg));
3965
4059
  const yolo = yoloFlag();
3966
4060
  const backend = valueFlag("backend"), herdrSocket = valueFlag("herdr-socket");
@@ -4110,6 +4204,9 @@ function spawnCmd() {
4110
4204
  // model, runtime) is a fact about the selection, not a spawn-mechanism
4111
4205
  // failure: it keeps its own code so a GUI can act on it.
4112
4206
  if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_RUNTIME$/.test(e.code)) { bail(e.code, e.message); throw e; }
4207
+ // An unmet declared requirement is a fact about the soul's configuration
4208
+ // (with a remedy), not a spawn-mechanism failure: keep its code and details.
4209
+ 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; }
4113
4210
  bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
4114
4211
  }
4115
4212
  // The instance exists from here on: a failed wake save is reported beside
@@ -4418,7 +4515,8 @@ function onboardCmd() {
4418
4515
  const source = parsePortableSource(`git:${catalogSource.url}@${pkg.commit}#${pkg.path}`);
4419
4516
  const requires = { capabilities: Object.fromEntries(SETUP_CAPABILITIES.map(id => [id, { source: `${source.source}#${source.path}` }])) };
4420
4517
  const soulFile = join(created.soul, "soul.yaml");
4421
- writeFileAtomic(soulFile, readFileSync(soulFile, "utf8") + `requires: ${JSON.stringify(requires)}\ndefaults: ${JSON.stringify(edition.declaration.defaults)}\n`);
4518
+ writeFileAtomic(soulFile, readFileSync(soulFile, "utf8") + `requires: ${JSON.stringify(requires)}\ndefaults: ${JSON.stringify(edition.declaration.defaults)}\n`
4519
+ + `provenance: ${JSON.stringify({ kind: edition.source.kind, source: edition.source.source, revision: edition.source.revision, path: edition.source.path, ...(edition.source.workspaceRevision ? { workspaceRevision: edition.source.workspaceRevision } : {}) })}\n`);
4422
4520
  const agent = findAgent(root, SETUP_EXPERT), composition = composeInstanceAgentsMd(created.soul, deployment, SETUP_EXPERT, "directory", "local");
4423
4521
  planInstanceResources({ resolved: composition.resolved, soulDir: created.soul, agent, contextDir: deployment, composition });
4424
4522
  const argv = [process.execPath, CLI_BIN, "spawn", SETUP_EXPERT, "--dir", deployment, "--no-yolo", "--task", "Help me configure this deployment and adopt my workspace with explicit approvals."];
@@ -4469,11 +4567,17 @@ function createCmd() {
4469
4567
  work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo,
4470
4568
  instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
4471
4569
  });
4472
- if (args.includes("--json")) { console.log(JSON.stringify({ ...r, ...(bootstrapped ? { agentsRoot: root } : {}) }, null, 2)); return; }
4473
- for (const information of r.notes || []) console.error(`[${information.code}] ${information.message}`);
4570
+ // A declared oats.core is a requirement spawn will enforce: say the next
4571
+ // step here, not only at the refusal.
4572
+ const declared = r.declaredCapabilities || [];
4573
+ const inactive = declared.filter((id) => !(resolveOatsConfig(workspaceOf(root), name).capabilities || []).some((c) => c.id === id));
4574
+ const next = inactive.length ? [{ code: "next-step", message: `${name} declares ${inactive.join(", ")}; before spawning, acquire if needed (oats install oats.framework) and activate: ${inactive.map((id) => `oats use ${id} --soul ${name}`).join(" && ")}` }] : [];
4575
+ const notes = [...(r.notes || []), ...next];
4576
+ if (args.includes("--json")) { console.log(JSON.stringify({ ...r, ...(notes.length ? { notes } : {}), ...(bootstrapped ? { agentsRoot: root } : {}) }, null, 2)); return; }
4577
+ for (const information of notes) console.error(`[${information.code}] ${information.message}`);
4474
4578
  if (bootstrapped) console.log(`Created deployment root ${shortPath(root)} (this scope had no agents/ yet)`);
4475
4579
  console.log(`Created ${r.kind === "local" ? "LOCAL agent (uncommitted — soul lives in local-agents/, gitignored)" : "agent"} "${r.agent}" — soul at ${shortPath(r.soul)}`);
4476
- console.log(`Edit ${shortPath(join(r.soul, "AGENTS.md"))} to define its role, then: oats spawn ${r.agent} --task "..."`);
4580
+ console.log(`Edit ${shortPath(join(r.soul, "AGENTS.md"))} to define its role, then${inactive.length ? ` activate ${inactive.join(", ")} (above) and` : ":"} oats spawn ${r.agent} --task "..."`);
4477
4581
  }
4478
4582
 
4479
4583
  // ---------- capability command dispatch ----------
@@ -5174,6 +5278,7 @@ else if (cmd === "config") configCmd();
5174
5278
  else if (cmd === "trust") trust();
5175
5279
  else if (cmd === "list") listCmd();
5176
5280
  else if (cmd === "catalog") catalogCmd();
5281
+ else if (cmd === "instance") instanceCmd();
5177
5282
  else if (cmd === "remove") removeCmd();
5178
5283
  else if (cmd === "migrate") migrateCmd();
5179
5284
  else if (cmd === "root") console.log(resolve(new URL("..", import.meta.url).pathname));
@@ -5368,6 +5473,13 @@ Usage:
5368
5473
  scopes, trust state
5369
5474
  oats catalog [--json] the effective official package catalog (read-only:
5370
5475
  identity/discovery, no acquisition or trust)
5476
+ oats instance git <instance> [--home <abs>] [--dir <d>] [--json]
5477
+ read-only Git observation of the instance's work
5478
+ tree: branch, status (renames kept), ahead/behind
5479
+ vs upstream AND vs default-branch merge-base
5480
+ oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json]
5481
+ bounded diff of one observed file; refuses when
5482
+ the tree moved since the observation
5371
5483
  oats update <package> [<package>@<ref>] transactional package update: temp fetch,
5372
5484
  [--to <ref>] [--dir <d>] closure validation, diff, lock replace,
5373
5485
  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-22 07:00Z · main `508c4b5f`+ · **OATS v0.24.6 cutting** (PR45 slice 1a + `oats catalog`) · S8 slice 1b next
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
6
6
 
7
7
  Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
8
8
 
@@ -12,12 +12,12 @@ Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, n
12
12
  |---|---|---|---|---|
13
13
  | S1 | Knowledge capability contract rework (kernel↔provider boundary, OKF 2.x) | ✅ OATS 0.24.1 / OKF 2.1.1 published | P | done for this phase |
14
14
  | S2 | Workspace/Portable Souls adoption of the OATS repos | ✅ **FIRST SECOND-OPERATOR PUBLICATION (735296c5, 2026-09-21)**: fresh machine, published definition alone, no `launch` → `status: prepared`, resolution `sha256-f11c433c…`, zero problems. ❌ then OKF `check` → `needs-configuration / provider-not-qualified`, **no message/key/origins**, identical for `harvest-runtime` pi and claude — OKF's check phase is code-only and folds ≥4 causes into one code (P, OKF 2.1.3). ⏳ Pi `launch` blocked by kernel ifInstalled defect (L, 0.24.5). Earlier: ✅ knowledge: bound to public `oats-knowledge` via OKF 2.1.2 from a fresh machine, four kernel versions · ✅ messaging normalizes; `wider` deadlock gone; named reasons in one run · ❌ **operator's amended verdict (9816b8ec): no resolution can publish — behind `responsibleHuman`, helper composition refuses because `oats.core` and `oats.aweb` ship `inject` without `helperInjection`** (OKF adopted the contract; its siblings did not). Both remaining blockers are packaging, not operator input. Fix: [decision](https://github.com/awebai/oats/blob/main/agents/oats-expert/soul/knowledge/decisions/helper-injection-policy-on-every-injecting-capability.md) — core `inherit`, aweb `omit`, framework release check, attributed early refusal · then unchanged re-run | lead, Antares | oats.framework 1.1.3 + aweb 1.11.2 + pins; re-run |
15
- | S3 | Messaging capability readiness on the new infrastructure (aweb) | ✅ aweb 1.11.0 released · ✅ **catalog + six editions pin v1.11.0 (0.24.2)** · ⬜ second-operator re-run | P, lead | Antares re-run |
15
+ | S3 | Messaging capability readiness on the new infrastructure (aweb) | ✅ aweb 1.11.0 released · ✅ **catalog + six editions pin v1.11.0 (0.24.2)** · ✅ **second-operator `launch` re-run on 0.24.6 (Antares, 2026-09-21)**: resolution `sha256-0cbedec4…` published WITH launch selection {pi, model}; helper `oats.okf:memory-harvest` resolved alongside; hard runtime row (`delivery: channel`) refused attributed with capability/slot/runtime/package/install/origin — no bare `needs-configuration`. `check` half parked with OKF 2.1.3 by direction | P, lead | Antares re-run |
16
16
  | S4 | Official capabilities `oats.core` / `oats.setup` + explicit default + onboarding `oats-setup-expert` | ✅ D1, D2, **D3 merged (PR35)**: `oats onboard` verified live (acquire 1.1.1 → setup expert with both caps → scaffold composes the five capability skills, no legacy) · `oats.framework` 1.1.1 tagged | P, L | done; Desktop surfaces → S8 |
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 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 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.
@@ -14,7 +14,8 @@ Status: **proposal accepted for direction** by the lead on 2026-09-22; contracts
14
14
  | 3 | 03 Souls + Sources: imported editions + local souls, requirements, provenance, editability | **K4** |
15
15
  | 4 | 04 Capabilities: official catalog (`oats catalog --json`, 0.24.6+) + deployment inventory/readiness/used-by; Add capability = exact command | landed catalog + list/inspect + **K5** |
16
16
  | 5 | 09 First-run readiness quartet; View policy; Skip/Enrol | **K5** + enrollment decision |
17
- | 6 | 02 Spawn (two-column): soul chooser, provider/model, launch config (restored), work-area naming/worktree/base+branch, opening instruction, attach knowledge, child spawns, auto-PR, readiness, ⌘↵ | **K6** (+ knowledge-node JSON from the knowledge provider; 05 excluded but attach stays) |
17
+ | 6a | 02 Spawn modal **design parity on existing seams** (human pulled forward 2026-09-22): two-column layout, soul chooser, provider/model dropdowns, launch config restored, opening instruction, readiness from known facts (`unknown` where not), ⌘↵ guards; not-yet-backed fields rendered disabled with "available after <seam>" | existing spawn/launch-config seams |
18
+ | 6b | 02 Spawn fields live: soul chooser, provider/model, launch config (restored), work-area naming/worktree/base+branch, opening instruction, attach knowledge, child spawns, auto-PR, readiness, ⌘↵ | **K6** (+ knowledge-node JSON from the knowledge provider; 05 excluded but attach stays) |
18
19
  | 7a | 07 Active overview: counts, relations, activity/waiting-on-you, actions, pan/zoom | **K7** |
19
20
  | 7b | 08 Schedules: table/toggles/new/edit, next/last, recent runs, transcript handoff, captured-policy preservation | **K8** |
20
21
  | 8 | 10 Components: dropdowns, workspace join/manage, Open in split, Detach to window, Open worktree in editor, actions, toasts, collapsed rail | existing seams + Desktop IPC review for detach/editor |
@@ -23,15 +24,18 @@ Status: **proposal accepted for direction** by the lead on 2026-09-22; contracts
23
24
 
24
25
  Envelope `{schemaVersion:1, ok, result|error}` unchanged. Requests address a server-admitted exact target (`{home, server}` / exact source+revision+soul), never a renderer cwd. Results echo target + `contract`, `version`, `observedAt`, opaque `revision`, typed `problems[]`, explicit completeness/truncation. `null` = not known; empty = observed empty only when complete. Per-section availability `available | not-applicable | unavailable | denied | unsupported | error`. No stack traces, auth stderr, tokens or token-bearing URLs in renderer data. Remote paths are provenance, never local authority. Read-only calls never install, trust, enroll, spawn, fetch into the operator's worktree, switch branches or repair config.
25
26
 
27
+
28
+ > **Ownership amendment (2026-09-22, human):** contract-dependent slices are not waits — the team implements the contracts. Kernel seams may be assigned to the Desktop engineer under lead review (the `lib/`/`bin/` lane rule is lifted per assigned seam). Current split (revised 17:10Z): **K1, P1 Decision, K3, K5 → lead** — the engineer's composed instructions forbid kernel edits and a mail cannot recompose them (role change = soul edit + recomposition; spawns blocked by the deployment's pi-profile pin). Engineer: slice 3 now (K4 merged), then 2a after K1. Kernel PRs: full root `npm test` + DTO in `docs/desktop-cli-api.md` in the same PR.
29
+
26
30
  ## Seams
27
31
 
28
32
  - **K1 `oats.instance-git` / `oats.instance-diff`** (kernel): typed per-instance Git state — worktree, head, upstream/merge-base comparison (missing upstream ≠ 0/0), NUL-delimited changes with rename paths and per-file counts; bounded unified diff by opaque file id + observation revision (stale selection refuses, never a different file). Replaces the Desktop-only `instance.git` aggregate whose fallback zeros can masquerade as clean.
29
33
  - **P1 `oats.instance-github`** (provider, not kernel): PR summary/checks/reviews through an additive Git/review **capability** with its own credential policy (native custody; Desktop never runs `gh`, reads tokens or opens credential forms). Needs a kernel dispatch contract for additive-capability structured views (today `operation run` accepts only knowledge/messaging/tasks). "No PR" ≠ unavailable ≠ unauthenticated ≠ rate-limited. Checks bind to exact head OID. Review markdown is untrusted text.
30
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.
31
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.
32
- - **K4 souls/sources enumeration**: qualified list of imported editions + authored local souls with identity/source/revision/requirements/declarations/editability; readiness separate from launchability and adoption; no renderer YAML.
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.
33
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.
34
- - **K6 spawn preview/apply**: 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.
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.
35
39
  - **K7 activity feed**: bounded typed events per instance with provenance; "waiting on you" only from a producer that reports it.
36
40
  - **K8 schedule run history** + captured-policy-preserving edit contract; transcript access via the owning CLI/provider.
37
41
 
@@ -46,6 +46,109 @@ no progress prose (progress goes to stderr):
46
46
  - success (exit 0): `{"schemaVersion":1,"ok":true,"result":{...}}`
47
47
  - failure (nonzero exit): `{"schemaVersion":1,"ok":false,"error":{"code":"...","message":"..."}}`
48
48
 
49
+ ## Souls and sources (`oats inspect --json`, `soulsApi: 1`, OATS 0.24.7+)
50
+
51
+ Every entry in `result.souls[]` carries what the soul's **own `soul.yaml`
52
+ declares**, parsed by the kernel — a consumer never parses YAML and never
53
+ infers a field that is not there:
54
+
55
+ - `soulsApi: 1`
56
+ - `declarations: { requires, defaults, knowledge, teams, resources }` — each
57
+ the declared object, or `null` when the section is absent.
58
+ - `provenance: { kind, source, revision, path, workspaceRevision } | null` —
59
+ where this soul copy came from, as recorded by the kernel when it created
60
+ it (`oats onboard` records `packaged-definition` or
61
+ `exported-edition-copy`). Souls created before 0.24.7 or authored by hand
62
+ read `null`; render that as *unrecorded*, not as local or as anything else.
63
+ - `readiness` — the soul's **declared sources**, joined against
64
+ `result.capabilities[]` from the same payload. Distinct from launchability
65
+ (`oats spawn`) and adoption (`oats prepare`); never a green "Ready".
66
+ - `source: "recorded" | "unrecorded"`
67
+ - `requirements: [{ capability, source, installed, approved, active, version }] | null`
68
+ (`null` = nothing declared). `installed: false` = not in the inventory;
69
+ `approved`/`active`/`version` are `null` when there is no inventory row.
70
+ - `status: "undeclared" | "sources-installed" | "sources-missing" | "unknown"`
71
+ - `declarationProblems: [{ code, message }]` — an unreadable file reports why;
72
+ the soul is still listed.
73
+
74
+ `result.sources` is the scope's **portable source context**: the distinct
75
+ provenance sources its souls record.
76
+
77
+ ```json
78
+ {"soulsApi":1,"kind":"recorded-provenance","note":null,
79
+ "items":[{"kind":"exported-edition-copy","source":"git:https://…/oats.git","revision":"<sha>","path":"souls/oats-setup-expert","workspaceRevision":"<sha>","souls":["oats-setup-expert"]}]}
80
+ ```
81
+
82
+ `kind: "none-recorded"` (empty `items`, explanatory `note`) means no soul in the
83
+ scope records a portable **source address** — a soul may still carry a
84
+ `provenance` of kind `packaged-definition` with `source: null`. Say "no portable
85
+ source recorded"; do not infer "local" or "authored" from this state.
86
+ Workspace imports adopted onto a deployment will appear here at their pinned
87
+ revisions when that adoption is recorded on the deployment; nothing is
88
+ enumerated from a source repository that the deployment does not record.
89
+
90
+ ## Instance Git state (`oats instance git|diff`, `instanceGitApi: 1`, OATS 0.24.7+)
91
+
92
+ Read-only observation of one instance's **work tree**. Truth comes from the
93
+ tree — the branch the tree is on, not the branch recorded at spawn (that is
94
+ reported under `recorded` with a `drift` flag). Fixed-argv `git`, no shell.
95
+
96
+ Address the instance qualified: `oats instance git <instance> --dir <scope>`
97
+ resolves the name under the scope's agents roots (team roots included) and
98
+ **refuses when several homes match** (`E_AMBIGUOUS_INSTANCE`, `details.candidates`);
99
+ pass `--home <abs>` to pick one. Unknown → `E_SESSION_UNKNOWN`; retired or
100
+ un-materialized tree → `E_NO_WORKTREE`.
101
+
102
+ ```json
103
+ {"instanceGitApi":1,"instance":"dev-1","agent":"dev","home":"/abs/home","workMode":"worktree",
104
+ "observation":{"revision":"<HEAD oid|unborn>","indexRevision":"<index tree oid>","at":"<iso>","worktree":"/abs/work","branch":"feat/y","detached":false,"unborn":false},
105
+ "recorded":{"branch":"feat/x","repo":"/abs/repo","drift":true},
106
+ "upstream":{"ref":"origin/feat/y","ahead":1,"behind":0},
107
+ "base":{"ref":"origin/main","source":"origin/HEAD","mergeBase":"<oid>","ahead":2,"behind":0},
108
+ "summary":{"changed":1,"renamed":1,"copied":0,"unmerged":0,"untracked":1},
109
+ "files":[{"id":"<24 hex>","kind":"renamed","xy":"R.","submodule":false,"score":"R100","path":"src/new.txt","origPath":"src/old.txt"}],
110
+ "notes":[]}
111
+ ```
112
+
113
+ - `upstream` and `base` are **two separate comparisons**. No upstream →
114
+ `upstream: {ref:null, ahead:null, behind:null}` — unknown, **not 0/0**. `base`
115
+ is against the merge-base with the default branch (`origin/HEAD`, else a
116
+ well-known name; `source` says which); none found → all `null` plus a note.
117
+ - Status is porcelain v2, NUL-delimited: renames/copies carry `origPath`;
118
+ paths with spaces/newlines are intact. `kind` ∈ changed | renamed | copied |
119
+ unmerged | untracked. Ignored files are not listed.
120
+ - `files[].id` is **opaque**, minted under (`revision`, `indexRevision`). It is
121
+ the only way to ask for a diff.
122
+
123
+ `oats instance diff <instance> --file <id> --revision <rev> [--index-revision <idx>] --json`
124
+ returns a bounded unified diff:
125
+
126
+ ```json
127
+ {"instanceGitApi":1,"observation":{…},"file":{"id":"…","kind":"changed","xy":".M","path":"README.md","origPath":null},
128
+ "against":"<captured revision oid>","binary":false,"bytes":2683,"truncated":false,"limit":262144,"patch":"diff --git …",
129
+ "readOnly":{"helpers":"disabled","optionalLocks":"off","objectsWritten":0}}
130
+ ```
131
+
132
+ - `against` is the **captured revision oid** for tracked changes (working tree
133
+ vs that exact commit, index included — never the moving `HEAD`) and `empty`
134
+ for untracked files. Binary → `binary: true`, empty patch. Over 256 KiB →
135
+ `truncated: true` at the byte limit.
136
+ - **Read-only, helper-free, consistent across the read** (`readOnly` echoes
137
+ it): the observed tree may carry a hostile repo config, so external diff,
138
+ textconv, fsmonitor and hooks are disabled and the caller's Git environment
139
+ and global config are not inherited; `--no-optional-locks` means no index
140
+ refresh and no object is written (`ls-files --stage` hash, not `write-tree`).
141
+ After producing the patch the CLI re-checks HEAD, index and the file's own
142
+ content against the observation and refuses `E_STALE_OBSERVATION` if any
143
+ moved mid-read — the result is never internally inconsistent.
144
+ - If HEAD or the index moved since the id was minted, or the id is not in the
145
+ current observation, the CLI **refuses** with `E_STALE_OBSERVATION` and
146
+ attaches the current `observation` in `error.details` — re-observe, never
147
+ render a diff against a tree that is not the one on screen. A path in
148
+ `--file` is `E_BAD_ARGS`.
149
+
150
+ No GitHub/PR data here: that is a capability seam (see the `oats.git` decision).
151
+
49
152
  ## Mutations exposed to Desktop v1
50
153
 
51
154
  The commands below use the same envelope. Additional capability operations
@@ -0,0 +1,81 @@
1
+ # OATS v0.24.7 — instance Git observation, soul declarations, and no hollow agents
2
+
3
+ Kernel/Pi/Desktop **0.24.7**. Two additive read-only contracts for the Desktop
4
+ (`instanceGitApi: 1`, `soulsApi: 1`), one spawn refusal that closes a
5
+ first-team-path defect, and four Desktop parity slices.
6
+
7
+ ## Kernel
8
+
9
+ - **`oats instance git <instance> [--home] [--dir] [--json]`** and
10
+ **`oats instance diff <instance> --file <id> --revision <rev> [--index-revision <idx>] [--json]`** (K1) —
11
+ read-only Git observation of one instance's work tree. Truth from the tree:
12
+ the branch the tree is on (the spawn-recorded branch is reported as
13
+ `recorded` with a `drift` flag). Porcelain v2 NUL status with renames keeping
14
+ both paths; **upstream and default-branch merge-base as two separate
15
+ comparisons** — no upstream is `null`, never 0/0. Opaque file ids minted
16
+ under (HEAD, index); a diff is taken against the *captured* revision oid and
17
+ refuses `E_STALE_OBSERVATION` (fresh observation attached) if HEAD, index or
18
+ the file's content moved, before **or during** the read. Bounded (256 KiB,
19
+ binary flagged). Qualified addressing: several homes with one name refuse
20
+ `E_AMBIGUOUS_INSTANCE` with candidates; retired/unmaterialized tree is
21
+ `E_NO_WORKTREE`.
22
+ **Hardened after a consumer-side adversarial probe**: the observed tree is
23
+ worked in by an agent, so its repo config is input — external diff, textconv,
24
+ fsmonitor and hooks are disabled, the caller's Git environment and global/
25
+ system config are not inherited, optional locks are off (no index refresh),
26
+ and the index revision is a hash of `ls-files --stage` (no `write-tree`, no
27
+ object written). The response states it: `readOnly: {helpers, optionalLocks, objectsWritten}`.
28
+ - **`oats inspect --json` souls carry their declarations** (K4, `soulsApi: 1`):
29
+ `declarations` (requires/defaults/knowledge/teams/resources, parsed
30
+ kernel-side, `null` when absent), `provenance` (recorded by `oats onboard`
31
+ from now on; `null` = *unrecorded*, never inferred), `readiness` (declared
32
+ sources joined against the same payload's capability inventory — separate
33
+ from launchability and adoption; never a green "Ready"), and
34
+ `result.sources` — the scope's portable source context, with an honest
35
+ `none-recorded` state.
36
+ - **No hollow agents.** `oats create` declares `oats.core` by default; composition
37
+ honours the declaration by suppressing the kernel's legacy skills — but nothing
38
+ on that path activates the replacement, so a `spawn` produced an agent with
39
+ **no operational curriculum and no warning** (second-operator finding on 0.24.6).
40
+ Now `spawn` refuses `E_REQUIREMENT_INACTIVE` before creating anything, with
41
+ the remedy (`oats install oats.framework` if needed, then
42
+ `oats use oats.core --soul <name>`) and the opt-out (remove the declaration).
43
+ `create` states the activation step up front (`next-step` note,
44
+ `declaredCapabilities` in `--json`). `create` stays declaration, not acquisition.
45
+
46
+ ## Desktop (parity slices 1b, 2a, 3, 4, 6a, 7a)
47
+
48
+ - **1b** shared contextual right panel (Instance · Git & GitHub · Soul), collapsed
49
+ rail, per-workspace preferences, temporary focus mode.
50
+ - **2a** Git & GitHub panel on K1: worktree/branch/drift, upstream and base named
51
+ separately, real file counts and rename paths, bounded read-only diff; stale
52
+ → re-observe, never rendered. The legacy background `gitState` collector —
53
+ which ran Git against every instance tree on every roster poll without helper
54
+ controls and substituted healthy zeros on failure — is **removed**. Desktop
55
+ Git reads are the K1 route only. GitHub/PR card is *unavailable* pending the
56
+ `oats.git` decision.
57
+ - **3** Souls + Sources on K4: declarations, recorded provenance (`null` renders
58
+ *Unrecorded*, never *Local*), "Not declared" vs "Not reported" distinguished,
59
+ sources-installed ≠ Ready.
60
+ - **4** Capabilities: official catalog via `oats catalog` (0.24.6+) plus the
61
+ deployment's classic inventory; aliases are mappings not exports, refs are refs,
62
+ override catalogs conspicuously labelled; "Add capability" copies the schema's
63
+ exact `oats install <package>` argv and never executes it.
64
+ - **6a** Spawn modal to the redesign on existing seams: two-column layout,
65
+ grouped soul chooser, provider/model with reported-vs-assumed provenance,
66
+ launch configuration restored (an already-supported property the renderer had
67
+ regressed out of), fields that need K6 rendered disabled with their seam named.
68
+ - **7a** Active overview (frame 07) on the roster: anonymous count-labelled
69
+ relation groups (the roster reports no group names), activity `unknown` until K7.
70
+
71
+ Desktop CLI floor for the new views is **0.24.7** (`instanceGitApi`/`soulsApi`
72
+ gates); an older CLI renders those views as *unavailable*, never as empty-healthy.
73
+
74
+ ## Also
75
+
76
+ - Board and seams doc dates corrected (2026-09-21, not 09-22 — lead clock error
77
+ caught by the second operator). Pinned SHAs unaffected.
78
+ - Second-operator `launch` re-run on 0.24.6 passed both halves (publish with an
79
+ executable launch selection; hard runtime row refuses attributed).
80
+ - OKF 2.1.3 bug list (deferred with harvest): `check` per-cause reasons; retire
81
+ leaves the per-source `okf-<id>` schedule definition enabled.
package/lib/core.mjs CHANGED
@@ -4248,6 +4248,22 @@ export function planInstanceResources({ resolved, soulDir, agent, contextDir, co
4248
4248
  for (const b of composition.blocks || []) expected.push({ type: "instruction-block", source: b.source, declared: b.file, path: b.file });
4249
4249
  }
4250
4250
 
4251
+ // A soul that DECLARES an operational capability (requires.capabilities,
4252
+ // e.g. oats.core written by `oats create`) has, by that declaration, given up
4253
+ // the kernel's legacy skill injection. If the declared capability is not
4254
+ // active for this soul in this context, the instance would start with no
4255
+ // operational curriculum and no warning — a hollow agent. That is an unmet
4256
+ // declared requirement: refuse, attributed, with the exact remedy. Removing
4257
+ // the declaration from soul.yaml is the supported way to opt out.
4258
+ const declaredOps = declaredOperationalCapabilities(soulDir);
4259
+ const inactiveOps = declaredOps.filter((id) => !(resolved.capabilities || []).some((c) => c.id === id));
4260
+ if (inactiveOps.length) {
4261
+ const soulName = agent?.name ?? basename(dirname(soulDir));
4262
+ const remedy = inactiveOps.map((id) => `oats use ${id} --soul ${soulName}${contextDir ? ` --dir ${contextDir}` : ""}`).join(" && ");
4263
+ throw Object.assign(oatsError("E_REQUIREMENT_INACTIVE", `soul ${soulName} declares ${inactiveOps.join(", ")} in requires.capabilities, but ${inactiveOps.length === 1 ? "it is" : "they are"} not active for this soul in ${contextDir}; the declaration suppresses the kernel's legacy skills, so the instance would start with no operational curriculum.\n Acquire if needed (\`oats install oats.framework\`), then activate: ${remedy}\n Or remove the declaration from ${join(soulDir, "soul.yaml")} to run without it.`),
4264
+ { soul: soulName, capabilities: inactiveOps, context: contextDir, remedy });
4265
+ }
4266
+
4251
4267
  // A capability that declares a REQUIRED hook it cannot execute must not spawn.
4252
4268
  // Advisory executable hooks stay disabled-with-warning; a required one is a
4253
4269
  // promise, and starting without it is the failure required:true exists to stop.
@@ -5196,7 +5212,7 @@ export function writeSoul(root, { name, kind, repo, work, runtime, model, yolo,
5196
5212
  home: soulDir, instance: name, agentName: name, soulDir,
5197
5213
  contextDir: ctx, workspaceDir: workspaceOf(root), rootDir: root, resolved,
5198
5214
  });
5199
- return { agentDir, soulDir, ...(notes.length ? { notes } : {}) };
5215
+ return { agentDir, soulDir, declaredCapabilities: Object.keys(requires?.capabilities ?? {}), ...(notes.length ? { notes } : {}) };
5200
5216
  }
5201
5217
  function defaultSoulAgentsMd(name, description) {
5202
5218
  return `# ${name}
@@ -5218,8 +5234,8 @@ export function createAgent(root, o) {
5218
5234
  // kind: "local" → a FULL soul (memory, skills, instances) under the scope's
5219
5235
  // local-agents/ — uncommitted by contract; otherwise a committed persistent soul.
5220
5236
  const kind = o.local || o.kind === "local" ? "local" : "persistent";
5221
- const { agentDir, notes } = writeSoul(root, { ...o, name, kind });
5222
- return { agent: name, kind, soul: soulOf(agentDir), ...(notes ? { notes } : {}) };
5237
+ const { agentDir, notes, declaredCapabilities } = writeSoul(root, { ...o, name, kind });
5238
+ return { agent: name, kind, soul: soulOf(agentDir), declaredCapabilities, ...(notes ? { notes } : {}) };
5223
5239
  }
5224
5240
 
5225
5241
  /** Upsert a local agent soul (from raw instructions or a Claude-style def file).
@@ -0,0 +1,204 @@
1
+ /** K1 — read-only Git observation of one instance's work tree.
2
+ *
3
+ * Truth comes from the tree itself (never from recorded spawn metadata):
4
+ * branch/HEAD via the worktree, status via porcelain v2 NUL records, ahead/
5
+ * behind reported twice and separately (upstream; merge-base with the
6
+ * repository's default branch) with "no upstream" ≠ 0/0. Diffs are bounded and
7
+ * addressed by an opaque file id minted with an observation revision; a diff
8
+ * against a tree that has since moved is refused, never served. */
9
+ import { execFileSync } from "node:child_process";
10
+ import { createHash } from "node:crypto";
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { oatsError } from "./errors.mjs";
14
+
15
+ const GIT_MAX_BUFFER = 64 * 1024 * 1024;
16
+ const DIFF_MAX_BYTES = 256 * 1024;
17
+ export const INSTANCE_GIT_API = 1;
18
+
19
+ /** Every invocation is read-only and helper-free: the tree being observed may
20
+ * carry a hostile repo config (an agent works there), so external diff /
21
+ * textconv drivers, fsmonitor and hooks are disabled explicitly, the caller's
22
+ * Git environment is not inherited, and optional locks are off so status/diff
23
+ * never refresh (write) the index. */
24
+ const READ_ONLY_GIT = ["--no-optional-locks",
25
+ "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "-c", "diff.external=", "-c", "core.pager=cat",
26
+ "-c", "core.untrackedCache=false", "-c", "index.threads=1", "-c", "safe.bareRepository=explicit"];
27
+ function gitEnv() {
28
+ const env = { PATH: process.env.PATH ?? "", HOME: process.env.HOME ?? "", LANG: "C", LC_ALL: "C", GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1" };
29
+ // The user's global config may name helpers too; observations do not need it.
30
+ env.GIT_CONFIG_GLOBAL = "/dev/null";
31
+ return env;
32
+ }
33
+ function git(cwd, argv, { allowFail = false, input, diffExit = false } = {}) {
34
+ const [sub, ...rest] = argv;
35
+ const extra = sub === "diff" ? ["--no-ext-diff", "--no-textconv", "--no-color"] : [];
36
+ try {
37
+ return execFileSync("git", [...READ_ONLY_GIT, "-C", cwd, sub, ...extra, ...rest], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER, input, shell: false, timeout: 30_000, env: gitEnv() });
38
+ } catch (e) {
39
+ // `git diff --no-index` exits 1 when the inputs differ: that is the answer, not a failure.
40
+ if (diffExit && e.status === 1 && typeof e.stdout === "string") return e.stdout;
41
+ if (allowFail) return null;
42
+ throw oatsError("E_GIT_FAILED", `git ${sub} failed in ${cwd}: ${String(e.stderr ?? e.message ?? "").trim() || "unknown error"}`);
43
+ }
44
+ }
45
+ const trim = (s) => (s === null ? null : s.trim());
46
+
47
+ /** The instance's work tree, from its home. The home's instance.json names
48
+ * the work mode; the tree is `<home>/work` (a directory or a symlink to the
49
+ * shared checkout). Missing/retired → attributed refusal, never a crash. */
50
+ export function instanceWorkTree(home) {
51
+ const metaFile = join(home, "instance.json");
52
+ if (!existsSync(metaFile)) throw oatsError("E_SESSION_UNKNOWN", `${home} is not an OATS instance home (no instance.json)`);
53
+ let meta;
54
+ try { meta = JSON.parse(readFileSync(metaFile, "utf8")); } catch (e) { throw oatsError("E_SESSION_UNKNOWN", `${metaFile}: ${e.message}`); }
55
+ const work = join(home, "work");
56
+ if (!existsSync(work)) throw oatsError("E_NO_WORKTREE", `${meta.instance ?? home} has no work tree at ${work} (retired, recovered, or never materialized)`);
57
+ const inside = trim(git(work, ["rev-parse", "--is-inside-work-tree"], { allowFail: true }));
58
+ if (inside !== "true") throw oatsError("E_NO_WORKTREE", `${work} is not inside a git work tree`);
59
+ return { meta, work, mode: meta.work ?? null };
60
+ }
61
+
62
+ /** Porcelain v2 `-z` records → entries. Renames carry both paths. */
63
+ export function parsePorcelainV2(raw) {
64
+ const fields = raw.split("\0");
65
+ const entries = [];
66
+ let branch = { oid: null, head: null, upstream: null, ahead: null, behind: null };
67
+ for (let i = 0; i < fields.length; i++) {
68
+ const rec = fields[i];
69
+ if (!rec) continue;
70
+ if (rec.startsWith("# ")) {
71
+ const [, key, ...rest] = rec.split(" ");
72
+ const value = rest.join(" ");
73
+ if (key === "branch.oid") branch.oid = value === "(initial)" ? null : value;
74
+ else if (key === "branch.head") branch.head = value === "(detached)" ? null : value;
75
+ else if (key === "branch.upstream") branch.upstream = value;
76
+ else if (key === "branch.ab") { const m = /^\+(\d+) -(\d+)$/.exec(value); if (m) { branch.ahead = Number(m[1]); branch.behind = Number(m[2]); } }
77
+ continue;
78
+ }
79
+ const type = rec[0];
80
+ if (type === "1") {
81
+ const parts = rec.split(" ");
82
+ entries.push({ kind: "changed", xy: parts[1], submodule: parts[2] !== "N...", path: parts.slice(8).join(" "), origPath: null });
83
+ } else if (type === "2") {
84
+ const parts = rec.split(" ");
85
+ const path = parts.slice(9).join(" ");
86
+ const origPath = fields[++i] ?? null; // rename/copy: the original path is the next NUL field
87
+ entries.push({ kind: /^R/.test(parts[8]) ? "renamed" : "copied", xy: parts[1], submodule: parts[2] !== "N...", score: parts[8], path, origPath });
88
+ } else if (type === "u") {
89
+ const parts = rec.split(" ");
90
+ entries.push({ kind: "unmerged", xy: parts[1], submodule: parts[2] !== "N...", path: parts.slice(10).join(" "), origPath: null });
91
+ } else if (type === "?") entries.push({ kind: "untracked", xy: "??", submodule: false, path: rec.slice(2), origPath: null });
92
+ else if (type === "!") entries.push({ kind: "ignored", xy: "!!", submodule: false, path: rec.slice(2), origPath: null });
93
+ }
94
+ return { branch, entries };
95
+ }
96
+
97
+ function defaultBranch(work) {
98
+ const sym = trim(git(work, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], { allowFail: true }));
99
+ if (sym) return { ref: sym, source: "origin/HEAD" };
100
+ for (const candidate of ["origin/main", "origin/master", "main", "master"]) {
101
+ if (git(work, ["rev-parse", "--verify", "--quiet", `${candidate}^{commit}`], { allowFail: true }) !== null) return { ref: candidate, source: "well-known" };
102
+ }
103
+ return null;
104
+ }
105
+
106
+ function countRange(work, range) {
107
+ const out = trim(git(work, ["rev-list", "--left-right", "--count", range], { allowFail: true }));
108
+ if (out === null) return null;
109
+ const [left, right] = out.split(/\s+/).map(Number);
110
+ return { left, right };
111
+ }
112
+
113
+ function indexRevisionOf(work) {
114
+ const listing = git(work, ["ls-files", "--stage", "-z"], { allowFail: true });
115
+ return listing === null ? "no-index" : createHash("sha256").update(listing).digest("hex").slice(0, 40);
116
+ }
117
+ function blobOf(work, path) {
118
+ // Content fingerprint of the working-tree file without writing an object.
119
+ return trim(git(work, ["hash-object", "--no-filters", "--", path], { allowFail: true }));
120
+ }
121
+ function fileId(revision, indexOid, entry) {
122
+ return createHash("sha256").update(`${revision}\0${indexOid}\0${entry.kind}\0${entry.path}\0${entry.origPath ?? ""}`).digest("hex").slice(0, 24);
123
+ }
124
+
125
+ /** One consistent observation of the tree. Every field is what git said. */
126
+ export function observeInstanceGit(home) {
127
+ const { meta, work, mode } = instanceWorkTree(home);
128
+ const headOid = trim(git(work, ["rev-parse", "--verify", "--quiet", "HEAD"], { allowFail: true }));
129
+ const raw = git(work, ["status", "--porcelain=v2", "-z", "--branch", "--untracked-files=all", "--ignore-submodules=none"]);
130
+ const { branch, entries } = parsePorcelainV2(raw);
131
+ // The index state participates in the revision so that a stage/unstage
132
+ // between observation and diff is a moved tree, not a stale-but-served diff.
133
+ // Hashed from the index listing: no `write-tree`, so observing creates no object.
134
+ const indexOid = indexRevisionOf(work);
135
+ const revision = headOid ?? "unborn";
136
+ const at = new Date().toISOString();
137
+ const upstream = branch.upstream
138
+ ? { ref: branch.upstream, ahead: branch.ahead, behind: branch.behind }
139
+ : { ref: null, ahead: null, behind: null };
140
+ const base = defaultBranch(work);
141
+ let baseComparison = { ref: null, source: null, mergeBase: null, ahead: null, behind: null };
142
+ if (base && headOid) {
143
+ const mergeBase = trim(git(work, ["merge-base", "HEAD", base.ref], { allowFail: true }));
144
+ const counts = mergeBase ? countRange(work, `${base.ref}...HEAD`) : null;
145
+ baseComparison = { ref: base.ref, source: base.source, mergeBase, ahead: counts ? counts.right : null, behind: counts ? counts.left : null };
146
+ }
147
+ const files = entries.filter((e) => e.kind !== "ignored").map((e) => ({ id: fileId(revision, indexOid, e), ...e }));
148
+ const summary = { changed: 0, renamed: 0, copied: 0, unmerged: 0, untracked: 0 };
149
+ for (const f of files) summary[f.kind]++;
150
+ return {
151
+ instanceGitApi: INSTANCE_GIT_API,
152
+ instance: meta.instance ?? null, agent: meta.agent ?? null, home, workMode: mode,
153
+ observation: { revision, indexRevision: indexOid, at, worktree: work, branch: branch.head, detached: branch.head === null && headOid !== null, unborn: headOid === null },
154
+ recorded: { branch: meta.branch ?? null, repo: meta.repo ?? null, drift: meta.branch !== undefined && meta.branch !== null && branch.head !== meta.branch },
155
+ upstream, base: baseComparison,
156
+ summary, files,
157
+ notes: [
158
+ ...(upstream.ref === null ? ["no upstream configured: upstream ahead/behind are unknown, not zero"] : []),
159
+ ...(baseComparison.ref === null ? ["no default branch found (origin/HEAD, origin/main, origin/master, main, master): base comparison unknown"] : []),
160
+ ],
161
+ };
162
+ }
163
+
164
+ /** A bounded unified diff for one observed file. The caller passes the id
165
+ * and the observation revision it was minted under; a moved tree refuses. */
166
+ export function diffInstanceFile(home, { fileId: id, revision, indexRevision } = {}) {
167
+ if (typeof id !== "string" || !/^[a-f0-9]{24}$/.test(id)) throw oatsError("E_BAD_ARGS", "--file needs the opaque file id from `oats instance git --json`");
168
+ if (typeof revision !== "string" || !revision) throw oatsError("E_BAD_ARGS", "--revision needs the observation revision the file id was minted under");
169
+ const current = observeInstanceGit(home);
170
+ if (current.observation.revision !== revision || (indexRevision !== undefined && current.observation.indexRevision !== indexRevision)) {
171
+ throw Object.assign(oatsError("E_STALE_OBSERVATION", "the work tree moved since this file id was observed; re-observe with `oats instance git --json`"), { observation: current.observation });
172
+ }
173
+ const file = current.files.find((f) => f.id === id);
174
+ if (!file) throw Object.assign(oatsError("E_STALE_OBSERVATION", "this file id is not part of the current observation; re-observe"), { observation: current.observation });
175
+ const work = current.observation.worktree;
176
+ const paths = file.origPath ? [file.origPath, file.path] : [file.path];
177
+ const before = { blob: blobOf(work, file.path) };
178
+ let patch, binary = false;
179
+ if (file.kind === "untracked") {
180
+ const numstat = trim(git(work, ["diff", "--no-index", "--numstat", "--", "/dev/null", file.path], { diffExit: true, allowFail: true }));
181
+ binary = /^-\t-\t/.test(numstat ?? "");
182
+ patch = binary ? "" : (git(work, ["diff", "--no-index", "--", "/dev/null", file.path], { diffExit: true, allowFail: true }) ?? "");
183
+ } else {
184
+ const numstat = trim(git(work, ["diff", current.observation.revision, "--numstat", "-M", "--", ...paths], { allowFail: true }));
185
+ binary = /^-\t-\t/.test(numstat ?? "");
186
+ patch = binary ? "" : git(work, ["diff", current.observation.revision, "-M", "--", ...paths]);
187
+ }
188
+ // Consistency across the read, not only before it: HEAD, index and the file's
189
+ // own content must be what the observation said when the patch was produced.
190
+ const after = { revision: trim(git(work, ["rev-parse", "--verify", "--quiet", "HEAD"], { allowFail: true })) ?? "unborn", indexRevision: indexRevisionOf(work), blob: blobOf(work, file.path) };
191
+ if (after.revision !== current.observation.revision || after.indexRevision !== current.observation.indexRevision || after.blob !== before.blob) {
192
+ const moved = observeInstanceGit(home);
193
+ throw Object.assign(oatsError("E_STALE_OBSERVATION", "the work tree moved while the diff was being read; re-observe with `oats instance git --json`"), { observation: moved.observation });
194
+ }
195
+ const bytes = Buffer.byteLength(patch, "utf8");
196
+ const truncated = bytes > DIFF_MAX_BYTES;
197
+ const body = truncated ? Buffer.from(patch, "utf8").subarray(0, DIFF_MAX_BYTES).toString("utf8") : patch;
198
+ return {
199
+ instanceGitApi: INSTANCE_GIT_API, observation: current.observation,
200
+ file: { id: file.id, kind: file.kind, xy: file.xy, path: file.path, origPath: file.origPath },
201
+ against: file.kind === "untracked" ? "empty" : current.observation.revision, binary, bytes, truncated, limit: DIFF_MAX_BYTES, patch: body,
202
+ readOnly: { helpers: "disabled", optionalLocks: "off", objectsWritten: 0 },
203
+ };
204
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.24.6",
3
+ "version": "0.24.7",
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",