@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 +31 -18
- package/docs/capabilities.md +6 -1
- package/docs/desktop-cli-api.md +6 -6
- package/docs/execution-targets.md +14 -2
- package/docs/implementation.md +1 -1
- package/docs/integrations.md +2 -2
- package/docs/knowledge.md +3 -2
- package/docs/official-catalog.md +3 -3
- package/docs/packages.md +10 -10
- package/docs/release-notes/v0.34.2.md +68 -0
- package/docs/souls-and-instances.md +3 -3
- package/docs/workspaces.md +1 -1
- package/lib/canonical-json.mjs +3 -1
- package/lib/config-data.mjs +4 -2
- package/lib/core.mjs +64 -12
- package/lib/workspace.mjs +3 -2
- package/package-catalog.json +2 -2
- package/package.json +1 -1
- package/skills/oats-getting-started/SKILL.md +1 -1
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
|
|
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
|
-
|
|
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
|
|
773
|
-
// (E_LAUNCH_LEGACY: re-spawn it from the deployment).
|
|
774
|
-
let d;
|
|
775
|
-
try {
|
|
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(
|
|
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
|
|
2842
|
-
//
|
|
2843
|
-
//
|
|
2844
|
-
//
|
|
2845
|
-
const
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
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,
|
|
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;
|
package/docs/capabilities.md
CHANGED
|
@@ -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.
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -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.
|
|
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.
|
|
786
|
-
"version":"4.0.
|
|
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.
|
|
876
|
-
"commit":"
|
|
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":"
|
|
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] --
|
|
17
|
-
| Codex | `codex` | `codex --cd <home> [--model m] --
|
|
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`,
|
package/docs/implementation.md
CHANGED
|
@@ -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,
|
|
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
|
|
package/docs/integrations.md
CHANGED
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.
|
|
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
|
package/docs/official-catalog.md
CHANGED
|
@@ -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.
|
|
14
|
-
| `oats.aweb` | `v1.17.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
79
|
-
oats.aweb: v1.17.
|
|
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.
|
|
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.
|
|
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.
|
|
164
|
-
"commit": "
|
|
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.
|
|
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.
|
|
156
|
-
"commit": "
|
|
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": {
|
package/docs/workspaces.md
CHANGED
|
@@ -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.
|
|
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
|
package/lib/canonical-json.mjs
CHANGED
|
@@ -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++;
|
package/lib/config-data.mjs
CHANGED
|
@@ -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
|
-
|
|
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) {
|
|
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} --
|
|
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} --
|
|
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).
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
338
|
-
|
|
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);
|
package/package-catalog.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
+
oats.okf: v4.0.6
|
|
65
65
|
defaults:
|
|
66
66
|
capabilities: { oats.core: { from: package } }
|
|
67
67
|
knowledge: { oats.okf: { from: package } }
|