@awebai/oats 0.22.14 → 0.22.17

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.
@@ -23,29 +23,44 @@
23
23
  * OATS_TASK (spawn), OATS_REPO/OATS_BRANCH/OATS_WORK (spawn), OATS_META (retire).
24
24
  * Output: JSON { meta, brief, warning } on stdout. Failures warn, never block.
25
25
  */
26
- import { existsSync, mkdirSync, mkdtempSync, writeFileSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
26
+ import { existsSync, mkdirSync, mkdtempSync, writeFileSync, readFileSync, readdirSync, realpathSync, rmSync, writeSync } from "node:fs";
27
27
  import { join, isAbsolute, dirname } from "node:path";
28
28
  import { tmpdir } from "node:os";
29
29
  import { execFile, spawnSync } from "node:child_process";
30
30
  import { reclaimHarvestBranch } from "../lib/harvest-branch.mjs";
31
31
 
32
- const out = (o) => { process.stdout.write(JSON.stringify(o) + "\n"); process.exit(0); };
32
+ // Every answer leaves through here: written whole, then exit. stdout is a
33
+ // pipe when the kernel's operation runner, a Desktop or a test reads it, and
34
+ // on macOS Node writes to a pipe asynchronously: process.exit right after
35
+ // process.stdout.write drops whatever has not left Node's buffer (64 KiB,
36
+ // so a view of one long STATE.md arrived cut). fs.writeSync on fd 1 blocks
37
+ // until the OS has the bytes; EAGAIN (a non-blocking fd whose reader is
38
+ // slower than us) is retried after a short wait. The exit stays synchronous,
39
+ // which the call sites below rely on to end their flow.
40
+ const emit = (text, code) => {
41
+ const buf = Buffer.from(text, "utf8");
42
+ for (let off = 0; off < buf.length;) {
43
+ try { off += writeSync(1, buf, off, buf.length - off); }
44
+ catch (e) { if (e.code !== "EAGAIN") throw e; Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5); }
45
+ }
46
+ process.exit(code);
47
+ };
48
+ const out = (o) => emit(JSON.stringify(o) + "\n", 0);
33
49
  const warn = (m) => out({ warning: `oats-okf: ${String(m).slice(0, 300)}` });
34
50
  // A reported failure must not exit 0: callers and hooks read the status.
35
- const warnFail = (m) => { process.stdout.write(JSON.stringify({ warning: `oats-okf: ${String(m).slice(0, 300)}` }) + "\n"); process.exit(1); };
51
+ const warnFail = (m) => emit(JSON.stringify({ warning: `oats-okf: ${String(m).slice(0, 300)}` }) + "\n", 1);
36
52
 
37
53
  // Desktop CLI API v1: `oats okf harvest --json` emits EXACTLY ONE envelope
38
54
  // object on stdout — {schemaVersion:1,ok,result|error} — and a nonzero exit
39
55
  // on failure. Ordinary (non---json) output keeps the hook JSON shape above.
40
56
  const JSON_MODE = process.argv.includes("--json");
41
- const jsonOk = (result) => { process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: true, result }) + "\n"); process.exit(0); };
42
- const jsonFail = (code, message) => { process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message).slice(0, 300) } }) + "\n"); process.exit(1); };
57
+ const jsonOk = (result) => emit(JSON.stringify({ schemaVersion: 1, ok: true, result }) + "\n", 0);
58
+ const jsonFail = (code, message) => emit(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message).slice(0, 300) } }) + "\n", 1);
43
59
 
44
60
  // --help/-h never runs a command here either (the kernel answers it from the
45
61
  // manifest since 0.22.6; this keeps an older kernel from spawning a harvester).
46
62
  if (process.argv.slice(2).some((a) => a === "--help" || a === "-h")) {
47
- process.stdout.write("oats okf harvest [--json] [--from-record] [--force] promote this instance's pending notes (and record windows) into its soul by spawning a memory-harvest worker; --help never spawns\noats okf status [--json]\n");
48
- process.exit(0);
63
+ emit("oats okf harvest [--json] [--from-record] [--force] promote this instance's pending notes (and record windows) into its soul by spawning a memory-harvest worker; --help never runs it\noats okf inspect [--json] answer this instance's working knowledge (STATE.md, log.md, pending notes) as labeled documents\n", 0);
49
64
  }
50
65
  const event = process.env.OATS_EVENT || process.argv[2];
51
66
  const instance = process.env.OATS_INSTANCE;
@@ -473,6 +488,30 @@ _(the single next action — keep this current; a fresh session on any model res
473
488
  if (JSON_MODE) jsonFail(e.code || "E_HARVEST_FAILED", `harvest spawn failed (notes are safe on disk): ${e.message || e}`);
474
489
  warnFail(`harvest spawn failed (notes are safe on disk): ${e.message || e}`);
475
490
  }
491
+ } else if (event === "inspect") {
492
+ // VIEW OPERATION (kernel contract, docs/design/operations-contract.md):
493
+ // the instance's working knowledge as labeled documents, read from the
494
+ // home this command runs in. Text only; paths are provenance for the
495
+ // reader. Answers the JSON-v1 envelope regardless of --json.
496
+ try {
497
+ const CAP = 256 * 1024;
498
+ const doc = (label, file) => {
499
+ if (!existsSync(file)) return null;
500
+ const bytes = readFileSync(file);
501
+ const truncated = bytes.length > CAP;
502
+ let text = truncated ? bytes.subarray(0, CAP).toString("utf8") : bytes.toString("utf8");
503
+ if (truncated && text.endsWith("\uFFFD")) text = text.slice(0, -1);
504
+ return { label, kind: "markdown", path: file, text, ...(truncated ? { truncated: true, bytes: bytes.length } : {}) };
505
+ };
506
+ const documents = [doc("Working state (STATE.md)", join(home, "STATE.md")), doc("Log (log.md)", join(home, "log.md"))].filter(Boolean);
507
+ const notesDir = join(home, "notes");
508
+ const notes = existsSync(notesDir) ? readdirSync(notesDir).filter((f) => f.endsWith(".md")).sort() : [];
509
+ for (const f of notes) { const d = doc(`Pending note: ${f}`, join(notesDir, f)); if (d) documents.push(d); }
510
+ const summary = documents.length ? `${documents.length} document${documents.length === 1 ? "" : "s"}: ${existsSync(join(home, "STATE.md")) ? "state" : "no state"}, ${existsSync(join(home, "log.md")) ? "log" : "no log"}, ${notes.length} pending note${notes.length === 1 ? "" : "s"}` : "no working memory in this home yet (STATE.md, log.md and notes/ are written by the instance)";
511
+ jsonOk({ summary, documents });
512
+ } catch (e) {
513
+ jsonFail("E_INSPECT_FAILED", e.message || e);
514
+ }
476
515
  } else if (event === "retire") {
477
516
  // Retirement is intentionally a no-op for knowledge (for now): promotion happens
478
517
  // continuously via agent-initiated harvest. Uncommitted notes die with the home —
@@ -480,5 +519,5 @@ _(the single next action — keep this current; a fresh session on any model res
480
519
  // before finishing.
481
520
  out({ meta: {} });
482
521
  } else {
483
- warn(`unknown event "${event}" (expected soul-scaffold|spawn|retire)`);
522
+ warn(`unknown event "${event}" (expected soul-scaffold|spawn|retire|harvest|inspect)`);
484
523
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "capability": "oats.okf",
3
3
  "command": "okf",
4
- "version": "1.5.2",
4
+ "version": "1.6.1",
5
5
  "compatibility": {
6
6
  "oats": ">=0.22.3"
7
7
  },
8
8
  "layer": "knowledge",
9
- "description": "Knowledge layer via OKF: soul bundles, instance memory (STATE.md/log.md/notes/), continuous post-commit harvest into the soul (commit, PR, or direct-edit for local souls), craft + memory skills, validator.",
9
+ "description": "Knowledge layer via OKF: soul bundles, instance memory (STATE.md/log.md/notes/), continuous post-commit harvest into the soul (commit, PR, or direct-edit for local souls), craft + memory skills, validator; declares the operations a GUI or schedule may run (inspect, harvest).",
10
10
  "requires": [],
11
11
  "settings": {
12
12
  "harvest-runtime": {
@@ -29,11 +29,26 @@
29
29
  "skills"
30
30
  ],
31
31
  "commands": {
32
- "harvest": "bin/oats-okf.mjs harvest"
32
+ "harvest": "bin/oats-okf.mjs harvest",
33
+ "inspect": "bin/oats-okf.mjs inspect"
33
34
  },
34
35
  "inject": "injects/okf.md",
35
36
  "hooks": {
36
37
  "soul-scaffold": "bin/oats-okf.mjs soul-scaffold",
37
38
  "spawn": "bin/oats-okf.mjs spawn"
39
+ },
40
+ "operations": {
41
+ "inspect": {
42
+ "kind": "view",
43
+ "command": "inspect",
44
+ "context": "home",
45
+ "description": "Show this instance's working knowledge: STATE.md, log.md and pending notes"
46
+ },
47
+ "harvest": {
48
+ "kind": "action",
49
+ "command": "harvest",
50
+ "context": "home",
51
+ "description": "Promote this instance's pending notes (or captured record) into its soul by spawning a memory-harvest worker"
52
+ }
38
53
  }
39
54
  }
@@ -503,3 +503,10 @@ The source packages live under `capabilities/`. Acquired packages live under
503
503
  `<level>/.agents/capabilities/installed/` (gitignored, restorable); packages
504
504
  authored at a scope live under `<level>/.agents/capabilities/owned/`
505
505
  (committed where the scope is a git repo). Within one scope `owned/` overrides `installed/` on ID collision.
506
+
507
+ ## Operations a capability declares
508
+
509
+ A manifest may declare `operations` (named actions or views delegating to
510
+ its own commands) that a GUI or a schedule invokes through `oats operation
511
+ run <layer>:<name>`; `oats inspect --json` reports them with availability.
512
+ See [docs/design/operations-contract.md](design/operations-contract.md).
@@ -150,6 +150,38 @@
150
150
  "minLength": 1
151
151
  }
152
152
  },
153
+ "operations": {
154
+ "description": "Named operations a GUI or scheduler may invoke through `oats operation run`, each delegating to one of this manifest's commands. kind \"view\" answers { documents: [{label, kind, path, text}] }; \"action\" (default) runs something. context \"home\" (default) runs in an instance home, \"scope\" in the config scope.",
155
+ "type": "object",
156
+ "propertyNames": {
157
+ "pattern": "^[a-z][a-z0-9-]*$"
158
+ },
159
+ "additionalProperties": {
160
+ "type": "object",
161
+ "required": ["command"],
162
+ "additionalProperties": false,
163
+ "properties": {
164
+ "command": { "type": "string", "minLength": 1 },
165
+ "kind": { "enum": ["action", "view"] },
166
+ "context": { "enum": ["home", "scope"] },
167
+ "description": { "type": "string" },
168
+ "args": {
169
+ "type": "array",
170
+ "items": {
171
+ "type": "object",
172
+ "required": ["name"],
173
+ "additionalProperties": false,
174
+ "properties": {
175
+ "name": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
176
+ "flag": { "type": "string", "pattern": "^--[a-z][a-z0-9-]*$" },
177
+ "required": { "type": "boolean" },
178
+ "description": { "type": "string" }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ },
153
185
  "hooks": {
154
186
  "type": "object",
155
187
  "properties": {
@@ -0,0 +1,50 @@
1
+ # Souls and capabilities in Desktop
2
+
3
+ Selecting a soul opens its details. Launch and Schedule are explicit actions;
4
+ Quick Open and Enter inspect instead of launching. The existing local Files
5
+ browser remains available. It is separate from provider knowledge inspection.
6
+
7
+ The inspector reads the kernel's `oats inspect --json` answer on selection,
8
+ Refresh, or after an explicit mutation. The roster's eight-second poll never
9
+ rescans capabilities or replaces an editor. Workspace changes invalidate old
10
+ responses. Same-named souls are addressed by name plus agents root; instances
11
+ are addressed by their exact home.
12
+
13
+ The Capabilities button inspects workspace defaults, with a member-scope
14
+ selector where a workspace contains several roots. Installed version, source
15
+ and health are separate from effective activation, provenance and settings.
16
+ Enable, explicit disable and return to inheritance call `oats use`; layer-wide
17
+ disable is distinct from excluding a single capability, and is available only
18
+ when inspecting a workspace or member scope, never an individual soul. These changes apply to
19
+ future instances. Existing homes retain their captured bindings and settings.
20
+
21
+ Authored souls expose only the fields the kernel reports as editable. Desktop
22
+ edits runtime, model, backend, YOLO, description and instructions through
23
+ `oats soul set`. Packaged souls remain read-only and explain where their source
24
+ must be changed. Instructions use a private temporary file; the CLI owns remote
25
+ transfer and mutation. Desktop does not modify YAML or resolve capabilities.
26
+
27
+ An instance's action menu opens Knowledge & capabilities. The kernel supplies
28
+ its snapshot, differences from current configuration, and declared provider
29
+ operations. View operations return labeled documents; Desktop renders their
30
+ text and treats remote paths as provenance, never as local files to open. Actions
31
+ without required arguments can run here. Operations requiring arguments explain
32
+ that they need the CLI. No provider is assumed to support harvesting.
33
+
34
+ Schedules store an operation address and exact home with the kernel's generic
35
+ `operation` job kind. The form discovers declared, available actions on demand.
36
+ The server verifies that declaration again at save time, and the kernel resolves
37
+ and validates the provider at execution time. Existing command schedules remain
38
+ visible and runnable, but the GUI does not reverse-parse their argv to edit them.
39
+
40
+ Every capability request requires an exact advertised workspace. Remote requests
41
+ require its saved server registration and the CLI's operations API feature; all
42
+ routing stays in OATS. The consumer never performs SSH or silently substitutes a
43
+ local workspace. The proxy allows the CLI's bounded operation to return its
44
+ receipt before timing out.
45
+
46
+ Validation: scope/identity and remote-route refusal; private instructions and
47
+ failure cleanup; declared schedule actions; stale inspection/save responses;
48
+ provider text rendering; explicit launch and existing runtime flows. Native
49
+ Electron rendering is checked with a temporary fixture frame in the existing
50
+ GUI, without starting a model, agent, or another application instance.
@@ -0,0 +1,123 @@
1
+ # Provider operations and inspection contract
2
+
3
+ Implemented 2026-09-07 on the existing capability engine. The kernel
4
+ resolves providers; a GUI reads one answer and calls the commands below.
5
+ Nothing here names a provider: what a knowledge, messaging or tasks
6
+ capability offers is what its manifest declares.
7
+
8
+ ## Manifest: operations
9
+
10
+ ```json
11
+ "operations": {
12
+ "harvest": { "kind": "action", "command": "harvest", "context": "home", "description": "Promote this instance's notes into its soul" },
13
+ "inspect": { "kind": "view", "command": "inspect", "context": "home", "description": "Show this instance's working knowledge" }
14
+ }
15
+ ```
16
+
17
+ `command` names one of the manifest's `commands`. `kind` is `action`
18
+ (default) or `view`. `context` is `home` (default; runs in an instance
19
+ home) or `scope` (runs in the config scope). Optional `args` declare
20
+ `{name, flag, required, description}`; the runner passes `--arg name=value`
21
+ pairs as those flags and refuses unknown or missing required ones. A view
22
+ must answer `{ documents: [{ label, kind: "markdown"|"text", path?, text? }],
23
+ summary? }`; the kernel validates that shape and relays it. Paths are
24
+ provenance for the reader; the kernel reads nothing for a view. Validation
25
+ happens when the manifest loads (`docs/capability-manifest.schema.json`).
26
+
27
+ ## oats inspect
28
+
29
+ `oats inspect [--dir <scope>] [--soul <name> [--agents-root <abs>]] [--home
30
+ <abs>] [--server <id>] --json` answers, in one envelope: `scope` (context,
31
+ workspace, team, chain, agentsRoots); `souls` (persistent, local and
32
+ packaged, each with runtime defaults, `editable {fields, instructions,
33
+ reason}`, instances, and for the selected soul its `instructions {file,
34
+ text, sha256, bytes, truncated, error}` capped at 256 KiB of bytes on a
35
+ character boundary); `layers` (effective provider per layer); `capabilities`
36
+ (installed state and `health` from the package engine, `missingRequires`,
37
+ and separately `activation {enabled, source, target, level, provenance,
38
+ settings, declaredAt}` plus `operations` with `available` and a `reason`);
39
+ `knowledge` (the effective knowledge provider's operations); `snapshot` and
40
+ `currentConfig` with `--home`; `problems`.
41
+
42
+ A `--home` answer is authoritative for that home: activation, settings,
43
+ layers and operation availability come from the home's captured bindings
44
+ with the currently acquired manifests and current trust, marked `source:
45
+ "snapshot"`; the live config is `currentConfig`, and `snapshot.drift`
46
+ lists activation, settings and integrity differences. The home selects its
47
+ own soul under its own agents root (its recorded work repository may be
48
+ another repository); `--dir`, if given, must be the recorded repository or
49
+ the workspace of that root, and `--agents-root` must be that root
50
+ (`E_HOME_MISMATCH`). Without a home, `--dir` is the config context and
51
+ same-named souls under two member roots need `--agents-root`
52
+ (`E_SOUL_AMBIGUOUS`). The ambient `PI_AGENTS_ROOT` never redirects these
53
+ commands. Integrity scanning happens only in this command.
54
+
55
+ ## oats operation run
56
+
57
+ `oats operation run <layer>:<name> (--home <abs> | --soul <name> [--dir
58
+ <scope>] [--agents-root <abs>]) [--arg k=v ...] --json` resolves the
59
+ provider that fills `<layer>` for the home (captured bindings and settings)
60
+ or for the soul in the scope (config), requires the operation to be declared
61
+ (`E_OPERATION_UNKNOWN`), a provider to exist (`E_OPERATION_UNAVAILABLE`,
62
+ also for a home-context operation without `--home`), the executable surface
63
+ to be trusted (`E_CAPABILITY_BLOCKED`) and its `requires` to be on PATH
64
+ (`E_CAPABILITY_REQUIRES`), then runs the provider's own command exactly as
65
+ `oats <ns> <cmd>` would, with cwd and identity set to the selected target
66
+ (`OATS_HOME`, `OATS_INSTANCE`, `OATS_AGENT`, `OATS_SOUL`, `OATS_ROOT`,
67
+ `OATS_CONTEXT`, `OATS_WORKSPACE`, `OATS_OPERATION`, `OATS_SETTINGS`,
68
+ `OATS_CLI_BIN`, team variables) and every ambient identity of the invoking
69
+ process removed. The answer is `{ operation, capability, version, argv,
70
+ cwd, target: {home, instance}|null, result, instance?, home? }`; top-level
71
+ `instance`/`home` appear only when the provider's answer names something it
72
+ launched, never the source home.
73
+
74
+ The receipt must be exactly one JSON-v1 envelope on stdout from a process
75
+ that exits 0. Otherwise the outcome is unconfirmed: `E_OPERATION_TIMEOUT`
76
+ (240 s, below every wrapper) or `E_OPERATION_RESULT` (no valid envelope,
77
+ contaminated output, or a success envelope contradicted by the exit
78
+ status), with `error.details {unconfirmed: true, exit, envelope?, stderr?}`
79
+ carrying what was observed. A provider's own `ok: false` is relayed with
80
+ its code.
81
+
82
+ ## Schedules
83
+
84
+ Kind `operation` `{operation: "<layer>:<name>", home}` runs `oats operation
85
+ run <op> --home <home>` as a command job: same admission, tracking and
86
+ reconciliation. An unconfirmed outcome keeps the slot as unknown and any
87
+ name the provider answered is kept for `oats schedule reconcile`.
88
+
89
+ ## Mutations
90
+
91
+ `oats use ... --json` answers `{ capability, action: enable|disable|
92
+ layer-none|inherit, target, layer, level, file, settings, before, after
93
+ {..., effective}, remaining?, note?, missingRequires }`. `--inherit`
94
+ removes only the addressed target at this level; other bindings stay and
95
+ are listed. `use none --layer l` is a level statement and takes no soul or
96
+ type. A layer bound to another capability at a level is never overwritten
97
+ (`E_LAYER_BOUND` with the exact remedy).
98
+
99
+ `oats soul set <name> [--dir] [--agents-root] [--runtime] [--model |
100
+ --no-model] [--yolo | --no-yolo] [--backend] [--description |
101
+ --no-description] [--instructions-file <path>] --json` edits only the given
102
+ `soul.yaml` lines and replaces `AGENTS.md`; packaged souls are refused
103
+ (`E_SOUL_READONLY`). The receipt carries before/after and sha256s.
104
+
105
+ ## Remote
106
+
107
+ `inspect`, `operation`, `use` and `soul` route with `--server <id>` through
108
+ the saved route. The gate is the destination's `features` list containing
109
+ `operations` and its `operationsApi: 1` (`E_REMOTE_INCOMPATIBLE` before
110
+ anything is sent): `features` describes what a kernel can do locally, which
111
+ is what runs on the host. The probe's `remote` list describes what a CLI
112
+ can ROUTE to a server and is what a GUI checks on the local CLI before
113
+ offering remote actions; it is not a gate on the destination. An explicit `--dir` is the exact member context and
114
+ travels as is; `--home` is its own context; otherwise the registered
115
+ workspace is the scope. Soul instructions travel as bytes on the ssh stdin
116
+ (`--instructions-stdin` on the host), never as a local path.
117
+
118
+ ## Replaceability
119
+
120
+ `test/inspect.test.mjs`, `test/operation.test.mjs` and
121
+ `test/operations-routing.test.mjs` use an owned alternative knowledge
122
+ provider (namespace `notes`, one `MEMORY.md`, operations `harvest` and
123
+ `inspect`) and never mention the official provider.
@@ -0,0 +1,48 @@
1
+ # OATS v0.22.15
2
+
3
+ Souls and capabilities become something you inspect and manage, through
4
+ one kernel contract, and knowledge providers declare what they can do.
5
+
6
+ ## Provider operations and inspection (kernel)
7
+
8
+ `oats inspect --json` answers everything a GUI needs in one envelope for a
9
+ scope, a selected soul or a running home: souls with runtime defaults,
10
+ editability and instructions; installed capabilities with health from the
11
+ package engine, separately from their effective activation, provenance and
12
+ settings; effective layer bindings; and the operations each provider
13
+ declares, with availability and the reason when unavailable. For a home the
14
+ answer is its captured bindings (with current manifests and trust), the live
15
+ config beside it, and the drift between them. Same-named souls in member
16
+ repositories are addressed by name plus agents root; a home selects its own
17
+ soul under its own root.
18
+
19
+ `oats operation run <layer>:<name>` resolves the provider that fills a layer
20
+ for a home or a soul, checks declaration, trust and host requirements, runs
21
+ the provider's own command in the right place with the right identity, and
22
+ relays its receipt; a view answers labeled documents. Unconfirmed outcomes
23
+ carry what was observed and a scheduled operation keeps its slot until
24
+ reconciled. Manifests declare `operations`. Schedules gain kind
25
+ `operation`. `oats use --json` answers receipts and `--inherit` returns a
26
+ level to inheritance; `oats soul set` edits an editable soul's defaults and
27
+ instructions in place. All four route to a registered server through the
28
+ saved route; the probe advertises `operations` and `operationsApi: 1`. The
29
+ retire receipt no longer implies a harvest. See
30
+ docs/design/operations-contract.md.
31
+
32
+ ## oats.okf 1.6.0
33
+
34
+ The official knowledge provider declares `inspect` (a view of STATE.md,
35
+ log.md and pending notes) and `harvest` (the existing action); the catalog
36
+ pins v1.6.0. Existing scopes keep their locked 1.5.2 until their owners
37
+ update them; the GUI then shows no operations for those providers, which is
38
+ the truthful state.
39
+
40
+ ## Desktop
41
+
42
+ Souls & capabilities replaces the launch-centric roster: select a soul to
43
+ inspect it, Launch and Schedule are explicit, defaults and instructions are
44
+ editable where the kernel says so, installed capabilities are shown apart
45
+ from activation with enable, disable and inherit at an exact scope, an
46
+ instance's menu opens Knowledge & capabilities, provider views render as
47
+ text, and schedules run declared provider operations. See
48
+ docs/design/2026-09-07-desktop-souls-capabilities.md.
@@ -0,0 +1,53 @@
1
+ # OATS v0.22.16
2
+
3
+ The same reviewed content as the unpublished v0.22.15 tag, whose hosted
4
+ run failed in frozen test data (retirement goldens still carrying the
5
+ removed `harvested` field, and a Desktop harvest fixture predating the
6
+ operations gate) before anything was published; only tests changed.
7
+
8
+ Souls and capabilities become something you inspect and manage, through
9
+ one kernel contract, and knowledge providers declare what they can do.
10
+
11
+ ## Provider operations and inspection (kernel)
12
+
13
+ `oats inspect --json` answers everything a GUI needs in one envelope for a
14
+ scope, a selected soul or a running home: souls with runtime defaults,
15
+ editability and instructions; installed capabilities with health from the
16
+ package engine, separately from their effective activation, provenance and
17
+ settings; effective layer bindings; and the operations each provider
18
+ declares, with availability and the reason when unavailable. For a home the
19
+ answer is its captured bindings (with current manifests and trust), the live
20
+ config beside it, and the drift between them. Same-named souls in member
21
+ repositories are addressed by name plus agents root; a home selects its own
22
+ soul under its own root.
23
+
24
+ `oats operation run <layer>:<name>` resolves the provider that fills a layer
25
+ for a home or a soul, checks declaration, trust and host requirements, runs
26
+ the provider's own command in the right place with the right identity, and
27
+ relays its receipt; a view answers labeled documents. Unconfirmed outcomes
28
+ carry what was observed and a scheduled operation keeps its slot until
29
+ reconciled. Manifests declare `operations`. Schedules gain kind
30
+ `operation`. `oats use --json` answers receipts and `--inherit` returns a
31
+ level to inheritance; `oats soul set` edits an editable soul's defaults and
32
+ instructions in place. All four route to a registered server through the
33
+ saved route; the probe advertises `operations` and `operationsApi: 1`. The
34
+ retire receipt no longer implies a harvest. See
35
+ docs/design/operations-contract.md.
36
+
37
+ ## oats.okf 1.6.0
38
+
39
+ The official knowledge provider declares `inspect` (a view of STATE.md,
40
+ log.md and pending notes) and `harvest` (the existing action); the catalog
41
+ pins v1.6.0. Existing scopes keep their locked 1.5.2 until their owners
42
+ update them; the GUI then shows no operations for those providers, which is
43
+ the truthful state.
44
+
45
+ ## Desktop
46
+
47
+ Souls & capabilities replaces the launch-centric roster: select a soul to
48
+ inspect it, Launch and Schedule are explicit, defaults and instructions are
49
+ editable where the kernel says so, installed capabilities are shown apart
50
+ from activation with enable, disable and inherit at an exact scope, an
51
+ instance's menu opens Knowledge & capabilities, provider views render as
52
+ text, and schedules run declared provider operations. See
53
+ docs/design/2026-09-07-desktop-souls-capabilities.md.
@@ -0,0 +1,25 @@
1
+ # OATS v0.22.17
2
+
3
+ Two corrections found by installed acceptance of 0.22.16, nothing else.
4
+
5
+ ## oats.okf 1.6.1
6
+
7
+ A provider view larger than one pipe buffer arrived cut at 65536 bytes
8
+ with exit 0 when the kernel's operation runner or a Desktop read it: the
9
+ provider wrote its envelope and exited at once, and on macOS Node writes
10
+ to a pipe asynchronously. Every answer now leaves the provider whole
11
+ before it exits. The catalog pins v1.6.1; scopes on 1.6.0 keep working
12
+ for small views until their owners run
13
+ `oats update oats.okf --to v1.6.1 --dir <scope>` and re-trust. The
14
+ kernel's runner is unchanged; the regression covers a 128 KiB view
15
+ through an actual pipe, directly and through `oats operation run`, and a
16
+ 300 KiB view from any provider.
17
+
18
+ ## Desktop
19
+
20
+ Workspaces added at runtime survive an app restart: the opened set is
21
+ persisted on its own (not the recent-suggestion store), restored and
22
+ re-validated at startup with absent paths skipped and aliases collapsed,
23
+ and a backend is reused only when it covers every restored workspace. A
24
+ failed persistence write keeps the previous set and reports the failed
25
+ add.
package/docs/schedules.md CHANGED
@@ -56,6 +56,15 @@ and no queue.
56
56
  home is started again only at due minutes, never every minute, so a
57
57
  harness that keeps exiting is not restarted in a loop. A job holds at most
58
58
  one pending delivery: a due minute while one is pending adds nothing.
59
+ - **operation** `{id, enabled, cron, tz, kind: "operation", operation, home}`
60
+ — runs a provider operation such as `knowledge:harvest` in the instance at
61
+ `home` through `oats operation run <layer>:<name> --home <home>`. The
62
+ provider is whatever fills that layer for the home when the job runs (its
63
+ snapshot), not something stored in the job, so the job stays valid across
64
+ provider changes and a GUI can list and edit it without parsing argv.
65
+ Admission, tracking and reconciliation are those of a command job: a
66
+ launch receipt the provider answers (a harvester it spawned) is followed
67
+ until that home is gone; the source home is never treated as a launch.
59
68
  Unobservable or still starting: skipped with the reason, delivery kept
60
69
  pending. Whether a running harness is busy cannot be seen from the
61
70
  terminal: delivery is terminal input (bracketed paste plus Enter), never an
package/lib/core.mjs CHANGED
@@ -987,9 +987,45 @@ function loadManifestAt(idir, origin) {
987
987
  if (hookDeclaration(value)?.required && hook !== "spawn") throw new Error(`capability ${id} hook "${hook}" cannot be required — only the spawn hook is enforced (retire and soul-scaffold run outside a spawn transaction)`);
988
988
  }
989
989
  if (m.agents !== undefined && (!Array.isArray(m.agents) || m.agents.some((a) => typeof a !== "string"))) throw new Error(`capability ${id} "agents" must be an array of package-relative soul directories`);
990
+ validateManifestOperations(m, id);
990
991
  return { ...m, _dir: idir, _origin: origin };
991
992
  }
992
993
 
994
+ /** `operations`: what a capability offers a GUI or scheduler by name, each
995
+ * delegating to one of its own `commands`. kind "action" runs something;
996
+ * kind "view" answers { documents: [...] } for presentation (a knowledge
997
+ * provider's own view of an instance's working knowledge). context "home"
998
+ * runs in an instance home; "scope" runs in the config scope. */
999
+ const OPERATION_NAME_RE = /^[a-z][a-z0-9-]*$/;
1000
+ function validateManifestOperations(m, id) {
1001
+ if (m.operations === undefined) return;
1002
+ if (!m.operations || typeof m.operations !== "object" || Array.isArray(m.operations)) throw new Error(`capability ${id} "operations" must be an object map`);
1003
+ for (const [name, op] of Object.entries(m.operations)) {
1004
+ if (!OPERATION_NAME_RE.test(name)) throw new Error(`capability ${id} operation "${name}": name must match ${OPERATION_NAME_RE.source}`);
1005
+ if (!op || typeof op !== "object" || Array.isArray(op)) throw new Error(`capability ${id} operation "${name}" must be an object`);
1006
+ if (typeof op.command !== "string" || !Object.prototype.hasOwnProperty.call(m.commands || {}, op.command)) throw new Error(`capability ${id} operation "${name}": "command" must name one of the manifest's commands`);
1007
+ if (op.kind !== undefined && !["action", "view"].includes(op.kind)) throw new Error(`capability ${id} operation "${name}": "kind" must be action or view`);
1008
+ if (op.context !== undefined && !["home", "scope"].includes(op.context)) throw new Error(`capability ${id} operation "${name}": "context" must be home or scope`);
1009
+ if (op.description !== undefined && typeof op.description !== "string") throw new Error(`capability ${id} operation "${name}": "description" must be a string`);
1010
+ if (op.args !== undefined) {
1011
+ if (!Array.isArray(op.args)) throw new Error(`capability ${id} operation "${name}": "args" must be an array`);
1012
+ for (const a of op.args) {
1013
+ if (!a || typeof a !== "object" || typeof a.name !== "string" || !OPERATION_NAME_RE.test(a.name)) throw new Error(`capability ${id} operation "${name}": each arg needs a name matching ${OPERATION_NAME_RE.source}`);
1014
+ if (a.flag !== undefined && (typeof a.flag !== "string" || !/^--[a-z][a-z0-9-]*$/.test(a.flag))) throw new Error(`capability ${id} operation "${name}" arg "${a.name}": "flag" must look like --name`);
1015
+ if (a.required !== undefined && typeof a.required !== "boolean") throw new Error(`capability ${id} operation "${name}" arg "${a.name}": "required" must be a boolean`);
1016
+ }
1017
+ }
1018
+ }
1019
+ }
1020
+ /** The normalized operations a manifest declares (empty when none). */
1021
+ export function manifestOperations(manifest) {
1022
+ return Object.entries(manifest?.operations || {}).map(([name, op]) => ({
1023
+ name, kind: op.kind || "action", command: op.command, context: op.context || "home",
1024
+ description: op.description || null,
1025
+ args: (op.args || []).map((a) => ({ name: a.name, flag: a.flag || `--${a.name}`, required: !!a.required, description: a.description || null })),
1026
+ }));
1027
+ }
1028
+
993
1029
  /** Discover capability manifests. Later sources take precedence: outer scopes < inner scopes; installed < owned within one scope. Duplicates inside one source layer are errors. */
994
1030
  export function capabilityManifests(startDir) {
995
1031
  // Capability-id keyed — never answer for `constructor`/`toString`. The ids
@@ -7230,7 +7266,6 @@ export function retireInstance(root, name, o = {}) {
7230
7266
  contextDir: meta.repo, workspaceDir: workspaceOf(root), rootDir: root, resolved, priorMeta: meta.capabilityMeta || {},
7231
7267
  });
7232
7268
  }
7233
- const harvested = hookResults?.meta?.["oats.okf"]?.harvested || [];
7234
7269
 
7235
7270
  // ORDINARY retirement must not delete a home whose cleanup did not finish.
7236
7271
  // The failures were already collected above; until now they were read ONLY on
@@ -7439,7 +7474,7 @@ export function retireInstance(root, name, o = {}) {
7439
7474
  }
7440
7475
 
7441
7476
 
7442
- const result = { retired: name, agent: found.agent.name, workRecovery, workRecoveries: workRecoveries.length > 1 ? workRecoveries : undefined, worktreeRemoved: isWorktree, branchDeleted: !!(o.deleteBranch && meta.branch) || quarantineBranchDeleted, removedDir: !o.keepDir && (!stillIncomplete || forced), rollbackIncomplete: forced ? undefined : stillIncomplete, forcedIncomplete: forced ? stillIncomplete : undefined, retainedHome: stillIncomplete && !forced ? found.home : undefined, harvested, relinked: relinked.length ? relinked : undefined, capabilityMeta: hookResults?.meta, warnings: hookResults?.warnings?.length ? hookResults.warnings : undefined };
7477
+ const result = { retired: name, agent: found.agent.name, workRecovery, workRecoveries: workRecoveries.length > 1 ? workRecoveries : undefined, worktreeRemoved: isWorktree, branchDeleted: !!(o.deleteBranch && meta.branch) || quarantineBranchDeleted, removedDir: !o.keepDir && (!stillIncomplete || forced), rollbackIncomplete: forced ? undefined : stillIncomplete, forcedIncomplete: forced ? stillIncomplete : undefined, retainedHome: stillIncomplete && !forced ? found.home : undefined, relinked: relinked.length ? relinked : undefined, capabilityMeta: hookResults?.meta, warnings: hookResults?.warnings?.length ? hookResults.warnings : undefined };
7443
7478
  if (self) {
7444
7479
  // The caller is the instance: its process lives in the window we are about to
7445
7480
  // kill. Detach the kill so this function can return and the caller can report