@awebai/oats 0.34.1 → 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,
@@ -583,7 +583,8 @@ function legacyLayoutProblems(root) {
583
583
  }
584
584
  async function doctorWorkspaceJson(ctx, soulName, ws) {
585
585
  const composition = await doctorComposition(ctx, soulName, ws, (code, msg, details) => jsonFail(code, msg, details));
586
- 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);
587
588
  return {
588
589
  schemaVersion: 1, workspaceApi: 2, context: ctx,
589
590
  workspace: { file: ws.local.path, ref: ws.local.workspace },
@@ -622,7 +623,8 @@ async function doctor(dir) {
622
623
  doctorVersionSkew();
623
624
  const composition = await doctorComposition(ctx, soulName, ws, (code, msg) => die(`${msg} [${code}]`));
624
625
  printDoctorWorkspace(ws);
625
- 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}`);
626
628
  if (soulName) {
627
629
  const information = operationalKnowledgeNote(composition, soulName);
628
630
  if (information) console.log(`\nINFO: ${information}`);
@@ -769,11 +771,11 @@ function launchPreview(bail) {
769
771
  instance = meta.instance || basename(home);
770
772
  if (!(meta.launch && typeof meta.launch === "object") && !selectionGiven) {
771
773
  // A home that predates recipes, asked nothing: its frozen command is
772
- // described as is. Under a selection the planner refuses it
773
- // (E_LAUNCH_LEGACY: re-spawn it from the deployment).
774
- let d;
775
- try { d = describeLaunchCommand(meta.command); } catch (e) { bail(e.code || "E_LAUNCH_COMMAND_UNSUPPORTED", e.message); }
776
- 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 });
777
779
  return;
778
780
  }
779
781
  const agentsRoot = agentsRootOfHome(home);
@@ -2838,14 +2840,20 @@ async function capabilityCommand() {
2838
2840
  if (!hit) return NOT_DISPATCHED;
2839
2841
  // The same team/workspace facts a spawn hook receives (lead decision c3-7).
2840
2842
  const teamCtx = teamEnv(resolvedFromPrepared(hit.prepared, hit.deployment));
2841
- // No home, so no recorded soul: the soul's per-commit copy is OATS_SOUL when a spawn
2842
- // already fetched exactly this commit; otherwise the command gets none (never ambient).
2843
- // The agent directory is the soul entry's own (a package soul's is `<package>--<soul>`), never
2844
- // the bare name, which a same-named member soul's copy may occupy (as instance-inspect does).
2845
- const { agentDirOf } = await import("../lib/instance-resolution.mjs");
2846
- const entry = hit.prepared?.soulEntry;
2847
- const cachedSoul = entry?.commit ? join(hit.deployment, "agents", agentDirOf(entry), "souls", String(entry.commit).slice(0, 12)) : null;
2848
- 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);
2849
2857
  }
2850
2858
 
2851
2859
  async function dispatch() {
@@ -2917,14 +2925,14 @@ async function capabilityCommand() {
2917
2925
  const live = await liveTeams(instanceHome, homeMeta.meta, { remoteOptions: remoteOptionsFromEnv() });
2918
2926
  teamCtx = homeTeamCtx(live);
2919
2927
  }
2920
- 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: () => {} }));
2921
2929
  }
2922
2930
 
2923
2931
  /** Help / unknown-command / spec validation / exec — shared by every context.
2924
2932
  * `m` is the manifest (with `capability`; `_dir` may be absent until `ensureDir`
2925
2933
  * resolves the directory holding the executable — the operator branch fetches
2926
2934
  * the module tree only when a command is actually going to run). */
2927
- async function runManifestCommand(m, { settings, origins }, teamCtx, ensureDir, soulDir) {
2935
+ async function runManifestCommand(m, { settings, origins }, teamCtx, ensureDir, soul) {
2928
2936
  const sub = args[1];
2929
2937
  const cmds = Object.keys(m.commands);
2930
2938
  // `oats <ns> --help` and `oats <ns> <cmd> --help` answer from the manifest
@@ -2958,6 +2966,11 @@ async function capabilityCommand() {
2958
2966
  try { abs = capabilityExecutablePath(withDir, script); }
2959
2967
  catch (e) { bail("E_CAPABILITY_BROKEN", e.message); }
2960
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; }
2961
2974
  // OATS_SOUL is the recorded soul or nothing: an ambient value inherited from the
2962
2975
  // invoking process names some other soul (a coordinator's own), never this one.
2963
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
@@ -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`,
@@ -225,7 +225,7 @@ scaling by call count.
225
225
  | `npm test` | every suite under `test/` (`node --test`), through `scripts/run-tests.mjs` |
226
226
  | `npm run check` | syntax of every shipped file |
227
227
  | `npm run check:pi` | the pi adapter's TypeScript |
228
- | `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`) |
229
229
  | `npm run pack:check` | an `npm pack` dry run of both packages: nothing missing, nothing leaked |
230
230
  | `npm run smoke:tarball` | installs the packed tarballs outside the checkout and exercises them |
231
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:
@@ -225,7 +225,8 @@ identical copies in both role capabilities.
225
225
  `--soul <name>` (`E_BAD_ARGS` without it). Inside an instance home, a
226
226
  `--soul` naming another soul is refused (`E_HOME_MISMATCH`). The kernel resolves the
227
227
  soul as a spawn would, fetches its module at the locked commit into
228
- `<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.
229
230
  An unlocked package is `E_PACKAGE_MISSING` until `oats sync`.
230
231
 
231
232
  ### Consult
@@ -10,8 +10,8 @@ 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) | |
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
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) | |
@@ -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,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.
@@ -129,7 +129,7 @@ full copy** of every capability the soul resolved to:
129
129
  .oats/modules/<capability>/ # the whole capability: oats.json, bin/, injects/, skills/ (hooks run from here)
130
130
  .oats/bin/oats → <kernel>/bin/oats.mjs # the kernel that last launched this home: first on the harness's PATH
131
131
  work/ # worktree, checkout symlink, attached tree, or private directory
132
- TASK.md # briefing and task
132
+ TASK.md # briefing and task (0600; the home itself is 0700)
133
133
  instance.json # provenance (below); `soulDir` = the soul directory hooks get as OATS_SOUL
134
134
  STATE.md, log.md, notes/ # optional, from the knowledge capability
135
135
  ```
@@ -152,8 +152,8 @@ composed skills and instructions, a spawn records:
152
152
  "commit": "3f2a9c1e…", "digest": "sha256-…", "materializedAt": "2026-09-24T10:12:44.118Z"
153
153
  },
154
154
  "oats.okf": {
155
- "from": { "kind": "package", "package": "oats.okf", "version": "4.0.5", "commit": "26d8216f…", "integrity": "sha256-…", "repoKey": "github.com/awebai/oats-okf" },
156
- "commit": "26d8216f…", "digest": "sha256-…", "materializedAt": "2026-09-24T10:12:44.201Z"
155
+ "from": { "kind": "package", "package": "oats.okf", "version": "4.0.6", "commit": "2a62df8e…", "integrity": "sha256-…", "repoKey": "github.com/awebai/oats-okf" },
156
+ "commit": "2a62df8e…", "digest": "sha256-…", "materializedAt": "2026-09-24T10:12:44.201Z"
157
157
  }
158
158
  },
159
159
  "providers": {
@@ -55,7 +55,7 @@ members: # repo refs, NO @revision (E_WORKSPAC
55
55
 
56
56
  packages: # the ONLY versioned things
57
57
  oats.framework: v1.4.1 # bare version → resolves through the official catalog
58
- oats.okf: v4.0.5
58
+ oats.okf: v4.0.6
59
59
  acme.tools: git:github.com/acme/tools@v0.4.0 # outside the catalog → git:<repo>@<tag|OID>; still a package
60
60
 
61
61
  teams: # SHARED teams: the same provider team for everyone
@@ -103,6 +103,8 @@ export function canonicalJson(value, options) {
103
103
  return chunks.join("");
104
104
  }
105
105
 
106
+ /** The 1-based line of character `offset` in `text`: where a reader's error points. */
107
+ export function lineAt(text, offset) { return text.slice(0, offset).split("\n").length; }
106
108
  /** JSON.parse alone cannot detect duplicate decoded keys. This small JSON
107
109
  * decoder preserves that check and budgets; YAML must use it for JSON input,
108
110
  * not maintain another permissive JSON reader. Objects have null prototypes. */
@@ -114,7 +116,7 @@ export function parseStrictJson(input, options) {
114
116
  }
115
117
  const text = decodeUtf8(data);
116
118
  let at = 0, entries = 0;
117
- const bad = () => { throw oatsError("invalid-declaration", `invalid JSON at character ${at}`); };
119
+ const bad = () => { throw oatsError("invalid-declaration", `invalid JSON at character ${at}`, { offset: at }); };
118
120
  const space = () => { while (at < text.length && /[\x20\t\r\n]/.test(text[at])) at++; };
119
121
  const string = () => {
120
122
  const start = at++;
@@ -3,7 +3,7 @@
3
3
  * Legacy core readers are unchanged until the explicit consumer migration. */
4
4
  import { Composer, CST, Lexer, Parser, isAlias, isMap, isScalar, isSeq } from "yaml";
5
5
  import { bytesIntegrity } from "./digest.mjs";
6
- import { byteView, canonicalJson, dataLimits, decodeUtf8, parseStrictJson } from "./canonical-json.mjs";
6
+ import { byteView, canonicalJson, dataLimits, decodeUtf8, lineAt, parseStrictJson } from "./canonical-json.mjs";
7
7
  import { oatsError } from "./errors.mjs";
8
8
 
9
9
  const pointerKey = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
@@ -71,7 +71,9 @@ export function parseConfigData(input, { format = "auto", origin = null, limits:
71
71
  }
72
72
  if (document.errors.length || document.warnings.length || document.directives.yaml.version !== "1.2") {
73
73
  const issue = document.errors[0] ?? document.warnings[0];
74
- throw oatsError("invalid-declaration", `invalid portable YAML${issue ? ` (${issue.code})` : " version"}`);
74
+ const offset = issue?.pos?.[0];
75
+ const line = Number.isInteger(offset) ? lineAt(source, offset) : undefined;
76
+ throw oatsError("invalid-declaration", `invalid portable YAML${issue ? ` (${issue.code}${line ? ` at line ${line}` : ""})` : " version"}`);
75
77
  }
76
78
  const decode = (node, pointer, depth) => {
77
79
  mark(pointer, depth, node);
package/lib/core.mjs CHANGED
@@ -63,7 +63,7 @@ import { validateBindingInterface } from "./provider-binding.mjs";
63
63
  /** Retirement/rollback tree fingerprint (exported for scope tests: kernel-field neutrality is opt-in per instance home). */
64
64
  export { fingerprintTree };
65
65
 
66
- import { canonicalJson, parseStrictJson } from "./canonical-json.mjs";
66
+ import { canonicalJson, lineAt, parseStrictJson } from "./canonical-json.mjs";
67
67
  import { readPortableBytes } from "./bounded-read.mjs";
68
68
  import { copyTreeSafe } from "./tree-copy.mjs";
69
69
  /** The package and capability id grammar (namespaced, lowercase): an id names a
@@ -89,6 +89,10 @@ function capabilityAgentDirs(root) {
89
89
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !RESERVED.has(e.name) && !existsSync(join(root, e.name, "soul", "soul.yaml")))
90
90
  .map((e) => ({ name: e.name, dir: join(root, e.name) }));
91
91
  }
92
+ /** The names of the non-hidden directories in `d` ([] when it cannot be read). */
93
+ function subdirs(d) {
94
+ try { return readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name); } catch { return []; }
95
+ }
92
96
  /** What OATS 0.25 left under <scope>/local-agents/ (and the nested <root>/local-agents|
93
97
  * tmp-agents) for the agents root `root`: one `legacy-local-agents` problem naming the
94
98
  * instance homes there, or null. Names only — nothing there is read, spawned into or
@@ -97,7 +101,6 @@ export function legacyLocalAgents(root) {
97
101
  if (!root) return null;
98
102
  const bases = [join(dirname(root), LEGACY_LOCAL_AGENTS_DIR), join(root, LEGACY_LOCAL_AGENTS_DIR), join(root, "tmp-agents")];
99
103
  const dirs = [], instances = [];
100
- const subdirs = (d) => { try { return readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name); } catch { return []; } };
101
104
  for (const base of bases) {
102
105
  let st; try { st = lstatSync(base); } catch { continue; }
103
106
  if (!st.isDirectory()) continue;
@@ -109,12 +112,32 @@ export function legacyLocalAgents(root) {
109
112
  return { code: "legacy-local-agents", dirs, instances,
110
113
  message: `${instances.length} instance home${instances.length === 1 ? "" : "s"} under local-agents/ ${instances.length === 1 ? "is" : "are"} from OATS 0.25 and ${instances.length === 1 ? "is" : "are"} not managed by this kernel; retire ${instances.length === 1 ? "it" : "them"} with the 0.25 kernel or delete the directory once ${instances.length === 1 ? "it is" : "they are"} stopped${instances.length ? ` (${instances.join(", ")})` : ""}` };
111
114
  }
115
+ /** The instance homes under the agents root `root` (a directory <root>/<agent>/instances/<name>
116
+ * whose instance.json records that name) that other users on the machine can read or enter (any
117
+ * group or other permission bit): one `home-readable` problem naming them, with the exact chmod,
118
+ * or null. A home holds identity keys, TASK.md and transcripts; spawn creates it 0700. Only
119
+ * reported: the operator decides. */
120
+ export function readableInstanceHomes(root) {
121
+ if (!root) return null;
122
+ const homes = [];
123
+ for (const agent of subdirs(root)) for (const name of subdirs(join(root, agent, "instances"))) {
124
+ const home = join(root, agent, "instances", name);
125
+ try {
126
+ if (JSON.parse(readFileSync(join(home, "instance.json"), "utf8"))?.instance !== name) continue;
127
+ if (lstatSync(home).mode & 0o077) homes.push(home);
128
+ } catch { /* no readable instance.json, or gone meanwhile: not a home */ }
129
+ }
130
+ if (!homes.length) return null;
131
+ homes.sort();
132
+ const fix = `chmod 700 ${homes.map(shq).join(" ")}`;
133
+ return { code: "home-readable", homes, fix,
134
+ message: `${homes.length} instance home${homes.length === 1 ? " can" : "s can"} be read by other users on this machine (${homes.length === 1 ? "it holds" : "they hold"} identity keys, TASK.md and transcripts); fix: ${fix}` };
135
+ }
112
136
  /** The captured homes (the 0.24–0.25 captured/portable path, removed in 0.26) under the
113
137
  * agents root `root`: one `legacy-captured-home` problem naming them, or null. They have
114
138
  * no 0.26 runtime (start, inspect and in-home commands refuse them); retire still works. */
115
139
  export function legacyCapturedHomes(root) {
116
140
  if (!root) return null;
117
- const subdirs = (d) => { try { return readdirSync(d, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name); } catch { return []; } };
118
141
  const homes = [];
119
142
  for (const agent of subdirs(root)) for (const inst of subdirs(join(root, agent, "instances"))) {
120
143
  const home = join(root, agent, "instances", inst);
@@ -883,11 +906,21 @@ function readCatalogFile() {
883
906
  // catalog belongs to the kernel install.
884
907
  const override = !!process.env.OATS_PACKAGE_CATALOG;
885
908
  if (!existsSync(file)) { if (override) recordLocalInput(file, null); return empty; }
886
- let doc;
887
909
  const text = readFileSync(file, "utf8");
888
910
  if (override) recordLocalInput(file, text);
911
+ return parsePackageCatalog(text, file);
912
+ }
913
+ /** A package catalog's text, parsed and checked as the kernel reads it (named `file` in errors):
914
+ * { packages, capabilities, file }, both maps null-prototype; invalid-source when it is broken. */
915
+ export function parsePackageCatalog(text, file) {
916
+ let doc;
889
917
  try { doc = JSON.parse(text); }
890
- catch (e) { throw oatsError("invalid-source", `broken package catalog ${file}: ${e.message}`); }
918
+ catch (e) {
919
+ // JSON.parse does not say where; the kernel's strict reader (same grammar) locates the error.
920
+ let line;
921
+ try { parseStrictJson(text, { maxBytes: 64 * 1024 * 1024 }); } catch (s) { if (Number.isInteger(s.provenance?.offset)) line = lineAt(text, s.provenance.offset); }
922
+ throw oatsError("invalid-source", `broken package catalog ${file}${line ? ` (line ${line})` : ""}: ${e.message}`);
923
+ }
891
924
  if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw oatsError("invalid-source", `broken package catalog ${file}: root must be a JSON object`);
892
925
  const out = { packages: Object.create(null), capabilities: Object.create(null), file };
893
926
  const packages = doc.packages;
@@ -2082,6 +2115,12 @@ export function upgradeHomeMeta(meta, home) {
2082
2115
  return { ...rest, ...(runtime !== undefined || meta.harness !== undefined ? { harness: meta.harness ?? runtime } : {}), ...(meta.launch !== undefined ? { launch: upgradeLaunchRecipe(meta.launch) } : {}) };
2083
2116
  }
2084
2117
  const LAUNCH_PROMPT = { kind: "task-file", file: "TASK.md" };
2118
+ /** The fixed prompt a codex launch starts with: it names the task file, never its text (argv is
2119
+ * readable by every local user). Codex reads the file with a tool. */
2120
+ export const CODEX_TASK_PROMPT = "Read TASK.md in this directory first: it is your briefing and your task.";
2121
+ /** The prompt each harness gets, so the task's text never travels in argv: Claude Code inlines an
2122
+ * `@file` mention; codex is pointed at the file. pi takes `@TASK.md` among its own arguments. */
2123
+ const TASK_PROMPT = Object.freeze({ claude: "@TASK.md", codex: CODEX_TASK_PROMPT });
2085
2124
 
2086
2125
  /** The executable a launch uses: a configuration's declared one (a bare name
2087
2126
  * on PATH; a path against the deployment directory when relative) or the
@@ -2362,7 +2401,7 @@ export function renderLaunchRecipe(recipe, { home, instance, redact = false, tru
2362
2401
  }
2363
2402
  let cmdline;
2364
2403
  if (harness === "claude") {
2365
- cmdline = `${shq(executable)}${yolo ? " --dangerously-skip-permissions" : ""}${model ? ` --model ${shq(model)}` : ""}${tail} -- "$(cat TASK.md)"`;
2404
+ cmdline = `${shq(executable)}${yolo ? " --dangerously-skip-permissions" : ""}${model ? ` --model ${shq(model)}` : ""}${tail} -- ${shq(TASK_PROMPT.claude)}`;
2366
2405
  } else if (harness === "codex") {
2367
2406
  const codexTrust = `projects={${JSON.stringify(realPathOrNearest(home))}={trust_level="trusted"}}`;
2368
2407
  // Codex may run tool commands outside the session's process (its shared app-server daemon),
@@ -2370,7 +2409,7 @@ export function renderLaunchRecipe(recipe, { home, instance, redact = false, tru
2370
2409
  // never goes on argv, and PATH is the execution's (CODEX_TOOL_PATH: the shim first).
2371
2410
  const toolEnvArgs = env.filter((e) => !e.reference && e.name !== "PATH")
2372
2411
  .map((e) => ` -c ${shq(`shell_environment_policy.set.${e.name}=${JSON.stringify(e.value)}`)}`).join("");
2373
- cmdline = `${shq(executable)} --cd ${shq(home)} -c check_for_update_on_startup=false${yolo ? " --yolo" : ""}${yolo || trustHome ? ` -c ${shq(codexTrust)}` : ""}${toolEnvArgs}${model ? ` --model ${shq(model)}` : ""}${tail} -- "$(cat TASK.md)"`;
2412
+ cmdline = `${shq(executable)} --cd ${shq(home)} -c check_for_update_on_startup=false${yolo ? " --yolo" : ""}${yolo || trustHome ? ` -c ${shq(codexTrust)}` : ""}${toolEnvArgs}${model ? ` --model ${shq(model)}` : ""}${tail} -- ${shq(TASK_PROMPT.codex)}`;
2374
2413
  } else {
2375
2414
  // Decision 13: pi starts NORMALLY — its own skill discovery (~/.pi/agent/skills,
2376
2415
  // .agents/skills up the tree, so the instance's copied capability skills are
@@ -2387,7 +2426,7 @@ export function renderLaunchRecipe(recipe, { home, instance, redact = false, tru
2387
2426
  * with the home's kernel shim first on PATH (every launch runs through here). */
2388
2427
  function nativeRecordCommand(command, home, harness) {
2389
2428
  const { tokens, binary } = parseLaunchCommand(command);
2390
- const args = tokens.slice(binary + 1).filter(t => t.kind !== "prompt").map(t => t.value ?? t.text);
2429
+ const args = tokens.slice(binary + 1).map(t => t.value ?? t.text);
2391
2430
  const id = prepareNativeStart(home, harness);
2392
2431
  const recorder = join(PKG_ROOT, "packages", "record", "bin", "record-native-start.mjs");
2393
2432
  const inner = `${shq(process.execPath)} ${shq(recorder)} ${shq(home)} ${shq(id)} ${shq(harness)} ${shq(JSON.stringify(args))} && exec ${executionArgv(tokens, binary, harness).join(" ")}`;
@@ -3102,7 +3141,8 @@ function* spawnBody(root, agent, o = {}) {
3102
3141
  // apply of the same decision, or anything else) got here first, and this one
3103
3142
  // has touched nothing.
3104
3143
  mkdirSync(dirname(home), { recursive: true });
3105
- try { mkdirSync(home); }
3144
+ // 0700: the home holds the instance's identity keys, TASK.md and transcripts.
3145
+ try { mkdirSync(home, { mode: 0o700 }); }
3106
3146
  catch (e) {
3107
3147
  if (e?.code === "EEXIST") throw Object.assign(oatsError("E_PLACEMENT_TAKEN", `${instance} already exists at ${home} (a concurrent spawn won the placement); nothing was created by this call`), { instance, home });
3108
3148
  throw e;
@@ -3606,7 +3646,7 @@ You are instance "${instance}" of agent "${agent.name}".
3606
3646
  - Home: ${home}
3607
3647
  - Work tree: ./work — ${workDesc}
3608
3648
  - Do all repository work inside ./work. Read ./work/AGENTS.md or ./work/CLAUDE.md first if present.${briefLines}${harness === "codex" ? "\n## Harness notification delivery\n\nNative Codex has no built-in messaging channel. Follow the explicit delivery briefing for this instance from your messaging capability, if present; it may arrange notification through this terminal. Shared channel instructions alone do not establish that delivery is configured. Without an instance delivery briefing, check your messaging capability's inbox and pending commands at task boundaries or when the operator asks; do not assume messages will wake this session.\n" : ""}
3609
- ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spawn time — await instructions.\n"}`);
3649
+ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spawn time — await instructions.\n"}`, { mode: 0o600 }); // human text: the owner's only
3610
3650
 
3611
3651
  // Launch command. Spawn IS session start: the recipe is persisted in
3612
3652
  // instance.json beside its rendering, which is executed in the instance's
@@ -4564,8 +4604,18 @@ export function inputInstanceSession(home, text) {
4564
4604
 
4565
4605
  // ---------------------------------------------------------------- session start
4566
4606
 
4567
- /** The exact prompt token spawn renders for claude and codex launches. */
4607
+ /** The prompt token of a recorded claude or codex command that hands the harness the task's own
4608
+ * text in argv, where any local user can read it: `"$(cat TASK.md)"`. It is parsed, and replaced
4609
+ * when the home starts (withSafeTaskPrompt). */
4568
4610
  const LAUNCH_PROMPT_TOKEN = '"$(cat TASK.md)"';
4611
+ /** A recorded command with its harness's safe task prompt: a `"$(cat TASK.md)"` token becomes
4612
+ * TASK_PROMPT[harness]. Applied when a home starts from its recorded command, which is saved so. */
4613
+ export function withSafeTaskPrompt(command, harness) {
4614
+ const { tokens } = parseLaunchCommand(command);
4615
+ if (!tokens.some((t) => t.kind === "prompt")) return command;
4616
+ if (!Object.hasOwn(TASK_PROMPT, harness)) throw oatsError("E_LAUNCH_COMMAND_UNSUPPORTED", `a ${harness} command never carried the task as "$(cat TASK.md)"; inspect the saved command in instance.json`);
4617
+ return renderLaunchCommand(tokens.map((t) => t.kind === "prompt" ? { kind: "word", value: TASK_PROMPT[harness], quoted: true, text: shq(TASK_PROMPT[harness]) } : t));
4618
+ }
4569
4619
 
4570
4620
  /** Tokenize a persisted OATS launch command. The grammar is exactly what
4571
4621
  * spawn renders: space-separated tokens that are env assignments NAME='v',
@@ -5004,6 +5054,8 @@ export function startInstanceSession(home, o = {}) {
5004
5054
  command = withLaunchModel(command, resolved);
5005
5055
  model = resolved; explicitModelFrom = "start";
5006
5056
  } else parseLaunchCommand(command);
5057
+ // A recorded command starts, and is saved, with its harness's safe task prompt.
5058
+ if (!launchPlan) command = withSafeTaskPrompt(command, harness);
5007
5059
  // References recorded for this home must resolve on this host on every
5008
5060
  // start path, and the source variables go to the pane, not the command.
5009
5061
  const recipeForEnv = launchPlan?.recipe || (meta.launch && typeof meta.launch === "object" ? meta.launch : null);
@@ -5098,7 +5150,7 @@ export function startInstanceSession(home, o = {}) {
5098
5150
  }
5099
5151
  target = { backend: "tmux", session, window, socket: resolve(socket) };
5100
5152
  // Keep launch evidence until the command exits or the target disappears.
5101
- // A transient child (for example cat TASK.md) is not proof that startup
5153
+ // A transient child (for example the native-start recorder) is not proof that startup
5102
5154
  // has finished. A later start reconciles the receipt without a watcher.
5103
5155
  try { return { ...record(meta, { id, target, model, command, startedAt, reused, ...planExtra, ...(stopReceipt ? { stop: stopReceipt } : {}) }, false), warnings }; }
5104
5156
  catch (e) {
package/lib/workspace.mjs CHANGED
@@ -334,8 +334,9 @@ function schemaHint(kind, value) {
334
334
  }
335
335
  return "";
336
336
  }
337
- /** Parse + validate a declaration file → { value } | { problems }. Never throws. */
338
- function readDeclaration(kind, bytes, origin, validateOptions) {
337
+ /** Parse + validate a declaration file (`kind` workspace | membership | soul | local) → { value } |
338
+ * { problems }. Never throws. The reader `oats sync` uses, exported for the repository's validate. */
339
+ export function readDeclaration(kind, bytes, origin, validateOptions) {
339
340
  const decoded = decodeDocument(bytes, origin);
340
341
  if (decoded.problems) return decoded;
341
342
  const problems = FILE_KINDS[kind].validate(decoded.value, validateOptions);
@@ -3,12 +3,12 @@
3
3
  "packages": {
4
4
  "oats.okf": {
5
5
  "url": "https://github.com/awebai/oats-okf.git",
6
- "ref": "v4.0.5",
6
+ "ref": "v4.0.6",
7
7
  "path": "oats-package"
8
8
  },
9
9
  "oats.aweb": {
10
10
  "url": "https://github.com/awebai/oats-aweb.git",
11
- "ref": "v1.17.5",
11
+ "ref": "v1.17.6",
12
12
  "path": "oats-package"
13
13
  },
14
14
  "oats.jira": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.34.1",
3
+ "version": "0.34.2",
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",
@@ -61,7 +61,7 @@ members:
61
61
  - git:github.com/acme/platform
62
62
  packages:
63
63
  oats.framework: v1.4.1 # bare versions resolve through the official catalog
64
- oats.okf: v4.0.5
64
+ oats.okf: v4.0.6
65
65
  defaults:
66
66
  capabilities: { oats.core: { from: package } }
67
67
  knowledge: { oats.okf: { from: package } }