@awebai/oats 0.24.6 → 0.24.7
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 +120 -8
- package/docs/design/2026-09-20-redesign-program-board.md +3 -3
- package/docs/design/2026-09-22-desktop-parity-seams.md +7 -3
- package/docs/desktop-cli-api.md +103 -0
- package/docs/release-notes/v0.24.7.md +81 -0
- package/lib/core.mjs +19 -3
- package/lib/instance-git.mjs +204 -0
- 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,
|
|
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,13 @@ 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";
|
|
61
62
|
import { parsePortableSource } from "../lib/source-spec.mjs";
|
|
62
63
|
|
|
63
64
|
const args = process.argv.slice(2);
|
|
64
65
|
let cmd = args[0];
|
|
65
66
|
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"]);
|
|
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"]);
|
|
67
68
|
const flag = (name) => {
|
|
68
69
|
const i = args.indexOf(`--${name}`);
|
|
69
70
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -815,11 +816,36 @@ function homeContexts(home, meta) {
|
|
|
815
816
|
out.push(dirname(agentsRootOfHome(realOrResolved(home))));
|
|
816
817
|
return out;
|
|
817
818
|
}
|
|
819
|
+
/** A soul's own declared requirements/defaults/provenance, read from its
|
|
820
|
+
* soul.yaml through the kernel's parser (never guessed, never re-parsed by a
|
|
821
|
+
* consumer). Absent sections are null: an unrecorded provenance is a fact
|
|
822
|
+
* about the soul, not a prompt to infer one. */
|
|
823
|
+
function soulDeclarations(soulDir) {
|
|
824
|
+
const file = join(soulDir, "soul.yaml");
|
|
825
|
+
const empty = { declarations: { requires: null, defaults: null, knowledge: null, teams: null, resources: null }, provenance: null, problems: [] };
|
|
826
|
+
if (!existsSync(file)) return empty;
|
|
827
|
+
let parsed;
|
|
828
|
+
try { parsed = withConfigFile(file, () => parseYamlNested(readFileSync(file, "utf8"))); }
|
|
829
|
+
catch (e) { return { ...empty, problems: [{ code: "soul-declarations-unreadable", message: e.message }] }; }
|
|
830
|
+
const section = (key) => (parsed[key] !== undefined && parsed[key] !== null && typeof parsed[key] === "object") ? parsed[key] : (parsed[key] === undefined ? null : parsed[key]);
|
|
831
|
+
const provenance = section("provenance");
|
|
832
|
+
return {
|
|
833
|
+
declarations: { requires: section("requires"), defaults: section("defaults"), knowledge: section("knowledge"), teams: section("teams"), resources: section("resources") },
|
|
834
|
+
provenance: provenance && typeof provenance === "object" ? {
|
|
835
|
+
kind: provenance.kind ?? null, source: provenance.source ?? null, revision: provenance.revision ?? null,
|
|
836
|
+
path: provenance.path ?? null, workspaceRevision: provenance.workspaceRevision ?? null,
|
|
837
|
+
} : null,
|
|
838
|
+
problems: [],
|
|
839
|
+
};
|
|
840
|
+
}
|
|
818
841
|
function soulEntry(soul, root, { capability } = {}) {
|
|
819
842
|
const dir = soul._dir || soul.soulDir;
|
|
820
843
|
const soulDir = capability ? soul.soulDir : join(dir, "soul");
|
|
821
844
|
const packaged = !!capability;
|
|
845
|
+
const declared = soulDeclarations(soulDir);
|
|
822
846
|
return {
|
|
847
|
+
soulsApi: 1, declarations: declared.declarations, provenance: declared.provenance,
|
|
848
|
+
declarationProblems: declared.problems,
|
|
823
849
|
name: soul.name, kind: packaged ? "capability" : (soul.kind || "persistent"), capability: capability || null,
|
|
824
850
|
type: soul.type ?? null, description: soul.description ?? null, repo: soul.repo ?? null, work: soul.work || "checkout",
|
|
825
851
|
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,
|
|
@@ -996,11 +1022,32 @@ function inspectCmd() {
|
|
|
996
1022
|
instructions: { ...readTextCapped(join(home, "AGENTS.md")), sources: meta.instructions || [] }, drift,
|
|
997
1023
|
};
|
|
998
1024
|
}
|
|
1025
|
+
// K4: the deployment's portable source context and each soul's declared
|
|
1026
|
+
// requirements joined against the capability inventory in this same payload.
|
|
1027
|
+
// Readiness is about the soul's declared sources, kept apart from
|
|
1028
|
+
// launchability (spawn) and adoption (prepare); unobservable = null.
|
|
1029
|
+
const capabilityById = new Map(capabilities.map((c) => [c.id, c]));
|
|
1030
|
+
for (const s of souls) {
|
|
1031
|
+
const required = s.declarations?.requires?.capabilities;
|
|
1032
|
+
const requirements = required && typeof required === "object" ? Object.entries(required).map(([id, spec]) => {
|
|
1033
|
+
const cap = capabilityById.get(id) || null;
|
|
1034
|
+
return { capability: id, source: spec && typeof spec === "object" ? spec.source ?? null : null,
|
|
1035
|
+
installed: cap ? cap.health?.installed ?? null : false, approved: cap ? cap.health?.trusted ?? null : null,
|
|
1036
|
+
active: cap ? !!cap.activation?.enabled : null, version: cap?.version ?? null };
|
|
1037
|
+
}) : null;
|
|
1038
|
+
s.readiness = { source: s.provenance ? "recorded" : "unrecorded", requirements,
|
|
1039
|
+
status: requirements === null ? "undeclared" : requirements.every((q) => q.installed === true) ? "sources-installed" : requirements.some((q) => q.installed === false) ? "sources-missing" : "unknown" };
|
|
1040
|
+
}
|
|
1041
|
+
const sourceKey = (p) => JSON.stringify([p.source, p.revision, p.path]);
|
|
1042
|
+
const sourceItems = [...new Map(souls.filter((s) => s.provenance?.source).map((s) => [sourceKey(s.provenance), { ...s.provenance, souls: [] }])).values()];
|
|
1043
|
+
for (const s of souls) if (s.provenance?.source) sourceItems.find((i) => sourceKey(i) === sourceKey(s.provenance)).souls.push(s.name);
|
|
1044
|
+
const sources = { soulsApi: 1, kind: sourceItems.length ? "recorded-provenance" : "none-recorded", items: sourceItems,
|
|
1045
|
+
note: sourceItems.length ? null : "no soul in this scope records a portable source address" };
|
|
999
1046
|
const result = {
|
|
1000
1047
|
operationsApi: 1, kernel: OATS_VERSION,
|
|
1001
1048
|
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
1049
|
selected: { soul: selectedSoul?.name || null, agentsRoot: selectedSoul?.agentsRoot || null, home: home || null, source: meta ? "snapshot" : "config" },
|
|
1003
|
-
souls, layers, capabilities, knowledge, snapshot, currentConfig,
|
|
1050
|
+
souls, sources, layers, capabilities, knowledge, snapshot, currentConfig,
|
|
1004
1051
|
problems: [...(lockError ? [lockError] : []), ...packagedDiagnostics.map((d) => ({ code: d.code, message: d.message, capability: d.capability })),
|
|
1005
1052
|
...(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
1053
|
};
|
|
@@ -3062,6 +3109,53 @@ function renderMergeRegion(r) {
|
|
|
3062
3109
|
/** `oats catalog [--json]` — the effective official package catalog, read-only.
|
|
3063
3110
|
* Identity/discovery for consumers that cannot import the kernel (Desktop):
|
|
3064
3111
|
* never acquires, never trusts, never fetches. */
|
|
3112
|
+
/** `oats instance <git|diff> <instance>` — K1: read-only Git observation of one
|
|
3113
|
+
* instance's work tree. The instance is addressed qualified: an explicit
|
|
3114
|
+
* --home, or a name under the --dir scope (team roots included) that resolves
|
|
3115
|
+
* to exactly one home; several homes refuse with every candidate named. */
|
|
3116
|
+
function instanceCmd() {
|
|
3117
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
3118
|
+
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);
|
|
3121
|
+
dropAmbientRoot();
|
|
3122
|
+
let home = flag("home");
|
|
3123
|
+
if (home === true) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
3124
|
+
if (home !== undefined && !isAbsolute(home)) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
3125
|
+
if (home === undefined) {
|
|
3126
|
+
let root;
|
|
3127
|
+
try { root = ensureRoot(dirFlag()); } catch (e) { return bail(e.code || "E_NO_ROOT", e.message); }
|
|
3128
|
+
let r; try { r = resolveOatsConfig(dirFlag()); } catch (e) { return bail(e.code || "E_CONFIG_BROKEN", e.message); }
|
|
3129
|
+
const roots = [...new Set([root, ...(r.team ? teamAgentRoots(r.team.scope) : [])].map((p) => realOrResolved(p)))];
|
|
3130
|
+
const candidates = [];
|
|
3131
|
+
for (const rt of roots) for (const hit of findInstanceHomes(rt, name)) candidates.push({ root: rt, agent: hit.agent?.name ?? null, home: hit.home });
|
|
3132
|
+
if (!candidates.length) return bail("E_SESSION_UNKNOWN", `no instance ${JSON.stringify(name)} under ${roots.join(", ")}`);
|
|
3133
|
+
if (candidates.length > 1) return bail("E_AMBIGUOUS_INSTANCE", `instance ${JSON.stringify(name)} has ${candidates.length} homes; pass --home <abs>`, { candidates });
|
|
3134
|
+
home = candidates[0].home;
|
|
3135
|
+
} else if (basename(home) !== name) return bail("E_HOME_MISMATCH", `--home ${home} is not the home of instance ${JSON.stringify(name)}`);
|
|
3136
|
+
try {
|
|
3137
|
+
if (sub === "git") {
|
|
3138
|
+
const observed = observeInstanceGit(home);
|
|
3139
|
+
if (JSON_MODE) { jsonOk(observed); return; }
|
|
3140
|
+
const o = observed.observation;
|
|
3141
|
+
console.log(`${observed.instance} — ${shortPath(o.worktree)} @ ${o.branch ?? (o.detached ? `detached ${o.revision.slice(0, 12)}` : "unborn")}`);
|
|
3142
|
+
console.log(` upstream: ${observed.upstream.ref ? `${observed.upstream.ref} +${observed.upstream.ahead} -${observed.upstream.behind}` : "none (ahead/behind unknown)"}`);
|
|
3143
|
+
console.log(` base: ${observed.base.ref ? `${observed.base.ref} +${observed.base.ahead} -${observed.base.behind} (merge-base ${observed.base.mergeBase?.slice(0, 12)})` : "unknown"}`);
|
|
3144
|
+
console.log(` files: ${observed.files.length} (${Object.entries(observed.summary).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(", ") || "clean"})`);
|
|
3145
|
+
for (const f of observed.files) console.log(` ${f.xy} ${f.origPath ? `${f.origPath} -> ` : ""}${f.path} [${f.id}]`);
|
|
3146
|
+
for (const n of observed.notes) console.log(` note: ${n}`);
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
const fileId = flag("file"), revision = flag("revision"), indexRevision = flag("index-revision");
|
|
3150
|
+
if (fileId === true || revision === true || indexRevision === true) return bail("E_BAD_ARGS", usage);
|
|
3151
|
+
const d = diffInstanceFile(home, { fileId, revision, indexRevision });
|
|
3152
|
+
if (JSON_MODE) { jsonOk(d); return; }
|
|
3153
|
+
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]` : ""}`);
|
|
3154
|
+
if (!d.binary) process.stdout.write(d.patch);
|
|
3155
|
+
} catch (e) {
|
|
3156
|
+
bail(e.code || "E_GIT_FAILED", e.message, e.observation ? { observation: e.observation } : undefined);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3065
3159
|
function catalogCmd() {
|
|
3066
3160
|
const described = describeOfficialCatalog();
|
|
3067
3161
|
if (JSON_MODE) { jsonOk(described); return; }
|
|
@@ -3960,7 +4054,7 @@ function statusTeam() {
|
|
|
3960
4054
|
|
|
3961
4055
|
function spawnCmd() {
|
|
3962
4056
|
// JSON mode: contract envelope, stable error codes, stderr-only progress.
|
|
3963
|
-
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
4057
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
3964
4058
|
const note = (msg) => (JSON_MODE ? console.error(msg) : console.log(msg));
|
|
3965
4059
|
const yolo = yoloFlag();
|
|
3966
4060
|
const backend = valueFlag("backend"), herdrSocket = valueFlag("herdr-socket");
|
|
@@ -4110,6 +4204,9 @@ function spawnCmd() {
|
|
|
4110
4204
|
// model, runtime) is a fact about the selection, not a spawn-mechanism
|
|
4111
4205
|
// failure: it keeps its own code so a GUI can act on it.
|
|
4112
4206
|
if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_RUNTIME$/.test(e.code)) { bail(e.code, e.message); throw e; }
|
|
4207
|
+
// An unmet declared requirement is a fact about the soul's configuration
|
|
4208
|
+
// (with a remedy), not a spawn-mechanism failure: keep its code and details.
|
|
4209
|
+
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; }
|
|
4113
4210
|
bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
|
|
4114
4211
|
}
|
|
4115
4212
|
// The instance exists from here on: a failed wake save is reported beside
|
|
@@ -4418,7 +4515,8 @@ function onboardCmd() {
|
|
|
4418
4515
|
const source = parsePortableSource(`git:${catalogSource.url}@${pkg.commit}#${pkg.path}`);
|
|
4419
4516
|
const requires = { capabilities: Object.fromEntries(SETUP_CAPABILITIES.map(id => [id, { source: `${source.source}#${source.path}` }])) };
|
|
4420
4517
|
const soulFile = join(created.soul, "soul.yaml");
|
|
4421
|
-
writeFileAtomic(soulFile, readFileSync(soulFile, "utf8") + `requires: ${JSON.stringify(requires)}\ndefaults: ${JSON.stringify(edition.declaration.defaults)}\n`
|
|
4518
|
+
writeFileAtomic(soulFile, readFileSync(soulFile, "utf8") + `requires: ${JSON.stringify(requires)}\ndefaults: ${JSON.stringify(edition.declaration.defaults)}\n`
|
|
4519
|
+
+ `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
4520
|
const agent = findAgent(root, SETUP_EXPERT), composition = composeInstanceAgentsMd(created.soul, deployment, SETUP_EXPERT, "directory", "local");
|
|
4423
4521
|
planInstanceResources({ resolved: composition.resolved, soulDir: created.soul, agent, contextDir: deployment, composition });
|
|
4424
4522
|
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 +4567,17 @@ function createCmd() {
|
|
|
4469
4567
|
work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo,
|
|
4470
4568
|
instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
|
|
4471
4569
|
});
|
|
4472
|
-
|
|
4473
|
-
|
|
4570
|
+
// A declared oats.core is a requirement spawn will enforce: say the next
|
|
4571
|
+
// step here, not only at the refusal.
|
|
4572
|
+
const declared = r.declaredCapabilities || [];
|
|
4573
|
+
const inactive = declared.filter((id) => !(resolveOatsConfig(workspaceOf(root), name).capabilities || []).some((c) => c.id === id));
|
|
4574
|
+
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(" && ")}` }] : [];
|
|
4575
|
+
const notes = [...(r.notes || []), ...next];
|
|
4576
|
+
if (args.includes("--json")) { console.log(JSON.stringify({ ...r, ...(notes.length ? { notes } : {}), ...(bootstrapped ? { agentsRoot: root } : {}) }, null, 2)); return; }
|
|
4577
|
+
for (const information of notes) console.error(`[${information.code}] ${information.message}`);
|
|
4474
4578
|
if (bootstrapped) console.log(`Created deployment root ${shortPath(root)} (this scope had no agents/ yet)`);
|
|
4475
4579
|
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 "..."`);
|
|
4580
|
+
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
4581
|
}
|
|
4478
4582
|
|
|
4479
4583
|
// ---------- capability command dispatch ----------
|
|
@@ -5174,6 +5278,7 @@ else if (cmd === "config") configCmd();
|
|
|
5174
5278
|
else if (cmd === "trust") trust();
|
|
5175
5279
|
else if (cmd === "list") listCmd();
|
|
5176
5280
|
else if (cmd === "catalog") catalogCmd();
|
|
5281
|
+
else if (cmd === "instance") instanceCmd();
|
|
5177
5282
|
else if (cmd === "remove") removeCmd();
|
|
5178
5283
|
else if (cmd === "migrate") migrateCmd();
|
|
5179
5284
|
else if (cmd === "root") console.log(resolve(new URL("..", import.meta.url).pathname));
|
|
@@ -5368,6 +5473,13 @@ Usage:
|
|
|
5368
5473
|
scopes, trust state
|
|
5369
5474
|
oats catalog [--json] the effective official package catalog (read-only:
|
|
5370
5475
|
identity/discovery, no acquisition or trust)
|
|
5476
|
+
oats instance git <instance> [--home <abs>] [--dir <d>] [--json]
|
|
5477
|
+
read-only Git observation of the instance's work
|
|
5478
|
+
tree: branch, status (renames kept), ahead/behind
|
|
5479
|
+
vs upstream AND vs default-branch merge-base
|
|
5480
|
+
oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json]
|
|
5481
|
+
bounded diff of one observed file; refuses when
|
|
5482
|
+
the tree moved since the observation
|
|
5371
5483
|
oats update <package> [<package>@<ref>] transactional package update: temp fetch,
|
|
5372
5484
|
[--to <ref>] [--dir <d>] closure validation, diff, lock replace,
|
|
5373
5485
|
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-21 19:25Z (earlier stamps reading 2026-09-22 were a lead clock error — the work happened 2026-09-21; corrected on Antares's observation) · S8: 1a/1b/3/4/6a/7a MERGED · K1+K4 MERGED · engineer → 2a · lead → P1 Decision, K3, K5; tag 0.24.7 once 2a 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 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,15 +24,18 @@ 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
|
|
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.
|
|
33
37
|
- **K5 readiness quartet**: `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.
|
|
34
|
-
- **K6 spawn preview/apply
|
|
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.
|
|
35
39
|
- **K7 activity feed**: 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
|
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -46,6 +46,109 @@ no progress prose (progress goes to stderr):
|
|
|
46
46
|
- success (exit 0): `{"schemaVersion":1,"ok":true,"result":{...}}`
|
|
47
47
|
- failure (nonzero exit): `{"schemaVersion":1,"ok":false,"error":{"code":"...","message":"..."}}`
|
|
48
48
|
|
|
49
|
+
## Souls and sources (`oats inspect --json`, `soulsApi: 1`, OATS 0.24.7+)
|
|
50
|
+
|
|
51
|
+
Every entry in `result.souls[]` carries what the soul's **own `soul.yaml`
|
|
52
|
+
declares**, parsed by the kernel — a consumer never parses YAML and never
|
|
53
|
+
infers a field that is not there:
|
|
54
|
+
|
|
55
|
+
- `soulsApi: 1`
|
|
56
|
+
- `declarations: { requires, defaults, knowledge, teams, resources }` — each
|
|
57
|
+
the declared object, or `null` when the section is absent.
|
|
58
|
+
- `provenance: { kind, source, revision, path, workspaceRevision } | null` —
|
|
59
|
+
where this soul copy came from, as recorded by the kernel when it created
|
|
60
|
+
it (`oats onboard` records `packaged-definition` or
|
|
61
|
+
`exported-edition-copy`). Souls created before 0.24.7 or authored by hand
|
|
62
|
+
read `null`; render that as *unrecorded*, not as local or as anything else.
|
|
63
|
+
- `readiness` — the soul's **declared sources**, joined against
|
|
64
|
+
`result.capabilities[]` from the same payload. Distinct from launchability
|
|
65
|
+
(`oats spawn`) and adoption (`oats prepare`); never a green "Ready".
|
|
66
|
+
- `source: "recorded" | "unrecorded"`
|
|
67
|
+
- `requirements: [{ capability, source, installed, approved, active, version }] | null`
|
|
68
|
+
(`null` = nothing declared). `installed: false` = not in the inventory;
|
|
69
|
+
`approved`/`active`/`version` are `null` when there is no inventory row.
|
|
70
|
+
- `status: "undeclared" | "sources-installed" | "sources-missing" | "unknown"`
|
|
71
|
+
- `declarationProblems: [{ code, message }]` — an unreadable file reports why;
|
|
72
|
+
the soul is still listed.
|
|
73
|
+
|
|
74
|
+
`result.sources` is the scope's **portable source context**: the distinct
|
|
75
|
+
provenance sources its souls record.
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{"soulsApi":1,"kind":"recorded-provenance","note":null,
|
|
79
|
+
"items":[{"kind":"exported-edition-copy","source":"git:https://…/oats.git","revision":"<sha>","path":"souls/oats-setup-expert","workspaceRevision":"<sha>","souls":["oats-setup-expert"]}]}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`kind: "none-recorded"` (empty `items`, explanatory `note`) means no soul in the
|
|
83
|
+
scope records a portable **source address** — a soul may still carry a
|
|
84
|
+
`provenance` of kind `packaged-definition` with `source: null`. Say "no portable
|
|
85
|
+
source recorded"; do not infer "local" or "authored" from this state.
|
|
86
|
+
Workspace imports adopted onto a deployment will appear here at their pinned
|
|
87
|
+
revisions when that adoption is recorded on the deployment; nothing is
|
|
88
|
+
enumerated from a source repository that the deployment does not record.
|
|
89
|
+
|
|
90
|
+
## Instance Git state (`oats instance git|diff`, `instanceGitApi: 1`, OATS 0.24.7+)
|
|
91
|
+
|
|
92
|
+
Read-only observation of one instance's **work tree**. Truth comes from the
|
|
93
|
+
tree — the branch the tree is on, not the branch recorded at spawn (that is
|
|
94
|
+
reported under `recorded` with a `drift` flag). Fixed-argv `git`, no shell.
|
|
95
|
+
|
|
96
|
+
Address the instance qualified: `oats instance git <instance> --dir <scope>`
|
|
97
|
+
resolves the name under the scope's agents roots (team roots included) and
|
|
98
|
+
**refuses when several homes match** (`E_AMBIGUOUS_INSTANCE`, `details.candidates`);
|
|
99
|
+
pass `--home <abs>` to pick one. Unknown → `E_SESSION_UNKNOWN`; retired or
|
|
100
|
+
un-materialized tree → `E_NO_WORKTREE`.
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{"instanceGitApi":1,"instance":"dev-1","agent":"dev","home":"/abs/home","workMode":"worktree",
|
|
104
|
+
"observation":{"revision":"<HEAD oid|unborn>","indexRevision":"<index tree oid>","at":"<iso>","worktree":"/abs/work","branch":"feat/y","detached":false,"unborn":false},
|
|
105
|
+
"recorded":{"branch":"feat/x","repo":"/abs/repo","drift":true},
|
|
106
|
+
"upstream":{"ref":"origin/feat/y","ahead":1,"behind":0},
|
|
107
|
+
"base":{"ref":"origin/main","source":"origin/HEAD","mergeBase":"<oid>","ahead":2,"behind":0},
|
|
108
|
+
"summary":{"changed":1,"renamed":1,"copied":0,"unmerged":0,"untracked":1},
|
|
109
|
+
"files":[{"id":"<24 hex>","kind":"renamed","xy":"R.","submodule":false,"score":"R100","path":"src/new.txt","origPath":"src/old.txt"}],
|
|
110
|
+
"notes":[]}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `upstream` and `base` are **two separate comparisons**. No upstream →
|
|
114
|
+
`upstream: {ref:null, ahead:null, behind:null}` — unknown, **not 0/0**. `base`
|
|
115
|
+
is against the merge-base with the default branch (`origin/HEAD`, else a
|
|
116
|
+
well-known name; `source` says which); none found → all `null` plus a note.
|
|
117
|
+
- Status is porcelain v2, NUL-delimited: renames/copies carry `origPath`;
|
|
118
|
+
paths with spaces/newlines are intact. `kind` ∈ changed | renamed | copied |
|
|
119
|
+
unmerged | untracked. Ignored files are not listed.
|
|
120
|
+
- `files[].id` is **opaque**, minted under (`revision`, `indexRevision`). It is
|
|
121
|
+
the only way to ask for a diff.
|
|
122
|
+
|
|
123
|
+
`oats instance diff <instance> --file <id> --revision <rev> [--index-revision <idx>] --json`
|
|
124
|
+
returns a bounded unified diff:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{"instanceGitApi":1,"observation":{…},"file":{"id":"…","kind":"changed","xy":".M","path":"README.md","origPath":null},
|
|
128
|
+
"against":"<captured revision oid>","binary":false,"bytes":2683,"truncated":false,"limit":262144,"patch":"diff --git …",
|
|
129
|
+
"readOnly":{"helpers":"disabled","optionalLocks":"off","objectsWritten":0}}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- `against` is the **captured revision oid** for tracked changes (working tree
|
|
133
|
+
vs that exact commit, index included — never the moving `HEAD`) and `empty`
|
|
134
|
+
for untracked files. Binary → `binary: true`, empty patch. Over 256 KiB →
|
|
135
|
+
`truncated: true` at the byte limit.
|
|
136
|
+
- **Read-only, helper-free, consistent across the read** (`readOnly` echoes
|
|
137
|
+
it): the observed tree may carry a hostile repo config, so external diff,
|
|
138
|
+
textconv, fsmonitor and hooks are disabled and the caller's Git environment
|
|
139
|
+
and global config are not inherited; `--no-optional-locks` means no index
|
|
140
|
+
refresh and no object is written (`ls-files --stage` hash, not `write-tree`).
|
|
141
|
+
After producing the patch the CLI re-checks HEAD, index and the file's own
|
|
142
|
+
content against the observation and refuses `E_STALE_OBSERVATION` if any
|
|
143
|
+
moved mid-read — the result is never internally inconsistent.
|
|
144
|
+
- If HEAD or the index moved since the id was minted, or the id is not in the
|
|
145
|
+
current observation, the CLI **refuses** with `E_STALE_OBSERVATION` and
|
|
146
|
+
attaches the current `observation` in `error.details` — re-observe, never
|
|
147
|
+
render a diff against a tree that is not the one on screen. A path in
|
|
148
|
+
`--file` is `E_BAD_ARGS`.
|
|
149
|
+
|
|
150
|
+
No GitHub/PR data here: that is a capability seam (see the `oats.git` decision).
|
|
151
|
+
|
|
49
152
|
## Mutations exposed to Desktop v1
|
|
50
153
|
|
|
51
154
|
The commands below use the same envelope. Additional capability operations
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# OATS v0.24.7 — instance Git observation, soul declarations, and no hollow agents
|
|
2
|
+
|
|
3
|
+
Kernel/Pi/Desktop **0.24.7**. Two additive read-only contracts for the Desktop
|
|
4
|
+
(`instanceGitApi: 1`, `soulsApi: 1`), one spawn refusal that closes a
|
|
5
|
+
first-team-path defect, and four Desktop parity slices.
|
|
6
|
+
|
|
7
|
+
## Kernel
|
|
8
|
+
|
|
9
|
+
- **`oats instance git <instance> [--home] [--dir] [--json]`** and
|
|
10
|
+
**`oats instance diff <instance> --file <id> --revision <rev> [--index-revision <idx>] [--json]`** (K1) —
|
|
11
|
+
read-only Git observation of one instance's work tree. Truth from the tree:
|
|
12
|
+
the branch the tree is on (the spawn-recorded branch is reported as
|
|
13
|
+
`recorded` with a `drift` flag). Porcelain v2 NUL status with renames keeping
|
|
14
|
+
both paths; **upstream and default-branch merge-base as two separate
|
|
15
|
+
comparisons** — no upstream is `null`, never 0/0. Opaque file ids minted
|
|
16
|
+
under (HEAD, index); a diff is taken against the *captured* revision oid and
|
|
17
|
+
refuses `E_STALE_OBSERVATION` (fresh observation attached) if HEAD, index or
|
|
18
|
+
the file's content moved, before **or during** the read. Bounded (256 KiB,
|
|
19
|
+
binary flagged). Qualified addressing: several homes with one name refuse
|
|
20
|
+
`E_AMBIGUOUS_INSTANCE` with candidates; retired/unmaterialized tree is
|
|
21
|
+
`E_NO_WORKTREE`.
|
|
22
|
+
**Hardened after a consumer-side adversarial probe**: the observed tree is
|
|
23
|
+
worked in by an agent, so its repo config is input — external diff, textconv,
|
|
24
|
+
fsmonitor and hooks are disabled, the caller's Git environment and global/
|
|
25
|
+
system config are not inherited, optional locks are off (no index refresh),
|
|
26
|
+
and the index revision is a hash of `ls-files --stage` (no `write-tree`, no
|
|
27
|
+
object written). The response states it: `readOnly: {helpers, optionalLocks, objectsWritten}`.
|
|
28
|
+
- **`oats inspect --json` souls carry their declarations** (K4, `soulsApi: 1`):
|
|
29
|
+
`declarations` (requires/defaults/knowledge/teams/resources, parsed
|
|
30
|
+
kernel-side, `null` when absent), `provenance` (recorded by `oats onboard`
|
|
31
|
+
from now on; `null` = *unrecorded*, never inferred), `readiness` (declared
|
|
32
|
+
sources joined against the same payload's capability inventory — separate
|
|
33
|
+
from launchability and adoption; never a green "Ready"), and
|
|
34
|
+
`result.sources` — the scope's portable source context, with an honest
|
|
35
|
+
`none-recorded` state.
|
|
36
|
+
- **No hollow agents.** `oats create` declares `oats.core` by default; composition
|
|
37
|
+
honours the declaration by suppressing the kernel's legacy skills — but nothing
|
|
38
|
+
on that path activates the replacement, so a `spawn` produced an agent with
|
|
39
|
+
**no operational curriculum and no warning** (second-operator finding on 0.24.6).
|
|
40
|
+
Now `spawn` refuses `E_REQUIREMENT_INACTIVE` before creating anything, with
|
|
41
|
+
the remedy (`oats install oats.framework` if needed, then
|
|
42
|
+
`oats use oats.core --soul <name>`) and the opt-out (remove the declaration).
|
|
43
|
+
`create` states the activation step up front (`next-step` note,
|
|
44
|
+
`declaredCapabilities` in `--json`). `create` stays declaration, not acquisition.
|
|
45
|
+
|
|
46
|
+
## Desktop (parity slices 1b, 2a, 3, 4, 6a, 7a)
|
|
47
|
+
|
|
48
|
+
- **1b** shared contextual right panel (Instance · Git & GitHub · Soul), collapsed
|
|
49
|
+
rail, per-workspace preferences, temporary focus mode.
|
|
50
|
+
- **2a** Git & GitHub panel on K1: worktree/branch/drift, upstream and base named
|
|
51
|
+
separately, real file counts and rename paths, bounded read-only diff; stale
|
|
52
|
+
→ re-observe, never rendered. The legacy background `gitState` collector —
|
|
53
|
+
which ran Git against every instance tree on every roster poll without helper
|
|
54
|
+
controls and substituted healthy zeros on failure — is **removed**. Desktop
|
|
55
|
+
Git reads are the K1 route only. GitHub/PR card is *unavailable* pending the
|
|
56
|
+
`oats.git` decision.
|
|
57
|
+
- **3** Souls + Sources on K4: declarations, recorded provenance (`null` renders
|
|
58
|
+
*Unrecorded*, never *Local*), "Not declared" vs "Not reported" distinguished,
|
|
59
|
+
sources-installed ≠ Ready.
|
|
60
|
+
- **4** Capabilities: official catalog via `oats catalog` (0.24.6+) plus the
|
|
61
|
+
deployment's classic inventory; aliases are mappings not exports, refs are refs,
|
|
62
|
+
override catalogs conspicuously labelled; "Add capability" copies the schema's
|
|
63
|
+
exact `oats install <package>` argv and never executes it.
|
|
64
|
+
- **6a** Spawn modal to the redesign on existing seams: two-column layout,
|
|
65
|
+
grouped soul chooser, provider/model with reported-vs-assumed provenance,
|
|
66
|
+
launch configuration restored (an already-supported property the renderer had
|
|
67
|
+
regressed out of), fields that need K6 rendered disabled with their seam named.
|
|
68
|
+
- **7a** Active overview (frame 07) on the roster: anonymous count-labelled
|
|
69
|
+
relation groups (the roster reports no group names), activity `unknown` until K7.
|
|
70
|
+
|
|
71
|
+
Desktop CLI floor for the new views is **0.24.7** (`instanceGitApi`/`soulsApi`
|
|
72
|
+
gates); an older CLI renders those views as *unavailable*, never as empty-healthy.
|
|
73
|
+
|
|
74
|
+
## Also
|
|
75
|
+
|
|
76
|
+
- Board and seams doc dates corrected (2026-09-21, not 09-22 — lead clock error
|
|
77
|
+
caught by the second operator). Pinned SHAs unaffected.
|
|
78
|
+
- Second-operator `launch` re-run on 0.24.6 passed both halves (publish with an
|
|
79
|
+
executable launch selection; hard runtime row refuses attributed).
|
|
80
|
+
- OKF 2.1.3 bug list (deferred with harvest): `check` per-cause reasons; retire
|
|
81
|
+
leaves the per-source `okf-<id>` schedule definition enabled.
|
package/lib/core.mjs
CHANGED
|
@@ -4248,6 +4248,22 @@ export function planInstanceResources({ resolved, soulDir, agent, contextDir, co
|
|
|
4248
4248
|
for (const b of composition.blocks || []) expected.push({ type: "instruction-block", source: b.source, declared: b.file, path: b.file });
|
|
4249
4249
|
}
|
|
4250
4250
|
|
|
4251
|
+
// A soul that DECLARES an operational capability (requires.capabilities,
|
|
4252
|
+
// e.g. oats.core written by `oats create`) has, by that declaration, given up
|
|
4253
|
+
// the kernel's legacy skill injection. If the declared capability is not
|
|
4254
|
+
// active for this soul in this context, the instance would start with no
|
|
4255
|
+
// operational curriculum and no warning — a hollow agent. That is an unmet
|
|
4256
|
+
// declared requirement: refuse, attributed, with the exact remedy. Removing
|
|
4257
|
+
// the declaration from soul.yaml is the supported way to opt out.
|
|
4258
|
+
const declaredOps = declaredOperationalCapabilities(soulDir);
|
|
4259
|
+
const inactiveOps = declaredOps.filter((id) => !(resolved.capabilities || []).some((c) => c.id === id));
|
|
4260
|
+
if (inactiveOps.length) {
|
|
4261
|
+
const soulName = agent?.name ?? basename(dirname(soulDir));
|
|
4262
|
+
const remedy = inactiveOps.map((id) => `oats use ${id} --soul ${soulName}${contextDir ? ` --dir ${contextDir}` : ""}`).join(" && ");
|
|
4263
|
+
throw Object.assign(oatsError("E_REQUIREMENT_INACTIVE", `soul ${soulName} declares ${inactiveOps.join(", ")} in requires.capabilities, but ${inactiveOps.length === 1 ? "it is" : "they are"} not active for this soul in ${contextDir}; the declaration suppresses the kernel's legacy skills, so the instance would start with no operational curriculum.\n Acquire if needed (\`oats install oats.framework\`), then activate: ${remedy}\n Or remove the declaration from ${join(soulDir, "soul.yaml")} to run without it.`),
|
|
4264
|
+
{ soul: soulName, capabilities: inactiveOps, context: contextDir, remedy });
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4251
4267
|
// A capability that declares a REQUIRED hook it cannot execute must not spawn.
|
|
4252
4268
|
// Advisory executable hooks stay disabled-with-warning; a required one is a
|
|
4253
4269
|
// promise, and starting without it is the failure required:true exists to stop.
|
|
@@ -5196,7 +5212,7 @@ export function writeSoul(root, { name, kind, repo, work, runtime, model, yolo,
|
|
|
5196
5212
|
home: soulDir, instance: name, agentName: name, soulDir,
|
|
5197
5213
|
contextDir: ctx, workspaceDir: workspaceOf(root), rootDir: root, resolved,
|
|
5198
5214
|
});
|
|
5199
|
-
return { agentDir, soulDir, ...(notes.length ? { notes } : {}) };
|
|
5215
|
+
return { agentDir, soulDir, declaredCapabilities: Object.keys(requires?.capabilities ?? {}), ...(notes.length ? { notes } : {}) };
|
|
5200
5216
|
}
|
|
5201
5217
|
function defaultSoulAgentsMd(name, description) {
|
|
5202
5218
|
return `# ${name}
|
|
@@ -5218,8 +5234,8 @@ export function createAgent(root, o) {
|
|
|
5218
5234
|
// kind: "local" → a FULL soul (memory, skills, instances) under the scope's
|
|
5219
5235
|
// local-agents/ — uncommitted by contract; otherwise a committed persistent soul.
|
|
5220
5236
|
const kind = o.local || o.kind === "local" ? "local" : "persistent";
|
|
5221
|
-
const { agentDir, notes } = writeSoul(root, { ...o, name, kind });
|
|
5222
|
-
return { agent: name, kind, soul: soulOf(agentDir), ...(notes ? { notes } : {}) };
|
|
5237
|
+
const { agentDir, notes, declaredCapabilities } = writeSoul(root, { ...o, name, kind });
|
|
5238
|
+
return { agent: name, kind, soul: soulOf(agentDir), declaredCapabilities, ...(notes ? { notes } : {}) };
|
|
5223
5239
|
}
|
|
5224
5240
|
|
|
5225
5241
|
/** Upsert a local agent soul (from raw instructions or a Claude-style def file).
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/** K1 — read-only Git observation of one instance's work tree.
|
|
2
|
+
*
|
|
3
|
+
* Truth comes from the tree itself (never from recorded spawn metadata):
|
|
4
|
+
* branch/HEAD via the worktree, status via porcelain v2 NUL records, ahead/
|
|
5
|
+
* behind reported twice and separately (upstream; merge-base with the
|
|
6
|
+
* repository's default branch) with "no upstream" ≠ 0/0. Diffs are bounded and
|
|
7
|
+
* addressed by an opaque file id minted with an observation revision; a diff
|
|
8
|
+
* against a tree that has since moved is refused, never served. */
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { oatsError } from "./errors.mjs";
|
|
14
|
+
|
|
15
|
+
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
16
|
+
const DIFF_MAX_BYTES = 256 * 1024;
|
|
17
|
+
export const INSTANCE_GIT_API = 1;
|
|
18
|
+
|
|
19
|
+
/** Every invocation is read-only and helper-free: the tree being observed may
|
|
20
|
+
* carry a hostile repo config (an agent works there), so external diff /
|
|
21
|
+
* textconv drivers, fsmonitor and hooks are disabled explicitly, the caller's
|
|
22
|
+
* Git environment is not inherited, and optional locks are off so status/diff
|
|
23
|
+
* never refresh (write) the index. */
|
|
24
|
+
const READ_ONLY_GIT = ["--no-optional-locks",
|
|
25
|
+
"-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "-c", "diff.external=", "-c", "core.pager=cat",
|
|
26
|
+
"-c", "core.untrackedCache=false", "-c", "index.threads=1", "-c", "safe.bareRepository=explicit"];
|
|
27
|
+
function gitEnv() {
|
|
28
|
+
const env = { PATH: process.env.PATH ?? "", HOME: process.env.HOME ?? "", LANG: "C", LC_ALL: "C", GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1" };
|
|
29
|
+
// The user's global config may name helpers too; observations do not need it.
|
|
30
|
+
env.GIT_CONFIG_GLOBAL = "/dev/null";
|
|
31
|
+
return env;
|
|
32
|
+
}
|
|
33
|
+
function git(cwd, argv, { allowFail = false, input, diffExit = false } = {}) {
|
|
34
|
+
const [sub, ...rest] = argv;
|
|
35
|
+
const extra = sub === "diff" ? ["--no-ext-diff", "--no-textconv", "--no-color"] : [];
|
|
36
|
+
try {
|
|
37
|
+
return execFileSync("git", [...READ_ONLY_GIT, "-C", cwd, sub, ...extra, ...rest], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER, input, shell: false, timeout: 30_000, env: gitEnv() });
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// `git diff --no-index` exits 1 when the inputs differ: that is the answer, not a failure.
|
|
40
|
+
if (diffExit && e.status === 1 && typeof e.stdout === "string") return e.stdout;
|
|
41
|
+
if (allowFail) return null;
|
|
42
|
+
throw oatsError("E_GIT_FAILED", `git ${sub} failed in ${cwd}: ${String(e.stderr ?? e.message ?? "").trim() || "unknown error"}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const trim = (s) => (s === null ? null : s.trim());
|
|
46
|
+
|
|
47
|
+
/** The instance's work tree, from its home. The home's instance.json names
|
|
48
|
+
* the work mode; the tree is `<home>/work` (a directory or a symlink to the
|
|
49
|
+
* shared checkout). Missing/retired → attributed refusal, never a crash. */
|
|
50
|
+
export function instanceWorkTree(home) {
|
|
51
|
+
const metaFile = join(home, "instance.json");
|
|
52
|
+
if (!existsSync(metaFile)) throw oatsError("E_SESSION_UNKNOWN", `${home} is not an OATS instance home (no instance.json)`);
|
|
53
|
+
let meta;
|
|
54
|
+
try { meta = JSON.parse(readFileSync(metaFile, "utf8")); } catch (e) { throw oatsError("E_SESSION_UNKNOWN", `${metaFile}: ${e.message}`); }
|
|
55
|
+
const work = join(home, "work");
|
|
56
|
+
if (!existsSync(work)) throw oatsError("E_NO_WORKTREE", `${meta.instance ?? home} has no work tree at ${work} (retired, recovered, or never materialized)`);
|
|
57
|
+
const inside = trim(git(work, ["rev-parse", "--is-inside-work-tree"], { allowFail: true }));
|
|
58
|
+
if (inside !== "true") throw oatsError("E_NO_WORKTREE", `${work} is not inside a git work tree`);
|
|
59
|
+
return { meta, work, mode: meta.work ?? null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Porcelain v2 `-z` records → entries. Renames carry both paths. */
|
|
63
|
+
export function parsePorcelainV2(raw) {
|
|
64
|
+
const fields = raw.split("\0");
|
|
65
|
+
const entries = [];
|
|
66
|
+
let branch = { oid: null, head: null, upstream: null, ahead: null, behind: null };
|
|
67
|
+
for (let i = 0; i < fields.length; i++) {
|
|
68
|
+
const rec = fields[i];
|
|
69
|
+
if (!rec) continue;
|
|
70
|
+
if (rec.startsWith("# ")) {
|
|
71
|
+
const [, key, ...rest] = rec.split(" ");
|
|
72
|
+
const value = rest.join(" ");
|
|
73
|
+
if (key === "branch.oid") branch.oid = value === "(initial)" ? null : value;
|
|
74
|
+
else if (key === "branch.head") branch.head = value === "(detached)" ? null : value;
|
|
75
|
+
else if (key === "branch.upstream") branch.upstream = value;
|
|
76
|
+
else if (key === "branch.ab") { const m = /^\+(\d+) -(\d+)$/.exec(value); if (m) { branch.ahead = Number(m[1]); branch.behind = Number(m[2]); } }
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const type = rec[0];
|
|
80
|
+
if (type === "1") {
|
|
81
|
+
const parts = rec.split(" ");
|
|
82
|
+
entries.push({ kind: "changed", xy: parts[1], submodule: parts[2] !== "N...", path: parts.slice(8).join(" "), origPath: null });
|
|
83
|
+
} else if (type === "2") {
|
|
84
|
+
const parts = rec.split(" ");
|
|
85
|
+
const path = parts.slice(9).join(" ");
|
|
86
|
+
const origPath = fields[++i] ?? null; // rename/copy: the original path is the next NUL field
|
|
87
|
+
entries.push({ kind: /^R/.test(parts[8]) ? "renamed" : "copied", xy: parts[1], submodule: parts[2] !== "N...", score: parts[8], path, origPath });
|
|
88
|
+
} else if (type === "u") {
|
|
89
|
+
const parts = rec.split(" ");
|
|
90
|
+
entries.push({ kind: "unmerged", xy: parts[1], submodule: parts[2] !== "N...", path: parts.slice(10).join(" "), origPath: null });
|
|
91
|
+
} else if (type === "?") entries.push({ kind: "untracked", xy: "??", submodule: false, path: rec.slice(2), origPath: null });
|
|
92
|
+
else if (type === "!") entries.push({ kind: "ignored", xy: "!!", submodule: false, path: rec.slice(2), origPath: null });
|
|
93
|
+
}
|
|
94
|
+
return { branch, entries };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function defaultBranch(work) {
|
|
98
|
+
const sym = trim(git(work, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], { allowFail: true }));
|
|
99
|
+
if (sym) return { ref: sym, source: "origin/HEAD" };
|
|
100
|
+
for (const candidate of ["origin/main", "origin/master", "main", "master"]) {
|
|
101
|
+
if (git(work, ["rev-parse", "--verify", "--quiet", `${candidate}^{commit}`], { allowFail: true }) !== null) return { ref: candidate, source: "well-known" };
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function countRange(work, range) {
|
|
107
|
+
const out = trim(git(work, ["rev-list", "--left-right", "--count", range], { allowFail: true }));
|
|
108
|
+
if (out === null) return null;
|
|
109
|
+
const [left, right] = out.split(/\s+/).map(Number);
|
|
110
|
+
return { left, right };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function indexRevisionOf(work) {
|
|
114
|
+
const listing = git(work, ["ls-files", "--stage", "-z"], { allowFail: true });
|
|
115
|
+
return listing === null ? "no-index" : createHash("sha256").update(listing).digest("hex").slice(0, 40);
|
|
116
|
+
}
|
|
117
|
+
function blobOf(work, path) {
|
|
118
|
+
// Content fingerprint of the working-tree file without writing an object.
|
|
119
|
+
return trim(git(work, ["hash-object", "--no-filters", "--", path], { allowFail: true }));
|
|
120
|
+
}
|
|
121
|
+
function fileId(revision, indexOid, entry) {
|
|
122
|
+
return createHash("sha256").update(`${revision}\0${indexOid}\0${entry.kind}\0${entry.path}\0${entry.origPath ?? ""}`).digest("hex").slice(0, 24);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** One consistent observation of the tree. Every field is what git said. */
|
|
126
|
+
export function observeInstanceGit(home) {
|
|
127
|
+
const { meta, work, mode } = instanceWorkTree(home);
|
|
128
|
+
const headOid = trim(git(work, ["rev-parse", "--verify", "--quiet", "HEAD"], { allowFail: true }));
|
|
129
|
+
const raw = git(work, ["status", "--porcelain=v2", "-z", "--branch", "--untracked-files=all", "--ignore-submodules=none"]);
|
|
130
|
+
const { branch, entries } = parsePorcelainV2(raw);
|
|
131
|
+
// The index state participates in the revision so that a stage/unstage
|
|
132
|
+
// between observation and diff is a moved tree, not a stale-but-served diff.
|
|
133
|
+
// Hashed from the index listing: no `write-tree`, so observing creates no object.
|
|
134
|
+
const indexOid = indexRevisionOf(work);
|
|
135
|
+
const revision = headOid ?? "unborn";
|
|
136
|
+
const at = new Date().toISOString();
|
|
137
|
+
const upstream = branch.upstream
|
|
138
|
+
? { ref: branch.upstream, ahead: branch.ahead, behind: branch.behind }
|
|
139
|
+
: { ref: null, ahead: null, behind: null };
|
|
140
|
+
const base = defaultBranch(work);
|
|
141
|
+
let baseComparison = { ref: null, source: null, mergeBase: null, ahead: null, behind: null };
|
|
142
|
+
if (base && headOid) {
|
|
143
|
+
const mergeBase = trim(git(work, ["merge-base", "HEAD", base.ref], { allowFail: true }));
|
|
144
|
+
const counts = mergeBase ? countRange(work, `${base.ref}...HEAD`) : null;
|
|
145
|
+
baseComparison = { ref: base.ref, source: base.source, mergeBase, ahead: counts ? counts.right : null, behind: counts ? counts.left : null };
|
|
146
|
+
}
|
|
147
|
+
const files = entries.filter((e) => e.kind !== "ignored").map((e) => ({ id: fileId(revision, indexOid, e), ...e }));
|
|
148
|
+
const summary = { changed: 0, renamed: 0, copied: 0, unmerged: 0, untracked: 0 };
|
|
149
|
+
for (const f of files) summary[f.kind]++;
|
|
150
|
+
return {
|
|
151
|
+
instanceGitApi: INSTANCE_GIT_API,
|
|
152
|
+
instance: meta.instance ?? null, agent: meta.agent ?? null, home, workMode: mode,
|
|
153
|
+
observation: { revision, indexRevision: indexOid, at, worktree: work, branch: branch.head, detached: branch.head === null && headOid !== null, unborn: headOid === null },
|
|
154
|
+
recorded: { branch: meta.branch ?? null, repo: meta.repo ?? null, drift: meta.branch !== undefined && meta.branch !== null && branch.head !== meta.branch },
|
|
155
|
+
upstream, base: baseComparison,
|
|
156
|
+
summary, files,
|
|
157
|
+
notes: [
|
|
158
|
+
...(upstream.ref === null ? ["no upstream configured: upstream ahead/behind are unknown, not zero"] : []),
|
|
159
|
+
...(baseComparison.ref === null ? ["no default branch found (origin/HEAD, origin/main, origin/master, main, master): base comparison unknown"] : []),
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A bounded unified diff for one observed file. The caller passes the id
|
|
165
|
+
* and the observation revision it was minted under; a moved tree refuses. */
|
|
166
|
+
export function diffInstanceFile(home, { fileId: id, revision, indexRevision } = {}) {
|
|
167
|
+
if (typeof id !== "string" || !/^[a-f0-9]{24}$/.test(id)) throw oatsError("E_BAD_ARGS", "--file needs the opaque file id from `oats instance git --json`");
|
|
168
|
+
if (typeof revision !== "string" || !revision) throw oatsError("E_BAD_ARGS", "--revision needs the observation revision the file id was minted under");
|
|
169
|
+
const current = observeInstanceGit(home);
|
|
170
|
+
if (current.observation.revision !== revision || (indexRevision !== undefined && current.observation.indexRevision !== indexRevision)) {
|
|
171
|
+
throw Object.assign(oatsError("E_STALE_OBSERVATION", "the work tree moved since this file id was observed; re-observe with `oats instance git --json`"), { observation: current.observation });
|
|
172
|
+
}
|
|
173
|
+
const file = current.files.find((f) => f.id === id);
|
|
174
|
+
if (!file) throw Object.assign(oatsError("E_STALE_OBSERVATION", "this file id is not part of the current observation; re-observe"), { observation: current.observation });
|
|
175
|
+
const work = current.observation.worktree;
|
|
176
|
+
const paths = file.origPath ? [file.origPath, file.path] : [file.path];
|
|
177
|
+
const before = { blob: blobOf(work, file.path) };
|
|
178
|
+
let patch, binary = false;
|
|
179
|
+
if (file.kind === "untracked") {
|
|
180
|
+
const numstat = trim(git(work, ["diff", "--no-index", "--numstat", "--", "/dev/null", file.path], { diffExit: true, allowFail: true }));
|
|
181
|
+
binary = /^-\t-\t/.test(numstat ?? "");
|
|
182
|
+
patch = binary ? "" : (git(work, ["diff", "--no-index", "--", "/dev/null", file.path], { diffExit: true, allowFail: true }) ?? "");
|
|
183
|
+
} else {
|
|
184
|
+
const numstat = trim(git(work, ["diff", current.observation.revision, "--numstat", "-M", "--", ...paths], { allowFail: true }));
|
|
185
|
+
binary = /^-\t-\t/.test(numstat ?? "");
|
|
186
|
+
patch = binary ? "" : git(work, ["diff", current.observation.revision, "-M", "--", ...paths]);
|
|
187
|
+
}
|
|
188
|
+
// Consistency across the read, not only before it: HEAD, index and the file's
|
|
189
|
+
// own content must be what the observation said when the patch was produced.
|
|
190
|
+
const after = { revision: trim(git(work, ["rev-parse", "--verify", "--quiet", "HEAD"], { allowFail: true })) ?? "unborn", indexRevision: indexRevisionOf(work), blob: blobOf(work, file.path) };
|
|
191
|
+
if (after.revision !== current.observation.revision || after.indexRevision !== current.observation.indexRevision || after.blob !== before.blob) {
|
|
192
|
+
const moved = observeInstanceGit(home);
|
|
193
|
+
throw Object.assign(oatsError("E_STALE_OBSERVATION", "the work tree moved while the diff was being read; re-observe with `oats instance git --json`"), { observation: moved.observation });
|
|
194
|
+
}
|
|
195
|
+
const bytes = Buffer.byteLength(patch, "utf8");
|
|
196
|
+
const truncated = bytes > DIFF_MAX_BYTES;
|
|
197
|
+
const body = truncated ? Buffer.from(patch, "utf8").subarray(0, DIFF_MAX_BYTES).toString("utf8") : patch;
|
|
198
|
+
return {
|
|
199
|
+
instanceGitApi: INSTANCE_GIT_API, observation: current.observation,
|
|
200
|
+
file: { id: file.id, kind: file.kind, xy: file.xy, path: file.path, origPath: file.origPath },
|
|
201
|
+
against: file.kind === "untracked" ? "empty" : current.observation.revision, binary, bytes, truncated, limit: DIFF_MAX_BYTES, patch: body,
|
|
202
|
+
readOnly: { helpers: "disabled", optionalLocks: "off", objectsWritten: 0 },
|
|
203
|
+
};
|
|
204
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.7",
|
|
4
4
|
"description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|