@awebai/oats 0.34.0 → 0.34.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/oats.mjs CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
31
31
  capabilityManifests, capabilityTrust, capabilityExecutablePath,
32
32
  officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
33
- findInstanceHome, findInstanceHomes, enclosingInstanceHome, logicalCwd, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, recordedKernelBin, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, validateLaunchConfigDefaults, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
33
+ findInstanceHome, findInstanceHomes, enclosingInstanceHome, logicalCwd, readableInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, recordedKernelBin, launchConfigsAt, launchReportFor, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, validateLaunchConfigDefaults, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, withSafeTaskPrompt, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
34
34
  } from "../lib/core.mjs";
35
35
  import {
36
36
  writeFileAtomic, LOCK_FILE, readLock, readLockIfPresent, writeLock, resolvePackages, memoizedRemote,
@@ -391,7 +391,7 @@ async function workspaceTarget(bail, { command, liveTeams = true }) {
391
391
  try { meta = JSON.parse(readFileSync(join(homeFlag, "instance.json"), "utf8")); } catch (e) { return bail("E_SESSION_UNKNOWN", `${homeFlag} is not an OATS instance home (${e.code === "ENOENT" ? "no instance.json" : e.message})`); }
392
392
  if (isCapturedHome(meta)) { const e = capturedHomeRefusal(homeFlag, "nothing was read"); return bail(e.code, e.message, e.details); }
393
393
  if (!meta || typeof meta.modules !== "object" || meta.modules === null) return bail("E_UNSUPPORTED_MODE", `${homeFlag} is not a workspace-model home (it records no modules): it was spawned by an earlier kernel — re-spawn it from the deployment`);
394
- if (soulFlag && soulFlag !== meta.agent) return bail("E_HOME_MISMATCH", `--soul ${soulFlag} is not the soul of ${homeFlag} (${meta.agent})`);
394
+ if (soulFlag && !(await import("../lib/instance-resolution.mjs")).homeSoulMatches(soulFlag, meta)) return bail("E_HOME_MISMATCH", `--soul ${soulFlag} is not the soul of ${homeFlag} (${meta.agent})`);
395
395
  const deployment = dirname(dirname(dirname(dirname(realOrResolved(homeFlag)))));
396
396
  // A v2 home lives at <deployment>/agents/<soul>/instances/<name>: its deployment is
397
397
  // derived, so it must hold oats-local.yaml EXACTLY there (never found by walking up).
@@ -537,8 +537,9 @@ async function workspaceOperation(t, { bail, address, layer, opName }) {
537
537
  const settings = lp.settings;
538
538
  const cwd = op.context === "home" ? t.home : t.deployment;
539
539
  const env = { ...lp.env(mod.name, settings), OATS_OPERATION: address, OATS_CONTEXT: t.deployment, OATS_ROOT: t.agentsRoot, PI_AGENTS_ROOT: t.agentsRoot };
540
- if (op.context === "home") Object.assign(env, { OATS_INSTANCE: t.meta.instance, OATS_INSTANCE_HOME: t.home, OATS_HOME: t.home, PI_AGENT_INSTANCE: t.meta.instance, PI_AGENT_HOME: t.home });
541
- else for (const k of ["OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "PI_AGENT_INSTANCE", "PI_AGENT_HOME"]) delete env[k];
540
+ // The home's identity, never one inherited from the caller (the reserved PI_AGENT_* names included).
541
+ for (const k of ["OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "PI_AGENT_INSTANCE", "PI_AGENT_HOME"]) delete env[k];
542
+ if (op.context === "home") Object.assign(env, { OATS_INSTANCE: t.meta.instance, OATS_INSTANCE_HOME: t.home, OATS_HOME: t.home });
542
543
  await readSession?.closeBatches(); // no idle `git cat-file --batch` child held for the provider's whole run
543
544
  const r = spawnSync("node", [abs, ...rest, ...argFlags, "--json"], { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 16 * 1024 * 1024, timeout: OPERATION_TIMEOUT_MS, killSignal: "SIGTERM" });
544
545
  finishOperation({ r, bail, address, provider, op, argFlags, cwd, home: t.home, meta: t.meta, api: INSPECT_OPERATIONS_API });
@@ -582,7 +583,8 @@ function legacyLayoutProblems(root) {
582
583
  }
583
584
  async function doctorWorkspaceJson(ctx, soulName, ws) {
584
585
  const composition = await doctorComposition(ctx, soulName, ws, (code, msg, details) => jsonFail(code, msg, details));
585
- const problems = legacyLayoutProblems(join(dirname(ws.local.path), "agents"));
586
+ const agentsRoot = join(dirname(ws.local.path), "agents");
587
+ const problems = [...legacyLayoutProblems(agentsRoot), readableInstanceHomes(agentsRoot)].filter(Boolean);
586
588
  return {
587
589
  schemaVersion: 1, workspaceApi: 2, context: ctx,
588
590
  workspace: { file: ws.local.path, ref: ws.local.workspace },
@@ -621,7 +623,8 @@ async function doctor(dir) {
621
623
  doctorVersionSkew();
622
624
  const composition = await doctorComposition(ctx, soulName, ws, (code, msg) => die(`${msg} [${code}]`));
623
625
  printDoctorWorkspace(ws);
624
- for (const p of legacyLayoutProblems(join(dirname(ws.local.path), "agents"))) console.log(`\n! ${p.code}: ${p.message}`);
626
+ const agentsRoot = join(dirname(ws.local.path), "agents");
627
+ for (const p of [...legacyLayoutProblems(agentsRoot), readableInstanceHomes(agentsRoot)].filter(Boolean)) console.log(`\n! ${p.code}: ${p.message}`);
625
628
  if (soulName) {
626
629
  const information = operationalKnowledgeNote(composition, soulName);
627
630
  if (information) console.log(`\nINFO: ${information}`);
@@ -768,11 +771,11 @@ function launchPreview(bail) {
768
771
  instance = meta.instance || basename(home);
769
772
  if (!(meta.launch && typeof meta.launch === "object") && !selectionGiven) {
770
773
  // A home that predates recipes, asked nothing: its frozen command is
771
- // described as is. Under a selection the planner refuses it
772
- // (E_LAUNCH_LEGACY: re-spawn it from the deployment).
773
- let d;
774
- try { d = describeLaunchCommand(meta.command); } catch (e) { bail(e.code || "E_LAUNCH_COMMAND_UNSUPPORTED", e.message); }
775
- jsonOk({ context, selected, selection: { source: "frozen-command", launchConfig: null, harness: null, model: null, yolo: null }, harness: meta.harness, model: meta.model || null, modelSource: meta.model ? "recorded" : "native default", yolo: meta.yolo ?? null, launchConfig: null, launchConfigSource: null, launchConfigDefault: false, executable: { path: d.executable, declared: null, resolvedFrom: "recorded" }, argv: d.argv, environment: d.environment, command: redactLaunchCommand(meta.command), prompt: { kind: "task-file", file: "TASK.md" }, hooks: null, preflight: [{ check: "recipe", ok: true, detail: "frozen command; a selection is refused (E_LAUNCH_LEGACY): re-spawn it" }], ok: true });
774
+ // described as a start runs it (with its harness's safe task prompt). Under a
775
+ // selection the planner refuses it (E_LAUNCH_LEGACY: re-spawn it from the deployment).
776
+ let d, frozen;
777
+ try { frozen = withSafeTaskPrompt(meta.command, meta.harness); d = describeLaunchCommand(frozen); } catch (e) { bail(e.code || "E_LAUNCH_COMMAND_UNSUPPORTED", e.message); }
778
+ jsonOk({ context, selected, selection: { source: "frozen-command", launchConfig: null, harness: null, model: null, yolo: null }, harness: meta.harness, model: meta.model || null, modelSource: meta.model ? "recorded" : "native default", yolo: meta.yolo ?? null, launchConfig: null, launchConfigSource: null, launchConfigDefault: false, executable: { path: d.executable, declared: null, resolvedFrom: "recorded" }, argv: d.argv, environment: d.environment, command: redactLaunchCommand(frozen), prompt: { kind: "task-file", file: "TASK.md" }, hooks: null, preflight: [{ check: "recipe", ok: true, detail: "frozen command; a selection is refused (E_LAUNCH_LEGACY): re-spawn it" }], ok: true });
776
779
  return;
777
780
  }
778
781
  const agentsRoot = agentsRootOfHome(home);
@@ -2324,8 +2327,8 @@ function retireCmd() {
2324
2327
  }
2325
2328
  // The calling instance knows its own home: self-retire never needs to
2326
2329
  // disambiguate a same-named twin by hand.
2327
- 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;
2328
- const isSelf = process.env.PI_AGENT_INSTANCE === name || process.env.OATS_INSTANCE === name;
2330
+ if (homeFlag === undefined && process.env.OATS_INSTANCE_HOME && process.env.OATS_INSTANCE === name) homeFlag = process.env.OATS_INSTANCE_HOME;
2331
+ const isSelf = process.env.OATS_INSTANCE === name;
2329
2332
  if (isSelf && !args.includes("--self")) die(`"${name}" is the calling instance — self-retire is irreversible; if your task is complete and you were told to retire, re-run with --self (finish your memory files FIRST; your session dies ~8s after)`);
2330
2333
  if (!isSelf && args.includes("--self")) die(`--self given but "${name}" is not the calling instance`);
2331
2334
  const root = ensureRoot(dirFlag());
@@ -2837,23 +2840,32 @@ async function capabilityCommand() {
2837
2840
  if (!hit) return NOT_DISPATCHED;
2838
2841
  // The same team/workspace facts a spawn hook receives (lead decision c3-7).
2839
2842
  const teamCtx = teamEnv(resolvedFromPrepared(hit.prepared, hit.deployment));
2840
- // No home, so no recorded soul: the soul's per-commit copy is OATS_SOUL when a spawn
2841
- // already fetched exactly this commit; otherwise the command gets none (never ambient).
2842
- // The agent directory is the soul entry's own (a package soul's is `<package>--<soul>`), never
2843
- // the bare name, which a same-named member soul's copy may occupy (as instance-inspect does).
2844
- const { agentDirOf } = await import("../lib/instance-resolution.mjs");
2845
- const entry = hit.prepared?.soulEntry;
2846
- const cachedSoul = entry?.commit ? join(hit.deployment, "agents", agentDirOf(entry), "souls", String(entry.commit).slice(0, 12)) : null;
2847
- return runManifestCommand({ capability: hit.module.name, ...hit.manifest }, { settings: hit.settings, origins: hit.resolution?.payloadOrigins?.[hit.module.name] }, teamCtx, hit.ensureTree, cachedSoul && existsSync(join(cachedSoul, "soul.yaml")) ? realpathSync(cachedSoul) : undefined);
2843
+ // No home, so no recorded soul: OATS_SOUL is the soul's source at the resolved commit, read
2844
+ // as a spawn preview reads it (the per-commit copy a spawn left under the agents root, else a
2845
+ // temporary fetch removed when the command ends). Read only when the command runs (after its
2846
+ // --help). A soul that cannot be read refuses the command: it never runs with OATS_SOUL unset.
2847
+ const soul = async () => {
2848
+ const { previewWorkspaceSoul } = await import("../lib/instance-resolution.mjs");
2849
+ const entry = hit.prepared?.soulEntry;
2850
+ try { const p = await previewWorkspaceSoul(hit.prepared, join(hit.deployment, "agents")); return { dir: p.soulDir, cleanup: p.cleanup }; }
2851
+ catch (e) {
2852
+ throw Object.assign(new Error(`oats ${cmd}: cannot read soul ${flag("soul")} at ${String(entry?.commit ?? "?").slice(0, 12)}, so the command would run without OATS_SOUL; nothing was run: ${e.message}`),
2853
+ { code: typeof e?.code === "string" && e.code.startsWith("E_") ? e.code : "E_REMOTE_UNREADABLE", details: { soul: flag("soul"), repoKey: entry?.repoKey ?? null, commit: entry?.commit ?? null } });
2854
+ }
2855
+ };
2856
+ return runManifestCommand({ capability: hit.module.name, ...hit.manifest }, { settings: hit.settings, origins: hit.resolution?.payloadOrigins?.[hit.module.name] }, teamCtx, hit.ensureTree, soul);
2848
2857
  }
2849
2858
 
2850
2859
  async function dispatch() {
2851
2860
  let activeIds;
2852
2861
  let context = process.cwd();
2853
2862
  let teamCtx, homeMeta, homeTeamCtx;
2854
- // OATS_INSTANCE_HOME is the canonical identity; the older names still count. With none set
2855
- // (a harness that strips the session env), the home enclosing the cwd.
2856
- const instanceHome = process.env.OATS_INSTANCE_HOME || process.env.PI_AGENT_HOME || process.env.OATS_HOME || enclosingInstanceHome(logicalCwd());
2863
+ // OATS_INSTANCE_HOME is the identity; OATS_HOME still counts. With neither set (a harness that
2864
+ // strips the session env), the home enclosing the cwd. What chose the home is named in every
2865
+ // refusal about it.
2866
+ const homeVariable = ["OATS_INSTANCE_HOME", "OATS_HOME"].find((name) => process.env[name]);
2867
+ const instanceHome = homeVariable ? process.env[homeVariable] : enclosingInstanceHome(logicalCwd());
2868
+ const chosenBy = homeVariable ?? "the working directory";
2857
2869
  const metaFile = instanceHome && join(instanceHome, "instance.json");
2858
2870
  // Capability-id keyed — never answer for `constructor`/`toString`. Belt and
2859
2871
  // braces: the ids come from instance.json, which spawn wrote from resolved
@@ -2872,11 +2884,19 @@ async function capabilityCommand() {
2872
2884
  if (!isWorkspaceHome(meta)) { const e = preWorkspaceHome(instanceHome, "nothing was dispatched"); bail(e.code, e.message); }
2873
2885
  context = meta.repo || context;
2874
2886
  soulDir = instanceSoulDir(instanceHome, meta);
2887
+ const ws = meta.workspace && typeof meta.workspace === "object" ? meta.workspace : {};
2888
+ // Inside a home the namespace is the home's, so a --soul for another soul is refused, never
2889
+ // ignored (as inspect's --soul against --home is). The home's own soul, by any of its names, is fine.
2890
+ const soulFlag = flag("soul");
2891
+ if (soulFlag === true) bail("E_BAD_ARGS", "--soul needs a soul name", { flag: "--soul" });
2892
+ if (typeof soulFlag === "string" && !(await import("../lib/instance-resolution.mjs")).homeSoulMatches(soulFlag, meta)) {
2893
+ bail("E_HOME_MISMATCH", `--soul ${soulFlag} is not the soul of the instance home ${instanceHome} (${meta.agent}), which ${chosenBy} chose; to run "${cmd}" as a spawn of ${soulFlag} would, run it outside the instance home with OATS_INSTANCE_HOME and OATS_HOME unset`,
2894
+ { home: instanceHome, soul: meta.agent, chosenBy, flag: "--soul" });
2895
+ }
2875
2896
  // The team/workspace facts the home recorded at spawn, as its hooks got them, with the
2876
2897
  // recorded eligible teams (OATS_TEAMS_SOURCE=recorded). Only the home's MESSAGING module
2877
2898
  // gets them live (below): its team verbs (join/leave/teams) must see what the workspace
2878
2899
  // allows now, and no other command pays a remote read for them.
2879
- const ws = meta.workspace && typeof meta.workspace === "object" ? meta.workspace : {};
2880
2900
  const messaging = (meta.capabilities || []).find((c) => c.layer === "messaging")?.id;
2881
2901
  homeMeta = { meta, messaging };
2882
2902
  homeTeamCtx = (t) => teamEnv({ workspace: { key: ws.key, name: ws.name, deployment: ws.deployment }, teams: t.teams, defaultTeam: t.defaultTeam, teamsSource: t.source });
@@ -2894,7 +2914,7 @@ async function capabilityCommand() {
2894
2914
  // Workspace model: an instance's own materialized modules are the command
2895
2915
  // namespaces available to it (instance.json.modules → <home>/.oats/modules).
2896
2916
  const mans = Object.values(capabilityManifests(instanceHome)).filter((m) => m.command === cmd && m.commands);
2897
- if (!mans.length) return NOT_DISPATCHED;
2917
+ if (!mans.length) bail("E_UNKNOWN_COMMAND", `oats ${cmd}: no capability of the instance home ${instanceHome} (soul ${homeMeta.meta.agent}), which ${chosenBy} chose, provides "${cmd}"`, { home: instanceHome, chosenBy, namespace: cmd });
2898
2918
  if (mans.length > 1) bail("E_DUPLICATE_NAMESPACE", `duplicate operational command namespace "${cmd}": ${mans.map((m) => m.capability).join(", ")}`);
2899
2919
  const m = mans[0];
2900
2920
  if (!activeIds.includes(m.capability)) bail("E_CAPABILITY_INACTIVE", `${m.capability} command namespace is not active in the current context/instance`);
@@ -2905,14 +2925,14 @@ async function capabilityCommand() {
2905
2925
  const live = await liveTeams(instanceHome, homeMeta.meta, { remoteOptions: remoteOptionsFromEnv() });
2906
2926
  teamCtx = homeTeamCtx(live);
2907
2927
  }
2908
- return runManifestCommand(m, { settings: capSettings[m.capability] || {}, origins: capOrigins[m.capability] }, teamCtx, () => m._dir, soulDir);
2928
+ return runManifestCommand(m, { settings: capSettings[m.capability] || {}, origins: capOrigins[m.capability] }, teamCtx, () => m._dir, () => ({ dir: soulDir, cleanup: () => {} }));
2909
2929
  }
2910
2930
 
2911
2931
  /** Help / unknown-command / spec validation / exec — shared by every context.
2912
2932
  * `m` is the manifest (with `capability`; `_dir` may be absent until `ensureDir`
2913
2933
  * resolves the directory holding the executable — the operator branch fetches
2914
2934
  * the module tree only when a command is actually going to run). */
2915
- async function runManifestCommand(m, { settings, origins }, teamCtx, ensureDir, soulDir) {
2935
+ async function runManifestCommand(m, { settings, origins }, teamCtx, ensureDir, soul) {
2916
2936
  const sub = args[1];
2917
2937
  const cmds = Object.keys(m.commands);
2918
2938
  // `oats <ns> --help` and `oats <ns> <cmd> --help` answer from the manifest
@@ -2946,6 +2966,11 @@ async function capabilityCommand() {
2946
2966
  try { abs = capabilityExecutablePath(withDir, script); }
2947
2967
  catch (e) { bail("E_CAPABILITY_BROKEN", e.message); }
2948
2968
  if (!abs) bail("E_CAPABILITY_BROKEN", `${cmd} ${sub}: script not found (${join(dir, script)})`);
2969
+ // The soul the command acts for → { dir, cleanup }: a home's recorded soul directory, or (operator
2970
+ // dispatch) the soul read at its resolved commit, whose temporary copy goes when this process exits.
2971
+ let soulDir;
2972
+ try { const read = await soul(); soulDir = read.dir; process.once("exit", read.cleanup); }
2973
+ catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) bail(e.code, e.message, e.details); throw e; }
2949
2974
  // OATS_SOUL is the recorded soul or nothing: an ambient value inherited from the
2950
2975
  // invoking process names some other soul (a coordinator's own), never this one.
2951
2976
  const { OATS_SOUL: _ambientSoul, ...inherited } = process.env;
@@ -533,7 +533,12 @@ passed as arguments; no shell is involved.
533
533
  - `OATS_SOUL`, the soul directory: a home's recorded one, or for a soul
534
534
  (`readiness --soul`, `inspect --soul`) its copy at the resolved commit,
535
535
  which the kernel materialises first as a spawn would (0.30; a copy that
536
- cannot be made fails readiness under `installed`, producer `soul copy`);
536
+ cannot be made fails readiness under `installed`, producer `soul copy`).
537
+ An operator command (`oats <namespace> … --soul <name>` from the
538
+ deployment) gets the soul's source at the resolved commit too: the copy a
539
+ spawn left under the agents root, else a temporary copy removed when the
540
+ command ends. A soul that cannot be read refuses the command; it never
541
+ runs without `OATS_SOUL`;
537
542
  - for a home, `OATS_INSTANCE` and `OATS_INSTANCE_HOME`.
538
543
  - `OATS_TEAM_SCOPE` is the deployment directory; `OATS_TEAM_NAME` is always
539
544
  empty.
@@ -723,7 +723,7 @@ Read-only (it writes no lock):
723
723
  "souls":["rm"],"capabilities":["nw-house-style"],"publishes":null,"url":"https://github.com/nw/agents/tree/66566512…",
724
724
  "membershipFile":{"path":"oats-membership.yaml","url":"https://github.com/nw/agents/blob/66566512…/oats-membership.yaml"}}],
725
725
  "packages":[{"id":"oats.okf","version":"3.0.0","source":"catalog:oats.okf","commit":"ab897841…","integrity":"sha256-bada35…",
726
- "capabilities":["oats.okf"],"souls":[],"latest":{"version":"4.0.5","ref":"v4.0.5"}}],
726
+ "capabilities":["oats.okf"],"souls":[],"latest":{"version":"4.0.6","ref":"v4.0.6"}}],
727
727
  "declaredPackages":["oats.framework","oats.okf"],"unsynced":["oats.framework"],"stale":[],
728
728
  "external":[{"source":"git:github.com/oss/experts@3c606e09…","soul":"security-reviewer"}],
729
729
  "problems":[],"warnings":[],
@@ -782,8 +782,8 @@ packages' capabilities and souls, sorted by name, then origin. Both carry
782
782
  "defaultTeam":{"label":"mine","team":"mine:ana.aweb.ai","from":"deployment"},
783
783
  "private":false,"path":"souls/writer","work":"directory","description":"Drafts campaigns.","harness":"pi","model":null,"harnessFrom":"kernel-default",
784
784
  "file":{"path":"souls/writer/soul.yaml","url":null},"spawnable":true,"problem":null},
785
- {"name":"knowledge-maintainer","qualifiedName":"oats.okf/knowledge-maintainer","origin":"package oats.okf v4.0.5","kind":"package","package":"oats.okf",
786
- "version":"4.0.5","repoKey":"github.com/awebai/oats-okf","commit":"26d8216f…","teams":null,"defaultTeam":null,"private":false,
785
+ {"name":"knowledge-maintainer","qualifiedName":"oats.okf/knowledge-maintainer","origin":"package oats.okf v4.0.6","kind":"package","package":"oats.okf",
786
+ "version":"4.0.6","repoKey":"github.com/awebai/oats-okf","commit":"2a62df8e…","teams":null,"defaultTeam":null,"private":false,
787
787
  "path":"oats-package/souls/knowledge-maintainer","work":"directory","description":"Reviews harvested knowledge.","harness":"pi","model":null,
788
788
  "harnessFrom":"kernel-default","file":{"path":"oats-package/souls/knowledge-maintainer/soul.yaml","url":null},
789
789
  "spawnable":false,"problem":{"code":"E_TEAM_UNKNOWN","message":"team \"reviewers\" is not declared (oats-local.yaml#/souls/teams/…)"}}],
@@ -872,8 +872,8 @@ nothing reads a working clone.
872
872
  **The show:**
873
873
 
874
874
  ```json
875
- {"capabilityShowApi":1,"name":"oats.okf","kind":"package","repoKey":"github.com/awebai/oats-okf","package":"oats.okf","version":"4.0.5",
876
- "commit":"26d8216f…","path":"oats-package/capabilities/oats-okf",
875
+ {"capabilityShowApi":1,"name":"oats.okf","kind":"package","repoKey":"github.com/awebai/oats-okf","package":"oats.okf","version":"4.0.6",
876
+ "commit":"2a62df8e…","path":"oats-package/capabilities/oats-okf",
877
877
  "inject":{"path":"injects/okf.md","bytes":2422,"text":"## Knowledge: OKF\n\nYou have two kinds of knowledge. …","binary":false,"truncated":false},
878
878
  "skills":[{"name":"okf-consultation","path":"skills/okf-consultation","description":"Consulting your soul's knowledge with the `oats okf` CLI: …",
879
879
  "files":[{"path":"skills/okf-consultation/SKILL.md","bytes":6947},{"path":"skills/okf-consultation/references/consult.md","bytes":4465}],
@@ -899,7 +899,7 @@ nothing reads a working clone.
899
899
  **The `--file` answer:**
900
900
 
901
901
  ```json
902
- {"capabilityShowApi":1,"name":"oats.okf","kind":"package","commit":"26d8216f…",
902
+ {"capabilityShowApi":1,"name":"oats.okf","kind":"package","commit":"2a62df8e…",
903
903
  "file":{"path":"skills/okf-instance-knowledge/SKILL.md","bytes":4787,"text":"---\nname: okf-instance-knowledge\n…","binary":false,"truncated":false}}
904
904
  ```
905
905
 
@@ -13,8 +13,8 @@ home. To run them on another machine, see [servers.md](servers.md).
13
13
  | Harness | `--harness` | Launched as |
14
14
  |---|---|---|
15
15
  | pi | `pi` (the default) | `pi --append-system-prompt <home>/AGENTS.md --approve --name <instance> [--model m] @TASK.md` |
16
- | Claude Code | `claude` | `claude [--model m] -- "$(cat TASK.md)"` |
17
- | Codex | `codex` | `codex --cd <home> [--model m] -- "$(cat TASK.md)"` |
16
+ | Claude Code | `claude` | `claude [--model m] -- '@TASK.md'` |
17
+ | Codex | `codex` | `codex --cd <home> [--model m] -- 'Read TASK.md in this directory first: it is your briefing and your task.'` |
18
18
 
19
19
  Each harness starts in the instance home with its own native settings,
20
20
  authentication and skill discovery. The command line also carries the
@@ -27,9 +27,9 @@ named launch configuration (`--launch-config <name>`); see
27
27
  [configuration.md](configuration.md#launch-configurations).
28
28
  `--model @native-default` uses the harness's own default model.
29
29
 
30
- The launch command sets `OATS_INSTANCE`, `OATS_INSTANCE_HOME`,
31
- `PI_AGENT_INSTANCE` and `PI_AGENT_HOME`, plus the environment that the launch
32
- configuration and capabilities contribute. The home's layout and what those
30
+ The launch command sets `OATS_INSTANCE` and `OATS_INSTANCE_HOME`, plus the
31
+ environment that the launch configuration and capabilities contribute. No
32
+ `PI_AGENT_*` name is set, for any harness. The home's layout and what those
33
33
  variables point at are described in
34
34
  [souls-and-instances.md](souls-and-instances.md#instance-anatomy).
35
35
 
@@ -166,6 +166,18 @@ and `restart`, opens a new harness conversation on `TASK.md`. The instance
166
166
  resumes its work from its own state files, as its knowledge capability
167
167
  prescribes.
168
168
 
169
+ The task's text never travels on a command line, where any local user could
170
+ read it in the process list:
171
+
172
+ - pi and Claude Code get `@TASK.md`, which each harness reads as the file.
173
+ - Codex gets a fixed pointer to the file and reads it with a tool.
174
+ - A home whose recorded command still hands over `"$(cat TASK.md)"` starts
175
+ with its harness's safe prompt instead, and the command is saved that way.
176
+
177
+ The home is created `0700` and `TASK.md` `0600`. `oats doctor` reports an
178
+ instance home that other users can read (`home-readable`), with the exact
179
+ `chmod`; it never changes a home's mode itself.
180
+
169
181
  ## Permissions (yolo)
170
182
 
171
183
  Yolo is chosen per launch: `--yolo` / `--no-yolo` on `oats spawn`,
@@ -72,6 +72,27 @@ Every CLI command owns one read session (`createReadSession` in
72
72
  CLI closes it when the command ends). A library caller without a session
73
73
  gets the plain per-call behaviour. Within a session:
74
74
 
75
+ - a HEAD observation speaks protocol v0 when the operator has not pinned
76
+ `protocol.version` (`git config --get`, read once per command): the whole
77
+ ref advertisement in one round trip, resolved exactly as v2's filtered
78
+ answer. `V0_ADVERTISEMENT_BUDGET` (4 MiB, git's `maxBuffer`) bounds what
79
+ is kept, not the transfer: git reads the whole advertisement before it
80
+ prints a ref, so a remote over budget costs its advertisement once, then
81
+ git is killed, the remote observed again under v2 and recorded. A v0
82
+ timeout stays today's error (no retry) and is recorded too, unless the
83
+ session's `deadline` cut that read's timeout (the deadline, not the remote,
84
+ may have ended it). The
85
+ v0 read and its `protocol.version` check go through `sessionExec` like every
86
+ other git call. The record is
87
+ `<cacheRoot>/.ls-remote/<sha256(key)>.<reason>.json`, `{ protocol: "v2",
88
+ reason: "overflow" | "timeout", recordedAt }`, one file per reason (an
89
+ in-flight timeout never replaces an overflow), written atomically with no lock;
90
+ `overflow` is permanent, `timeout` expires after 7 days, and an unreadable,
91
+ corrupt or expired record is no record (v0 is tried). Another v0 failure
92
+ that is not final (auth, not-found, cache, an abort) is retried once under
93
+ v2. Each is a session notice. The v2 argv (`lsRemoteArgs`) stays the
94
+ observation's identity: records and memo keys do not depend on the
95
+ protocol;
75
96
  - a head is observed once per (cache repo, ref), and a commit peeled once; at
76
97
  most eight observations run at once (`OBSERVE_LIMIT`), each holding its slot
77
98
  for all its git work (the `ls-remote` and the fetch of the commit it names,
@@ -107,7 +128,8 @@ gets the plain per-call behaviour. Within a session:
107
128
  exit hook (`closeNow`), and the system reaps them once the process is gone;
108
129
  - a session may have a `deadline` (`READ_REMOTE_BUDGET_MS`, 12 s after it
109
130
  starts): the CLI gives one to `status` and `workspace status` only
110
- (`readBudgetMs`; `OATS_READ_REMOTE_BUDGET_MS` overrides it for tests). Every
131
+ (`readBudgetMs`; `OATS_READ_REMOTE_BUDGET_MS` is a test and ops override,
132
+ not a contract). Every
111
133
  remote step then gets what is left of it instead of its own default: each
112
134
  git call's timeout (`sessionExec`: ls-remote, fetch, ls-tree, the cache's
113
135
  plumbing; none starts once nothing is left), the git version probe
@@ -203,7 +225,7 @@ scaling by call count.
203
225
  | `npm test` | every suite under `test/` (`node --test`), through `scripts/run-tests.mjs` |
204
226
  | `npm run check` | syntax of every shipped file |
205
227
  | `npm run check:pi` | the pi adapter's TypeScript |
206
- | `npm run validate` | the JSON schemas, the example manifests and configs, and every local link and anchor in the public docs |
228
+ | `npm run validate` | the JSON schemas, the example manifests and configs, every local link and anchor in the public docs, the repository's own `oats-workspace.yaml` and `package-catalog.json` read by the kernel's readers, and no unresolved merge-conflict marker in a tracked text file (`scripts/check-workspace-files.mjs`) |
207
229
  | `npm run pack:check` | an `npm pack` dry run of both packages: nothing missing, nothing leaked |
208
230
  | `npm run smoke:tarball` | installs the packed tarballs outside the checkout and exercises them |
209
231
 
@@ -40,8 +40,8 @@ arrives from.
40
40
  ```yaml
41
41
  # oats-workspace.yaml: one default per slot, for every soul
42
42
  packages:
43
- oats.okf: v4.0.5
44
- oats.aweb: v1.17.5
43
+ oats.okf: v4.0.6
44
+ oats.aweb: v1.17.6
45
45
  oats.linear: v1.0.1
46
46
  oats.jira: v1.0.1
47
47
  defaults:
package/docs/knowledge.md CHANGED
@@ -26,7 +26,7 @@ The workspace pins the package and fills the slot for every soul by default:
26
26
  ```yaml
27
27
  # oats-workspace.yaml (excerpt)
28
28
  packages:
29
- oats.okf: v4.0.5
29
+ oats.okf: v4.0.6
30
30
  defaults:
31
31
  knowledge: { oats.okf: { from: package } }
32
32
  stores:
@@ -220,10 +220,13 @@ identical copies in both role capabilities.
220
220
  - **From an instance home**, `oats okf …` runs the home's copy of the module
221
221
  with the settings recorded at spawn.
222
222
  - **From the deployment directory** (holding `oats-local.yaml`), in a shell
223
- without another instance's `OATS_*` identity, every capability command
224
- needs `--soul <name>` (`E_BAD_ARGS` without it). The kernel resolves the
223
+ where neither `OATS_INSTANCE_HOME` nor `OATS_HOME` is set (either one pins
224
+ the command to that instance home), every capability command needs
225
+ `--soul <name>` (`E_BAD_ARGS` without it). Inside an instance home, a
226
+ `--soul` naming another soul is refused (`E_HOME_MISMATCH`). The kernel resolves the
225
227
  soul as a spawn would, fetches its module at the locked commit into
226
- `<deployment>/.oats/modules/` and runs it with the soul's merged settings.
228
+ `<deployment>/.oats/modules/` and runs it with the soul's merged settings
229
+ and `OATS_SOUL`, the soul's source at that commit.
227
230
  An unlocked package is `E_PACKAGE_MISSING` until `oats sync`.
228
231
 
229
232
  ### Consult
@@ -10,9 +10,9 @@ or workspace membership alone does not make a package official.
10
10
  | package | release | capabilities | package souls |
11
11
  |---|---|---|---|
12
12
  | `oats.framework` | `oats-framework/v1.4.1` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
13
- | `oats.okf` | `v4.0.5` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
14
- | `oats.aweb` | `v1.17.5` | `oats.aweb` (messaging) | |
15
- | `oats.engineering` | `v1.4.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
13
+ | `oats.okf` | `v4.0.6` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
14
+ | `oats.aweb` | `v1.17.6` | `oats.aweb` (messaging) | |
15
+ | `oats.engineering` | `v1.5.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
16
16
  | `oats.authoring` | `v1.0.3` | `oats.authoring` | |
17
17
  | `oats.jira` | `v1.0.1` | `oats.jira` (tasks) | |
18
18
  | `oats.linear` | `v1.0.1` | `oats.linear` (tasks) | |
@@ -27,7 +27,7 @@ no lock and adds nothing to an existing workspace.
27
27
  ## Find and use packages
28
28
 
29
29
  - A workspace pins an official package by **bare version** in its
30
- `packages:` map (`oats.okf: v4.0.5`); `oats sync` resolves it through the
30
+ `packages:` map (`oats.okf: v4.0.6`); `oats sync` resolves it through the
31
31
  catalog to an exact commit, fetches it, verifies its integrity and locks it.
32
32
  A package outside the catalog is written `git:<repo>@<ref>`. Pinning does
33
33
  not join a team or adopt the publisher's workspace. See
package/docs/packages.md CHANGED
@@ -44,14 +44,14 @@ whole organisation:
44
44
 
45
45
  ```yaml
46
46
  packages:
47
- oats.okf: v4.0.5 # bare version → the official catalog
47
+ oats.okf: v4.0.6 # bare version → the official catalog
48
48
  acme.tools: git:github.com/acme/tools@v0.4.0 # direct ref: git:<repo>@<tag or full OID>
49
49
  ```
50
50
 
51
- - **Bare version** (`v4.0.5`, `4.0.5`, `1.0.0-rc.1`): the id is looked up in
51
+ - **Bare version** (`v4.0.6`, `4.0.6`, `1.0.0-rc.1`): the id is looked up in
52
52
  the official catalog — `package-catalog.json` in the `oats` repo, or the file
53
53
  named by `OATS_PACKAGE_CATALOG` — which supplies the repo url, the tag
54
- convention (`v4.0.5` or `oats-framework/v1.4.1`) and the payload path. An id
54
+ convention (`v4.0.6` or `oats-framework/v1.4.1`) and the payload path. An id
55
55
  the catalog does not know is `E_PACKAGE_MISSING` ("use `git:<repo>@<ref>` for
56
56
  a package outside the catalog"). The catalog is the reviewed official list
57
57
  ([official-catalog.md](official-catalog.md)) and the only way a
@@ -75,8 +75,8 @@ members:
75
75
  - git:github.com/acme/platform
76
76
  packages:
77
77
  oats.framework: v1.4.1
78
- oats.okf: v4.0.5
79
- oats.aweb: v1.17.5
78
+ oats.okf: v4.0.6
79
+ oats.aweb: v1.17.6
80
80
  teams:
81
81
  platform: { team: "platform:acme.aweb.ai", description: Platform engineering }
82
82
  defaults:
@@ -105,7 +105,7 @@ decision recorded in the lock.
105
105
  $ oats sync
106
106
  workspace acme (github.com/acme/agents @ 3f2a9c1e)
107
107
  members agents ✓↔ (@ 3f2a9c1e) platform ✓↔ (@ 77c0a1b2) billing ✗ (no-backlink)
108
- packages acme.tools 0.4.0 ✓ (@ 47f4b816) oats.okf 4.0.5 ✓ (@ 26d8216f)
108
+ packages acme.tools 0.4.0 ✓ (@ 47f4b816) oats.okf 4.0.6 ✓ (@ 2a62df8e)
109
109
  changed acme.tools — → 0.4.0 (@ 47f4b816)
110
110
  souls 9 discovered (6 members, 1 external, 2 package, 0 disabled here) · 0 private capabilities
111
111
  teams platform (shared) · this deployment's: oats teams
@@ -134,7 +134,7 @@ Declaring a package in the workspace's `packages:` is the trust decision
134
134
  ## `oats package add | remove`
135
135
 
136
136
  ```bash
137
- oats package add oats.aweb v1.17.5 # a catalog version
137
+ oats package add oats.aweb v1.17.6 # a catalog version
138
138
  oats package add acme.tools git:github.com/acme/tools@v0.4.0
139
139
  oats package remove acme.tools
140
140
  ```
@@ -160,8 +160,8 @@ same workspace commit hold identical locks.
160
160
  "source": "catalog:oats.okf",
161
161
  "url": "https://github.com/awebai/oats-okf.git",
162
162
  "path": "oats-package",
163
- "version": "4.0.5",
164
- "commit": "26d8216f8ce986cad1ffa7a8291b4a4978510ee2",
163
+ "version": "4.0.6",
164
+ "commit": "2a62df8eb247b58b3d89b2ce38d84c49c2d8ae23",
165
165
  "integrity": "sha256-…",
166
166
  "capabilities": ["oats.okf", "oats.okf-harvest", "oats.okf-maintenance"]
167
167
  },
@@ -331,7 +331,7 @@ A soul that names one of the package's capabilities with
331
331
  {
332
332
  "policy": "docs/official-catalog.md",
333
333
  "packages": {
334
- "oats.okf": { "url": "https://github.com/awebai/oats-okf.git", "ref": "v4.0.5", "path": "oats-package" },
334
+ "oats.okf": { "url": "https://github.com/awebai/oats-okf.git", "ref": "v4.0.6", "path": "oats-package" },
335
335
  "oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.4.1", "path": "oats-package" }
336
336
  }
337
337
  }
@@ -0,0 +1,57 @@
1
+ # OATS 0.34.1
2
+
3
+ ## Changed
4
+
5
+ - **Reading a remote's current head takes one round trip instead of two.**
6
+ `oats spawn` (preview and apply) and the read verbs ask each workspace
7
+ member for its head with Git's protocol v0, which returns the whole ref list
8
+ in one request, where protocol v2 takes two. On a 6-member deployment the
9
+ live phase of a spawn preview went from 0.77 s to 0.49 s (median of 5). Tags
10
+ and branches are read as before. If your Git configuration sets
11
+ `protocol.version`, OATS uses it as set. A remote whose ref list is over
12
+ 4 MiB is read with protocol v2 from then on (remembered in the remote
13
+ cache). A remote that times out under v0 fails as before and is read with
14
+ v2 for the next 7 days; a read ended by the 12 s budget of `oats status` or
15
+ `oats workspace status` is not counted as the remote's timeout. One that
16
+ fails under v0 for another reason than an
17
+ authentication refusal, a missing repository or the local cache is read
18
+ again with v2. Each case is one `oats: warning`. What a read observes is
19
+ unchanged, and an apply still observes every member live. Known limit: the
20
+ 4 MiB bounds what OATS keeps, not what Git downloads, so a remote with a
21
+ larger ref list transfers it once before OATS switches to protocol v2.
22
+ - **oats.engineering 1.5.0** (catalog and workspace pin, and the bundled
23
+ mirrors): developers keep each dynamic workflow under 10 agents and ask
24
+ their human for permission first for more. Within the cap they run
25
+ workflows without asking where the harness allows it; where it requires the
26
+ human's opt-in (Claude Code's workflow tool does), they use a standing
27
+ opt-in the human configured, or ask once per task.
28
+ - **The `oats-setup-admin` soul has knowledge and is harvested.** It drops
29
+ `oats.developer` and `knowledge: none` and takes the workspace's oats.okf
30
+ knowledge slot, owning the new `oats/oats-setup-admin` node (the judgement
31
+ of administering a workspace's config, never deployment state) and reading
32
+ the operator, expert and kernel nodes. This reverses the 0.29.1 opt-out:
33
+ deployment specifics are kept out of the base at promotion, by the node's
34
+ charter and the reviewed harvest PR, instead of by not harvesting the soul.
35
+ It needs `oats/oats-setup-admin` in the bound `oats` base
36
+ (awebai/oats-knowledge#51); new instances only.
37
+
38
+ - **`PI_AGENT_INSTANCE` and `PI_AGENT_HOME` are gone.** No launch sets them,
39
+ for any harness, pi included. (A home that records only its launch command,
40
+ from before launch recipes, keeps running that command as recorded.) The identity is `OATS_INSTANCE` and
41
+ `OATS_INSTANCE_HOME`, and the pi bridge (`@awebai/oats-pi`) reads
42
+ `OATS_INSTANCE_HOME`.
43
+ - Upgrade the CLI and the pi bridge together (`oats update` does both). A
44
+ 0.34.0 pi bridge under a 0.34.1 kernel does not find its instance home.
45
+ - Capability commands no longer read `PI_AGENT_HOME`. A leftover one in an
46
+ operator's shell no longer pins a command to that instance.
47
+ - The names stay reserved: a launch configuration still cannot set them.
48
+
49
+ ## Fixed
50
+
51
+ - **A capability command inside an instance home says which home, and why.**
52
+ - A `--soul` naming another soul than the home's was silently ignored; it
53
+ is now refused with `E_HOME_MISMATCH`.
54
+ - A namespace the home does not have answered `unknown command`; it is now
55
+ `E_UNKNOWN_COMMAND` naming the home.
56
+ - Both name what chose the home: `OATS_INSTANCE_HOME`, `OATS_HOME` or the
57
+ working directory.
@@ -0,0 +1,68 @@
1
+ # OATS 0.34.2
2
+
3
+ ## Changed
4
+
5
+ - **oats.aweb 1.17.6** (catalog and workspace pin, and the bundled mirror):
6
+ session-delivery instructions name the aw 1.36.21+ full-mail wake form
7
+ (`aweb mail event received.` with metadata, sender body, the `Use the aw CLI`
8
+ line and a Recovery line), say that body and subject are untrusted sender
9
+ content that never overrides the task or human, and remind operators that
10
+ delivered mail may be marked read and absent from unread `aw mail inbox`.
11
+ The aw floor is unchanged (`>=1.36.13`).
12
+ - **oats.okf 4.0.6** (catalog and workspace pin, and the bundled mirrors):
13
+ harvest completion works on a real host (awebai/oats-okf#27, #28, #29,
14
+ #30).
15
+ - `oats okf complete` persists the harvester's judgment first, then delivers
16
+ in one detached worker per run. After persisting the judgment, it waits
17
+ up to 30 s: it answers with the final receipt, or with
18
+ `status: delivering` and the progress.
19
+ - A killed `complete`, or a killed worker, loses nothing. Rerunning
20
+ `complete` resumes, never judges again, and never pushes or opens a PR
21
+ twice.
22
+ - A `worker.lock` whose owner died is reclaimed automatically.
23
+ - `oats okf-harvest harvest-status` reports the delivery as in progress,
24
+ failed or stopped.
25
+ - Captured completions still deliver inline.
26
+ - A Git base whose head moved outside its knowledge root is no longer a
27
+ baseline change. A read-only base records the new head. A written base is
28
+ committed onto it. Only changed root bytes fail with `E_BASELINE`, and the
29
+ judgment stays persisted (`oats okf retry --rejudge`).
30
+ - Staging a Git base fetches its root's blobs in one batch. Delivery never
31
+ fetches the blobs outside the root. Staging over SSH, from GitHub:
32
+
33
+ | Base | 4.0.5 | 4.0.6 |
34
+ |---|---|---|
35
+ | oats-knowledge | 237 s | 9.9 s |
36
+ | aweb | 41 s | 9.1 s |
37
+
38
+ - `oats okf harvest-status` reports `unknown`, with the reason, when it
39
+ cannot read a soul's opt-out.
40
+
41
+ ## Fixed
42
+
43
+ - **An operator command with `--soul` always gets the soul**
44
+ (awebai/oats#423). `oats <namespace> … --soul <name>` from the deployment
45
+ passed `OATS_SOUL` only when a spawn had already fetched that soul at the
46
+ resolved commit, so `oats okf harvest-status --soul <s>` reported harvest
47
+ "off" for a soul no spawn had fetched yet. The soul is now read at the
48
+ resolved commit as a spawn preview reads it: the copy a spawn left, else a
49
+ temporary copy removed when the command ends. A soul that cannot be read
50
+ refuses the command; it never runs without `OATS_SOUL`.
51
+
52
+ - **A task's text no longer appears in the process list** (awebai/oats#427).
53
+ Claude Code and Codex instances were launched with the whole `TASK.md` as a
54
+ command-line argument, which any local user can read. Now:
55
+ - Claude Code gets `@TASK.md` (it reads the file).
56
+ - Codex gets a fixed instruction to read `TASK.md`.
57
+ - A home recorded the old way starts with the safe prompt, and its saved
58
+ command is updated.
59
+ - New instance homes are created `0700` and `TASK.md` `0600`.
60
+ - `oats doctor` reports a home other users can read (`home-readable`) with
61
+ the exact `chmod`. Existing homes are not changed: run that `chmod`.
62
+
63
+ - **`npm run validate` catches a broken workspace file** (a development
64
+ check). It reads the repository's own `oats-workspace.yaml` and
65
+ `package-catalog.json` with the kernel's readers, and fails on an unresolved
66
+ merge-conflict marker in any tracked text file, naming the file and the
67
+ line. A conflicted `oats-workspace.yaml` had passed check, validate, pack
68
+ and smoke. The kernel's YAML and catalog errors now name the line too.