@awebai/oats 0.24.6 → 0.24.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/oats.mjs +300 -16
- package/docs/design/2026-09-20-redesign-program-board.md +3 -3
- package/docs/design/2026-09-22-desktop-parity-seams.md +10 -6
- package/docs/desktop-cli-api.md +406 -0
- package/docs/release-notes/v0.24.7.md +81 -0
- package/docs/release-notes/v0.24.8.md +107 -0
- package/lib/core.mjs +224 -17
- package/lib/instance-events.mjs +76 -0
- package/lib/instance-git.mjs +225 -0
- package/lib/instance-lifecycle.mjs +181 -0
- package/lib/portable-soul.mjs +5 -1
- package/lib/readiness.mjs +141 -0
- package/lib/schedule.mjs +22 -3
- package/package.json +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
approveCapability, approveAvailableCapability, updatePackage, removePackage, migrateLegacyLock, applyLegacyLockMigration,
|
|
31
31
|
packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir, installedCapabilitiesDir, ownedCapabilitiesDir, loadPackageManifestAt,
|
|
32
32
|
resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, planInstanceResources, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
|
|
33
|
-
findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, listCapabilityAgents, workspaceOf,
|
|
33
|
+
findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, findInstanceHomes, listCapabilityAgents, workspaceOf, stopInstanceSession, recomposeInstanceInstructions,
|
|
34
34
|
ensureRoot, findRoot, findAgent, listAgents, listInstances, listAgentDefs, createAgent as coreCreateAgent,
|
|
35
35
|
spawnInstance, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, upsertLocalAgent, defaultRepo, RELATIONS, validateLaunchConfig, resolveLaunchSelection, resolveLaunchExecutable, checkLaunchExecutable, missingLaunchEnvRefs, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_RUNTIMES, LAUNCH_RECIPE_VERSION, parseLaunchCommand, resolveYolo, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
36
36
|
} from "../lib/core.mjs";
|
|
@@ -58,12 +58,17 @@ import { portableScope } from "../lib/portable-state.mjs";
|
|
|
58
58
|
import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../lib/captured-operation-process.mjs";
|
|
59
59
|
import { approveCapturedCapability } from "../lib/artifact-approvals.mjs";
|
|
60
60
|
import { loadSetupExpertEdition, SETUP_EXPERT, SETUP_CAPABILITIES } from "../lib/setup-expert-source.mjs";
|
|
61
|
+
import { observeInstanceGit, diffInstanceFile } from "../lib/instance-git.mjs";
|
|
62
|
+
import { planStop, applyStop, planRetire, resolveInstance as resolveInstanceForCli } from "../lib/instance-lifecycle.mjs";
|
|
63
|
+
const await_import_lifecycle = () => ({ resolveInstance: resolveInstanceForCli });
|
|
64
|
+
import { readinessOf, policyOf } from "../lib/readiness.mjs";
|
|
65
|
+
import { readEvents } from "../lib/instance-events.mjs";
|
|
61
66
|
import { parsePortableSource } from "../lib/source-spec.mjs";
|
|
62
67
|
|
|
63
68
|
const args = process.argv.slice(2);
|
|
64
69
|
let cmd = args[0];
|
|
65
70
|
const HELP_WORDS = new Set(["help", "--help", "-h"]);
|
|
66
|
-
const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "operation", "soul", "launch-config", "experimental", "onboard", "init", "inject", "install", "list", "catalog", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
|
|
71
|
+
const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "instance", "operation", "readiness", "soul", "launch-config", "experimental", "onboard", "init", "inject", "install", "list", "catalog", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
|
|
67
72
|
const flag = (name) => {
|
|
68
73
|
const i = args.indexOf(`--${name}`);
|
|
69
74
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -815,11 +820,36 @@ function homeContexts(home, meta) {
|
|
|
815
820
|
out.push(dirname(agentsRootOfHome(realOrResolved(home))));
|
|
816
821
|
return out;
|
|
817
822
|
}
|
|
823
|
+
/** A soul's own declared requirements/defaults/provenance, read from its
|
|
824
|
+
* soul.yaml through the kernel's parser (never guessed, never re-parsed by a
|
|
825
|
+
* consumer). Absent sections are null: an unrecorded provenance is a fact
|
|
826
|
+
* about the soul, not a prompt to infer one. */
|
|
827
|
+
function soulDeclarations(soulDir) {
|
|
828
|
+
const file = join(soulDir, "soul.yaml");
|
|
829
|
+
const empty = { declarations: { requires: null, defaults: null, knowledge: null, teams: null, resources: null, children: null }, provenance: null, problems: [] };
|
|
830
|
+
if (!existsSync(file)) return empty;
|
|
831
|
+
let parsed;
|
|
832
|
+
try { parsed = withConfigFile(file, () => parseYamlNested(readFileSync(file, "utf8"))); }
|
|
833
|
+
catch (e) { return { ...empty, problems: [{ code: "soul-declarations-unreadable", message: e.message }] }; }
|
|
834
|
+
const section = (key) => (parsed[key] !== undefined && parsed[key] !== null && typeof parsed[key] === "object") ? parsed[key] : (parsed[key] === undefined ? null : parsed[key]);
|
|
835
|
+
const provenance = section("provenance");
|
|
836
|
+
return {
|
|
837
|
+
declarations: { requires: section("requires"), defaults: section("defaults"), knowledge: section("knowledge"), teams: section("teams"), resources: section("resources"), children: section("children") },
|
|
838
|
+
provenance: provenance && typeof provenance === "object" ? {
|
|
839
|
+
kind: provenance.kind ?? null, source: provenance.source ?? null, revision: provenance.revision ?? null,
|
|
840
|
+
path: provenance.path ?? null, workspaceRevision: provenance.workspaceRevision ?? null,
|
|
841
|
+
} : null,
|
|
842
|
+
problems: [],
|
|
843
|
+
};
|
|
844
|
+
}
|
|
818
845
|
function soulEntry(soul, root, { capability } = {}) {
|
|
819
846
|
const dir = soul._dir || soul.soulDir;
|
|
820
847
|
const soulDir = capability ? soul.soulDir : join(dir, "soul");
|
|
821
848
|
const packaged = !!capability;
|
|
849
|
+
const declared = soulDeclarations(soulDir);
|
|
822
850
|
return {
|
|
851
|
+
soulsApi: 1, declarations: declared.declarations, provenance: declared.provenance,
|
|
852
|
+
declarationProblems: declared.problems,
|
|
823
853
|
name: soul.name, kind: packaged ? "capability" : (soul.kind || "persistent"), capability: capability || null,
|
|
824
854
|
type: soul.type ?? null, description: soul.description ?? null, repo: soul.repo ?? null, work: soul.work || "checkout",
|
|
825
855
|
runtime: soul.runtime || "pi", model: soul.model ?? null, yolo: soul.yolo === true || soul.yolo === "true" ? true : soul.yolo === false || soul.yolo === "false" ? false : null, launchConfig: soul["launch-config"] ?? null, backend: soul.backend ?? null,
|
|
@@ -834,8 +864,12 @@ function soulEntry(soul, root, { capability } = {}) {
|
|
|
834
864
|
* invoking process's ambient agents-root override must not redirect them
|
|
835
865
|
* to its own deployment. */
|
|
836
866
|
function dropAmbientRoot() { delete process.env.PI_AGENTS_ROOT; }
|
|
837
|
-
function inspectCmd() {
|
|
838
|
-
|
|
867
|
+
function inspectCmd() { const result = computeInspect(); if (!result) return; if (JSON_MODE) { const { _print, ...data } = result; jsonOk(data); return; } printInspect(result); }
|
|
868
|
+
/** The inspect answer as data — shared by `oats inspect` and `oats readiness`
|
|
869
|
+
* (K5), so the readiness quartet is derived from the SAME capability,
|
|
870
|
+
* activation, trust and soul facts inspect reports, never a second opinion. */
|
|
871
|
+
function computeInspect({ onFail } = {}) {
|
|
872
|
+
const bail = onFail || ((code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg)));
|
|
839
873
|
dropAmbientRoot();
|
|
840
874
|
const homeFlag = flag("home");
|
|
841
875
|
const home = homeFlag === true ? bail("E_BAD_ARGS", "--home needs an absolute instance home") : homeFlag;
|
|
@@ -910,7 +944,7 @@ function inspectCmd() {
|
|
|
910
944
|
const h = capabilityHealth(p.level, c, rows.capabilities[c.id], rows.packages[p.package]);
|
|
911
945
|
byId.set(c.id, {
|
|
912
946
|
id: c.id, package: p.package, version: c.version || null, layer: c.manifest?.layer || null, command: c.manifest?.command || null,
|
|
913
|
-
origin: "installed", level: p.level, source: p.source || null, dir: h.dir,
|
|
947
|
+
origin: "installed", level: p.level, source: p.source || null, commit: p.commit ?? rows.packages[p.package]?.commit ?? null, dir: h.dir,
|
|
914
948
|
health: { status: h.status, code: h.code, detail: h.detail, installed: !!c.installed, locked: true, trusted: c.trusted === true, integrity: c.integrity || null, installedIntegrity: h.integrity ?? null },
|
|
915
949
|
});
|
|
916
950
|
}
|
|
@@ -996,15 +1030,41 @@ function inspectCmd() {
|
|
|
996
1030
|
instructions: { ...readTextCapped(join(home, "AGENTS.md")), sources: meta.instructions || [] }, drift,
|
|
997
1031
|
};
|
|
998
1032
|
}
|
|
1033
|
+
// K4: the deployment's portable source context and each soul's declared
|
|
1034
|
+
// requirements joined against the capability inventory in this same payload.
|
|
1035
|
+
// Readiness is about the soul's declared sources, kept apart from
|
|
1036
|
+
// launchability (spawn) and adoption (prepare); unobservable = null.
|
|
1037
|
+
const capabilityById = new Map(capabilities.map((c) => [c.id, c]));
|
|
1038
|
+
for (const s of souls) {
|
|
1039
|
+
const required = s.declarations?.requires?.capabilities;
|
|
1040
|
+
const requirements = required && typeof required === "object" ? Object.entries(required).map(([id, spec]) => {
|
|
1041
|
+
const cap = capabilityById.get(id) || null;
|
|
1042
|
+
return { capability: id, source: spec && typeof spec === "object" ? spec.source ?? null : null,
|
|
1043
|
+
installed: cap ? cap.health?.installed ?? null : false, approved: cap ? cap.health?.trusted ?? null : null,
|
|
1044
|
+
active: cap ? !!cap.activation?.enabled : null, version: cap?.version ?? null };
|
|
1045
|
+
}) : null;
|
|
1046
|
+
s.readiness = { source: s.provenance ? "recorded" : "unrecorded", requirements,
|
|
1047
|
+
status: requirements === null ? "undeclared" : requirements.every((q) => q.installed === true) ? "sources-installed" : requirements.some((q) => q.installed === false) ? "sources-missing" : "unknown" };
|
|
1048
|
+
}
|
|
1049
|
+
const sourceKey = (p) => JSON.stringify([p.source, p.revision, p.path]);
|
|
1050
|
+
const sourceItems = [...new Map(souls.filter((s) => s.provenance?.source).map((s) => [sourceKey(s.provenance), { ...s.provenance, souls: [] }])).values()];
|
|
1051
|
+
for (const s of souls) if (s.provenance?.source) sourceItems.find((i) => sourceKey(i) === sourceKey(s.provenance)).souls.push(s.name);
|
|
1052
|
+
const sources = { soulsApi: 1, kind: sourceItems.length ? "recorded-provenance" : "none-recorded", items: sourceItems,
|
|
1053
|
+
note: sourceItems.length ? null : "no soul in this scope records a portable source address" };
|
|
999
1054
|
const result = {
|
|
1000
1055
|
operationsApi: 1, kernel: OATS_VERSION,
|
|
1001
1056
|
scope: { context: ctx, requestedContext: requestedContext === ctx ? null : requestedContext, workspace: roots.length ? workspaceOf(roots[0]) : ctx, team: r.team || null, chain: chain.map((c) => ({ file: c._file, level: c._level, levelKind: levelOf(c._level) })), agentsRoots: roots },
|
|
1002
1057
|
selected: { soul: selectedSoul?.name || null, agentsRoot: selectedSoul?.agentsRoot || null, home: home || null, source: meta ? "snapshot" : "config" },
|
|
1003
|
-
souls, layers, capabilities, knowledge, snapshot, currentConfig,
|
|
1058
|
+
souls, sources, layers, capabilities, knowledge, snapshot, currentConfig,
|
|
1004
1059
|
problems: [...(lockError ? [lockError] : []), ...packagedDiagnostics.map((d) => ({ code: d.code, message: d.message, capability: d.capability })),
|
|
1005
1060
|
...(meta ? snapshotCaps.filter((c) => !mans[c.id]).map((c) => ({ code: "captured-capability-missing", message: `${c.id} was active when this home was composed but no manifest for it is acquired now`, capability: c.id })) : [])],
|
|
1006
1061
|
};
|
|
1007
|
-
|
|
1062
|
+
result._print = { ctx, selectedSoul, home };
|
|
1063
|
+
return result;
|
|
1064
|
+
}
|
|
1065
|
+
function printInspect(result) {
|
|
1066
|
+
const { ctx, selectedSoul, home } = result._print; delete result._print;
|
|
1067
|
+
const { souls, layers, capabilities } = result;
|
|
1008
1068
|
console.log(`oats inspect — ${shortPath(ctx)}${selectedSoul ? ` soul ${selectedSoul.name}` : ""}${home ? ` home ${shortPath(home)}` : ""}`);
|
|
1009
1069
|
for (const s of souls) console.log(` soul ${s.name} [${s.kind}${s.capability ? ` ${s.capability}` : ""}] runtime ${s.runtime}${s.model ? ` model ${s.model}` : ""} work ${s.work}${s.editable.fields.length ? "" : " (read-only)"}`);
|
|
1010
1070
|
for (const l of LAYERS) console.log(` layer ${l}: ${layers[l].id || (layers[l].disabled ? "disabled" : "none")}${layers[l].provenance ? ` (${layers[l].provenance})` : ""}`);
|
|
@@ -3062,6 +3122,126 @@ function renderMergeRegion(r) {
|
|
|
3062
3122
|
/** `oats catalog [--json]` — the effective official package catalog, read-only.
|
|
3063
3123
|
* Identity/discovery for consumers that cannot import the kernel (Desktop):
|
|
3064
3124
|
* never acquires, never trusts, never fetches. */
|
|
3125
|
+
/** `oats instance <git|diff> <instance>` — K1: read-only Git observation of one
|
|
3126
|
+
* instance's work tree. The instance is addressed qualified: an explicit
|
|
3127
|
+
* --home, or a name under the --dir scope (team roots included) that resolves
|
|
3128
|
+
* to exactly one home; several homes refuse with every candidate named. */
|
|
3129
|
+
function instanceCmd() {
|
|
3130
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
3131
|
+
const sub = args[1], name = args[2];
|
|
3132
|
+
const usage = "usage: oats instance events <instance> [--limit <n>] [--since <iso>] [--home <abs>] [--dir <d>] [--json] | oats instance git <instance> [--home <abs>] [--dir <d>] [--json] | oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json] | oats instance stop <instance> (--plan | --apply --plan-revision <rev> --idempotency-key <key>) [--no-recursive] [--grace-ms <n>] [--home <abs>] [--dir <d>] [--json]";
|
|
3133
|
+
if (!["git", "diff", "stop", "events"].includes(sub) || !name || name.startsWith("--")) return bail("E_BAD_ARGS", usage);
|
|
3134
|
+
dropAmbientRoot();
|
|
3135
|
+
if (sub === "events") {
|
|
3136
|
+
// K7: typed producer events, bounded window; nothing inferred.
|
|
3137
|
+
const homeOpt = flag("home"); if (homeOpt === true || (homeOpt !== undefined && !isAbsolute(homeOpt))) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
3138
|
+
let root; try { root = ensureRoot(dirFlag()); } catch (e) { return bail(e.code || "E_NO_ROOT", e.message); }
|
|
3139
|
+
const limit = flag("limit"); const since = flag("since");
|
|
3140
|
+
if (limit === true || since === true) return bail("E_BAD_ARGS", usage);
|
|
3141
|
+
try {
|
|
3142
|
+
const { resolveInstance } = await_import_lifecycle();
|
|
3143
|
+
let home = homeOpt;
|
|
3144
|
+
if (!home) home = resolveInstance(dirFlag(), root, name).home;
|
|
3145
|
+
const ev = readEvents(home, { ...(limit !== undefined ? { limit: Math.max(1, Math.min(2000, Number(limit) || 200)) } : {}), ...(since ? { since } : {}) });
|
|
3146
|
+
if (JSON_MODE) { jsonOk(ev); return; }
|
|
3147
|
+
console.log(`${ev.instance}: ${ev.returned} of ${ev.count} event(s)${ev.truncated ? " (window truncated)" : ""}${ev.waitingOnYou ? ` — waiting on you since ${ev.waitingOnYou.since} (${ev.waitingOnYou.producer})` : ""}`);
|
|
3148
|
+
for (const e of ev.events) console.log(` ${e.at ?? "?"} ${e.kind.padEnd(20)} ${e.producer}${e.data ? ` ${JSON.stringify(e.data).slice(0, 120)}` : ""}`);
|
|
3149
|
+
return;
|
|
3150
|
+
} catch (e) { return bail(e.code || "E_EVENTS_FAILED", e.message, e.candidates ? { candidates: e.candidates } : undefined); }
|
|
3151
|
+
}
|
|
3152
|
+
if (sub === "stop") {
|
|
3153
|
+
// K3: plan → apply. The plan is what a confirmation shows; apply carries
|
|
3154
|
+
// its revision back and refuses if reality moved.
|
|
3155
|
+
const homeOpt = flag("home");
|
|
3156
|
+
if (homeOpt === true || (homeOpt !== undefined && !isAbsolute(homeOpt))) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
3157
|
+
let root; try { root = ensureRoot(dirFlag()); } catch (e) { return bail(e.code || "E_NO_ROOT", e.message); }
|
|
3158
|
+
const recursive = !args.includes("--no-recursive");
|
|
3159
|
+
const wantPlan = args.includes("--plan"), wantApply = args.includes("--apply");
|
|
3160
|
+
if (wantPlan === wantApply) return bail("E_BAD_ARGS", "stop needs exactly one of --plan or --apply");
|
|
3161
|
+
try {
|
|
3162
|
+
if (wantPlan) {
|
|
3163
|
+
const plan = planStop(dirFlag(), root, name, { home: homeOpt, recursive });
|
|
3164
|
+
if (JSON_MODE) { jsonOk(plan); return; }
|
|
3165
|
+
console.log(`stop ${name}${recursive ? " (and recorded children)" : ""} — plan ${plan.planRevision}`);
|
|
3166
|
+
for (const t of plan.targets) console.log(` ${" ".repeat(t.depth)}${t.instance}: session ${t.session.state}${t.work.observed ? `, ${t.work.changed} changed / ${t.work.untracked} untracked on ${t.work.branch ?? "detached"}` : ", work not observed"}${t.midTask === true ? " — mid-task" : t.midTask === "unknown" ? " — activity unknown" : ""}`);
|
|
3167
|
+
for (const n of plan.notes) console.log(` note: ${n}`);
|
|
3168
|
+
console.log(`apply with: oats instance stop ${name} --apply --plan-revision ${plan.planRevision} --idempotency-key <key>`);
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
const rev = flag("plan-revision"), key = flag("idempotency-key"), grace = flag("grace-ms");
|
|
3172
|
+
if (rev === true || key === true || grace === true) return bail("E_BAD_ARGS", usage);
|
|
3173
|
+
const receipt = applyStop(dirFlag(), root, name, { home: homeOpt, recursive, planRevision: rev, idempotencyKey: key, ...(grace !== undefined ? { graceMs: Number(grace) } : {}) });
|
|
3174
|
+
if (JSON_MODE) { jsonOk(receipt); return; }
|
|
3175
|
+
for (const r of receipt.results) console.log(` ${r.instance}: ${r.ok ? (r.stopped ? "stopped" : `already ${r.state}`) : `${r.code} — ${r.message}`}`);
|
|
3176
|
+
console.log(receipt.ok ? `stopped${receipt.replayed ? " (replayed receipt)" : ""}; home, work, transcript and launch configuration retained — restart with \`oats session restart\`` : "some targets are still running; nothing was escalated");
|
|
3177
|
+
if (!receipt.ok) process.exit(1);
|
|
3178
|
+
return;
|
|
3179
|
+
} catch (e) { return bail(e.code || "E_LIFECYCLE_FAILED", e.message, e.plan ? { plan: e.plan } : e.candidates ? { candidates: e.candidates } : undefined); }
|
|
3180
|
+
}
|
|
3181
|
+
let home = flag("home");
|
|
3182
|
+
if (home === true) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
3183
|
+
if (home !== undefined && !isAbsolute(home)) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
3184
|
+
if (home === undefined) {
|
|
3185
|
+
let root;
|
|
3186
|
+
try { root = ensureRoot(dirFlag()); } catch (e) { return bail(e.code || "E_NO_ROOT", e.message); }
|
|
3187
|
+
let r; try { r = resolveOatsConfig(dirFlag()); } catch (e) { return bail(e.code || "E_CONFIG_BROKEN", e.message); }
|
|
3188
|
+
const roots = [...new Set([root, ...(r.team ? teamAgentRoots(r.team.scope) : [])].map((p) => realOrResolved(p)))];
|
|
3189
|
+
const candidates = [];
|
|
3190
|
+
for (const rt of roots) for (const hit of findInstanceHomes(rt, name)) candidates.push({ root: rt, agent: hit.agent?.name ?? null, home: hit.home });
|
|
3191
|
+
if (!candidates.length) return bail("E_SESSION_UNKNOWN", `no instance ${JSON.stringify(name)} under ${roots.join(", ")}`);
|
|
3192
|
+
if (candidates.length > 1) return bail("E_AMBIGUOUS_INSTANCE", `instance ${JSON.stringify(name)} has ${candidates.length} homes; pass --home <abs>`, { candidates });
|
|
3193
|
+
home = candidates[0].home;
|
|
3194
|
+
} else if (basename(home) !== name) return bail("E_HOME_MISMATCH", `--home ${home} is not the home of instance ${JSON.stringify(name)}`);
|
|
3195
|
+
try {
|
|
3196
|
+
if (sub === "git") {
|
|
3197
|
+
const observed = observeInstanceGit(home);
|
|
3198
|
+
if (JSON_MODE) { jsonOk(observed); return; }
|
|
3199
|
+
const o = observed.observation;
|
|
3200
|
+
console.log(`${observed.instance} — ${shortPath(o.worktree)} @ ${o.branch ?? (o.detached ? `detached ${o.revision.slice(0, 12)}` : "unborn")}`);
|
|
3201
|
+
console.log(` upstream: ${observed.upstream.ref ? `${observed.upstream.ref} +${observed.upstream.ahead} -${observed.upstream.behind}` : "none (ahead/behind unknown)"}`);
|
|
3202
|
+
console.log(` base: ${observed.base.ref ? `${observed.base.ref} +${observed.base.ahead} -${observed.base.behind} (merge-base ${observed.base.mergeBase?.slice(0, 12)})` : "unknown"}`);
|
|
3203
|
+
console.log(` files: ${observed.files.length} (${Object.entries(observed.summary).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(", ") || "clean"})`);
|
|
3204
|
+
for (const f of observed.files) console.log(` ${f.xy} ${f.origPath ? `${f.origPath} -> ` : ""}${f.path} [${f.id}]`);
|
|
3205
|
+
for (const n of observed.notes) console.log(` note: ${n}`);
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
const fileId = flag("file"), revision = flag("revision"), indexRevision = flag("index-revision");
|
|
3209
|
+
if (fileId === true || revision === true || indexRevision === true) return bail("E_BAD_ARGS", usage);
|
|
3210
|
+
const d = diffInstanceFile(home, { fileId, revision, indexRevision });
|
|
3211
|
+
if (JSON_MODE) { jsonOk(d); return; }
|
|
3212
|
+
console.log(`${d.file.origPath ? `${d.file.origPath} -> ` : ""}${d.file.path} (${d.file.kind}, against ${d.against})${d.binary ? " [binary]" : ""}${d.truncated ? ` [truncated at ${d.limit} bytes]` : ""}`);
|
|
3213
|
+
if (!d.binary) process.stdout.write(d.patch);
|
|
3214
|
+
} catch (e) {
|
|
3215
|
+
bail(e.code || "E_GIT_FAILED", e.message, e.observation ? { observation: e.observation } : undefined);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
/** `oats readiness [--soul <name>] [--home <abs>] [--verify-signatures] [--policy] [--dir <d>] --json` — K5. */
|
|
3219
|
+
function readinessCmd() {
|
|
3220
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
3221
|
+
dropAmbientRoot();
|
|
3222
|
+
const inspect = computeInspect({ onFail: bail });
|
|
3223
|
+
if (!inspect) return;
|
|
3224
|
+
const soul = flag("soul") === true ? null : flag("soul") || inspect.selected?.soul || null;
|
|
3225
|
+
const verify = args.includes("--verify-signatures");
|
|
3226
|
+
let catalog = null; try { catalog = describeOfficialCatalog(); catalog = { packages: Object.fromEntries(catalog.packages.map((p) => [p.package, p])) }; } catch { catalog = null; }
|
|
3227
|
+
const deploymentDir = inspect.scope?.context ?? null;
|
|
3228
|
+
const readiness = readinessOf(inspect, { soul, verifySignatures: verify, catalog, deploymentDir });
|
|
3229
|
+
if (args.includes("--policy")) {
|
|
3230
|
+
const homeOpt = flag("home");
|
|
3231
|
+
let meta = null;
|
|
3232
|
+
if (homeOpt && homeOpt !== true) { try { meta = JSON.parse(readFileSync(join(homeOpt, "instance.json"), "utf8")); } catch (e) { return bail("E_SESSION_UNKNOWN", `${homeOpt}: ${e.message}`); } }
|
|
3233
|
+
readiness.policy = policyOf({ instanceMeta: meta, soul: soul ? inspect.souls.find((s) => s.name === soul) : null }).policy;
|
|
3234
|
+
readiness.notes.push("policy: a lifecycle-authority claim enforced by the spawn route, not an OS sandbox");
|
|
3235
|
+
}
|
|
3236
|
+
if (JSON_MODE) { jsonOk(readiness); return; }
|
|
3237
|
+
console.log(`readiness — ${readiness.subject.kind === "soul" ? `soul ${readiness.subject.name}` : shortPath(readiness.subject.context)}: ${readiness.summary.ready ? "READY" : `${readiness.summary.fail} failing, ${readiness.summary.unknown} unknown of ${readiness.summary.required} required`}`);
|
|
3238
|
+
for (const [name, check] of Object.entries(readiness.checks)) {
|
|
3239
|
+
console.log(` ${name}: ${check.status}`);
|
|
3240
|
+
for (const i of check.items) console.log(` ${i.status.padEnd(14)} ${i.subject}${i.required ? "" : " (optional)"}${i.reason ? ` — ${i.reason}` : ""}${i.signature ? ` · signature ${i.signature.status}${i.signature.signer?.label ? ` by ${i.signature.signer.label}` : ""}` : ""}${i.remedy ? ` → ${i.remedy}` : ""}`);
|
|
3241
|
+
}
|
|
3242
|
+
if (readiness.policy) console.log(` policy: child spawns ${readiness.policy.childSpawns.allowed ? "allowed" : "disabled"} (${readiness.policy.childSpawns.origin.kind}${readiness.policy.childSpawns.enforced ? ", enforced" : ""}); worktrees ${readiness.policy.worktrees.allowed === null ? "unknown" : readiness.policy.worktrees.allowed ? "allowed" : "not in this work mode"}`);
|
|
3243
|
+
for (const n of readiness.notes) console.log(` note: ${n}`);
|
|
3244
|
+
}
|
|
3065
3245
|
function catalogCmd() {
|
|
3066
3246
|
const described = describeOfficialCatalog();
|
|
3067
3247
|
if (JSON_MODE) { jsonOk(described); return; }
|
|
@@ -3960,7 +4140,7 @@ function statusTeam() {
|
|
|
3960
4140
|
|
|
3961
4141
|
function spawnCmd() {
|
|
3962
4142
|
// JSON mode: contract envelope, stable error codes, stderr-only progress.
|
|
3963
|
-
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
4143
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
3964
4144
|
const note = (msg) => (JSON_MODE ? console.error(msg) : console.log(msg));
|
|
3965
4145
|
const yolo = yoloFlag();
|
|
3966
4146
|
const backend = valueFlag("backend"), herdrSocket = valueFlag("herdr-socket");
|
|
@@ -3972,7 +4152,7 @@ function spawnCmd() {
|
|
|
3972
4152
|
};
|
|
3973
4153
|
checkDirectoryOptions(requestedWork); // before a local soul could be upserted
|
|
3974
4154
|
const name = args[1];
|
|
3975
|
-
if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--instructions-file <f>|--def-file <f>] [--no-launch] [--json]");
|
|
4155
|
+
if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>] [--preview] [--base <ref>] [--model <id>|@native-default] [--allow-child-spawns|--no-child-spawns] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--instructions-file <f>|--def-file <f>] [--no-launch] [--json]");
|
|
3976
4156
|
// Retired boundary flags (maintainer transport ruling): fail LOUDLY before
|
|
3977
4157
|
// ANY side effect — including root discovery and local-agent upsert (an
|
|
3978
4158
|
// --instructions-file spawn must not scaffold/overwrite a local soul before
|
|
@@ -4090,8 +4270,11 @@ function spawnCmd() {
|
|
|
4090
4270
|
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message); throw e; }
|
|
4091
4271
|
let r;
|
|
4092
4272
|
try {
|
|
4273
|
+
if (args.includes("--allow-child-spawns") && args.includes("--no-child-spawns")) bail("E_BAD_ARGS", "--allow-child-spawns and --no-child-spawns contradict");
|
|
4274
|
+
if (flag("base") === true) bail("E_BAD_ARGS", "--base needs a ref");
|
|
4093
4275
|
r = spawnInstance(root, agent, {
|
|
4094
4276
|
purpose: flag("purpose"), task: taskText, taskFile: taskFileFlag, relation, relativeTo, relativeRoot,
|
|
4277
|
+
...(args.includes("--allow-child-spawns") ? { allowChildSpawns: true } : args.includes("--no-child-spawns") ? { allowChildSpawns: false } : {}),
|
|
4095
4278
|
// Directory execution uses deployment configuration, not an ambient Git
|
|
4096
4279
|
// checkout (especially when invoked via --dir from a source instance).
|
|
4097
4280
|
repo: (requestedWork || agent.work) === "directory"
|
|
@@ -4099,7 +4282,13 @@ function spawnCmd() {
|
|
|
4099
4282
|
work: requestedWork, workDir, runtime: flag("runtime"), backend, herdrSocket, yolo, model: flag("model"), branch,
|
|
4100
4283
|
launchConfig: valueFlag("launch-config"),
|
|
4101
4284
|
launch: !args.includes("--no-launch"),
|
|
4285
|
+
// K6: --preview decides everything and touches nothing; --base <ref>
|
|
4286
|
+
// selects a worktree's start point; --model @native-default is the
|
|
4287
|
+
// explicit "runtime's own default" (distinct from omitting --model).
|
|
4288
|
+
...(args.includes("--preview") ? { preview: true } : {}),
|
|
4289
|
+
...(flag("base") !== undefined && flag("base") !== true ? { baseRef: flag("base") } : {}),
|
|
4102
4290
|
});
|
|
4291
|
+
if (args.includes("--preview")) { if (JSON_MODE) { jsonOk(r); return; } console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""}) runtime ${r.runtime}${r.model ? ` model ${r.model}` : ` (${r.modelSource})`}; nothing was created`); return; }
|
|
4103
4292
|
} catch (e) {
|
|
4104
4293
|
// A typed CLI failure keeps ITS OWN code: re-badging an unsafe-config-key
|
|
4105
4294
|
// (raised by the readers spawn walks) as E_SPAWN_FAILED tells an agent
|
|
@@ -4110,6 +4299,11 @@ function spawnCmd() {
|
|
|
4110
4299
|
// model, runtime) is a fact about the selection, not a spawn-mechanism
|
|
4111
4300
|
// failure: it keeps its own code so a GUI can act on it.
|
|
4112
4301
|
if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_RUNTIME$/.test(e.code)) { bail(e.code, e.message); throw e; }
|
|
4302
|
+
// An unmet declared requirement is a fact about the soul's configuration
|
|
4303
|
+
// (with a remedy), not a spawn-mechanism failure: keep its code and details.
|
|
4304
|
+
if (e?.code === "E_REQUIREMENT_INACTIVE") { bail(e.code, e.message, { soul: e.soul, capabilities: e.capabilities, context: e.context, remedy: e.remedy }); throw e; }
|
|
4305
|
+
if (e?.code === "E_CHILD_SPAWNS_DISABLED") { bail(e.code, e.message, { parent: e.parent, policy: e.policy }); throw e; }
|
|
4306
|
+
if (["E_BRANCH_EXISTS", "E_BASE_UNKNOWN"].includes(e?.code)) { bail(e.code, e.message); throw e; }
|
|
4113
4307
|
bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
|
|
4114
4308
|
}
|
|
4115
4309
|
// The instance exists from here on: a failed wake save is reported beside
|
|
@@ -4146,9 +4340,23 @@ function spawnCmd() {
|
|
|
4146
4340
|
|
|
4147
4341
|
function retireCmd() {
|
|
4148
4342
|
const name = args[1];
|
|
4149
|
-
if (!name || name.startsWith("--")) die("usage: oats retire <instance> [--home <path>] [--self] [--delete-branch] [--keep-dir] [--force] [--json]");
|
|
4343
|
+
if (!name || name.startsWith("--")) die("usage: oats retire <instance> [--plan] [--plan-revision <rev> --idempotency-key <key>] [--home <path>] [--self] [--discard-worktree] [--delete-branch] [--keep-dir] [--force] [--json]");
|
|
4150
4344
|
let homeFlag = flag("home");
|
|
4151
4345
|
if (homeFlag === true) die("--home needs the instance home path");
|
|
4346
|
+
if (args.includes("--plan")) {
|
|
4347
|
+
// K3: what retirement would touch, with the design's defaults — read-only.
|
|
4348
|
+
dropAmbientRoot();
|
|
4349
|
+
let root; try { root = ensureRoot(dirFlag()); } catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_NO_ROOT", e.message) : die(e.message); }
|
|
4350
|
+
try {
|
|
4351
|
+
const plan = planRetire(dirFlag(), root, name, { home: homeFlag });
|
|
4352
|
+
if (args.includes("--json")) { jsonOk(plan); return; }
|
|
4353
|
+
console.log(`retire ${name} — plan ${plan.planRevision}`);
|
|
4354
|
+
console.log(` session ${plan.facts.session.state}; work ${plan.facts.work.observed ? `${plan.facts.work.changed} changed / ${plan.facts.work.untracked} untracked on ${plan.facts.work.branch ?? "detached"}` : `not observed (${plan.facts.work.reason})`}; children ${plan.facts.children.length}; pull request ${plan.facts.pullRequest}`);
|
|
4355
|
+
console.log(` defaults: retain worktree ${plan.defaults.retainWorktree}, delete branch ${plan.defaults.deleteBranch}, stop children ${plan.defaults.stopChildren}`);
|
|
4356
|
+
for (const n of plan.notes) console.log(` note: ${n}`);
|
|
4357
|
+
return;
|
|
4358
|
+
} catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message, e.candidates ? { candidates: e.candidates } : undefined) : die(e.message); }
|
|
4359
|
+
}
|
|
4152
4360
|
// The calling instance knows its own home: self-retire never needs to
|
|
4153
4361
|
// disambiguate a same-named twin by hand.
|
|
4154
4362
|
if (homeFlag === undefined && process.env.OATS_INSTANCE_HOME && (process.env.PI_AGENT_INSTANCE === name || process.env.OATS_INSTANCE === name)) homeFlag = process.env.OATS_INSTANCE_HOME;
|
|
@@ -4163,7 +4371,39 @@ function retireCmd() {
|
|
|
4163
4371
|
if (hit && resolve(hit.root) !== resolve(root)) { root = hit.root; (args.includes("--json") ? console.error : console.log)(`(cross-repo: instance homes at ${shortPath(root)})`); }
|
|
4164
4372
|
}
|
|
4165
4373
|
const retiringHome = homeFlag || findInstanceHome(root, name);
|
|
4166
|
-
|
|
4374
|
+
// K3: a GUI-driven Remove carries the plan revision it showed and an
|
|
4375
|
+
// idempotency key. The revision is revalidated against a fresh plan
|
|
4376
|
+
// (E_PLAN_STALE with that plan attached — re-confirm, never act on the old
|
|
4377
|
+
// one); a retried key replays the recorded receipt instead of retiring twice.
|
|
4378
|
+
const planRev = flag("plan-revision"), idemKey = flag("idempotency-key");
|
|
4379
|
+
if (planRev === true || idemKey === true) die("--plan-revision and --idempotency-key need values");
|
|
4380
|
+
if ((planRev !== undefined) !== (idemKey !== undefined)) die("--plan-revision and --idempotency-key go together");
|
|
4381
|
+
let replayPath = null, childrenStopped = null, expectedBranch;
|
|
4382
|
+
if (planRev !== undefined) {
|
|
4383
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(idemKey)) die("--idempotency-key: 1-128 chars of [A-Za-z0-9._:-]");
|
|
4384
|
+
// Replay first: after a successful retire the home is gone, so the receipt
|
|
4385
|
+
// (beside the instances dir, keyed by the idempotency key) is the answer.
|
|
4386
|
+
const replay = (dir) => { const p = join(dir, `.oats-retire-receipt.${idemKey}.json`); if (!existsSync(p)) return false; try { const prior = JSON.parse(readFileSync(p, "utf8")); if (prior.retired !== name) return false; if (args.includes("--json")) jsonOk({ ...prior, replayed: true }); else console.log(`retire ${name}: replayed receipt for key ${idemKey}`); return true; } catch { return false; } };
|
|
4387
|
+
for (const a of listAgents(root)) if (replay(join(a._dir, "instances"))) return;
|
|
4388
|
+
let fresh;
|
|
4389
|
+
try { fresh = planRetire(dirFlag(), root, name, { home: homeFlag }); } catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message) : die(e.message); }
|
|
4390
|
+
replayPath = join(dirname(fresh.home), `.oats-retire-receipt.${idemKey}.json`);
|
|
4391
|
+
if (fresh.planRevision !== planRev) return args.includes("--json") ? jsonFail("E_PLAN_STALE", `the retire plan changed since it was shown (${planRev} → ${fresh.planRevision}); review the fresh plan`, { plan: fresh }) : die(`the retire plan changed since it was shown; re-run oats retire ${name} --plan`);
|
|
4392
|
+
// The plan promised: recorded children are STOPPED first (bounded, never
|
|
4393
|
+
// escalated) and retained. A child still running after the grace refuses
|
|
4394
|
+
// the retirement — nothing is retired, the receipt names the pid.
|
|
4395
|
+
childrenStopped = [];
|
|
4396
|
+
for (const kid of fresh.facts.children) {
|
|
4397
|
+
try { const s = stopInstanceSession(kid.home, {}); childrenStopped.push({ instance: kid.instance, home: kid.home, ok: true, stopped: s.stopped, alreadyIdle: s.alreadyIdle }); }
|
|
4398
|
+
catch (e) { childrenStopped.push({ instance: kid.instance, home: kid.home, ok: false, code: e.code || "E_SESSION_STOP_FAILED", message: e.message, stillRunning: e.receipt?.stillRunning ?? null }); }
|
|
4399
|
+
}
|
|
4400
|
+
const running = childrenStopped.filter((k) => !k.ok);
|
|
4401
|
+
if (running.length) return args.includes("--json") ? jsonFail("E_CHILDREN_RUNNING", `${running.map((k) => k.instance).join(", ")} ${running.length === 1 ? "is" : "are"} still running after a bounded stop; nothing was retired and nothing was escalated`, { childrenStopped, plan: fresh }) : die(`children still running: ${running.map((k) => k.instance).join(", ")}; nothing retired`);
|
|
4402
|
+
expectedBranch = fresh.facts.work.observed ? fresh.facts.work.branch : undefined;
|
|
4403
|
+
}
|
|
4404
|
+
const r = retireInstance(root, name, { home: homeFlag, self: isSelf, deleteBranch: args.includes("--delete-branch"), discardWorktree: args.includes("--discard-worktree"), keepDir: args.includes("--keep-dir"), force: args.includes("--force"), ...(expectedBranch !== undefined ? { expectedBranch } : {}) });
|
|
4405
|
+
if (childrenStopped) r.childrenStopped = childrenStopped;
|
|
4406
|
+
if (replayPath) { r.planRevision = planRev; r.idempotencyKey = idemKey; r.replayed = false; try { writeFileAtomic(replayPath, JSON.stringify(r, null, 2)); } catch { /* receipt is evidence, not authority */ } }
|
|
4167
4407
|
// A retired home's wake jobs are forgotten (definitions only; nothing is
|
|
4168
4408
|
// stopped by this); a deferred self-retire keeps them until the home is gone.
|
|
4169
4409
|
if (retiringHome && r.removedDir !== false && !r.deferred) { try { const gone = removeWakeForHome(scheduleScopeOf(workspaceOf(root)), retiringHome); if (gone.length) r.wakeSchedulesRemoved = gone; } catch (e) { r.warnings = [...(r.warnings || []), `wake schedules not cleaned: ${e.message}`]; } }
|
|
@@ -4265,6 +4505,7 @@ async function sessionCmd() {
|
|
|
4265
4505
|
return;
|
|
4266
4506
|
}
|
|
4267
4507
|
if (args[1] === "inspect") result = inspectInstanceSession(home);
|
|
4508
|
+
else if (args[1] === "recompose") result = recomposeInstanceInstructions(home, { dryRun: args.includes("--dry-run") });
|
|
4268
4509
|
else if (args[1] === "start" || args[1] === "restart") {
|
|
4269
4510
|
const bad = (msg) => { throw Object.assign(new Error(msg), { code: "E_BAD_ARGS" }); };
|
|
4270
4511
|
const model = flag("model");
|
|
@@ -4418,7 +4659,8 @@ function onboardCmd() {
|
|
|
4418
4659
|
const source = parsePortableSource(`git:${catalogSource.url}@${pkg.commit}#${pkg.path}`);
|
|
4419
4660
|
const requires = { capabilities: Object.fromEntries(SETUP_CAPABILITIES.map(id => [id, { source: `${source.source}#${source.path}` }])) };
|
|
4420
4661
|
const soulFile = join(created.soul, "soul.yaml");
|
|
4421
|
-
writeFileAtomic(soulFile, readFileSync(soulFile, "utf8") + `requires: ${JSON.stringify(requires)}\ndefaults: ${JSON.stringify(edition.declaration.defaults)}\n`
|
|
4662
|
+
writeFileAtomic(soulFile, readFileSync(soulFile, "utf8") + `requires: ${JSON.stringify(requires)}\ndefaults: ${JSON.stringify(edition.declaration.defaults)}\n`
|
|
4663
|
+
+ `provenance: ${JSON.stringify({ kind: edition.source.kind, source: edition.source.source, revision: edition.source.revision, path: edition.source.path, ...(edition.source.workspaceRevision ? { workspaceRevision: edition.source.workspaceRevision } : {}) })}\n`);
|
|
4422
4664
|
const agent = findAgent(root, SETUP_EXPERT), composition = composeInstanceAgentsMd(created.soul, deployment, SETUP_EXPERT, "directory", "local");
|
|
4423
4665
|
planInstanceResources({ resolved: composition.resolved, soulDir: created.soul, agent, contextDir: deployment, composition });
|
|
4424
4666
|
const argv = [process.execPath, CLI_BIN, "spawn", SETUP_EXPERT, "--dir", deployment, "--no-yolo", "--task", "Help me configure this deployment and adopt my workspace with explicit approvals."];
|
|
@@ -4469,11 +4711,17 @@ function createCmd() {
|
|
|
4469
4711
|
work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo,
|
|
4470
4712
|
instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
|
|
4471
4713
|
});
|
|
4472
|
-
|
|
4473
|
-
|
|
4714
|
+
// A declared oats.core is a requirement spawn will enforce: say the next
|
|
4715
|
+
// step here, not only at the refusal.
|
|
4716
|
+
const declared = r.declaredCapabilities || [];
|
|
4717
|
+
const inactive = declared.filter((id) => !(resolveOatsConfig(workspaceOf(root), name).capabilities || []).some((c) => c.id === id));
|
|
4718
|
+
const next = inactive.length ? [{ code: "next-step", message: `${name} declares ${inactive.join(", ")}; before spawning, acquire if needed (oats install oats.framework) and activate: ${inactive.map((id) => `oats use ${id} --soul ${name}`).join(" && ")}` }] : [];
|
|
4719
|
+
const notes = [...(r.notes || []), ...next];
|
|
4720
|
+
if (args.includes("--json")) { console.log(JSON.stringify({ ...r, ...(notes.length ? { notes } : {}), ...(bootstrapped ? { agentsRoot: root } : {}) }, null, 2)); return; }
|
|
4721
|
+
for (const information of notes) console.error(`[${information.code}] ${information.message}`);
|
|
4474
4722
|
if (bootstrapped) console.log(`Created deployment root ${shortPath(root)} (this scope had no agents/ yet)`);
|
|
4475
4723
|
console.log(`Created ${r.kind === "local" ? "LOCAL agent (uncommitted — soul lives in local-agents/, gitignored)" : "agent"} "${r.agent}" — soul at ${shortPath(r.soul)}`);
|
|
4476
|
-
console.log(`Edit ${shortPath(join(r.soul, "AGENTS.md"))} to define its role, then: oats spawn ${r.agent} --task "..."`);
|
|
4724
|
+
console.log(`Edit ${shortPath(join(r.soul, "AGENTS.md"))} to define its role, then${inactive.length ? ` activate ${inactive.join(", ")} (above) and` : ":"} oats spawn ${r.agent} --task "..."`);
|
|
4477
4725
|
}
|
|
4478
4726
|
|
|
4479
4727
|
// ---------- capability command dispatch ----------
|
|
@@ -4732,7 +4980,7 @@ function versionCmd() {
|
|
|
4732
4980
|
// on it (an older CLI without the surface must fail closed with a
|
|
4733
4981
|
// reason, not an argument error). `features`: kernel abilities a peer
|
|
4734
4982
|
// must see before relying on them (retire-home: retire --home).
|
|
4735
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations"], scheduleApi: SCHEDULE_API, operationsApi: 1, capturedDispatchApi: 1, capturedDispatchActions: ["inspect", "compose", "command", "operation", "spawn", "trust"] }));
|
|
4983
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "catalog", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "schedule-history", "session-recompose"], instanceGitApi: 1, soulsApi: 1, lifecycleApi: 1, readinessApi: 1, spawnPreviewApi: 1, eventsApi: 1, scheduleHistoryApi: 2, scheduleApi: SCHEDULE_API, operationsApi: 1, capturedDispatchApi: 1, capturedDispatchActions: ["inspect", "compose", "command", "operation", "spawn", "trust"] }));
|
|
4736
4984
|
return;
|
|
4737
4985
|
}
|
|
4738
4986
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -5174,6 +5422,8 @@ else if (cmd === "config") configCmd();
|
|
|
5174
5422
|
else if (cmd === "trust") trust();
|
|
5175
5423
|
else if (cmd === "list") listCmd();
|
|
5176
5424
|
else if (cmd === "catalog") catalogCmd();
|
|
5425
|
+
else if (cmd === "readiness") readinessCmd();
|
|
5426
|
+
else if (cmd === "instance") instanceCmd();
|
|
5177
5427
|
else if (cmd === "remove") removeCmd();
|
|
5178
5428
|
else if (cmd === "migrate") migrateCmd();
|
|
5179
5429
|
else if (cmd === "root") console.log(resolve(new URL("..", import.meta.url).pathname));
|
|
@@ -5368,6 +5618,40 @@ Usage:
|
|
|
5368
5618
|
scopes, trust state
|
|
5369
5619
|
oats catalog [--json] the effective official package catalog (read-only:
|
|
5370
5620
|
identity/discovery, no acquisition or trust)
|
|
5621
|
+
oats instance git <instance> [--home <abs>] [--dir <d>] [--json]
|
|
5622
|
+
read-only Git observation of the instance's work
|
|
5623
|
+
tree: branch, status (renames kept), ahead/behind
|
|
5624
|
+
vs upstream AND vs default-branch merge-base
|
|
5625
|
+
oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json]
|
|
5626
|
+
bounded diff of one observed file; refuses when
|
|
5627
|
+
the tree moved since the observation
|
|
5628
|
+
oats session recompose --home <abs> [--dry-run] [--json]
|
|
5629
|
+
refresh a LIVE home's AGENTS.md from its current
|
|
5630
|
+
canonical soul + context (same composer as spawn);
|
|
5631
|
+
previous text retained; nothing restarted
|
|
5632
|
+
oats instance events <instance> [--limit <n>] [--since <iso>] [--json]
|
|
5633
|
+
typed lifecycle events (spawned, launched, stopped,
|
|
5634
|
+
restarted, retired, worktree-retained…) written by
|
|
5635
|
+
the action that made them true; nothing inferred
|
|
5636
|
+
oats instance stop <instance> --plan [--no-recursive] [--json]
|
|
5637
|
+
what Stop would touch: session state, recorded
|
|
5638
|
+
children, dirty work; a planRevision to apply
|
|
5639
|
+
oats instance stop <instance> --apply --plan-revision <rev> --idempotency-key <key>
|
|
5640
|
+
quiesce (SIGTERM, bounded, never escalated),
|
|
5641
|
+
children first; home/work/launch retained
|
|
5642
|
+
oats readiness [--soul <n>] [--home <abs>] [--verify-signatures] [--policy] [--json]
|
|
5643
|
+
installed | trusted | configured | enrolled, each
|
|
5644
|
+
pass|fail|unknown|not-applicable with items and
|
|
5645
|
+
remedies; signature status per artifact; enforced
|
|
5646
|
+
child-spawn / worktree policy with origins
|
|
5647
|
+
oats retire <instance> --plan [--json] what Remove would touch, with retention defaults
|
|
5648
|
+
oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree] [--delete-branch]
|
|
5649
|
+
with a plan revision: refuses E_PLAN_STALE (fresh plan
|
|
5650
|
+
attached) if facts moved; a repeated key replays
|
|
5651
|
+
retire; a worktree is RETAINED (re-homed under
|
|
5652
|
+
<workspace>/.agents/worktrees/<repo>/<branch>)
|
|
5653
|
+
unless discarded; --delete-branch deletes the
|
|
5654
|
+
worktree's verified branch and implies discard
|
|
5371
5655
|
oats update <package> [<package>@<ref>] transactional package update: temp fetch,
|
|
5372
5656
|
[--to <ref>] [--dir <d>] closure validation, diff, lock replace,
|
|
5373
5657
|
all capability approvals invalidated; a
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Purpose:** the one accurate view of every work stream in the redesign, what is on main, what is in flight, who owns it, and what blocks it. Lead: `oats-expert` (redesign lead). Updated whenever anything merges, is returned, or reality changes. Older per-lane boards are superseded by this file.
|
|
4
4
|
|
|
5
|
-
**Last update:** 2026-09-22
|
|
5
|
+
**Last update:** 2026-09-22 10:30Z (clock verified with `date -u`; the previous "22:20Z" stamp was a wall-clock error) · **Slice 2b MERGED** (PR65) · **K3 producer pins closed** (PR66 advertisement + guarded Remove; PR67 kernel-owned child stop, confirmed-branch binding, per-key stop replay, ambiguous parentage) · `oats session recompose` seam (in-place instruction refresh) · engineer wiring **2c** on `aed94b74` → 5 → 6b → 7b → 8 · lead: reviews; tag 0.24.8 when 2c lands
|
|
6
6
|
|
|
7
7
|
Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
|
|
8
8
|
|
|
@@ -12,12 +12,12 @@ Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, n
|
|
|
12
12
|
|---|---|---|---|---|
|
|
13
13
|
| S1 | Knowledge capability contract rework (kernel↔provider boundary, OKF 2.x) | ✅ OATS 0.24.1 / OKF 2.1.1 published | P | done for this phase |
|
|
14
14
|
| S2 | Workspace/Portable Souls adoption of the OATS repos | ✅ **FIRST SECOND-OPERATOR PUBLICATION (735296c5, 2026-09-21)**: fresh machine, published definition alone, no `launch` → `status: prepared`, resolution `sha256-f11c433c…`, zero problems. ❌ then OKF `check` → `needs-configuration / provider-not-qualified`, **no message/key/origins**, identical for `harvest-runtime` pi and claude — OKF's check phase is code-only and folds ≥4 causes into one code (P, OKF 2.1.3). ⏳ Pi `launch` blocked by kernel ifInstalled defect (L, 0.24.5). Earlier: ✅ knowledge: bound to public `oats-knowledge` via OKF 2.1.2 from a fresh machine, four kernel versions · ✅ messaging normalizes; `wider` deadlock gone; named reasons in one run · ❌ **operator's amended verdict (9816b8ec): no resolution can publish — behind `responsibleHuman`, helper composition refuses because `oats.core` and `oats.aweb` ship `inject` without `helperInjection`** (OKF adopted the contract; its siblings did not). Both remaining blockers are packaging, not operator input. Fix: [decision](https://github.com/awebai/oats/blob/main/agents/oats-expert/soul/knowledge/decisions/helper-injection-policy-on-every-injecting-capability.md) — core `inherit`, aweb `omit`, framework release check, attributed early refusal · then unchanged re-run | lead, Antares | oats.framework 1.1.3 + aweb 1.11.2 + pins; re-run |
|
|
15
|
-
| S3 | Messaging capability readiness on the new infrastructure (aweb) | ✅ aweb 1.11.0 released · ✅ **catalog + six editions pin v1.11.0 (0.24.2)** ·
|
|
15
|
+
| S3 | Messaging capability readiness on the new infrastructure (aweb) | ✅ aweb 1.11.0 released · ✅ **catalog + six editions pin v1.11.0 (0.24.2)** · ✅ **second-operator `launch` re-run on 0.24.6 (Antares, 2026-09-21)**: resolution `sha256-0cbedec4…` published WITH launch selection {pi, model}; helper `oats.okf:memory-harvest` resolved alongside; hard runtime row (`delivery: channel`) refused attributed with capability/slot/runtime/package/install/origin — no bare `needs-configuration`. `check` half parked with OKF 2.1.3 by direction | P, lead | Antares re-run |
|
|
16
16
|
| S4 | Official capabilities `oats.core` / `oats.setup` + explicit default + onboarding `oats-setup-expert` | ✅ D1, D2, **D3 merged (PR35)**: `oats onboard` verified live (acquire 1.1.1 → setup expert with both caps → scaffold composes the five capability skills, no legacy) · `oats.framework` 1.1.1 tagged | P, L | done; Desktop surfaces → S8 |
|
|
17
17
|
| S5 | Official marketplace = reviewed list in oats repo | ✅ D4 merged · ✅ `oats.framework` 1.1.1 listed (`oats.core`, `oats.setup`, `oats.knowledge-theory` aliases) | M | Desktop view → S8 |
|
|
18
18
|
| S6 | Five expert souls created in the oats repo (`souls/<name>/`) | ✅ five + `oats-setup-expert` on main, all declaring `oats.core`, exported + imported | M, L, lead | legacy `agents/` cutover after S7 proof |
|
|
19
19
|
| S7 | Centralised per-soul knowledge in `oats-knowledge` (migration + PR-only learning) | ✅ **MIGRATED — PR #2 merged → main `7148a36`, 58 concepts / 35k words from 399 legacy** (kernel 26, expert 18, desktop 13, assistant 1, market-research chartered empty); option A roster; theory + no-code-teaching rule; two workflow passes (24 agents) + lead review; validator 58/0/0; ownership 14/14 · ⏸ harvest OFF until new souls run from the base (human) | lead | next: legacy `agents/` decommission with the `~/OATS` cutover |
|
|
20
|
-
| S8 | Desktop parity with the redesign (Redesign v3, aweb palette, discovery-first control panel) | 🔄 **STARTED 2026-09-22**: existing `oats-desktop-engineer-1` retrofitted with an aweb identity (spawn hook replayed, `capabilityMeta` persisted, session reloaded); redesign artefacts copied into its home; brief sent (f16eed3f): merge main (0.24.5) first, then PR slices — shell/palette/sidebar → right panel follows selection → Souls view → **Capabilities view incl. official catalog (human's required feature; server-side via kernel catalog API, never auto-acquire/auto-trust)** → Knowledge/Tasks adapters → remainder. Lead reviews/merges each slice. **Decisions 2026-09-22:** human's later palette/row instructions supersede the HTML where they conflict; grouping is agent-group (cross-repo clusters), not repo; kernel seam `oats catalog --json` merged (PR46 `524180b7`, ships 0.24.6); the Juan-host fresh-engineer plan is superseded — this instance owns S8. | oats-desktop-engineer-1, lead | per-slice PRs | ✅ **Slice 1a MERGED `508c4b5f`** (PR45; 273/273 real jsdom; gates green) → v0.24.6. **Plan + seams recorded: [`docs/design/2026-09-22-desktop-parity-seams.md`](2026-09-22-desktop-parity-seams.md)** — slices 1a–8, seams K1–K8/P1, five policy decisions **DECIDED 2026-09-22 under delegation** — see `decisions/desktop-parity-lifecycle-and-policy-decisions.md`.
|
|
20
|
+
| S8 | Desktop parity with the redesign (Redesign v3, aweb palette, discovery-first control panel) | 🔄 **STARTED 2026-09-22**: existing `oats-desktop-engineer-1` retrofitted with an aweb identity (spawn hook replayed, `capabilityMeta` persisted, session reloaded); redesign artefacts copied into its home; brief sent (f16eed3f): merge main (0.24.5) first, then PR slices — shell/palette/sidebar → right panel follows selection → Souls view → **Capabilities view incl. official catalog (human's required feature; server-side via kernel catalog API, never auto-acquire/auto-trust)** → Knowledge/Tasks adapters → remainder. Lead reviews/merges each slice. **Decisions 2026-09-22:** human's later palette/row instructions supersede the HTML where they conflict; grouping is agent-group (cross-repo clusters), not repo; kernel seam `oats catalog --json` merged (PR46 `524180b7`, ships 0.24.6); the Juan-host fresh-engineer plan is superseded — this instance owns S8. | oats-desktop-engineer-1, lead | per-slice PRs | ✅ **Slice 2b MERGED** (PR65; Connections + PR card; 1654/1654) · ✅ **K5+K6 MERGED** (PR63; readiness quartet/signatures/enforced child policy; spawn --preview/--base/@native-default) · ✅ **K7+K8 MERGED** (PR64; typed instance events; schedule recentRuns) · ✅ **K3 MERGED** (PR62; `instance stop --plan|--apply`, `retire --plan`, retention-by-default retire with re-home) · ✅ baseline flakes fixed (PR61) · ✅ **P1 decided + K1 `remote` MERGED** (PR60) · ✅ **Components (frame 10, existing data) MERGED** (PR59; 1606/1606) · ✅ **Slice 2a MERGED** (PR57; Git panel on K1; legacy gitState collector removed) · ✅ **v0.24.7 tagged/published** (tag → `c923d81b`) · ✅ **Slice 3 MERGED** (PR54; Souls + Sources on K4 soulsApi 1; 1503/1503) · ✅ **K1 MERGED** (PR53; `oats instance git|diff`, instanceGitApi 1; DTO in docs/desktop-cli-api.md) · ✅ **K4 MERGED** (PR52; `inspect --json` soulsApi 1: declarations/provenance/readiness/sources; onboard records provenance; DTO in docs/desktop-cli-api.md) · ✅ **Slice 7a MERGED** (PR51; frame 07 Active overview on /api/panel; anonymous groups; 1466/1466) · ✅ **Slice 6a MERGED** (PR50; Spawn modal frame 02 on existing seams; launchConfig restored; 1435/1435) · ✅ **Slice 4 MERGED** (PR49; Capabilities view: official catalog via `oats catalog` + classic inventory; 1386/1386; server boundary reviewed) · ✅ **Slice 1b MERGED** (PR48; contextual panel + focus mode; 1238/1238) · ✅ **Slice 1a MERGED `508c4b5f`** (PR45; 273/273 real jsdom; gates green) → v0.24.6. **Plan + seams recorded: [`docs/design/2026-09-22-desktop-parity-seams.md`](2026-09-22-desktop-parity-seams.md)** — slices 1a–8, seams K1–K8/P1, five policy decisions **DECIDED 2026-09-22 under delegation** — see `decisions/desktop-parity-lifecycle-and-policy-decisions.md`.
|
|
21
21
|
|
|
22
22
|
## S1 — Knowledge capability contract rework
|
|
23
23
|
- ✅ Provider-neutral contract, binding wire v1, helper/input contract, retained execution: OATS 0.24.0 + OKF 2.1.0 (f20f8e57) published.
|
|
@@ -14,7 +14,8 @@ Status: **proposal accepted for direction** by the lead on 2026-09-22; contracts
|
|
|
14
14
|
| 3 | 03 Souls + Sources: imported editions + local souls, requirements, provenance, editability | **K4** |
|
|
15
15
|
| 4 | 04 Capabilities: official catalog (`oats catalog --json`, 0.24.6+) + deployment inventory/readiness/used-by; Add capability = exact command | landed catalog + list/inspect + **K5** |
|
|
16
16
|
| 5 | 09 First-run readiness quartet; View policy; Skip/Enrol | **K5** + enrollment decision |
|
|
17
|
-
|
|
|
17
|
+
| 6a | 02 Spawn modal **design parity on existing seams** (human pulled forward 2026-09-22): two-column layout, soul chooser, provider/model dropdowns, launch config restored, opening instruction, readiness from known facts (`unknown` where not), ⌘↵ guards; not-yet-backed fields rendered disabled with "available after <seam>" | existing spawn/launch-config seams |
|
|
18
|
+
| 6b | 02 Spawn fields live: soul chooser, provider/model, launch config (restored), work-area naming/worktree/base+branch, opening instruction, attach knowledge, child spawns, auto-PR, readiness, ⌘↵ | **K6** (+ knowledge-node JSON from the knowledge provider; 05 excluded but attach stays) |
|
|
18
19
|
| 7a | 07 Active overview: counts, relations, activity/waiting-on-you, actions, pan/zoom | **K7** |
|
|
19
20
|
| 7b | 08 Schedules: table/toggles/new/edit, next/last, recent runs, transcript handoff, captured-policy preservation | **K8** |
|
|
20
21
|
| 8 | 10 Components: dropdowns, workspace join/manage, Open in split, Detach to window, Open worktree in editor, actions, toasts, collapsed rail | existing seams + Desktop IPC review for detach/editor |
|
|
@@ -23,16 +24,19 @@ Status: **proposal accepted for direction** by the lead on 2026-09-22; contracts
|
|
|
23
24
|
|
|
24
25
|
Envelope `{schemaVersion:1, ok, result|error}` unchanged. Requests address a server-admitted exact target (`{home, server}` / exact source+revision+soul), never a renderer cwd. Results echo target + `contract`, `version`, `observedAt`, opaque `revision`, typed `problems[]`, explicit completeness/truncation. `null` = not known; empty = observed empty only when complete. Per-section availability `available | not-applicable | unavailable | denied | unsupported | error`. No stack traces, auth stderr, tokens or token-bearing URLs in renderer data. Remote paths are provenance, never local authority. Read-only calls never install, trust, enroll, spawn, fetch into the operator's worktree, switch branches or repair config.
|
|
25
26
|
|
|
27
|
+
|
|
28
|
+
> **Ownership amendment (2026-09-22, human):** contract-dependent slices are not waits — the team implements the contracts. Kernel seams may be assigned to the Desktop engineer under lead review (the `lib/`/`bin/` lane rule is lifted per assigned seam). Current split (revised 17:10Z): **K1, P1 Decision, K3, K5 → lead** — the engineer's composed instructions forbid kernel edits and a mail cannot recompose them (role change = soul edit + recomposition; spawns blocked by the deployment's pi-profile pin). Engineer: slice 3 now (K4 merged), then 2a after K1. Kernel PRs: full root `npm test` + DTO in `docs/desktop-cli-api.md` in the same PR.
|
|
29
|
+
|
|
26
30
|
## Seams
|
|
27
31
|
|
|
28
32
|
- **K1 `oats.instance-git` / `oats.instance-diff`** (kernel): typed per-instance Git state — worktree, head, upstream/merge-base comparison (missing upstream ≠ 0/0), NUL-delimited changes with rename paths and per-file counts; bounded unified diff by opaque file id + observation revision (stale selection refuses, never a different file). Replaces the Desktop-only `instance.git` aggregate whose fallback zeros can masquerade as clean.
|
|
29
33
|
- **P1 `oats.instance-github`** (provider, not kernel): PR summary/checks/reviews through an additive Git/review **capability** with its own credential policy (native custody; Desktop never runs `gh`, reads tokens or opens credential forms). Needs a kernel dispatch contract for additive-capability structured views (today `operation run` accepts only knowledge/messaging/tasks). "No PR" ≠ unavailable ≠ unauthenticated ≠ rate-limited. Checks bind to exact head OID. Review markdown is untrusted text.
|
|
30
34
|
- **K2 review-thread delivery**: explicit, confirmed send of selected threads to the exact home's session input with receipt (`delivered | refused | unknown`); delivered ≠ consumed. Typed producer events for commit / branch-renamed / PR-updated / review-request; **no prose parsing** to infer actions.
|
|
31
35
|
- **K3 lifecycle plan/apply**: read-only plan (runtime activity, children, worktree dirt, branch + open PRs, retention per artefact, per-option allowed/default/reason, warnings, blockers) → apply with plan revision + idempotency key, revalidated under the lifecycle lock; per-target `completed | retained | partial | unknown`. Today there is **no standalone stop**, and retire removes owned worktrees; the design's default Remove retains worktree/branch/PR.
|
|
32
|
-
- **K4 souls/sources enumeration
|
|
33
|
-
- **K5 readiness quartet
|
|
34
|
-
- **K6 spawn preview/apply
|
|
35
|
-
- **K7 activity feed
|
|
36
|
+
- **K4 souls/sources enumeration** (✅ MERGED PR52 as additive `inspect --json` `soulsApi:1`, not a new command — see docs/desktop-cli-api.md): qualified list of imported editions + authored local souls with identity/source/revision/requirements/declarations/editability; readiness separate from launchability and adoption; no renderer YAML.
|
|
37
|
+
- **K5 readiness quartet** (✅ MERGED PR63 — `oats readiness`): `installed | trusted | configured | enrolled`, each `pass | fail | unknown | not-applicable` with items (subject, requiredness, reason, producer, evidence, remedy); trust separates artifact approval from `signature {verified|unsigned|unknown|invalid, signer}`; policy view returns **enforced** child-spawn/worktree permissions with origins; native config items report labels/scope state, never secrets; unknown ≠ granted.
|
|
38
|
+
- **K6 spawn preview/apply** (✅ MERGED PR63 — `oats spawn --preview`, `--base`, `--model @native-default`) (add, 2026-09-22 from 6a review: an explicit `model: {kind: "native-default"}` request field — today an omitted model inherits the configured/soul model and there is no force-native override; 6a renders that control disabled until K6): kernel returns suggestions, canonical worktree/home/branch/base OID, resolved knowledge refs, enforced child policy, auto-PR policy, readiness, typed field problems; apply revalidates and captures; no Desktop-derived paths or branches.
|
|
39
|
+
- **K7 activity feed** (✅ MERGED PR64 — `oats instance events`): bounded typed events per instance with provenance; "waiting on you" only from a producer that reports it.
|
|
36
40
|
- **K8 schedule run history** + captured-policy-preserving edit contract; transcript access via the owning CLI/provider.
|
|
37
41
|
|
|
38
42
|
## Decisions — DECIDED 2026-09-22 (lead, delegated by the human)
|
|
@@ -51,4 +55,4 @@ Also to confirm: prototype "Gemini" runtime is illustrative (not an OATS runtime
|
|
|
51
55
|
|
|
52
56
|
## Ownership
|
|
53
57
|
|
|
54
|
-
Lead: K1, K4, K5 (readiness/policy shape), K6 preview/apply plumbing, K7/K8 projections, dispatch contract for additive-capability views — proposed as Decisions where they change contracts, implemented in small PRs otherwise. P1:
|
|
58
|
+
Lead: K1, K4, K5 (readiness/policy shape), K6 preview/apply plumbing, K7/K8 projections, dispatch contract for additive-capability views — proposed as Decisions where they change contracts, implemented in small PRs otherwise. P1: **forge connections are ADE/workstation integrations, not capabilities** — **Decision ACCEPTED** (`agents/oats-expert/soul/knowledge/decisions/p1-forge-connection-is-an-ade-integration.md`). Desktop *Connections* surface (GitHub card: status / Connect = `gh auth login --web` in an owned pane / Disconnect), PR card read by the Desktop server via fixed-argv `gh pr view … --json` at the existing guarded boundary, typed states (available / no-pull-request / not-connected / cli-not-installed / unsupported-forge / no-remote / unavailable), remote workspaces refuse; OATS never holds a token. No kernel dispatch contract. Kernel: K1 gains `remote {name,url,host,path}` (lead). Slice **2b** = Connections + PR card (engineer; lead security gate before wiring). Auto-PR: ADE-owned, off, per spawn, after K6. Desktop engineer: all slices, Desktop IPC review items (detach, open-in-editor).
|