@awebai/oats 0.24.7 → 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 +186 -14
- package/docs/design/2026-09-20-redesign-program-board.md +2 -2
- package/docs/design/2026-09-22-desktop-parity-seams.md +4 -4
- package/docs/desktop-cli-api.md +306 -3
- package/docs/release-notes/v0.24.7.md +2 -2
- package/docs/release-notes/v0.24.8.md +107 -0
- package/lib/core.mjs +205 -14
- package/lib/instance-events.mjs +76 -0
- package/lib/instance-git.mjs +21 -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, findInstanceHomes, 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";
|
|
@@ -59,12 +59,16 @@ import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../l
|
|
|
59
59
|
import { approveCapturedCapability } from "../lib/artifact-approvals.mjs";
|
|
60
60
|
import { loadSetupExpertEdition, SETUP_EXPERT, SETUP_CAPABILITIES } from "../lib/setup-expert-source.mjs";
|
|
61
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";
|
|
62
66
|
import { parsePortableSource } from "../lib/source-spec.mjs";
|
|
63
67
|
|
|
64
68
|
const args = process.argv.slice(2);
|
|
65
69
|
let cmd = args[0];
|
|
66
70
|
const HELP_WORDS = new Set(["help", "--help", "-h"]);
|
|
67
|
-
const KERNEL_COMMANDS = new Set(["prepare", "capture", "config", "create", "doctor", "inspect", "instance", "operation", "soul", "launch-config", "experimental", "onboard", "init", "inject", "install", "list", "catalog", "migrate", "pane", "recall", "remove", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "trust", "type", "update", "use", "version"]);
|
|
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"]);
|
|
68
72
|
const flag = (name) => {
|
|
69
73
|
const i = args.indexOf(`--${name}`);
|
|
70
74
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -822,7 +826,7 @@ function homeContexts(home, meta) {
|
|
|
822
826
|
* about the soul, not a prompt to infer one. */
|
|
823
827
|
function soulDeclarations(soulDir) {
|
|
824
828
|
const file = join(soulDir, "soul.yaml");
|
|
825
|
-
const empty = { declarations: { requires: null, defaults: null, knowledge: null, teams: null, resources: null }, provenance: null, problems: [] };
|
|
829
|
+
const empty = { declarations: { requires: null, defaults: null, knowledge: null, teams: null, resources: null, children: null }, provenance: null, problems: [] };
|
|
826
830
|
if (!existsSync(file)) return empty;
|
|
827
831
|
let parsed;
|
|
828
832
|
try { parsed = withConfigFile(file, () => parseYamlNested(readFileSync(file, "utf8"))); }
|
|
@@ -830,7 +834,7 @@ function soulDeclarations(soulDir) {
|
|
|
830
834
|
const section = (key) => (parsed[key] !== undefined && parsed[key] !== null && typeof parsed[key] === "object") ? parsed[key] : (parsed[key] === undefined ? null : parsed[key]);
|
|
831
835
|
const provenance = section("provenance");
|
|
832
836
|
return {
|
|
833
|
-
declarations: { requires: section("requires"), defaults: section("defaults"), knowledge: section("knowledge"), teams: section("teams"), resources: section("resources") },
|
|
837
|
+
declarations: { requires: section("requires"), defaults: section("defaults"), knowledge: section("knowledge"), teams: section("teams"), resources: section("resources"), children: section("children") },
|
|
834
838
|
provenance: provenance && typeof provenance === "object" ? {
|
|
835
839
|
kind: provenance.kind ?? null, source: provenance.source ?? null, revision: provenance.revision ?? null,
|
|
836
840
|
path: provenance.path ?? null, workspaceRevision: provenance.workspaceRevision ?? null,
|
|
@@ -860,8 +864,12 @@ function soulEntry(soul, root, { capability } = {}) {
|
|
|
860
864
|
* invoking process's ambient agents-root override must not redirect them
|
|
861
865
|
* to its own deployment. */
|
|
862
866
|
function dropAmbientRoot() { delete process.env.PI_AGENTS_ROOT; }
|
|
863
|
-
function inspectCmd() {
|
|
864
|
-
|
|
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)));
|
|
865
873
|
dropAmbientRoot();
|
|
866
874
|
const homeFlag = flag("home");
|
|
867
875
|
const home = homeFlag === true ? bail("E_BAD_ARGS", "--home needs an absolute instance home") : homeFlag;
|
|
@@ -936,7 +944,7 @@ function inspectCmd() {
|
|
|
936
944
|
const h = capabilityHealth(p.level, c, rows.capabilities[c.id], rows.packages[p.package]);
|
|
937
945
|
byId.set(c.id, {
|
|
938
946
|
id: c.id, package: p.package, version: c.version || null, layer: c.manifest?.layer || null, command: c.manifest?.command || null,
|
|
939
|
-
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,
|
|
940
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 },
|
|
941
949
|
});
|
|
942
950
|
}
|
|
@@ -1051,7 +1059,12 @@ function inspectCmd() {
|
|
|
1051
1059
|
problems: [...(lockError ? [lockError] : []), ...packagedDiagnostics.map((d) => ({ code: d.code, message: d.message, capability: d.capability })),
|
|
1052
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 })) : [])],
|
|
1053
1061
|
};
|
|
1054
|
-
|
|
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;
|
|
1055
1068
|
console.log(`oats inspect — ${shortPath(ctx)}${selectedSoul ? ` soul ${selectedSoul.name}` : ""}${home ? ` home ${shortPath(home)}` : ""}`);
|
|
1056
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)"}`);
|
|
1057
1070
|
for (const l of LAYERS) console.log(` layer ${l}: ${layers[l].id || (layers[l].disabled ? "disabled" : "none")}${layers[l].provenance ? ` (${layers[l].provenance})` : ""}`);
|
|
@@ -3116,9 +3129,55 @@ function renderMergeRegion(r) {
|
|
|
3116
3129
|
function instanceCmd() {
|
|
3117
3130
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
3118
3131
|
const sub = args[1], name = args[2];
|
|
3119
|
-
const usage = "usage: oats instance git <instance> [--home <abs>] [--dir <d>] [--json] | oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json]";
|
|
3120
|
-
if (!["git", "diff"].includes(sub) || !name || name.startsWith("--")) return bail("E_BAD_ARGS", usage);
|
|
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);
|
|
3121
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
|
+
}
|
|
3122
3181
|
let home = flag("home");
|
|
3123
3182
|
if (home === true) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
3124
3183
|
if (home !== undefined && !isAbsolute(home)) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
@@ -3156,6 +3215,33 @@ function instanceCmd() {
|
|
|
3156
3215
|
bail(e.code || "E_GIT_FAILED", e.message, e.observation ? { observation: e.observation } : undefined);
|
|
3157
3216
|
}
|
|
3158
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
|
+
}
|
|
3159
3245
|
function catalogCmd() {
|
|
3160
3246
|
const described = describeOfficialCatalog();
|
|
3161
3247
|
if (JSON_MODE) { jsonOk(described); return; }
|
|
@@ -4066,7 +4152,7 @@ function spawnCmd() {
|
|
|
4066
4152
|
};
|
|
4067
4153
|
checkDirectoryOptions(requestedWork); // before a local soul could be upserted
|
|
4068
4154
|
const name = args[1];
|
|
4069
|
-
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]");
|
|
4070
4156
|
// Retired boundary flags (maintainer transport ruling): fail LOUDLY before
|
|
4071
4157
|
// ANY side effect — including root discovery and local-agent upsert (an
|
|
4072
4158
|
// --instructions-file spawn must not scaffold/overwrite a local soul before
|
|
@@ -4184,8 +4270,11 @@ function spawnCmd() {
|
|
|
4184
4270
|
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message); throw e; }
|
|
4185
4271
|
let r;
|
|
4186
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");
|
|
4187
4275
|
r = spawnInstance(root, agent, {
|
|
4188
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 } : {}),
|
|
4189
4278
|
// Directory execution uses deployment configuration, not an ambient Git
|
|
4190
4279
|
// checkout (especially when invoked via --dir from a source instance).
|
|
4191
4280
|
repo: (requestedWork || agent.work) === "directory"
|
|
@@ -4193,7 +4282,13 @@ function spawnCmd() {
|
|
|
4193
4282
|
work: requestedWork, workDir, runtime: flag("runtime"), backend, herdrSocket, yolo, model: flag("model"), branch,
|
|
4194
4283
|
launchConfig: valueFlag("launch-config"),
|
|
4195
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") } : {}),
|
|
4196
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; }
|
|
4197
4292
|
} catch (e) {
|
|
4198
4293
|
// A typed CLI failure keeps ITS OWN code: re-badging an unsafe-config-key
|
|
4199
4294
|
// (raised by the readers spawn walks) as E_SPAWN_FAILED tells an agent
|
|
@@ -4207,6 +4302,8 @@ function spawnCmd() {
|
|
|
4207
4302
|
// An unmet declared requirement is a fact about the soul's configuration
|
|
4208
4303
|
// (with a remedy), not a spawn-mechanism failure: keep its code and details.
|
|
4209
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; }
|
|
4210
4307
|
bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
|
|
4211
4308
|
}
|
|
4212
4309
|
// The instance exists from here on: a failed wake save is reported beside
|
|
@@ -4243,9 +4340,23 @@ function spawnCmd() {
|
|
|
4243
4340
|
|
|
4244
4341
|
function retireCmd() {
|
|
4245
4342
|
const name = args[1];
|
|
4246
|
-
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]");
|
|
4247
4344
|
let homeFlag = flag("home");
|
|
4248
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
|
+
}
|
|
4249
4360
|
// The calling instance knows its own home: self-retire never needs to
|
|
4250
4361
|
// disambiguate a same-named twin by hand.
|
|
4251
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;
|
|
@@ -4260,7 +4371,39 @@ function retireCmd() {
|
|
|
4260
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)})`); }
|
|
4261
4372
|
}
|
|
4262
4373
|
const retiringHome = homeFlag || findInstanceHome(root, name);
|
|
4263
|
-
|
|
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 */ } }
|
|
4264
4407
|
// A retired home's wake jobs are forgotten (definitions only; nothing is
|
|
4265
4408
|
// stopped by this); a deferred self-retire keeps them until the home is gone.
|
|
4266
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}`]; } }
|
|
@@ -4362,6 +4505,7 @@ async function sessionCmd() {
|
|
|
4362
4505
|
return;
|
|
4363
4506
|
}
|
|
4364
4507
|
if (args[1] === "inspect") result = inspectInstanceSession(home);
|
|
4508
|
+
else if (args[1] === "recompose") result = recomposeInstanceInstructions(home, { dryRun: args.includes("--dry-run") });
|
|
4365
4509
|
else if (args[1] === "start" || args[1] === "restart") {
|
|
4366
4510
|
const bad = (msg) => { throw Object.assign(new Error(msg), { code: "E_BAD_ARGS" }); };
|
|
4367
4511
|
const model = flag("model");
|
|
@@ -4836,7 +4980,7 @@ function versionCmd() {
|
|
|
4836
4980
|
// on it (an older CLI without the surface must fail closed with a
|
|
4837
4981
|
// reason, not an argument error). `features`: kernel abilities a peer
|
|
4838
4982
|
// must see before relying on them (retire-home: retire --home).
|
|
4839
|
-
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"] }));
|
|
4840
4984
|
return;
|
|
4841
4985
|
}
|
|
4842
4986
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -5278,6 +5422,7 @@ else if (cmd === "config") configCmd();
|
|
|
5278
5422
|
else if (cmd === "trust") trust();
|
|
5279
5423
|
else if (cmd === "list") listCmd();
|
|
5280
5424
|
else if (cmd === "catalog") catalogCmd();
|
|
5425
|
+
else if (cmd === "readiness") readinessCmd();
|
|
5281
5426
|
else if (cmd === "instance") instanceCmd();
|
|
5282
5427
|
else if (cmd === "remove") removeCmd();
|
|
5283
5428
|
else if (cmd === "migrate") migrateCmd();
|
|
@@ -5480,6 +5625,33 @@ Usage:
|
|
|
5480
5625
|
oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json]
|
|
5481
5626
|
bounded diff of one observed file; refuses when
|
|
5482
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
|
|
5483
5655
|
oats update <package> [<package>@<ref>] transactional package update: temp fetch,
|
|
5484
5656
|
[--to <ref>] [--dir <d>] closure validation, diff, lock replace,
|
|
5485
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-
|
|
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
|
|
|
@@ -17,7 +17,7 @@ Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, n
|
|
|
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 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`.
|
|
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.
|
|
@@ -34,9 +34,9 @@ Envelope `{schemaVersion:1, ok, result|error}` unchanged. Requests address a ser
|
|
|
34
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.
|
|
35
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.
|
|
36
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
|
|
38
|
-
- **K6 spawn preview/apply** (add, 2026-09-22 from 6a review: an explicit `model: {kind: "native-default"}` request field — today an omitted model inherits the configured/soul model and there is no force-native override; 6a renders that control disabled until K6): kernel returns suggestions, canonical worktree/home/branch/base OID, resolved knowledge refs, enforced child policy, auto-PR policy, readiness, typed field problems; apply revalidates and captures; no Desktop-derived paths or branches.
|
|
39
|
-
- **K7 activity feed
|
|
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.
|
|
40
40
|
- **K8 schedule run history** + captured-policy-preserving edit contract; transcript access via the owning CLI/provider.
|
|
41
41
|
|
|
42
42
|
## Decisions — DECIDED 2026-09-22 (lead, delegated by the human)
|
|
@@ -55,4 +55,4 @@ Also to confirm: prototype "Gemini" runtime is illustrative (not an OATS runtime
|
|
|
55
55
|
|
|
56
56
|
## Ownership
|
|
57
57
|
|
|
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:
|
|
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).
|