@awebai/oats 0.32.0 → 0.33.0
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/README.md +1 -1
- package/bin/oats.mjs +30 -15
- package/docs/capabilities.md +5 -0
- package/docs/configuration.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +3 -2
- package/docs/desktop-cli-api.md +50 -6
- package/docs/first-team.md +20 -2
- package/docs/implementation.md +64 -2
- package/docs/integrations.md +1 -1
- package/docs/official-catalog.md +2 -2
- package/docs/packages.md +2 -2
- package/docs/release-notes/v0.33.0.md +174 -0
- package/docs/souls-and-instances.md +64 -0
- package/docs/workspaces.md +1 -1
- package/lib/core.mjs +89 -17
- package/lib/harness-trust.mjs +139 -0
- package/lib/instance-inspect.mjs +16 -2
- package/lib/process-group.mjs +54 -0
- package/lib/remote.mjs +594 -102
- package/package-catalog.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,7 +208,7 @@ Begin with a small team and a real piece of work:
|
|
|
208
208
|
4. Create instances for their assignments.
|
|
209
209
|
5. Verify that work, communication, learning and handoff behave as intended.
|
|
210
210
|
|
|
211
|
-
On a machine with Node.js 22+, Git and tmux, and a workspace repository to point at:
|
|
211
|
+
On a machine with Node.js 22+, Git (2.45+ to fetch only what OATS reads; an older git fetches whole trees) and tmux, and a workspace repository to point at:
|
|
212
212
|
|
|
213
213
|
```bash
|
|
214
214
|
npm install -g @awebai/oats
|
package/bin/oats.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
|
|
29
29
|
capabilityManifests, capabilityTrust, capabilityExecutablePath,
|
|
30
30
|
officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
|
|
31
|
-
findInstanceHome, findInstanceHomes, 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,
|
|
31
|
+
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,
|
|
32
32
|
} from "../lib/core.mjs";
|
|
33
33
|
import {
|
|
34
34
|
writeFileAtomic, LOCK_FILE, readLock, readLockIfPresent, writeLock, resolvePackages, memoizedRemote,
|
|
@@ -789,7 +789,7 @@ function launchPreview(bail) {
|
|
|
789
789
|
let plan;
|
|
790
790
|
try { plan = planLaunch({ home, instance, meta, contextDir: context, agentLike, selection: sel, resolvedCfg: r, preview: true }); } catch (e) { bail(e.code || "E_BAD_ARGS", e.message); }
|
|
791
791
|
const { recipe } = plan;
|
|
792
|
-
const command = renderLaunchRecipe(recipe, { home, instance, redact: true });
|
|
792
|
+
const command = renderLaunchRecipe(recipe, { home, instance, redact: true, trustHome: plan.trustHome });
|
|
793
793
|
const d = describeLaunchCommand(command);
|
|
794
794
|
const environment = d.environment.map((e) => e.reference && recipe.env[e.name]?.fromEnv ? { name: e.name, fromEnv: recipe.env[e.name].fromEnv } : e);
|
|
795
795
|
jsonOk({ context, selected, selection: { source: plan.selectionSource, launchConfig: recipe.launchConfig, harness: sel.harness ?? null, model: sel.model ?? null, yolo: sel.yolo ?? null }, harness: plan.harness, model: recipe.model, modelSource: plan.modelSource, yolo: recipe.yolo ?? null, launchConfig: recipe.launchConfig, launchConfigSource: recipe.launchConfigSource, launchConfigDefault: recipe.launchConfigDefault === true, executable: { path: plan.executable.path, declared: plan.executable.declared ?? null, resolvedFrom: plan.executable.resolvedFrom }, argv: d.argv, environment, command, prompt: recipe.prompt, hooks: redactLaunchRecipe(recipe).hooks, preflight: plan.preflight, ok: plan.ok });
|
|
@@ -1030,7 +1030,7 @@ let maxAgeGiven = null;
|
|
|
1030
1030
|
function commandSession() {
|
|
1031
1031
|
if (!readSession) {
|
|
1032
1032
|
readSession = remoteModule.createReadSession({ maxAge: maxAgeGiven ?? 0 });
|
|
1033
|
-
process.on("exit", () => readSession.closeNow());
|
|
1033
|
+
process.on("exit", () => { sayReadNotices(); readSession.closeNow(); });
|
|
1034
1034
|
for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143], ["SIGHUP", 129]]) process.once(signal, () => {
|
|
1035
1035
|
readSession.closeNow();
|
|
1036
1036
|
// Another handler (a scheduler lock's release) exits on its own after this one.
|
|
@@ -1039,15 +1039,22 @@ function commandSession() {
|
|
|
1039
1039
|
}
|
|
1040
1040
|
return readSession;
|
|
1041
1041
|
}
|
|
1042
|
+
/** What the read session found worth telling the operator (a remote that cannot serve partial fetches),
|
|
1043
|
+
* once, on stderr when the command ends: stdout, and so every JSON answer, is unchanged. */
|
|
1044
|
+
function sayReadNotices() {
|
|
1045
|
+
for (const notice of readSession?.notices.splice(0) ?? []) process.stderr.write(`oats: warning: ${notice}\n`);
|
|
1046
|
+
}
|
|
1042
1047
|
/** Which kernel command forms take --max-age: THE allow-list (docs/desktop-cli-api.md "Observation reuse").
|
|
1043
1048
|
* → null when this form reads with observation reuse, else the E_BAD_ARGS message. `head` is argv before `--`. */
|
|
1044
|
-
const MAX_AGE_READS = "status, workspace status, souls, capabilities, inspect --soul|--home, and the read forms of teams and soul teams";
|
|
1049
|
+
const MAX_AGE_READS = "status, workspace status, souls, capabilities, inspect --soul|--home, spawn --preview, and the read forms of teams and soul teams";
|
|
1045
1050
|
function maxAgeRefusal(command, head) {
|
|
1046
1051
|
const word = (i) => (head[i] !== undefined && !head[i].startsWith("--") ? head[i] : undefined);
|
|
1047
1052
|
const refuse = (form) => `--max-age is not accepted by \`oats ${form}\`: only the read verbs reuse observations (${MAX_AGE_READS})`;
|
|
1048
1053
|
if (head.includes("--server")) return "--max-age cannot be combined with --server: observation reuse is local to this machine";
|
|
1049
1054
|
switch (command) {
|
|
1050
1055
|
case "status": case "souls": case "capabilities": case "inspect": return null;
|
|
1056
|
+
// A preview reads (feature spawn-preview-max-age); an apply always observes live.
|
|
1057
|
+
case "spawn": return head.includes("--preview") ? null : refuse("spawn");
|
|
1051
1058
|
case "workspace": return word(1) === "status" ? null : refuse(["workspace", word(1)].filter(Boolean).join(" "));
|
|
1052
1059
|
case "teams": return word(1) === undefined ? null : refuse(`teams ${word(1)}`);
|
|
1053
1060
|
case "soul": {
|
|
@@ -1884,8 +1891,9 @@ async function status() {
|
|
|
1884
1891
|
const livenessWord = (i) => i.running === true ? "RUNNING" : i.running === false ? "idle" : "unknown";
|
|
1885
1892
|
/** The flags `oats spawn` reads: those taking a value, and switches. `--provider` takes two words.
|
|
1886
1893
|
* `--instance` is refused by a local spawn (with its replacement) but still travels to an older
|
|
1887
|
-
* host through `--server`, whose route reads it.
|
|
1888
|
-
|
|
1894
|
+
* host through `--server`, whose route reads it. `--max-age` reaches here only on a preview: the
|
|
1895
|
+
* dispatch allow-list (maxAgeRefusal) refuses it on an apply. */
|
|
1896
|
+
const SPAWN_VALUE_FLAGS = new Set(["agents-root", "backend", "base", "branch", "dir", "expect-decision", "harness", "idempotency-key", "instance", "launch-config", "max-age", "model", "name", "parent", "purpose", "relation", "relative-root", "relative-to", "repo", "runtime", "task", "task-file", "trigger-event", "wake-cron", "wake-every", "wake-file", "wake-json", "wake-message", "wake-message-file", "wake-tz", "work", "work-dir"]);
|
|
1889
1897
|
const SPAWN_SWITCHES = new Set(["allow-child-spawns", "json", "no-child-spawns", "no-launch", "no-yolo", "preview", "yolo"]);
|
|
1890
1898
|
/** Why `argv` (after `spawn`, the soul first) is not a spawn, or undefined: a positional after the
|
|
1891
1899
|
* soul or a flag spawn does not read is never ignored. A value flag consumes its value exactly as
|
|
@@ -2141,7 +2149,7 @@ async function spawnCmd() {
|
|
|
2141
2149
|
// A workspace preview may have fetched the soul's SOURCE to a temporary copy
|
|
2142
2150
|
// (the deployment's cache had no entry for its commit): the result says so.
|
|
2143
2151
|
if (prepared) r.soulFetched = soulFetched;
|
|
2144
|
-
if (JSON_MODE) { jsonOk(r); return; }
|
|
2152
|
+
if (JSON_MODE) { jsonOk(withObservation(r)); return; }
|
|
2145
2153
|
console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""}) harness ${r.harness}${r.model ? ` model ${r.model}` : ` (${r.modelSource})`}${r.launchConfig ? ` via launch configuration ${r.launchConfig}${r.launchConfigDefault ? ` (this machine's ${r.harness} default)` : ""}` : ""}${r.yolo ? " YOLO" : ""}; nothing was created${soulFetched ? " (the soul source was fetched to a temporary copy, not kept)" : ""}`);
|
|
2146
2154
|
return;
|
|
2147
2155
|
}
|
|
@@ -2758,8 +2766,9 @@ async function capabilityCommand() {
|
|
|
2758
2766
|
let activeIds;
|
|
2759
2767
|
let context = process.cwd();
|
|
2760
2768
|
let teamCtx, homeMeta, homeTeamCtx;
|
|
2761
|
-
// OATS_INSTANCE_HOME is the canonical identity; the older names still count.
|
|
2762
|
-
|
|
2769
|
+
// OATS_INSTANCE_HOME is the canonical identity; the older names still count. With none set
|
|
2770
|
+
// (a harness that strips the session env), the home enclosing the cwd.
|
|
2771
|
+
const instanceHome = process.env.OATS_INSTANCE_HOME || process.env.PI_AGENT_HOME || process.env.OATS_HOME || enclosingInstanceHome(logicalCwd());
|
|
2763
2772
|
const metaFile = instanceHome && join(instanceHome, "instance.json");
|
|
2764
2773
|
// Capability-id keyed — never answer for `constructor`/`toString`. Belt and
|
|
2765
2774
|
// braces: the ids come from instance.json, which spawn wrote from resolved
|
|
@@ -2937,7 +2946,7 @@ function versionCmd() {
|
|
|
2937
2946
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
2938
2947
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
2939
2948
|
// never listed.
|
|
2940
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations", "readiness", "instance-events", "instance-git", "lifecycle-plans"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "team-model-2", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "launch-config-default"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
|
|
2949
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations", "readiness", "instance-events", "instance-git", "lifecycle-plans"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "team-model-2", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts", "launch-preference", "preview-composed-from", "observe-max-age", "spawn-preview-max-age", "launch-config-default"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
|
|
2941
2950
|
return;
|
|
2942
2951
|
}
|
|
2943
2952
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3543,6 +3552,9 @@ Usage:
|
|
|
3543
3552
|
[--provider <capability> <key>=<value>] a provider setting for this spawn only
|
|
3544
3553
|
(repeatable; dotted keys nest; recorded in
|
|
3545
3554
|
instance.json providers.<capability>)
|
|
3555
|
+
[--preview [--max-age <s>]] decide everything, create nothing; the JSON's
|
|
3556
|
+
decision binds an apply (--expect-decision <rev>);
|
|
3557
|
+
--max-age reuses recent heads (preview only)
|
|
3546
3558
|
oats retire <instance> [--force] retire an instance (window, hooks,
|
|
3547
3559
|
[--self] [--delete-branch] worktree, home); --self = retire the
|
|
3548
3560
|
[--keep-dir] [--json] CALLING instance: the window dies, then
|
|
@@ -3660,15 +3672,17 @@ The turn record (core — every conversation captured, searchable, replicated):
|
|
|
3660
3672
|
|
|
3661
3673
|
Observation reuse (feature observe-max-age):
|
|
3662
3674
|
--max-age <seconds> on the read verbs only — status, workspace status,
|
|
3663
|
-
souls, capabilities, inspect --soul|--home,
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3675
|
+
souls, capabilities, inspect --soul|--home,
|
|
3676
|
+
spawn --preview (feature spawn-preview-max-age),
|
|
3677
|
+
and the read forms of teams and soul teams —
|
|
3678
|
+
reuse a remote head observation up to <seconds>
|
|
3679
|
+
old (0–86400; 0 is live) instead of asking the
|
|
3680
|
+
remote again; the JSON
|
|
3667
3681
|
then carries observation { observedAt (the oldest
|
|
3668
3682
|
head used), reused, localRevision (a digest of
|
|
3669
3683
|
the local configuration read) }. Refused
|
|
3670
3684
|
(E_BAD_ARGS) by every other command, an edit form,
|
|
3671
|
-
and with --server
|
|
3685
|
+
a spawn apply, and with --server
|
|
3672
3686
|
|
|
3673
3687
|
Layers: ${LAYERS.join(", ")}. Workspace model v2: docs/design/2026-09-23-workspace-module-contracts.md.`;
|
|
3674
3688
|
}
|
|
@@ -3681,5 +3695,6 @@ Layers: ${LAYERS.join(", ")}. Workspace model v2: docs/design/2026-09-23-workspa
|
|
|
3681
3695
|
die(e.message);
|
|
3682
3696
|
} finally {
|
|
3683
3697
|
// The command's read session: every `git cat-file --batch` child ends before the process does.
|
|
3698
|
+
sayReadNotices();
|
|
3684
3699
|
if (readSession) await readSession.close();
|
|
3685
3700
|
}
|
package/docs/capabilities.md
CHANGED
|
@@ -355,6 +355,11 @@ for this contract. Hyphenated vendors are also excluded because translating a
|
|
|
355
355
|
hyphen to `_` would let `aweb-evil.*` collide with names already inside
|
|
356
356
|
`aweb.*`'s `AWEB_*` namespace.
|
|
357
357
|
|
|
358
|
+
Hook environment values must not be secrets. A codex launch also passes them
|
|
359
|
+
to Codex as command-line arguments (`-c shell_environment_policy.set.<NAME>=…`,
|
|
360
|
+
so its tool commands see them), and any local user can read those. A secret
|
|
361
|
+
reaches a launch through a launch configuration's environment reference.
|
|
362
|
+
|
|
358
363
|
A manifest's `settings.<key>` may carry `hostOnly: true`. Such a
|
|
359
364
|
key is a fact about the machine — a custody directory, a state root — and the
|
|
360
365
|
resolver accepts it only from the deployment's own `oats-local.yaml`
|
package/docs/configuration.md
CHANGED
|
@@ -330,6 +330,6 @@ Environment: `OATS_REMOTE_CACHE` relocates the fetch cache (which also holds
|
|
|
330
330
|
the bounded parsed-read cache and the observations `--max-age` reuses; all of
|
|
331
331
|
it is safe to delete); `OATS_PACKAGE_CATALOG` names an alternative package
|
|
332
332
|
catalog file. The read verbs (`status`, `workspace status`, `souls`,
|
|
333
|
-
`capabilities`, `inspect`,
|
|
334
|
-
take `--max-age <seconds>` to reuse a remote head observed that recently
|
|
333
|
+
`capabilities`, `inspect`, the read forms of `teams` and `soul teams`, and
|
|
334
|
+
`spawn --preview`) take `--max-age <seconds>` to reuse a remote head observed that recently
|
|
335
335
|
([Observation reuse](desktop-cli-api.md#observation-reuse-feature-observe-max-age-oats-0311)).
|
|
@@ -55,8 +55,9 @@ symlink enters as `symlink:<target>`; empty directories and a top-level `.git/`
|
|
|
55
55
|
### 1.3 Access, cache, failures
|
|
56
56
|
|
|
57
57
|
- Git uses the operator's own configuration and credentials; `GIT_TERMINAL_PROMPT=0`,
|
|
58
|
-
`GIT_ASKPASS=/usr/bin/false` and ssh `-o BatchMode=yes` mean nothing prompts. 30 s per git call.
|
|
59
|
-
- A commit is fetched depth 1
|
|
58
|
+
`GIT_ASKPASS=/usr/bin/false` and ssh `-o BatchMode=yes` mean nothing prompts. 30 s per git call; 10 minutes for the fetch of a commit.
|
|
59
|
+
- A commit is fetched depth 1 with its trees and its blobs up to 64 KiB (larger blobs on demand, when a read
|
|
60
|
+
needs them; whole trees from a server without partial fetches; awebai/oats#384) into a bare cache `<cacheDir>/<sha256(key)>/` (default
|
|
60
61
|
`~/.cache/oats/remotes`; the CLI honours `OATS_REMOTE_CACHE`) and pinned as `refs/oats/commits/<oid>`.
|
|
61
62
|
The cache may be wiped at any time. Operations on one cache repo are serialized.
|
|
62
63
|
- Failures: `E_REMOTE_UNREADABLE { url, key, reason }`, `reason` ∈ `auth`, `not-found`, `network`,
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -38,7 +38,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
38
38
|
"instance-events-2","schedule-history","schedule-read-2","spawn-preview-2","spawn-idempotency","spawn-idempotency-2","spawn-apply-2",
|
|
39
39
|
"workspace-v2","instance-modules","spawn-provider-payload","served-identity","packages-no-approval","spawn-name","settings-origins",
|
|
40
40
|
"team-model-2","settings-declared","capabilities-private","layers-from","harness","package-souls","triggers","automations","desktop-facts","launch-preference",
|
|
41
|
-
"preview-composed-from","observe-max-age"],
|
|
41
|
+
"preview-composed-from","observe-max-age","spawn-preview-max-age"],
|
|
42
42
|
"automationsApi":1,"workspaceApi":2,"instanceGitApi":1,"spawnApplyApi":1,"soulsApi":2,"lifecycleApi":1,
|
|
43
43
|
"readinessApi":2,"spawnPreviewApi":2,"eventsApi":2,"scheduleHistoryApi":3,"scheduleApi":2,"operationsApi":2}
|
|
44
44
|
```
|
|
@@ -96,6 +96,7 @@ canonical (`github.com/<org>/<repo>`, or `local/<abs-path>`). Examples use
|
|
|
96
96
|
| `launch-preference` | soul and local launch preferences; `launch`, `launchCurrent`, `launchFrom`; `--reselect-launch`; `key` on soul and agent rows ([Launch preferences](#soul-launch-preferences-feature-launch-preference-oats-0300)) | |
|
|
97
97
|
| `preview-composed-from` | `composedFrom` on preview `modules[]` ([Composition](#the-preview)) | |
|
|
98
98
|
| `observe-max-age` | `--max-age <s>` on the read verbs and their `observation` block ([Observation reuse](#observation-reuse-feature-observe-max-age-oats-0311)) | |
|
|
99
|
+
| `spawn-preview-max-age` | `--max-age <s>` on `spawn --preview` and its `observation` block ([Observation reuse](#observation-reuse-feature-observe-max-age-oats-0311), [The preview](#the-preview)) | |
|
|
99
100
|
|
|
100
101
|
Payload-only integers, never in the probe: `onboardApi: 2`, `syncApi: 1`,
|
|
101
102
|
`workspaceStatusApi: 1`, `capabilitiesApi: 1`, the `oats souls` document's
|
|
@@ -194,6 +195,7 @@ it and answer live, without the block.
|
|
|
194
195
|
```text
|
|
195
196
|
oats status | workspace status | souls | capabilities | inspect --soul|--home
|
|
196
197
|
| teams | soul teams <soul> … --max-age <seconds> --json
|
|
198
|
+
oats spawn <soul> … --preview --max-age <seconds> --json (feature spawn-preview-max-age)
|
|
197
199
|
```
|
|
198
200
|
|
|
199
201
|
- **Values:** whole seconds, `0` to `86400`. `0` is live: it reuses nothing.
|
|
@@ -206,7 +208,15 @@ oats status | workspace status | souls | capabilities | inspect --soul|--home
|
|
|
206
208
|
everywhere; `reused` is `true` when any head came from an earlier observation.
|
|
207
209
|
A command that read no remote head reports the time it started and
|
|
208
210
|
`reused: false`. Without the flag the key is absent and every document is
|
|
209
|
-
exactly as before.
|
|
211
|
+
exactly as before. A refusal (`E_SOUL_UNKNOWN`, any error envelope) never
|
|
212
|
+
carries the block.
|
|
213
|
+
- **Spawn preview** (feature `spawn-preview-max-age`, OATS 0.33.0): the
|
|
214
|
+
preview's `result` gains the same block, so you can say "as of
|
|
215
|
+
`<observedAt>`". The `decision` covers the heads the preview used, reused
|
|
216
|
+
or live: a reused head the member has since moved from makes the apply
|
|
217
|
+
(which always observes live) refuse `E_DECISION_STALE`, and the apply
|
|
218
|
+
records the head it observed, so the next preview under `--max-age` shows
|
|
219
|
+
it with a new `decision.revision`. The apply itself refuses the flag.
|
|
210
220
|
- **`localRevision`:** 24 lowercase hex characters, opaque. It digests every
|
|
211
221
|
piece of local configuration the kernel read for this answer:
|
|
212
222
|
`oats-local.yaml` (and each closer `oats-local.yaml` it looked for and did
|
|
@@ -237,12 +247,14 @@ oats status | workspace status | souls | capabilities | inspect --soul|--home
|
|
|
237
247
|
spelling of the same repository (ssh vs https) or for a different ref; its
|
|
238
248
|
commit can no longer be fetched. Each is observed live, as without the flag.
|
|
239
249
|
A live observation that fails is the usual error, never an older head.
|
|
240
|
-
- **Refusals:** every other command,
|
|
250
|
+
- **Refusals:** every other command, a spawn apply (with or without
|
|
251
|
+
`--expect-decision`; the form named is `spawn`), every edit form (`teams add|remove|default`,
|
|
241
252
|
`soul teams --add|--remove|--default|--clear-default`) and any `--server`
|
|
242
253
|
invocation refuse the flag before reading or writing anything, with
|
|
243
254
|
`E_BAD_ARGS` "--max-age is not accepted by \`oats <form>\`: only the read
|
|
244
255
|
verbs reuse observations (status, workspace status, souls, capabilities,
|
|
245
|
-
inspect --soul|--home, and the read forms of teams and soul
|
|
256
|
+
inspect --soul|--home, spawn --preview, and the read forms of teams and soul
|
|
257
|
+
teams)" and,
|
|
246
258
|
with `--server`, "--max-age cannot be combined with --server: observation
|
|
247
259
|
reuse is local to this machine".
|
|
248
260
|
A capability command's argv (`oats <namespace> …`) is its provider's: the
|
|
@@ -523,7 +535,25 @@ Feature `workspace-v2`, `workspaceApi: 2`. Model: [workspaces.md](workspaces.md)
|
|
|
523
535
|
`E_LOCAL_MISSING {dir, searched}`.
|
|
524
536
|
- The workspace is read over Git remotes with the operator's credentials,
|
|
525
537
|
never prompting: `E_REMOTE_UNREADABLE {url, reason: "auth" | "not-found" |
|
|
526
|
-
"network" | "timeout"}`.
|
|
538
|
+
"network" | "timeout" | "killed" | "cache" | "unknown"}`. `killed` (the
|
|
539
|
+
system killed git, for example out of memory) also carries `signal`.
|
|
540
|
+
`cache` (OATS 0.33.0) is local: the
|
|
541
|
+
remote cache on this machine could not be written (a git lock still held,
|
|
542
|
+
another oats process still writing it, or a lock file one left when it
|
|
543
|
+
died); `details.cacheDir` and, when known, `details.lock`,
|
|
544
|
+
`details.guard` or `details.holderPid` say which, and the message says
|
|
545
|
+
what to do.
|
|
546
|
+
- **How the Desktop reads it** (0.33.0). Of an `E_REMOTE_UNREADABLE` from
|
|
547
|
+
`status` / `workspace status`, the Desktop keeps the `message` (shown as
|
|
548
|
+
given) and only a bounded cause: `details.reason` (matching
|
|
549
|
+
`^[a-z][a-z-]{0,31}$`) and the host of `details.url`. No path, pid, lock,
|
|
550
|
+
`cacheDir` or other detail field crosses to the renderer. It keys only on
|
|
551
|
+
`code` + `details.reason`: `cache` words the roster "OATS cache
|
|
552
|
+
problem" with the message in full; `network` / `timeout` read "Couldn't
|
|
553
|
+
reach <host>"; any other reason keeps the generic wording. When the
|
|
554
|
+
deployment was observed before, the failed read keeps that observation:
|
|
555
|
+
`/api/panel` serves it with `error` (the message) and `errorCause {code,
|
|
556
|
+
reason, host?}`, and the roster shows it stale instead of empty.
|
|
527
557
|
- There is no package approval: declaring a package is the trust decision.
|
|
528
558
|
No payload carries `approvalNeeded`, `approval` or `approved`.
|
|
529
559
|
- A **standalone view** is a member repository whose workspace is not read
|
|
@@ -1162,7 +1192,7 @@ the result with `oats inspect --soul <name> --json`.
|
|
|
1162
1192
|
### The preview
|
|
1163
1193
|
|
|
1164
1194
|
```text
|
|
1165
|
-
oats spawn <soul> [the flags of a real spawn] --preview --json
|
|
1195
|
+
oats spawn <soul> [the flags of a real spawn] --preview [--max-age <s>] --json
|
|
1166
1196
|
```
|
|
1167
1197
|
|
|
1168
1198
|
Feature `spawn-preview-2`, `spawnPreviewApi: 2`. The preview runs every
|
|
@@ -1250,6 +1280,20 @@ it to a temporary copy (`soulFetched: true`).
|
|
|
1250
1280
|
`payloadRevision` (the merged payloads). `workspace` is the host key;
|
|
1251
1281
|
`standalone` marks a standalone view. `task` is the task text or `null`.
|
|
1252
1282
|
|
|
1283
|
+
**Observation reuse** (feature `spawn-preview-max-age`, OATS 0.33.0).
|
|
1284
|
+
- `--preview --max-age <s>` reuses member heads this machine observed at
|
|
1285
|
+
most `<s>` seconds ago, as the read verbs do ([Observation
|
|
1286
|
+
reuse](#observation-reuse-feature-observe-max-age-oats-0311): the same
|
|
1287
|
+
values, refusals and fallbacks to a live observation). With the flag (`0`
|
|
1288
|
+
included) the result gains `observation: {observedAt, reused,
|
|
1289
|
+
localRevision}`, shaped exactly as the read verbs' block; without it the
|
|
1290
|
+
preview is exactly as before, and no other field changes shape.
|
|
1291
|
+
- `decision.revision` covers the heads the preview used, reused or live.
|
|
1292
|
+
Apply never reuses (it refuses `--max-age`): a head that moved since the
|
|
1293
|
+
reused observation refuses `E_DECISION_STALE`, and the apply records what
|
|
1294
|
+
it observed, so re-preview under `--max-age` to get the new head and
|
|
1295
|
+
revision.
|
|
1296
|
+
|
|
1253
1297
|
**Provider settings.**
|
|
1254
1298
|
- `providers` is the `--provider` map as typed.
|
|
1255
1299
|
- `settings.<cap>`: the merged payload (manifest defaults, then workspace,
|
package/docs/first-team.md
CHANGED
|
@@ -80,6 +80,23 @@ Host-owned provider values (absolute paths, state roots) go under `settings:` in
|
|
|
80
80
|
`oats-local.yaml` afterwards — never in the workspace file, whose schema refuses
|
|
81
81
|
them. Do not commit `oats-local.yaml`.
|
|
82
82
|
|
|
83
|
+
**Trust the deployment once, for unattended launches.** Claude Code and Codex
|
|
84
|
+
ask before they work in a folder they have not seen, and every instance home is
|
|
85
|
+
new: a launch that stops at that prompt waits for a human. OATS never writes
|
|
86
|
+
the harnesses' configuration, so trust the deployment directory yourself, once
|
|
87
|
+
per harness you use:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cd ~/acme && claude # accept the folder-trust prompt, then quit
|
|
91
|
+
cd ~/acme && codex # choose "Trust and continue", then quit
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
One entry covers every instance home under the deployment
|
|
95
|
+
([souls-and-instances.md](souls-and-instances.md#unattended-launches-folder-trust)
|
|
96
|
+
says how each harness applies it). Until then, a claude or codex spawn warns
|
|
97
|
+
that its session will stop at the folder-trust prompt, and so does
|
|
98
|
+
`oats readiness`.
|
|
99
|
+
|
|
83
100
|
## 3. Give the deployment a team
|
|
84
101
|
|
|
85
102
|
With a messaging capability in the soul's composition, every instance lives in
|
|
@@ -117,8 +134,9 @@ oats spawn backend-expert --purpose first-fix --task "Fix one small issue, run t
|
|
|
117
134
|
oats status
|
|
118
135
|
```
|
|
119
136
|
|
|
120
|
-
`--harness pi|claude|codex` picks the harness; complete any native
|
|
121
|
-
|
|
137
|
+
`--harness pi|claude|codex` picks the harness; complete any native
|
|
138
|
+
authentication prompt in the printed session (folder trust is the one-time step
|
|
139
|
+
in section 2). The instance home is
|
|
122
140
|
`agents/<soul>/instances/<instance>/`; `work/` is its repository view;
|
|
123
141
|
`.oats/modules/<cap>/` are the copied capabilities and `.agents/skills/<skill>/` their skills;
|
|
124
142
|
`instance.json` records `modules` (from, commit, digest), `providers` and
|
package/docs/implementation.md
CHANGED
|
@@ -30,7 +30,7 @@ published to npm. Its developer docs are in
|
|
|
30
30
|
| `lib/` | the kernel (below) |
|
|
31
31
|
| `injects/` | the kernel and work-mode instruction blocks composed into every instance |
|
|
32
32
|
| `skills/` | bootstrap skills shipped with the kernel |
|
|
33
|
-
| `capabilities/` | this repository's own member capabilities (`oats-workspace-experts`), discovered at the member's latest state |
|
|
33
|
+
| `capabilities/` | this repository's own member capabilities (`oats-desktop-ui`, `oats-workspace-experts`), discovered at the member's latest state |
|
|
34
34
|
| `mirrors/` | generated byte mirrors of the official packages' capabilities (for example `oats-okf*`, checked by `scripts/check-okf-mirror.mjs`): release-lane and test material, kept out of `capabilities/` so member discovery does not list them a second time; not shipped in the npm package |
|
|
35
35
|
| `oats-package/` | the `oats.framework` package |
|
|
36
36
|
| `souls/` | this repository's own souls (a workspace member) |
|
|
@@ -58,6 +58,7 @@ published to npm. Its developer docs are in
|
|
|
58
58
|
| `tmux-config.mjs`, `session-*.mjs` | the tmux session backend and terminal input |
|
|
59
59
|
| `capability-contract.mjs`, `provider-binding.mjs` | manifest validation, the hook environment rules, the readiness wire |
|
|
60
60
|
| `servers.mjs` | routing commands to a registered server |
|
|
61
|
+
| `harness-trust.mjs` | reading (never writing) Claude's and Codex's folder trust for a launch |
|
|
61
62
|
|
|
62
63
|
The kernel is runtime-neutral: nothing in `lib/` depends on a harness or on
|
|
63
64
|
a provider. Provider behaviour lives in capabilities; the kernel supplies
|
|
@@ -93,7 +94,29 @@ gets the plain per-call behaviour. Within a session:
|
|
|
93
94
|
- a commit's tree is listed once (`git ls-tree -r -t -l`, bounded by
|
|
94
95
|
`TREE_INDEX_BUDGET`; anything odd falls back to the per-path reads), and
|
|
95
96
|
blobs come from one `git cat-file --batch` reader per cache repo (at most
|
|
96
|
-
12 open,
|
|
97
|
+
12 open, ended through `process-group.mjs` on timeout and at close);
|
|
98
|
+
`fetchRemoteTree` copies a module through it too, once `ensureBlobs` has
|
|
99
|
+
fetched what was missing (git re-reads its packs on a miss, so a reader
|
|
100
|
+
opened earlier finds the new blobs; one still missing answers `missing`,
|
|
101
|
+
never a fetch), with what is left of `TREE_BUDGET` as each read's bound;
|
|
102
|
+
a blob the reader answers `missing` or over its bound is read once more
|
|
103
|
+
alone, so the error is the one a copy without a session gives. A
|
|
104
|
+
command that ends normally awaits the close, so its readers are reaped
|
|
105
|
+
before it exits; a `process.exit` (every refusal) ends them in the
|
|
106
|
+
exit hook (`closeNow`), and the system reaps them once the process is gone;
|
|
107
|
+
- every git child is ended with SIGTERM first and SIGKILL only after a
|
|
108
|
+
grace (`terminateGroup`): git removes its own lock files on SIGTERM, and
|
|
109
|
+
a git killed outright leaves one that blocks every later write. The
|
|
110
|
+
SIGKILL goes to the whole group even when git itself has exited, so a
|
|
111
|
+
descendant that ignores SIGTERM (ssh, a remote helper) still ends; but
|
|
112
|
+
never to a group seen empty, whose id may already lead an unrelated
|
|
113
|
+
group. Until git's `close` (`watchGroup`), a member holding its pipes
|
|
114
|
+
keeps the id ours; after it, the group is probed every 50 ms through the
|
|
115
|
+
grace (no pid is allocated while it is a live group's id): empty, and it
|
|
116
|
+
is never signalled again. git's pipes are drained on a kill, never
|
|
117
|
+
destroyed, so `close` keeps waiting for a pipe-holding descendant. The exit
|
|
118
|
+
hook cannot wait for a timer, so it waits a bounded 200 ms synchronously
|
|
119
|
+
(`reapOnExit`);
|
|
97
120
|
- discovery reads members eight at a time (`DISCOVERY_CONCURRENCY`) with
|
|
98
121
|
serial results: declaration order, the first failure in that order. The
|
|
99
122
|
observations and the member reads are two pools, so a discovery runs at
|
|
@@ -102,6 +125,45 @@ gets the plain per-call behaviour. Within a session:
|
|
|
102
125
|
shared pool would deadlock: a member read holding a slot waits on its
|
|
103
126
|
member's observation, which needs a slot of its own.
|
|
104
127
|
|
|
128
|
+
The cache repos are partial: a commit is fetched with all its trees and
|
|
129
|
+
only the blobs up to `SMALL_BLOB_LIMIT` (64 KiB), which covers every file
|
|
130
|
+
discovery reads, so listings and discovery stay local after one fetch. A
|
|
131
|
+
read that needs a larger blob, or a `fetchRemoteTree` of a module, fetches
|
|
132
|
+
the missing blobs first in one fetch by id (`ensureBlobs`), then applies the
|
|
133
|
+
budgets to their real sizes before anything is written. git never fetches a
|
|
134
|
+
blob lazily (`GIT_NO_LAZY_FETCH=1`, and no url is stored in the cache: each
|
|
135
|
+
fetch passes it with `-c remote.origin.url=`). A server without partial
|
|
136
|
+
fetches gets whole trees; the cache records that (`oats.fetch = full` in its
|
|
137
|
+
config) and the CLI prints the session's notice once, on stderr. Partial
|
|
138
|
+
caches need git 2.45 or later (`PARTIAL_FETCH_GIT`, the first git with
|
|
139
|
+
`GIT_NO_LAZY_FETCH`): with an older git every cache fetches whole trees, a
|
|
140
|
+
partial cache it meets is deleted and fetched again whole, and the same
|
|
141
|
+
notice says why.
|
|
142
|
+
|
|
143
|
+
Every write to a cache repo (its `git init`, config, fetches and pins) holds
|
|
144
|
+
the repo's cross-process write lock, `<cache>/.locks/<repo>.lock`
|
|
145
|
+
(`withCacheWriteLock`): an exclusive file holding `{pid, token, startedAt}`,
|
|
146
|
+
waited for while its holder lives (bounded by a whole fetch, then
|
|
147
|
+
`reason: "cache"` naming the pid), reclaimed when the holder is dead, and
|
|
148
|
+
released only by its owner. Reclaimers take a short guard,
|
|
149
|
+
`<lock>.reclaim`, and check under it that the lock is still the dead
|
|
150
|
+
record before removing it, so a reclaimer that paused cannot delete a
|
|
151
|
+
live process's new lock. A guard whose holder died is never removed
|
|
152
|
+
automatically (that removal would race the same way, with nothing left to
|
|
153
|
+
serialize it): every write refuses at once, `reason: "cache"` naming the
|
|
154
|
+
guard (`details.guard`), until a human removes it once no oats process is
|
|
155
|
+
running. Reads take no lock. A cache repo appears whole
|
|
156
|
+
(`git init` into a private directory, then a rename), so processes making
|
|
157
|
+
the first fetch of one remote all succeed. A git `*.lock` a write meets is
|
|
158
|
+
judged under that lock (`cacheGit`): older oats kernels take no write lock,
|
|
159
|
+
so it is retried briefly, then removed only when it is inside the cache
|
|
160
|
+
repo, a regular file and older than the longest fetch
|
|
161
|
+
(`GIT_FETCH_TIMEOUT_MS` plus a margin): a git killed mid-write. A removal
|
|
162
|
+
is said once as a warning. Anything else is `reason: "cache"` naming the
|
|
163
|
+
file and when it is safe to remove; so is any other local write failure
|
|
164
|
+
(a `FETCH_HEAD` git cannot open, a read-only or full disk), with git's own
|
|
165
|
+
words.
|
|
166
|
+
|
|
105
167
|
Across commands, `memoAtCommit` keeps parsed reads under
|
|
106
168
|
`<cache>/.parsed/<kernel fingerprint>/`, keyed by (repo key, full commit,
|
|
107
169
|
item). The items: `workspace` (the workspace file), `membership` (a member's
|
package/docs/integrations.md
CHANGED
package/docs/official-catalog.md
CHANGED
|
@@ -11,8 +11,8 @@ or workspace membership alone does not make a package official.
|
|
|
11
11
|
|---|---|---|---|
|
|
12
12
|
| `oats.framework` | `oats-framework/v1.4.1` (this repository) | `oats.core`, `oats.setup`, `oats.knowledge-theory` | `knowledge-theory-expert` |
|
|
13
13
|
| `oats.okf` | `v4.0.5` | `oats.okf` (knowledge), `oats.okf-harvest`, `oats.okf-maintenance` | `knowledge-harvester`, `knowledge-maintainer` |
|
|
14
|
-
| `oats.aweb` | `v1.17.
|
|
15
|
-
| `oats.engineering` | `v1.
|
|
14
|
+
| `oats.aweb` | `v1.17.5` | `oats.aweb` (messaging) | |
|
|
15
|
+
| `oats.engineering` | `v1.4.0` | `oats.engineering-expert`, `oats.developer`, `oats.code-review` | `code-reviewer` |
|
|
16
16
|
| `oats.authoring` | `v1.0.3` | `oats.authoring` | |
|
|
17
17
|
| `oats.jira` | `v1.0.1` | `oats.jira` (tasks) | |
|
|
18
18
|
| `oats.linear` | `v1.0.1` | `oats.linear` (tasks) | |
|
package/docs/packages.md
CHANGED
|
@@ -76,7 +76,7 @@ members:
|
|
|
76
76
|
packages:
|
|
77
77
|
oats.framework: v1.4.1
|
|
78
78
|
oats.okf: v4.0.5
|
|
79
|
-
oats.aweb: v1.17.
|
|
79
|
+
oats.aweb: v1.17.5
|
|
80
80
|
teams:
|
|
81
81
|
platform: { team: "platform:acme.aweb.ai", description: Platform engineering }
|
|
82
82
|
defaults:
|
|
@@ -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.5 # 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
|
```
|