@awebai/oats 0.22.14 → 0.22.16
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 +678 -23
- package/capabilities/oats-okf/bin/oats-okf.mjs +26 -2
- package/capabilities/oats-okf/oats.json +18 -3
- package/docs/capabilities.md +7 -0
- package/docs/capability-manifest.schema.json +32 -0
- package/docs/design/2026-09-07-desktop-souls-capabilities.md +50 -0
- package/docs/design/operations-contract.md +123 -0
- package/docs/release-notes/v0.22.15.md +48 -0
- package/docs/release-notes/v0.22.16.md +53 -0
- package/docs/schedules.md +9 -0
- package/lib/core.mjs +37 -2
- package/lib/schedule.mjs +32 -8
- package/lib/servers.mjs +32 -1
- package/package-catalog.json +29 -8
- package/package.json +1 -1
|
@@ -44,7 +44,7 @@ const jsonFail = (code, message) => { process.stdout.write(JSON.stringify({ sche
|
|
|
44
44
|
// --help/-h never runs a command here either (the kernel answers it from the
|
|
45
45
|
// manifest since 0.22.6; this keeps an older kernel from spawning a harvester).
|
|
46
46
|
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
|
|
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 runs it\noats okf inspect [--json] answer this instance's working knowledge (STATE.md, log.md, pending notes) as labeled documents\n");
|
|
48
48
|
process.exit(0);
|
|
49
49
|
}
|
|
50
50
|
const event = process.env.OATS_EVENT || process.argv[2];
|
|
@@ -473,6 +473,30 @@ _(the single next action — keep this current; a fresh session on any model res
|
|
|
473
473
|
if (JSON_MODE) jsonFail(e.code || "E_HARVEST_FAILED", `harvest spawn failed (notes are safe on disk): ${e.message || e}`);
|
|
474
474
|
warnFail(`harvest spawn failed (notes are safe on disk): ${e.message || e}`);
|
|
475
475
|
}
|
|
476
|
+
} else if (event === "inspect") {
|
|
477
|
+
// VIEW OPERATION (kernel contract, docs/design/operations-contract.md):
|
|
478
|
+
// the instance's working knowledge as labeled documents, read from the
|
|
479
|
+
// home this command runs in. Text only; paths are provenance for the
|
|
480
|
+
// reader. Answers the JSON-v1 envelope regardless of --json.
|
|
481
|
+
try {
|
|
482
|
+
const CAP = 256 * 1024;
|
|
483
|
+
const doc = (label, file) => {
|
|
484
|
+
if (!existsSync(file)) return null;
|
|
485
|
+
const bytes = readFileSync(file);
|
|
486
|
+
const truncated = bytes.length > CAP;
|
|
487
|
+
let text = truncated ? bytes.subarray(0, CAP).toString("utf8") : bytes.toString("utf8");
|
|
488
|
+
if (truncated && text.endsWith("\uFFFD")) text = text.slice(0, -1);
|
|
489
|
+
return { label, kind: "markdown", path: file, text, ...(truncated ? { truncated: true, bytes: bytes.length } : {}) };
|
|
490
|
+
};
|
|
491
|
+
const documents = [doc("Working state (STATE.md)", join(home, "STATE.md")), doc("Log (log.md)", join(home, "log.md"))].filter(Boolean);
|
|
492
|
+
const notesDir = join(home, "notes");
|
|
493
|
+
const notes = existsSync(notesDir) ? readdirSync(notesDir).filter((f) => f.endsWith(".md")).sort() : [];
|
|
494
|
+
for (const f of notes) { const d = doc(`Pending note: ${f}`, join(notesDir, f)); if (d) documents.push(d); }
|
|
495
|
+
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)";
|
|
496
|
+
process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: true, result: { summary, documents } }) + "\n"); process.exit(0);
|
|
497
|
+
} catch (e) {
|
|
498
|
+
process.stdout.write(JSON.stringify({ schemaVersion: 1, ok: false, error: { code: "E_INSPECT_FAILED", message: String(e.message || e).slice(0, 300) } }) + "\n"); process.exit(1);
|
|
499
|
+
}
|
|
476
500
|
} else if (event === "retire") {
|
|
477
501
|
// Retirement is intentionally a no-op for knowledge (for now): promotion happens
|
|
478
502
|
// continuously via agent-initiated harvest. Uncommitted notes die with the home —
|
|
@@ -480,5 +504,5 @@ _(the single next action — keep this current; a fresh session on any model res
|
|
|
480
504
|
// before finishing.
|
|
481
505
|
out({ meta: {} });
|
|
482
506
|
} else {
|
|
483
|
-
warn(`unknown event "${event}" (expected soul-scaffold|spawn|retire)`);
|
|
507
|
+
warn(`unknown event "${event}" (expected soul-scaffold|spawn|retire|harvest|inspect)`);
|
|
484
508
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"capability": "oats.okf",
|
|
3
3
|
"command": "okf",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
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
|
}
|
package/docs/capabilities.md
CHANGED
|
@@ -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.
|
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,
|
|
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
|
package/lib/schedule.mjs
CHANGED
|
@@ -40,11 +40,12 @@ export function scheduleError(code, message, extra) { return Object.assign(new E
|
|
|
40
40
|
* is never observed or executed, and is reported invalid on that job only. */
|
|
41
41
|
export function shapeError(def) {
|
|
42
42
|
if (!def || typeof def !== "object" || Array.isArray(def)) return "definition is not an object";
|
|
43
|
-
if (!["spawn", "command", "wake"].includes(def.kind)) return `kind ${JSON.stringify(def.kind)} is not spawn, command or
|
|
43
|
+
if (!["spawn", "command", "wake", "operation"].includes(def.kind)) return `kind ${JSON.stringify(def.kind)} is not spawn, command, wake or operation`;
|
|
44
44
|
if (typeof def.cron !== "string" || typeof def.tz !== "string") return "cron and tz must be strings";
|
|
45
45
|
if (def.kind === "spawn" && typeof def.agent !== "string") return "agent must be a string";
|
|
46
46
|
if (def.kind === "command" && (typeof def.cwd !== "string" || !Array.isArray(def.argv))) return "cwd and argv are required";
|
|
47
47
|
if (def.kind === "wake" && (typeof def.home !== "string" || typeof def.message !== "string")) return "home and message are required";
|
|
48
|
+
if (def.kind === "operation" && (typeof def.operation !== "string" || typeof def.home !== "string")) return "operation and home are required";
|
|
48
49
|
return null;
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -229,7 +230,14 @@ export function validateDefinition(ws, def, { checkAgent = true } = {}) {
|
|
|
229
230
|
if (typeof def.home !== "string" || !isAbsolute(def.home) || !inside(ws, def.home) || !existsSync(join(def.home, "instance.json"))) throw scheduleError("E_SCHEDULE_INVALID", "home: an existing instance home inside the scope", { field: "home" });
|
|
230
231
|
validateMessage(def.message, "message");
|
|
231
232
|
out.home = resolve(def.home); out.message = def.message;
|
|
232
|
-
} else
|
|
233
|
+
} else if (def.kind === "operation") {
|
|
234
|
+
// A provider operation in a running home, resolved when the job runs
|
|
235
|
+
// (oats operation run <layer>:<name> --home): the provider is whatever
|
|
236
|
+
// fills that layer for the home then; nothing about it is stored here.
|
|
237
|
+
if (typeof def.operation !== "string" || !/^(knowledge|messaging|tasks):[a-z][a-z0-9-]*$/.test(def.operation)) throw scheduleError("E_SCHEDULE_INVALID", "operation: <layer>:<name>, e.g. knowledge:harvest", { field: "operation" });
|
|
238
|
+
if (typeof def.home !== "string" || !isAbsolute(def.home) || !inside(ws, def.home) || !existsSync(join(def.home, "instance.json"))) throw scheduleError("E_SCHEDULE_INVALID", "home: an existing instance home inside the scope", { field: "home" });
|
|
239
|
+
out.operation = def.operation; out.home = resolve(def.home);
|
|
240
|
+
} else throw scheduleError("E_SCHEDULE_INVALID", "kind: spawn, command, wake or operation", { field: "kind" });
|
|
233
241
|
return out;
|
|
234
242
|
}
|
|
235
243
|
|
|
@@ -320,6 +328,13 @@ function launchSpawn(ws, def, minute, io) {
|
|
|
320
328
|
}
|
|
321
329
|
return run;
|
|
322
330
|
}
|
|
331
|
+
/** The command job an operation job becomes when it runs: the kernel's own
|
|
332
|
+
* operation runner in the home, which resolves the provider at that moment
|
|
333
|
+
* and reports a launch receipt (instance/home) only for what the provider
|
|
334
|
+
* launched, never for the source home. */
|
|
335
|
+
export function operationAsCommand(def) {
|
|
336
|
+
return { ...def, kind: "command", cwd: def.home, argv: ["oats", "operation", "run", def.operation, "--home", def.home] };
|
|
337
|
+
}
|
|
323
338
|
/** Parse a command's stdout as one JSON document (remote output can be
|
|
324
339
|
* multi-line); fall back to the last {...} block; null when nothing parses. */
|
|
325
340
|
export function parseEnvelopeText(text) {
|
|
@@ -355,9 +370,16 @@ function launchCommand(ws, def, io) {
|
|
|
355
370
|
if (instance && !home) { const found = findHomesInScope(ws, instance); if (found.length === 1) home = found[0]; }
|
|
356
371
|
if (envelope.ok && instance && !home) return { kind: "command", launched: true, instance, unconfirmed: true, error: `the envelope names instance ${instance} but no home for it is in this scope's roster; run oats schedule reconcile once it appears` };
|
|
357
372
|
// A valid ok:false answer that reports an incomplete rollback (a harvest
|
|
358
|
-
// spawn whose compensation could not stop or remove everything)
|
|
359
|
-
//
|
|
360
|
-
|
|
373
|
+
// spawn whose compensation could not stop or remove everything), or an
|
|
374
|
+
// operation runner's unconfirmed outcome (timeout, no valid receipt, a
|
|
375
|
+
// receipt contradicted by the exit status), is not a confirmed failure.
|
|
376
|
+
// Whatever name the provider managed to answer travels in error.details
|
|
377
|
+
// so reconcile can adopt it.
|
|
378
|
+
if (!envelope.ok && (UNCONFIRMED_ERROR_CODES.has(envelope.error?.code) || envelope.error?.details?.unconfirmed === true || reportsRetainedEffects(envelope.error?.message))) {
|
|
379
|
+
const partial = envelope.error?.details?.envelope?.result;
|
|
380
|
+
const named = instance || (partial && typeof partial.instance === "string" ? partial.instance : undefined);
|
|
381
|
+
return { kind: "command", launched: false, unconfirmed: true, ...(named ? { instance: named } : {}), error: `command outcome unconfirmed: ${envelope.error?.message}`, errorCode: envelope.error?.code };
|
|
382
|
+
}
|
|
361
383
|
return { kind: "command", launched: envelope.ok === true, ...(instance ? { instance } : {}), ...(home ? { home } : {}), ...(envelope.ok ? {} : { error: envelope.error?.message || "command failed", errorCode: envelope.error?.code }) };
|
|
362
384
|
}
|
|
363
385
|
/** One wake. `startIfStopped` false between due minutes (a harness that
|
|
@@ -367,6 +389,8 @@ function launchCommand(ws, def, io) {
|
|
|
367
389
|
* confirm (spawn compensation's "rollback INCOMPLETE", a quarantined or
|
|
368
390
|
* retained home): the launch's effects are unconfirmed, never a confirmed
|
|
369
391
|
* failure. Shared by caught spawn errors and ok:false command envelopes. */
|
|
392
|
+
/** Error codes whose meaning is "the effects are unconfirmed" by contract. */
|
|
393
|
+
const UNCONFIRMED_ERROR_CODES = new Set(["E_OPERATION_TIMEOUT", "E_OPERATION_RESULT"]);
|
|
370
394
|
export function reportsRetainedEffects(message) { return /INCOMPLETE|quarantin|retain|could not (?:be )?(?:verif|confirm)/i.test(String(message || "")); }
|
|
371
395
|
function performWake(def, io, { startIfStopped = true, canStart = true, reserve } = {}) {
|
|
372
396
|
const seen = observeHome(def.home, io);
|
|
@@ -498,7 +522,7 @@ export function tickWorkspace(ws, { now = new Date(), io, reg, wsList, dryRun =
|
|
|
498
522
|
js.lastLaunchedAt = now.toISOString();
|
|
499
523
|
writeState(ws, st);
|
|
500
524
|
let run;
|
|
501
|
-
try { run = def.kind === "
|
|
525
|
+
try { run = def.kind === "spawn" ? launchSpawn(ws, def, minute, io) : launchCommand(ws, def.kind === "operation" ? operationAsCommand(def) : def, io); }
|
|
502
526
|
catch (e) {
|
|
503
527
|
// spawnInstance compensates its own failures, but its rollback can be
|
|
504
528
|
// INCOMPLETE (a pane it could not stop, a quarantined home it kept):
|
|
@@ -595,7 +619,7 @@ export function reconcile(ws, id, { io, now = new Date(), clear = false } = {})
|
|
|
595
619
|
const homes = findHomesInScope(ws, `${def.agent}-${purpose}`);
|
|
596
620
|
if (homes.length === 1) adopted = { instance: `${def.agent}-${purpose}`, home: homes[0] };
|
|
597
621
|
else if (homes.length > 1) ambiguous = `${homes.length} homes named ${def.agent}-${purpose}: ${homes.join(", ")}`;
|
|
598
|
-
} else if (def.kind === "command") {
|
|
622
|
+
} else if (def.kind === "command" || def.kind === "operation") {
|
|
599
623
|
const named = js.lastRun?.instance ? findHomesInScope(ws, js.lastRun.instance).map((home) => ({ instance: js.lastRun.instance, home })) : [];
|
|
600
624
|
if (named.length === 1) adopted = named[0];
|
|
601
625
|
else if (named.length > 1) ambiguous = `${named.length} homes named ${js.lastRun.instance}: ${named.map((c) => c.home).join(", ")}`;
|
|
@@ -651,7 +675,7 @@ export function updateSchedule(ws, id, spec, io) {
|
|
|
651
675
|
return withHostLock(() => withScopeLock(ws, () => {
|
|
652
676
|
const defs = readDefinitions(ws);
|
|
653
677
|
if (!defs.jobs[id]) throw scheduleError("E_SCHEDULE_UNKNOWN", `no schedule ${JSON.stringify(id)} in ${ws}`);
|
|
654
|
-
const identity = (d) => JSON.stringify([d.kind, d.agent, d.agentsRoot, d.repo, d.purpose, d.home, d.cwd, d.argv]);
|
|
678
|
+
const identity = (d) => JSON.stringify([d.kind, d.agent, d.agentsRoot, d.repo, d.purpose, d.home, d.cwd, d.argv, d.operation]);
|
|
655
679
|
const busy = !!jobLockInfo(ws, id) || !!readState(ws).jobs[id]?.attempt;
|
|
656
680
|
if (busy && identity(defs.jobs[id]) !== identity(def)) throw scheduleError("E_SCHEDULE_RUNNING", `schedule ${id} is running or has an unresolved attempt; its kind, agent, agentsRoot, repo, purpose, home, cwd and argv cannot change until it ends (cron, tz, task, message, runtime, model and enabled can)`);
|
|
657
681
|
defs.jobs[id] = { ...def, createdAt: defs.jobs[id].createdAt, updatedAt: new Date().toISOString() };
|