@awebai/oats 0.27.2 → 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 +445 -96
- package/capabilities/oats-okf/bin/oats-okf.mjs +55 -30
- package/capabilities/oats-okf/injects/okf.md +36 -28
- package/capabilities/oats-okf/lib/binding-wire.mjs +4 -1
- package/capabilities/oats-okf/lib/config.mjs +6 -1
- package/capabilities/oats-okf/lib/consult.mjs +496 -0
- 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/inspection.mjs +11 -3
- package/capabilities/oats-okf/lib/io.mjs +9 -2
- package/capabilities/oats-okf/lib/okf-validate.mjs +123 -0
- package/capabilities/oats-okf/lib/sources.mjs +42 -55
- package/capabilities/oats-okf/lib/stores.mjs +19 -11
- package/capabilities/oats-okf/lib/worker.mjs +90 -8
- package/capabilities/oats-okf/oats.json +24 -9
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +144 -0
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +86 -0
- 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 -22
- 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/design/2026-09-24-phase-d-plan.md +11 -0
- package/docs/design/2026-09-26-desktop-design-brief-architecture.md +241 -0
- package/docs/design/2026-09-26-okf-knowledge-operations.md +389 -0
- package/docs/desktop-cli-api.md +342 -10
- 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 +33 -2
- package/docs/oats-package.schema.json +39 -0
- package/docs/official-catalog.md +7 -4
- package/docs/packages.md +76 -6
- package/docs/release-lane.md +1 -1
- package/docs/release-notes/v0.28.0.md +144 -0
- package/docs/release-notes/v0.29.0.md +240 -0
- package/docs/schedules.md +230 -4
- package/docs/souls-and-instances.md +11 -9
- package/docs/workspaces.md +18 -3
- package/lib/automations.mjs +369 -0
- package/lib/core.mjs +87 -158
- package/lib/instance-inspect.mjs +16 -8
- package/lib/instance-resolution.mjs +90 -197
- package/lib/materialize.mjs +18 -7
- package/lib/operator-dispatch.mjs +1 -2
- package/lib/packages.mjs +107 -6
- package/lib/remote.mjs +21 -1
- package/lib/resolve.mjs +71 -9
- package/lib/schedule.mjs +228 -45
- package/lib/triggers.mjs +678 -0
- package/lib/workspace.mjs +81 -4
- package/package-catalog.json +6 -4
- package/package.json +1 -1
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +0 -21
- 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,18 +27,20 @@ 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
|
-
import { loadLocal, validateWorkspace, validateLocal } from "../lib/workspace.mjs";
|
|
35
|
+
import { loadLocal, validateWorkspace, validateLocal, discoverPackageSouls, workspaceWarnings } from "../lib/workspace.mjs";
|
|
36
36
|
import { parseConfigData } from "../lib/config-data.mjs";
|
|
37
37
|
import * as remoteModule from "../lib/remote.mjs";
|
|
38
38
|
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
|
-
import {
|
|
41
|
+
import { tickTriggers } from "../lib/triggers.mjs";
|
|
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";
|
|
42
44
|
import { hostUnitStatus, installHostUnit, uninstallHostUnit } from "../lib/schedule-host.mjs";
|
|
43
45
|
import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_BYTES } from "../lib/attachments.mjs";
|
|
44
46
|
|
|
@@ -48,10 +50,32 @@ const await_import_lifecycle = () => ({ resolveInstance: resolveInstanceForCli }
|
|
|
48
50
|
import { homeTarget, soulTarget, isWorkspaceContext, inspectDocument, readinessDocument, policyOf, policySoul, manifestMissingRequires, INSPECT_OPERATIONS_API } from "../lib/instance-inspect.mjs";
|
|
49
51
|
import { readEvents } from "../lib/instance-events.mjs";
|
|
50
52
|
|
|
51
|
-
const
|
|
53
|
+
const rawArgs = process.argv.slice(2);
|
|
54
|
+
/** The kernel's switches: a value never rides one (`--yolo=false` must not turn yolo on). */
|
|
55
|
+
const KERNEL_SWITCHES = new Set(["allow-child-spawns", "apply", "check", "clear", "delete-branch", "discard-worktree", "dry-run", "ephemeral", "force", "help", "host", "json", "keep-dir", "keep-env", "no-child-spawns", "no-launch", "no-recursive", "no-yolo", "plan", "policy", "preview", "print", "replace", "self", "verbose", "yes", "yolo"]);
|
|
56
|
+
/** `--flag=value` is `--flag value`: every kernel reader (flag(), valueFlag(), the onboard and
|
|
57
|
+
* routed-command loops) then applies the spaced form's validation to it. `problem` is an empty
|
|
58
|
+
* `--flag=` or a switch given a value. */
|
|
59
|
+
function expandInlineValues(argv) {
|
|
60
|
+
const out = [];
|
|
61
|
+
let problem;
|
|
62
|
+
for (const a of argv) {
|
|
63
|
+
const eq = a.indexOf("=");
|
|
64
|
+
if (!a.startsWith("--") || eq <= 2) { out.push(a); continue; }
|
|
65
|
+
const name = a.slice(2, eq), value = a.slice(eq + 1);
|
|
66
|
+
problem ??= KERNEL_SWITCHES.has(name) ? `--${name} takes no value (got ${a})` : value === "" ? `--${name}= needs a value` : undefined;
|
|
67
|
+
out.push(`--${name}`, value);
|
|
68
|
+
}
|
|
69
|
+
return { argv: out, problem };
|
|
70
|
+
}
|
|
71
|
+
const { argv: args, problem: argvProblem } = expandInlineValues(rawArgs);
|
|
52
72
|
let cmd = args[0];
|
|
53
73
|
const HELP_WORDS = new Set(["help", "--help", "-h"]);
|
|
54
|
-
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"]);
|
|
75
|
+
/** Commands whose argv another parser reads (packages/record and packages/experimental parse process.argv). */
|
|
76
|
+
const OWN_ARGV_COMMANDS = new Set(["capture", "recall", "setup", "experimental"]);
|
|
77
|
+
/** Commands `--server <id>` runs on a registered server. */
|
|
78
|
+
const ROUTED_COMMANDS = new Set(["spawn", "retire", "status", "session", "okf", "schedule", "inspect", "operation", "launch-config"]);
|
|
55
79
|
const flag = (name) => {
|
|
56
80
|
const i = args.indexOf(`--${name}`);
|
|
57
81
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -154,7 +178,7 @@ function operationalKnowledgeNote(composition, soulName) {
|
|
|
154
178
|
* home-relative (`.oats/modules/<cap>/<inject>`), where an instance carries them. */
|
|
155
179
|
async function doctorComposition(ctx, soulName, ws, bail) {
|
|
156
180
|
if (!soulName) return undefined;
|
|
157
|
-
const { prepareInstance, previewWorkspaceSoul, materializePrepared, discoverOrStandalone } = await import("../lib/instance-resolution.mjs");
|
|
181
|
+
const { prepareInstance, previewWorkspaceSoul, materializePrepared, discoverOrStandalone, agentDirOf } = await import("../lib/instance-resolution.mjs");
|
|
158
182
|
const deployment = dirname(ws.local.path);
|
|
159
183
|
const root = join(deployment, "agents");
|
|
160
184
|
const remoteOptions = remoteOptionsFromEnv();
|
|
@@ -162,11 +186,11 @@ async function doctorComposition(ctx, soulName, ws, bail) {
|
|
|
162
186
|
// A bail exits the process without unwinding: the temporary copies are removed at exit too.
|
|
163
187
|
process.once("exit", () => { for (const c of cleanups) { try { c(); } catch { /* best effort */ } } });
|
|
164
188
|
try {
|
|
165
|
-
const discovery = await discoverOrStandalone(loadLocal(deployment).local, { remoteOptions });
|
|
189
|
+
const discovery = await discoverOrStandalone(loadLocal(deployment).local, { deployment, remoteOptions });
|
|
166
190
|
const prepared = await prepareInstance(deployment, soulName, { remoteOptions, discovery });
|
|
167
191
|
const pv = await previewWorkspaceSoul(prepared, root);
|
|
168
192
|
cleanups.push(pv.cleanup);
|
|
169
|
-
const agent = findAgentAt(root, prepared.soulEntry
|
|
193
|
+
const agent = findAgentAt(root, agentDirOf(prepared.soulEntry), pv.soulDir);
|
|
170
194
|
if (!agent) bail("E_SOUL_UNKNOWN", `soul "${soulName}" was fetched but is not readable as a soul`);
|
|
171
195
|
const composition = composeInstanceAgentsMd(pv.soulDir, deployment, agent.name, agent.work || "checkout", agent.kind, prepared);
|
|
172
196
|
const scratch = realpathSync(mkdtempSync(join(tmpdir(), "oats-doctor-home-")));
|
|
@@ -1035,7 +1059,7 @@ function standaloneCatalogVersion(ref) {
|
|
|
1035
1059
|
async function discoverForCli(ctx, bail) {
|
|
1036
1060
|
// The standalone case (decisions 10/25) is a discovery too: the repo's own view
|
|
1037
1061
|
// plus the kernel's oats.core default — discoverOrStandalone decides.
|
|
1038
|
-
try { const { discoverOrStandalone } = await import("../lib/instance-resolution.mjs"); return await discoverOrStandalone(ctx.local, { remoteOptions: ctx.remoteOptions }); }
|
|
1062
|
+
try { const { discoverOrStandalone } = await import("../lib/instance-resolution.mjs"); return await discoverOrStandalone(ctx.local, { deployment: ctx.deploymentDir, remoteOptions: ctx.remoteOptions }); }
|
|
1039
1063
|
catch (e) {
|
|
1040
1064
|
if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance);
|
|
1041
1065
|
throw e;
|
|
@@ -1049,6 +1073,10 @@ const memberLabel = (key) => String(key).split("/").filter(Boolean).pop()?.repla
|
|
|
1049
1073
|
const teamLabel = (team) => team ?? "unassigned";
|
|
1050
1074
|
const originOf = (item) => (item.package ? `package ${item.package} v${item.version}` : `member ${item.repoKey} @ ${short(item.commit)}`);
|
|
1051
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
|
+
|
|
1052
1080
|
/** Rows of every soul and capability of confirmed members (+ external souls) + locked package
|
|
1053
1081
|
* capabilities. Souls have no private mode (0.26.0); a private member capability is listed with
|
|
1054
1082
|
* `private: true` — repo-owned: usable only by its own repo's souls (E_CAPABILITY_PRIVATE). */
|
|
@@ -1057,12 +1085,15 @@ function workspaceItems(discovery, lock) {
|
|
|
1057
1085
|
const capabilities = [];
|
|
1058
1086
|
for (const m of discovery.members) {
|
|
1059
1087
|
if (!m.confirmed && !(discovery.standalone === true && m.key === discovery.key)) continue;
|
|
1060
|
-
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) });
|
|
1061
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 });
|
|
1062
1090
|
}
|
|
1063
1091
|
for (const ext of discovery.external || []) {
|
|
1064
1092
|
const s = ext.soul;
|
|
1065
|
-
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) });
|
|
1094
|
+
}
|
|
1095
|
+
for (const s of discovery.packageSouls || []) {
|
|
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) });
|
|
1066
1097
|
}
|
|
1067
1098
|
for (const [id, entry] of Object.entries(lock?.packages || {})) {
|
|
1068
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 });
|
|
@@ -1083,7 +1114,7 @@ function memberRows(discovery) {
|
|
|
1083
1114
|
|
|
1084
1115
|
/** Package rows for `sync` / `workspace status` from the lock. */
|
|
1085
1116
|
function packageRows(lock) {
|
|
1086
|
-
return Object.entries(lock.packages).map(([id, p]) => ({ id, version: p.version, source: p.source, commit: p.commit, integrity: p.integrity, capabilities: p.capabilities }));
|
|
1117
|
+
return Object.entries(lock.packages).map(([id, p]) => ({ id, version: p.version, source: p.source, commit: p.commit, integrity: p.integrity, capabilities: p.capabilities, souls: (p.souls ?? []).map((x) => x.name) }));
|
|
1087
1118
|
}
|
|
1088
1119
|
|
|
1089
1120
|
/** Print a padded table: rows are arrays of strings. */
|
|
@@ -1124,14 +1155,31 @@ async function performSync(ctx, bail, { onDiscovered } = {}) {
|
|
|
1124
1155
|
const lock = resolved.lock;
|
|
1125
1156
|
let lockFile;
|
|
1126
1157
|
try { lockFile = writeLock(ctx.deploymentDir, lock); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
|
|
1158
|
+
// Package souls come from the lock: list what THIS sync locked, not what the previous lock held.
|
|
1159
|
+
if (discovery.standalone !== true) {
|
|
1160
|
+
let pkg;
|
|
1161
|
+
try { pkg = await discoverPackageSouls(discovery.workspace, lock, { remoteOptions: ctx.remoteOptions }); }
|
|
1162
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance); throw e; }
|
|
1163
|
+
const kept = (list) => list.filter((p) => typeof p.package !== "string");
|
|
1164
|
+
discovery.packageSouls = pkg.souls;
|
|
1165
|
+
discovery.problems = [...kept(discovery.problems), ...pkg.problems];
|
|
1166
|
+
discovery.warnings = workspaceWarnings(discovery.workspace, discovery.members, discovery.external || [], pkg.souls);
|
|
1167
|
+
problems.splice(0, problems.length, ...kept(problems), ...pkg.problems);
|
|
1168
|
+
}
|
|
1127
1169
|
// The deployment's instance root: <deployment>/agents/ (findRoot's marker). A hand-written
|
|
1128
1170
|
// oats-local.yaml + sync is a complete deployment; spawn must not answer E_NO_DEPLOYMENT after it.
|
|
1129
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);
|
|
1130
1178
|
const members = memberRows(discovery);
|
|
1131
1179
|
const packages = packageRows(lock);
|
|
1132
1180
|
const changes = resolved.changes;
|
|
1133
1181
|
const items = workspaceItems(discovery, lock);
|
|
1134
|
-
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 ?? [] };
|
|
1135
1183
|
return { report, lock, discovery, items, lockFile, problems };
|
|
1136
1184
|
}
|
|
1137
1185
|
|
|
@@ -1152,6 +1200,7 @@ function printSyncReport(ctx, synced) {
|
|
|
1152
1200
|
const { report, discovery, items, lockFile } = synced;
|
|
1153
1201
|
const { members, packages, changes } = report;
|
|
1154
1202
|
const disabled = new Set(ctx.local.souls?.disabled || []);
|
|
1203
|
+
const isDisabled = (s) => disabled.has(s.name) || disabled.has(s.qualifiedName ?? `${memberLabel(s.repoKey)}/${s.name}`);
|
|
1155
1204
|
console.log(`workspace ${discovery.workspace?.name ?? `(${standaloneNote(discovery)})`} (${discovery.key} @ ${short(discovery.commit)})`);
|
|
1156
1205
|
console.log(`members ${members.map((m) => m.confirmed ? `${m.name} ✓↔ (@ ${short(m.commit)})` : `${m.name} ✗ (${m.status})`).join(" ") || "(none)"}`);
|
|
1157
1206
|
console.log(`packages ${packages.map((p) => `${p.id} ${p.version} ✓ (@ ${short(p.commit)})`).join(" ") || "(none)"}`);
|
|
@@ -1162,17 +1211,98 @@ function printSyncReport(ctx, synced) {
|
|
|
1162
1211
|
const externalSouls = items.souls.filter((s) => s.kind === "external");
|
|
1163
1212
|
// Souls have no private mode (0.26.0); the private count is of repo-owned member capabilities.
|
|
1164
1213
|
const repoOwned = items.capabilities.filter((c) => c.kind === "member" && c.private);
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1214
|
+
const packageSouls = items.souls.filter((s) => s.kind === "package");
|
|
1215
|
+
const disabledHere = items.souls.filter(isDisabled);
|
|
1216
|
+
console.log(`souls ${items.souls.length} discovered (${memberSouls.length} members, ${externalSouls.length} external, ${packageSouls.length ? `${packageSouls.length} package, ` : ""}${disabledHere.length} disabled here) · ${repoOwned.length} private capabilit${repoOwned.length === 1 ? "y" : "ies"}${repoOwned.length ? ` (${repoOwned.map((c) => `${c.name}, ${memberLabel(c.repoKey)} only`).join("; ")})` : ""}`);
|
|
1167
1217
|
const teams = new Map();
|
|
1168
1218
|
for (const s of items.souls) { const t = teams.get(s.team) || { souls: 0, capabilities: 0 }; t.souls++; teams.set(s.team, t); }
|
|
1169
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); }
|
|
1170
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)`);
|
|
1171
1223
|
for (const p of synced.problems ?? discovery.problems) console.log(`problem ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`);
|
|
1172
1224
|
for (const w of discovery.warnings ?? []) console.log(`warning ${w.code} ${w.message}`);
|
|
1173
1225
|
console.log(`\nlock ${shortPath(lockFile)}`);
|
|
1174
1226
|
}
|
|
1175
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
|
+
|
|
1176
1306
|
/** `oats sync [--dir] [--json]` — contract §6. */
|
|
1177
1307
|
async function syncCmd() {
|
|
1178
1308
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
@@ -1286,6 +1416,10 @@ async function workspaceCmd() {
|
|
|
1286
1416
|
const unsynced = declared.filter((id) => !locked.has(id));
|
|
1287
1417
|
const stale = packages.filter((p) => !declared.includes(p.id)).map((p) => p.id);
|
|
1288
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);
|
|
1289
1423
|
if (JSON_MODE) { jsonOk(result); return; }
|
|
1290
1424
|
console.log(`workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)}) local ${shortPath(ctx.localPath)}\n`);
|
|
1291
1425
|
if (standalone) console.log(` (${standaloneNote(discovery)})\n`);
|
|
@@ -1294,14 +1428,51 @@ async function workspaceCmd() {
|
|
|
1294
1428
|
for (const m of members.filter((m) => !m.confirmed)) console.log(` ${m.name}: ${m.detail}`);
|
|
1295
1429
|
console.log("\nPackages:");
|
|
1296
1430
|
if (!packages.length) console.log(unsynced.length ? ` (none locked yet — \`oats sync\` resolves ${unsynced.join(", ")})` : " (none)");
|
|
1297
|
-
else printTable(["package", "version", "source", "commit", "capabilities"], packages.map((p) => [p.id, p.version, p.source, short(p.commit), p.capabilities.join(",")]));
|
|
1431
|
+
else printTable(["package", "version", "source", "commit", "capabilities", "souls"], packages.map((p) => [p.id, p.version, p.source, short(p.commit), p.capabilities.join(","), p.souls.join(",") || "—"]));
|
|
1298
1432
|
if (packages.length && unsynced.length) console.log(` declared but not locked (run \`oats sync\`): ${unsynced.join(", ")}`);
|
|
1299
1433
|
if (stale.length) console.log(` locked but no longer declared (run \`oats sync\`): ${stale.join(", ")}`);
|
|
1300
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
|
+
}
|
|
1301
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}`); }
|
|
1302
1440
|
if (discovery.warnings?.length) { console.log("\nWarnings:"); for (const w of discovery.warnings) console.log(` ${w.code} ${w.message}`); }
|
|
1303
1441
|
}
|
|
1304
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
|
+
|
|
1305
1476
|
/** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
|
|
1306
1477
|
async function itemsCmd(kind) {
|
|
1307
1478
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
@@ -1310,6 +1481,10 @@ async function itemsCmd(kind) {
|
|
|
1310
1481
|
let lock;
|
|
1311
1482
|
try { lock = readLock(ctx.deploymentDir); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
|
|
1312
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);
|
|
1313
1488
|
const standalone = discovery.standalone === true;
|
|
1314
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; }
|
|
1315
1490
|
console.log(`${kind} of workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)})${standalone ? ` — ${standaloneNote(discovery)}` : ""}\n`);
|
|
@@ -1320,6 +1495,60 @@ async function itemsCmd(kind) {
|
|
|
1320
1495
|
if (kind === "capabilities" && unsynced.length) console.log(`\n package capabilities of ${unsynced.join(", ")} appear after \`oats sync\``);
|
|
1321
1496
|
}
|
|
1322
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
|
+
|
|
1323
1552
|
// ---------- roster: status / spawn / retire / create ----------
|
|
1324
1553
|
/** Workspace drift for `oats status` (decision 17: shown, not prevented). ONE discovery over
|
|
1325
1554
|
* the remotes serves every instance; `driftOf` compares each instance's recorded modules to
|
|
@@ -1343,8 +1572,8 @@ async function statusDrift(data) {
|
|
|
1343
1572
|
const souls = new Map();
|
|
1344
1573
|
for (const a of data) {
|
|
1345
1574
|
if (!a.dir) continue;
|
|
1346
|
-
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 }); }
|
|
1347
|
-
catch { /* not a workspace soul (classic,
|
|
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 } : {}) }); }
|
|
1576
|
+
catch { /* not a workspace soul (classic, or an unreadable stamp) */ }
|
|
1348
1577
|
}
|
|
1349
1578
|
const anything = data.some((a) => (a.instances || []).some((i) => hasModules(i) || hasSoul(i)));
|
|
1350
1579
|
if (!anything) return { drift: new Map(), soul: new Map(), souls, unreachable: null };
|
|
@@ -1354,7 +1583,7 @@ async function statusDrift(data) {
|
|
|
1354
1583
|
let discovery;
|
|
1355
1584
|
// The standalone view (decisions 10/25) is a discovery too: drift of a standalone
|
|
1356
1585
|
// instance is computed against its member's current state, not reported "unreachable".
|
|
1357
|
-
try { const { discoverOrStandalone } = await import("../lib/instance-resolution.mjs"); discovery = await discoverOrStandalone(ctx.local, { remoteOptions: remoteOptionsFromEnv() }); }
|
|
1586
|
+
try { const { discoverOrStandalone } = await import("../lib/instance-resolution.mjs"); discovery = await discoverOrStandalone(ctx.local, { lock, remoteOptions: remoteOptionsFromEnv() }); }
|
|
1358
1587
|
catch (e) {
|
|
1359
1588
|
const reason = e?.details?.reason ? `${e.code}: ${e.details.reason}` : (e?.code || e?.message || "unknown");
|
|
1360
1589
|
return { drift: new Map(), soul: new Map(), souls, unreachable: { code: e?.code ?? null, reason, message: e?.message ?? String(e) } };
|
|
@@ -1369,6 +1598,7 @@ async function statusDrift(data) {
|
|
|
1369
1598
|
}
|
|
1370
1599
|
// The roster's soul row reflects the CURRENT member commit too (the pointer may lag a moved member).
|
|
1371
1600
|
for (const [, stamp] of souls) {
|
|
1601
|
+
if (typeof stamp.package === "string") { const now = (discovery.packageSouls || []).find((p) => p.package === stamp.package && p.path === stamp.path); if (now) stamp.current = now.commit; continue; }
|
|
1372
1602
|
const member = discovery.members.find((m) => m && m.key === stamp.repoKey);
|
|
1373
1603
|
if (member && typeof member.commit === "string" && (member.confirmed || (discovery.standalone === true && member.key === discovery.key))) stamp.current = member.commit;
|
|
1374
1604
|
}
|
|
@@ -1385,6 +1615,12 @@ function driftLine(row) {
|
|
|
1385
1615
|
}
|
|
1386
1616
|
/** The `soul:` line of an instance — decision 17 for the soul source. */
|
|
1387
1617
|
function soulDriftLine(row, agentName) {
|
|
1618
|
+
if (row.package) {
|
|
1619
|
+
const base = `soul: ${row.name ?? agentName} from package ${row.package} v${row.version} @ ${short7(row.commit)}`;
|
|
1620
|
+
if (row.status === "moved") return `${base} [package moved since (now v${row.current?.version} @ ${short7(row.current?.commit)})]`;
|
|
1621
|
+
if (row.status === "missing") return `${base} [${row.reason === "soul-absent" ? "soul no longer shipped by the package" : "package no longer locked"}]`;
|
|
1622
|
+
return base;
|
|
1623
|
+
}
|
|
1388
1624
|
const base = `soul: ${row.name ?? agentName} from ${memberLabel(row.repoKey)} @ ${short7(row.commit)}`;
|
|
1389
1625
|
if (row.status === "moved") return `${base} [member moved since (now @ ${short7(row.current?.commit)})]`;
|
|
1390
1626
|
if (row.status === "missing") return `${base} [${row.reason === "soul-absent" ? "soul no longer present" : `member ${row.reason || "unconfirmed"}`}]`;
|
|
@@ -1394,6 +1630,7 @@ function soulDriftLine(row, agentName) {
|
|
|
1394
1630
|
function soulRepoLabel(a, ws) {
|
|
1395
1631
|
const stamp = ws?.souls?.get(a.name);
|
|
1396
1632
|
if (!stamp) return a.repo || "?";
|
|
1633
|
+
if (typeof stamp.package === "string") return `package ${stamp.package} v${stamp.version} @ ${short7(stamp.commit)}`;
|
|
1397
1634
|
const moved = stamp.current && stamp.commit && stamp.current !== stamp.commit ? ` (member now @ ${short7(stamp.current)})` : "";
|
|
1398
1635
|
return `${memberLabel(stamp.repoKey)} @ ${short7(stamp.commit)}${moved}`;
|
|
1399
1636
|
}
|
|
@@ -1417,7 +1654,7 @@ async function status() {
|
|
|
1417
1654
|
const rows = ws.drift.get(key);
|
|
1418
1655
|
if (rows) i.modules = rows.map((r) => ({ name: r.module, from: r.from, commit: r.recorded?.commit ?? null, current: r.current, status: r.status, ...(r.reason ? { reason: r.reason } : {}) }));
|
|
1419
1656
|
const s = ws.soul.get(key);
|
|
1420
|
-
if (s) i.soul = { repoKey: s.repoKey, commit: s.commit, current: s.current?.commit ?? null, status: s.status, ...(s.reason ? { reason: s.reason } : {}) };
|
|
1657
|
+
if (s) i.soul = { repoKey: s.repoKey, commit: s.commit, current: s.current?.commit ?? null, status: s.status, ...(s.reason ? { reason: s.reason } : {}), ...(s.package ? { package: s.package, version: s.version, currentVersion: s.current?.version ?? null } : {}) };
|
|
1421
1658
|
}
|
|
1422
1659
|
}
|
|
1423
1660
|
console.log(JSON.stringify({ root, agents: data, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}), ...(problems.length ? { problems } : {}), ...envelopeWarnings() }, null, 2)); return;
|
|
@@ -1499,16 +1736,16 @@ async function spawnCmd() {
|
|
|
1499
1736
|
// the per-commit cache, or fetches it to a temporary copy (reported as soulFetched).
|
|
1500
1737
|
const providerPairs = [];
|
|
1501
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; }
|
|
1502
|
-
let wsPrepared, soulFetched = false,
|
|
1739
|
+
let wsPrepared, soulFetched = false, wsDiscovery;
|
|
1503
1740
|
{
|
|
1504
1741
|
let discovery = null;
|
|
1505
1742
|
try {
|
|
1506
|
-
const { prepareInstance, ensureWorkspaceSoul, previewWorkspaceSoul, parseProviderFlags, discoverOrStandalone } = await import("../lib/instance-resolution.mjs");
|
|
1743
|
+
const { prepareInstance, ensureWorkspaceSoul, previewWorkspaceSoul, parseProviderFlags, discoverOrStandalone, agentDirOf } = await import("../lib/instance-resolution.mjs");
|
|
1507
1744
|
const remoteOptions = remoteOptionsFromEnv();
|
|
1508
|
-
const { local } = loadLocal(dirFlag());
|
|
1509
|
-
discovery = wsDiscovery = await discoverOrStandalone(local, { remoteOptions });
|
|
1745
|
+
const { local, path: localPath } = loadLocal(dirFlag());
|
|
1746
|
+
discovery = wsDiscovery = await discoverOrStandalone(local, { deployment: dirname(localPath), remoteOptions });
|
|
1510
1747
|
wsPrepared = await prepareInstance(dirFlag(), name, { spawn: { providers: parseProviderFlags(providerPairs) }, remoteOptions, discovery });
|
|
1511
|
-
const soulName = wsPrepared.soulEntry
|
|
1748
|
+
const soulName = agentDirOf(wsPrepared.soulEntry);
|
|
1512
1749
|
let soulDir;
|
|
1513
1750
|
if (isPreview) {
|
|
1514
1751
|
// A preview writes nothing in the deployment: the soul comes from the
|
|
@@ -1534,50 +1771,12 @@ async function spawnCmd() {
|
|
|
1534
1771
|
const file = officialCatalogFile();
|
|
1535
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 });
|
|
1536
1773
|
}
|
|
1537
|
-
|
|
1538
|
-
// soul, resolved below from a materialized copy) may still answer to this name.
|
|
1539
|
-
if (e?.code === "E_SOUL_UNKNOWN" && !isPreview) { wsSoulUnknown = e; }
|
|
1540
|
-
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);
|
|
1541
1775
|
else throw e;
|
|
1542
1776
|
}
|
|
1543
1777
|
}
|
|
1544
1778
|
if (agentsRootFlag !== undefined && !agent) bail("E_SOUL_UNKNOWN", `soul "${name}" is not at agents root ${String(agentsRootFlag)}`);
|
|
1545
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"})`);
|
|
1546
|
-
// Capability agent (lead decision c3 Q1): a prepared spawn of its providing module only.
|
|
1547
|
-
let capabilityPrepared, capabilityPkg = null;
|
|
1548
|
-
if (!agent) {
|
|
1549
|
-
// Workspace model: the agent is declared by a capability some INSTANCE already
|
|
1550
|
-
// materialized (the --parent home first, then any home under this root) —
|
|
1551
|
-
// OKF's memory-harvest worker spawned by a knowledge source, for example.
|
|
1552
|
-
const anchorName = flag("parent") || flag("relative-to");
|
|
1553
|
-
const anchorHome = anchorName ? (findInstanceHome(root, String(anchorName)) ?? null) : null;
|
|
1554
|
-
let modAgent;
|
|
1555
|
-
try { modAgent = findModuleCapabilityAgent(root, name, { anchorHome }); }
|
|
1556
|
-
catch (e) { bail(e.code || "E_CAPABILITY_BROKEN", e.message, e.details); }
|
|
1557
|
-
if (modAgent) {
|
|
1558
|
-
agent = modAgent;
|
|
1559
|
-
note(`(capability agent: "${name}" from ${modAgent.capability}, materialized in ${shortPath(modAgent._manifestSource)} — fresh soul, instances home under ${shortPath(join(root, name, "instances"))})`);
|
|
1560
|
-
} else {
|
|
1561
|
-
// No instance carries it: resolve from the deployment's LOCK — a locked
|
|
1562
|
-
// package whose capability declares agents/<name> is fetched into the
|
|
1563
|
-
// deployment's module store and read from there.
|
|
1564
|
-
try {
|
|
1565
|
-
const { resolvePackageCapabilityAgent } = await import("../lib/instance-resolution.mjs");
|
|
1566
|
-
const hit = await resolvePackageCapabilityAgent(dirFlag(), name, { remoteOptions: remoteOptionsFromEnv(), discovery: wsDiscovery, catalog: (() => { try { return officialPackageCatalog(); } catch { return null; } })() });
|
|
1567
|
-
if (hit) {
|
|
1568
|
-
agent = capabilityAgentFromDir(hit.dir, name, root, { module: { from: { kind: "package", package: hit.package, version: hit.version, commit: hit.commit } } });
|
|
1569
|
-
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"))})`); }
|
|
1570
|
-
}
|
|
1571
|
-
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
1572
|
-
}
|
|
1573
|
-
if (agent) {
|
|
1574
|
-
try {
|
|
1575
|
-
const { prepareCapabilityAgent } = await import("../lib/instance-resolution.mjs");
|
|
1576
|
-
capabilityPrepared = await prepareCapabilityAgent(dirFlag(), agent, { discovery: wsDiscovery, pkg: capabilityPkg, remoteOptions: remoteOptionsFromEnv(), catalog: (() => { try { return officialPackageCatalog(); } catch { return null; } })() });
|
|
1577
|
-
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
if (!agent && wsSoulUnknown) bail(wsSoulUnknown.code, wsSoulUnknown.message, wsSoulUnknown.details);
|
|
1581
1780
|
checkDirectoryOptions(requestedWork || agent?.work);
|
|
1582
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\``);
|
|
1583
1782
|
for (const information of agent.notes || []) note(`[${information.code}] ${information.message}`);
|
|
@@ -1606,9 +1805,6 @@ async function spawnCmd() {
|
|
|
1606
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");
|
|
1607
1806
|
// NOTE: explicit "unrelated" is passed through to the kernel.
|
|
1608
1807
|
if (relativeTo && relation !== "unrelated") {
|
|
1609
|
-
// findInstanceHome also sees capability-defined agents' instance homes
|
|
1610
|
-
// (<root>/<name>/ without a soul) — e.g. a reviewer passing
|
|
1611
|
-
// --parent "$OATS_INSTANCE" from a capability agent.
|
|
1612
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`);
|
|
1613
1809
|
}
|
|
1614
1810
|
const taskText = flag("task");
|
|
@@ -1616,6 +1812,17 @@ async function spawnCmd() {
|
|
|
1616
1812
|
const taskFileFlag = flag("task-file");
|
|
1617
1813
|
if (taskFileFlag === true) bail("E_BAD_ARGS", "--task-file needs a path");
|
|
1618
1814
|
if (taskFileFlag && !existsSync(taskFileFlag)) bail("E_BAD_ARGS", `--task-file not found: ${taskFileFlag}`);
|
|
1815
|
+
// --trigger-event <file>: the event a trigger spawns this instance for (lib/triggers.mjs). It is
|
|
1816
|
+
// copied to <home>/.oats/trigger-event.json (OATS_TRIGGER_EVENT_FILE) and recorded as instance.json.trigger.
|
|
1817
|
+
let triggerEvent;
|
|
1818
|
+
{ const f = flag("trigger-event");
|
|
1819
|
+
if (f === true) bail("E_BAD_ARGS", "--trigger-event needs a path");
|
|
1820
|
+
if (f !== undefined) {
|
|
1821
|
+
if (!existsSync(f)) bail("E_BAD_ARGS", `--trigger-event not found: ${f}`);
|
|
1822
|
+
try { triggerEvent = JSON.parse(readFileSync(f, "utf8")); } catch (e) { bail("E_BAD_ARGS", `--trigger-event is not valid JSON: ${e.message}`); }
|
|
1823
|
+
const ok = triggerEvent && typeof triggerEvent === "object" && !Array.isArray(triggerEvent) && typeof triggerEvent.trigger === "string" && typeof triggerEvent.source === "string" && typeof triggerEvent.repo === "string" && Number.isInteger(triggerEvent.number) && typeof triggerEvent.event === "string";
|
|
1824
|
+
if (!ok) bail("E_BAD_ARGS", "--trigger-event must hold { trigger, source, repo, number, url, event, headSha, labels, observedAt }");
|
|
1825
|
+
} }
|
|
1619
1826
|
const relativeRoot = flag("relative-root");
|
|
1620
1827
|
if (relativeRoot !== undefined && (relativeRoot === true || !String(relativeRoot).trim())) bail("E_BAD_ARGS", "--relative-root needs an agents-root path");
|
|
1621
1828
|
if (relativeRoot && !relativeTo) bail("E_BAD_ARGS", "--relative-root only qualifies --relative-to/--parent");
|
|
@@ -1657,10 +1864,10 @@ async function spawnCmd() {
|
|
|
1657
1864
|
// convention <deployment>/<member name>. Never an ambient Git checkout around the
|
|
1658
1865
|
// deployment. Resolved once here so a preview sees exactly what the apply would.
|
|
1659
1866
|
let preparedRepo;
|
|
1660
|
-
if (wsPrepared
|
|
1867
|
+
if (wsPrepared) {
|
|
1661
1868
|
try {
|
|
1662
1869
|
const { toCapabilityRows, modulesPreview, requireMemberClone } = await import("../lib/instance-resolution.mjs");
|
|
1663
|
-
prepared = wsPrepared
|
|
1870
|
+
prepared = wsPrepared;
|
|
1664
1871
|
prepared.capabilityRows = []; // filled after materialization (paths live in the home); preview uses modulesPreview
|
|
1665
1872
|
prepared.preview = modulesPreview(prepared.resolution, root, agent.name);
|
|
1666
1873
|
prepared.toCapabilityRows = toCapabilityRows;
|
|
@@ -1684,6 +1891,7 @@ async function spawnCmd() {
|
|
|
1684
1891
|
work: requestedWork, workDir, harness: harnessFlag(), backend, herdrSocket, yolo, model: flag("model"), branch,
|
|
1685
1892
|
launchConfig: valueFlag("launch-config"),
|
|
1686
1893
|
launch: !args.includes("--no-launch"),
|
|
1894
|
+
...(triggerEvent ? { triggerEvent } : {}),
|
|
1687
1895
|
// K6: --preview decides everything and touches nothing; --base <ref>
|
|
1688
1896
|
// selects a worktree's start point; --model @native-default is the
|
|
1689
1897
|
// explicit "harness's own default" (distinct from omitting --model).
|
|
@@ -1723,7 +1931,7 @@ async function spawnCmd() {
|
|
|
1723
1931
|
if (e?.code === "E_IDEMPOTENCY_CONFLICT") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
|
|
1724
1932
|
if (e?.code === "E_PLACEMENT_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
|
|
1725
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; }
|
|
1726
|
-
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; }
|
|
1727
1935
|
if (e?.code === "E_SPAWN_INCOMPLETE") { bail(e.code, e.message, { instance: e.instance, home: e.home, launched: e.launched }); throw e; }
|
|
1728
1936
|
bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
|
|
1729
1937
|
}
|
|
@@ -1876,7 +2084,7 @@ function retireCmd() {
|
|
|
1876
2084
|
/** `oats schedule ...`: workspace-scoped definitions, host-owned execution
|
|
1877
2085
|
* (lib/schedule.mjs). Every subcommand answers the envelope; nothing here
|
|
1878
2086
|
* launches unless a job is due or run-now is asked. */
|
|
1879
|
-
function scheduleCmd() {
|
|
2087
|
+
async function scheduleCmd() {
|
|
1880
2088
|
const sub = args[1];
|
|
1881
2089
|
const id = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
|
|
1882
2090
|
// One schedule-owning scope for a directory: its deployment (the directory
|
|
@@ -1885,7 +2093,11 @@ function scheduleCmd() {
|
|
|
1885
2093
|
let scope;
|
|
1886
2094
|
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
1887
2095
|
const io = { hostStatus: () => hostUnitStatus() };
|
|
1888
|
-
|
|
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)); };
|
|
1889
2101
|
const readSpec = () => {
|
|
1890
2102
|
const inline = flag("spec-json");
|
|
1891
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" }); } }
|
|
@@ -1897,20 +2109,41 @@ function scheduleCmd() {
|
|
|
1897
2109
|
const needId = () => { if (!id) throw scheduleError("E_BAD_ARGS", `oats schedule ${sub} <id>`); return id; };
|
|
1898
2110
|
try {
|
|
1899
2111
|
switch (sub) {
|
|
1900
|
-
case "list":
|
|
1901
|
-
case "show": return out({ schedule: describeSchedule(ws(), needId(), io) });
|
|
1902
|
-
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
|
+
}
|
|
1903
2128
|
case "update": return out({ schedule: updateSchedule(ws(), needId(), readSpec(), io) });
|
|
1904
|
-
case "enable":
|
|
1905
|
-
case "disable":
|
|
1906
|
-
|
|
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() }));
|
|
1907
2139
|
case "remove": return out(removeSchedule(ws(), needId(), { force: args.includes("--force") }));
|
|
1908
|
-
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() }));
|
|
1909
2141
|
case "tick": {
|
|
1910
2142
|
const dryRun = args.includes("--dry-run");
|
|
1911
2143
|
if (args.includes("--host")) return out(tickHost({ io, dryRun }));
|
|
1912
2144
|
const reg = readRegistry();
|
|
1913
|
-
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 })]);
|
|
1914
2147
|
return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws(), io) });
|
|
1915
2148
|
}
|
|
1916
2149
|
case "host": {
|
|
@@ -1920,7 +2153,7 @@ function scheduleCmd() {
|
|
|
1920
2153
|
if (op === "status") return out({ scheduler: schedulerStatus(ws(), io) });
|
|
1921
2154
|
throw scheduleError("E_BAD_ARGS", "oats schedule host install|uninstall|status");
|
|
1922
2155
|
}
|
|
1923
|
-
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]");
|
|
1924
2157
|
}
|
|
1925
2158
|
} catch (e) {
|
|
1926
2159
|
// K8b: typed refusal details travel (identity mismatch: key/declared; a refused file: its integrity source).
|
|
@@ -1929,6 +2162,101 @@ function scheduleCmd() {
|
|
|
1929
2162
|
}
|
|
1930
2163
|
}
|
|
1931
2164
|
|
|
2165
|
+
/** `oats trigger …` (lib/triggers.mjs): event-driven spawns stored beside the schedules of the
|
|
2166
|
+
* deployment scope and evaluated by the same host tick. Every subcommand answers the envelope. */
|
|
2167
|
+
async function triggerCmd() {
|
|
2168
|
+
const sub = args[1];
|
|
2169
|
+
const id = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
|
|
2170
|
+
const T = await import("../lib/triggers.mjs");
|
|
2171
|
+
let scope;
|
|
2172
|
+
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
2173
|
+
const out = (result, text) => { if (JSON_MODE) jsonOk(result); else console.log(text ? text(result) : JSON.stringify(result, null, 2)); };
|
|
2174
|
+
const needId = () => { if (!id) throw T.triggerError("E_BAD_ARGS", `oats trigger ${sub} <id>`); return id; };
|
|
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";
|
|
2182
|
+
try {
|
|
2183
|
+
switch (sub) {
|
|
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)");
|
|
2197
|
+
case "test": {
|
|
2198
|
+
let workspaceTeams = null;
|
|
2199
|
+
try {
|
|
2200
|
+
const { observeWorkspace } = await import("../lib/workspace.mjs");
|
|
2201
|
+
const { local } = loadLocal(ws());
|
|
2202
|
+
if (typeof local.workspace === "string") workspaceTeams = Object.keys((await observeWorkspace(local.workspace, { remoteOptions: remoteOptionsFromEnv() })).workspace.teams || {});
|
|
2203
|
+
} catch { /* reported as unknown (null) */ }
|
|
2204
|
+
return out(T.testTrigger(ws(), needId(), { workspaceTeams, ctx: ctx() }), (r) => [
|
|
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)"}`,
|
|
2207
|
+
` gh auth ${r.gh.ok ? `ok — ${r.gh.account ?? "?"} via ${r.gh.credentialSource}` : `FAILED${r.gh.detail ? ` — ${r.gh.detail}` : ""}`}`,
|
|
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}`}`,
|
|
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}`}`,
|
|
2210
|
+
` teams ${r.teams.requested.join(", ") || "(none)"}${r.teams.undeclared?.length ? ` undeclared: ${r.teams.undeclared.join(", ")}` : ""}`,
|
|
2211
|
+
` would fire ${r.wouldFire.length ? r.wouldFire.map((w) => `${w.event} #${w.number}${w.held ? ` (held: ${w.held})` : ""}`).join(", ") : "nothing now"}`,
|
|
2212
|
+
...r.problems.map((p) => ` problem ${p}`),
|
|
2213
|
+
...(r.warnings ?? []).map((w) => ` warning ${w}`),
|
|
2214
|
+
].join("\n"));
|
|
2215
|
+
}
|
|
2216
|
+
case "add": {
|
|
2217
|
+
const file = flag("file"), from = flag("from");
|
|
2218
|
+
if ((file === undefined) === (from === undefined)) throw T.triggerError("E_BAD_ARGS", `oats trigger add needs exactly one of --file <trigger.json> or --from <package>:<template>\n${usage}`);
|
|
2219
|
+
const sets = {};
|
|
2220
|
+
for (let i = 0; i < args.length; i++) if (args[i] === "--set") {
|
|
2221
|
+
const kv = args[i + 1];
|
|
2222
|
+
if (typeof kv !== "string" || !kv.includes("=")) throw T.triggerError("E_BAD_ARGS", "--set needs <name>=<value>");
|
|
2223
|
+
sets[kv.slice(0, kv.indexOf("="))] = kv.slice(kv.indexOf("=") + 1); i++;
|
|
2224
|
+
}
|
|
2225
|
+
const idFlag = flag("id");
|
|
2226
|
+
if (idFlag === true) throw T.triggerError("E_BAD_ARGS", "--id needs a trigger id");
|
|
2227
|
+
let spec;
|
|
2228
|
+
if (file !== undefined) {
|
|
2229
|
+
if (file === true || !existsSync(file)) throw T.triggerError("E_BAD_ARGS", `--file ${file === true ? "needs a path" : `not found: ${file}`}`);
|
|
2230
|
+
if (Object.keys(sets).length) throw T.triggerError("E_BAD_ARGS", "--set fills a package template's parameters; with --file, edit the file");
|
|
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" } }); }
|
|
2232
|
+
if (idFlag !== undefined) spec.id = idFlag;
|
|
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);
|
|
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)`);
|
|
2245
|
+
}
|
|
2246
|
+
default: throw T.triggerError("E_BAD_ARGS", usage);
|
|
2247
|
+
}
|
|
2248
|
+
} catch (e) {
|
|
2249
|
+
const details = { ...(e.details && typeof e.details === "object" ? e.details : {}), ...(e.field !== undefined ? { field: e.field } : {}) };
|
|
2250
|
+
if (JSON_MODE) jsonFail(e.code || "E_TRIGGER_FAILED", e.message, Object.keys(details).length ? details : undefined); else die(e.message);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
/** A package trigger template (`triggers: [{ id, file }]` in the locked package's oats-package.json),
|
|
2254
|
+
* read at the lock's commit and instantiated with the --set parameters. */
|
|
2255
|
+
async function triggerFromPackage(T, from, sets, idFlag) {
|
|
2256
|
+
const { bindRemote } = await import("../lib/packages.mjs");
|
|
2257
|
+
return T.packageTriggerTemplate(readLock(scheduleScopeOf(dirFlag())), from, sets, { id: idFlag, remote: bindRemote(remoteModule, remoteOptionsFromEnv()) });
|
|
2258
|
+
}
|
|
2259
|
+
|
|
1932
2260
|
async function sessionCmd() {
|
|
1933
2261
|
try {
|
|
1934
2262
|
if (flag("native-record") !== undefined) throw Object.assign(new Error("--native-record outcome inspection is gone (the captured/portable path was removed in 0.26)"), { code: "E_BAD_ARGS" });
|
|
@@ -2175,7 +2503,7 @@ async function capabilityCommand() {
|
|
|
2175
2503
|
// No home, so no recorded soul: the soul's per-commit copy is OATS_SOUL when a spawn
|
|
2176
2504
|
// already fetched exactly this commit; otherwise the command gets none (never ambient).
|
|
2177
2505
|
const cachedSoul = hit.soul?.commit ? join(hit.deployment, "agents", hit.soul.name, "souls", String(hit.soul.commit).slice(0, 12)) : null;
|
|
2178
|
-
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);
|
|
2179
2507
|
}
|
|
2180
2508
|
|
|
2181
2509
|
async function dispatch() {
|
|
@@ -2189,14 +2517,14 @@ async function capabilityCommand() {
|
|
|
2189
2517
|
// braces: the ids come from instance.json, which spawn wrote from resolved
|
|
2190
2518
|
// manifests. Null-prototype because the dispatcher indexes it with the
|
|
2191
2519
|
// namespace the operator typed on the command line.
|
|
2192
|
-
let capSettings = Object.create(null);
|
|
2520
|
+
let capSettings = Object.create(null), capOrigins = Object.create(null);
|
|
2193
2521
|
let deployment = null;
|
|
2194
2522
|
let soulDir;
|
|
2195
2523
|
try {
|
|
2196
2524
|
if (metaFile && existsSync(metaFile)) {
|
|
2197
2525
|
const meta = JSON.parse(readFileSync(metaFile, "utf8"));
|
|
2198
2526
|
activeIds = (meta.capabilities || []).map((c) => c.id);
|
|
2199
|
-
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 || {}; }
|
|
2200
2528
|
// Workspace-model homes only (lead decision c3 Q2).
|
|
2201
2529
|
if (isCapturedHome(meta)) { const e = capturedHomeRefusal(instanceHome, "nothing was dispatched"); bail(e.code, e.message, e.details); }
|
|
2202
2530
|
if (!isWorkspaceHome(meta)) { const e = preWorkspaceHome(instanceHome, "nothing was dispatched"); bail(e.code, e.message); }
|
|
@@ -2235,14 +2563,14 @@ async function capabilityCommand() {
|
|
|
2235
2563
|
const live = await liveTeams(instanceHome, homeMeta.meta, { remoteOptions: remoteOptionsFromEnv() });
|
|
2236
2564
|
teamCtx = homeTeamCtx(live.teams, live.source);
|
|
2237
2565
|
}
|
|
2238
|
-
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);
|
|
2239
2567
|
}
|
|
2240
2568
|
|
|
2241
2569
|
/** Help / unknown-command / spec validation / exec — shared by every context.
|
|
2242
2570
|
* `m` is the manifest (with `capability`; `_dir` may be absent until `ensureDir`
|
|
2243
2571
|
* resolves the directory holding the executable — the operator branch fetches
|
|
2244
2572
|
* the module tree only when a command is actually going to run). */
|
|
2245
|
-
async function runManifestCommand(m, settings, teamCtx, ensureDir, soulDir) {
|
|
2573
|
+
async function runManifestCommand(m, { settings, origins }, teamCtx, ensureDir, soulDir) {
|
|
2246
2574
|
const sub = args[1];
|
|
2247
2575
|
const cmds = Object.keys(m.commands);
|
|
2248
2576
|
// `oats <ns> --help` and `oats <ns> <cmd> --help` answer from the manifest
|
|
@@ -2279,7 +2607,7 @@ async function capabilityCommand() {
|
|
|
2279
2607
|
// OATS_SOUL is the recorded soul or nothing: an ambient value inherited from the
|
|
2280
2608
|
// invoking process names some other soul (a coordinator's own), never this one.
|
|
2281
2609
|
const { OATS_SOUL: _ambientSoul, ...inherited } = process.env;
|
|
2282
|
-
const r = spawnSync("node", [abs, ...rest, ...
|
|
2610
|
+
const r = spawnSync("node", [abs, ...rest, ...rawArgs.slice(2)], { stdio: "inherit", env: {
|
|
2283
2611
|
...inherited, OATS_CAPABILITY: m.capability,
|
|
2284
2612
|
// Package-runtime boundary: dispatched commands receive the active
|
|
2285
2613
|
// capability's EFFECTIVE settings (instance snapshot, resolved context, or
|
|
@@ -2287,6 +2615,8 @@ async function capabilityCommand() {
|
|
|
2287
2615
|
// lifecycle hooks — capabilities read their settings here instead of
|
|
2288
2616
|
// importing the kernel resolver.
|
|
2289
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 || {}),
|
|
2290
2620
|
// PATH is not a trusted runtime boundary (maintainer finding 1): pass the
|
|
2291
2621
|
// canonical absolute executable of THIS CLI; official consumers execFile
|
|
2292
2622
|
// it directly and never resolve `oats` from PATH or a shell.
|
|
@@ -2356,7 +2686,7 @@ function versionCmd() {
|
|
|
2356
2686
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
2357
2687
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
2358
2688
|
// never listed.
|
|
2359
|
-
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"], 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 }));
|
|
2360
2690
|
return;
|
|
2361
2691
|
}
|
|
2362
2692
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -2495,7 +2825,7 @@ async function serverRouteCmd() {
|
|
|
2495
2825
|
// so its explicit --dir travels; every other routed command takes its
|
|
2496
2826
|
// scope from the registration.
|
|
2497
2827
|
const explicitScopeOk = ["inspect", "operation", "launch-config"].includes(cmd);
|
|
2498
|
-
if (!explicitScopeOk &&
|
|
2828
|
+
if (!explicitScopeOk && flag("dir") !== undefined) bail("E_BAD_ARGS", "--dir cannot be combined with --server: the remote workspace comes from the server registration");
|
|
2499
2829
|
if (cmd === "launch-config") {
|
|
2500
2830
|
const action = args[1];
|
|
2501
2831
|
const value = (name) => { const v = flag(name); if (v === true) bail("E_BAD_ARGS", `--${name} needs a value`); return v; };
|
|
@@ -2715,13 +3045,16 @@ try {
|
|
|
2715
3045
|
// resolved against the current context instead. `version` answers regardless:
|
|
2716
3046
|
// host protocol negotiation describes this executable.
|
|
2717
3047
|
{
|
|
3048
|
+
// The kernel's own argv only: a capability command's flags are its provider's to parse.
|
|
3049
|
+
const kernelArgv = (KERNEL_COMMANDS.has(cmd) && !OWN_ARGV_COMMANDS.has(cmd)) || (ROUTED_COMMANDS.has(cmd) && flag("server") !== undefined);
|
|
3050
|
+
if (argvProblem && kernelArgv) cmdFail("E_BAD_ARGS", argvProblem);
|
|
2718
3051
|
const end = args.indexOf("--"), head = end < 0 ? args : args.slice(0, end);
|
|
2719
|
-
const selector = head.find((a) => /^--(deployment|resolution|artifact-set)
|
|
3052
|
+
const selector = head.find((a) => /^--(deployment|resolution|artifact-set)$/.test(a));
|
|
2720
3053
|
const refuse = (message, details) => { if (JSON_MODE) jsonFail("E_UNSUPPORTED_MODE", message, details); die(message); };
|
|
2721
|
-
if (selector) refuse(`${selector
|
|
3054
|
+
if (selector) refuse(`${selector}: a captured selector is refused (the captured/portable path was removed in 0.26); run the command in its workspace deployment or instance home instead`, { selector });
|
|
2722
3055
|
const inherited = ["OATS_RESOLUTION", "OATS_DEPLOYMENT"].filter((k) => process.env[k]);
|
|
2723
3056
|
if (inherited.length && cmd !== "version") refuse(`this environment carries a captured context (${inherited.join(", ")}): the captured/portable path was removed in 0.26, and nothing is run against the current context in its place — retire the captured home and re-spawn it from the deployment`, { inherited });
|
|
2724
|
-
if (cmd === "inspect" && head.
|
|
3057
|
+
if (cmd === "inspect" && head.includes("--request")) {
|
|
2725
3058
|
const message = "oats inspect --request (portable onboarding inspection) is gone (the captured/portable path was removed in 0.26); use `oats onboard` / `oats sync` to set up a workspace and `oats spawn <soul> --preview` to see what a spawn would resolve";
|
|
2726
3059
|
if (JSON_MODE) jsonFail("E_UNKNOWN_COMMAND", message, { removed: "inspect --request", replacement: "oats onboard / oats sync; oats spawn --preview" });
|
|
2727
3060
|
die(message);
|
|
@@ -2738,7 +3071,7 @@ else {
|
|
|
2738
3071
|
// `okf harvest --help` spawned a harvester (BeadHub, 2026-09-05).
|
|
2739
3072
|
const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
|
|
2740
3073
|
if (cmd && KERNEL_COMMANDS.has(cmd) && wantsHelp) { if (JSON_MODE) { jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); process.exit(0); } usageFor(cmd); process.exit(0); }
|
|
2741
|
-
if (flag("server") !== undefined &&
|
|
3074
|
+
if (flag("server") !== undefined && ROUTED_COMMANDS.has(cmd)) await serverRouteCmd();
|
|
2742
3075
|
else if (cmd === "server") serverCmd();
|
|
2743
3076
|
else if (cmd === "inspect") await inspectCmd();
|
|
2744
3077
|
else if (cmd === "operation") await operationCmd();
|
|
@@ -2767,7 +3100,9 @@ else if (cmd === "version" || cmd === "--version" || cmd === "-v") versionCmd();
|
|
|
2767
3100
|
// Same rule as the inner catch: a typed CLI failure surfaces with its own code
|
|
2768
3101
|
// through the shared boundary, never re-badged as a spawn-mechanism failure.
|
|
2769
3102
|
else if (cmd === "session") await sessionCmd();
|
|
2770
|
-
else if (cmd === "schedule") scheduleCmd();
|
|
3103
|
+
else if (cmd === "schedule") await scheduleCmd();
|
|
3104
|
+
else if (cmd === "trigger") await triggerCmd();
|
|
3105
|
+
else if (cmd === "automations") await automationsCmd();
|
|
2771
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; } }
|
|
2772
3107
|
else if (cmd === "retire") retireCmd();
|
|
2773
3108
|
else if (cmd === "capture" || cmd === "recall" || cmd === "setup") await recordCmd(cmd);
|
|
@@ -2856,11 +3191,22 @@ Usage:
|
|
|
2856
3191
|
next steps (clone members you work IN, spawn
|
|
2857
3192
|
oats-operator-expert); creates no soul, spawns nothing
|
|
2858
3193
|
oats session inspect|input|attach --home <absolute-home> [--text-file <path>] [--json]
|
|
2859
|
-
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>
|
|
2860
3195
|
enable|disable|run|remove|reconcile <id> workspace-scoped, host-owned schedules (spawn,
|
|
2861
3196
|
tick [--dry-run] [--host] command or wake jobs on a five-field cron with an
|
|
2862
3197
|
host install|uninstall|status explicit IANA tz; see docs/schedules.md); --server
|
|
2863
3198
|
routes to that host's workspace
|
|
3199
|
+
oats trigger add (--file <json> | --from <package>:<template> [--set k=v]) | list | show | enable
|
|
3200
|
+
| disable | remove <id> | test <id> | status [<id>] event-driven spawns (github.pull_request
|
|
3201
|
+
polled with the host's gh by the schedule tick;
|
|
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)
|
|
2864
3210
|
oats spawn ... --wake-file <json> | --wake-every <N> --wake-message <text> save a wake schedule
|
|
2865
3211
|
bound to the new instance's home (docs/schedules.md)
|
|
2866
3212
|
oats session upload --file <path> store a copy of a local file as a private attachment
|
|
@@ -2894,6 +3240,9 @@ Usage:
|
|
|
2894
3240
|
[--name <slug>] the exact instance name (no <agent>- prefix;
|
|
2895
3241
|
not with --purpose); must be a slug, not a
|
|
2896
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>)
|
|
2897
3246
|
oats retire <instance> [--force] retire an instance (window, hooks,
|
|
2898
3247
|
[--self] [--delete-branch] worktree, home); --self = retire the
|
|
2899
3248
|
[--keep-dir] [--json] CALLING instance: the window dies, then
|