@awebai/oats 0.28.0 → 0.29.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 +298 -107
- package/capabilities/oats-okf/bin/oats-okf.mjs +28 -8
- package/capabilities/oats-okf/injects/okf.md +33 -33
- package/capabilities/oats-okf/lib/binding-wire.mjs +4 -1
- package/capabilities/oats-okf/lib/config.mjs +2 -1
- package/capabilities/oats-okf/lib/consult.mjs +1 -5
- package/capabilities/oats-okf/lib/harvest-status.mjs +88 -0
- package/capabilities/oats-okf/lib/harvest-switch.mjs +81 -0
- package/capabilities/oats-okf/lib/okf-validate.mjs +123 -0
- package/capabilities/oats-okf/lib/sources.mjs +28 -3
- package/capabilities/oats-okf/lib/stores.mjs +9 -4
- package/capabilities/oats-okf/lib/worker.mjs +82 -8
- package/capabilities/oats-okf/oats.json +14 -8
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +8 -6
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +1 -1
- package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +104 -0
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +140 -0
- package/capabilities/oats-okf-harvest/injects/harvester.md +12 -0
- package/capabilities/oats-okf-harvest/oats.json +26 -0
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +168 -0
- package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +192 -0
- package/capabilities/{oats-okf/skills/okf → oats-okf-harvest/skills/okf-authoring}/SKILL.md +15 -30
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +149 -0
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +12 -0
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +45 -0
- package/capabilities/oats-okf-maintenance/oats.json +21 -0
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +144 -0
- package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +192 -0
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +151 -0
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +123 -0
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +146 -0
- package/capabilities/oats-review/injects/review.md +3 -2
- package/capabilities/oats-review/oats.json +3 -4
- package/docs/capabilities.md +41 -9
- package/docs/capability-manifest.schema.json +0 -7
- package/docs/desktop-cli-api.md +277 -15
- package/docs/implementation.md +1 -1
- package/docs/knowledge-capability-authoring.md +8 -2
- package/docs/knowledge-reference/package-craft.md +8 -5
- package/docs/knowledge.md +101 -0
- package/docs/oats-local.schema.json +31 -1
- package/docs/official-catalog.md +7 -4
- package/docs/packages.md +11 -5
- package/docs/release-lane.md +1 -1
- package/docs/release-notes/v0.29.0.md +240 -0
- package/docs/release-notes/v0.29.1.md +79 -0
- package/docs/schedules.md +133 -5
- package/docs/souls-and-instances.md +4 -6
- package/docs/workspaces.md +11 -2
- package/lib/automations.mjs +369 -0
- package/lib/core.mjs +65 -154
- package/lib/instance-git.mjs +30 -1
- package/lib/instance-inspect.mjs +12 -4
- package/lib/instance-resolution.mjs +31 -182
- package/lib/materialize.mjs +5 -7
- package/lib/operator-dispatch.mjs +1 -2
- package/lib/packages.mjs +17 -0
- package/lib/remote.mjs +21 -1
- package/lib/resolve.mjs +51 -7
- package/lib/schedule.mjs +211 -41
- package/lib/triggers.mjs +182 -49
- package/lib/workspace.mjs +1 -1
- package/package-catalog.json +6 -4
- package/package.json +1 -1
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +0 -26
- package/capabilities/oats-okf/agents/memory-harvest/soul.yaml +0 -5
- package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +0 -285
- package/capabilities/oats-review/agents/reviewer/AGENTS.md +0 -53
- package/capabilities/oats-review/agents/reviewer/soul.yaml +0 -6
- /package/capabilities/{oats-okf/skills/okf → oats-okf-harvest/skills/okf-authoring}/scripts/okf-validate.mjs +0 -0
package/bin/oats.mjs
CHANGED
|
@@ -27,10 +27,10 @@ import {
|
|
|
27
27
|
LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
|
|
28
28
|
capabilityManifests, capabilityTrust, capabilityExecutablePath,
|
|
29
29
|
officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
|
|
30
|
-
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, explicitInstanceName,
|
|
30
|
+
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, explicitInstanceName, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
31
31
|
} from "../lib/core.mjs";
|
|
32
32
|
import {
|
|
33
|
-
writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages,
|
|
33
|
+
writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages, memoizedRemote,
|
|
34
34
|
classifyPackageValue, parsePackageRequest } from "../lib/packages.mjs";
|
|
35
35
|
import { loadLocal, validateWorkspace, validateLocal, discoverPackageSouls, workspaceWarnings } from "../lib/workspace.mjs";
|
|
36
36
|
import { parseConfigData } from "../lib/config-data.mjs";
|
|
@@ -39,7 +39,8 @@ import YAML from "yaml";
|
|
|
39
39
|
import { attachArgv, checkRemote, forgetSnapshot, getServer, inspectRemote, startRemote, restartRemote, launchConfigRemote, scheduleRemote, listSnapshots, readServers, rosterGroups, routeCommand, targetOf, validateServer, writeServers, SERVERS_FILE } from "../lib/servers.mjs";
|
|
40
40
|
import { spawnSync as spawnSyncProc } from "node:child_process";
|
|
41
41
|
import { tickTriggers } from "../lib/triggers.mjs";
|
|
42
|
-
import
|
|
42
|
+
import * as A from "../lib/automations.mjs";
|
|
43
|
+
import { parseEnvelopeText, scheduleScopeOf, listSchedules, describe as describeSchedule, testSchedule, addSchedule, updateSchedule, setEnabled as setScheduleEnabled, removeSchedule, runNow as runScheduleNow, reconcile as reconcileSchedule, tickHost, tickWorkspace, scopeAutomations, scheduleKind, registerWorkspace, unregisterWorkspace, readRegistry, schedulerStatus, saveWakeForHome, removeWakeForHome, wakeFromFlags, withHostLock, scheduleError, SCHEDULE_API } from "../lib/schedule.mjs";
|
|
43
44
|
import { hostUnitStatus, installHostUnit, uninstallHostUnit } from "../lib/schedule-host.mjs";
|
|
44
45
|
import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_BYTES } from "../lib/attachments.mjs";
|
|
45
46
|
|
|
@@ -70,7 +71,7 @@ function expandInlineValues(argv) {
|
|
|
70
71
|
const { argv: args, problem: argvProblem } = expandInlineValues(rawArgs);
|
|
71
72
|
let cmd = args[0];
|
|
72
73
|
const HELP_WORDS = new Set(["help", "--help", "-h"]);
|
|
73
|
-
const KERNEL_COMMANDS = new Set(["capture", "capabilities", "doctor", "inspect", "instance", "operation", "package", "readiness", "souls", "launch-config", "experimental", "onboard", "pane", "recall", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "sync", "update", "version", "workspace"]);
|
|
74
|
+
const KERNEL_COMMANDS = new Set(["automations", "trigger", "capture", "capabilities", "doctor", "inspect", "instance", "operation", "package", "readiness", "souls", "launch-config", "experimental", "onboard", "pane", "recall", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "sync", "update", "version", "workspace"]);
|
|
74
75
|
/** Commands whose argv another parser reads (packages/record and packages/experimental parse process.argv). */
|
|
75
76
|
const OWN_ARGV_COMMANDS = new Set(["capture", "recall", "setup", "experimental"]);
|
|
76
77
|
/** Commands `--server <id>` runs on a registered server. */
|
|
@@ -957,7 +958,8 @@ function instanceCmd() {
|
|
|
957
958
|
console.log(` upstream: ${observed.upstream.ref ? `${observed.upstream.ref} +${observed.upstream.ahead} -${observed.upstream.behind}` : "none (ahead/behind unknown)"}`);
|
|
958
959
|
console.log(` base: ${observed.base.ref ? `${observed.base.ref} +${observed.base.ahead} -${observed.base.behind} (merge-base ${observed.base.mergeBase?.slice(0, 12)})` : "unknown"}`);
|
|
959
960
|
console.log(` files: ${observed.files.length} (${Object.entries(observed.summary).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(", ") || "clean"})`);
|
|
960
|
-
|
|
961
|
+
const lines = (f) => (f.binary ? " binary" : Number.isInteger(f.additions) ? ` +${f.additions} -${f.deletions}` : "");
|
|
962
|
+
for (const f of observed.files) console.log(` ${f.xy} ${f.origPath ? `${f.origPath} -> ` : ""}${f.path}${lines(f)} [${f.id}]`);
|
|
961
963
|
for (const n of observed.notes) console.log(` note: ${n}`);
|
|
962
964
|
return;
|
|
963
965
|
}
|
|
@@ -1072,6 +1074,10 @@ const memberLabel = (key) => String(key).split("/").filter(Boolean).pop()?.repla
|
|
|
1072
1074
|
const teamLabel = (team) => team ?? "unassigned";
|
|
1073
1075
|
const originOf = (item) => (item.package ? `package ${item.package} v${item.version}` : `member ${item.repoKey} @ ${short(item.commit)}`);
|
|
1074
1076
|
|
|
1077
|
+
/** A soul's spawn default harness and model (feature desktop-facts): its definition's, else the kernel's (pi, the
|
|
1078
|
+
* harness's native model) — what `oats spawn <soul>` takes with no --harness/--model/--launch-config. */
|
|
1079
|
+
const soulDefaults = (def) => ({ harness: def?.harness ?? "pi", model: def?.model ?? null, harnessFrom: def?.harness ? "soul" : "kernel-default" });
|
|
1080
|
+
|
|
1075
1081
|
/** Rows of every soul and capability of confirmed members (+ external souls) + locked package
|
|
1076
1082
|
* capabilities. Souls have no private mode (0.26.0); a private member capability is listed with
|
|
1077
1083
|
* `private: true` — repo-owned: usable only by its own repo's souls (E_CAPABILITY_PRIVATE). */
|
|
@@ -1080,15 +1086,15 @@ function workspaceItems(discovery, lock) {
|
|
|
1080
1086
|
const capabilities = [];
|
|
1081
1087
|
for (const m of discovery.members) {
|
|
1082
1088
|
if (!m.confirmed && !(discovery.standalone === true && m.key === discovery.key)) continue;
|
|
1083
|
-
for (const s of m.souls) souls.push({ name: s.name, origin: originOf(s), kind: "member", repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? (s.team ? [s.team] : []))], private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null });
|
|
1089
|
+
for (const s of m.souls) souls.push({ name: s.name, origin: originOf(s), kind: "member", repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? (s.team ? [s.team] : []))], private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null, ...soulDefaults(s.definition) });
|
|
1084
1090
|
for (const c of m.capabilities) capabilities.push({ name: c.name, origin: originOf(c), kind: "member", repoKey: c.repoKey, commit: c.commit, team: teamLabel(c.team), private: c.private, path: c.path, layer: c.manifest.layer ?? null, version: c.manifest.version ?? null });
|
|
1085
1091
|
}
|
|
1086
1092
|
for (const ext of discovery.external || []) {
|
|
1087
1093
|
const s = ext.soul;
|
|
1088
|
-
souls.push({ name: s.name, origin: `external ${s.repoKey} @ ${short(s.commit)}`, kind: "external", repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? (s.team ? [s.team] : []))], private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null });
|
|
1094
|
+
souls.push({ name: s.name, origin: `external ${s.repoKey} @ ${short(s.commit)}`, kind: "external", repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? (s.team ? [s.team] : []))], private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null, ...soulDefaults(s.definition) });
|
|
1089
1095
|
}
|
|
1090
1096
|
for (const s of discovery.packageSouls || []) {
|
|
1091
|
-
souls.push({ name: s.name, qualifiedName: s.qualifiedName, origin: originOf(s), kind: "package", package: s.package, version: s.version, repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? [])], private: false, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null });
|
|
1097
|
+
souls.push({ name: s.name, qualifiedName: s.qualifiedName, origin: originOf(s), kind: "package", package: s.package, version: s.version, repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? [])], private: false, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null, ...soulDefaults(s.definition) });
|
|
1092
1098
|
}
|
|
1093
1099
|
for (const [id, entry] of Object.entries(lock?.packages || {})) {
|
|
1094
1100
|
for (const name of entry.capabilities) capabilities.push({ name, origin: originOf({ package: id, version: entry.version }), kind: "package", package: id, version: entry.version, commit: entry.commit, team: teamLabel(null), private: false });
|
|
@@ -1164,11 +1170,17 @@ async function performSync(ctx, bail, { onDiscovered } = {}) {
|
|
|
1164
1170
|
// The deployment's instance root: <deployment>/agents/ (findRoot's marker). A hand-written
|
|
1165
1171
|
// oats-local.yaml + sync is a complete deployment; spawn must not answer E_NO_DEPLOYMENT after it.
|
|
1166
1172
|
try { mkdirSync(join(ctx.deploymentDir, "agents"), { recursive: true }); } catch { /* reported by spawn's E_NO_DEPLOYMENT remedy if it matters */ }
|
|
1173
|
+
// Workspace automations (0.29.0): discovered with the members and the new lock (a trigger
|
|
1174
|
+
// template instantiates at the locked package commit), kept as the snapshot the host tick reads.
|
|
1175
|
+
let automations;
|
|
1176
|
+
try { automations = await takeAutomationSnapshot(ctx, discovery, lock); }
|
|
1177
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
1178
|
+
problems.push(...automations.problems);
|
|
1167
1179
|
const members = memberRows(discovery);
|
|
1168
1180
|
const packages = packageRows(lock);
|
|
1169
1181
|
const changes = resolved.changes;
|
|
1170
1182
|
const items = workspaceItems(discovery, lock);
|
|
1171
|
-
const report = { syncApi: 1, standalone: discovery.standalone === true || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, url: discovery.url, commit: discovery.commit, observedAt: discovery.observedAt, local: ctx.localPath, lock: lockFile }, members, packages, changes, problems, warnings: discovery.warnings ?? [] };
|
|
1183
|
+
const report = { syncApi: 1, automations: automationCounts(automations), standalone: discovery.standalone === true || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, url: discovery.url, commit: discovery.commit, observedAt: discovery.observedAt, local: ctx.localPath, lock: lockFile }, members, packages, changes, problems, warnings: discovery.warnings ?? [] };
|
|
1172
1184
|
return { report, lock, discovery, items, lockFile, problems };
|
|
1173
1185
|
}
|
|
1174
1186
|
|
|
@@ -1207,11 +1219,91 @@ function printSyncReport(ctx, synced) {
|
|
|
1207
1219
|
for (const s of items.souls) { const t = teams.get(s.team) || { souls: 0, capabilities: 0 }; t.souls++; teams.set(s.team, t); }
|
|
1208
1220
|
for (const c of items.capabilities.filter((c) => c.kind === "member")) { const t = teams.get(c.team) || { souls: 0, capabilities: 0 }; t.capabilities++; teams.set(c.team, t); }
|
|
1209
1221
|
console.log(`teams ${[...teams.entries()].sort(([a], [b]) => (a < b ? -1 : 1)).map(([team, n]) => `${team} ${n.souls} soul${n.souls === 1 ? "" : "s"}${n.capabilities ? `, ${n.capabilities} capabilit${n.capabilities === 1 ? "y" : "ies"}` : ""}`).join(" · ") || "(none)"}`);
|
|
1222
|
+
const ac = report.automations;
|
|
1223
|
+
if (ac.triggers || ac.schedules) console.log(`automations ${ac.triggers} trigger${ac.triggers === 1 ? "" : "s"}, ${ac.schedules} schedule${ac.schedules === 1 ? "" : "s"} in the members (oats trigger list · oats schedule list)`);
|
|
1210
1224
|
for (const p of synced.problems ?? discovery.problems) console.log(`problem ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`);
|
|
1211
1225
|
for (const w of discovery.warnings ?? []) console.log(`warning ${w.code} ${w.message}`);
|
|
1212
1226
|
console.log(`\nlock ${shortPath(lockFile)}`);
|
|
1213
1227
|
}
|
|
1214
1228
|
|
|
1229
|
+
/** Discover the workspace automations of a discovery and write the snapshot the host tick reads
|
|
1230
|
+
* (lib/automations.mjs). A trigger's `from:` instantiates the package template at the lock's
|
|
1231
|
+
* commit; a schedule is checked against this deployment. → the snapshot */
|
|
1232
|
+
async function takeAutomationSnapshot(ctx, discovery, lock) {
|
|
1233
|
+
const kinds = await automationKinds(ctx.deploymentDir, lock, ctx.remoteOptions);
|
|
1234
|
+
const found = await A.discoverAutomations(discovery, { remote: kinds.remote, memberName: memberLabel, kinds: [kinds.trigger, kinds.schedule], previous: A.readSnapshot(ctx.deploymentDir) });
|
|
1235
|
+
const snap = A.snapshotOf(discovery, found, { souls: A.soulIndexOf(discovery, memberLabel) });
|
|
1236
|
+
// A workspace with no triggers or schedules leaves nothing behind (onboard creates what the
|
|
1237
|
+
// kernel needs and nothing else); a snapshot that existed is replaced, so a removal lands.
|
|
1238
|
+
const empty = !snap.triggers.length && !snap.schedules.length && !snap.problems.length;
|
|
1239
|
+
if (!empty || A.readSnapshot(ctx.deploymentDir)) A.writeSnapshot(ctx.deploymentDir, snap);
|
|
1240
|
+
return snap;
|
|
1241
|
+
}
|
|
1242
|
+
/** The two kinds' descriptors (lib/triggers.mjs triggerKind, lib/schedule.mjs scheduleKind). */
|
|
1243
|
+
async function automationKinds(dep, lock, remoteOptions) {
|
|
1244
|
+
const T = await import("../lib/triggers.mjs");
|
|
1245
|
+
const { bindRemote } = await import("../lib/packages.mjs");
|
|
1246
|
+
const remote = bindRemote(remoteModule, remoteOptions);
|
|
1247
|
+
return { remote, trigger: T.triggerKind({ lock, remote }), schedule: scheduleKind({ dep }) };
|
|
1248
|
+
}
|
|
1249
|
+
const automationCounts = (snap) => ({ triggers: snap.triggers.length, schedules: snap.schedules.length, problems: snap.problems.length, takenAt: snap.takenAt });
|
|
1250
|
+
|
|
1251
|
+
/** `oats automations refresh [--dir] [--json]`: re-discover the workspace automations and rewrite
|
|
1252
|
+
* the snapshot (what `oats sync` does for them, without touching the lock). The host tick runs it
|
|
1253
|
+
* when the snapshot is ten minutes old. */
|
|
1254
|
+
async function automationsCmd() {
|
|
1255
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1256
|
+
if (args[1] !== "refresh") return bail("E_USAGE", "usage: oats automations refresh [--dir <deployment>] [--json]");
|
|
1257
|
+
const ctx = workspaceContext(bail);
|
|
1258
|
+
const discovery = await discoverForCli(ctx, bail);
|
|
1259
|
+
let lock;
|
|
1260
|
+
try { lock = readLock(ctx.deploymentDir); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
|
|
1261
|
+
let snap;
|
|
1262
|
+
try { snap = await takeAutomationSnapshot(ctx, discovery, lock); }
|
|
1263
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
1264
|
+
const result = { automationsApi: A.AUTOMATIONS_API, snapshot: A.snapshotPath(ctx.deploymentDir), ...automationCounts(snap), problems: snap.problems };
|
|
1265
|
+
if (JSON_MODE) return jsonOk(result);
|
|
1266
|
+
console.log(`automations ${result.triggers} trigger(s), ${result.schedules} schedule(s) — snapshot ${shortPath(result.snapshot)}`);
|
|
1267
|
+
for (const p of snap.problems) console.log(`problem ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/** `oats trigger|schedule add --workspace <member> --runs-on <host> --owner <host>/<login>`: the
|
|
1271
|
+
* workspace file, written into the member's checkout when `--dir` (or the cwd) is inside one, else
|
|
1272
|
+
* printed. What is written must read back as the same automation (parseAutomationFile + expand). */
|
|
1273
|
+
async function addWorkspaceAutomation(desc, { id, body }) {
|
|
1274
|
+
const bail = (code, msg, details) => { throw Object.assign(new Error(msg), { code, details }); };
|
|
1275
|
+
const member = flag("workspace"), runsOn = flag("runs-on"), owner = flag("owner"), description = flag("description");
|
|
1276
|
+
if (typeof member !== "string" || !member) bail("E_BAD_ARGS", "--workspace needs a member name");
|
|
1277
|
+
if (typeof runsOn !== "string" || !A.HOST_NAME_RE.test(runsOn)) bail("E_BAD_ARGS", "--runs-on <host>: the host.name (oats-local.yaml) of the machine that runs it");
|
|
1278
|
+
if (!A.parseOwner(owner)) bail("E_BAD_ARGS", "--owner <host>/<login>: the GitHub account it acts as, e.g. github.com/acme-kb-bot");
|
|
1279
|
+
const ctx = workspaceContext(bail);
|
|
1280
|
+
const discovery = await discoverForCli(ctx, bail);
|
|
1281
|
+
const row = (discovery.members || []).find((m) => m.confirmed && (memberLabel(m.key) === member || m.key === member));
|
|
1282
|
+
if (!row) bail("E_AUTOMATION_MEMBER", `${member} is not a confirmed member of this workspace (members: ${(discovery.members || []).filter((m) => m.confirmed).map((m) => memberLabel(m.key)).join(", ") || "none"})`, { member });
|
|
1283
|
+
const name = memberLabel(row.key);
|
|
1284
|
+
const content = A.automationFileText(desc, { id, description: typeof description === "string" ? description : undefined, runsOn, owner, body });
|
|
1285
|
+
const parsed = A.parseAutomationFile(desc, { stem: id, path: `${desc.folder}/${id}.yaml`, bytes: Buffer.from(content), member: name, repoKey: row.key, commit: row.commit });
|
|
1286
|
+
if (parsed.problem) bail(parsed.problem.code, parsed.problem.message, { path: parsed.problem.path });
|
|
1287
|
+
await desc.expand(parsed.entry);
|
|
1288
|
+
const sameRepo = (url) => { try { return remoteModule.parseRepoRef(url).key === row.key; } catch { return false; } };
|
|
1289
|
+
const at = A.memberCheckoutFor(dirFlag(), desc, id, { sameRepo });
|
|
1290
|
+
const file = { member: name, repoKey: row.key, path: `${desc.folder}/${id}.yaml`, content };
|
|
1291
|
+
if (!at) return { file, written: false, id: `${name}/${id}` };
|
|
1292
|
+
if (existsSync(at.file)) bail(desc.kind === "trigger" ? "E_TRIGGER_EXISTS" : "E_SCHEDULE_EXISTS", `${at.file} already exists; edit it in place`, { path: at.file });
|
|
1293
|
+
mkdirSync(dirname(at.file), { recursive: true });
|
|
1294
|
+
writeFileSync(at.file, content);
|
|
1295
|
+
return { file: { ...file, written: at.file }, written: true, id: `${name}/${id}` };
|
|
1296
|
+
}
|
|
1297
|
+
const printWorkspaceAdd = (r) => r.written
|
|
1298
|
+
? `wrote ${shortPath(r.file.written)} — commit and push it; after \`oats sync\` (or within ten minutes) ${r.id} runs on its host`
|
|
1299
|
+
: `# ${r.file.path} in ${r.file.member} (${r.file.repoKey}) — this directory is not a checkout of it; save this there, commit and push:\n${r.file.content}`;
|
|
1300
|
+
/** Enable/disable a WORKSPACE trigger or schedule on this host: oats-local.yaml
|
|
1301
|
+
* `triggers.disabled` / `schedules.disabled`. */
|
|
1302
|
+
function setDisabledHereCli(ws, kind, qid, enabled) {
|
|
1303
|
+
const { path } = loadLocal(ws);
|
|
1304
|
+
return A.setDisabledHere(path, kind, qid, !enabled, { validate: validateLocal });
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1215
1307
|
/** `oats sync [--dir] [--json]` — contract §6. */
|
|
1216
1308
|
async function syncCmd() {
|
|
1217
1309
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
@@ -1325,6 +1417,10 @@ async function workspaceCmd() {
|
|
|
1325
1417
|
const unsynced = declared.filter((id) => !locked.has(id));
|
|
1326
1418
|
const stale = packages.filter((p) => !declared.includes(p.id)).map((p) => p.id);
|
|
1327
1419
|
const result = { workspaceStatusApi: 1, standalone: standalone || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, url: discovery.url, commit: discovery.commit, observedAt: discovery.observedAt, local: ctx.localPath, teams: Object.keys(discovery.workspace?.teams || {}) }, members, packages, declaredPackages: declared, unsynced, stale, external: (discovery.external || []).map((e) => ({ source: e.source, soul: e.soul.name, team: teamLabel(e.soul.team) })), problems: discovery.problems, warnings: discovery.warnings ?? [] };
|
|
1420
|
+
// Workspace automations (0.29.0), from the snapshot `oats sync` took, placed on this host.
|
|
1421
|
+
const actx = scopeAutomations(ctx.deploymentDir, {});
|
|
1422
|
+
result.automations = { host: actx.host.name, snapshot: actx.snapshot ? { takenAt: actx.snapshot.takenAt, problems: actx.snapshot.problems.length } : null, rows: [...actx.triggers, ...actx.schedules].map((a) => ({ kind: a.kind, id: a.id, runsOn: a.runsOn, owner: a.owner, runsHere: a.placement.runsHere, reason: a.placement.reason, enabledHere: a.placement.enabledHere, origin: a.origin, ...(a.invalid ? { invalid: a.invalid } : {}) })) };
|
|
1423
|
+
await workspaceStatusFacts(result, discovery, lock, ctx);
|
|
1328
1424
|
if (JSON_MODE) { jsonOk(result); return; }
|
|
1329
1425
|
console.log(`workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)}) local ${shortPath(ctx.localPath)}\n`);
|
|
1330
1426
|
if (standalone) console.log(` (${standaloneNote(discovery)})\n`);
|
|
@@ -1337,10 +1433,47 @@ async function workspaceCmd() {
|
|
|
1337
1433
|
if (packages.length && unsynced.length) console.log(` declared but not locked (run \`oats sync\`): ${unsynced.join(", ")}`);
|
|
1338
1434
|
if (stale.length) console.log(` locked but no longer declared (run \`oats sync\`): ${stale.join(", ")}`);
|
|
1339
1435
|
if (result.external.length) console.log(`\nExternal: ${result.external.map((e) => `${e.soul} (${e.source.replace(/@([0-9a-f]{40})$/, (_, o) => `@${short(o)}`)}, ${e.team})`).join(" ")}`);
|
|
1436
|
+
if (result.automations.rows.length) {
|
|
1437
|
+
console.log(`\nAutomations (this host: ${result.automations.host ?? "unnamed — set host.name in oats-local.yaml"}):`);
|
|
1438
|
+
printTable(["kind", "id", "runs on", "owner", "here"], result.automations.rows.map((r) => [r.kind, r.id, r.runsOn, r.owner, r.runsHere ? "runs here" : r.invalid ? "invalid" : r.reason ?? "disabled here"]));
|
|
1439
|
+
}
|
|
1340
1440
|
if (discovery.problems.length) { console.log("\nProblems:"); for (const p of discovery.problems) console.log(` ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`); }
|
|
1341
1441
|
if (discovery.warnings?.length) { console.log("\nWarnings:"); for (const w of discovery.warnings) console.log(` ${w.code} ${w.message}`); }
|
|
1342
1442
|
}
|
|
1343
1443
|
|
|
1444
|
+
/** `oats workspace status` facts (feature desktop-facts): the workspace and team defaults as rows; this
|
|
1445
|
+
* computer (member clones and the rule that found each, disabled souls, the lock); file locations with
|
|
1446
|
+
* browsable urls; each catalog package's newer version, when the official catalog has one. */
|
|
1447
|
+
async function workspaceStatusFacts(result, discovery, lock, ctx) {
|
|
1448
|
+
const { memberCloneOf } = await import("../lib/instance-resolution.mjs");
|
|
1449
|
+
const { browseUrl } = remoteModule;
|
|
1450
|
+
const defaults = discovery.standalone === true ? {} : (discovery.workspace?.defaults ?? {});
|
|
1451
|
+
const rows = (entries) => Object.entries(entries && typeof entries === "object" ? entries : {}).map(([name, v]) => ({ name, from: v === "off" ? null : v?.from ?? null, off: v === "off" })).sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
1452
|
+
const slot = (d) => (d && typeof d === "object" ? Object.entries(d).map(([name, v]) => ({ name, from: v?.from ?? null }))[0] ?? null : d ?? null);
|
|
1453
|
+
result.defaults = {
|
|
1454
|
+
slots: Object.fromEntries(LAYERS.map((l) => [l, slot(defaults[l])])),
|
|
1455
|
+
capabilities: rows(defaults.capabilities),
|
|
1456
|
+
byTeam: Object.fromEntries(Object.entries(defaults.byTeam && typeof defaults.byTeam === "object" ? defaults.byTeam : {}).map(([label, t]) => [label, { capabilities: rows(t?.capabilities) }])),
|
|
1457
|
+
};
|
|
1458
|
+
result.workspace.file = discovery.standalone === true ? null : { path: "oats-workspace.yaml", url: browseUrl(discovery.key, discovery.commit, "oats-workspace.yaml") };
|
|
1459
|
+
for (const m of result.members) Object.assign(m, { url: browseUrl(m.key, m.commit), membershipFile: { path: "oats-membership.yaml", url: browseUrl(m.key, m.commit, "oats-membership.yaml") } });
|
|
1460
|
+
result.clones = result.members.map((m) => {
|
|
1461
|
+
try { return { key: m.key, name: m.name, ...memberCloneOf(ctx.deploymentDir, ctx.local, m.key) }; }
|
|
1462
|
+
catch (e) { if (e?.code === "E_CLONE_MISMATCH") return { key: m.key, name: m.name, path: null, rule: null, problem: { code: e.code, message: e.message } }; throw e; }
|
|
1463
|
+
});
|
|
1464
|
+
result.disabledSouls = [...(Array.isArray(ctx.local?.souls?.disabled) ? ctx.local.souls.disabled : [])];
|
|
1465
|
+
result.lock = { path: join(ctx.deploymentDir, LOCK_FILE), lockfileVersion: lock.lockfileVersion ?? null };
|
|
1466
|
+
const { compareSemver } = await import("../lib/servers.mjs");
|
|
1467
|
+
let catalog = null; try { catalog = officialPackageCatalog(); } catch { catalog = null; }
|
|
1468
|
+
const cat = catalog && typeof catalog === "object" ? (catalog.packages && typeof catalog.packages === "object" && !("url" in catalog.packages) ? catalog.packages : catalog) : {};
|
|
1469
|
+
for (const p of result.packages) {
|
|
1470
|
+
const ref = p.source === `catalog:${p.id}` ? cat[p.id]?.ref : undefined;
|
|
1471
|
+
const tag = standaloneCatalogVersion(ref);
|
|
1472
|
+
const version = tag ? tag.replace(/^v/, "") : null;
|
|
1473
|
+
p.latest = version && typeof p.version === "string" && compareSemver(version, p.version) > 0 ? { version, ref } : null;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1344
1477
|
/** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
|
|
1345
1478
|
async function itemsCmd(kind) {
|
|
1346
1479
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
@@ -1349,6 +1482,10 @@ async function itemsCmd(kind) {
|
|
|
1349
1482
|
let lock;
|
|
1350
1483
|
try { lock = readLock(ctx.deploymentDir); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
|
|
1351
1484
|
const items = workspaceItems(discovery, lock)[kind];
|
|
1485
|
+
// One command's reads at a commit are shared: many souls resolve over the same manifests and listings.
|
|
1486
|
+
const remote = memoizedRemote(remoteModule);
|
|
1487
|
+
if (kind === "capabilities") await capabilityFacts(items, discovery, lock, ctx, remote);
|
|
1488
|
+
if (kind === "souls") await soulFacts(items, discovery, lock, ctx, remote);
|
|
1352
1489
|
const standalone = discovery.standalone === true;
|
|
1353
1490
|
if (JSON_MODE) { jsonOk({ [`${kind}Api`]: 1, standalone: standalone || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, commit: discovery.commit }, [kind]: items, problems: discovery.problems }); return; }
|
|
1354
1491
|
console.log(`${kind} of workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)})${standalone ? ` — ${standaloneNote(discovery)}` : ""}\n`);
|
|
@@ -1359,6 +1496,60 @@ async function itemsCmd(kind) {
|
|
|
1359
1496
|
if (kind === "capabilities" && unsynced.length) console.log(`\n package capabilities of ${unsynced.join(", ")} appear after \`oats sync\``);
|
|
1360
1497
|
}
|
|
1361
1498
|
|
|
1499
|
+
/** Capability rows' manifest facts (feature desktop-facts): layer, description, and what each provides
|
|
1500
|
+
* (skills, commands, hooks by name) — member rows from discovery's manifest, package rows from the manifest
|
|
1501
|
+
* at the locked commit (the sync cache; no network beyond sync's). An unreadable package leaves its rows' facts null. */
|
|
1502
|
+
async function capabilityFacts(rows, discovery, lock, ctx, remote = remoteModule) {
|
|
1503
|
+
const { capabilityProvides, lockedPackageCapabilities, memberRef } = await import("../lib/resolve.mjs");
|
|
1504
|
+
const remoteOptions = ctx.remoteOptions;
|
|
1505
|
+
const catalog = (() => { try { return officialPackageCatalog(); } catch { return null; } })();
|
|
1506
|
+
const packages = new Map();
|
|
1507
|
+
for (const [id, entry] of Object.entries(lock?.packages || {})) {
|
|
1508
|
+
try { packages.set(id, await lockedPackageCapabilities(id, entry, { catalog, remote, remoteOptions })); } catch { packages.set(id, null); }
|
|
1509
|
+
}
|
|
1510
|
+
// A member capability's fingerprint is its Git tree at the commit (one listing per member commit); a
|
|
1511
|
+
// package's is the package integrity (the lock).
|
|
1512
|
+
const trees = new Map();
|
|
1513
|
+
const treeOf = async ({ repoKey, commit }, ref, dir) => {
|
|
1514
|
+
const key = `${repoKey}\0${commit}`;
|
|
1515
|
+
if (!trees.has(key)) {
|
|
1516
|
+
const member = discovery.members.find((m) => m.key === repoKey);
|
|
1517
|
+
const dirs = rows.filter((r) => r.kind !== "package" && r.repoKey === repoKey && r.commit === commit).map((r) => member?.capabilities.find((c) => c.name === r.name)?.path).filter(Boolean);
|
|
1518
|
+
trees.set(key, remoteModule.remoteTreeOids(ref, commit, [...new Set(dirs)], remoteOptions).catch(() => new Map()));
|
|
1519
|
+
}
|
|
1520
|
+
return (await trees.get(key)).get(dir) ?? null;
|
|
1521
|
+
};
|
|
1522
|
+
for (const row of rows) {
|
|
1523
|
+
let manifest = null, ref = null, dir = null;
|
|
1524
|
+
if (row.kind === "package") {
|
|
1525
|
+
const read = packages.get(row.package), cap = read?.capabilities.find((c) => c.name === row.name);
|
|
1526
|
+
if (cap) ({ manifest, dir } = cap), ref = read.ref;
|
|
1527
|
+
} else {
|
|
1528
|
+
const cap = discovery.members.find((m) => m.key === row.repoKey)?.capabilities.find((c) => c.name === row.name);
|
|
1529
|
+
if (cap) { manifest = cap.manifest; dir = cap.path; ref = memberRef(discovery, remoteModule, row.repoKey); }
|
|
1530
|
+
}
|
|
1531
|
+
const provides = manifest ? await capabilityProvides({ ref, commit: row.commit, dir, manifest, remote, remoteOptions }) : { skills: null, commands: null, hooks: null };
|
|
1532
|
+
const repoKey = row.kind === "package" && ref ? (() => { try { return remoteModule.parseRepoRef(ref).key; } catch { return null; } })() : row.repoKey;
|
|
1533
|
+
const file = dir ? { path: `${dir}/oats.json`, url: remoteModule.browseUrl(repoKey, row.commit, `${dir}/oats.json`) } : null;
|
|
1534
|
+
const tree = row.kind !== "package" && ref && dir ? await treeOf(row, ref, dir) : null;
|
|
1535
|
+
Object.assign(row, { layer: manifest?.layer ?? row.layer ?? null, description: manifest?.description ?? null, ...provides, file, tree });
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/** Souls rows' spawnability (feature desktop-facts): the refusal a spawn of each would meet, computed from
|
|
1540
|
+
* discovery and the lock without spawning (soulSpawnability). */
|
|
1541
|
+
async function soulFacts(rows, discovery, lock, ctx, remote = remoteModule) {
|
|
1542
|
+
const { soulSpawnability } = await import("../lib/instance-resolution.mjs");
|
|
1543
|
+
const entryOf = (row) => row.kind === "package" ? (discovery.packageSouls || []).find((p) => p.qualifiedName === row.qualifiedName)
|
|
1544
|
+
: row.kind === "external" ? (discovery.external || []).map((e) => e.soul).find((x) => x.name === row.name && x.repoKey === row.repoKey)
|
|
1545
|
+
: discovery.members.find((m) => m.key === row.repoKey)?.souls.find((x) => x.name === row.name);
|
|
1546
|
+
for (const row of rows) {
|
|
1547
|
+
const entry = entryOf(row);
|
|
1548
|
+
row.file = { path: `${row.path}/soul.yaml`, url: remoteModule.browseUrl(row.repoKey, row.commit, `${row.path}/soul.yaml`) };
|
|
1549
|
+
Object.assign(row, entry ? await soulSpawnability(ctx.local, discovery, lock, entry, { remoteOptions: ctx.remoteOptions, remote }) : { spawnable: false, problem: { code: "E_SOUL_UNKNOWN", message: `${row.name} is not in this discovery` } });
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1362
1553
|
// ---------- roster: status / spawn / retire / create ----------
|
|
1363
1554
|
/** Workspace drift for `oats status` (decision 17: shown, not prevented). ONE discovery over
|
|
1364
1555
|
* the remotes serves every instance; `driftOf` compares each instance's recorded modules to
|
|
@@ -1383,7 +1574,7 @@ async function statusDrift(data) {
|
|
|
1383
1574
|
for (const a of data) {
|
|
1384
1575
|
if (!a.dir) continue;
|
|
1385
1576
|
try { const stamp = JSON.parse(readFileSync(join(a.dir, ".oats-soul-source.json"), "utf8")); if (stamp && typeof stamp.repoKey === "string") souls.set(a.name, { repoKey: stamp.repoKey, commit: typeof stamp.commit === "string" ? stamp.commit : null, path: stamp.path ?? null, ...(typeof stamp.package === "string" ? { package: stamp.package, version: stamp.version ?? null } : {}) }); }
|
|
1386
|
-
catch { /* not a workspace soul (classic,
|
|
1577
|
+
catch { /* not a workspace soul (classic, or an unreadable stamp) */ }
|
|
1387
1578
|
}
|
|
1388
1579
|
const anything = data.some((a) => (a.instances || []).some((i) => hasModules(i) || hasSoul(i)));
|
|
1389
1580
|
if (!anything) return { drift: new Map(), soul: new Map(), souls, unreachable: null };
|
|
@@ -1546,7 +1737,7 @@ async function spawnCmd() {
|
|
|
1546
1737
|
// the per-commit cache, or fetches it to a temporary copy (reported as soulFetched).
|
|
1547
1738
|
const providerPairs = [];
|
|
1548
1739
|
for (let i = 0; i < args.length; i++) if (args[i] === "--provider") { if (!args[i + 1] || !args[i + 2]) bail("E_BAD_ARGS", "--provider needs <capability> <key>=<value>"); providerPairs.push([args[i + 1], args[i + 2]]); i += 2; }
|
|
1549
|
-
let wsPrepared, soulFetched = false,
|
|
1740
|
+
let wsPrepared, soulFetched = false, wsDiscovery;
|
|
1550
1741
|
{
|
|
1551
1742
|
let discovery = null;
|
|
1552
1743
|
try {
|
|
@@ -1581,50 +1772,12 @@ async function spawnCmd() {
|
|
|
1581
1772
|
const file = officialCatalogFile();
|
|
1582
1773
|
bail(e.code, `${e.details?.capability ?? "oats.core"}: the catalog has no package providing oats.core (OATS_PACKAGE_CATALOG=${file}) — standalone spawns resolve only the kernel's default package from the catalog`, { ...(e.details ?? {}), standalone: true, reason: "no-catalog", catalog: file });
|
|
1583
1774
|
}
|
|
1584
|
-
|
|
1585
|
-
// soul, resolved below from a materialized copy) may still answer to this name.
|
|
1586
|
-
if (e?.code === "E_SOUL_UNKNOWN" && !isPreview) { wsSoulUnknown = e; }
|
|
1587
|
-
else if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details);
|
|
1775
|
+
if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details);
|
|
1588
1776
|
else throw e;
|
|
1589
1777
|
}
|
|
1590
1778
|
}
|
|
1591
1779
|
if (agentsRootFlag !== undefined && !agent) bail("E_SOUL_UNKNOWN", `soul "${name}" is not at agents root ${String(agentsRootFlag)}`);
|
|
1592
1780
|
if (isPreview && !agent) bail("E_SOUL_UNKNOWN", `soul "${name}" is not in ${shortPath(root)}; a preview never creates or imports a soul (known: ${listAgents(root).map((a) => a.name).join(", ") || "none"})`);
|
|
1593
|
-
// Capability agent (lead decision c3 Q1): a prepared spawn of its providing module only.
|
|
1594
|
-
let capabilityPrepared, capabilityPkg = null;
|
|
1595
|
-
if (!agent) {
|
|
1596
|
-
// Workspace model: the agent is declared by a capability some INSTANCE already
|
|
1597
|
-
// materialized (the --parent home first, then any home under this root) —
|
|
1598
|
-
// OKF's memory-harvest worker spawned by a knowledge source, for example.
|
|
1599
|
-
const anchorName = flag("parent") || flag("relative-to");
|
|
1600
|
-
const anchorHome = anchorName ? (findInstanceHome(root, String(anchorName)) ?? null) : null;
|
|
1601
|
-
let modAgent;
|
|
1602
|
-
try { modAgent = findModuleCapabilityAgent(root, name, { anchorHome }); }
|
|
1603
|
-
catch (e) { bail(e.code || "E_CAPABILITY_BROKEN", e.message, e.details); }
|
|
1604
|
-
if (modAgent) {
|
|
1605
|
-
agent = modAgent;
|
|
1606
|
-
note(`(capability agent: "${name}" from ${modAgent.capability}, materialized in ${shortPath(modAgent._manifestSource)} — fresh soul, instances home under ${shortPath(join(root, name, "instances"))})`);
|
|
1607
|
-
} else {
|
|
1608
|
-
// No instance carries it: resolve from the deployment's LOCK — a locked
|
|
1609
|
-
// package whose capability declares agents/<name> is fetched into the
|
|
1610
|
-
// deployment's module store and read from there.
|
|
1611
|
-
try {
|
|
1612
|
-
const { resolvePackageCapabilityAgent } = await import("../lib/instance-resolution.mjs");
|
|
1613
|
-
const hit = await resolvePackageCapabilityAgent(dirFlag(), name, { remoteOptions: remoteOptionsFromEnv(), discovery: wsDiscovery, catalog: (() => { try { return officialPackageCatalog(); } catch { return null; } })() });
|
|
1614
|
-
if (hit) {
|
|
1615
|
-
agent = capabilityAgentFromDir(hit.dir, name, root, { module: { from: { kind: "package", package: hit.package, version: hit.version, commit: hit.commit } } });
|
|
1616
|
-
if (agent) { capabilityPkg = hit; note(`(capability agent: "${name}" from ${hit.capability} — package ${hit.package} v${hit.version}, fetched to ${shortPath(hit.dir)} — fresh soul, instances home under ${shortPath(join(root, name, "instances"))})`); }
|
|
1617
|
-
}
|
|
1618
|
-
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
1619
|
-
}
|
|
1620
|
-
if (agent) {
|
|
1621
|
-
try {
|
|
1622
|
-
const { prepareCapabilityAgent } = await import("../lib/instance-resolution.mjs");
|
|
1623
|
-
capabilityPrepared = await prepareCapabilityAgent(dirFlag(), agent, { discovery: wsDiscovery, pkg: capabilityPkg, remoteOptions: remoteOptionsFromEnv(), catalog: (() => { try { return officialPackageCatalog(); } catch { return null; } })() });
|
|
1624
|
-
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
if (!agent && wsSoulUnknown) bail(wsSoulUnknown.code, wsSoulUnknown.message, wsSoulUnknown.details);
|
|
1628
1781
|
checkDirectoryOptions(requestedWork || agent?.work);
|
|
1629
1782
|
if (!agent) bail("E_UNKNOWN_AGENT", `unknown agent "${name}" (known: ${listAgents(root).map((a) => a.name).join(", ") || "none"}) — a soul is a member repository's souls/<name>; add it there and run \`oats sync\``);
|
|
1630
1783
|
for (const information of agent.notes || []) note(`[${information.code}] ${information.message}`);
|
|
@@ -1653,9 +1806,6 @@ async function spawnCmd() {
|
|
|
1653
1806
|
if ((flag("work") === "attached") && relation && relation !== "child") bail("E_BAD_ARGS", "attached agents are always children of the work-tree owner — only --parent <instance> (or --relation child) is valid with --work attached");
|
|
1654
1807
|
// NOTE: explicit "unrelated" is passed through to the kernel.
|
|
1655
1808
|
if (relativeTo && relation !== "unrelated") {
|
|
1656
|
-
// findInstanceHome also sees capability-defined agents' instance homes
|
|
1657
|
-
// (<root>/<name>/ without a soul) — e.g. a reviewer passing
|
|
1658
|
-
// --parent "$OATS_INSTANCE" from a capability agent.
|
|
1659
1809
|
if (!findInstanceHome(root, relativeTo)) bail(parent ? "E_PARENT_NOT_FOUND" : "E_RELATIVE_NOT_FOUND", `${parent ? "--parent" : "--relative-to"} "${relativeTo}" does not match any known instance`);
|
|
1660
1810
|
}
|
|
1661
1811
|
const taskText = flag("task");
|
|
@@ -1715,10 +1865,10 @@ async function spawnCmd() {
|
|
|
1715
1865
|
// convention <deployment>/<member name>. Never an ambient Git checkout around the
|
|
1716
1866
|
// deployment. Resolved once here so a preview sees exactly what the apply would.
|
|
1717
1867
|
let preparedRepo;
|
|
1718
|
-
if (wsPrepared
|
|
1868
|
+
if (wsPrepared) {
|
|
1719
1869
|
try {
|
|
1720
1870
|
const { toCapabilityRows, modulesPreview, requireMemberClone } = await import("../lib/instance-resolution.mjs");
|
|
1721
|
-
prepared = wsPrepared
|
|
1871
|
+
prepared = wsPrepared;
|
|
1722
1872
|
prepared.capabilityRows = []; // filled after materialization (paths live in the home); preview uses modulesPreview
|
|
1723
1873
|
prepared.preview = modulesPreview(prepared.resolution, root, agent.name);
|
|
1724
1874
|
prepared.toCapabilityRows = toCapabilityRows;
|
|
@@ -1782,7 +1932,7 @@ async function spawnCmd() {
|
|
|
1782
1932
|
if (e?.code === "E_IDEMPOTENCY_CONFLICT") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
|
|
1783
1933
|
if (e?.code === "E_PLACEMENT_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
|
|
1784
1934
|
if (e?.code === "E_INSTANCE_NAME_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home ?? null, ...(e.session ? { session: e.session } : {}) }); throw e; }
|
|
1785
|
-
if (e?.code === "E_INSTANCE_NAME_INVALID") { bail(e.code, e.message); throw e; }
|
|
1935
|
+
if (e?.code === "E_INSTANCE_NAME_INVALID") { bail(e.code, e.message, e.details); throw e; }
|
|
1786
1936
|
if (e?.code === "E_SPAWN_INCOMPLETE") { bail(e.code, e.message, { instance: e.instance, home: e.home, launched: e.launched }); throw e; }
|
|
1787
1937
|
bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
|
|
1788
1938
|
}
|
|
@@ -1935,7 +2085,7 @@ function retireCmd() {
|
|
|
1935
2085
|
/** `oats schedule ...`: workspace-scoped definitions, host-owned execution
|
|
1936
2086
|
* (lib/schedule.mjs). Every subcommand answers the envelope; nothing here
|
|
1937
2087
|
* launches unless a job is due or run-now is asked. */
|
|
1938
|
-
function scheduleCmd() {
|
|
2088
|
+
async function scheduleCmd() {
|
|
1939
2089
|
const sub = args[1];
|
|
1940
2090
|
const id = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
|
|
1941
2091
|
// One schedule-owning scope for a directory: its deployment (the directory
|
|
@@ -1944,7 +2094,11 @@ function scheduleCmd() {
|
|
|
1944
2094
|
let scope;
|
|
1945
2095
|
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
1946
2096
|
const io = { hostStatus: () => hostUnitStatus() };
|
|
1947
|
-
|
|
2097
|
+
// The workspace automations of this deployment (the snapshot) placed on this host.
|
|
2098
|
+
let actx;
|
|
2099
|
+
const ctx = () => (actx ??= scopeAutomations(ws(), { io }));
|
|
2100
|
+
const isWorkspace = (qid) => A.splitId(qid).scope !== "local";
|
|
2101
|
+
const out = (result, text) => { if (JSON_MODE) jsonOk(result); else console.log(text ? text(result) : JSON.stringify(result, null, 2)); };
|
|
1948
2102
|
const readSpec = () => {
|
|
1949
2103
|
const inline = flag("spec-json");
|
|
1950
2104
|
if (inline && inline !== true) { try { return JSON.parse(inline); } catch (e) { throw scheduleError("E_SCHEDULE_INVALID", `--spec-json is not valid JSON: ${e.message}`, { field: "file" }); } }
|
|
@@ -1956,20 +2110,41 @@ function scheduleCmd() {
|
|
|
1956
2110
|
const needId = () => { if (!id) throw scheduleError("E_BAD_ARGS", `oats schedule ${sub} <id>`); return id; };
|
|
1957
2111
|
try {
|
|
1958
2112
|
switch (sub) {
|
|
1959
|
-
case "list": { const r = listSchedules(ws(), io); out(r); if (!JSON_MODE && r.triggers.count) console.log(`${r.triggers.count} trigger${r.triggers.count === 1 ? " is" : "s are"} not listed here: ${r.triggers.command}`); return; }
|
|
1960
|
-
case "show": return out({ schedule: describeSchedule(ws(), needId(), io) });
|
|
1961
|
-
case "
|
|
2113
|
+
case "list": { const r = listSchedules(ws(), io, { ctx: ctx() }); out(r); if (!JSON_MODE && r.triggers.count) console.log(`${r.triggers.count} trigger${r.triggers.count === 1 ? " is" : "s are"} not listed here: ${r.triggers.command}`); return; }
|
|
2114
|
+
case "show": return out({ schedule: describeSchedule(ws(), needId(), io, { ctx: ctx() }) });
|
|
2115
|
+
case "test": return out({ test: testSchedule(ws(), needId(), io, { ctx: ctx() }) }, (r) => `${r.test.qualifiedId}: ${r.test.placement.runsHere ? "runs here" : `not here (${r.test.placement.reason ?? "disabled here"})`}${r.test.soul ? `; soul ${r.test.soul.name} ${r.test.soul.resolves ? "resolves" : `does NOT resolve (${r.test.soul.error?.code})`}` : ""}; next due ${r.test.nextDue ?? "never"}${r.test.problems.length ? `\n ${r.test.problems.join("\n ")}` : ""}\n(spawned nothing)`);
|
|
2116
|
+
case "add": {
|
|
2117
|
+
const spec = readSpec();
|
|
2118
|
+
if (flag("workspace") !== undefined) {
|
|
2119
|
+
const wid = id ?? spec.id;
|
|
2120
|
+
if (typeof wid !== "string") throw scheduleError("E_BAD_ARGS", "oats schedule add <id> --workspace <member> …: the id names the file (oats-schedules/<id>.yaml)");
|
|
2121
|
+
const { id: _i, enabled: _e, kind, ...rest } = spec; void _i; void _e;
|
|
2122
|
+
const r = await addWorkspaceAutomation(scheduleKind({ dep: ws() }), { id: wid, body: { run: kind ?? "spawn", ...rest } });
|
|
2123
|
+
return out(r, printWorkspaceAdd);
|
|
2124
|
+
}
|
|
2125
|
+
if (id && spec.id === undefined) spec.id = id;
|
|
2126
|
+
if (id && spec.id !== id) throw scheduleError("E_SCHEDULE_INVALID", `id ${JSON.stringify(spec.id)} in the file does not match ${JSON.stringify(id)}`, { field: "id" });
|
|
2127
|
+
return out({ schedule: addSchedule(ws(), spec, io) });
|
|
2128
|
+
}
|
|
1962
2129
|
case "update": return out({ schedule: updateSchedule(ws(), needId(), readSpec(), io) });
|
|
1963
|
-
case "enable":
|
|
1964
|
-
case "disable":
|
|
1965
|
-
|
|
2130
|
+
case "enable":
|
|
2131
|
+
case "disable": {
|
|
2132
|
+
const on = sub === "enable";
|
|
2133
|
+
if (!isWorkspace(needId())) return out({ schedule: setScheduleEnabled(ws(), id, on, io) });
|
|
2134
|
+
describeSchedule(ws(), id, io, { ctx: ctx() }); // an unknown id is refused before the file changes
|
|
2135
|
+
setDisabledHereCli(ws(), "schedule", A.qualifiedId(id), on);
|
|
2136
|
+
actx = undefined;
|
|
2137
|
+
return out({ schedule: describeSchedule(ws(), id, io, { ctx: ctx() }) });
|
|
2138
|
+
}
|
|
2139
|
+
case "run": return out(runScheduleNow(ws(), needId(), { io, force: args.includes("--force"), ctx: ctx() }));
|
|
1966
2140
|
case "remove": return out(removeSchedule(ws(), needId(), { force: args.includes("--force") }));
|
|
1967
|
-
case "reconcile": return out(reconcileSchedule(ws(), needId(), { io, clear: args.includes("--clear") }));
|
|
2141
|
+
case "reconcile": return out(reconcileSchedule(ws(), needId(), { io, clear: args.includes("--clear"), ctx: ctx() }));
|
|
1968
2142
|
case "tick": {
|
|
1969
2143
|
const dryRun = args.includes("--dry-run");
|
|
1970
2144
|
if (args.includes("--host")) return out(tickHost({ io, dryRun }));
|
|
1971
2145
|
const reg = readRegistry();
|
|
1972
|
-
const
|
|
2146
|
+
const tctx = scopeAutomations(ws(), { io, refresh: !dryRun });
|
|
2147
|
+
const considered = withHostLock(() => [...(tctx.refresh && !tctx.refresh.ok ? [{ workspace: ws(), action: "error", error: `automations refresh: ${tctx.refresh.error}` }] : []), ...tickWorkspace(ws(), { io, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()], dryRun, ctx: tctx }), ...tickTriggers(ws(), { io, dryRun, ctx: tctx })]);
|
|
1973
2148
|
return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws(), io) });
|
|
1974
2149
|
}
|
|
1975
2150
|
case "host": {
|
|
@@ -1979,7 +2154,7 @@ function scheduleCmd() {
|
|
|
1979
2154
|
if (op === "status") return out({ scheduler: schedulerStatus(ws(), io) });
|
|
1980
2155
|
throw scheduleError("E_BAD_ARGS", "oats schedule host install|uninstall|status");
|
|
1981
2156
|
}
|
|
1982
|
-
default: throw scheduleError("E_BAD_ARGS", "usage: oats schedule list|show <id>|add <id> --file <spec.json
|
|
2157
|
+
default: throw scheduleError("E_BAD_ARGS", "usage: oats schedule list|show <id>|test <id>|add <id> --file <spec.json> [--workspace <member> --runs-on <host> --owner <host>/<login>]|update <id> --file <spec.json>|enable <id>|disable <id>|run <id> [--force]|remove <id> [--force]|reconcile <id> [--clear]|tick [--dry-run] [--host]|host install|uninstall|status [--dir <workspace>|--server <id>] [--json]");
|
|
1983
2158
|
}
|
|
1984
2159
|
} catch (e) {
|
|
1985
2160
|
// K8b: typed refusal details travel (identity mismatch: key/declared; a refused file: its integrity source).
|
|
@@ -1998,16 +2173,28 @@ async function triggerCmd() {
|
|
|
1998
2173
|
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
1999
2174
|
const out = (result, text) => { if (JSON_MODE) jsonOk(result); else console.log(text ? text(result) : JSON.stringify(result, null, 2)); };
|
|
2000
2175
|
const needId = () => { if (!id) throw T.triggerError("E_BAD_ARGS", `oats trigger ${sub} <id>`); return id; };
|
|
2001
|
-
const usage = "usage: oats trigger add (--file <trigger.json> | --from <package>:<template> [--set <name>=<value>]… [--id <id>]) | list | show <id> | enable <id> | disable <id> | remove <id> | test <id> | status [<id>] [--dir <deployment>] [--json]";
|
|
2002
|
-
const
|
|
2176
|
+
const usage = "usage: oats trigger add (--file <trigger.json> | --from <package>:<template> [--set <name>=<value>]… [--id <id>]) [--workspace <member> --runs-on <host> --owner <host>/<login>] | list | show <id> | enable <id> | disable <id> | remove <id> | test <id> | status [<id>] [--dir <deployment>] [--json] (<id>: local/<id> or <member>/<id>)";
|
|
2177
|
+
const where = (t) => (t.origin?.kind === "workspace" ? (t.runsHere ? `runs here as ${t.owner}` : `${t.reason === "assigned-elsewhere" ? `runs on ${t.runsOn}` : t.reason ?? "disabled here"}`) : t.enabledHere ? "local" : "local, disabled");
|
|
2178
|
+
const line = (t) => `${t.id} ${where(t)} ${t.on?.source ?? "?"} ${t.on?.repo ?? "?"} [${(t.on?.events || []).join(",")}]${t.on?.labels?.length ? ` labels ${t.on.labels.join(",")}` : ""} every ${t.on?.poll ?? "?"} → spawn ${t.spawn?.soul ?? "?"}${t.spawn?.teams?.length ? ` in ${t.spawn.teams.join(",")}` : ""}${t.invalid ? ` INVALID: ${t.invalid.message}` : ""}`;
|
|
2179
|
+
// The workspace automations of this deployment (the snapshot) placed on this host.
|
|
2180
|
+
let actx;
|
|
2181
|
+
const ctx = () => (actx ??= scopeAutomations(ws(), {}));
|
|
2182
|
+
const isWorkspace = (qid) => A.splitId(qid).scope !== "local";
|
|
2003
2183
|
try {
|
|
2004
2184
|
switch (sub) {
|
|
2005
|
-
case "list": return out(T.listTriggers(ws()), (r) => r.triggers.length ? r.triggers.map(line).join("\n") : "(no triggers — oats trigger add --file <trigger.json> | --from <package>:<template>)");
|
|
2006
|
-
case "show": return out({ trigger: T.describeTrigger(ws(), needId()) });
|
|
2007
|
-
case "enable":
|
|
2008
|
-
case "disable":
|
|
2009
|
-
|
|
2010
|
-
|
|
2185
|
+
case "list": return out(T.listTriggers(ws(), ctx(), { io: { hostStatus: () => hostUnitStatus() } }), (r) => r.triggers.length ? r.triggers.map(line).join("\n") : "(no triggers — oats trigger add --file <trigger.json> | --from <package>:<template>)");
|
|
2186
|
+
case "show": return out({ trigger: T.describeTrigger(ws(), needId(), ctx()) });
|
|
2187
|
+
case "enable":
|
|
2188
|
+
case "disable": {
|
|
2189
|
+
const on = sub === "enable";
|
|
2190
|
+
if (!isWorkspace(needId())) return out({ trigger: T.setTriggerEnabled(ws(), id, on, ctx()) }, (r) => line(r.trigger));
|
|
2191
|
+
T.describeTrigger(ws(), id, ctx()); // an unknown id is refused before the file changes
|
|
2192
|
+
setDisabledHereCli(ws(), "trigger", A.qualifiedId(id), on);
|
|
2193
|
+
actx = undefined;
|
|
2194
|
+
return out({ trigger: T.describeTrigger(ws(), id, ctx()) }, (r) => line(r.trigger));
|
|
2195
|
+
}
|
|
2196
|
+
case "remove": return out(T.removeTrigger(ws(), needId(), ctx()), (r) => `removed trigger ${r.removed}${r.live.length ? ` (its live instances keep running: ${r.live.join(", ")})` : ""}`);
|
|
2197
|
+
case "status": return out(T.triggerStatus(ws(), id, ctx()), (r) => r.triggers.map((t) => `${t.id} last poll ${t.lastPoll ? `${t.lastPoll.at} ${t.lastPoll.ok ? `ok (${t.lastPoll.matching}/${t.lastPoll.prs} PRs match)` : `FAILED: ${t.lastPoll.error}`}` : "never"} pending ${t.pending.length} fired ${t.firedTotal} live ${t.live.map((l) => l.instance).join(",") || "none"}${t.lastError ? `\n last error ${t.lastError.at}: ${t.lastError.message}` : ""}`).join("\n") || "(no triggers)");
|
|
2011
2198
|
case "test": {
|
|
2012
2199
|
let workspaceTeams = null;
|
|
2013
2200
|
try {
|
|
@@ -2015,8 +2202,9 @@ async function triggerCmd() {
|
|
|
2015
2202
|
const { local } = loadLocal(ws());
|
|
2016
2203
|
if (typeof local.workspace === "string") workspaceTeams = Object.keys((await observeWorkspace(local.workspace, { remoteOptions: remoteOptionsFromEnv() })).workspace.teams || {});
|
|
2017
2204
|
} catch { /* reported as unknown (null) */ }
|
|
2018
|
-
return out(T.testTrigger(ws(), needId(), { workspaceTeams }), (r) => [
|
|
2205
|
+
return out(T.testTrigger(ws(), needId(), { workspaceTeams, ctx: ctx() }), (r) => [
|
|
2019
2206
|
`trigger ${r.id}: ${r.ok ? "ready" : "NOT ready"} (nothing was spawned)`,
|
|
2207
|
+
` placement ${r.placement.runsOn ? `runs on ${r.placement.runsOn} as ${r.placement.owner}; this host is ${r.placement.host ?? "(unnamed)"} — ${r.placement.runsHere ? "runs here" : r.placement.reason ?? "disabled here"}` : "local (this host)"}`,
|
|
2020
2208
|
` gh auth ${r.gh.ok ? `ok — ${r.gh.account ?? "?"} via ${r.gh.credentialSource}` : `FAILED${r.gh.detail ? ` — ${r.gh.detail}` : ""}`}`,
|
|
2021
2209
|
` repo ${r.repo.key} ${r.repo.readable ? `readable; push ${r.repo.permissions.push} maintain ${r.repo.permissions.maintain} admin ${r.repo.permissions.admin}` : `NOT readable: ${r.repo.error}`}`,
|
|
2022
2210
|
` soul ${r.soul.name} ${r.soul.resolves ? `resolves${r.soul.messaging ? ` (messaging ${r.soul.messaging})` : ""}` : `does NOT resolve: ${r.soul.error.code} ${r.soul.error.message}`}`,
|
|
@@ -2043,7 +2231,17 @@ async function triggerCmd() {
|
|
|
2043
2231
|
if (Object.keys(sets).length) throw T.triggerError("E_BAD_ARGS", "--set fills a package template's parameters; with --file, edit the file");
|
|
2044
2232
|
try { spec = JSON.parse(readFileSync(file, "utf8")); } catch (e) { throw T.triggerError("E_TRIGGER_INVALID", `${file} is not valid JSON: ${e.message}`, { details: { field: "file" } }); }
|
|
2045
2233
|
if (idFlag !== undefined) spec.id = idFlag;
|
|
2046
|
-
}
|
|
2234
|
+
}
|
|
2235
|
+
if (flag("workspace") !== undefined) {
|
|
2236
|
+
// A workspace trigger (0.29.0): the file in the member, from/set when made from a template.
|
|
2237
|
+
const kinds = await automationKinds(ws(), readLock(ws()), remoteOptionsFromEnv());
|
|
2238
|
+
const wid = typeof idFlag === "string" ? idFlag : from !== undefined ? String(from).split(":")[1] : spec?.id;
|
|
2239
|
+
if (typeof wid !== "string") throw T.triggerError("E_BAD_ARGS", "--workspace: name the trigger with --id (or an id in the file)");
|
|
2240
|
+
const body = from !== undefined ? { from: String(from), ...(Object.keys(sets).length ? { set: sets } : {}) } : (({ id: _i, kind: _k, enabled: _e, template: _t, createdAt: _c, updatedAt: _u, ...rest }) => rest)(spec);
|
|
2241
|
+
const r = await addWorkspaceAutomation(kinds.trigger, { id: wid, body });
|
|
2242
|
+
return out(r, printWorkspaceAdd);
|
|
2243
|
+
}
|
|
2244
|
+
if (spec === undefined) spec = await triggerFromPackage(T, String(from), sets, idFlag);
|
|
2047
2245
|
return out({ trigger: T.addTrigger(ws(), spec) }, (r) => `added ${line(r.trigger)}\n(\`oats trigger test ${r.trigger.id}\` checks gh, the repository, the soul and the teams on this host)`);
|
|
2048
2246
|
}
|
|
2049
2247
|
default: throw T.triggerError("E_BAD_ARGS", usage);
|
|
@@ -2056,28 +2254,8 @@ async function triggerCmd() {
|
|
|
2056
2254
|
/** A package trigger template (`triggers: [{ id, file }]` in the locked package's oats-package.json),
|
|
2057
2255
|
* read at the lock's commit and instantiated with the --set parameters. */
|
|
2058
2256
|
async function triggerFromPackage(T, from, sets, idFlag) {
|
|
2059
|
-
const
|
|
2060
|
-
|
|
2061
|
-
const [, pkg, template] = m;
|
|
2062
|
-
const deployment = scheduleScopeOf(dirFlag());
|
|
2063
|
-
const lock = readLock(deployment);
|
|
2064
|
-
const entry = lock.packages[pkg];
|
|
2065
|
-
if (!entry) throw T.triggerError("E_PACKAGE_MISSING", `package ${pkg} is not in ${LOCK_FILE} — declare it in packages: and run \`oats sync\``, { details: { package: pkg } });
|
|
2066
|
-
const { lockedPackageRef, bindRemote } = await import("../lib/packages.mjs");
|
|
2067
|
-
const ref = lockedPackageRef(entry);
|
|
2068
|
-
const remote = bindRemote(remoteModule, remoteOptionsFromEnv());
|
|
2069
|
-
const read = async (rel, what) => {
|
|
2070
|
-
let bytes;
|
|
2071
|
-
try { ({ bytes } = await remote.readRemoteFile(ref, entry.commit, `${entry.path}/${rel}`)); }
|
|
2072
|
-
catch (e) { throw T.triggerError(e?.code === "E_REMOTE_PATH_MISSING" ? "E_PACKAGE_MANIFEST" : (e?.code || "E_REMOTE_UNREADABLE"), `${pkg} v${entry.version}: ${what} ${entry.path}/${rel} cannot be read: ${e.message}`, { details: { package: pkg, path: rel } }); }
|
|
2073
|
-
try { return JSON.parse(Buffer.from(bytes).toString("utf8")); } catch (e) { throw T.triggerError("E_PACKAGE_MANIFEST", `${pkg} v${entry.version}: ${what} ${entry.path}/${rel} is not valid JSON: ${e.message}`, { details: { package: pkg, path: rel } }); }
|
|
2074
|
-
};
|
|
2075
|
-
const manifest = await read("oats-package.json", "the package manifest");
|
|
2076
|
-
const listed = Array.isArray(manifest.triggers) ? manifest.triggers : [];
|
|
2077
|
-
const row = listed.find((t) => t && t.id === template);
|
|
2078
|
-
if (!row || typeof row.file !== "string" || !row.file || row.file.split("/").includes("..") || row.file.startsWith("/")) throw T.triggerError("E_TRIGGER_UNKNOWN", `package ${pkg} v${entry.version} has no trigger template ${JSON.stringify(template)} (templates: ${listed.map((t) => t?.id).filter(Boolean).join(", ") || "none"})`, { details: { package: pkg, template, templates: listed.map((t) => t?.id).filter(Boolean) } });
|
|
2079
|
-
const doc = await read(row.file, `trigger template ${template}`);
|
|
2080
|
-
return T.instantiateTemplate(doc, sets, { id: idFlag, provenance: { package: pkg, version: entry.version, commit: entry.commit, template } });
|
|
2257
|
+
const { bindRemote } = await import("../lib/packages.mjs");
|
|
2258
|
+
return T.packageTriggerTemplate(readLock(scheduleScopeOf(dirFlag())), from, sets, { id: idFlag, remote: bindRemote(remoteModule, remoteOptionsFromEnv()) });
|
|
2081
2259
|
}
|
|
2082
2260
|
|
|
2083
2261
|
async function sessionCmd() {
|
|
@@ -2326,7 +2504,7 @@ async function capabilityCommand() {
|
|
|
2326
2504
|
// No home, so no recorded soul: the soul's per-commit copy is OATS_SOUL when a spawn
|
|
2327
2505
|
// already fetched exactly this commit; otherwise the command gets none (never ambient).
|
|
2328
2506
|
const cachedSoul = hit.soul?.commit ? join(hit.deployment, "agents", hit.soul.name, "souls", String(hit.soul.commit).slice(0, 12)) : null;
|
|
2329
|
-
return runManifestCommand({ capability: hit.module.name, ...hit.manifest }, hit.settings, teamCtx, hit.ensureTree, cachedSoul && existsSync(join(cachedSoul, "soul.yaml")) ? realpathSync(cachedSoul) : undefined);
|
|
2507
|
+
return runManifestCommand({ capability: hit.module.name, ...hit.manifest }, { settings: hit.settings, origins: hit.resolution?.payloadOrigins?.[hit.module.name] }, teamCtx, hit.ensureTree, cachedSoul && existsSync(join(cachedSoul, "soul.yaml")) ? realpathSync(cachedSoul) : undefined);
|
|
2330
2508
|
}
|
|
2331
2509
|
|
|
2332
2510
|
async function dispatch() {
|
|
@@ -2340,14 +2518,14 @@ async function capabilityCommand() {
|
|
|
2340
2518
|
// braces: the ids come from instance.json, which spawn wrote from resolved
|
|
2341
2519
|
// manifests. Null-prototype because the dispatcher indexes it with the
|
|
2342
2520
|
// namespace the operator typed on the command line.
|
|
2343
|
-
let capSettings = Object.create(null);
|
|
2521
|
+
let capSettings = Object.create(null), capOrigins = Object.create(null);
|
|
2344
2522
|
let deployment = null;
|
|
2345
2523
|
let soulDir;
|
|
2346
2524
|
try {
|
|
2347
2525
|
if (metaFile && existsSync(metaFile)) {
|
|
2348
2526
|
const meta = JSON.parse(readFileSync(metaFile, "utf8"));
|
|
2349
2527
|
activeIds = (meta.capabilities || []).map((c) => c.id);
|
|
2350
|
-
for (const c of meta.capabilities || []) capSettings[c.id] = c.settings || {};
|
|
2528
|
+
for (const c of meta.capabilities || []) { capSettings[c.id] = c.settings || {}; capOrigins[c.id] = c.settingsOrigins || {}; }
|
|
2351
2529
|
// Workspace-model homes only (lead decision c3 Q2).
|
|
2352
2530
|
if (isCapturedHome(meta)) { const e = capturedHomeRefusal(instanceHome, "nothing was dispatched"); bail(e.code, e.message, e.details); }
|
|
2353
2531
|
if (!isWorkspaceHome(meta)) { const e = preWorkspaceHome(instanceHome, "nothing was dispatched"); bail(e.code, e.message); }
|
|
@@ -2386,14 +2564,14 @@ async function capabilityCommand() {
|
|
|
2386
2564
|
const live = await liveTeams(instanceHome, homeMeta.meta, { remoteOptions: remoteOptionsFromEnv() });
|
|
2387
2565
|
teamCtx = homeTeamCtx(live.teams, live.source);
|
|
2388
2566
|
}
|
|
2389
|
-
return runManifestCommand(m, capSettings[m.capability] || {}, teamCtx, () => m._dir, soulDir);
|
|
2567
|
+
return runManifestCommand(m, { settings: capSettings[m.capability] || {}, origins: capOrigins[m.capability] }, teamCtx, () => m._dir, soulDir);
|
|
2390
2568
|
}
|
|
2391
2569
|
|
|
2392
2570
|
/** Help / unknown-command / spec validation / exec — shared by every context.
|
|
2393
2571
|
* `m` is the manifest (with `capability`; `_dir` may be absent until `ensureDir`
|
|
2394
2572
|
* resolves the directory holding the executable — the operator branch fetches
|
|
2395
2573
|
* the module tree only when a command is actually going to run). */
|
|
2396
|
-
async function runManifestCommand(m, settings, teamCtx, ensureDir, soulDir) {
|
|
2574
|
+
async function runManifestCommand(m, { settings, origins }, teamCtx, ensureDir, soulDir) {
|
|
2397
2575
|
const sub = args[1];
|
|
2398
2576
|
const cmds = Object.keys(m.commands);
|
|
2399
2577
|
// `oats <ns> --help` and `oats <ns> <cmd> --help` answer from the manifest
|
|
@@ -2438,6 +2616,8 @@ async function capabilityCommand() {
|
|
|
2438
2616
|
// lifecycle hooks — capabilities read their settings here instead of
|
|
2439
2617
|
// importing the kernel resolver.
|
|
2440
2618
|
OATS_SETTINGS: JSON.stringify(settings || {}),
|
|
2619
|
+
// Where each leaf came from (JSON pointer → { kind, at }), as lifecycle hooks get it.
|
|
2620
|
+
OATS_SETTINGS_ORIGINS: JSON.stringify(origins || {}),
|
|
2441
2621
|
// PATH is not a trusted runtime boundary (maintainer finding 1): pass the
|
|
2442
2622
|
// canonical absolute executable of THIS CLI; official consumers execFile
|
|
2443
2623
|
// it directly and never resolve `oats` from PATH or a shell.
|
|
@@ -2507,7 +2687,7 @@ function versionCmd() {
|
|
|
2507
2687
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
2508
2688
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
2509
2689
|
// never listed.
|
|
2510
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "teams", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
|
|
2690
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "teams", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers", "automations", "desktop-facts"], automationsApi: A.AUTOMATIONS_API, workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
|
|
2511
2691
|
return;
|
|
2512
2692
|
}
|
|
2513
2693
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -2921,8 +3101,9 @@ else if (cmd === "version" || cmd === "--version" || cmd === "-v") versionCmd();
|
|
|
2921
3101
|
// Same rule as the inner catch: a typed CLI failure surfaces with its own code
|
|
2922
3102
|
// through the shared boundary, never re-badged as a spawn-mechanism failure.
|
|
2923
3103
|
else if (cmd === "session") await sessionCmd();
|
|
2924
|
-
else if (cmd === "schedule") scheduleCmd();
|
|
3104
|
+
else if (cmd === "schedule") await scheduleCmd();
|
|
2925
3105
|
else if (cmd === "trigger") await triggerCmd();
|
|
3106
|
+
else if (cmd === "automations") await automationsCmd();
|
|
2926
3107
|
else if (cmd === "spawn") { try { await spawnCmd(); } catch (e) { if (TYPED_CLI_FAILURES.has(e?.code)) throw e; if (JSON_MODE) jsonFail("E_SPAWN_FAILED", e.message || e); throw e; } }
|
|
2927
3108
|
else if (cmd === "retire") retireCmd();
|
|
2928
3109
|
else if (cmd === "capture" || cmd === "recall" || cmd === "setup") await recordCmd(cmd);
|
|
@@ -3011,7 +3192,7 @@ Usage:
|
|
|
3011
3192
|
next steps (clone members you work IN, spawn
|
|
3012
3193
|
oats-operator-expert); creates no soul, spawns nothing
|
|
3013
3194
|
oats session inspect|input|attach --home <absolute-home> [--text-file <path>] [--json]
|
|
3014
|
-
oats schedule list|show <id>|add <id> --file <spec.json>|update <id> --file <spec.json>
|
|
3195
|
+
oats schedule list|show <id>|test <id>|add <id> --file <spec.json>|update <id> --file <spec.json>
|
|
3015
3196
|
enable|disable|run|remove|reconcile <id> workspace-scoped, host-owned schedules (spawn,
|
|
3016
3197
|
tick [--dry-run] [--host] command or wake jobs on a five-field cron with an
|
|
3017
3198
|
host install|uninstall|status explicit IANA tz; see docs/schedules.md); --server
|
|
@@ -3020,6 +3201,13 @@ Usage:
|
|
|
3020
3201
|
| disable | remove <id> | test <id> | status [<id>] event-driven spawns (github.pull_request
|
|
3021
3202
|
polled with the host's gh by the schedule tick;
|
|
3022
3203
|
see docs/schedules.md#triggers)
|
|
3204
|
+
oats trigger|schedule add … --workspace <member> --runs-on <host> --owner <host>/<login>
|
|
3205
|
+
a trigger/schedule declared in Git (<member>/<id>):
|
|
3206
|
+
writes oats-triggers|oats-schedules/<id>.yaml in a
|
|
3207
|
+
checkout of the member, else prints it
|
|
3208
|
+
oats automations refresh [--json] re-discover the workspace triggers and schedules
|
|
3209
|
+
(the snapshot the host tick reads; oats sync does
|
|
3210
|
+
it too)
|
|
3023
3211
|
oats spawn ... --wake-file <json> | --wake-every <N> --wake-message <text> save a wake schedule
|
|
3024
3212
|
bound to the new instance's home (docs/schedules.md)
|
|
3025
3213
|
oats session upload --file <path> store a copy of a local file as a private attachment
|
|
@@ -3053,6 +3241,9 @@ Usage:
|
|
|
3053
3241
|
[--name <slug>] the exact instance name (no <agent>- prefix;
|
|
3054
3242
|
not with --purpose); must be a slug, not a
|
|
3055
3243
|
soul name, and unused in the deployment
|
|
3244
|
+
[--provider <capability> <key>=<value>] a provider setting for this spawn only
|
|
3245
|
+
(repeatable; dotted keys nest; recorded in
|
|
3246
|
+
instance.json providers.<capability>)
|
|
3056
3247
|
oats retire <instance> [--force] retire an instance (window, hooks,
|
|
3057
3248
|
[--self] [--delete-branch] worktree, home); --self = retire the
|
|
3058
3249
|
[--keep-dir] [--json] CALLING instance: the window dies, then
|