@awebai/oats 0.27.0 → 0.27.2
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/capabilities/oats-aweb/bin/oats-aweb.mjs +272 -72
- package/capabilities/oats-aweb/injects/aweb.md +9 -1
- package/capabilities/oats-aweb/lib/binding-wire.mjs +42 -11
- package/capabilities/oats-aweb/oats.json +47 -5
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +79 -301
- package/docs/capabilities.md +3 -1
- package/docs/desktop-cli-api.md +6 -0
- package/docs/packages.md +1 -1
- package/docs/release-notes/v0.27.1.md +29 -0
- package/docs/release-notes/v0.27.2.md +51 -0
- package/docs/souls-and-instances.md +12 -0
- package/lib/core.mjs +102 -6
- package/package-catalog.json +1 -1
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* OATS_CONTEXT resolution context dir (the soul's repo / agents root parent)
|
|
18
18
|
* OATS_WORKSPACE the agents root's parent — the team boundary
|
|
19
19
|
* OATS_SETTINGS JSON of the provider's `settings:` block
|
|
20
|
-
*
|
|
20
|
+
* OATS_TEAM_ID/OATS_TEAM_LABEL/OATS_TEAM_LABELS/OATS_TEAMS workspace team facts (may be empty)
|
|
21
21
|
* OATS_META JSON persisted from this hook's previous spawn output (retire only)
|
|
22
22
|
*
|
|
23
23
|
* Output (spawn, stdout JSON):
|
|
@@ -74,6 +74,8 @@ const run = (argv, cwd, timeout = 45000, { secrets = [], secretSafe = false, env
|
|
|
74
74
|
const why = secretSafe ? "" : (scrub(e.stderr).trim() || (e.status === undefined ? String(e.code || "failed") : ""));
|
|
75
75
|
const err = new Error(`${where} failed${e.status === undefined ? "" : ` (exit ${e.status})`}${why ? `: ${why}` : ""}${secretSafe ? " (output withheld: this command handles credentials)" : ""}`);
|
|
76
76
|
err.status = e.status;
|
|
77
|
+
err.stdout = scrub(e.stdout).trim();
|
|
78
|
+
err.stderr = scrub(e.stderr).trim();
|
|
77
79
|
// A classification, never the text: the caller may name a KNOWN failure
|
|
78
80
|
// class (an alias that still holds a certificate) without any output of a
|
|
79
81
|
// credential-handling command reaching a log.
|
|
@@ -158,10 +160,13 @@ try {
|
|
|
158
160
|
|
|
159
161
|
const event = process.env.OATS_EVENT || process.argv[2];
|
|
160
162
|
const instance = process.env.OATS_INSTANCE;
|
|
161
|
-
|
|
163
|
+
let home = process.env.OATS_HOME || process.cwd();
|
|
162
164
|
const AWEB_ALIAS_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
163
165
|
const AWEB_ALIAS_RULE = "invalid alias: aweb aliases must be 1-64 characters, start with a letter or digit, and then contain only letters, digits, '-' or '_'";
|
|
164
166
|
const ALIAS_REUSE_REMEDY = "spawn with a different --name (kernels 0.26.0+) or a different --purpose";
|
|
167
|
+
const CLASSIC_REFUSAL = "oats.aweb 1.14 needs OATS 0.26.0 or newer (workspace model); on an older kernel pin oats.aweb v1.13.x";
|
|
168
|
+
const hasWorkspaceV2Facts = () => !!(process.env.OATS_WORKSPACE_KEY || process.env.OATS_WORKSPACE_NAME || process.env.OATS_TEAM_LABEL);
|
|
169
|
+
const isClassicEnvironment = () => !!process.env.OATS_TEAM_SCOPE && !hasWorkspaceV2Facts();
|
|
165
170
|
// Effective capability settings, injected by kernel dispatch (OATS_SETTINGS).
|
|
166
171
|
// delivery: "channel" (default) keeps the native channel packages waking the
|
|
167
172
|
// instance; "session" hands delivery to the host wake broker (aweb-abil):
|
|
@@ -175,51 +180,18 @@ const deliveryMode = (() => {
|
|
|
175
180
|
})();
|
|
176
181
|
const identitySettings = settings.identity && typeof settings.identity === "object" && !Array.isArray(settings.identity) ? settings.identity : {};
|
|
177
182
|
const identityMode = identitySettings.mode === undefined || identitySettings.mode === null || identitySettings.mode === "" ? "local" : String(identitySettings.mode);
|
|
183
|
+
if (isClassicEnvironment() && ["spawn", "setup"].includes(event)) {
|
|
184
|
+
if (event === "setup") { console.error(CLASSIC_REFUSAL); process.exit(1); }
|
|
185
|
+
fatal(CLASSIC_REFUSAL);
|
|
186
|
+
}
|
|
178
187
|
if (!["local", "global"].includes(identityMode) && ["spawn", "retire"].includes(event)) fatal(`identity.mode must be either "local" or "global" (got ${JSON.stringify(identitySettings.mode)})`);
|
|
179
188
|
if (event === "spawn" && identityMode === "local" && !identitySettings.source && (!instance || !AWEB_ALIAS_RE.test(instance))) fatal(`${AWEB_ALIAS_RULE}; OATS_INSTANCE is ${instance ? "not valid" : "missing"}, so no identity could be minted`);
|
|
180
189
|
const payloadTeam = () => {
|
|
181
190
|
const fromSettings = typeof settings.team === "string" && settings.team.trim() ? settings.team.trim() : undefined;
|
|
182
|
-
|
|
183
|
-
return { team: fromSettings || fromEnv, payload: fromSettings, env: fromEnv };
|
|
191
|
+
return { team: fromSettings, payload: fromSettings, env: process.env.OATS_TEAM_ID || undefined };
|
|
184
192
|
};
|
|
185
193
|
const identityMeta = ({ mode = "local", alias, team, address = null, resident = null, grant }) => ({ mode, alias, team, address: address || null, resident: resident || null, ...(grant ? { grant } : {}) });
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* The aweb root (minting authority). BOUNDED candidates — the deployment's team
|
|
189
|
-
* scope (from config `team:`) is the natural home; we never walk past the
|
|
190
|
-
* workspace to the laptop root (a `.aw` there would be a different team;
|
|
191
|
-
* minting into it would be a silent cross-team leak):
|
|
192
|
-
* 1. the declared team scope (OATS_TEAM_SCOPE)
|
|
193
|
-
* 2. the instance home itself
|
|
194
|
-
* 3. the git repo root containing the home (if any)
|
|
195
|
-
* 4. the resolution context (the soul's target repo) and its git repo root
|
|
196
|
-
* 5. the workspace root (OATS_WORKSPACE — e.g. ~/lfx)
|
|
197
|
-
* First candidate with a `.aw` wins; none → no minting.
|
|
198
|
-
*/
|
|
199
|
-
function gitRootOf(startDir) {
|
|
200
|
-
let d = resolve(startDir);
|
|
201
|
-
while (true) {
|
|
202
|
-
if (existsSync(join(d, ".git"))) return d;
|
|
203
|
-
const parent = dirname(d);
|
|
204
|
-
if (parent === d) return undefined;
|
|
205
|
-
d = parent;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
function classicAwebRoot() {
|
|
209
|
-
const candidates = [];
|
|
210
|
-
const push = (p) => { if (p && !candidates.includes(resolve(p))) candidates.push(resolve(p)); };
|
|
211
|
-
push(process.env.OATS_TEAM_SCOPE);
|
|
212
|
-
push(home);
|
|
213
|
-
push(gitRootOf(home));
|
|
214
|
-
push(process.env.OATS_CONTEXT);
|
|
215
|
-
if (process.env.OATS_CONTEXT) push(gitRootOf(process.env.OATS_CONTEXT));
|
|
216
|
-
push(process.env.OATS_WORKSPACE);
|
|
217
|
-
for (const c of candidates) if (existsSync(join(c, ".aw"))) return c;
|
|
218
|
-
return undefined;
|
|
219
|
-
}
|
|
220
|
-
const hasWorkspaceV2Facts = () => !!(process.env.OATS_WORKSPACE_KEY || process.env.OATS_WORKSPACE_NAME || process.env.OATS_TEAM_LABEL);
|
|
221
|
-
const isClassicDeployment = () => !!process.env.OATS_TEAM_SCOPE && !hasWorkspaceV2Facts();
|
|
222
|
-
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)" : ""}`;
|
|
194
|
+
const teamConfigRemedy = () => "set settings.oats.aweb.team or keep an active team at the aweb root";
|
|
223
195
|
function declaredRootCandidate(team = payloadTeam().team) {
|
|
224
196
|
const roots = settings.roots && typeof settings.roots === "object" && !Array.isArray(settings.roots) ? settings.roots : {};
|
|
225
197
|
if (team && typeof roots[team] === "string" && roots[team].trim()) return { root: roots[team].trim(), key: `settings.oats.aweb.roots[${JSON.stringify(team)}]`, declared: true };
|
|
@@ -229,7 +201,7 @@ function declaredRootCandidate(team = payloadTeam().team) {
|
|
|
229
201
|
function rootSettingCandidate(team = payloadTeam().team) {
|
|
230
202
|
const declared = declaredRootCandidate(team);
|
|
231
203
|
if (declared) return declared;
|
|
232
|
-
const fallback = process.env.OATS_WORKSPACE ||
|
|
204
|
+
const fallback = process.env.OATS_WORKSPACE || process.cwd();
|
|
233
205
|
return fallback ? { root: fallback, key: "settings.oats.aweb.root", declared: false } : undefined;
|
|
234
206
|
}
|
|
235
207
|
function awebRootProblem(candidate) {
|
|
@@ -241,7 +213,6 @@ function resolveAwebRoot() {
|
|
|
241
213
|
const declared = declaredRootCandidate();
|
|
242
214
|
if (declared && isAbsolute(declared.root) && existsSync(join(resolve(declared.root), ".aw"))) return resolve(declared.root);
|
|
243
215
|
if (declared?.declared) return undefined;
|
|
244
|
-
if (isClassicDeployment()) return classicAwebRoot();
|
|
245
216
|
const candidate = rootSettingCandidate();
|
|
246
217
|
if (candidate && isAbsolute(candidate.root) && existsSync(join(resolve(candidate.root), ".aw"))) return resolve(candidate.root);
|
|
247
218
|
return undefined;
|
|
@@ -257,7 +228,7 @@ const teamMemberships = (listed) => listed?.memberships || listed?.teams || [];
|
|
|
257
228
|
const teamIdsOf = (listed) => teamMemberships(listed).map((m) => m.team_id || m.id || m);
|
|
258
229
|
|
|
259
230
|
const AW_INSTALL = "install the aw CLI first — see https://aweb.ai/docs (or `oats aweb setup` for guided onboarding)";
|
|
260
|
-
const isCommand = ["roster", "setup"].includes(event);
|
|
231
|
+
const isCommand = ["roster", "setup", "teams", "join", "leave"].includes(event);
|
|
261
232
|
if (!onPath("aw")) {
|
|
262
233
|
if (isCommand) { console.error(`oats aweb ${event}: aw CLI not on PATH — ${AW_INSTALL}`); process.exit(1); }
|
|
263
234
|
if (event === "spawn") fatal(`aw CLI not on PATH, so no identity could be minted and this instance would have no messaging — ${AW_INSTALL}`);
|
|
@@ -477,10 +448,15 @@ function globalGrantRenew() {
|
|
|
477
448
|
out({ meta: newMeta, ...retainedLaunchOutput(newMeta, grantHome), ...(warning ? { warning } : {}) });
|
|
478
449
|
}
|
|
479
450
|
function globalGrantSpawn() {
|
|
480
|
-
const
|
|
481
|
-
if (!team) fatal("identity.mode \"global\" requires settings.oats.aweb.team (or OATS_TEAM_ID/OATS_TEAM_NAME) before minting a grant");
|
|
451
|
+
const resolvedTeam = payloadTeam();
|
|
482
452
|
const resident = String(identitySettings.resident || "");
|
|
483
453
|
const custody = resolveResidentCustody(resident);
|
|
454
|
+
let team = resolvedTeam.team;
|
|
455
|
+
const teamWarnings = [];
|
|
456
|
+
const unmappedPrimary = !team ? unmappedPrimaryRow() : undefined;
|
|
457
|
+
if (!team) team = activeTeamAt(custody);
|
|
458
|
+
if (unmappedPrimary && team) teamWarnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using personal team ${team}`);
|
|
459
|
+
if (!team) fatal("identity.mode \"global\" requires settings.oats.aweb.team or an active team at the resident custody root before minting a grant");
|
|
484
460
|
const grantHome = join(home, ".aweb-identity");
|
|
485
461
|
if (existsSync(grantHome)) fatal(`${grantHome} already exists; refusing to overwrite an existing aweb session grant home`);
|
|
486
462
|
const scopes = grantScopes();
|
|
@@ -528,8 +504,7 @@ function globalGrantSpawn() {
|
|
|
528
504
|
catch (revokeError) { failAfterMint(`session delivery registration failed for minted grant ${grantId}: ${e.message || e}; revoke failed: ${revokeError.message || revokeError}`); }
|
|
529
505
|
}
|
|
530
506
|
}
|
|
531
|
-
const warnings = [...preflight.warnings];
|
|
532
|
-
if (payload && envTeam && payload !== envTeam) warnings.push(`oats-aweb: settings.oats.aweb.team ${payload} differs from OATS team ${envTeam}; using payload team`);
|
|
507
|
+
const warnings = [...teamWarnings, ...preflight.warnings];
|
|
533
508
|
const e2eeBrief = preflight.warnings.length ? ` Warning: ${preflight.warnings.join(" ")}` : "";
|
|
534
509
|
const deliveryBrief = deliveryMode === "session"
|
|
535
510
|
? ` Notification delivery: external (AWEB_DELIVERY=session): the host wake broker (aw wake) is registered for this home and nudges you when mail or chat arrives; the native aweb channel is not running. If you have waited long with nothing arriving, check \`aw mail inbox\` and \`aw chat pending\` yourself at task boundaries.`
|
|
@@ -573,10 +548,32 @@ const workspaceAliasOf = (homeDir) => {
|
|
|
573
548
|
* signing key, a team certificate and a workspace binding. */
|
|
574
549
|
const joinedLate = (homeDir) => existsSync(join(homeDir, ".aw", "signing.key")) && existsSync(join(homeDir, ".aw", "team-certs")) && !!workspaceAliasOf(homeDir);
|
|
575
550
|
const JOIN_TIMEOUT_MS = Number(process.env.OATS_AWEB_JOIN_TIMEOUT_MS) > 0 ? Number(process.env.OATS_AWEB_JOIN_TIMEOUT_MS) : 120000;
|
|
551
|
+
// The first aw whose joined-team external homes are fully operable: local
|
|
552
|
+
// accept-invite under --identity-home, hosted workspace auto-connect, self-release
|
|
553
|
+
// and joined-root E2E key publication.
|
|
554
|
+
const JOINED_TEAMS_AW_MIN = "1.36.12";
|
|
555
|
+
function joinedTeamsAwFloorProblem() {
|
|
556
|
+
if (/^\d+\.\d+\.\d+$/.test(JOINED_TEAMS_AW_MIN) && awAtLeast(JOINED_TEAMS_AW_MIN)) return undefined;
|
|
557
|
+
if (!/^\d+\.\d+\.\d+$/.test(JOINED_TEAMS_AW_MIN)) return `E_TEAM_AW_FLOOR: joined-team identities need an aw release that admits local accept-invite under --identity-home, auto-connects the joined workspace and publishes the joined-root E2E key; installed aw is ${awVersionLabel()}. join= and oats aweb join are gated until that aw release exists`;
|
|
558
|
+
return `E_TEAM_AW_FLOOR: joined-team identities require aw >= ${JOINED_TEAMS_AW_MIN}; installed aw is ${awVersionLabel()}. join= and oats aweb join are gated until aw admits local accept-invite under --identity-home, auto-connects the joined workspace and publishes the joined-root E2E key`;
|
|
559
|
+
}
|
|
560
|
+
function requireJoinedTeamsAwFloor() {
|
|
561
|
+
const problem = joinedTeamsAwFloorProblem();
|
|
562
|
+
if (!problem) return;
|
|
563
|
+
const error = new Error(problem.replace(/^E_TEAM_AW_FLOOR: /, ""));
|
|
564
|
+
error.code = "E_TEAM_AW_FLOOR";
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
576
567
|
const yamlScalar = (text, key) => {
|
|
577
568
|
const m = String(text).match(new RegExp(`^${key}:\\s*["']?([^"'\\n#]+)["']?\\s*$`, "m"));
|
|
578
569
|
return m ? m[1].trim() : undefined;
|
|
579
570
|
};
|
|
571
|
+
function activeTeamAt(root) {
|
|
572
|
+
try {
|
|
573
|
+
const text = readFileSync(join(resolve(root), ".aw", "teams.yaml"), "utf8");
|
|
574
|
+
return yamlScalar(text, "active_team") || yamlScalar(text, "active");
|
|
575
|
+
} catch { return undefined; }
|
|
576
|
+
}
|
|
580
577
|
function retainedSeatSpawn(source, takeOver) {
|
|
581
578
|
if (typeof source !== "string" || !source.startsWith("/")) fatal("identity.source must be the absolute path of the legacy .aw directory to retain");
|
|
582
579
|
if (!existsSync(join(source, "signing.key"))) fatal(`identity.source ${source} holds no signing.key, so there is no identity to retain`);
|
|
@@ -699,19 +696,209 @@ function retainedSeatSpawn(source, takeOver) {
|
|
|
699
696
|
}
|
|
700
697
|
}
|
|
701
698
|
|
|
699
|
+
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
700
|
+
function parseOatsTeams(env = process.env) {
|
|
701
|
+
try {
|
|
702
|
+
const rows = JSON.parse(env.OATS_TEAMS || "[]");
|
|
703
|
+
return Array.isArray(rows) ? rows.filter((r) => r && typeof r === "object").map((r) => ({ label: String(r.label || ""), team: typeof r.team === "string" ? r.team : null, mapped: r.mapped === true, payload: r.payload && typeof r.payload === "object" ? r.payload : {} })) : [];
|
|
704
|
+
} catch { return []; }
|
|
705
|
+
}
|
|
706
|
+
function eligibleTeams() { return parseOatsTeams().filter((t) => t.mapped && t.label && t.team); }
|
|
707
|
+
function labelsCsv(text) { return String(text || "").split(",").map((s) => s.trim()).filter(Boolean); }
|
|
708
|
+
function primaryTeamLabel() { return process.env.OATS_TEAM_LABEL || labelsCsv(process.env.OATS_TEAM_LABELS)[0] || null; }
|
|
709
|
+
function requestedJoinLabels() { return labelsCsv(settings.join); }
|
|
710
|
+
function unmappedPrimaryRow() {
|
|
711
|
+
const primary = primaryTeamLabel();
|
|
712
|
+
return primary ? parseOatsTeams().find((t) => t.label === primary && !t.mapped) : undefined;
|
|
713
|
+
}
|
|
714
|
+
function validateJoinLabels(labels, { action = "join" } = {}) {
|
|
715
|
+
const eligible = eligibleTeams();
|
|
716
|
+
const byLabel = new Map(eligible.map((t) => [t.label, t]));
|
|
717
|
+
for (const label of labels) {
|
|
718
|
+
if (action === "leave" && label === "personal") {
|
|
719
|
+
const error = new Error(`E_TEAM_PERSONAL: ${label} is the personal team and cannot be left`);
|
|
720
|
+
error.code = "E_TEAM_PERSONAL";
|
|
721
|
+
throw error;
|
|
722
|
+
}
|
|
723
|
+
if (!LABEL_RE.test(label) || !byLabel.has(label)) {
|
|
724
|
+
const choices = eligible.map((t) => t.label).join(", ") || "(none)";
|
|
725
|
+
const error = new Error(`E_TEAM_NOT_ELIGIBLE: ${label} is not an eligible team label for this instance (eligible: ${choices})`);
|
|
726
|
+
error.code = "E_TEAM_NOT_ELIGIBLE";
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return labels.map((label) => byLabel.get(label));
|
|
731
|
+
}
|
|
732
|
+
const joinedTeamsOf = (meta = {}) => Array.isArray(meta.joinedTeams) ? meta.joinedTeams.filter((j) => j && typeof j === "object" && j.label && j.team && j.identityHome) : [];
|
|
733
|
+
const providerStateDir = () => join(home, ".oats-aweb");
|
|
734
|
+
const providerTeamsFile = () => join(providerStateDir(), "teams.json");
|
|
735
|
+
function readProviderTeamsState(meta = {}) {
|
|
736
|
+
try {
|
|
737
|
+
const doc = JSON.parse(readFileSync(providerTeamsFile(), "utf8"));
|
|
738
|
+
return { joinedTeams: joinedTeamsOf(doc) };
|
|
739
|
+
} catch { return { joinedTeams: joinedTeamsOf(meta) }; }
|
|
740
|
+
}
|
|
741
|
+
function writeProviderTeamsState(meta) {
|
|
742
|
+
mkdirSync(providerStateDir(), { recursive: true, mode: 0o700 });
|
|
743
|
+
writeFileSync(providerTeamsFile(), JSON.stringify({ joinedTeams: joinedTeamsOf(meta) }, null, 2) + "\n", { mode: 0o600 });
|
|
744
|
+
}
|
|
745
|
+
function withProviderTeams(meta = {}) { return { ...meta, joinedTeams: readProviderTeamsState(meta).joinedTeams }; }
|
|
746
|
+
function identityHomeForLabel(label) { return join(home, `.aweb-identity-${label}`); }
|
|
747
|
+
function awWithIdentity(identityHome, args) { return ["aw", "--identity-home", identityHome, ...args]; }
|
|
748
|
+
function commandOutput(e) { return [e?.stdout, e?.stderr, e?.message].filter(Boolean).join("\n"); }
|
|
749
|
+
function workspaceConnectRecovery(text) {
|
|
750
|
+
const m = String(text || "").match(/aw\s+--identity-home\s+\S+\s+workspace\s+connect\s+--service\s+\S+(?:\s+--json)?|aw\s+workspace\s+connect\s+--service\s+\S+(?:\s+--json)?|workspace\s+connect\s+--service\s+\S+(?:\s+--json)?/i);
|
|
751
|
+
return m ? m[0].trim() : undefined;
|
|
752
|
+
}
|
|
753
|
+
function joinedWorkspacePresent(identityHome) { return existsSync(join(identityHome, "workspace.yaml")); }
|
|
754
|
+
function releaseReceiptStatus(doc) {
|
|
755
|
+
return doc && typeof doc === "object" && doc.alias_released === true ? "released" : undefined;
|
|
756
|
+
}
|
|
757
|
+
function readCapabilityMeta() {
|
|
758
|
+
if (process.env.OATS_META) { try { return withProviderTeams(JSON.parse(process.env.OATS_META || "{}")); } catch { return withProviderTeams({}); } }
|
|
759
|
+
try { return withProviderTeams(JSON.parse(readFileSync(join(home, "instance.json"), "utf8")).capabilityMeta?.["oats.aweb"] || {}); } catch { return withProviderTeams({}); }
|
|
760
|
+
}
|
|
761
|
+
function teamsDocument(meta = readCapabilityMeta()) {
|
|
762
|
+
const teams = parseOatsTeams();
|
|
763
|
+
const joined = joinedTeamsOf(meta);
|
|
764
|
+
const joinedLabels = new Set(joined.map((j) => j.label));
|
|
765
|
+
const primary = primaryTeamLabel();
|
|
766
|
+
const personalTeam = meta.team || meta.identity?.team || payloadTeam().team || null;
|
|
767
|
+
return {
|
|
768
|
+
personal: { team: personalTeam },
|
|
769
|
+
primary,
|
|
770
|
+
eligible: teams.filter((t) => t.mapped && t.team).map((t) => ({ label: t.label, team: t.team, joined: joinedLabels.has(t.label) })),
|
|
771
|
+
joined: joined.map((j) => ({ label: j.label, team: j.team, identityHome: j.identityHome, receive: j.receive || "poll", since: j.since })),
|
|
772
|
+
unmapped: teams.filter((t) => !t.mapped).map((t) => t.label),
|
|
773
|
+
at: new Date().toISOString(),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function mintJoinedTeam(row, meta) {
|
|
777
|
+
const existing = joinedTeamsOf(meta).find((j) => j.label === row.label);
|
|
778
|
+
if (existing) return { meta, joined: existing, changed: false };
|
|
779
|
+
requireJoinedTeamsAwFloor();
|
|
780
|
+
const identityHome = identityHomeForLabel(row.label);
|
|
781
|
+
const root = awebRootForTeam(row.team);
|
|
782
|
+
if (!root) throw new Error(`${awebRootProblem(rootSettingCandidate(row.team))}, so team ${row.label} could not be joined`);
|
|
783
|
+
const inv = parseSecretJson(run(["aw", "team", "invite", "--team-id", row.team, "--json"], root, 45000, { secretSafe: true }), "aw team invite");
|
|
784
|
+
if (!inv?.token || typeof inv.token !== "string") throw new Error(`aw team invite returned no usable token for ${row.label}`);
|
|
785
|
+
let raw, acceptWarning;
|
|
786
|
+
try {
|
|
787
|
+
raw = parseSecretJson(run(awWithIdentity(identityHome, ["id", "team", "accept-invite", inv.token, "--name", instance || meta.alias, "--local", "--json"]), home, JOIN_TIMEOUT_MS, { secrets: [inv.token], secretSafe: true }), "aw id team accept-invite");
|
|
788
|
+
} catch (e) {
|
|
789
|
+
const output = commandOutput(e);
|
|
790
|
+
const recovery = workspaceConnectRecovery(output);
|
|
791
|
+
let accepted;
|
|
792
|
+
try { accepted = parseAwJson(e.stdout || "", "aw id team accept-invite"); } catch { accepted = undefined; }
|
|
793
|
+
if (!accepted?.team_id) throw new Error(`aw id team accept-invite failed for joined team ${row.label}${recovery ? `; recovery: ${recovery}` : ""}`);
|
|
794
|
+
raw = accepted;
|
|
795
|
+
acceptWarning = `joined team ${row.label} was accepted but workspace connect failed${recovery ? `; recovery: ${recovery}` : "; rerun the aw workspace connect recovery command printed by aw"}`;
|
|
796
|
+
}
|
|
797
|
+
const alias = typeof raw.alias === "string" && AWEB_ALIAS_RE.test(raw.alias) ? raw.alias : (instance || meta.alias);
|
|
798
|
+
const team = typeof raw.team_id === "string" && raw.team_id ? raw.team_id : row.team;
|
|
799
|
+
if (team !== row.team) throw new Error(`joined team ${team} differs from requested ${row.team}`);
|
|
800
|
+
const joined = { label: row.label, team, identityHome, receive: "poll", since: new Date().toISOString(), alias };
|
|
801
|
+
const next = { ...meta, joinedTeams: [...joinedTeamsOf(meta), joined] };
|
|
802
|
+
if (!joinedWorkspacePresent(identityHome)) {
|
|
803
|
+
const recovery = workspaceConnectRecovery(JSON.stringify(raw)) || raw.recovery_command || raw.recoveryCommand;
|
|
804
|
+
return { meta: next, joined, changed: true, warning: acceptWarning || `joined team ${row.label} was accepted but no workspace connection was written under ${identityHome}${recovery ? `; recovery: ${recovery}` : "; rerun the aw workspace connect recovery command printed by aw"}` };
|
|
805
|
+
}
|
|
806
|
+
return { meta: next, joined, changed: true, ...(acceptWarning ? { warning: acceptWarning } : {}) };
|
|
807
|
+
}
|
|
808
|
+
function leaveJoinedTeam(label, meta) {
|
|
809
|
+
const joined = joinedTeamsOf(meta);
|
|
810
|
+
const entry = joined.find((j) => j.label === label);
|
|
811
|
+
if (!entry) return { meta, changed: false };
|
|
812
|
+
let receipt, released;
|
|
813
|
+
try {
|
|
814
|
+
receipt = parseAwJson(run(awWithIdentity(entry.identityHome, ["workspace", "delete", entry.alias || meta.alias || instance, "--json"]), home, 60000), "aw workspace delete");
|
|
815
|
+
released = releaseReceiptStatus(receipt);
|
|
816
|
+
} catch (e) {
|
|
817
|
+
throw new Error(`failed to leave team ${label}; kept ${entry.identityHome} so cleanup can be retried: ${String(commandOutput(e)).slice(0, 300)}`);
|
|
818
|
+
}
|
|
819
|
+
if (!released) throw new Error(`failed to leave team ${label}; workspace delete did not report alias_released: true; kept ${entry.identityHome} so cleanup can be retried: ${JSON.stringify(receipt)}`);
|
|
820
|
+
try { rmSync(entry.identityHome, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
821
|
+
return { meta: { ...meta, joinedTeams: joined.filter((j) => j.label !== label) }, changed: true, released, receipt };
|
|
822
|
+
}
|
|
823
|
+
function awebRootForTeam(team) {
|
|
824
|
+
const declared = declaredRootCandidate(team);
|
|
825
|
+
if (declared && isAbsolute(declared.root) && existsSync(join(resolve(declared.root), ".aw"))) return resolve(declared.root);
|
|
826
|
+
if (declared?.declared) return undefined;
|
|
827
|
+
const candidate = rootSettingCandidate(team);
|
|
828
|
+
if (candidate && isAbsolute(candidate.root) && existsSync(join(resolve(candidate.root), ".aw"))) return resolve(candidate.root);
|
|
829
|
+
return undefined;
|
|
830
|
+
}
|
|
831
|
+
function parseHomeCommandArgs(argv = process.argv.slice(3)) {
|
|
832
|
+
const rest = [];
|
|
833
|
+
let json = false, labels;
|
|
834
|
+
for (let i = 0; i < argv.length; i++) {
|
|
835
|
+
const arg = argv[i];
|
|
836
|
+
if (arg === "--json") { json = true; continue; }
|
|
837
|
+
if (arg === "--home" && argv[i + 1]) { home = resolve(argv[++i]); process.env.OATS_HOME = home; continue; }
|
|
838
|
+
if (arg.startsWith("--home=")) { home = resolve(arg.slice("--home=".length)); process.env.OATS_HOME = home; continue; }
|
|
839
|
+
if (arg === "--labels" && argv[i + 1]) { labels = argv[++i]; continue; }
|
|
840
|
+
if (arg.startsWith("--labels=")) { labels = arg.slice("--labels=".length); continue; }
|
|
841
|
+
rest.push(arg);
|
|
842
|
+
}
|
|
843
|
+
if (!labels && rest[0]) labels = rest[0];
|
|
844
|
+
return { json, labels: labelsCsv(labels) };
|
|
845
|
+
}
|
|
846
|
+
function outputTeamsDocument(doc, json) {
|
|
847
|
+
if (json) { console.log(JSON.stringify(doc)); return; }
|
|
848
|
+
console.log(`personal: ${doc.personal.team || "unknown"}`);
|
|
849
|
+
for (const row of doc.eligible) console.log(`${row.joined ? "joined" : "eligible"}: ${row.label} (${row.team})`);
|
|
850
|
+
for (const label of doc.unmapped) console.log(`unmapped: ${label}`);
|
|
851
|
+
}
|
|
852
|
+
function runTeamsCommand(kind) {
|
|
853
|
+
const args = parseHomeCommandArgs();
|
|
854
|
+
let meta = readCapabilityMeta();
|
|
855
|
+
const actions = [];
|
|
856
|
+
const warnings = [];
|
|
857
|
+
if (kind === "join" || kind === "leave") {
|
|
858
|
+
const rows = validateJoinLabels(args.labels, { action: kind });
|
|
859
|
+
if (!rows.length) throw new Error("labels are required");
|
|
860
|
+
for (const row of rows) {
|
|
861
|
+
const result = kind === "join" ? mintJoinedTeam(row, meta) : leaveJoinedTeam(row.label, meta);
|
|
862
|
+
meta = result.meta;
|
|
863
|
+
actions.push({ action: kind, label: row.label, ...(result.released ? { released: result.released } : {}), ...(result.receipt ? { receipt: result.receipt } : {}), ...(result.warning ? { warning: result.warning } : {}) });
|
|
864
|
+
if (result.warning) warnings.push(`oats-aweb: ${result.warning}`);
|
|
865
|
+
}
|
|
866
|
+
writeProviderTeamsState(meta);
|
|
867
|
+
}
|
|
868
|
+
const doc = { ...teamsDocument(meta), ...(actions.length ? { actions } : {}), ...(warnings.length ? { warnings } : {}) };
|
|
869
|
+
outputTeamsDocument(doc, args.json);
|
|
870
|
+
}
|
|
871
|
+
|
|
702
872
|
if (event === "launch") {
|
|
703
873
|
if (identityMode === "global" || grantRenewMode() === "launch") globalGrantRenew();
|
|
704
|
-
|
|
874
|
+
let oldMeta = withProviderTeams(JSON.parse(process.env.OATS_META || "{}"));
|
|
875
|
+
const joined = joinedTeamsOf(oldMeta);
|
|
876
|
+
if (joined.length && process.env.OATS_TEAMS_SOURCE === "live") {
|
|
877
|
+
const eligible = new Set(eligibleTeams().map((t) => t.label));
|
|
878
|
+
let changed = false;
|
|
879
|
+
const warnings = [];
|
|
880
|
+
for (const row of joined) if (!eligible.has(row.label)) {
|
|
881
|
+
try { oldMeta = leaveJoinedTeam(row.label, oldMeta).meta; changed = true; }
|
|
882
|
+
catch (e) { warnings.push(`joined team ${row.label} cleanup failed: ${e.message || e}`); }
|
|
883
|
+
}
|
|
884
|
+
if (changed) writeProviderTeamsState(oldMeta);
|
|
885
|
+
out({ ...(changed ? { meta: oldMeta } : {}), ...retainedLaunchOutput(oldMeta), ...(warnings.length ? { warning: `oats-aweb: ${warnings.join(" | ")}` } : {}) });
|
|
886
|
+
}
|
|
887
|
+
if (joined.length && process.env.OATS_TEAMS_SOURCE !== "live") out({ ...retainedLaunchOutput(oldMeta), warning: "oats-aweb: teams-unverified — keeping joined team memberships because live eligible teams are unavailable" });
|
|
888
|
+
out(retainedLaunchOutput(oldMeta));
|
|
705
889
|
} else if (event === "spawn") {
|
|
706
890
|
if (identityMode === "global" && identitySettings.source) fatal('identity.mode "global" cannot be combined with identity.source; use identity.mode "local" with identity.source for a retained seat, or identity.mode "global" with identity.resident for a resident grant');
|
|
891
|
+
if (identityMode === "global" && requestedJoinLabels().length) fatal('settings.oats.aweb.join is supported only with local per-team identities; identity.mode "global" is explicit resident-grant mode');
|
|
707
892
|
if (identityMode === "global") globalGrantSpawn();
|
|
708
893
|
if (identityMode === "local" && settings.identity && typeof settings.identity === "object" && settings.identity.source) retainedSeatSpawn(String(settings.identity.source), settings.identity.takeOver === true);
|
|
894
|
+
let joinRows;
|
|
895
|
+
try { joinRows = validateJoinLabels(requestedJoinLabels()); }
|
|
896
|
+
catch (e) { fatal(e.message || e); }
|
|
709
897
|
let minted; // external identity, once `aw team join` succeeds
|
|
710
898
|
const root = awebRoot();
|
|
711
899
|
if (!root) {
|
|
712
900
|
const candidate = rootSettingCandidate();
|
|
713
|
-
|
|
714
|
-
fatal(`${awebRootProblem(candidate)}, so no identity could be minted and this instance would have no messaging${classicHint}`);
|
|
901
|
+
fatal(`${awebRootProblem(candidate)}, so no identity could be minted and this instance would have no messaging`);
|
|
715
902
|
}
|
|
716
903
|
try {
|
|
717
904
|
// Team correctness: the config's `team:` block wins (id, then name), else the
|
|
@@ -721,9 +908,10 @@ if (event === "launch") {
|
|
|
721
908
|
// cross-machine instance directory).
|
|
722
909
|
const resolvedTeam = payloadTeam();
|
|
723
910
|
let team = resolvedTeam.team;
|
|
724
|
-
const
|
|
725
|
-
|
|
911
|
+
const warnings = [];
|
|
912
|
+
const unmappedPrimary = !team ? unmappedPrimaryRow() : undefined;
|
|
726
913
|
if (!team) team = JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
914
|
+
if (unmappedPrimary && team) warnings.push(`oats-aweb: team-unmapped — workspace label ${unmappedPrimary.label} is not mapped; using personal team ${team}`);
|
|
727
915
|
if (!team) fatal(`cannot determine target team, so no identity could be minted — ${teamConfigRemedy()}, or activate a team at the aweb root`);
|
|
728
916
|
// A bare team name (no namespace) resolves against the root's memberships.
|
|
729
917
|
if (!team.includes(":")) {
|
|
@@ -779,7 +967,7 @@ if (event === "launch") {
|
|
|
779
967
|
run(["aw", "init", "--do-not-touch-agents-md"], home);
|
|
780
968
|
const alias = joined.alias;
|
|
781
969
|
const mismatch = joined.team_id !== team
|
|
782
|
-
? ` [WARNING: joined ${joined.team_id}, expected ${team}]` :
|
|
970
|
+
? ` [WARNING: joined ${joined.team_id}, expected ${team}]` : "";
|
|
783
971
|
// Runtime integration: for Claude Code sessions the aweb-channel plugin
|
|
784
972
|
// carries real-time push events. This hook does NOT install it. The plugin
|
|
785
973
|
// is a DECLARED runtime requirement (oats.json), consented once at
|
|
@@ -799,12 +987,17 @@ if (event === "launch") {
|
|
|
799
987
|
const deliveryBrief = deliveryMode === "session"
|
|
800
988
|
? ` Notification delivery: external (AWEB_DELIVERY=session): the host wake broker (aw wake) is registered for this home and nudges you when mail or chat arrives; the native aweb channel is not running. If you have waited long with nothing arriving, check \`aw mail inbox\` and \`aw chat pending\` yourself at task boundaries.`
|
|
801
989
|
: "";
|
|
990
|
+
let meta = { team: joined.team_id, alias, delivery: deliveryMode, identity: identityMeta({ mode: "local", alias, team: joined.team_id }) };
|
|
991
|
+
const joinFloorProblem = joinRows.length ? joinedTeamsAwFloorProblem() : undefined;
|
|
992
|
+
if (joinFloorProblem) warnings.push(`oats-aweb: ${joinFloorProblem}`);
|
|
993
|
+
else for (const row of joinRows) { const result = mintJoinedTeam(row, meta); meta = result.meta; if (result.warning) warnings.push(`oats-aweb: ${result.warning}`); }
|
|
994
|
+
writeProviderTeamsState(meta);
|
|
802
995
|
out({
|
|
803
|
-
meta
|
|
996
|
+
meta,
|
|
804
997
|
env,
|
|
805
|
-
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.`,
|
|
998
|
+
brief: `Comms: you have an aweb identity — alias "${alias}" on team ${joined.team_id}.${mismatch}${deliveryBrief} Joined team identities receive by polling in oats.aweb 1.14; run \`oats aweb teams --json\` for identity homes. Use \`aw mail\`/\`aw chat\` for messaging (see the aweb-messaging skill); coordination stays in your deployment's task layer.`,
|
|
806
999
|
...(launch ? { launch } : {}),
|
|
807
|
-
...(joined.team_id !== team ? { warning: `oats-aweb: team mismatch — joined ${joined.team_id}, expected ${team}` } :
|
|
1000
|
+
...(joined.team_id !== team ? { warning: `oats-aweb: team mismatch — joined ${joined.team_id}, expected ${team}` } : warnings.length ? { warning: warnings.join(" | ") } : channelWarning ? { warning: channelWarning } : {}),
|
|
808
1001
|
});
|
|
809
1002
|
} catch (e) {
|
|
810
1003
|
// A join may already have created a REMOTE identity before the failure.
|
|
@@ -813,16 +1006,23 @@ if (event === "launch") {
|
|
|
813
1006
|
fatal(`identity minting failed: ${e.message || e}`, minted);
|
|
814
1007
|
}
|
|
815
1008
|
} else if (event === "retire") {
|
|
816
|
-
let meta = JSON.parse(process.env.OATS_META || "{}");
|
|
1009
|
+
let meta = withProviderTeams(JSON.parse(process.env.OATS_META || "{}"));
|
|
1010
|
+
const retireWarnings = [];
|
|
817
1011
|
// A retained seat: release the lock and leave the identity alone. Never
|
|
818
1012
|
// aw workspace delete (it would soft-delete the standing identity's row)
|
|
819
1013
|
// and never team retire; the source .aw stays until a human removes it.
|
|
820
1014
|
if (meta.delivery === "session" && (meta.retained || meta.identity?.mode !== "global")) { if (!wakeDeregister(home)) process.stderr.write("oats-aweb: aw wake deregister failed; the broker treats a retired home as inactive on its own\n"); }
|
|
1015
|
+
if (meta.identity?.mode === "global" && !meta.retained) globalGrantRetire(meta);
|
|
1016
|
+
for (const joined of joinedTeamsOf(meta)) {
|
|
1017
|
+
try { meta = leaveJoinedTeam(joined.label, meta).meta; }
|
|
1018
|
+
catch (e) { retireWarnings.push(`joined team ${joined.label} cleanup failed: ${e.message || e}`); }
|
|
1019
|
+
}
|
|
1020
|
+
writeProviderTeamsState(meta);
|
|
821
1021
|
if (meta.retained) {
|
|
822
1022
|
if (meta.lock) { try { rmSync(meta.lock, { force: true }); } catch { /* the lock may already be gone */ } }
|
|
823
|
-
|
|
1023
|
+
const retainedWarning = `released the retained identity "${meta.alias}" (lock ${meta.lock || "?"} removed); the identity itself and ${meta.source || "its source"} are untouched${meta.tookOverFrom ? `; this seat had taken over from ${meta.tookOverFrom}` : ""}`;
|
|
1024
|
+
out({ meta: { retired: true, retained: true, identityReleased: true, joinedTeams: joinedTeamsOf(meta), ...(meta.tookOverFrom ? { tookOverFrom: meta.tookOverFrom } : {}) }, warning: `oats-aweb: ${[...retireWarnings, retainedWarning].join(" | ")}` });
|
|
824
1025
|
}
|
|
825
|
-
if (meta.identity?.mode === "global") globalGrantRetire(meta);
|
|
826
1026
|
// No alias means the spawn hook never reported an identity: nothing exists to
|
|
827
1027
|
// undo, which is completion. An alias WITH no local `.aw` is the opposite —
|
|
828
1028
|
// the remote record exists and its key is gone, so the self-delete cannot be
|
|
@@ -849,28 +1049,31 @@ if (event === "launch") {
|
|
|
849
1049
|
// aw 1.36.1 prints the cause as alias_released_reason (workspace.go,
|
|
850
1050
|
// workspace_self_retire.go); `reason` is tolerated for a later rename.
|
|
851
1051
|
const reason = typeof doc?.alias_released_reason === "string" ? doc.alias_released_reason : typeof doc?.reason === "string" ? doc.reason : (doc ? "unstated" : "no JSON answer");
|
|
852
|
-
out({ meta: { retired: true, aliasReusable: released, aliasReason: reason }, ...(released ? {} : { warning: `oats-aweb: workspace "${meta.alias}" deleted but its alias was not released (${reason}); spawn successors with a different --name (kernels 0.26.0+) or a different --purpose until it is` }) });
|
|
1052
|
+
out({ meta: { retired: true, aliasReusable: released, aliasReason: reason, joinedTeams: joinedTeamsOf(meta) }, ...(retireWarnings.length ? { warning: `oats-aweb: ${retireWarnings.join(" | ")}` } : released ? {} : { warning: `oats-aweb: workspace "${meta.alias}" deleted but its alias was not released (${reason}); spawn successors with a different --name (kernels 0.26.0+) or a different --purpose until it is` }) });
|
|
853
1053
|
}
|
|
854
1054
|
run(["aw", "workspace", "delete", meta.alias], home);
|
|
855
1055
|
// Honest: the workspace row is deleted, but a hosted local member cannot
|
|
856
1056
|
// revoke its own AWID certificate (aweb-abim), so the alias is NOT
|
|
857
1057
|
// reusable. retired stays true because the cleanup is as complete as the
|
|
858
1058
|
// platform allows; the field and the line carry the truth.
|
|
859
|
-
out({ meta: { retired: true, aliasReusable: false }, warning: `oats-aweb: workspace "${meta.alias}" deleted; its certificate is not revoked (aweb-abim), so the alias is not reusable — spawn successors with a different --name (kernels 0.26.0+) or a different --purpose` });
|
|
1059
|
+
out({ meta: { retired: true, aliasReusable: false, joinedTeams: joinedTeamsOf(meta) }, warning: `oats-aweb: ${[...retireWarnings, `workspace "${meta.alias}" deleted; its certificate is not revoked (aweb-abim), so the alias is not reusable — spawn successors with a different --name (kernels 0.26.0+) or a different --purpose`].join(" | ")}` });
|
|
860
1060
|
} catch (e) {
|
|
861
1061
|
// Exit nonzero: during a required-hook rollback this is the signal that
|
|
862
1062
|
// compensation did NOT complete, so the spawn is not reported as cleanly
|
|
863
1063
|
// rolled back while a remote identity still exists.
|
|
864
1064
|
out({ meta: { retired: false, reason: "self-delete-failed" }, warning: `oats-aweb: self-delete failed (the remote record will linger until stale): ${e.message || e}` }, 1);
|
|
865
1065
|
}
|
|
1066
|
+
} else if (["teams", "join", "leave"].includes(event)) {
|
|
1067
|
+
try { runTeamsCommand(event); process.exit(0); }
|
|
1068
|
+
catch (e) { console.error(e.code ? `${e.code}: ${e.message}` : `oats aweb ${event}: ${e.message || e}`); process.exit(1); }
|
|
866
1069
|
} else if (event === "roster") {
|
|
867
1070
|
// Cross-machine directory: every OATS-spawned instance joins the team with
|
|
868
1071
|
// alias = instance name, so the team's member roster lists live instances
|
|
869
1072
|
// wherever they run (plus human members). Local liveness comes from
|
|
870
|
-
// `oats status
|
|
1073
|
+
// `oats status` in the deployment; this is the network view.
|
|
871
1074
|
const root = awebRoot();
|
|
872
1075
|
if (!root) { console.error(`oats aweb roster: ${awebRootProblem(rootSettingCandidate())}`); process.exit(1); }
|
|
873
|
-
const team = process.env.OATS_TEAM_ID ||
|
|
1076
|
+
const team = process.env.OATS_TEAM_ID || JSON.parse(run(["aw", "team", "list", "--json"], root)).active_team;
|
|
874
1077
|
if (!team) { console.error(`oats aweb roster: cannot determine team (${teamConfigRemedy()}, or activate a team at the aweb root)`); process.exit(1); }
|
|
875
1078
|
const teamFlag = team.includes(":") ? ["--team-id", team] : ["--team", team];
|
|
876
1079
|
const r = JSON.parse(run(["aw", "id", "team", "members", ...teamFlag, "--json"], root, 60000));
|
|
@@ -904,11 +1107,8 @@ if (event === "launch") {
|
|
|
904
1107
|
const teamName = resolvedTeam.team;
|
|
905
1108
|
const teamId = typeof settings.team === "string" && settings.team.trim() ? settings.team.trim() : process.env.OATS_TEAM_ID;
|
|
906
1109
|
const candidate = rootSettingCandidate(teamName);
|
|
907
|
-
const scope =
|
|
908
|
-
|
|
909
|
-
: (candidate?.root ? resolve(candidate.root) : process.cwd());
|
|
910
|
-
const classic = isClassicDeployment() && settings.root === undefined && settings.roots === undefined && settings.team === undefined;
|
|
911
|
-
console.log(`aweb onboarding — ${classic ? "team scope" : "messaging root"}: ${scope}${teamName ? `, team: ${teamName}` : ""}\n`);
|
|
1110
|
+
const scope = candidate?.root ? resolve(candidate.root) : process.cwd();
|
|
1111
|
+
console.log(`aweb onboarding — messaging root: ${scope}${teamName ? `, team: ${teamName}` : ""}\n`);
|
|
912
1112
|
if (!isAbsolute(scope)) {
|
|
913
1113
|
console.log(`${candidate?.key || "settings.oats.aweb.root"} must be an absolute directory whose .aw is the aweb minting root.`);
|
|
914
1114
|
process.exit(0);
|
|
@@ -928,7 +1128,7 @@ if (event === "launch") {
|
|
|
928
1128
|
console.log(`✓ aweb workspace initialized and member of ${match}.`);
|
|
929
1129
|
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}\`.`);
|
|
930
1130
|
console.log(" Done — spawned instances will join this team automatically (alias = instance name).");
|
|
931
|
-
console.log(" Roster: `oats aweb roster` · local: `oats status
|
|
1131
|
+
console.log(" Roster: `oats aweb roster` · local: `oats status` (in the deployment)");
|
|
932
1132
|
return;
|
|
933
1133
|
}
|
|
934
1134
|
console.log(`readiness: needs-configuration`);
|
|
@@ -940,7 +1140,7 @@ if (event === "launch") {
|
|
|
940
1140
|
return;
|
|
941
1141
|
}
|
|
942
1142
|
console.log(` Workspace initialized, but no membership matching "${want}".`);
|
|
943
|
-
if (defaultTeamForUsername) console.log(` New hosted users create ${defaultTeamForUsername}; set
|
|
1143
|
+
if (defaultTeamForUsername) console.log(` New hosted users create ${defaultTeamForUsername}; set settings.oats.aweb.team to that id, then re-run setup.`);
|
|
944
1144
|
console.log(" Existing team path: ask a member for an invite token, then run `oats aweb setup --invite <token>` (uses `aw team join <token>` at the root).");
|
|
945
1145
|
console.log(" Team API-key path: set AWEB_API_KEY in the environment and run `oats aweb setup` (uses `aw init` at the root; the key is never printed).");
|
|
946
1146
|
console.log(" New hosted-account path: run `oats aweb setup --username <u>` (uses `aw init --username <u>` and creates default:<u>.aweb.ai).");
|
|
@@ -949,7 +1149,7 @@ if (event === "launch") {
|
|
|
949
1149
|
try {
|
|
950
1150
|
const hasRoot = existsSync(join(scope, ".aw"));
|
|
951
1151
|
if (!hasRoot && !actions.length) {
|
|
952
|
-
console.log(`No aweb workspace at the
|
|
1152
|
+
console.log(`No aweb workspace at the messaging root yet (${candidate?.key || "settings.oats.aweb.root"}).`);
|
|
953
1153
|
if (!want) console.log(` Also choose the aweb team for this deployment: ${teamConfigRemedy()}.`);
|
|
954
1154
|
console.log(" Choose one guided setup path:");
|
|
955
1155
|
console.log(" oats aweb setup --username <u> # runs `aw init --username <u>` and creates default:<u>.aweb.ai");
|
|
@@ -49,9 +49,17 @@ write it to a temp file and use `--body-file` — inline `--body` shell
|
|
|
49
49
|
escaping is a recurring failure.
|
|
50
50
|
|
|
51
51
|
Aliases are instance names (e.g. `dev-coordinator-1`). Discovery:
|
|
52
|
-
`oats status
|
|
52
|
+
`oats status` (in the deployment) lists this machine's live instances; `oats aweb roster`
|
|
53
53
|
lists the aweb team across machines.
|
|
54
54
|
|
|
55
|
+
**Joined teams.** Your default identity is the personal team — in 1.14.1 the root's default team as a stand-in until per-workspace personal teams exist. Workspace labels, including the primary label, are wider teams only when explicitly joined. If `oats aweb teams --json`
|
|
56
|
+
shows joined wider teams, each joined entry has an `identityHome`. Send or reply as
|
|
57
|
+
that team with exactly `aw --identity-home <identityHome> mail|chat ...`. In
|
|
58
|
+
1.14.1 joined teams receive by polling: check `aw --identity-home <identityHome>
|
|
59
|
+
mail inbox` and `aw --identity-home <identityHome> chat pending` at task
|
|
60
|
+
boundaries when you are working through that team. The native channel/wake path
|
|
61
|
+
listens to the primary identity only.
|
|
62
|
+
|
|
55
63
|
**Notification delivery.** Your instance briefing (TASK.md, the Comms line)
|
|
56
64
|
says how messages reach you. If it carries "Notification delivery: external",
|
|
57
65
|
the native channel is NOT running in this session and, until the host wake
|