@awebai/oats 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/oats.mjs +296 -106
- 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 +257 -11
- 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/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-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. */
|
|
@@ -1072,6 +1073,10 @@ const memberLabel = (key) => String(key).split("/").filter(Boolean).pop()?.repla
|
|
|
1072
1073
|
const teamLabel = (team) => team ?? "unassigned";
|
|
1073
1074
|
const originOf = (item) => (item.package ? `package ${item.package} v${item.version}` : `member ${item.repoKey} @ ${short(item.commit)}`);
|
|
1074
1075
|
|
|
1076
|
+
/** A soul's spawn default harness and model (feature desktop-facts): its definition's, else the kernel's (pi, the
|
|
1077
|
+
* harness's native model) — what `oats spawn <soul>` takes with no --harness/--model/--launch-config. */
|
|
1078
|
+
const soulDefaults = (def) => ({ harness: def?.harness ?? "pi", model: def?.model ?? null, harnessFrom: def?.harness ? "soul" : "kernel-default" });
|
|
1079
|
+
|
|
1075
1080
|
/** Rows of every soul and capability of confirmed members (+ external souls) + locked package
|
|
1076
1081
|
* capabilities. Souls have no private mode (0.26.0); a private member capability is listed with
|
|
1077
1082
|
* `private: true` — repo-owned: usable only by its own repo's souls (E_CAPABILITY_PRIVATE). */
|
|
@@ -1080,15 +1085,15 @@ function workspaceItems(discovery, lock) {
|
|
|
1080
1085
|
const capabilities = [];
|
|
1081
1086
|
for (const m of discovery.members) {
|
|
1082
1087
|
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 });
|
|
1088
|
+
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
1089
|
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
1090
|
}
|
|
1086
1091
|
for (const ext of discovery.external || []) {
|
|
1087
1092
|
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 });
|
|
1093
|
+
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
1094
|
}
|
|
1090
1095
|
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 });
|
|
1096
|
+
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
1097
|
}
|
|
1093
1098
|
for (const [id, entry] of Object.entries(lock?.packages || {})) {
|
|
1094
1099
|
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 +1169,17 @@ async function performSync(ctx, bail, { onDiscovered } = {}) {
|
|
|
1164
1169
|
// The deployment's instance root: <deployment>/agents/ (findRoot's marker). A hand-written
|
|
1165
1170
|
// oats-local.yaml + sync is a complete deployment; spawn must not answer E_NO_DEPLOYMENT after it.
|
|
1166
1171
|
try { mkdirSync(join(ctx.deploymentDir, "agents"), { recursive: true }); } catch { /* reported by spawn's E_NO_DEPLOYMENT remedy if it matters */ }
|
|
1172
|
+
// Workspace automations (0.29.0): discovered with the members and the new lock (a trigger
|
|
1173
|
+
// template instantiates at the locked package commit), kept as the snapshot the host tick reads.
|
|
1174
|
+
let automations;
|
|
1175
|
+
try { automations = await takeAutomationSnapshot(ctx, discovery, lock); }
|
|
1176
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
1177
|
+
problems.push(...automations.problems);
|
|
1167
1178
|
const members = memberRows(discovery);
|
|
1168
1179
|
const packages = packageRows(lock);
|
|
1169
1180
|
const changes = resolved.changes;
|
|
1170
1181
|
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 ?? [] };
|
|
1182
|
+
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
1183
|
return { report, lock, discovery, items, lockFile, problems };
|
|
1173
1184
|
}
|
|
1174
1185
|
|
|
@@ -1207,11 +1218,91 @@ function printSyncReport(ctx, synced) {
|
|
|
1207
1218
|
for (const s of items.souls) { const t = teams.get(s.team) || { souls: 0, capabilities: 0 }; t.souls++; teams.set(s.team, t); }
|
|
1208
1219
|
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
1220
|
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)"}`);
|
|
1221
|
+
const ac = report.automations;
|
|
1222
|
+
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
1223
|
for (const p of synced.problems ?? discovery.problems) console.log(`problem ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`);
|
|
1211
1224
|
for (const w of discovery.warnings ?? []) console.log(`warning ${w.code} ${w.message}`);
|
|
1212
1225
|
console.log(`\nlock ${shortPath(lockFile)}`);
|
|
1213
1226
|
}
|
|
1214
1227
|
|
|
1228
|
+
/** Discover the workspace automations of a discovery and write the snapshot the host tick reads
|
|
1229
|
+
* (lib/automations.mjs). A trigger's `from:` instantiates the package template at the lock's
|
|
1230
|
+
* commit; a schedule is checked against this deployment. → the snapshot */
|
|
1231
|
+
async function takeAutomationSnapshot(ctx, discovery, lock) {
|
|
1232
|
+
const kinds = await automationKinds(ctx.deploymentDir, lock, ctx.remoteOptions);
|
|
1233
|
+
const found = await A.discoverAutomations(discovery, { remote: kinds.remote, memberName: memberLabel, kinds: [kinds.trigger, kinds.schedule], previous: A.readSnapshot(ctx.deploymentDir) });
|
|
1234
|
+
const snap = A.snapshotOf(discovery, found, { souls: A.soulIndexOf(discovery, memberLabel) });
|
|
1235
|
+
// A workspace with no triggers or schedules leaves nothing behind (onboard creates what the
|
|
1236
|
+
// kernel needs and nothing else); a snapshot that existed is replaced, so a removal lands.
|
|
1237
|
+
const empty = !snap.triggers.length && !snap.schedules.length && !snap.problems.length;
|
|
1238
|
+
if (!empty || A.readSnapshot(ctx.deploymentDir)) A.writeSnapshot(ctx.deploymentDir, snap);
|
|
1239
|
+
return snap;
|
|
1240
|
+
}
|
|
1241
|
+
/** The two kinds' descriptors (lib/triggers.mjs triggerKind, lib/schedule.mjs scheduleKind). */
|
|
1242
|
+
async function automationKinds(dep, lock, remoteOptions) {
|
|
1243
|
+
const T = await import("../lib/triggers.mjs");
|
|
1244
|
+
const { bindRemote } = await import("../lib/packages.mjs");
|
|
1245
|
+
const remote = bindRemote(remoteModule, remoteOptions);
|
|
1246
|
+
return { remote, trigger: T.triggerKind({ lock, remote }), schedule: scheduleKind({ dep }) };
|
|
1247
|
+
}
|
|
1248
|
+
const automationCounts = (snap) => ({ triggers: snap.triggers.length, schedules: snap.schedules.length, problems: snap.problems.length, takenAt: snap.takenAt });
|
|
1249
|
+
|
|
1250
|
+
/** `oats automations refresh [--dir] [--json]`: re-discover the workspace automations and rewrite
|
|
1251
|
+
* the snapshot (what `oats sync` does for them, without touching the lock). The host tick runs it
|
|
1252
|
+
* when the snapshot is ten minutes old. */
|
|
1253
|
+
async function automationsCmd() {
|
|
1254
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1255
|
+
if (args[1] !== "refresh") return bail("E_USAGE", "usage: oats automations refresh [--dir <deployment>] [--json]");
|
|
1256
|
+
const ctx = workspaceContext(bail);
|
|
1257
|
+
const discovery = await discoverForCli(ctx, bail);
|
|
1258
|
+
let lock;
|
|
1259
|
+
try { lock = readLock(ctx.deploymentDir); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
|
|
1260
|
+
let snap;
|
|
1261
|
+
try { snap = await takeAutomationSnapshot(ctx, discovery, lock); }
|
|
1262
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
1263
|
+
const result = { automationsApi: A.AUTOMATIONS_API, snapshot: A.snapshotPath(ctx.deploymentDir), ...automationCounts(snap), problems: snap.problems };
|
|
1264
|
+
if (JSON_MODE) return jsonOk(result);
|
|
1265
|
+
console.log(`automations ${result.triggers} trigger(s), ${result.schedules} schedule(s) — snapshot ${shortPath(result.snapshot)}`);
|
|
1266
|
+
for (const p of snap.problems) console.log(`problem ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/** `oats trigger|schedule add --workspace <member> --runs-on <host> --owner <host>/<login>`: the
|
|
1270
|
+
* workspace file, written into the member's checkout when `--dir` (or the cwd) is inside one, else
|
|
1271
|
+
* printed. What is written must read back as the same automation (parseAutomationFile + expand). */
|
|
1272
|
+
async function addWorkspaceAutomation(desc, { id, body }) {
|
|
1273
|
+
const bail = (code, msg, details) => { throw Object.assign(new Error(msg), { code, details }); };
|
|
1274
|
+
const member = flag("workspace"), runsOn = flag("runs-on"), owner = flag("owner"), description = flag("description");
|
|
1275
|
+
if (typeof member !== "string" || !member) bail("E_BAD_ARGS", "--workspace needs a member name");
|
|
1276
|
+
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");
|
|
1277
|
+
if (!A.parseOwner(owner)) bail("E_BAD_ARGS", "--owner <host>/<login>: the GitHub account it acts as, e.g. github.com/acme-kb-bot");
|
|
1278
|
+
const ctx = workspaceContext(bail);
|
|
1279
|
+
const discovery = await discoverForCli(ctx, bail);
|
|
1280
|
+
const row = (discovery.members || []).find((m) => m.confirmed && (memberLabel(m.key) === member || m.key === member));
|
|
1281
|
+
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 });
|
|
1282
|
+
const name = memberLabel(row.key);
|
|
1283
|
+
const content = A.automationFileText(desc, { id, description: typeof description === "string" ? description : undefined, runsOn, owner, body });
|
|
1284
|
+
const parsed = A.parseAutomationFile(desc, { stem: id, path: `${desc.folder}/${id}.yaml`, bytes: Buffer.from(content), member: name, repoKey: row.key, commit: row.commit });
|
|
1285
|
+
if (parsed.problem) bail(parsed.problem.code, parsed.problem.message, { path: parsed.problem.path });
|
|
1286
|
+
await desc.expand(parsed.entry);
|
|
1287
|
+
const sameRepo = (url) => { try { return remoteModule.parseRepoRef(url).key === row.key; } catch { return false; } };
|
|
1288
|
+
const at = A.memberCheckoutFor(dirFlag(), desc, id, { sameRepo });
|
|
1289
|
+
const file = { member: name, repoKey: row.key, path: `${desc.folder}/${id}.yaml`, content };
|
|
1290
|
+
if (!at) return { file, written: false, id: `${name}/${id}` };
|
|
1291
|
+
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 });
|
|
1292
|
+
mkdirSync(dirname(at.file), { recursive: true });
|
|
1293
|
+
writeFileSync(at.file, content);
|
|
1294
|
+
return { file: { ...file, written: at.file }, written: true, id: `${name}/${id}` };
|
|
1295
|
+
}
|
|
1296
|
+
const printWorkspaceAdd = (r) => r.written
|
|
1297
|
+
? `wrote ${shortPath(r.file.written)} — commit and push it; after \`oats sync\` (or within ten minutes) ${r.id} runs on its host`
|
|
1298
|
+
: `# ${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}`;
|
|
1299
|
+
/** Enable/disable a WORKSPACE trigger or schedule on this host: oats-local.yaml
|
|
1300
|
+
* `triggers.disabled` / `schedules.disabled`. */
|
|
1301
|
+
function setDisabledHereCli(ws, kind, qid, enabled) {
|
|
1302
|
+
const { path } = loadLocal(ws);
|
|
1303
|
+
return A.setDisabledHere(path, kind, qid, !enabled, { validate: validateLocal });
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1215
1306
|
/** `oats sync [--dir] [--json]` — contract §6. */
|
|
1216
1307
|
async function syncCmd() {
|
|
1217
1308
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
@@ -1325,6 +1416,10 @@ async function workspaceCmd() {
|
|
|
1325
1416
|
const unsynced = declared.filter((id) => !locked.has(id));
|
|
1326
1417
|
const stale = packages.filter((p) => !declared.includes(p.id)).map((p) => p.id);
|
|
1327
1418
|
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 ?? [] };
|
|
1419
|
+
// Workspace automations (0.29.0), from the snapshot `oats sync` took, placed on this host.
|
|
1420
|
+
const actx = scopeAutomations(ctx.deploymentDir, {});
|
|
1421
|
+
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 } : {}) })) };
|
|
1422
|
+
await workspaceStatusFacts(result, discovery, lock, ctx);
|
|
1328
1423
|
if (JSON_MODE) { jsonOk(result); return; }
|
|
1329
1424
|
console.log(`workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)}) local ${shortPath(ctx.localPath)}\n`);
|
|
1330
1425
|
if (standalone) console.log(` (${standaloneNote(discovery)})\n`);
|
|
@@ -1337,10 +1432,47 @@ async function workspaceCmd() {
|
|
|
1337
1432
|
if (packages.length && unsynced.length) console.log(` declared but not locked (run \`oats sync\`): ${unsynced.join(", ")}`);
|
|
1338
1433
|
if (stale.length) console.log(` locked but no longer declared (run \`oats sync\`): ${stale.join(", ")}`);
|
|
1339
1434
|
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(" ")}`);
|
|
1435
|
+
if (result.automations.rows.length) {
|
|
1436
|
+
console.log(`\nAutomations (this host: ${result.automations.host ?? "unnamed — set host.name in oats-local.yaml"}):`);
|
|
1437
|
+
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"]));
|
|
1438
|
+
}
|
|
1340
1439
|
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
1440
|
if (discovery.warnings?.length) { console.log("\nWarnings:"); for (const w of discovery.warnings) console.log(` ${w.code} ${w.message}`); }
|
|
1342
1441
|
}
|
|
1343
1442
|
|
|
1443
|
+
/** `oats workspace status` facts (feature desktop-facts): the workspace and team defaults as rows; this
|
|
1444
|
+
* computer (member clones and the rule that found each, disabled souls, the lock); file locations with
|
|
1445
|
+
* browsable urls; each catalog package's newer version, when the official catalog has one. */
|
|
1446
|
+
async function workspaceStatusFacts(result, discovery, lock, ctx) {
|
|
1447
|
+
const { memberCloneOf } = await import("../lib/instance-resolution.mjs");
|
|
1448
|
+
const { browseUrl } = remoteModule;
|
|
1449
|
+
const defaults = discovery.standalone === true ? {} : (discovery.workspace?.defaults ?? {});
|
|
1450
|
+
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));
|
|
1451
|
+
const slot = (d) => (d && typeof d === "object" ? Object.entries(d).map(([name, v]) => ({ name, from: v?.from ?? null }))[0] ?? null : d ?? null);
|
|
1452
|
+
result.defaults = {
|
|
1453
|
+
slots: Object.fromEntries(LAYERS.map((l) => [l, slot(defaults[l])])),
|
|
1454
|
+
capabilities: rows(defaults.capabilities),
|
|
1455
|
+
byTeam: Object.fromEntries(Object.entries(defaults.byTeam && typeof defaults.byTeam === "object" ? defaults.byTeam : {}).map(([label, t]) => [label, { capabilities: rows(t?.capabilities) }])),
|
|
1456
|
+
};
|
|
1457
|
+
result.workspace.file = discovery.standalone === true ? null : { path: "oats-workspace.yaml", url: browseUrl(discovery.key, discovery.commit, "oats-workspace.yaml") };
|
|
1458
|
+
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") } });
|
|
1459
|
+
result.clones = result.members.map((m) => {
|
|
1460
|
+
try { return { key: m.key, name: m.name, ...memberCloneOf(ctx.deploymentDir, ctx.local, m.key) }; }
|
|
1461
|
+
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; }
|
|
1462
|
+
});
|
|
1463
|
+
result.disabledSouls = [...(Array.isArray(ctx.local?.souls?.disabled) ? ctx.local.souls.disabled : [])];
|
|
1464
|
+
result.lock = { path: join(ctx.deploymentDir, LOCK_FILE), lockfileVersion: lock.lockfileVersion ?? null };
|
|
1465
|
+
const { compareSemver } = await import("../lib/servers.mjs");
|
|
1466
|
+
let catalog = null; try { catalog = officialPackageCatalog(); } catch { catalog = null; }
|
|
1467
|
+
const cat = catalog && typeof catalog === "object" ? (catalog.packages && typeof catalog.packages === "object" && !("url" in catalog.packages) ? catalog.packages : catalog) : {};
|
|
1468
|
+
for (const p of result.packages) {
|
|
1469
|
+
const ref = p.source === `catalog:${p.id}` ? cat[p.id]?.ref : undefined;
|
|
1470
|
+
const tag = standaloneCatalogVersion(ref);
|
|
1471
|
+
const version = tag ? tag.replace(/^v/, "") : null;
|
|
1472
|
+
p.latest = version && typeof p.version === "string" && compareSemver(version, p.version) > 0 ? { version, ref } : null;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1344
1476
|
/** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
|
|
1345
1477
|
async function itemsCmd(kind) {
|
|
1346
1478
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
@@ -1349,6 +1481,10 @@ async function itemsCmd(kind) {
|
|
|
1349
1481
|
let lock;
|
|
1350
1482
|
try { lock = readLock(ctx.deploymentDir); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
|
|
1351
1483
|
const items = workspaceItems(discovery, lock)[kind];
|
|
1484
|
+
// One command's reads at a commit are shared: many souls resolve over the same manifests and listings.
|
|
1485
|
+
const remote = memoizedRemote(remoteModule);
|
|
1486
|
+
if (kind === "capabilities") await capabilityFacts(items, discovery, lock, ctx, remote);
|
|
1487
|
+
if (kind === "souls") await soulFacts(items, discovery, lock, ctx, remote);
|
|
1352
1488
|
const standalone = discovery.standalone === true;
|
|
1353
1489
|
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
1490
|
console.log(`${kind} of workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)})${standalone ? ` — ${standaloneNote(discovery)}` : ""}\n`);
|
|
@@ -1359,6 +1495,60 @@ async function itemsCmd(kind) {
|
|
|
1359
1495
|
if (kind === "capabilities" && unsynced.length) console.log(`\n package capabilities of ${unsynced.join(", ")} appear after \`oats sync\``);
|
|
1360
1496
|
}
|
|
1361
1497
|
|
|
1498
|
+
/** Capability rows' manifest facts (feature desktop-facts): layer, description, and what each provides
|
|
1499
|
+
* (skills, commands, hooks by name) — member rows from discovery's manifest, package rows from the manifest
|
|
1500
|
+
* at the locked commit (the sync cache; no network beyond sync's). An unreadable package leaves its rows' facts null. */
|
|
1501
|
+
async function capabilityFacts(rows, discovery, lock, ctx, remote = remoteModule) {
|
|
1502
|
+
const { capabilityProvides, lockedPackageCapabilities, memberRef } = await import("../lib/resolve.mjs");
|
|
1503
|
+
const remoteOptions = ctx.remoteOptions;
|
|
1504
|
+
const catalog = (() => { try { return officialPackageCatalog(); } catch { return null; } })();
|
|
1505
|
+
const packages = new Map();
|
|
1506
|
+
for (const [id, entry] of Object.entries(lock?.packages || {})) {
|
|
1507
|
+
try { packages.set(id, await lockedPackageCapabilities(id, entry, { catalog, remote, remoteOptions })); } catch { packages.set(id, null); }
|
|
1508
|
+
}
|
|
1509
|
+
// A member capability's fingerprint is its Git tree at the commit (one listing per member commit); a
|
|
1510
|
+
// package's is the package integrity (the lock).
|
|
1511
|
+
const trees = new Map();
|
|
1512
|
+
const treeOf = async ({ repoKey, commit }, ref, dir) => {
|
|
1513
|
+
const key = `${repoKey}\0${commit}`;
|
|
1514
|
+
if (!trees.has(key)) {
|
|
1515
|
+
const member = discovery.members.find((m) => m.key === repoKey);
|
|
1516
|
+
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);
|
|
1517
|
+
trees.set(key, remoteModule.remoteTreeOids(ref, commit, [...new Set(dirs)], remoteOptions).catch(() => new Map()));
|
|
1518
|
+
}
|
|
1519
|
+
return (await trees.get(key)).get(dir) ?? null;
|
|
1520
|
+
};
|
|
1521
|
+
for (const row of rows) {
|
|
1522
|
+
let manifest = null, ref = null, dir = null;
|
|
1523
|
+
if (row.kind === "package") {
|
|
1524
|
+
const read = packages.get(row.package), cap = read?.capabilities.find((c) => c.name === row.name);
|
|
1525
|
+
if (cap) ({ manifest, dir } = cap), ref = read.ref;
|
|
1526
|
+
} else {
|
|
1527
|
+
const cap = discovery.members.find((m) => m.key === row.repoKey)?.capabilities.find((c) => c.name === row.name);
|
|
1528
|
+
if (cap) { manifest = cap.manifest; dir = cap.path; ref = memberRef(discovery, remoteModule, row.repoKey); }
|
|
1529
|
+
}
|
|
1530
|
+
const provides = manifest ? await capabilityProvides({ ref, commit: row.commit, dir, manifest, remote, remoteOptions }) : { skills: null, commands: null, hooks: null };
|
|
1531
|
+
const repoKey = row.kind === "package" && ref ? (() => { try { return remoteModule.parseRepoRef(ref).key; } catch { return null; } })() : row.repoKey;
|
|
1532
|
+
const file = dir ? { path: `${dir}/oats.json`, url: remoteModule.browseUrl(repoKey, row.commit, `${dir}/oats.json`) } : null;
|
|
1533
|
+
const tree = row.kind !== "package" && ref && dir ? await treeOf(row, ref, dir) : null;
|
|
1534
|
+
Object.assign(row, { layer: manifest?.layer ?? row.layer ?? null, description: manifest?.description ?? null, ...provides, file, tree });
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/** Souls rows' spawnability (feature desktop-facts): the refusal a spawn of each would meet, computed from
|
|
1539
|
+
* discovery and the lock without spawning (soulSpawnability). */
|
|
1540
|
+
async function soulFacts(rows, discovery, lock, ctx, remote = remoteModule) {
|
|
1541
|
+
const { soulSpawnability } = await import("../lib/instance-resolution.mjs");
|
|
1542
|
+
const entryOf = (row) => row.kind === "package" ? (discovery.packageSouls || []).find((p) => p.qualifiedName === row.qualifiedName)
|
|
1543
|
+
: row.kind === "external" ? (discovery.external || []).map((e) => e.soul).find((x) => x.name === row.name && x.repoKey === row.repoKey)
|
|
1544
|
+
: discovery.members.find((m) => m.key === row.repoKey)?.souls.find((x) => x.name === row.name);
|
|
1545
|
+
for (const row of rows) {
|
|
1546
|
+
const entry = entryOf(row);
|
|
1547
|
+
row.file = { path: `${row.path}/soul.yaml`, url: remoteModule.browseUrl(row.repoKey, row.commit, `${row.path}/soul.yaml`) };
|
|
1548
|
+
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` } });
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1362
1552
|
// ---------- roster: status / spawn / retire / create ----------
|
|
1363
1553
|
/** Workspace drift for `oats status` (decision 17: shown, not prevented). ONE discovery over
|
|
1364
1554
|
* the remotes serves every instance; `driftOf` compares each instance's recorded modules to
|
|
@@ -1383,7 +1573,7 @@ async function statusDrift(data) {
|
|
|
1383
1573
|
for (const a of data) {
|
|
1384
1574
|
if (!a.dir) continue;
|
|
1385
1575
|
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,
|
|
1576
|
+
catch { /* not a workspace soul (classic, or an unreadable stamp) */ }
|
|
1387
1577
|
}
|
|
1388
1578
|
const anything = data.some((a) => (a.instances || []).some((i) => hasModules(i) || hasSoul(i)));
|
|
1389
1579
|
if (!anything) return { drift: new Map(), soul: new Map(), souls, unreachable: null };
|
|
@@ -1546,7 +1736,7 @@ async function spawnCmd() {
|
|
|
1546
1736
|
// the per-commit cache, or fetches it to a temporary copy (reported as soulFetched).
|
|
1547
1737
|
const providerPairs = [];
|
|
1548
1738
|
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,
|
|
1739
|
+
let wsPrepared, soulFetched = false, wsDiscovery;
|
|
1550
1740
|
{
|
|
1551
1741
|
let discovery = null;
|
|
1552
1742
|
try {
|
|
@@ -1581,50 +1771,12 @@ async function spawnCmd() {
|
|
|
1581
1771
|
const file = officialCatalogFile();
|
|
1582
1772
|
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
1773
|
}
|
|
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);
|
|
1774
|
+
if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details);
|
|
1588
1775
|
else throw e;
|
|
1589
1776
|
}
|
|
1590
1777
|
}
|
|
1591
1778
|
if (agentsRootFlag !== undefined && !agent) bail("E_SOUL_UNKNOWN", `soul "${name}" is not at agents root ${String(agentsRootFlag)}`);
|
|
1592
1779
|
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
1780
|
checkDirectoryOptions(requestedWork || agent?.work);
|
|
1629
1781
|
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
1782
|
for (const information of agent.notes || []) note(`[${information.code}] ${information.message}`);
|
|
@@ -1653,9 +1805,6 @@ async function spawnCmd() {
|
|
|
1653
1805
|
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
1806
|
// NOTE: explicit "unrelated" is passed through to the kernel.
|
|
1655
1807
|
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
1808
|
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
1809
|
}
|
|
1661
1810
|
const taskText = flag("task");
|
|
@@ -1715,10 +1864,10 @@ async function spawnCmd() {
|
|
|
1715
1864
|
// convention <deployment>/<member name>. Never an ambient Git checkout around the
|
|
1716
1865
|
// deployment. Resolved once here so a preview sees exactly what the apply would.
|
|
1717
1866
|
let preparedRepo;
|
|
1718
|
-
if (wsPrepared
|
|
1867
|
+
if (wsPrepared) {
|
|
1719
1868
|
try {
|
|
1720
1869
|
const { toCapabilityRows, modulesPreview, requireMemberClone } = await import("../lib/instance-resolution.mjs");
|
|
1721
|
-
prepared = wsPrepared
|
|
1870
|
+
prepared = wsPrepared;
|
|
1722
1871
|
prepared.capabilityRows = []; // filled after materialization (paths live in the home); preview uses modulesPreview
|
|
1723
1872
|
prepared.preview = modulesPreview(prepared.resolution, root, agent.name);
|
|
1724
1873
|
prepared.toCapabilityRows = toCapabilityRows;
|
|
@@ -1782,7 +1931,7 @@ async function spawnCmd() {
|
|
|
1782
1931
|
if (e?.code === "E_IDEMPOTENCY_CONFLICT") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
|
|
1783
1932
|
if (e?.code === "E_PLACEMENT_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
|
|
1784
1933
|
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; }
|
|
1934
|
+
if (e?.code === "E_INSTANCE_NAME_INVALID") { bail(e.code, e.message, e.details); throw e; }
|
|
1786
1935
|
if (e?.code === "E_SPAWN_INCOMPLETE") { bail(e.code, e.message, { instance: e.instance, home: e.home, launched: e.launched }); throw e; }
|
|
1787
1936
|
bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
|
|
1788
1937
|
}
|
|
@@ -1935,7 +2084,7 @@ function retireCmd() {
|
|
|
1935
2084
|
/** `oats schedule ...`: workspace-scoped definitions, host-owned execution
|
|
1936
2085
|
* (lib/schedule.mjs). Every subcommand answers the envelope; nothing here
|
|
1937
2086
|
* launches unless a job is due or run-now is asked. */
|
|
1938
|
-
function scheduleCmd() {
|
|
2087
|
+
async function scheduleCmd() {
|
|
1939
2088
|
const sub = args[1];
|
|
1940
2089
|
const id = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
|
|
1941
2090
|
// One schedule-owning scope for a directory: its deployment (the directory
|
|
@@ -1944,7 +2093,11 @@ function scheduleCmd() {
|
|
|
1944
2093
|
let scope;
|
|
1945
2094
|
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
1946
2095
|
const io = { hostStatus: () => hostUnitStatus() };
|
|
1947
|
-
|
|
2096
|
+
// The workspace automations of this deployment (the snapshot) placed on this host.
|
|
2097
|
+
let actx;
|
|
2098
|
+
const ctx = () => (actx ??= scopeAutomations(ws(), { io }));
|
|
2099
|
+
const isWorkspace = (qid) => A.splitId(qid).scope !== "local";
|
|
2100
|
+
const out = (result, text) => { if (JSON_MODE) jsonOk(result); else console.log(text ? text(result) : JSON.stringify(result, null, 2)); };
|
|
1948
2101
|
const readSpec = () => {
|
|
1949
2102
|
const inline = flag("spec-json");
|
|
1950
2103
|
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 +2109,41 @@ function scheduleCmd() {
|
|
|
1956
2109
|
const needId = () => { if (!id) throw scheduleError("E_BAD_ARGS", `oats schedule ${sub} <id>`); return id; };
|
|
1957
2110
|
try {
|
|
1958
2111
|
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 "
|
|
2112
|
+
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; }
|
|
2113
|
+
case "show": return out({ schedule: describeSchedule(ws(), needId(), io, { ctx: ctx() }) });
|
|
2114
|
+
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)`);
|
|
2115
|
+
case "add": {
|
|
2116
|
+
const spec = readSpec();
|
|
2117
|
+
if (flag("workspace") !== undefined) {
|
|
2118
|
+
const wid = id ?? spec.id;
|
|
2119
|
+
if (typeof wid !== "string") throw scheduleError("E_BAD_ARGS", "oats schedule add <id> --workspace <member> …: the id names the file (oats-schedules/<id>.yaml)");
|
|
2120
|
+
const { id: _i, enabled: _e, kind, ...rest } = spec; void _i; void _e;
|
|
2121
|
+
const r = await addWorkspaceAutomation(scheduleKind({ dep: ws() }), { id: wid, body: { run: kind ?? "spawn", ...rest } });
|
|
2122
|
+
return out(r, printWorkspaceAdd);
|
|
2123
|
+
}
|
|
2124
|
+
if (id && spec.id === undefined) spec.id = id;
|
|
2125
|
+
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" });
|
|
2126
|
+
return out({ schedule: addSchedule(ws(), spec, io) });
|
|
2127
|
+
}
|
|
1962
2128
|
case "update": return out({ schedule: updateSchedule(ws(), needId(), readSpec(), io) });
|
|
1963
|
-
case "enable":
|
|
1964
|
-
case "disable":
|
|
1965
|
-
|
|
2129
|
+
case "enable":
|
|
2130
|
+
case "disable": {
|
|
2131
|
+
const on = sub === "enable";
|
|
2132
|
+
if (!isWorkspace(needId())) return out({ schedule: setScheduleEnabled(ws(), id, on, io) });
|
|
2133
|
+
describeSchedule(ws(), id, io, { ctx: ctx() }); // an unknown id is refused before the file changes
|
|
2134
|
+
setDisabledHereCli(ws(), "schedule", A.qualifiedId(id), on);
|
|
2135
|
+
actx = undefined;
|
|
2136
|
+
return out({ schedule: describeSchedule(ws(), id, io, { ctx: ctx() }) });
|
|
2137
|
+
}
|
|
2138
|
+
case "run": return out(runScheduleNow(ws(), needId(), { io, force: args.includes("--force"), ctx: ctx() }));
|
|
1966
2139
|
case "remove": return out(removeSchedule(ws(), needId(), { force: args.includes("--force") }));
|
|
1967
|
-
case "reconcile": return out(reconcileSchedule(ws(), needId(), { io, clear: args.includes("--clear") }));
|
|
2140
|
+
case "reconcile": return out(reconcileSchedule(ws(), needId(), { io, clear: args.includes("--clear"), ctx: ctx() }));
|
|
1968
2141
|
case "tick": {
|
|
1969
2142
|
const dryRun = args.includes("--dry-run");
|
|
1970
2143
|
if (args.includes("--host")) return out(tickHost({ io, dryRun }));
|
|
1971
2144
|
const reg = readRegistry();
|
|
1972
|
-
const
|
|
2145
|
+
const tctx = scopeAutomations(ws(), { io, refresh: !dryRun });
|
|
2146
|
+
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
2147
|
return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws(), io) });
|
|
1974
2148
|
}
|
|
1975
2149
|
case "host": {
|
|
@@ -1979,7 +2153,7 @@ function scheduleCmd() {
|
|
|
1979
2153
|
if (op === "status") return out({ scheduler: schedulerStatus(ws(), io) });
|
|
1980
2154
|
throw scheduleError("E_BAD_ARGS", "oats schedule host install|uninstall|status");
|
|
1981
2155
|
}
|
|
1982
|
-
default: throw scheduleError("E_BAD_ARGS", "usage: oats schedule list|show <id>|add <id> --file <spec.json
|
|
2156
|
+
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
2157
|
}
|
|
1984
2158
|
} catch (e) {
|
|
1985
2159
|
// K8b: typed refusal details travel (identity mismatch: key/declared; a refused file: its integrity source).
|
|
@@ -1998,16 +2172,28 @@ async function triggerCmd() {
|
|
|
1998
2172
|
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
1999
2173
|
const out = (result, text) => { if (JSON_MODE) jsonOk(result); else console.log(text ? text(result) : JSON.stringify(result, null, 2)); };
|
|
2000
2174
|
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
|
|
2175
|
+
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>)";
|
|
2176
|
+
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");
|
|
2177
|
+
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}` : ""}`;
|
|
2178
|
+
// The workspace automations of this deployment (the snapshot) placed on this host.
|
|
2179
|
+
let actx;
|
|
2180
|
+
const ctx = () => (actx ??= scopeAutomations(ws(), {}));
|
|
2181
|
+
const isWorkspace = (qid) => A.splitId(qid).scope !== "local";
|
|
2003
2182
|
try {
|
|
2004
2183
|
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
|
-
|
|
2184
|
+
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>)");
|
|
2185
|
+
case "show": return out({ trigger: T.describeTrigger(ws(), needId(), ctx()) });
|
|
2186
|
+
case "enable":
|
|
2187
|
+
case "disable": {
|
|
2188
|
+
const on = sub === "enable";
|
|
2189
|
+
if (!isWorkspace(needId())) return out({ trigger: T.setTriggerEnabled(ws(), id, on, ctx()) }, (r) => line(r.trigger));
|
|
2190
|
+
T.describeTrigger(ws(), id, ctx()); // an unknown id is refused before the file changes
|
|
2191
|
+
setDisabledHereCli(ws(), "trigger", A.qualifiedId(id), on);
|
|
2192
|
+
actx = undefined;
|
|
2193
|
+
return out({ trigger: T.describeTrigger(ws(), id, ctx()) }, (r) => line(r.trigger));
|
|
2194
|
+
}
|
|
2195
|
+
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(", ")})` : ""}`);
|
|
2196
|
+
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
2197
|
case "test": {
|
|
2012
2198
|
let workspaceTeams = null;
|
|
2013
2199
|
try {
|
|
@@ -2015,8 +2201,9 @@ async function triggerCmd() {
|
|
|
2015
2201
|
const { local } = loadLocal(ws());
|
|
2016
2202
|
if (typeof local.workspace === "string") workspaceTeams = Object.keys((await observeWorkspace(local.workspace, { remoteOptions: remoteOptionsFromEnv() })).workspace.teams || {});
|
|
2017
2203
|
} catch { /* reported as unknown (null) */ }
|
|
2018
|
-
return out(T.testTrigger(ws(), needId(), { workspaceTeams }), (r) => [
|
|
2204
|
+
return out(T.testTrigger(ws(), needId(), { workspaceTeams, ctx: ctx() }), (r) => [
|
|
2019
2205
|
`trigger ${r.id}: ${r.ok ? "ready" : "NOT ready"} (nothing was spawned)`,
|
|
2206
|
+
` 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
2207
|
` gh auth ${r.gh.ok ? `ok — ${r.gh.account ?? "?"} via ${r.gh.credentialSource}` : `FAILED${r.gh.detail ? ` — ${r.gh.detail}` : ""}`}`,
|
|
2021
2208
|
` 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
2209
|
` 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 +2230,17 @@ async function triggerCmd() {
|
|
|
2043
2230
|
if (Object.keys(sets).length) throw T.triggerError("E_BAD_ARGS", "--set fills a package template's parameters; with --file, edit the file");
|
|
2044
2231
|
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
2232
|
if (idFlag !== undefined) spec.id = idFlag;
|
|
2046
|
-
}
|
|
2233
|
+
}
|
|
2234
|
+
if (flag("workspace") !== undefined) {
|
|
2235
|
+
// A workspace trigger (0.29.0): the file in the member, from/set when made from a template.
|
|
2236
|
+
const kinds = await automationKinds(ws(), readLock(ws()), remoteOptionsFromEnv());
|
|
2237
|
+
const wid = typeof idFlag === "string" ? idFlag : from !== undefined ? String(from).split(":")[1] : spec?.id;
|
|
2238
|
+
if (typeof wid !== "string") throw T.triggerError("E_BAD_ARGS", "--workspace: name the trigger with --id (or an id in the file)");
|
|
2239
|
+
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);
|
|
2240
|
+
const r = await addWorkspaceAutomation(kinds.trigger, { id: wid, body });
|
|
2241
|
+
return out(r, printWorkspaceAdd);
|
|
2242
|
+
}
|
|
2243
|
+
if (spec === undefined) spec = await triggerFromPackage(T, String(from), sets, idFlag);
|
|
2047
2244
|
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
2245
|
}
|
|
2049
2246
|
default: throw T.triggerError("E_BAD_ARGS", usage);
|
|
@@ -2056,28 +2253,8 @@ async function triggerCmd() {
|
|
|
2056
2253
|
/** A package trigger template (`triggers: [{ id, file }]` in the locked package's oats-package.json),
|
|
2057
2254
|
* read at the lock's commit and instantiated with the --set parameters. */
|
|
2058
2255
|
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 } });
|
|
2256
|
+
const { bindRemote } = await import("../lib/packages.mjs");
|
|
2257
|
+
return T.packageTriggerTemplate(readLock(scheduleScopeOf(dirFlag())), from, sets, { id: idFlag, remote: bindRemote(remoteModule, remoteOptionsFromEnv()) });
|
|
2081
2258
|
}
|
|
2082
2259
|
|
|
2083
2260
|
async function sessionCmd() {
|
|
@@ -2326,7 +2503,7 @@ async function capabilityCommand() {
|
|
|
2326
2503
|
// No home, so no recorded soul: the soul's per-commit copy is OATS_SOUL when a spawn
|
|
2327
2504
|
// already fetched exactly this commit; otherwise the command gets none (never ambient).
|
|
2328
2505
|
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);
|
|
2506
|
+
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
2507
|
}
|
|
2331
2508
|
|
|
2332
2509
|
async function dispatch() {
|
|
@@ -2340,14 +2517,14 @@ async function capabilityCommand() {
|
|
|
2340
2517
|
// braces: the ids come from instance.json, which spawn wrote from resolved
|
|
2341
2518
|
// manifests. Null-prototype because the dispatcher indexes it with the
|
|
2342
2519
|
// namespace the operator typed on the command line.
|
|
2343
|
-
let capSettings = Object.create(null);
|
|
2520
|
+
let capSettings = Object.create(null), capOrigins = Object.create(null);
|
|
2344
2521
|
let deployment = null;
|
|
2345
2522
|
let soulDir;
|
|
2346
2523
|
try {
|
|
2347
2524
|
if (metaFile && existsSync(metaFile)) {
|
|
2348
2525
|
const meta = JSON.parse(readFileSync(metaFile, "utf8"));
|
|
2349
2526
|
activeIds = (meta.capabilities || []).map((c) => c.id);
|
|
2350
|
-
for (const c of meta.capabilities || []) capSettings[c.id] = c.settings || {};
|
|
2527
|
+
for (const c of meta.capabilities || []) { capSettings[c.id] = c.settings || {}; capOrigins[c.id] = c.settingsOrigins || {}; }
|
|
2351
2528
|
// Workspace-model homes only (lead decision c3 Q2).
|
|
2352
2529
|
if (isCapturedHome(meta)) { const e = capturedHomeRefusal(instanceHome, "nothing was dispatched"); bail(e.code, e.message, e.details); }
|
|
2353
2530
|
if (!isWorkspaceHome(meta)) { const e = preWorkspaceHome(instanceHome, "nothing was dispatched"); bail(e.code, e.message); }
|
|
@@ -2386,14 +2563,14 @@ async function capabilityCommand() {
|
|
|
2386
2563
|
const live = await liveTeams(instanceHome, homeMeta.meta, { remoteOptions: remoteOptionsFromEnv() });
|
|
2387
2564
|
teamCtx = homeTeamCtx(live.teams, live.source);
|
|
2388
2565
|
}
|
|
2389
|
-
return runManifestCommand(m, capSettings[m.capability] || {}, teamCtx, () => m._dir, soulDir);
|
|
2566
|
+
return runManifestCommand(m, { settings: capSettings[m.capability] || {}, origins: capOrigins[m.capability] }, teamCtx, () => m._dir, soulDir);
|
|
2390
2567
|
}
|
|
2391
2568
|
|
|
2392
2569
|
/** Help / unknown-command / spec validation / exec — shared by every context.
|
|
2393
2570
|
* `m` is the manifest (with `capability`; `_dir` may be absent until `ensureDir`
|
|
2394
2571
|
* resolves the directory holding the executable — the operator branch fetches
|
|
2395
2572
|
* the module tree only when a command is actually going to run). */
|
|
2396
|
-
async function runManifestCommand(m, settings, teamCtx, ensureDir, soulDir) {
|
|
2573
|
+
async function runManifestCommand(m, { settings, origins }, teamCtx, ensureDir, soulDir) {
|
|
2397
2574
|
const sub = args[1];
|
|
2398
2575
|
const cmds = Object.keys(m.commands);
|
|
2399
2576
|
// `oats <ns> --help` and `oats <ns> <cmd> --help` answer from the manifest
|
|
@@ -2438,6 +2615,8 @@ async function capabilityCommand() {
|
|
|
2438
2615
|
// lifecycle hooks — capabilities read their settings here instead of
|
|
2439
2616
|
// importing the kernel resolver.
|
|
2440
2617
|
OATS_SETTINGS: JSON.stringify(settings || {}),
|
|
2618
|
+
// Where each leaf came from (JSON pointer → { kind, at }), as lifecycle hooks get it.
|
|
2619
|
+
OATS_SETTINGS_ORIGINS: JSON.stringify(origins || {}),
|
|
2441
2620
|
// PATH is not a trusted runtime boundary (maintainer finding 1): pass the
|
|
2442
2621
|
// canonical absolute executable of THIS CLI; official consumers execFile
|
|
2443
2622
|
// it directly and never resolve `oats` from PATH or a shell.
|
|
@@ -2507,7 +2686,7 @@ function versionCmd() {
|
|
|
2507
2686
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
2508
2687
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
2509
2688
|
// 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 }));
|
|
2689
|
+
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
2690
|
return;
|
|
2512
2691
|
}
|
|
2513
2692
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -2921,8 +3100,9 @@ else if (cmd === "version" || cmd === "--version" || cmd === "-v") versionCmd();
|
|
|
2921
3100
|
// Same rule as the inner catch: a typed CLI failure surfaces with its own code
|
|
2922
3101
|
// through the shared boundary, never re-badged as a spawn-mechanism failure.
|
|
2923
3102
|
else if (cmd === "session") await sessionCmd();
|
|
2924
|
-
else if (cmd === "schedule") scheduleCmd();
|
|
3103
|
+
else if (cmd === "schedule") await scheduleCmd();
|
|
2925
3104
|
else if (cmd === "trigger") await triggerCmd();
|
|
3105
|
+
else if (cmd === "automations") await automationsCmd();
|
|
2926
3106
|
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
3107
|
else if (cmd === "retire") retireCmd();
|
|
2928
3108
|
else if (cmd === "capture" || cmd === "recall" || cmd === "setup") await recordCmd(cmd);
|
|
@@ -3011,7 +3191,7 @@ Usage:
|
|
|
3011
3191
|
next steps (clone members you work IN, spawn
|
|
3012
3192
|
oats-operator-expert); creates no soul, spawns nothing
|
|
3013
3193
|
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>
|
|
3194
|
+
oats schedule list|show <id>|test <id>|add <id> --file <spec.json>|update <id> --file <spec.json>
|
|
3015
3195
|
enable|disable|run|remove|reconcile <id> workspace-scoped, host-owned schedules (spawn,
|
|
3016
3196
|
tick [--dry-run] [--host] command or wake jobs on a five-field cron with an
|
|
3017
3197
|
host install|uninstall|status explicit IANA tz; see docs/schedules.md); --server
|
|
@@ -3020,6 +3200,13 @@ Usage:
|
|
|
3020
3200
|
| disable | remove <id> | test <id> | status [<id>] event-driven spawns (github.pull_request
|
|
3021
3201
|
polled with the host's gh by the schedule tick;
|
|
3022
3202
|
see docs/schedules.md#triggers)
|
|
3203
|
+
oats trigger|schedule add … --workspace <member> --runs-on <host> --owner <host>/<login>
|
|
3204
|
+
a trigger/schedule declared in Git (<member>/<id>):
|
|
3205
|
+
writes oats-triggers|oats-schedules/<id>.yaml in a
|
|
3206
|
+
checkout of the member, else prints it
|
|
3207
|
+
oats automations refresh [--json] re-discover the workspace triggers and schedules
|
|
3208
|
+
(the snapshot the host tick reads; oats sync does
|
|
3209
|
+
it too)
|
|
3023
3210
|
oats spawn ... --wake-file <json> | --wake-every <N> --wake-message <text> save a wake schedule
|
|
3024
3211
|
bound to the new instance's home (docs/schedules.md)
|
|
3025
3212
|
oats session upload --file <path> store a copy of a local file as a private attachment
|
|
@@ -3053,6 +3240,9 @@ Usage:
|
|
|
3053
3240
|
[--name <slug>] the exact instance name (no <agent>- prefix;
|
|
3054
3241
|
not with --purpose); must be a slug, not a
|
|
3055
3242
|
soul name, and unused in the deployment
|
|
3243
|
+
[--provider <capability> <key>=<value>] a provider setting for this spawn only
|
|
3244
|
+
(repeatable; dotted keys nest; recorded in
|
|
3245
|
+
instance.json providers.<capability>)
|
|
3056
3246
|
oats retire <instance> [--force] retire an instance (window, hooks,
|
|
3057
3247
|
[--self] [--delete-branch] worktree, home); --self = retire the
|
|
3058
3248
|
[--keep-dir] [--json] CALLING instance: the window dies, then
|