@awebai/oats 0.25.0 → 0.25.1
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 +68 -21
- package/docs/conventions.md +51 -24
- package/docs/design/2026-09-20-redesign-program-board.md +1 -1
- package/docs/design/2026-09-23-simplified-workspace-model.md +1 -1
- package/docs/design/2026-09-23-workspace-module-contracts.md +151 -0
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +2 -2
- package/docs/desktop-cli-api.md +32 -0
- package/docs/desktop-succession.md +9 -2
- package/docs/desktop.md +9 -4
- package/docs/execution-targets.md +16 -4
- package/docs/implementation.md +35 -7
- package/docs/integrations.md +5 -3
- package/docs/knowledge-migration.md +16 -8
- package/docs/knowledge.md +36 -10
- package/docs/migration-from-oas.md +20 -9
- package/docs/rebuild-to-v2.md +119 -5
- package/docs/release-notes/v0.25.1.md +94 -0
- package/docs/schedules.md +12 -6
- package/docs/souls-and-instances.md +2 -2
- package/docs/workspaces.md +8 -1
- package/lib/core.mjs +45 -8
- package/lib/instance-resolution.mjs +96 -21
- package/lib/operator-dispatch.mjs +117 -0
- package/lib/packages.mjs +62 -1
- package/lib/remote.mjs +128 -49
- package/lib/resolve.mjs +70 -8
- package/lib/workspace.mjs +25 -6
- package/package.json +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -36,9 +36,8 @@ import {
|
|
|
36
36
|
} from "../lib/core.mjs";
|
|
37
37
|
import {
|
|
38
38
|
assertNoSymlinkedParents, writeFileAtomic,
|
|
39
|
-
LOCK_FILE, readLock, writeLock, resolvePackages, approve as approvePackage,
|
|
40
|
-
classifyPackageValue,
|
|
41
|
-
} from "../lib/packages.mjs";
|
|
39
|
+
LOCK_FILE, readLock, writeLock, resolvePackages, approve as approvePackage,
|
|
40
|
+
classifyPackageValue, parsePackageRequest, executablesDigestAt } from "../lib/packages.mjs";
|
|
42
41
|
import { loadLocal, discoverWorkspace, validateWorkspace } from "../lib/workspace.mjs";
|
|
43
42
|
import * as remoteModule from "../lib/remote.mjs";
|
|
44
43
|
import { createInterface } from "node:readline/promises";
|
|
@@ -1941,10 +1940,12 @@ function printTable(header, rows) {
|
|
|
1941
1940
|
|
|
1942
1941
|
/** Executables digest of a locked package, read over the remote at its locked commit. */
|
|
1943
1942
|
async function lockedExecutablesDigest(id, entry, workspace, catalog, remoteOptions) {
|
|
1943
|
+
// ONE definition of the approval digest (lib/packages.mjs executablesDigestAt) — the
|
|
1944
|
+
// same function resolveSoul re-runs at spawn (M3), so sync and spawn can never disagree.
|
|
1944
1945
|
const req = parsePackageRequest(id, workspace.packages[id], catalog);
|
|
1945
|
-
const
|
|
1946
|
-
const targets =
|
|
1947
|
-
return { digest
|
|
1946
|
+
const { digest, executables } = await executablesDigestAt(remoteModule, req.remoteRef, entry.commit, entry.path, entry.capabilities ?? null, { remoteOptions });
|
|
1947
|
+
const targets = executables.map((x) => `${x.capability}: ${x.kind} ${x.name} → ${x.target}`);
|
|
1948
|
+
return { digest, targets };
|
|
1948
1949
|
}
|
|
1949
1950
|
|
|
1950
1951
|
/** One yes/no question on the terminal (TTY only; the caller checks). */
|
|
@@ -3001,18 +3002,27 @@ function createCmd() {
|
|
|
3001
3002
|
* oats <namespace> <command> [args…] — run a command an active capability
|
|
3002
3003
|
* declares in its manifest (`commands: { name: "script args" }`).
|
|
3003
3004
|
* Kernel subcommands take precedence over capability namespaces.
|
|
3005
|
+
*
|
|
3006
|
+
* Three contexts, one contract (OATS_CAPABILITY / OATS_SETTINGS / OATS_CLI_BIN):
|
|
3007
|
+
* - inside an instance home: the home's materialized modules (instance.json.modules);
|
|
3008
|
+
* - from a v2 DEPLOYMENT (oats-local.yaml in reach, no home): operator-level
|
|
3009
|
+
* dispatch — resolve exactly as `oats spawn --soul <x>` would, fetch the
|
|
3010
|
+
* namespace's capability into <deployment>/.oats/modules/<cap>@<commit12>/
|
|
3011
|
+
* and run THAT copy with the soul's merged payload (lib/operator-dispatch.mjs;
|
|
3012
|
+
* contracts doc, "Post-0.25.0 clarifications");
|
|
3013
|
+
* - otherwise the classic config chain.
|
|
3004
3014
|
*/
|
|
3005
|
-
function capabilityCommand() {
|
|
3015
|
+
async function capabilityCommand() {
|
|
3006
3016
|
// JSON-aware boundary: in --json mode every dispatch failure — inactive or
|
|
3007
3017
|
// untrusted capability, duplicate namespace, unknown subcommand, broken
|
|
3008
3018
|
// metadata/manifests, malformed command values — must still emit exactly
|
|
3009
3019
|
// one envelope object on stdout. The WHOLE dispatcher runs inside the
|
|
3010
3020
|
// boundary; only "no namespace matched" escapes (returns false to the help
|
|
3011
3021
|
// fallthrough).
|
|
3012
|
-
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
3022
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
3013
3023
|
const NOT_DISPATCHED = Symbol("not-dispatched");
|
|
3014
3024
|
let outcome;
|
|
3015
|
-
try { outcome = dispatch(); }
|
|
3025
|
+
try { outcome = await dispatch(); }
|
|
3016
3026
|
catch (e) {
|
|
3017
3027
|
// Unexpected throw from discovery/trust/decoding: keep the envelope contract.
|
|
3018
3028
|
bail("E_CAPABILITY_BROKEN", e.message || e);
|
|
@@ -3020,7 +3030,23 @@ function capabilityCommand() {
|
|
|
3020
3030
|
}
|
|
3021
3031
|
return outcome !== NOT_DISPATCHED;
|
|
3022
3032
|
|
|
3023
|
-
|
|
3033
|
+
/** Operator-level dispatch from a deployment directory (no instance home). */
|
|
3034
|
+
async function operatorDispatch() {
|
|
3035
|
+
let hit;
|
|
3036
|
+
try {
|
|
3037
|
+
const { resolveOperatorDispatch } = await import("../lib/operator-dispatch.mjs");
|
|
3038
|
+
let catalog = null; try { catalog = officialPackageCatalog(); } catch { /* the lock carries url for catalog packages */ }
|
|
3039
|
+
hit = await resolveOperatorDispatch(process.cwd(), cmd, flag("soul"), { remoteOptions: remoteOptionsFromEnv(), catalog });
|
|
3040
|
+
} catch (e) {
|
|
3041
|
+
if (typeof e?.code === "string" && e.code.startsWith("E_")) bail(e.code, e.message, e.details);
|
|
3042
|
+
throw e;
|
|
3043
|
+
}
|
|
3044
|
+
if (!hit) return NOT_DISPATCHED;
|
|
3045
|
+
const teamCtx = hit.soul?.team ? { name: hit.soul.team } : undefined;
|
|
3046
|
+
return runManifestCommand({ capability: hit.module.name, ...hit.manifest }, hit.settings, teamCtx, hit.ensureTree);
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
async function dispatch() {
|
|
3024
3050
|
let activeIds;
|
|
3025
3051
|
let context = process.cwd();
|
|
3026
3052
|
let teamCtx;
|
|
@@ -3032,6 +3058,7 @@ function capabilityCommand() {
|
|
|
3032
3058
|
// namespace the operator typed on the command line.
|
|
3033
3059
|
let capSettings = Object.create(null);
|
|
3034
3060
|
let instanceModules = false;
|
|
3061
|
+
let deployment = null;
|
|
3035
3062
|
try {
|
|
3036
3063
|
if (metaFile && existsSync(metaFile)) {
|
|
3037
3064
|
const meta = JSON.parse(readFileSync(metaFile, "utf8"));
|
|
@@ -3043,12 +3070,19 @@ function capabilityCommand() {
|
|
|
3043
3070
|
// spawned before a team: block was declared have no snapshot.
|
|
3044
3071
|
teamCtx = meta.team || resolveOatsConfig(context).team;
|
|
3045
3072
|
} else {
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3073
|
+
// Not inside a home: a v2 deployment (oats-local.yaml in reach) resolves
|
|
3074
|
+
// through the workspace, exactly as a spawn of --soul would (below).
|
|
3075
|
+
try { const { deploymentOf } = await import("../lib/operator-dispatch.mjs"); deployment = deploymentOf(context); }
|
|
3076
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
3077
|
+
if (!deployment) {
|
|
3078
|
+
const resolved = resolveOatsConfig(context, flag("soul"));
|
|
3079
|
+
activeIds = resolved.capabilities.map((c) => c.id);
|
|
3080
|
+
for (const c of resolved.capabilities) capSettings[c.id] = c.settings || {};
|
|
3081
|
+
teamCtx = resolved.team;
|
|
3082
|
+
}
|
|
3050
3083
|
}
|
|
3051
3084
|
} catch (e) { bail("E_CONFIG_BROKEN", e.message || e); throw e; }
|
|
3085
|
+
if (deployment) return operatorDispatch();
|
|
3052
3086
|
// Workspace model: an instance's own materialized modules are the command
|
|
3053
3087
|
// namespaces available to it (instance.json.modules → <home>/.oats/modules).
|
|
3054
3088
|
const mans = Object.values(capabilityManifests(instanceModules ? instanceHome : context)).filter((m) => m.command === cmd && m.commands);
|
|
@@ -3058,6 +3092,14 @@ function capabilityCommand() {
|
|
|
3058
3092
|
if (!activeIds.includes(m.capability)) bail("E_CAPABILITY_INACTIVE", `${m.capability} command namespace is not active in the current context/instance`);
|
|
3059
3093
|
const trust = capabilityTrust(m, context);
|
|
3060
3094
|
if (!trust.trusted) bail("E_CAPABILITY_BLOCKED", `${m.capability} executable command is blocked: ${trust.reason}`);
|
|
3095
|
+
return runManifestCommand(m, capSettings[m.capability] || {}, teamCtx, () => m._dir);
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
/** Help / unknown-command / spec validation / exec — shared by every context.
|
|
3099
|
+
* `m` is the manifest (with `capability`; `_dir` may be absent until `ensureDir`
|
|
3100
|
+
* resolves the directory holding the executable — the operator branch fetches
|
|
3101
|
+
* the module tree only when a command is actually going to run). */
|
|
3102
|
+
async function runManifestCommand(m, settings, teamCtx, ensureDir) {
|
|
3061
3103
|
const sub = args[1];
|
|
3062
3104
|
const cmds = Object.keys(m.commands);
|
|
3063
3105
|
// `oats <ns> --help` and `oats <ns> <cmd> --help` answer from the manifest
|
|
@@ -3083,17 +3125,22 @@ function capabilityCommand() {
|
|
|
3083
3125
|
const spec = m.commands[sub];
|
|
3084
3126
|
if (typeof spec !== "string" || !spec.trim()) bail("E_CAPABILITY_BROKEN", `oats ${cmd} ${sub}: manifest command must be a non-empty string (got ${JSON.stringify(spec)})`);
|
|
3085
3127
|
const [script, ...rest] = spec.trim().split(/\s+/);
|
|
3128
|
+
let dir;
|
|
3129
|
+
try { dir = await ensureDir(); }
|
|
3130
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
3131
|
+
const withDir = { ...m, _dir: dir };
|
|
3086
3132
|
let abs;
|
|
3087
|
-
try { abs = capabilityExecutablePath(
|
|
3133
|
+
try { abs = capabilityExecutablePath(withDir, script); }
|
|
3088
3134
|
catch (e) { bail("E_CAPABILITY_BROKEN", e.message); }
|
|
3089
|
-
if (!abs) bail("E_CAPABILITY_BROKEN", `${cmd} ${sub}: script not found (${join(
|
|
3135
|
+
if (!abs) bail("E_CAPABILITY_BROKEN", `${cmd} ${sub}: script not found (${join(dir, script)})`);
|
|
3090
3136
|
const r = spawnSync("node", [abs, ...rest, ...args.slice(2)], { stdio: "inherit", env: {
|
|
3091
3137
|
...process.env, OATS_CAPABILITY: m.capability,
|
|
3092
3138
|
// Package-runtime boundary: dispatched commands receive the active
|
|
3093
|
-
// capability's EFFECTIVE settings (instance snapshot
|
|
3094
|
-
//
|
|
3095
|
-
//
|
|
3096
|
-
|
|
3139
|
+
// capability's EFFECTIVE settings (instance snapshot, resolved context, or
|
|
3140
|
+
// the soul's merged payload on operator-level dispatch), same contract as
|
|
3141
|
+
// lifecycle hooks — capabilities read their settings here instead of
|
|
3142
|
+
// importing the kernel resolver.
|
|
3143
|
+
OATS_SETTINGS: JSON.stringify(settings || {}),
|
|
3097
3144
|
// PATH is not a trusted runtime boundary (maintainer finding 1): pass the
|
|
3098
3145
|
// canonical absolute executable of THIS CLI; official consumers execFile
|
|
3099
3146
|
// it directly and never resolve `oats` from PATH or a shell.
|
|
@@ -3675,7 +3722,7 @@ else if (cmd && Object.hasOwn(REMOVED_VERBS, cmd)) {
|
|
|
3675
3722
|
console.log(usageText());
|
|
3676
3723
|
process.exit(1);
|
|
3677
3724
|
}
|
|
3678
|
-
else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && capabilityCommand()) { /* dispatched */ }
|
|
3725
|
+
else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && await capabilityCommand()) { /* dispatched */ }
|
|
3679
3726
|
// No matching kernel command or capability namespace: in --json mode the help
|
|
3680
3727
|
// text must NOT contaminate stdout — still one envelope object, nonzero exit.
|
|
3681
3728
|
else if (cmd && !cmd.startsWith("--") && !HELP_WORDS.has(cmd) && JSON_MODE) jsonFail("E_UNKNOWN_COMMAND", `unknown command "${cmd}" — no kernel subcommand or active capability namespace matches`);
|
package/docs/conventions.md
CHANGED
|
@@ -31,43 +31,70 @@ instance/.claude/skills -> ../.agents/skills
|
|
|
31
31
|
|
|
32
32
|
Spawn copies kernel + soul-private + active capability skills into real
|
|
33
33
|
instance-local directories there. Directory symlinks are not used because
|
|
34
|
-
harness recursive discovery may not descend through them.
|
|
35
|
-
|
|
34
|
+
harness recursive discovery may not descend through them. Under the workspace
|
|
35
|
+
model (0.25) every capability is copied whole into
|
|
36
|
+
`instance/.oats/modules/<capability>/` and its skills into
|
|
37
|
+
`instance/.agents/skills/<capability>/<skill>/`; nothing is installed at a
|
|
38
|
+
config level. Config-level `.agents/skills` is not an OATS capability source
|
|
36
39
|
or an ambient runtime discovery root.
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
and
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
Harnesses start normally (workspace-model decision 13, and the behaviour of
|
|
42
|
+
every launch a 0.25 kernel performs, classic homes included): Pi starts with
|
|
43
|
+
cwd = the instance home, the composed `AGENTS.md` appended to its system
|
|
44
|
+
prompt, and its own skill, context and extension discovery intact — the
|
|
45
|
+
instance's copied skills are found because they sit under cwd; machine-level
|
|
46
|
+
and repo-level skills resolve exactly as without OATS. *(0.24 kernels started
|
|
47
|
+
Pi with ambient skill and context discovery disabled and the one instance path
|
|
48
|
+
explicit; that exclusion is gone.)* Claude runs provider-native: it reads the
|
|
49
|
+
instance's `.claude/skills` and `CLAUDE.md` symlinks, and the operator's own
|
|
50
|
+
user and project configuration — skills, plugins, settings — stays in effect.
|
|
51
|
+
Neither runtime gets a redirected config home.
|
|
52
|
+
`composition.materialized.runtimePosture` in `instance.json` records what each
|
|
53
|
+
instance actually exposes. `oats-getting-started` is the sole pre-workspace
|
|
54
|
+
ambient bootstrap.
|
|
55
|
+
|
|
56
|
+
Duplicate skill directory names within the OATS-composed set are errors
|
|
57
|
+
(`E_SKILL_DUPLICATE`, naming both capabilities) unless the classic config's
|
|
58
|
+
`skill-overrides` selects a source; between a composed skill and an ambient
|
|
59
|
+
repo/machine skill the harness's own precedence decides (decision 16).
|
|
49
60
|
|
|
50
61
|
## Package locations
|
|
51
62
|
|
|
63
|
+
**Workspace model (0.25, current):** nothing is installed. A capability lives
|
|
64
|
+
where its owner keeps it and is copied whole into each instance at spawn:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
<member repo>/capabilities/<name>/oats.json # member-tier capability, latest state (membership is the trust)
|
|
68
|
+
<package repo>/oats-package/oats-package.json # package-tier: versioned via oats-workspace.yaml packages:
|
|
69
|
+
<deployment>/oats-lock.json # lockfileVersion 3: package commit, integrity, per-version approval
|
|
70
|
+
<instance>/.oats/modules/<capability>/ # the copy this instance runs
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Classic 0.24 layout** (still launched by the 0.24 kernel; a 0.25 kernel
|
|
74
|
+
reads none of it as configuration — see [rebuild-to-v2.md](rebuild-to-v2.md)):
|
|
75
|
+
|
|
52
76
|
```text
|
|
53
77
|
<package>/capabilities/<name>/oats.json # the official marketplace (install source, not ambient)
|
|
54
78
|
<level>/.agents/capabilities/installed/<name>/oats.json # acquired (gitignored, restorable)
|
|
55
79
|
<level>/.agents/capabilities/owned/<name>/oats.json # authored at this scope (source; committed where the scope is a repo)
|
|
56
|
-
<level>/oats-lock.json # external source/integrity/trust
|
|
80
|
+
<level>/oats-lock.json # lockfileVersion 2: external source/integrity/trust
|
|
57
81
|
```
|
|
58
82
|
|
|
59
83
|
## Quick map
|
|
60
84
|
|
|
61
|
-
| Thing | Canonical location |
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
| Soul
|
|
67
|
-
| Soul
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
| Instance
|
|
85
|
+
| Thing | Canonical location (0.25 workspace model) | 0.24 classic |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| Shared declaration | `oats-workspace.yaml` in the host repo; `oats-membership.yaml` in every member | `oats-config.yaml` chain, `oats.yaml` |
|
|
88
|
+
| Per-machine config | `<deployment>/oats-local.yaml` (uncommitted) | `oats-config.yaml` `settings:` |
|
|
89
|
+
| Acquisition lock | `<deployment>/oats-lock.json` (v3) | `<level>/oats-lock.json` (v2) |
|
|
90
|
+
| Soul source | `<member repo>/souls/<name>/` | `agents/<name>/soul/` |
|
|
91
|
+
| Soul operating doc | `souls/<name>/AGENTS.md` | `soul/AGENTS.md` |
|
|
92
|
+
| Soul Claude view | `souls/<name>/CLAUDE.md -> AGENTS.md` | `soul/CLAUDE.md -> AGENTS.md` |
|
|
93
|
+
| Soul-private skills | `souls/<name>/skills/` | `soul/skills/` |
|
|
94
|
+
| Instance operating doc | `instance/AGENTS.md` (generated) | same |
|
|
95
|
+
| Instance skill set | `instance/.agents/skills/` | same |
|
|
96
|
+
| Instance modules | `instance/.oats/modules/<capability>/` | `.agents/capabilities/installed/` (shared) |
|
|
97
|
+
| Instance metadata | `instance/instance.json` (`modules{}`, `providers{}`, `workspace{}`) | `instance/instance.json` |
|
|
71
98
|
|
|
72
99
|
Symlinks prevent compatibility paths from drifting. Generated regular files
|
|
73
100
|
separate canonical portable identity from scope-dependent runtime policy.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Purpose:** the one accurate view of every work stream in the redesign, what is on main, what is in flight, who owns it, and what blocks it. Lead: `oats-expert` (redesign lead). Updated whenever anything merges, is returned, or reality changes. Older per-lane boards are superseded by this file.
|
|
4
4
|
|
|
5
|
-
**Last update:** 2026-09-23 17:40Z · **Workspace model v2 phases A–C ON MAIN (`59ae22df`, PR99) →
|
|
5
|
+
**Last update:** 2026-09-23 17:40Z · **Workspace model v2 phases A–C ON MAIN (`59ae22df`, PR99) → **`v0.25.0` PUBLISHED** (bump `dc333e4e`)** · Phase D next (framework repos as the first workspace; six expert souls; `oats.core`/`oats.setup` rewrite) · ⏸ parity pipeline still paused except 10B-0 (→ 0.24.14 when the engineer hands off; note: 0.24.14 must be cut from the 0.24 line, not main).
|
|
6
6
|
|
|
7
7
|
Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
|
|
8
8
|
|
|
@@ -510,7 +510,7 @@ Today's `capabilities.layers.messaging.{capability, from, global, souls, setting
|
|
|
510
510
|
|---|---|---|
|
|
511
511
|
| True of every instance of the soul | `soul.yaml` → `messaging:` / `knowledge:` | `messaging: { channels: [northwind-eng] }`, `knowledge: { owns: release-manager }` |
|
|
512
512
|
| A fact about this machine | `oats-local.yaml` → `settings.<capability>.<key>` (absolute paths refused in the workspace file) | `settings.oats.okf.state-dir: /Users/ana/.oats/okf` |
|
|
513
|
-
| A fact about **this spawn** | `oats spawn … --provider <cap> key=value` → `instance.json` `providers.<cap>` (the Desktop's confirmed apply carries the same map) | `--provider oats.aweb identity.source
|
|
513
|
+
| A fact about **this spawn** | `oats spawn … --provider <cap> key=value` → `instance.json` `providers.<cap>` (the Desktop's confirmed apply carries the same map) | `--provider oats.aweb identity.source=/abs/path/to/retained/.aw` — one instance takes the retained seat; other instances of the soul mint fresh |
|
|
514
514
|
|
|
515
515
|
The provider's `binding` contract (`normalize → bind → check`) runs over the merged payload exactly as today; the provider still enforces its own rules (e.g. a state root outside the work tree).
|
|
516
516
|
|
|
@@ -307,3 +307,154 @@ Appended, not edited in place; each item names the section it refines. Decision
|
|
|
307
307
|
**§6 `oats package remove <id>` (M15).** BOTH branches — the file tracked by the checkout (edited) and untracked/absent (not edited) — answer `E_PACKAGE_MISSING { id, path? }` when `<id>` is not declared in `packages:`. The tracked receipt is `{ action: "remove", id, value: null, previous: <old value>, edited: true, file }`; the untracked receipt is `{ action: "remove", id, value: null, edited: false, file: null, line: null, hint }` — `previous` is absent and `line` is `null` (a removal has no line to add).
|
|
308
308
|
|
|
309
309
|
**§6 features.** `catalog` is no longer advertised in `oats version --json` `features` (the verb is removed; the official catalog is reached through `packages:` + `sync`, not a command).
|
|
310
|
+
|
|
311
|
+
## Post-0.25.0 clarifications (team review, 2026-09-23)
|
|
312
|
+
|
|
313
|
+
**Capability commands outside an instance home (`oats <ns> <cmd>` from the
|
|
314
|
+
deployment).** Inside an instance home the dispatcher resolves the namespace from
|
|
315
|
+
the home's materialized modules (`instance.json.modules` → `<home>/.oats/modules`);
|
|
316
|
+
that shipped in 0.25.0. Outside a home — the operator acts a knowledge layer needs
|
|
317
|
+
before any instance exists (`oats okf init`, base migration) — the intended rule
|
|
318
|
+
is: **resolve exactly as a spawn of `--soul <name>` would** (`prepareInstance` →
|
|
319
|
+
the soul's Resolution), fetch the namespace's capability into the deployment's
|
|
320
|
+
module store `<deployment>/.oats/modules/<cap>@<commit>/` (the same per-commit
|
|
321
|
+
store capability-defined agents use), and dispatch to that copy with the soul's
|
|
322
|
+
merged payload as `OATS_SETTINGS`. Never "the newest instance's copy" (an
|
|
323
|
+
instance is not an authority for the deployment) and never an unlocked cache
|
|
324
|
+
read (the lock's approval is the gate, as for spawn). `--soul` is required when
|
|
325
|
+
the namespace's capability is not a workspace default. **Status: 0.25.x
|
|
326
|
+
follow-up** — 0.25.0 still answers `E_CAPABILITY_INACTIVE` there (the pre-v2
|
|
327
|
+
chain); the interim is to run the module binary directly with `OATS_SETTINGS`
|
|
328
|
+
and `OATS_CLI_BIN`, as the tarball smoke does.
|
|
329
|
+
|
|
330
|
+
**`work: workspace` is kept.** A coordination soul's `./work` is the deployment
|
|
331
|
+
boundary — the directory holding `oats-local.yaml` (the taught
|
|
332
|
+
`<name>-workspace/`, with member clones beside it) — read-only across member
|
|
333
|
+
clones, no branch recorded. The clone map in `oats-local.yaml` (`clones:`) is
|
|
334
|
+
how such a soul finds a member whose clone is elsewhere. **Status: the
|
|
335
|
+
directory link is the intent; 0.25.0's kernel still derives the boundary from
|
|
336
|
+
the classic `team:` scope (`docs/souls-and-instances.md` open thread) — 0.25.x
|
|
337
|
+
follow-up binds it to the `oats-local.yaml` directory.**
|
|
338
|
+
|
|
339
|
+
**`identity.source` (oats.aweb) is the absolute path of the `.aw` directory to
|
|
340
|
+
retain**, given per spawn (`--provider oats.aweb identity.source=/abs/.aw`) or
|
|
341
|
+
per machine (`oats-local.yaml settings.oats.aweb.identity.source`); the kernel
|
|
342
|
+
resolves no symbolic seat names. Absolute paths never enter the workspace file
|
|
343
|
+
(decision 14).
|
|
344
|
+
|
|
345
|
+
**Member capabilities are a code-execution boundary** (decision 2: membership is
|
|
346
|
+
the trust; hooks and scripts of every member's default branch run on every
|
|
347
|
+
operator's machine at spawn). For a mixed public/private organisation the
|
|
348
|
+
recommendation is: **souls only in public members; executable capabilities come
|
|
349
|
+
from packages (approved per version) or from private members.** The onboarding
|
|
350
|
+
skill (Phase E) states this beside the hosting rule (decision 26).
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
### 0.25.1 fix round (team review, 2026-09-23) — appended, not edited in place
|
|
354
|
+
|
|
355
|
+
Each item names the section it refines and the review finding it closes. The
|
|
356
|
+
implementation lands in kernel 0.25.1 (`docs/release-notes/v0.25.1.md`); no
|
|
357
|
+
API integer or feature name changes.
|
|
358
|
+
|
|
359
|
+
**§3 slot `none` (L1).** A soul's `none` for a slot **empties the slot and drops
|
|
360
|
+
any layer-bearing capability the WORKSPACE DEFAULTS contributed for that
|
|
361
|
+
layer** — whether it arrived through `defaults.<slot>`, `defaults.capabilities`
|
|
362
|
+
or `defaults.byTeam[team].capabilities`. A layer-bearing capability **the soul
|
|
363
|
+
itself declares** in its own `capabilities:` alongside `none` for that layer is
|
|
364
|
+
`E_SLOT_CONFLICT { reason: "none" }` (spell `<cap>: off` to remove it). This
|
|
365
|
+
replaces the Phase B reading under which a layered default arriving via
|
|
366
|
+
`defaults.capabilities` was itself a conflict: the workspace's choice is a
|
|
367
|
+
default and `none` is the soul's answer to it; only the soul contradicting
|
|
368
|
+
itself is loud.
|
|
369
|
+
|
|
370
|
+
**§2/§5 per-commit soul cache (M1).** `ensureWorkspaceSoul` fetches a soul's
|
|
371
|
+
source at `(repoKey, commit)` into `<deployment>/agents/<name>/souls/<commit12>/`
|
|
372
|
+
— **immutable once written** (staged, then renamed in; never removed by the
|
|
373
|
+
kernel) — and maintains `<deployment>/agents/<name>/soul` as a **symlink to the
|
|
374
|
+
current commit's directory**, swapped atomically (symlink to a temp name +
|
|
375
|
+
rename over) so classic readers (`findAgent`, `doctor`, the classic spawn path)
|
|
376
|
+
keep seeing "current". A spawned home's `<home>/soul` links **its own commit's
|
|
377
|
+
directory** (the realpath of `souls/<commit12>/`), never the swapped pointer:
|
|
378
|
+
a running instance's soul never changes under it (decision 7), a `--preview`
|
|
379
|
+
may fetch a new commit and swap the pointer without touching any directory an
|
|
380
|
+
instance links, and OKF 2's owner pin (`owners.json` = `realpath(<home>/soul)`)
|
|
381
|
+
stays valid for the instance that registered it. `.oats-soul-source.json`
|
|
382
|
+
remains the stamp of "current". A 0.25.0 layout (`agents/<name>/soul` a real
|
|
383
|
+
directory, no `souls/`) is migrated in place on first use: the directory moves
|
|
384
|
+
to `souls/<commit from the stamp, else unknown>/` and the pointer replaces it.
|
|
385
|
+
A soul symlink whose target lies inside the same `agents/<name>/souls/` is the
|
|
386
|
+
one kernel-owned symlink soul readers accept.
|
|
387
|
+
|
|
388
|
+
**§1 transport (M2).** `parseRepoRef(ref).key` is unchanged — `<host>/<path>`
|
|
389
|
+
is the identity everywhere and every comparison is by key. The **fetch url
|
|
390
|
+
honours the form written**: `git@host:org/repo(.git)` and `ssh://…` fetch over
|
|
391
|
+
SSH as written; `https://…` fetches over HTTPS; the bare scheme
|
|
392
|
+
`git:host/org/repo` fetches over HTTPS by default **unless
|
|
393
|
+
`remoteOptions.transport === "ssh"`** (a per-machine choice; `oats-local.yaml`
|
|
394
|
+
may carry it once the schema admits it — reported by lane 3, not landed here).
|
|
395
|
+
The operator's SSH access is therefore used when the operator wrote an SSH ref,
|
|
396
|
+
and a private repo no longer degrades to `not-found` → standalone through an
|
|
397
|
+
unintended HTTPS probe. When the standalone fallback engages the discovery
|
|
398
|
+
carries `standaloneReason`/`hostFailure { code, reason, url }` so the CLI can
|
|
399
|
+
print *why*.
|
|
400
|
+
|
|
401
|
+
**§3/§4 approval re-verified at spawn (M3).** For a `from: package` module
|
|
402
|
+
`resolveSoul` recomputes `executablesDigest` over the package tree **at the
|
|
403
|
+
locked `entry.commit`** and requires equality with `entry.approved.executables`
|
|
404
|
+
→ else `E_PACKAGE_UNAPPROVED { reason: "digest-mismatch", approved, actual }`.
|
|
405
|
+
The one digest definition is `lib/packages.mjs#executablesDigestAt(remote, ref,
|
|
406
|
+
commit, path, capabilities)`, shared by `sync` and `resolve`; a hand-edited lock
|
|
407
|
+
(same id/version, different commit, copied approval) can no longer materialize
|
|
408
|
+
and run unapproved hooks. Cached per `(id, commit)` within a process.
|
|
409
|
+
|
|
410
|
+
**§1 peeled commit OIDs (M4).** `observeRemote(ref, { at })` accepts an
|
|
411
|
+
annotated tag's OID (or name) but **records the peeled commit** (`<oid>^{commit}`)
|
|
412
|
+
as `commit` — in its result, in the lock, in `fetchRemoteTree`'s errors and in
|
|
413
|
+
every `instance.json` record. A tag OID is never stored where a commit is
|
|
414
|
+
expected.
|
|
415
|
+
|
|
416
|
+
**§1 listing hygiene (L3, L4).** `listRemoteTree` filters by depth **before**
|
|
417
|
+
asserting entry-name safety, so one unsafe deep name does not blank a member's
|
|
418
|
+
souls (unsafe names at the kept depth are still `E_REMOTE_TREE_UNSAFE`). A git
|
|
419
|
+
child killed for `maxBuffer` (`ENOBUFS`) is not reported as `timeout`; an
|
|
420
|
+
unclassified listing failure is wrapped as `E_REMOTE_UNREADABLE { reason:
|
|
421
|
+
"unknown" }` so discovery records a problem row instead of aborting.
|
|
422
|
+
|
|
423
|
+
**§2 `validateWorkspace` absolute paths (L2).** The absolute-path refusal
|
|
424
|
+
applies to **ref/path fields only** — `members[]`, `packages` values, `stores`
|
|
425
|
+
values, `external[].source` / `external[].soul`, `defaults.*.from` — never to
|
|
426
|
+
`teams.<label>.description` or to the opaque `messaging` payload.
|
|
427
|
+
|
|
428
|
+
**§3 revision (L6).** `Resolution.revision = hash(declRevision, payloadRevision)`
|
|
429
|
+
where `declRevision` covers the declarations (member/package commits, the
|
|
430
|
+
composed capability set, skills, injects) and `payloadRevision` covers the
|
|
431
|
+
payload layers (soul slot payloads ⊕ `oats-local.yaml settings` ⊕
|
|
432
|
+
`--provider`). Both are exposed on the Resolution; decision binding keeps using
|
|
433
|
+
`revision`, so a settings-only difference still refuses a stale apply, while
|
|
434
|
+
`spawn --preview` can say **`changed since: declarations | payload | both`**
|
|
435
|
+
instead of a bare `changedSince`.
|
|
436
|
+
|
|
437
|
+
**§6 operator-level dispatch (B3, implements the rule stated above).** Outside a
|
|
438
|
+
home, with `oats-local.yaml` present, `oats <ns> <cmd> … --soul <name>` runs
|
|
439
|
+
`prepareInstance(dir, name)`, picks the module whose `manifest.command === <ns>`,
|
|
440
|
+
ensures its tree in `<deployment>/.oats/modules/<cap>@<commit12>/` (member: the
|
|
441
|
+
member repo at `module.from.commit`, `module.dir`; package: the lock entry) and
|
|
442
|
+
dispatches to that copy with `OATS_SETTINGS = resolution.payloads[cap]` and
|
|
443
|
+
`OATS_CLI_BIN`. `--soul` absent → `E_BAD_ARGS` naming it; a namespace no module
|
|
444
|
+
provides → `E_UNKNOWN_COMMAND`. Trust is the resolution's (membership;
|
|
445
|
+
`E_PACKAGE_UNAPPROVED` for an unapproved package).
|
|
446
|
+
|
|
447
|
+
**§5/§6 `work: workspace` under v2 (B2, implements the rule stated above).**
|
|
448
|
+
With `prepared` present, a `work: workspace` soul's `./work` links the
|
|
449
|
+
deployment directory (`prepared.deployment`, the one holding `oats-local.yaml`);
|
|
450
|
+
no branch is recorded; the "needs a declared boundary" remedy names
|
|
451
|
+
`oats-local.yaml`, not `oats-config.yaml`. The classic root is unchanged.
|
|
452
|
+
|
|
453
|
+
**Decision 13 reach (L7).** "Harnesses start normally" is a property of the
|
|
454
|
+
0.25 **launcher**: every `pi` launch a 0.25 kernel performs — module homes and
|
|
455
|
+
classic 0.24 homes alike — starts pi with cwd = home, the composed `AGENTS.md`
|
|
456
|
+
appended, and pi's own skill/context discovery intact. Consequently `oats
|
|
457
|
+
session recompose` refuses a **module home** (`instance.json.modules` present)
|
|
458
|
+
with `E_UNSUPPORTED_MODE` ("re-spawn"); the `session-recompose` feature name
|
|
459
|
+
stays advertised because the verb still serves classic homes
|
|
460
|
+
(`docs/desktop-cli-api.md`).
|
|
@@ -15,7 +15,7 @@ A clean v2 of the kernel's declaration, resolution and launch path: read `oats-w
|
|
|
15
15
|
|
|
16
16
|
## Work packages
|
|
17
17
|
|
|
18
|
-
Each is one PR (or two small ones), reviewed by me, on `main`,
|
|
18
|
+
Each is one PR (or two small ones), reviewed by me, on `main`, as canonical code from the first phase (no seam — see the approach above). Order is dependency order; W1–W3 can proceed in parallel lanes. **Status (2026-09-23): W1–W8 and W10 shipped in OATS 0.25.0 (PR99 `59ae22df`); W8's last part — folding the classic no-`oats-local.yaml` spawn path into the one pipeline and deleting the v1 residue — is a follow-up; W9/W9b/W11/W12 are Phases D–F.**
|
|
19
19
|
|
|
20
20
|
| # | Package | Delivers | Owner | Size |
|
|
21
21
|
|---|---|---|---|---|
|
|
@@ -52,7 +52,7 @@ Each phase = one developer-swarm workflow (parallel agents, disjoint files, agai
|
|
|
52
52
|
| Risk | Mitigation |
|
|
53
53
|
|---|---|
|
|
54
54
|
| Remote access context is subtle (SSH vs HTTPS vs `gh` token; private repos) | W2 uses git itself (`git ls-remote`, `git archive`/shallow fetch) with the operator's configured credential helpers — no new credential store; typed `cannot read` never guesses |
|
|
55
|
-
| `core.mjs` entanglement makes W8 risky | W8 is deletion behind a green W7;
|
|
55
|
+
| `core.mjs` entanglement makes W8 risky | W8 is deletion behind a green W7; `rg` proof of zero importers per module before each deletion (the v1-residue table from the Phase C review); CI + the Northwind fixture + Desktop suites are the gate |
|
|
56
56
|
| Package approval UX ("asks once") in non-interactive spawns | `oats sync` is where approval is asked; `spawn` refuses `E_PACKAGE_UNAPPROVED` pointing at `sync` — never prompts mid-spawn |
|
|
57
57
|
| Latest-state members drift between preview and apply | The resolution records the member commit; apply re-observes and refuses `E_DECISION_STALE` if it moved — same mechanism as today's decision revision |
|
|
58
58
|
| Desktop relying on "installed" | Removed as a state in W12; until then the Capabilities view keeps working on 0.24.x DTOs (contracts unchanged) |
|
package/docs/desktop-cli-api.md
CHANGED
|
@@ -450,6 +450,26 @@ the pre-fix marker and is never accepted for dispatch.
|
|
|
450
450
|
|
|
451
451
|
## Readiness quartet, signatures, enforced policy (`oats readiness`, `readinessApi: 1`, OATS 0.24.8+)
|
|
452
452
|
|
|
453
|
+
> **0.25 status — the producers below are the 0.24 tier.** The readiness DTO
|
|
454
|
+
> (`readinessApi: 1`) still ships unchanged, but its four checks are *produced*
|
|
455
|
+
> by the classic observers: `installed` by `oats list` over the
|
|
456
|
+
> `.agents/capabilities/installed/` tier, `trusted` by the per-artifact approval
|
|
457
|
+
> that `oats trust` wrote, `configured` by `oats-config.yaml` activation, and
|
|
458
|
+
> `enrolled` by the `oats.yaml` backlink. On a **workspace deployment**
|
|
459
|
+
> (`oats-local.yaml` present) none of those sources exists: nothing is installed,
|
|
460
|
+
> approval is per package version in `oats-lock.json` v3 (`oats sync`), activation
|
|
461
|
+
> is derived (workspace defaults ⊕ soul `capabilities:`), and membership is
|
|
462
|
+
> `oats-membership.yaml` observed over the remotes. `oats list` and `oats trust`
|
|
463
|
+
> are removed verbs (`E_UNKNOWN_COMMAND`), so a `remedy` naming them cannot be
|
|
464
|
+
> run. Treat the field names and producer strings as the stable wire shape they
|
|
465
|
+
> are; for the workspace-model facts read `oats spawn <soul> --preview --json`
|
|
466
|
+
> (`modules[]` with from/commit/digest — the "installed" and "configured"
|
|
467
|
+
> truth), `oats sync --json` `approvalNeeded[]` (the "trusted" truth) and
|
|
468
|
+
> `oats workspace status --json` (the "enrolled" truth). Re-basing the quartet on
|
|
469
|
+
> those producers is an open thread of [the workspace model](#workspace-model-workspaceapi-2);
|
|
470
|
+
> when it lands it will be announced as a new feature name, not a silent change of
|
|
471
|
+
> `readinessApi: 1`.
|
|
472
|
+
|
|
453
473
|
`oats readiness [--soul <name>] [--home <abs>] [--verify-signatures] [--policy] [--dir <d>] --json`
|
|
454
474
|
is the first-run readiness view (frame 09) and the Capabilities readiness rows
|
|
455
475
|
(frame 04). Every fact is derived from the **same** data `oats inspect` reports
|
|
@@ -1026,6 +1046,18 @@ refreshes the home in place:
|
|
|
1026
1046
|
schedule; the receipt's `note` says so. Refuses a retiring home
|
|
1027
1047
|
(`E_INSTANCE_RETIRING`), captured incarnations and capability-defined souls
|
|
1028
1048
|
(`E_UNSUPPORTED_MODE`: those are refreshed by a new resolution / package).
|
|
1049
|
+
- **Module homes (0.25+) are `E_UNSUPPORTED_MODE` too.** A home whose
|
|
1050
|
+
`instance.json` carries `modules{}` (spawned on a workspace deployment,
|
|
1051
|
+
`instance-modules`) answers `E_UNSUPPORTED_MODE` ("recompose from
|
|
1052
|
+
materialized modules is not supported yet; re-spawn"): its `AGENTS.md` was
|
|
1053
|
+
composed from the soul at a recorded member commit plus the materialized
|
|
1054
|
+
modules' injects, and the instance never changes under itself (decision 7).
|
|
1055
|
+
The refresh path for such a home is a new spawn (the soul is re-fetched at
|
|
1056
|
+
the member's current commit). `session-recompose` **stays advertised** in
|
|
1057
|
+
`features[]` because the verb still works for classic homes; gate the UI
|
|
1058
|
+
action on the feature AND on the absence of `instance.json.modules`
|
|
1059
|
+
(`oats status --json` `instances[].modules` is non-empty for a module home),
|
|
1060
|
+
and render the typed refusal otherwise.
|
|
1029
1061
|
- Gate on `features.includes("session-recompose")`. It is an **operator
|
|
1030
1062
|
action** (the human or the instance's parent), never something a Desktop
|
|
1031
1063
|
poll or an agent runs on itself.
|
|
@@ -12,10 +12,17 @@ the OATS Desktop app (`packages/desktop/` in the framework repo):
|
|
|
12
12
|
|
|
13
13
|
## Migrating a deployment that used `oats.web`
|
|
14
14
|
|
|
15
|
+
Under the **0.25 workspace model** there is nothing to uninstall: remove
|
|
16
|
+
`oats.web` from `oats-workspace.yaml` `packages:` / `defaults.capabilities`
|
|
17
|
+
and from any `soul.yaml` `capabilities:`, run `oats sync` (the lock v3 entry
|
|
18
|
+
disappears with the declaration), and use the Desktop app (step 3 below).
|
|
19
|
+
|
|
20
|
+
For a **0.24 classic** deployment:
|
|
21
|
+
|
|
15
22
|
1. Remove the `oats.web` entry from `capabilities.additive` in every
|
|
16
23
|
`oats-config.yaml` in your config chain.
|
|
17
|
-
2. Remove the `oats.web` entry from `oats-lock.json` at the same scope(s),
|
|
18
|
-
delete any stale installed copy under `.agents/capabilities/installed/`.
|
|
24
|
+
2. Remove the `oats.web` entry from `oats-lock.json` (v2) at the same scope(s),
|
|
25
|
+
and delete any stale installed copy under `.agents/capabilities/installed/`.
|
|
19
26
|
3. Use the OATS Desktop app instead: `cd packages/desktop && npm install &&
|
|
20
27
|
npm run rebuild && npm start` (see `packages/desktop/README.md`).
|
|
21
28
|
|
package/docs/desktop.md
CHANGED
|
@@ -84,10 +84,15 @@ The probe/mutation contract is specified in
|
|
|
84
84
|
|
|
85
85
|
The app starts on the directory it was launched with (its own folder by
|
|
86
86
|
default). To view a deployment, open the workspace switcher in the sidebar
|
|
87
|
-
and choose **Add workspace → Browse**, then point it at an OATS
|
|
88
|
-
a directory containing `agents
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
and choose **Add workspace → Browse**, then point it at an OATS deployment —
|
|
88
|
+
a directory containing `agents/` (under the 0.25 workspace model that is the
|
|
89
|
+
`<name>-workspace/` directory holding `oats-local.yaml` and `agents/`;
|
|
90
|
+
under 0.24, an `agents/` root, a `local-agents/` root for machine-local souls,
|
|
91
|
+
or a team scope whose `oats-config.yaml` declares `team:`). *The Desktop's own
|
|
92
|
+
multi-repo roster ("team scopes show every member repo's agents under one
|
|
93
|
+
roster") still keys on the 0.24 `oats-config.yaml` `team:` declaration; reading
|
|
94
|
+
the member set from `oats-local.yaml` / `oats workspace status` is the Phase F
|
|
95
|
+
follow-up named in the [0.25.0 notes](release-notes/v0.25.0.md#desktop).*
|
|
91
96
|
Added workspaces are remembered and offered as suggestions next time.
|
|
92
97
|
|
|
93
98
|
Local souls (uncommitted, machine-local agents under `local-agents/`) are
|
|
@@ -231,10 +231,22 @@ and needs equivalent registration glue when switched to session delivery.
|
|
|
231
231
|
|
|
232
232
|
## Shared permission setting
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
the
|
|
234
|
+
The opt-in is per launch or per soul: `oats spawn --yolo` / `--no-yolo`
|
|
235
|
+
(`oats create` accepts the same flags), an optional `yolo` in `soul.yaml`
|
|
236
|
+
(0.24 schema; the v2 `soul.yaml` schema does not carry it — use the spawn flag
|
|
237
|
+
or a launch configuration), and the Desktop's per-launch choice. With no
|
|
238
|
+
setting, native policy is retained.
|
|
239
|
+
|
|
240
|
+
*0.24 classic deployments* may also set `yolo: true` in an `oats-config.yaml`
|
|
241
|
+
to apply it to that scope; the closest scope wins, soul overrides scope, the
|
|
242
|
+
spawn flag overrides both. *Under the workspace model* `oats-config.yaml` is
|
|
243
|
+
not configuration ([configuration.md](configuration.md)); the kernel's
|
|
244
|
+
`composeInstance` still consults the classic chain for the machine-level knobs
|
|
245
|
+
`yolo` and `launch-configs` when such a file happens to sit above the
|
|
246
|
+
deployment, but nothing writes one and the rebuild guide tells you to delete
|
|
247
|
+
it — treat a scope-level `yolo` as a 0.24 feature and prefer the explicit
|
|
248
|
+
spawn flag.
|
|
249
|
+
|
|
238
250
|
Autonomous or unattended execution is not permission to synthesize `yolo: true`.
|
|
239
251
|
Explicit user CLI/UI input or a user-selected configuration is the opt-in; explicit
|
|
240
252
|
`false` remains false.
|