@awebai/oats 0.25.7 → 0.25.9

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
@@ -2938,7 +2938,7 @@ async function paneCmd() {
2938
2938
  * remotes, confirm membership, resolve `packages:`, approve (TTY) or list what
2939
2939
  * needs approval (exit 2), write `oats-lock.json`. Nothing is installed, no soul
2940
2940
  * is created, nothing is spawned, no `oats-config.yaml` is written: the member
2941
- * clones and the setup expert are the operator's next steps, printed here. */
2941
+ * clones and the operator expert are the operator's next steps, printed here. */
2942
2942
  async function onboardCmd() {
2943
2943
  const bail = (code, message, details) => (JSON_MODE ? jsonFail(code, message, details) : die(message));
2944
2944
  const usage = "usage: oats onboard [<dir>] --workspace <repo ref> [--json] (or --dir <dir>)";
@@ -3015,11 +3015,11 @@ async function onboardCmd() {
3015
3015
  // (4) The taught layout as next steps (design doc §4), and the envelope.
3016
3016
  const standalone = synced.discovery.standalone === true;
3017
3017
  const members = synced.report.members;
3018
- // The setup expert is suggested only when THIS workspace lists a soul by that name (a
3018
+ // The operator expert is suggested only when THIS workspace lists a soul by that name (a
3019
3019
  // confirmed member's or, standalone, the repo's own); otherwise any listed soul is spawnable.
3020
3020
  const soulNames = synced.items.souls.map((s) => s.name);
3021
- const setupExpert = soulNames.includes("oats-setup-expert");
3022
- const spawnHint = setupExpert ? `oats spawn oats-setup-expert --dir ${shortPath(dir)}` : null;
3021
+ const setupExpert = soulNames.includes("oats-operator-expert");
3022
+ const spawnHint = setupExpert ? `oats spawn oats-operator-expert --dir ${shortPath(dir)}` : null;
3023
3023
  const anySoulHint = `spawn any listed soul: oats spawn <soul> --dir ${shortPath(dir)}${soulNames.length ? ` (e.g. ${soulNames.slice(0, 3).join(", ")})` : ""}`;
3024
3024
  // A member's clone goes beside oats-local.yaml under its repo name; `agents/` is the instance
3025
3025
  // homes, so a member called "agents" is cloned as `agents-repo/` (design doc §4). The HOST is
@@ -3060,7 +3060,7 @@ Next:
3060
3060
  2. Check who may read the host: ${synced.discovery.key}${hostIsMember ? " is itself a member" : " is a dedicated host"}. The workspace file
3061
3061
  names every member, so if any member is private the host must be a private repo that is not
3062
3062
  a public member; public contributors then get the standalone case (from: here + oats.core).
3063
- 3. ${setupExpert ? "Spawn the setup expert to guide the rest (souls, teams, provider settings, approvals):" : "No soul named oats-setup-expert is listed here —"}
3063
+ 3. ${setupExpert ? "Spawn the operator expert to guide the rest (souls, teams, provider settings, approvals):" : "No soul named oats-operator-expert is listed here —"}
3064
3064
  ${spawnHint ?? anySoulHint}${synced.approvalNeeded.length ? `\n (first: \`oats sync --dir ${shortPath(dir)}\` in a terminal to approve ${synced.approvalNeeded.map((a) => `${a.id} ${a.version}`).join(", ")})` : ""}`);
3065
3065
  process.exitCode = synced.approvalNeeded.length ? 2 : 0;
3066
3066
  }
@@ -3911,7 +3911,7 @@ Usage:
3911
3911
  [--json] and agents/, then runs the oats sync path (lock v3;
3912
3912
  exit 2 while approvals are pending) and prints the
3913
3913
  next steps (clone members you work IN, spawn
3914
- oats-setup-expert); creates no soul, spawns nothing
3914
+ oats-operator-expert); creates no soul, spawns nothing
3915
3915
  oats create <name> [--local] [--no-oats-core] create an agent soul; --local = full
3916
3916
  [--description <d>] [--repo <r>] soul under local-agents/ (uncommitted,
3917
3917
  [--work <mode>] [--runtime pi|claude|codex] gitignored; same memory + lifecycle)
@@ -137,7 +137,7 @@ try {
137
137
  const manifest = JSON.parse(readFileSync(new URL("../oats.json", import.meta.url), "utf8"));
138
138
  const selected = requireCapturedAwebAction(loaded, event, manifest);
139
139
  const settings = parseBindingJson(Buffer.from(process.env.OATS_SETTINGS || "{}"));
140
- if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some(k => k !== "delivery")) throw new Error("captured settings support delivery only; no identity copying or ambient fallback");
140
+ if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some(k => !["delivery", "team", "root", "roots"].includes(k))) throw new Error("captured settings support delivery/team/root readiness only; no identity copying or ambient fallback");
141
141
  const checked = assessCapturedSessionReadiness({ binding: selected.binding, invocation: selected.context, settings }, {
142
142
  query(args, options) { selected.assertCurrent(); const result = querySelectedKernel(args, options); selected.assertCurrent(); return result; },
143
143
  });
@@ -194,7 +194,7 @@ function gitRootOf(startDir) {
194
194
  d = parent;
195
195
  }
196
196
  }
197
- function awebRoot() {
197
+ function classicAwebRoot() {
198
198
  const candidates = [];
199
199
  const push = (p) => { if (p && !candidates.includes(resolve(p))) candidates.push(resolve(p)); };
200
200
  push(process.env.OATS_TEAM_SCOPE);
@@ -206,6 +206,36 @@ function awebRoot() {
206
206
  for (const c of candidates) if (existsSync(join(c, ".aw"))) return c;
207
207
  return undefined;
208
208
  }
209
+ const hasWorkspaceV2Facts = () => !!(process.env.OATS_WORKSPACE_KEY || process.env.OATS_WORKSPACE_NAME || process.env.OATS_TEAM_LABEL);
210
+ const isClassicDeployment = () => !!process.env.OATS_TEAM_SCOPE && !hasWorkspaceV2Facts();
211
+ const teamConfigRemedy = () => `set messaging.byTeam.<label>.team in the workspace file or settings.oats.aweb.team${isClassicDeployment() ? " (classic: set team.id in oats-config.yaml)" : ""}`;
212
+ function declaredRootCandidate(team = payloadTeam().team) {
213
+ const roots = settings.roots && typeof settings.roots === "object" && !Array.isArray(settings.roots) ? settings.roots : {};
214
+ if (team && typeof roots[team] === "string" && roots[team].trim()) return { root: roots[team].trim(), key: `settings.oats.aweb.roots[${JSON.stringify(team)}]`, declared: true };
215
+ if (typeof settings.root === "string" && settings.root.trim()) return { root: settings.root.trim(), key: "settings.oats.aweb.root", declared: true };
216
+ return undefined;
217
+ }
218
+ function rootSettingCandidate(team = payloadTeam().team) {
219
+ const declared = declaredRootCandidate(team);
220
+ if (declared) return declared;
221
+ const fallback = process.env.OATS_WORKSPACE || (!isClassicDeployment() ? process.env.OATS_TEAM_SCOPE : undefined) || process.cwd();
222
+ return fallback ? { root: fallback, key: "settings.oats.aweb.root", declared: false } : undefined;
223
+ }
224
+ function awebRootProblem(candidate) {
225
+ if (!candidate?.root) return `no messaging root at ${process.cwd()}: run oats aweb setup there or set settings.oats.aweb.root`;
226
+ if (!isAbsolute(candidate.root)) return `${candidate.key} must be an absolute directory whose .aw is the aweb minting root`;
227
+ return `no messaging root at ${resolve(candidate.root)}: run oats aweb setup there or set ${candidate.key}`;
228
+ }
229
+ function resolveAwebRoot() {
230
+ const declared = declaredRootCandidate();
231
+ if (declared && isAbsolute(declared.root) && existsSync(join(resolve(declared.root), ".aw"))) return resolve(declared.root);
232
+ if (declared?.declared) return undefined;
233
+ if (isClassicDeployment()) return classicAwebRoot();
234
+ const candidate = rootSettingCandidate();
235
+ if (candidate && isAbsolute(candidate.root) && existsSync(join(resolve(candidate.root), ".aw"))) return resolve(candidate.root);
236
+ return undefined;
237
+ }
238
+ function awebRoot() { return resolveAwebRoot(); }
209
239
 
210
240
  /** Team memberships from `aw team list --json`. The current CLI returns
211
241
  * `memberships`; older output used `teams`. Spawn resolution and `oats aweb
@@ -411,9 +441,9 @@ function retainedSeatSpawn(source, takeOver) {
411
441
  const service = process.env.OATS_AWEB_URL || yamlScalar(srcWorkspace, "aweb_url");
412
442
  if (!service) fatal(`cannot determine the aweb service for ${source} (no aweb_url in its workspace.yaml)`);
413
443
  const role = yamlScalar(srcWorkspace, "role_name");
414
- let team = process.env.OATS_TEAM_ID;
444
+ let team = payloadTeam().team;
415
445
  if (!team && existsSync(join(source, "teams.yaml"))) team = yamlScalar(readFileSync(join(source, "teams.yaml"), "utf8"), "active_team") || yamlScalar(readFileSync(join(source, "teams.yaml"), "utf8"), "active");
416
- if (!team || !team.includes(":")) fatal(`cannot determine the team for the retained identity (set team.id in oats-config.yaml, or an active team in ${join(source, "teams.yaml")})`);
446
+ if (!team || !team.includes(":")) fatal(`cannot determine the team for the retained identity (${teamConfigRemedy()}, or keep an active team in ${join(source, "teams.yaml")})`);
417
447
  const dest = join(home, ".aw");
418
448
  const legacyHome = dirname(source);
419
449
  // The lock is taken FIRST: a concurrent second spawn must see it before any
@@ -490,7 +520,7 @@ function retainedSeatSpawn(source, takeOver) {
490
520
  const launch = (process.env.OATS_RUNTIME || "") === "claude" && deliveryMode === "channel"
491
521
  ? { claude: "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" }
492
522
  : undefined;
493
- const env = deliveryMode === "session" ? { AWEB_DELIVERY: "session" } : undefined;
523
+ const env = { ...(deliveryMode === "session" ? { AWEB_DELIVERY: "session" } : {}), AWEB_IDENTITY_HOME: dest };
494
524
  const deliveryBrief = deliveryMode === "session"
495
525
  ? ` Notification delivery: external (AWEB_DELIVERY=session); until the host wake broker registers this instance NOTHING wakes you: check \`aw mail inbox\` and \`aw chat pending\` at every task boundary.`
496
526
  : "";
@@ -500,7 +530,7 @@ function retainedSeatSpawn(source, takeOver) {
500
530
  if (hostNote) warnings.push(`oats-aweb: seated${hostNote}`);
501
531
  out({
502
532
  meta: { team, alias, retained: true, source, lock: lockPath, delivery: deliveryMode, identity: identityMeta({ mode: "global", alias, team, address: shownAddress || expectedAddress || null }), ...(takenOver ? { tookOverFrom: takenOver } : {}) },
503
- ...(env ? { env } : {}),
533
+ env,
504
534
  brief: `Comms: you are the retained seat of the existing aweb identity "${alias}" on team ${team} (same did and address as the seat you replace; its contacts, routes and conversations are yours).${deliveryBrief} Use \`aw mail\`/\`aw chat\` for messaging (see the aweb-messaging skill).`,
505
535
  ...(launch ? { launch } : {}),
506
536
  ...(warnings.length ? { warning: warnings.join(" | ") } : {}),
@@ -517,7 +547,11 @@ if (event === "spawn") {
517
547
  if (identityMode === "local" && settings.identity && typeof settings.identity === "object" && settings.identity.source) retainedSeatSpawn(String(settings.identity.source), settings.identity.takeOver === true);
518
548
  let minted; // external identity, once `aw team join` succeeds
519
549
  const root = awebRoot();
520
- if (!root) fatal(`no initialized aweb root (.aw) among the bounded candidates (home, its git repo, context repo, workspace ${process.env.OATS_WORKSPACE || "?"}), so no identity could be minted and this instance would have no messaging — run \`oats aweb setup\` for guided onboarding`);
550
+ if (!root) {
551
+ const candidate = rootSettingCandidate();
552
+ const classicHint = isClassicDeployment() ? " (classic fallback also checked the bounded team-scope candidates)" : "";
553
+ fatal(`${awebRootProblem(candidate)}, so no identity could be minted and this instance would have no messaging${classicHint}`);
554
+ }
521
555
  try {
522
556
  // Team correctness: the config's `team:` block wins (id, then name), else the
523
557
  // root's active team. ALWAYS pass --team-id explicitly — never inherit whatever
@@ -527,15 +561,16 @@ if (event === "spawn") {
527
561
  const resolvedTeam = payloadTeam();
528
562
  let team = resolvedTeam.team;
529
563
  const teamPayloadMismatch = resolvedTeam.payload && resolvedTeam.env && resolvedTeam.payload !== resolvedTeam.env;
564
+ if (!team && process.env.OATS_TEAM_LABEL) fatal(`cannot determine target team for workspace team label ${JSON.stringify(process.env.OATS_TEAM_LABEL)}, so no identity could be minted — ${teamConfigRemedy()}`);
530
565
  if (!team) team = JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
531
- if (!team) fatal("cannot determine target team (no config team block, no active team at root), so no identity could be minted — set a team: block in oats-config.yaml, or activate a team at the aweb root");
566
+ if (!team) fatal(`cannot determine target team, so no identity could be minted — ${teamConfigRemedy()}, or activate a team at the aweb root`);
532
567
  // A bare team name (no namespace) resolves against the root's memberships.
533
568
  if (!team.includes(":")) {
534
569
  const teams = JSON.parse(run(["aw", "team", "list", "--json"], root));
535
570
  const match = teamIdsOf(teams).filter((tid) => String(tid).startsWith(`${team}:`));
536
571
  if (match.length === 1) team = match[0];
537
- else if (match.length > 1) fatal(`team name "${team}" is ambiguous at ${root}: ${match.join(", ")}, so no identity could be minted — set team.id in oats-config.yaml`);
538
- else fatal(`no membership matching team "${team}" at ${root}, so no identity could be minted — join or create it first (aweb-team-membership skill), or set team.id`);
572
+ else if (match.length > 1) fatal(`team name "${team}" is ambiguous at ${root}: ${match.join(", ")}, so no identity could be minted — ${teamConfigRemedy()}`);
573
+ else fatal(`no membership matching team "${team}" at ${root}, so no identity could be minted — join or create it first (aweb-team-membership skill), or ${teamConfigRemedy()}`);
539
574
  }
540
575
  // Both of these carry the invite token — one mints it, the other spends it —
541
576
  // so neither their output nor their diagnostics may reach a log.
@@ -597,7 +632,7 @@ if (event === "spawn") {
597
632
  const launch = (process.env.OATS_RUNTIME || "") === "claude" && deliveryMode === "channel"
598
633
  ? { claude: "--dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace" }
599
634
  : undefined;
600
- const env = deliveryMode === "session" ? { AWEB_DELIVERY: "session" } : undefined;
635
+ const env = { ...(deliveryMode === "session" ? { AWEB_DELIVERY: "session" } : {}), AWEB_IDENTITY_HOME: join(home, ".aw") };
601
636
  const channelWarning = undefined;
602
637
  if (deliveryMode === "session") wakeRegister(home, join(home, ".aw"));
603
638
  const deliveryBrief = deliveryMode === "session"
@@ -605,7 +640,7 @@ if (event === "spawn") {
605
640
  : "";
606
641
  out({
607
642
  meta: { team: joined.team_id, alias, delivery: deliveryMode, identity: identityMeta({ mode: "local", alias, team: joined.team_id }) },
608
- ...(env ? { env } : {}),
643
+ env,
609
644
  brief: `Comms: you have an aweb identity — alias "${alias}" on team ${joined.team_id}.${mismatch}${deliveryBrief} Use \`aw mail\`/\`aw chat\` for messaging (see the aweb-messaging skill); coordination stays in your deployment's task layer.`,
610
645
  ...(launch ? { launch } : {}),
611
646
  ...(joined.team_id !== team ? { warning: `oats-aweb: team mismatch — joined ${joined.team_id}, expected ${team}` } : teamPayloadMismatch ? { warning: `oats-aweb: settings.oats.aweb.team ${resolvedTeam.payload} differs from OATS team ${resolvedTeam.env}; using payload team` } : channelWarning ? { warning: channelWarning } : {}),
@@ -673,9 +708,9 @@ if (event === "spawn") {
673
708
  // wherever they run (plus human members). Local liveness comes from
674
709
  // `oats status --team`; this is the network view.
675
710
  const root = awebRoot();
676
- if (!root) { console.error("oats aweb roster: no initialized aweb root (.aw) found"); process.exit(1); }
711
+ if (!root) { console.error(`oats aweb roster: ${awebRootProblem(rootSettingCandidate())}`); process.exit(1); }
677
712
  const team = process.env.OATS_TEAM_ID || process.env.OATS_TEAM_NAME || JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
678
- if (!team) { console.error("oats aweb roster: cannot determine team (no config team block, no active team)"); process.exit(1); }
713
+ if (!team) { console.error(`oats aweb roster: cannot determine team (${teamConfigRemedy()}, or activate a team at the aweb root)`); process.exit(1); }
679
714
  const teamFlag = team.includes(":") ? ["--team-id", team] : ["--team", team];
680
715
  const r = JSON.parse(run(["aw", "id", "team", "members", ...teamFlag, "--json"], root, 60000));
681
716
  if (process.argv.includes("--json")) { console.log(JSON.stringify(r, null, 2)); process.exit(0); }
@@ -687,18 +722,59 @@ if (event === "spawn") {
687
722
  process.exit(0);
688
723
  } else if (event === "setup") {
689
724
  // Guided onboarding — idempotent, prints what it finds and the one next step.
690
- const scope = process.env.OATS_TEAM_SCOPE || process.cwd();
691
- const teamName = process.env.OATS_TEAM_NAME;
692
- const teamId = process.env.OATS_TEAM_ID;
693
- console.log(`aweb onboarding — team scope: ${scope}${teamName ? `, config team: ${teamName}${teamId ? ` (${teamId})` : ""}` : ""}\n`);
725
+ if (isClassicDeployment() && settings.root === undefined && settings.roots === undefined && settings.team === undefined) {
726
+ const scope = process.env.OATS_TEAM_SCOPE || process.cwd();
727
+ const teamName = process.env.OATS_TEAM_NAME;
728
+ const teamId = process.env.OATS_TEAM_ID;
729
+ console.log(`aweb onboarding — team scope: ${scope}${teamName ? `, config team: ${teamName}${teamId ? ` (${teamId})` : ""}` : ""}\n`);
730
+ if (!teamName) {
731
+ console.log("1. Declare your team in the deployment scope's oats-config.yaml first:");
732
+ console.log(" team:\n name: <your-team>\n then re-run `oats aweb setup` from there.");
733
+ process.exit(0);
734
+ }
735
+ if (!existsSync(join(scope, ".aw"))) {
736
+ console.log(`No aweb workspace at the team scope yet. Initialize it (interactive — creates or connects an aweb account):`);
737
+ console.log(` cd ${scope} && aw init`);
738
+ console.log(" First time on aweb? `aw init` walks you through creating a hosted aweb.ai account.");
739
+ console.log(" Own your domain? Use `aw init --byod` (see the aweb-team-membership skill).");
740
+ process.exit(0);
741
+ }
742
+ let teams = { memberships: [] };
743
+ try { teams = JSON.parse(run(["aw", "team", "list", "--json"], scope)); } catch { /* fall through */ }
744
+ const want = teamId || teamName;
745
+ const match = teamIdsOf(teams).find((tid) => String(tid) === want || String(tid).startsWith(`${want}:`));
746
+ if (match) {
747
+ console.log(`✓ aweb workspace initialized and member of ${match}.`);
748
+ if (teams.active_team && teams.active_team !== match) console.log(` Note: active team is ${teams.active_team}; instances join ${match} explicitly, but consider \`aw team switch ${match}\`.`);
749
+ console.log(" Done — spawned instances will join this team automatically (alias = instance name).");
750
+ console.log(" Roster: `oats aweb roster` · local: `oats status --team`");
751
+ } else {
752
+ console.log(`Workspace initialized, but no membership matching "${want}".`);
753
+ console.log(` Create the team: cd ${scope} && aw team create ${teamName}`);
754
+ console.log(" Or join an existing one: get an invite token from a member, then `aw team join <token>`");
755
+ console.log(" (details: aweb-team-membership skill)");
756
+ }
757
+ process.exit(0);
758
+ }
759
+ const resolvedTeam = payloadTeam();
760
+ const teamName = resolvedTeam.team;
761
+ const teamId = typeof settings.team === "string" && settings.team.trim() ? settings.team.trim() : process.env.OATS_TEAM_ID;
762
+ const candidate = rootSettingCandidate(teamName);
763
+ const scope = candidate?.root ? resolve(candidate.root) : process.cwd();
764
+ console.log(`aweb onboarding — messaging root: ${scope}${teamName ? `, team: ${teamName}` : ""}\n`);
694
765
  if (!teamName) {
695
- console.log("1. Declare your team in the deployment scope's oats-config.yaml first:");
696
- console.log(" team:\n name: <your-team>\n then re-run `oats aweb setup` from there.");
766
+ console.log(`1. Choose the aweb team for this deployment: ${teamConfigRemedy()}.`);
767
+ console.log(" The workspace file's `messaging:` / `messaging.byTeam.<label>.team` payload is portable; host-specific overrides belong in `settings.oats.aweb.team`.");
768
+ process.exit(0);
769
+ }
770
+ if (!isAbsolute(scope)) {
771
+ console.log(`${candidate?.key || "settings.oats.aweb.root"} must be an absolute directory whose .aw is the aweb minting root.`);
697
772
  process.exit(0);
698
773
  }
699
774
  if (!existsSync(join(scope, ".aw"))) {
700
- console.log(`No aweb workspace at the team scope yet. Initialize it (interactive — creates or connects an aweb account):`);
775
+ console.log(`No aweb workspace at the messaging root yet (${candidate?.key || "settings.oats.aweb.root"}). Initialize it (interactive — creates or connects an aweb account):`);
701
776
  console.log(` cd ${scope} && aw init`);
777
+ console.log(" Or set settings.oats.aweb.root to an absolute directory whose .aw is the aweb minting root, then re-run `oats aweb setup`.");
702
778
  console.log(" First time on aweb? `aw init` walks you through creating a hosted aweb.ai account.");
703
779
  console.log(" Own your domain? Use `aw init --byod` (see the aweb-team-membership skill).");
704
780
  process.exit(0);
@@ -1,3 +1,5 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { isAbsolute, join, resolve } from 'node:path';
1
3
  import { TextDecoder } from 'node:util';
2
4
  import { assessCapturedSessionReadiness } from './session-readiness.mjs';
3
5
  import {
@@ -79,8 +81,12 @@ export function parseBindingJson(bytes,limits=BINDING_WIRE_LIMITS) {
79
81
  function settings(value) {
80
82
  if(!obj(value)) wireError('invalid-binding');
81
83
  if(Object.hasOwn(value,'identity')) wireError('provider-not-qualified');
82
- keys(value,['delivery'],[]);
84
+ keys(value,['delivery','team','root','roots'],[]);
83
85
  if(value.delivery!==undefined && !['channel','session'].includes(value.delivery)) wireError('needs-configuration');
86
+ if(value.team!==undefined && (typeof value.team!=='string' || !value.team.trim())) wireError('needs-configuration');
87
+ if(value.root!==undefined && (typeof value.root!=='string' || !value.root.trim())) wireError('needs-configuration');
88
+ if(value.roots!==undefined && !obj(value.roots)) wireError('needs-configuration');
89
+ if(obj(value.roots)) for(const [team,root] of Object.entries(value.roots)) if(!team || typeof root!=='string' || !root.trim()) wireError('needs-configuration');
84
90
  return value;
85
91
  }
86
92
  function request(value,phase) {
@@ -142,6 +148,23 @@ function binding(value) {
142
148
  return value;
143
149
  }
144
150
  function checkResult(message) {return {status:'needs-configuration',problems:[{code:'needs-configuration',message}]};}
151
+ function checkProblems(problems) {return problems.length?{status:'needs-configuration',problems}:null;}
152
+ function teamFromSettings(settings) {return typeof settings.team==='string' && settings.team.trim()?settings.team.trim():(process.env.OATS_TEAM_ID || process.env.OATS_TEAM_NAME || undefined);}
153
+ function classicEnv() {return !!process.env.OATS_TEAM_SCOPE && !(process.env.OATS_WORKSPACE_KEY || process.env.OATS_WORKSPACE_NAME || process.env.OATS_TEAM_LABEL);}
154
+ function rootCandidate(settings,team) {
155
+ const roots=obj(settings.roots)?settings.roots:{};
156
+ if(team && typeof roots[team]==='string' && roots[team].trim()) return {root:roots[team].trim(),key:`settings.oats.aweb.roots[${JSON.stringify(team)}]`,declared:true};
157
+ if(typeof settings.root==='string' && settings.root.trim()) return {root:settings.root.trim(),key:'settings.oats.aweb.root',declared:true};
158
+ const candidates=classicEnv()?[process.env.OATS_TEAM_SCOPE,process.env.OATS_WORKSPACE].filter(Boolean):[process.env.OATS_WORKSPACE || process.env.OATS_TEAM_SCOPE || process.cwd()];
159
+ for(const root of candidates) if(isAbsolute(root) && existsSync(join(resolve(root),'.aw'))) return {root,key:'settings.oats.aweb.root',declared:false};
160
+ return {root:candidates[0] || process.cwd(),key:'settings.oats.aweb.root',declared:false};
161
+ }
162
+ function readinessFromSettings(settings) {
163
+ const team=teamFromSettings(settings),candidate=rootCandidate(settings,team),problems=[];
164
+ if(!candidate.root || !isAbsolute(candidate.root) || !existsSync(join(resolve(candidate.root),'.aw'))) problems.push({code:'needs-configuration',message:`no messaging root at ${candidate.root?resolve(candidate.root):process.cwd()}: run oats aweb setup there or set ${candidate.key}`});
165
+ if(!team) problems.push({code:'needs-configuration',message:'no team: set messaging.byTeam.<label>.team in the workspace file or settings.oats.aweb.team'});
166
+ return checkProblems(problems) || {status:'ready',problems:[]};
167
+ }
145
168
  function checkPhase(req) {
146
169
  keys(req.input,['binding','context','action','invocation'],['binding','context','action']);
147
170
  if(!obj(req.input.action) || typeof req.input.action.kind!=='string') wireError('invalid-binding');
@@ -150,6 +173,9 @@ function checkPhase(req) {
150
173
  const invocation=Object.hasOwn(req.input,'invocation')?validateAwebInvocationContext(req.input.invocation,current,{context:req.input.context,action:req.input.action}):null;
151
174
  if(['command','hook','operation'].includes(req.input.action.kind) && (!invocation || invocation.instance===null || invocation.intent===null)) return checkResult('an admitted captured instance intent is required for execution');
152
175
  if(current.payload.privateTeam===null) return checkResult('an explicit private-team binding is required');
176
+ const hostReady=readinessFromSettings(req.settings);
177
+ if(hostReady.status!=='ready') return hostReady;
178
+ if(!invocation) return hostReady;
153
179
  // Read-only public kernel observations, not an account/grant attestation.
154
180
  // Native setup is performed only by the separately admitted execution path.
155
181
  return assessCapturedSessionReadiness({binding:current,invocation,settings:req.settings});
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "capability": "oats.aweb",
3
3
  "command": "aweb",
4
- "version": "1.12.0",
4
+ "version": "1.12.1",
5
5
  "compatibility": {
6
6
  "oats": ">=0.24.4"
7
7
  },
@@ -139,6 +139,12 @@
139
139
  "team": {
140
140
  "description": "Target aweb team id for identity lifecycle. In workspace v2 spawns this payload value wins over OATS_TEAM_ID/OATS_TEAM_NAME; if both are set and differ the hook warns and uses this setting."
141
141
  },
142
+ "root": {
143
+ "description": "Host-owned absolute directory whose .aw is the aweb minting root. Put this only in oats-local.yaml settings.oats.aweb.root; use oats aweb setup to initialize it."
144
+ },
145
+ "roots": {
146
+ "description": "Host-owned map of aweb team id to absolute directory whose .aw is that team's minting root, for deployments that mint into several teams. Put this only in oats-local.yaml settings.oats.aweb.roots."
147
+ },
142
148
  "identity": {
143
149
  "default": {
144
150
  "mode": "local"
@@ -175,7 +175,7 @@ else {
175
175
  } else if(event==='unlock') result=unlock(resolve(flags.lock),flags.token);
176
176
  else fail('E_USAGE',`unknown command ${event}; see --help`);
177
177
  answer=hook?result:{schemaVersion:1,ok:true,result};
178
- } catch(e) {exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:e.message}:{})},warning:`oats-okf: ${e.message}`}:{schemaVersion:1,ok:false,error:{code:e.code || 'E_OKF',message:e.message}};}
178
+ } catch(e) {const code=e.code || 'E_OKF';exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:e.message}:{})},warning:`oats-okf ${code}: ${e.message}`}:{schemaVersion:1,ok:false,error:{code,message:e.message}};}
179
179
  // Let Node drain the pipe; no process.exit after a possibly large view.
180
180
  process.stdout.write(JSON.stringify(answer)+'\n');process.exitCode=exit;
181
181
  }
@@ -291,7 +291,7 @@ function checkPhase(req) {
291
291
  if(gitBases.length) scratch=fs.mkdtempSync(join(fs.realpathSync(tmpdir()),'oats-okf-binding-check-'));
292
292
  for(const [alias,base] of Object.entries(bindings.bases)) {
293
293
  stage=base.kind==='directory'?'validate':'stage';
294
- accepted[alias]=(base.kind==='directory'?validateBase(base.path,base):stageBase(base,join(scratch,alias))).meta;
294
+ accepted[alias]=(base.kind==='directory'?validateBase(base.path,base):stageBase(base,join(scratch,alias),{alias})).meta;
295
295
  }
296
296
  stage='runtime';
297
297
  checkKnowledgeRuntime({rendered:runtime,accepted});
@@ -33,7 +33,7 @@ export function migrate(bindings,{legacy,alias,node,output}) {
33
33
  // Stage the accepted base before any record exists: a base that cannot be
34
34
  // read leaves nothing behind. From the record on, a failure is recorded in
35
35
  // it rather than left as an unexplained directory.
36
- const stage=stageBase(base,output);
36
+ const stage=stageBase(base,output,{alias});
37
37
  fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});
38
38
  const id=randomUUID();const dir=join(bindings.stateDir,'migrations',id);fs.mkdirSync(dir,{recursive:true,mode:0o700});
39
39
  save(join(dir,'legacy.json'),original); // byte-preserving backup BEFORE any delivery
@@ -106,7 +106,7 @@ export function cutoverMigration(file,soul) {
106
106
  try {
107
107
  for(const alias of new Set([...decl.owns,...decl.reads].map(r=>splitRef(r)[0]))) {
108
108
  if(!Object.hasOwn(bindings.bases,alias)) fail('E_CONFIG',`unresolved base: ${alias}`);
109
- const staged=stageBase(bindings.bases[alias],join(scratch,alias));
109
+ const staged=stageBase(bindings.bases[alias],join(scratch,alias),{alias});
110
110
  accepted[alias]=staged.meta;
111
111
  if(alias===m.alias) {
112
112
  if(hash(staged.meta.nodes[m.node] || null)!==hash(frozen)) fail('E_OWNER','accepted migration ownership/path differs from delivered node');
@@ -101,7 +101,7 @@ export function views(bindings, decl, target) {
101
101
  for(const [alias,base] of Object.entries(bindings.bases)) {
102
102
  const scratch=fs.mkdtempSync(join(bindings.stateDir,'read-'));
103
103
  try {
104
- const staged=stageBase(base,join(scratch,'base'));
104
+ const staged=stageBase(base,join(scratch,'base'),{alias});
105
105
  all[alias]=staged.meta;
106
106
  const path=`bases/${alias}`;
107
107
  materialize(join(pending,path),staged.files);
@@ -192,7 +192,8 @@ export function register(home) {
192
192
  if(fs.existsSync(join(home,'knowledge'))) fail('E_VIEW','unregistered knowledge view exists; preserve it and inspect before registering');
193
193
  if(['.okf-harvest-record.json','.okf-harvest-record.next.json'].some(p=>fs.existsSync(join(home,p))) && !fs.existsSync(join(home,'.okf-v1-migration.json'))) fail('E_MIGRATION','legacy source watermarks require explicit oats okf migrate --source-home PATH before v2 registration; no cursor is silently trusted');
194
194
  const meta=fs.existsSync(join(home,'instance.json'))?readJSON(join(home,'instance.json')):{};
195
- const soul=fs.realpathSync(process.env.OATS_SOUL || join(home,'soul'));
195
+ if(!process.env.OATS_SOUL) fail('E_OATS_SOUL_MISSING','OATS_SOUL is not set; oats.okf hooks and commands run only under the OATS kernel');
196
+ const soul=fs.realpathSync(process.env.OATS_SOUL);
196
197
  const work=fs.existsSync(join(home,'work'))?fs.realpathSync(join(home,'work')):join(home,'work');
197
198
  const decl=declaration(soul);
198
199
  const soulId=process.env.OATS_SOUL_ID || null;
@@ -204,7 +205,6 @@ export function register(home) {
204
205
  if(!agent || !instance) fail('E_SOURCE','source instance/agent required');
205
206
  fs.mkdirSync(bindings.stateDir,{recursive:true,mode:0o700});
206
207
  const ownersFile=join(bindings.stateDir,'owners.json');
207
- pinOwner(ownersFile,decl.owner,{id:soulId,soulName:agent,path:soul});
208
208
  const id=randomUUID(); const dir=join(bindings.stateDir,'sources',id);
209
209
  // Copy only the role document, never instance.json wholesale, launch recipes,
210
210
  // environment, credentials, source worktree, or third-party message stores.
@@ -217,6 +217,7 @@ export function register(home) {
217
217
  fs.mkdirSync(dir,{recursive:true,mode:0o700});
218
218
  try {
219
219
  source.acceptedView=views(bindings,decl,pending);
220
+ pinOwner(ownersFile,decl.owner,{id:soulId,soulName:agent,path:soul});
220
221
  source.acceptedNodes=Object.fromEntries(Object.entries(source.acceptedView).map(([alias,r])=>[alias,r.nodes]));
221
222
  save(file,source);
222
223
  save(join(dir,'status.json'),{version:1,captured:{notes:[],threads:{},inputs:[]},processed:[],delivered:{},accepted:{},retired:false,auto:true,activeRun:null});
@@ -245,7 +246,7 @@ export function pinOwner(ownersFile,owner,{id,soulName,path}) {
245
246
  if(!obj(owners)) fail('E_OWNER','invalid owner registry');
246
247
  const prior=Object.hasOwn(owners,owner)?owners[owner]:undefined;
247
248
  const samePath=typeof prior==='string' && new RegExp(`/agents/${soulName.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')}/(soul|souls/[^/]+)$`).test(prior);
248
- if(prior!==undefined && prior!==value && !(id && samePath)) fail('E_OWNER','stable owner ID already identifies a different soul in this state namespace');
249
+ if(prior!==undefined && prior!==value && !(id && samePath)) fail('E_OWNER',`stable owner ID already identifies a different soul in this state namespace: existing soul ${prior}; new soul ${value}. Remedies: retire the existing registration first, or use a fresh state directory.`);
249
250
  if(prior!==value) {owners[owner]=value;save(ownersFile,owners);}
250
251
  return value;
251
252
  });
@@ -9,6 +9,31 @@ const validator = fileURLToPath(new URL('../skills/okf/scripts/okf-validate.mjs'
9
9
  // transport subprocesses. Override even an explicitly supplied command env.
10
10
  const gitEnv = (env = cleanEnv()) => ({...env,GIT_NO_REPLACE_OBJECTS:'1'});
11
11
  export const git = (cwd,args,opts={}) => exec('git',['--no-replace-objects','-c','core.hooksPath=/dev/null','-c','protocol.ext.allow=never','-C',cwd,...args],{cwd,...opts,env:gitEnv(opts.env)});
12
+ function baseError(code,message,base,alias,step,reason) {throw Object.assign(new Error(message),{code,base:alias,repository:base.repository,step,reason});}
13
+ function baseRemedy(alias) {return `fix the binding for base alias "${alias}" in the bindings file, or remove the base from the bindings`;}
14
+ function classifyGitFailure(error) {
15
+ const text=String(error?.message || '');
16
+ if(error?.code==='ETIMEDOUT' || /ETIMEDOUT|timed out|timeout/i.test(text)) return 'timeout';
17
+ if(/authentication|permission denied|publickey|access denied|could not read Username|authorization|not authorized/i.test(text)) return 'auth';
18
+ if(/not found|repository .*not exist|does not exist|no such (file|repository)|couldn't find remote ref|Repository not found/i.test(text)) return 'not-found';
19
+ if(/could not resolve host|failed to connect|connection refused|network is unreachable|no route to host|proxy/i.test(text)) return 'network';
20
+ return 'unknown';
21
+ }
22
+ function unavailable(base,alias,step,error) {
23
+ const reason=classifyGitFailure(error),detail=reason==='unknown'?`; original Git failure: ${String(error?.message || 'unknown failure')}`:'';
24
+ const message=`Git base "${alias}" repository "${base.repository}" is required by the deployment's bindings, but ${step} failed (reason: ${reason}${detail}); ${baseRemedy(alias)}`;
25
+ baseError('E_BASE_UNAVAILABLE',message,base,alias,step,reason);
26
+ }
27
+ function requireNotShallow(base,alias,cwd) {
28
+ const shallow=git(cwd,['rev-parse','--is-shallow-repository']);
29
+ if(shallow==='true') baseError('E_BASE_SHALLOW',`Git base "${alias}" repository "${base.repository}" is required by the deployment's bindings, but the repository is shallow; oats.okf requires full accepted history before staging; ${baseRemedy(alias)}`,base,alias,'clone','shallow');
30
+ }
31
+ function preflightLocalRepository(base,alias) {
32
+ if(!base.repository.startsWith('/')) return;
33
+ if(!fs.existsSync(base.repository)) unavailable(base,alias,'clone',Object.assign(new Error('repository not found'),{code:'ENOENT'}));
34
+ try { requireNotShallow(base,alias,base.repository); }
35
+ catch(e) { if(e.code==='E_BASE_SHALLOW') throw e; unavailable(base,alias,'clone',e); }
36
+ }
12
37
  export const baseLock = b => `${b.path}.okf-lock`;
13
38
  export const journalPath = b => `${b.path}.okf-publication.json`;
14
39
  export function validateBase(root, base) {
@@ -53,9 +78,10 @@ function materializeGitObjects(base,dest,head) {
53
78
  writeBlob(dest,entry.oid,target,entry.mode==='100755'?0o755:0o644);
54
79
  }
55
80
  }
56
- function clone(base, dest, selectedHead) {
81
+ function clone(base, dest, selectedHead, { alias = base.id } = {}) {
57
82
  safePath(dest);
58
83
  if(fs.existsSync(dest)) fail('E_PATH',`staging destination exists: ${dest}`);
84
+ preflightLocalRepository(base,alias);
59
85
  fs.mkdirSync(dirname(dest),{recursive:true});
60
86
  // Fetch only what the store reads: the accepted branch, trees now and blobs
61
87
  // on demand. Only a remote that does not offer object filtering gets a plain
@@ -64,25 +90,30 @@ function clone(base, dest, selectedHead) {
64
90
  const cloneArgs=['clone','--no-hardlinks','--no-checkout','--single-branch','--branch',base.acceptedBranch];
65
91
  try { git(dirname(dest),[...cloneArgs,'--filter=blob:none','--',base.repository,dest],{timeout:gitTimeoutMs()}); }
66
92
  catch(e) {
67
- if(e.code!=='E_COMMAND' || !/filter/i.test(e.message)) throw e;
68
- fs.rmSync(dest,{recursive:true,force:true});
69
- git(dirname(dest),[...cloneArgs,'--',base.repository,dest],{timeout:gitTimeoutMs()});
93
+ if(e.code==='E_COMMAND' && /filter/i.test(e.message)) {
94
+ fs.rmSync(dest,{recursive:true,force:true});
95
+ try { git(dirname(dest),[...cloneArgs,'--',base.repository,dest],{timeout:gitTimeoutMs()}); }
96
+ catch(error) { unavailable(base,alias,'clone',error); }
97
+ } else unavailable(base,alias,'clone',e);
70
98
  }
71
- git(dest,['fetch','origin',`refs/heads/${base.acceptedBranch}`],{timeout:gitTimeoutMs()});
99
+ try { requireNotShallow(base,alias,dest); }
100
+ catch(e) { if(e.code==='E_BASE_SHALLOW') throw e; unavailable(base,alias,'clone',e); }
101
+ try { git(dest,['fetch','origin',`refs/heads/${base.acceptedBranch}`],{timeout:gitTimeoutMs()}); }
102
+ catch(e) { unavailable(base,alias,'fetch',e); }
72
103
  const head=selectedHead ?? git(dest,['rev-parse','FETCH_HEAD']);
73
- verifyRemote(base,dest);
74
- materializeGitObjects(base,dest,head);
104
+ try { requireNotShallow(base,alias,dest);verifyRemote(base,dest);materializeGitObjects(base,dest,head); }
105
+ catch(e) { if(['E_BASE_SHALLOW','E_OWNER','E_CONFIRM','E_PATH','E_BASE','E_VALIDATION'].includes(e.code)) throw e; unavailable(base,alias,'checkout',e); }
75
106
  // Reject a linked bundle even if Git happily checked the link out.
76
107
  safePath(join(dest,base.root));
77
108
  return head;
78
109
  }
79
- export function stageBase(base, dest) {
110
+ export function stageBase(base, dest, { alias = base.id } = {}) {
80
111
  safePath(dest);
81
112
  if(fs.existsSync(dest)) fail('E_PATH',`staging destination exists: ${dest}`);
82
113
  const acceptedPath=base.kind==='directory'?base.path:(base.repository.startsWith('/')?resolve(base.repository,base.root):null);
83
114
  if(acceptedPath && overlaps(acceptedPath,dest)) fail('E_PATH','stage overlaps accepted base');
84
115
  if(base.kind==='git') {
85
- const head=clone(base,dest); const root=join(dest,base.root);
116
+ const head=clone(base,dest,undefined,{alias}); const root=join(dest,base.root);
86
117
  const validated=validateBase(root,base);
87
118
  verifyPublicationTree(base,dest,head,head,validated.files);
88
119
  return { ...validated, head, root, checkout:dest };
@@ -142,7 +142,7 @@ function prepareWorker(source,run) {
142
142
  for(const [alias,base] of Object.entries(source.bindings.bases)) {
143
143
  if(run.settled?.includes(alias)) continue;
144
144
  const dest=join(work,'bases',alias);
145
- const staged=stageBase(base,dest);
145
+ const staged=stageBase(base,dest,{alias});
146
146
  const owned=source.decl.owns.map(splitRef).filter(([a])=>a===alias).map(([,n])=>n);
147
147
  for(const n of owned) if(staged.meta.nodes[n]?.owner!==source.owner || JSON.stringify(staged.meta.nodes[n])!==JSON.stringify(source.acceptedNodes[alias][n])) fail('E_OWNER','accepted ownership/path changed from frozen destination; explicit migration required');
148
148
  run.stages[alias]={root:staged.root,checkout:staged.checkout,head:staged.head,baseline:staged.files,digest:staged.digest,owned};persist(source,run);
@@ -235,7 +235,7 @@ export function complete(source,id,judgmentFile,opts={}) {
235
235
  if(base.kind==='git') verifyGitScope(base,s.checkout,s.head);
236
236
  const checkDir=fs.mkdtempSync(join(source.bindings.stateDir,'baseline-'));
237
237
  try {
238
- const current=stageBase(base,join(checkDir,'base'));
238
+ const current=stageBase(base,join(checkDir,'base'),{alias});
239
239
  if(current.digest!==s.digest || (base.kind==='git' && current.head!==s.head)) fail('E_BASELINE','accepted base changed; rejudge on fresh baseline');
240
240
  } finally {fs.rmSync(checkDir,{recursive:true,force:true});}
241
241
  const claims=judgment.outcomes.flatMap(o=>o.concepts).filter(c=>c.base===alias).map(c=>c.path);
@@ -310,7 +310,7 @@ export function retry(source,{run:id,rejudge=false,launch=false,adoptHome}={}) {
310
310
  const stages={...run.stages},outstanding=Object.keys(stages).filter(a=>!settled.includes(a));
311
311
  for(const alias of outstanding) {
312
312
  const base=source.bindings.bases[alias];
313
- const staged=stageBase(base,join(work,'rejudgments',attempt,'bases',alias));
313
+ const staged=stageBase(base,join(work,'rejudgments',attempt,'bases',alias),{alias});
314
314
  const owned=run.stages[alias].owned;
315
315
  for(const n of owned) if(staged.meta.nodes[n]?.owner!==source.owner || JSON.stringify(staged.meta.nodes[n])!==JSON.stringify(source.acceptedNodes[alias][n])) fail('E_OWNER','accepted ownership/path changed from frozen destination; explicit migration required');
316
316
  stages[alias]={root:staged.root,checkout:staged.checkout,head:staged.head,baseline:staged.files,digest:staged.digest,owned};
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "capability": "oats.okf",
3
3
  "command": "okf",
4
- "version": "2.1.4",
4
+ "version": "2.1.5",
5
5
  "compatibility": {
6
6
  "oats": ">=0.24.4"
7
7
  },
@@ -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-24 13:30Z · **0.25.0–0.25.6 PUBLISHED** (0.25.6 = decision 27 kernel half + Desktop 10B-0 terminal owner leases, native gate 23/23) (workspace model A–C; team-review fixes; operator-rebuild round; OATS_SOUL_ID; quarantine-retry fix; launch meta + catalog pins) · **OKF v2.1.4 + oats.aweb v1.12.0 TAGGED and pinned** · **decision 27 accepted; K1′/K1″/K2 kernel PR next** · **oats.aweb 1.13.0 (#110) queued** · **Desktop 10B-0 resumed** · Phase D plan next · **Desktop engineer paused by the human** (10B-0 uncommitted foundation preserved; resume is the human's call; 0.24.14 to be cut from a maintenance branch off `e5cdaf95` when its PR lands) · parity pipeline ⏸.
5
+ **Last update:** 2026-09-24 14:10Z · **0.25.0–0.25.7 PUBLISHED** (0.25.7 = v2 deployment root is a configuration boundary) (0.25.6 = decision 27 kernel half + Desktop 10B-0 terminal owner leases, native gate 23/23) (workspace model A–C; team-review fixes; operator-rebuild round; OATS_SOUL_ID; quarantine-retry fix; launch meta + catalog pins) · **OKF v2.1.4 + oats.aweb v1.12.0 TAGGED and pinned** · **decision 27 accepted; K1′/K1″/K2 kernel PR next** · **oats.aweb 1.13.0 (#110) queued** · **Desktop 10B-0 resumed** · Phase D plan next · **Desktop engineer paused by the human** (10B-0 uncommitted foundation preserved; resume is the human's call; 0.24.14 to be cut from a maintenance branch off `e5cdaf95` when its PR lands) · parity pipeline ⏸.
6
6
 
7
7
  Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
8
8
 
@@ -81,6 +81,33 @@ identity.mode=… identity.resident=…` (decision 27 — there is no kernel fla
81
81
  `SPAWN_ARG_RULES` gains one `provider` rule (capability id, dotted key, value
82
82
  grammar); no identity-named rules. Work mode select includes `workspace`.
83
83
 
84
+ **F3 amended by the human (2026-09-24). This supersedes design frame 02 and the
85
+ text above wherever they differ.** The dialog leads with the **instance name**:
86
+ the purpose field becomes a name field that shows `<soul>-<purpose>` live and
87
+ then the kernel's final name from the preview. A no-prefix toggle maps to
88
+ `spawn --name <slug>`, gated on the `spawn-name` feature; `--purpose` stays the
89
+ default. **Runtime and model** are always visible, with their resolved value and
90
+ its source. **Relationship** sits in the main form, shown by default: None /
91
+ Child of / Sibling of / Parent of. There is **no** modules/capabilities list and
92
+ no attach-knowledge / child-spawn / open-PR toggles, because those behaviours
93
+ come from capabilities. There is **no** preview button either. The preview runs
94
+ in the background to fill the real defaults, and apply still binds with
95
+ `--expect-decision` (`E_DECISION_STALE` re-previews). A collapsed section named
96
+ **Developer settings** holds:
97
+ - **work**: base | branch and the worktree path. A `checkout` soul is offered
98
+ "Use a worktree instead?" (`--work worktree`); the work mode itself comes
99
+ from the soul's `work:`.
100
+ - harness permissions
101
+ - launch config
102
+ - session backend
103
+ - Run on (the execution server)
104
+ - wake-up
105
+ - messaging identity (`--provider <cap> identity.mode=…`, decision 27)
106
+
107
+ (Second human redirect, same day: relationship moved into the main form, work
108
+ moved into the collapsed section, and the section was renamed from "Advanced".)
109
+ Every modal gets a darker backdrop.
110
+
84
111
  **F4 — Instance card and roster on v2 facts.** The served-identity line (`acts
85
112
  as <address> via grant, expires <t>` / `alias <a> on <team>`); module rows with
86
113
  "moved since" markers and a **re-spawn** action (preview → apply, then retire
@@ -22,7 +22,7 @@ pushes. Agreed by both on 2026-09-24:
22
22
  | oats.aweb 1.13.0 re-land end to end | Antares | assigned |
23
23
  | D1 operator node + integrations node | Antares | assigned (draft PR `d1/operator-node` handed over) |
24
24
  | aweb and okf package-expert seams | Antares | assigned |
25
- | D2, D3, D4, desktop + kernel bundle migrations (D1 remainder) | Antares | assigned (both humans agreed, 2026-09-24). D2's machine-bound onboard of the lead's deployment is run by the lead on Antares' word; kernel gaps come to the lead as asks. |
25
+ | D2, D3, D4, desktop + kernel bundle migrations (D1 remainder) | lead, driven by the child instance `oats-expert-phase-d` | assigned (human, 2026-09-24: picked up by the lead's side after all, so as not to wait on the other deployment's human). The lead reviews and merges; Class B items go to Antares for ACK; D3's aweb/okf seams are drafted by the driver and settled with Antares. **Antares is a REQUIRED cross-reviewer** on D4's `oats.setup` rewrite (it must not drift from the operator node) and on D3's `aweb-expert` and `okf-expert` souls (the seams); the rest of the driver's PRs it reads without gating. |
26
26
 
27
27
  **Push protocol.**
28
28
  - **Class A** — notify after, one line: stewardship, docs and knowledge inside
@@ -42,6 +42,26 @@ pushes. Agreed by both on 2026-09-24:
42
42
  - `main` on these repositories carries no branch or tag protection: the parity
43
43
  gate and the cross-review are the only things between a merge and `main`.
44
44
 
45
+ ## Human decision (2026-09-24): no package approval
46
+
47
+ **Package approval is removed from the kernel and from the Desktop.** People
48
+ install a package only when they trust it. Declaring it in the workspace's
49
+ `packages:` IS the trust decision, so there's no second, per-version approval
50
+ step. This supersedes the "executables approved once per version" rule in
51
+ `docs/workspaces.md` (§ Packages, lock, approval, catalog) and everything built
52
+ on it:
53
+ - `oats sync` exit 2 for pending approvals and `approvalNeeded`
54
+ - `--approve <id>@<version>` and the interactive prompt
55
+ - the lock's `approved` record
56
+ - `E_PACKAGE_UNAPPROVED` at spawn and dispatch
57
+ - the Desktop F2 approval flow (`E_APPROVAL_STALE`)
58
+ - the planned pinned `--approve …=<digest>`
59
+
60
+ What stays: the lock still pins each package to the exact commit and integrity,
61
+ and restore still refuses drift (`E_PACKAGE_INTEGRITY`). Reproducibility is not
62
+ approval. It's a breaking contract change, so it ships in a minor release, and
63
+ the Desktop requires that kernel.
64
+
45
65
  ## Slices, in order
46
66
 
47
67
  ### D1 — Knowledge centralisation (IN PROGRESS)
@@ -118,14 +138,44 @@ for, never named by convention — decision 9) is the acceptance: `oats onboard`
118
138
 
119
139
  ### D3 — Souls to `souls/<name>` and the six package-expert souls (decision 20)
120
140
 
121
- Each package repo carries `souls/<pkg>-expert` (okf-expert, aweb-expert,
122
- jira-expert, linear-expert, authoring-expert, dev-expert), the expert in that
123
- package, with a node in the central base from day one. **Seams named in the
124
- charters** (roster amendment): `aweb-expert` READS
141
+ Each package repo carries `souls/oats-<pkg>-expert` (oats-okf-expert,
142
+ oats-aweb-expert, oats-jira-expert, oats-linear-expert, oats-authoring-expert,
143
+ oats-dev-expert), the expert in that package, with a node in the central base
144
+ from day one. **Messaging (human, 2026-09-24; supersedes the lead's per-soul rule):**
145
+ `oats.aweb` is the workspace's messaging **default**
146
+ (`defaults.messaging: { oats.aweb: { from: package } }`, oats#140); a soul
147
+ without messaging says `messaging: none`. The human calls it "the most
148
+ important capability, and most workspaces will use it as default", so the
149
+ provider must work well as a default. Open items (oats.aweb unless noted):
150
+ an unconfigured default must not make every soul unspawnable (onboarding sets
151
+ the messaging root before the first spawn, and/or an unconfigured provider
152
+ reports what is missing instead of refusing); error texts and `setup` must stop
153
+ pointing at `oats-config.yaml`; the deployment names its messaging root
154
+ explicitly rather than by search; `aw mail` usable from `work/`; retired aliases
155
+ reusable. Kernel (lead): in v2 the team scope is the deployment directory and
156
+ the team id comes from the messaging payload, not the classic `team:` block.
157
+ #140 merges together with the first answer to the unspawnable-default item.
158
+ **Teams (human, 2026-09-24): seamless by default.** (R1) Every person gets a
159
+ **personal team per workspace**, created on first use with no invite and no
160
+ manual initialisation — two people, or one person on two workspaces, get
161
+ separate teams; stable across one person's machines. (R2) When a soul whose
162
+ `team:` label the workspace maps to a shared team (`messaging.byTeam.<label>.team`)
163
+ is spawned, the instance **joins that team seamlessly**. Authorization for R2
164
+ (what entitles a person to join without an invite) is oats.aweb's design call
165
+ with the aweb project, fail-closed and visible when the entitlement is missing.
166
+ Kernel (lead): pass the soul's team label and the workspace identity so the
167
+ provider derives the personal team deterministically; an unmapped team means
168
+ "personal". The onboarding skill's manual invite-then-join step is the 1.12.0
169
+ path and is rewritten when the provider ships R1/R2.
170
+ **Naming (lead, 2026-09-24):** the `oats-` prefix on all six —
171
+ it matches the repository names and the roster's `oats-kernel-`/`oats-desktop-`/
172
+ `oats-operator-expert`, and it keeps instance aliases from colliding with the
173
+ messaging project's own `aweb-expert` soul on a shared team. **Seams named in the
174
+ charters** (roster amendment): `oats-aweb-expert` READS
125
175
  `aweb-protocol-expert` in base `aweb-oss-knowledge` (repo
126
176
  `github.com/awebai/aweb`, root `knowledge/`, branch `main`, OKF 2.1.x
127
177
  descriptor at `knowledge/okf-base.json`; owner `1913b77b-…`) through a
128
- read-only store reference; `okf-expert` names its seam to the knowledge-theory
178
+ read-only store reference; `oats-okf-expert` names its seam to the knowledge-theory
129
179
  material in `oats-expert`. Whether the aweb bookshelf decisions the program
130
180
  rests on are published into that node is the aweb side's call (asked).
131
181
 
@@ -140,6 +190,27 @@ rule, the rebuild guide as procedure with the operator node as rationale.
140
190
  Developer souls in `oats.dev` gain `promotesTo: <node>` (roster amendment);
141
191
  the harvester delivers to that node as a PR the owning expert reviews.
142
192
 
193
+ **D4 also removes the legacy the v2 model already declared gone** (human,
194
+ 2026-09-24; boundary §3b's native-rework rule applied to the repository):
195
+ - **Docs, skills, examples** (driver, in D4): delete what v2 removed rather than
196
+ rewriting it (e.g. the OAS migration guide, the legacy Desktop succession
197
+ doc, `oats-config.yaml` examples, the `oats-config` skill); rewrite what
198
+ survives against `oats-local.yaml` and the workspace file; every remaining
199
+ mention of `oats-config.yaml` either describes its removal or is gone.
200
+ - **Kernel** (lead, one Class B PR after D2 lands, because the OATS workspace
201
+ itself stops reading `oats-config.yaml` only once D2 converts it): the
202
+ `oats-config.yaml` scope chain and its readers, the `local-agents/` and
203
+ `tmp-agents/` layouts, the OAS-scope probes, the installed-capability tier
204
+ remnants. Removed, not flagged; the `REMOVED_VERBS` answers stay.
205
+ - **In-repo package copies** (`capabilities/oats-{okf,aweb,jira,linear,authoring}`)
206
+ are NOT removed in D4: `package.json` ships `capabilities/` as the kernel's
207
+ bundled providers, pinned by the mirror-parity, release-packaging and
208
+ clean-room tests. Whether 0.26.0 still bundles them is a release decision
209
+ for D5 (lead); until then they stay unmarked. `private` becomes a schema key
210
+ (the kernel already reads it); `oats-review` is marked private.
211
+ - **Legacy souls** (`agents/*` and their knowledge bundles) are NOT part of D4:
212
+ they go when the live instances linking them retire (human rule).
213
+
143
214
  ### D5 — Catalog update and 0.26.0
144
215
 
145
216
  Catalog pins for the new package versions; **widen Desktop `ACCEPT_RANGE` and
@@ -960,9 +960,15 @@ between preview and apply is `E_DECISION_STALE`):
960
960
  (unchanged since the newest previous instance), or
961
961
  `{ instance, was }` (`was` = the previous commit, or `null` when the previous
962
962
  instance had no such module).
963
- - `capabilities[]` / `skills[]` keep their Preview-1 meaning; on a workspace
964
- spawn the authoritative module set is `modules[]` (`capabilities[]` may be
965
- empty there, since capability rows are filled after materialization).
963
+ - `capabilities[]` / `skills[]` on a **workspace** spawn are **objects**, not
964
+ Preview-1's strings: `capabilities[]` is `{ name, origin }` (`origin` =
965
+ `package:<id>@<version>` or `member:<repoKey>@<commit>`), and `skills[]` is
966
+ `{ name, source }`. Neither is a binding surface, because the authoritative
967
+ module set is `modules[]` and what apply binds is `decision.effective` /
968
+ `decision.resolution`. Consumers should read those fields, not project
969
+ `capabilities[]` / `skills[]`. (Corrected 2026-09-24: this said "keep their
970
+ Preview-1 meaning", which read as strings; found by the Desktop engineer in
971
+ F3.)
966
972
  - `workspace` is the workspace host's canonical key, `team` the soul's label
967
973
  (or `null`), `resolution` the 24-hex revision `decision.resolution` binds.
968
974
  - `--provider <cap> <key>=<value>` (repeatable; `a.b=c` nests) is accepted by
@@ -8,7 +8,7 @@ the OATS Desktop app (`packages/desktop/` in the framework repo):
8
8
  |---|---|
9
9
  | `oats.web` marketplace capability (`oats web start`, browser panel) | OATS Desktop app — the same zero-dependency loopback server is bundled at `packages/desktop/server/` and spawned by the app |
10
10
  | `oats pane` CLI command and the Control Pane TUI | OATS Desktop app (Active overview / instance roster) |
11
- | `@awebai/oats/control-pane` package export (`lib/control-pane/model.mjs`) | The roster model moved into `packages/desktop/server/model.mjs`; it is no longer a public kernel export |
11
+ | `@awebai/oats/control-pane` package export (`lib/control-pane/model.mjs`) | The roster model moved into the Desktop app; under workspace model v2 the app reads the kernel's `oats status --json` instead ([deployment model](../packages/desktop/docs/desktop-deployment-model.md)). It is not a public kernel export |
12
12
 
13
13
  ## Migrating a deployment that used `oats.web`
14
14
 
@@ -45,9 +45,9 @@ solarized) exist in the app's theme system.
45
45
  `import ... from "@awebai/oats/control-pane"` no longer resolves. The
46
46
  model's pure helpers (`readMarkdownSection`, `parseTmuxWindows`,
47
47
  `parseGitStatus`, `parseGitDiffStat`, `buildConstellation`, `relativeAge`)
48
- live in `packages/desktop/server/model.mjs`, which is private to the desktop
49
- app. If you depended on this export, vendor the helpers or open an issue —
50
- no known external consumer existed at removal time.
48
+ moved into the private Desktop app and were retired with its workspace-model v2
49
+ rebuild. If you depended on this export, vendor the helpers from a released tag
50
+ or open an issue — no known external consumer existed at removal time.
51
51
 
52
52
  ## Release gating (maintainers)
53
53
 
@@ -218,27 +218,52 @@ warning naming the fresh-purpose remedy. On an older `aw` the pre-1.36.1
218
218
  report stands (`aliasReusable: false`, warning naming aweb-abim), because
219
219
  that CLI cannot revoke the certificate.
220
220
 
221
- ## oats.aweb settings (1.12.0)
221
+ ## oats.aweb settings (1.12.1)
222
222
 
223
223
  Set in `oats-local.yaml` under `settings.oats.aweb.<key>` (host-owned), in the
224
224
  soul's `messaging:` payload (true of every instance), or per spawn with
225
225
  `oats spawn … --provider oats.aweb <key>=<value>`. The effective payload is
226
226
  merged in order: workspace messaging, `byTeam[team]`, soul messaging,
227
227
  `oats-local.yaml` `settings.oats.aweb`, then per-spawn `--provider` values.
228
- `residents` is host-file-only: put custody paths only in `oats-local.yaml`,
229
- never in a committed workspace or soul file (current kernels document this rule
230
- but do not yet enforce provenance in the hook payload).
228
+ `root`, `roots`, and `residents` are host-file-only: put absolute root/custody
229
+ paths only in `oats-local.yaml`, never in a committed workspace or soul file
230
+ (current kernels document this rule but do not yet enforce provenance in the
231
+ hook payload; the manifest schema does not yet carry a host-only marker).
231
232
 
232
233
  - `team: <team id>`. The payload team wins over `OATS_TEAM_ID`/
233
234
  `OATS_TEAM_NAME`; if both are set and differ, the hook warns and uses the
234
235
  payload. Workspace v2 spawns can have an empty `OATS_TEAM_ID`, so set this in
235
- the payload for global grants.
236
+ the workspace file's `messaging:` / `messaging.byTeam.<label>.team`, or in
237
+ `settings.oats.aweb.team` for a host override.
238
+ - `root: /absolute/dir`. Host-owned absolute directory whose `.aw` is the aweb
239
+ minting root. A declared root without `.aw` is fatal; run `oats aweb setup`
240
+ there or set `settings.oats.aweb.root` to the initialized root.
241
+ - `roots: { <team id>: /absolute/dir }`. Host-owned map for deployments that
242
+ mint into several aweb teams. When a team is known, `roots[team]` wins over
243
+ `root`.
244
+ - Minting root resolution for spawn and setup is: `roots[team]` when the team is
245
+ known and present, else `root`. With no declared root, workspace v2 uses
246
+ `<OATS_WORKSPACE>` (the deployment directory whose `.aw` is used) and never
247
+ searches above it; classic deployments with `OATS_TEAM_SCOPE` keep the
248
+ historical bounded candidate search order exactly (team scope, home, home git
249
+ root, context, context git root, then workspace).
250
+ - `binding-check` answers `needs-configuration` before spawn with one problem
251
+ per missing item: `no messaging root at <dir>: run oats aweb setup there or
252
+ set settings.oats.aweb.root`; `no team: set messaging.byTeam.<label>.team in
253
+ the workspace file or settings.oats.aweb.team`. With both present it answers
254
+ `ready` (subject to captured-session checks when an invocation is supplied).
255
+ In classic deployments this readiness check approximates the full bounded
256
+ spawn search by checking `OATS_TEAM_SCOPE` before `OATS_WORKSPACE`; the spawn
257
+ hook itself still keeps the exact 1.12.0 bounded candidate order.
236
258
  - `identity.mode: local | global` (default `local`). Any other value is fatal.
237
259
  Local mode is the historical behavior: a spawned team identity is minted for
238
260
  the instance, or `identity.source` uses the existing retained-seat flow below.
239
261
  Its spawn meta includes `identity: { mode: "local", alias, team, address:
240
262
  null, resident: null }` beside the existing top-level `alias`, `team`, and
241
- `delivery` keys.
263
+ `delivery` keys. Local-mode spawn output contributes
264
+ `env.AWEB_IDENTITY_HOME=<home>/.aw` (and retained-seat local mode contributes
265
+ the same path) so `aw mail`, `aw chat`, `aw whoami`, `aw wake`, and
266
+ `aw workspace status` work from the instance's `work/` or any other cwd.
242
267
  - `identity.mode: global` makes the instance act as a resident global identity
243
268
  through an aweb session grant; it never mints a new global identity and never
244
269
  copies root keys into the instance home. `identity.resident` is required and
@@ -77,8 +77,8 @@ members:
77
77
  - git:github.com/acme/platform
78
78
  packages:
79
79
  oats.framework: v1.1.3
80
- oats.okf: v2.1.4
81
- oats.aweb: v1.12.0
80
+ oats.okf: v2.1.5
81
+ oats.aweb: v1.12.1
82
82
  teams:
83
83
  global: { description: Org-wide }
84
84
  engineering: { description: Platform }
@@ -328,7 +328,7 @@ Member capabilities need no approval: membership is the trust.
328
328
  **Non-interactive approval (CI, scripted rebuilds):**
329
329
 
330
330
  ```bash
331
- oats sync --approve oats.okf@v2.1.4 --approve oats.aweb@v1.12.0
331
+ oats sync --approve oats.okf@v2.1.5 --approve oats.aweb@v1.12.1
332
332
  ```
333
333
 
334
334
  `--approve <id>@<version>` is repeatable and approves **exactly** the entry the
@@ -340,7 +340,7 @@ stays unapproved (exit `2`, as above).
340
340
 
341
341
  `<version>` is the value `sync --json` reports as `approvalNeeded[].version`,
342
342
  which is what the lock records as the package's `version`. For a **catalog**
343
- package that is the published version (`oats.okf@2.1.4`). For a **git** source
343
+ package that is the published version (`oats.okf@2.1.5`). For a **git** source
344
344
  pinned by commit (`git:github.com/awebai/oats-okf@<oid>`) it is the **full
345
345
  commit OID**, not the `git:` reference and not a tag name — copy it from the
346
346
  `approvalNeeded` line rather than from your workspace file.
@@ -0,0 +1,48 @@
1
+ # OATS 0.25.8
2
+
3
+ ## Fixed
4
+
5
+ - **Desktop: quotes are escaped in attribute contexts** (#139). The views share
6
+ one escaper (`escapeHtml`) for element content and attribute values.
7
+
8
+ - **The roster reports an instance where it actually is.** `oats status`
9
+ reports each instance at the directory the kernel enumerated
10
+ (`<soul dir>/instances/<name>`), under that directory's name. An
11
+ `instance.json` whose `home` or `instance` disagrees no longer relocates or
12
+ renames the instance; the disagreeing values appear only as the diagnostics
13
+ `recordedHome` / `recordedInstance`. Before this, a hostile or corrupted
14
+ `instance.json` could make every consumer that acts on `home` (retire,
15
+ `inspect --home`, the Desktop's file roots) target a path outside the
16
+ deployment. Found by the Desktop engineer in Phase F slice F1.
17
+
18
+ ## Added
19
+
20
+ - **Team facts for workspace-model hooks** (human decisions 2026-09-24:
21
+ `oats.aweb` is the messaging default; teams are seamless). A workspace
22
+ deployment has no classic `team:` block, so lifecycle hooks of a workspace
23
+ spawn now receive: `OATS_TEAM_SCOPE` = the deployment directory;
24
+ `OATS_TEAM_ID` = the messaging slot's merged payload `team` (workspace base ⊕
25
+ `byTeam[<soul team>]` ⊕ soul ⊕ host ⊕ spawn), **empty when no shared team is
26
+ mapped — meaning "personal"**; and new `OATS_TEAM_LABEL` (the soul's team
27
+ label), `OATS_WORKSPACE_NAME` and `OATS_WORKSPACE_KEY` (canonical repository
28
+ key), so a messaging provider can derive a personal team per person per
29
+ workspace deterministically. Classic deployments are unchanged.
30
+ - **Desktop F2: Capabilities, Sources, sync and approval, onboarding** (#143).
31
+ The Workspace view gains a Capabilities table (Capability | Status | Used
32
+ by, from `oats capabilities --json`, filtered by team and source) and a
33
+ Sources tab. It can run `oats sync` and approve pending packages: the Desktop
34
+ admits an approval only when its (id, version, executables digest) exactly
35
+ match the latest sync report it holds, and refuses a mismatch with
36
+ `E_APPROVAL_STALE`. A picked folder that isn't yet a workspace can be
37
+ onboarded through single-use offers held by the main process. The 0.24
38
+ Deployment inventory and Workspace readiness blocks are removed.
39
+ - **Desktop: design parity with Redesign v3 and brand artwork** (#137, #128).
40
+ The Desktop F1 deployment model on kernel JSON (#126) is included too.
41
+ - **`oats.aweb` is the workspace messaging default** (#140, human decision).
42
+ The onboarding skill makes messaging setup its own step before the first
43
+ spawn (#141).
44
+ - **The onboarding next-step hint names `oats-operator-expert`** (#134).
45
+ - **Phase D:**
46
+ - The OATS repository hosts a workspace-model workspace (D2, #127).
47
+ - `oats-operator-expert` and `integrations-expert` souls, with the experts' instructions on the workspace model (D3, #129).
48
+ - `oats.core` and `oats.setup` rewritten for the workspace model (D4, #133).
@@ -0,0 +1,23 @@
1
+ # OATS 0.25.9
2
+
3
+ A pins-only patch: the kernel is unchanged since 0.25.8.
4
+
5
+ ## Updated
6
+
7
+ - **`oats.aweb` 1.12.1** (catalog pin + bundled copy; #145). The messaging
8
+ root is explicit: `settings.oats.aweb.root` / `roots[team]` in
9
+ `oats-local.yaml`. On a workspace-model deployment it defaults to the
10
+ deployment directory and is never searched for upward. Error texts name the
11
+ workspace-model remedies. In local mode an instance gets `AWEB_IDENTITY_HOME`,
12
+ so `aw` works from any directory. `binding-check` answers
13
+ `needs-configuration` before spawn, with one problem per missing item (no
14
+ messaging root; no team), and an unmapped workspace team label fails the spawn
15
+ closed instead of inheriting the root's active team.
16
+ - **`oats.okf` 2.1.5** (catalog pin + bundled copy; #148).
17
+ - Every configured base must be usable at spawn. A shallow base is refused
18
+ with `E_BASE_SHALLOW`; clone, fetch and checkout failures are
19
+ `E_BASE_UNAVAILABLE` naming the alias, repository, step and reason, never a
20
+ raw `ETIMEDOUT`.
21
+ - A missing `OATS_SOUL` is `E_OATS_SOUL_MISSING`; there's no fallback to the
22
+ home's soul link.
23
+ - Refusing an owner rename names both souls and both remedies.
@@ -6,24 +6,22 @@ hook as `$OATS_INSTANCE_HOME`. It is not your user home (`~`), not the repositor
6
6
  root, and not the work tree. Anything that says "your home" means this directory.
7
7
 
8
8
  - **Your brain and your state live here**: `AGENTS.md` (your composed
9
- instructions), `soul/` (your durable knowledge), `TASK.md` (this task),
10
- `instance.json` (what you were given and from where), and whatever working
11
- state your role keeps — your knowledge layer names those files, if you have
12
- one. They belong here, not in the work tree.
9
+ instructions), `TASK.md` (this task), `instance.json` (what you were given
10
+ and from where), and whatever working state your role keeps — your knowledge
11
+ layer names those files, if you have one. They belong here, not in the work
12
+ tree.
13
13
  - **Run OATS operational/lifecycle commands, and commands from active
14
14
  capabilities, from instance home** — `oats status`, `oats doctor`, `oats spawn`,
15
- `oats retire`, and whatever your own capabilities add; for example, when the
15
+ and whatever your own capabilities add; for example, when the
16
16
  aweb messaging capability is active, run `aw` there too. They resolve their
17
17
  scope from the directory you run them in, so running them from the work tree
18
18
  points them at the wrong deployment. To act on a different package or config
19
19
  scope deliberately, pass an explicit resolved path: `oats <cmd> --dir <path>`.
20
- - **The home's `soul` link is not your edit surface.** It is there so you can
21
- READ your durable knowledge. Writing through it changes durable state outside
22
- your branch, where no review sees it and nothing records what changed or why.
23
- If your TASK is to change soul content that lives in this repository, that is
24
- ordinary code work — do it on tracked paths under `work/`, reviewed like the
25
- rest. How your own learnings reach your soul is your knowledge layer's
26
- business, and its instructions below say so if you have one.
20
+ - **Soul work is repository work.** If your TASK is to change soul content that
21
+ lives in this repository, that is ordinary code work — do it on tracked paths
22
+ under `work/`, reviewed like the rest. How your own learnings reach your soul
23
+ is your knowledge layer's business, and its instructions below say so if you
24
+ have one.
27
25
 
28
26
  **`<instance-home>/work` is your repository or workspace view** — whatever your
29
27
  work mode grants you of the code.
package/injects/oats.md CHANGED
@@ -1,10 +1,10 @@
1
1
  ## You run on OATS
2
2
 
3
3
  You are an agent instance in the OATS (Open Agent Team Specification) framework.
4
- You incarnate a durable soul (`./soul/`), you work in `./work/`, and you can
5
- be retired when your task ends. The **oats** skill teaches the essentials —
6
- your home layout, the agent roster (`oats status`), spawning and
7
- retiring instances (only when instructed), inspecting your configuration
4
+ You incarnate a durable soul and you work in `./work/`.
5
+ The **oats** skill teaches the essentials —
6
+ your home layout, the agent roster (`oats status`), spawning
7
+ instances (only when instructed), inspecting your configuration
8
8
  (`oats doctor`, `./instance.json`), and your lifecycle. **Load the oats skill
9
9
  before your first `oats` command of a session** and any time you reason about
10
10
  agents, spawning, or the framework itself — do not guess `oats` flags or
package/lib/core.mjs CHANGED
@@ -4299,7 +4299,8 @@ export function composeInstanceAgentsMd(soulDir, contextDir, soulName, workMode,
4299
4299
  resolved.capabilities = Array.isArray(prepared.capabilityRows) && prepared.capabilityRows.length
4300
4300
  ? prepared.capabilityRows
4301
4301
  : plannedCapabilityRows(prepared.resolution);
4302
- resolved.workspace = { key: prepared.discovery?.key ?? null, commit: prepared.discovery?.commit ?? null, standalone: prepared.discovery?.standalone === true, revision: prepared.resolution.revision, team: prepared.resolution.soul?.team ?? null, slots: prepared.resolution.slots };
4302
+ resolved.workspace = { key: prepared.discovery?.key ?? null, name: prepared.discovery?.workspace?.name ?? null, deployment: prepared.deployment ?? null, commit: prepared.discovery?.commit ?? null, standalone: prepared.discovery?.standalone === true, revision: prepared.resolution.revision, team: prepared.resolution.soul?.team ?? null, slots: prepared.resolution.slots };
4303
+ resolved.payloads = prepared.resolution.payloads ?? {};
4303
4304
  resolved.layers = Object.fromEntries(Object.entries(prepared.resolution.slots || {}).map(([slot, mod]) => [slot, mod ? { capability: mod } : null]));
4304
4305
  }
4305
4306
  const wanted = [];
@@ -4837,6 +4838,28 @@ export function stableSoulId({ soulId, home, soulDir, agentName } = {}) {
4837
4838
  }
4838
4839
  export const workspaceSoulId = (repoKey, name) => `${repoKey}#${name}`;
4839
4840
 
4841
+ /** The team facts a lifecycle hook receives. Classic deployment: the config
4842
+ * chain's `team:` block (name, id, the declaring scope). Workspace model: there is
4843
+ * no `team:` block — the team SCOPE is the deployment directory, the team ID is the
4844
+ * messaging slot's merged payload `team` (workspace base ⊕ byTeam[<soul team>] ⊕
4845
+ * soul ⊕ host ⊕ spawn), and the provider also gets the soul's team LABEL and the
4846
+ * workspace's name and canonical key, so it can derive a per-person, per-workspace
4847
+ * personal team when no shared team is mapped (empty OATS_TEAM_ID = "personal").
4848
+ * Human decision 2026-09-24 (messaging default; seamless teams). */
4849
+ export function teamEnv(resolved) {
4850
+ const ws = resolved?.workspace;
4851
+ if (!ws || typeof ws !== "object") {
4852
+ return { OATS_TEAM_NAME: resolved?.team?.name || "", OATS_TEAM_ID: resolved?.team?.id || "", OATS_TEAM_SCOPE: resolved?.team?.scope || "" };
4853
+ }
4854
+ const messaging = ws.slots?.messaging;
4855
+ const payload = messaging ? resolved.payloads?.[messaging] : null;
4856
+ const teamId = payload && typeof payload === "object" && typeof payload.team === "string" ? payload.team : "";
4857
+ return {
4858
+ OATS_TEAM_NAME: "", OATS_TEAM_ID: teamId, OATS_TEAM_SCOPE: ws.deployment || "",
4859
+ OATS_TEAM_LABEL: typeof ws.team === "string" ? ws.team : "",
4860
+ OATS_WORKSPACE_NAME: typeof ws.name === "string" ? ws.name : "", OATS_WORKSPACE_KEY: typeof ws.key === "string" ? ws.key : "",
4861
+ };
4862
+ }
4840
4863
  export function runLifecycleHooks(event, { home, instance, agentName, soulDir, soulId, contextDir, workspaceDir, rootDir, resolved, priorMeta = {}, extraEnv = {}, assertRoots }) {
4841
4864
  const results = { meta: {}, briefs: [], warnings: [], order: [], launch: {}, env: {}, failures: [], contributions: [] };
4842
4865
  const envOwners = new Map();
@@ -4866,7 +4889,7 @@ export function runLifecycleHooks(event, { home, instance, agentName, soulDir, s
4866
4889
  OATS_EVENT: event, OATS_INSTANCE: instance, OATS_INSTANCE_HOME: home, OATS_HOME: home, OATS_AGENT: agentName,
4867
4890
  OATS_CAPABILITY: cap.id, OATS_LAYER: cap.layer || "", OATS_ROOT: rootDir || "",
4868
4891
  OATS_SOUL: soulDir || "", OATS_SOUL_ID: stableSoulId({ soulId, home, soulDir, agentName }), OATS_CONTEXT: contextDir, OATS_WORKSPACE: workspaceDir || "", OATS_LEVEL: cap.level || "",
4869
- OATS_TEAM_NAME: resolved.team?.name || "", OATS_TEAM_ID: resolved.team?.id || "", OATS_TEAM_SCOPE: resolved.team?.scope || "",
4892
+ ...teamEnv(resolved),
4870
4893
  ...extraEnv,
4871
4894
  // Hooks also run through direct core callers (not only bin/oats).
4872
4895
  // Author this from the running kernel, never PATH, ambient env or
@@ -7428,7 +7451,16 @@ export function listInstances(root, tmuxSession = DEFAULT_TMUX_SESSION) {
7428
7451
  catch (error) { liveness = { running: null, runtimeState: "unreachable", runtimeError: error.message }; }
7429
7452
  }
7430
7453
  const identity = servedIdentityOf(meta);
7431
- return { ...meta, ...(identity ? { identity } : {}), ...(meta.launch && typeof meta.launch === "object" ? { launch: redactLaunchRecipe(meta.launch) } : {}), ...(typeof meta.command === "string" ? { command: redactLaunchCommand(meta.command) } : {}), ...liveness, ...(rollbackIncomplete ? { rollbackIncomplete } : {}), ...(retirePending ? { retirePending } : {}) };
7454
+ // The home IS the directory enumerated here, and the instance its name:
7455
+ // a file inside it cannot relocate or rename itself in the roster (every
7456
+ // consumer acting on `home` — retire, inspect --home, the Desktop's file
7457
+ // roots — would inherit the claim). A disagreeing claim survives only as
7458
+ // a diagnostic.
7459
+ const claims = {
7460
+ ...(meta.home !== undefined && meta.home !== home ? { recordedHome: meta.home } : {}),
7461
+ ...(meta.instance !== undefined && meta.instance !== e.name ? { recordedInstance: meta.instance } : {}),
7462
+ };
7463
+ return { ...meta, ...claims, home, instance: e.name, ...(identity ? { identity } : {}), ...(meta.launch && typeof meta.launch === "object" ? { launch: redactLaunchRecipe(meta.launch) } : {}), ...(typeof meta.command === "string" ? { command: redactLaunchCommand(meta.command) } : {}), ...liveness, ...(rollbackIncomplete ? { rollbackIncomplete } : {}), ...(retirePending ? { retirePending } : {}) };
7432
7464
 
7433
7465
  });
7434
7466
  };
@@ -3,12 +3,12 @@
3
3
  "packages": {
4
4
  "oats.okf": {
5
5
  "url": "https://github.com/awebai/oats-okf.git",
6
- "ref": "v2.1.4",
6
+ "ref": "v2.1.5",
7
7
  "path": "oats-package"
8
8
  },
9
9
  "oats.aweb": {
10
10
  "url": "https://github.com/awebai/oats-aweb.git",
11
- "ref": "v1.12.0",
11
+ "ref": "v1.12.1",
12
12
  "path": "oats-package"
13
13
  },
14
14
  "oats.jira": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.25.7",
3
+ "version": "0.25.9",
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",
@@ -36,8 +36,6 @@
36
36
  "docs/",
37
37
  "README.md",
38
38
  "package-catalog.json",
39
- "souls/oats-setup-expert/soul.yaml",
40
- "souls/oats-setup-expert/AGENTS.md",
41
39
  "packages/record/bin/",
42
40
  "packages/record/lib/",
43
41
  "packages/record/docs/",
@@ -1,60 +0,0 @@
1
- # OATS Setup Expert
2
-
3
- Help an operator turn an empty deployment into a deliberately configured OATS
4
- workspace. Explain the next small decision, inspect the existing state, obtain
5
- approval for effects, and verify the result before moving on. Do not replace
6
- working deployments or turn setup into an implicit enrollment operation.
7
-
8
- ## Your supplied procedures
9
-
10
- - Load **oats-workspace-setup** for workspace/source discovery and adoption:
11
- declare, inspect, prepare, approve, scaffold and start are different steps.
12
- - Load **oats-config** for version-scoped classic configuration and targeting;
13
- never use its cascade to fill a missing captured input.
14
- - Load **oats-packages** for official package discovery, acquisition, exact locks,
15
- executable approval and updates.
16
- - Load **oats-operate** for lifecycle, directory boundaries and supported CLI
17
- operations; load **oats-souls** for source editions, roster and relations.
18
-
19
- Use the procedures actually included in your composition. Do not fetch a current
20
- skill or invent a command when an older installed version lacks a feature.
21
-
22
- ## Setup sequence
23
-
24
- 1. Establish the operator's intended deployment, work target and workspace/source
25
- separately. Inspect existing configuration, locks and souls before proposing
26
- changes. A workspace is a shared definition, not a shared live runtime.
27
- 2. Explain `oats-workspace.yaml` and each member's separate `oats.yaml` exports
28
- and backlink. Check reciprocal observations; discovery is neither membership
29
- enrollment nor capability activation. Pin imports only after a source is
30
- published at a real reviewed revision; never invent a future commit or tag.
31
- 3. Select capabilities and their exact sources with the operator. New souls
32
- declare removable `oats.core` explicitly. Do not add knowledge, messaging or
33
- tasks merely because the package was discovered or acquired.
34
- 4. Keep package acquisition, executable approval, provider configuration and
35
- native account/team authorization distinct. Inspect the exact artifact and
36
- its effects before asking for approval. An official catalog entry is not a
37
- blanket grant to execute hooks or change credentials.
38
- 5. Use the supported prepare/approve/scaffold/start path for retained portable
39
- adoption. Verify complete resources and required provider readiness before
40
- native effects. A successful inspection, scaffold or submitted command is
41
- not proof of a working session, message delivery or accepted learning.
42
-
43
- ## Bootstrap and safety boundaries
44
-
45
- This setup role has no hard knowledge or messaging dependency: it must be useful
46
- before OKF or aweb is configured. Its defaults permit none. That does NOT permit
47
- removing another soul's hard requirements to make a failing launch appear ready.
48
-
49
- A classic local bootstrap copy is not a captured preparation or retained source
50
- identity. Say which path created your current soul and do not claim one path's
51
- receipts as evidence for the other. Keep a source edition and an operator-local
52
- configuration distinct; never commit live identities, machine paths, accounts,
53
- private bindings or credentials into exported source definitions.
54
-
55
- Never auto-launch a model session, enable dangerous permissions, enroll an
56
- identity, install a host service, overwrite an existing soul or migrate knowledge
57
- without the operator's explicit instruction. Use ordinary native runtime auth;
58
- missing auth is a human login step, not permission to inspect, copy or wrap
59
- credentials. Preserve existing instances, pending jobs, locks and failed receipts.
60
- Report unsupported operations or infrastructure faults instead of bypassing them.
@@ -1,14 +0,0 @@
1
- schemaVersion: 1
2
- name: oats-setup-expert
3
- description: Guide an operator through OATS workspace adoption and explicit capability setup.
4
- work: directory
5
- requires:
6
- capabilities:
7
- oats.core:
8
- source: repo:oats-package
9
- oats.setup:
10
- source: repo:oats-package
11
- defaults:
12
- knowledge: none
13
- messaging: none
14
- tasks: none