@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 +6 -6
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +97 -21
- package/capabilities/oats-aweb/lib/binding-wire.mjs +27 -1
- package/capabilities/oats-aweb/oats.json +7 -1
- package/capabilities/oats-okf/bin/oats-okf.mjs +1 -1
- package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
- package/capabilities/oats-okf/lib/migration.mjs +2 -2
- package/capabilities/oats-okf/lib/sources.mjs +5 -4
- package/capabilities/oats-okf/lib/stores.mjs +40 -9
- package/capabilities/oats-okf/lib/worker.mjs +3 -3
- package/capabilities/oats-okf/oats.json +1 -1
- package/docs/design/2026-09-20-redesign-program-board.md +1 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +27 -0
- package/docs/design/2026-09-24-phase-d-plan.md +77 -6
- package/docs/desktop-cli-api.md +9 -3
- package/docs/desktop-succession.md +4 -4
- package/docs/integrations.md +31 -6
- package/docs/rebuild-to-v2.md +4 -4
- package/docs/release-notes/v0.25.8.md +48 -0
- package/docs/release-notes/v0.25.9.md +23 -0
- package/injects/instance-boundary.md +10 -12
- package/injects/oats.md +4 -4
- package/lib/core.mjs +35 -3
- package/package-catalog.json +2 -2
- package/package.json +1 -3
- package/souls/oats-setup-expert/AGENTS.md +0 -60
- package/souls/oats-setup-expert/soul.yaml +0 -14
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
|
|
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
|
|
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-
|
|
3022
|
-
const spawnHint = setupExpert ? `oats spawn oats-
|
|
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
|
|
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-
|
|
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 =>
|
|
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
|
|
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 =
|
|
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 (
|
|
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" } :
|
|
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
|
-
|
|
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)
|
|
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(
|
|
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 —
|
|
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
|
|
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" } :
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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(
|
|
696
|
-
console.log("
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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'
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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};
|
|
@@ -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
|
|
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) |
|
|
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
|
|
122
|
-
|
|
123
|
-
package, with a node in the central base
|
|
124
|
-
|
|
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
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -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[]`
|
|
964
|
-
|
|
965
|
-
|
|
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/
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
package/docs/integrations.md
CHANGED
|
@@ -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.
|
|
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`
|
|
229
|
-
never in a committed workspace or soul file
|
|
230
|
-
but do not yet enforce provenance in the
|
|
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
|
|
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
|
package/docs/rebuild-to-v2.md
CHANGED
|
@@ -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.
|
|
81
|
-
oats.aweb: v1.12.
|
|
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.
|
|
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.
|
|
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), `
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
your home layout, the agent roster (`oats status`), spawning
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/package-catalog.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|