@awebai/oats 0.27.1 → 0.28.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 +185 -26
- package/capabilities/oats-okf/agents/memory-harvest/AGENTS.md +8 -3
- package/capabilities/oats-okf/bin/oats-okf.mjs +33 -28
- package/capabilities/oats-okf/injects/okf.md +29 -21
- package/capabilities/oats-okf/lib/config.mjs +5 -1
- package/capabilities/oats-okf/lib/consult.mjs +500 -0
- package/capabilities/oats-okf/lib/inspection.mjs +11 -3
- package/capabilities/oats-okf/lib/io.mjs +9 -2
- package/capabilities/oats-okf/lib/sources.mjs +15 -53
- package/capabilities/oats-okf/lib/stores.mjs +10 -7
- package/capabilities/oats-okf/lib/worker.mjs +9 -1
- package/capabilities/oats-okf/oats.json +13 -4
- package/capabilities/oats-okf/skills/okf/SKILL.md +14 -6
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +142 -0
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +86 -0
- package/docs/capabilities.md +3 -1
- 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 +99 -7
- package/docs/oats-local.schema.json +2 -1
- package/docs/oats-package.schema.json +39 -0
- package/docs/packages.md +67 -3
- package/docs/release-notes/v0.27.2.md +51 -0
- package/docs/release-notes/v0.28.0.md +144 -0
- package/docs/schedules.md +99 -1
- package/docs/souls-and-instances.md +19 -3
- package/docs/workspaces.md +8 -2
- package/lib/core.mjs +124 -10
- package/lib/instance-inspect.mjs +4 -4
- package/lib/instance-resolution.mjs +62 -18
- package/lib/materialize.mjs +13 -0
- package/lib/packages.mjs +90 -6
- package/lib/resolve.mjs +20 -2
- package/lib/schedule.mjs +24 -11
- package/lib/triggers.mjs +545 -0
- package/lib/workspace.mjs +80 -3
- package/package-catalog.json +1 -1
- package/package.json +1 -1
package/bin/oats.mjs
CHANGED
|
@@ -32,12 +32,13 @@ import {
|
|
|
32
32
|
import {
|
|
33
33
|
writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages,
|
|
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 { tickTriggers } from "../lib/triggers.mjs";
|
|
41
42
|
import { parseEnvelopeText, scheduleScopeOf, listSchedules, describe as describeSchedule, addSchedule, updateSchedule, setEnabled as setScheduleEnabled, removeSchedule, runNow as runScheduleNow, reconcile as reconcileSchedule, tickHost, tickWorkspace, registerWorkspace, unregisterWorkspace, readRegistry, schedulerStatus, saveWakeForHome, removeWakeForHome, wakeFromFlags, withHostLock, scheduleError, SCHEDULE_API } from "../lib/schedule.mjs";
|
|
42
43
|
import { hostUnitStatus, installHostUnit, uninstallHostUnit } from "../lib/schedule-host.mjs";
|
|
43
44
|
import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_BYTES } from "../lib/attachments.mjs";
|
|
@@ -48,10 +49,32 @@ const await_import_lifecycle = () => ({ resolveInstance: resolveInstanceForCli }
|
|
|
48
49
|
import { homeTarget, soulTarget, isWorkspaceContext, inspectDocument, readinessDocument, policyOf, policySoul, manifestMissingRequires, INSPECT_OPERATIONS_API } from "../lib/instance-inspect.mjs";
|
|
49
50
|
import { readEvents } from "../lib/instance-events.mjs";
|
|
50
51
|
|
|
51
|
-
const
|
|
52
|
+
const rawArgs = process.argv.slice(2);
|
|
53
|
+
/** The kernel's switches: a value never rides one (`--yolo=false` must not turn yolo on). */
|
|
54
|
+
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"]);
|
|
55
|
+
/** `--flag=value` is `--flag value`: every kernel reader (flag(), valueFlag(), the onboard and
|
|
56
|
+
* routed-command loops) then applies the spaced form's validation to it. `problem` is an empty
|
|
57
|
+
* `--flag=` or a switch given a value. */
|
|
58
|
+
function expandInlineValues(argv) {
|
|
59
|
+
const out = [];
|
|
60
|
+
let problem;
|
|
61
|
+
for (const a of argv) {
|
|
62
|
+
const eq = a.indexOf("=");
|
|
63
|
+
if (!a.startsWith("--") || eq <= 2) { out.push(a); continue; }
|
|
64
|
+
const name = a.slice(2, eq), value = a.slice(eq + 1);
|
|
65
|
+
problem ??= KERNEL_SWITCHES.has(name) ? `--${name} takes no value (got ${a})` : value === "" ? `--${name}= needs a value` : undefined;
|
|
66
|
+
out.push(`--${name}`, value);
|
|
67
|
+
}
|
|
68
|
+
return { argv: out, problem };
|
|
69
|
+
}
|
|
70
|
+
const { argv: args, problem: argvProblem } = expandInlineValues(rawArgs);
|
|
52
71
|
let cmd = args[0];
|
|
53
72
|
const HELP_WORDS = new Set(["help", "--help", "-h"]);
|
|
54
73
|
const KERNEL_COMMANDS = new Set(["capture", "capabilities", "doctor", "inspect", "instance", "operation", "package", "readiness", "souls", "launch-config", "experimental", "onboard", "pane", "recall", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "sync", "update", "version", "workspace"]);
|
|
74
|
+
/** Commands whose argv another parser reads (packages/record and packages/experimental parse process.argv). */
|
|
75
|
+
const OWN_ARGV_COMMANDS = new Set(["capture", "recall", "setup", "experimental"]);
|
|
76
|
+
/** Commands `--server <id>` runs on a registered server. */
|
|
77
|
+
const ROUTED_COMMANDS = new Set(["spawn", "retire", "status", "session", "okf", "schedule", "inspect", "operation", "launch-config"]);
|
|
55
78
|
const flag = (name) => {
|
|
56
79
|
const i = args.indexOf(`--${name}`);
|
|
57
80
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -154,7 +177,7 @@ function operationalKnowledgeNote(composition, soulName) {
|
|
|
154
177
|
* home-relative (`.oats/modules/<cap>/<inject>`), where an instance carries them. */
|
|
155
178
|
async function doctorComposition(ctx, soulName, ws, bail) {
|
|
156
179
|
if (!soulName) return undefined;
|
|
157
|
-
const { prepareInstance, previewWorkspaceSoul, materializePrepared, discoverOrStandalone } = await import("../lib/instance-resolution.mjs");
|
|
180
|
+
const { prepareInstance, previewWorkspaceSoul, materializePrepared, discoverOrStandalone, agentDirOf } = await import("../lib/instance-resolution.mjs");
|
|
158
181
|
const deployment = dirname(ws.local.path);
|
|
159
182
|
const root = join(deployment, "agents");
|
|
160
183
|
const remoteOptions = remoteOptionsFromEnv();
|
|
@@ -162,11 +185,11 @@ async function doctorComposition(ctx, soulName, ws, bail) {
|
|
|
162
185
|
// A bail exits the process without unwinding: the temporary copies are removed at exit too.
|
|
163
186
|
process.once("exit", () => { for (const c of cleanups) { try { c(); } catch { /* best effort */ } } });
|
|
164
187
|
try {
|
|
165
|
-
const discovery = await discoverOrStandalone(loadLocal(deployment).local, { remoteOptions });
|
|
188
|
+
const discovery = await discoverOrStandalone(loadLocal(deployment).local, { deployment, remoteOptions });
|
|
166
189
|
const prepared = await prepareInstance(deployment, soulName, { remoteOptions, discovery });
|
|
167
190
|
const pv = await previewWorkspaceSoul(prepared, root);
|
|
168
191
|
cleanups.push(pv.cleanup);
|
|
169
|
-
const agent = findAgentAt(root, prepared.soulEntry
|
|
192
|
+
const agent = findAgentAt(root, agentDirOf(prepared.soulEntry), pv.soulDir);
|
|
170
193
|
if (!agent) bail("E_SOUL_UNKNOWN", `soul "${soulName}" was fetched but is not readable as a soul`);
|
|
171
194
|
const composition = composeInstanceAgentsMd(pv.soulDir, deployment, agent.name, agent.work || "checkout", agent.kind, prepared);
|
|
172
195
|
const scratch = realpathSync(mkdtempSync(join(tmpdir(), "oats-doctor-home-")));
|
|
@@ -1035,7 +1058,7 @@ function standaloneCatalogVersion(ref) {
|
|
|
1035
1058
|
async function discoverForCli(ctx, bail) {
|
|
1036
1059
|
// The standalone case (decisions 10/25) is a discovery too: the repo's own view
|
|
1037
1060
|
// 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 }); }
|
|
1061
|
+
try { const { discoverOrStandalone } = await import("../lib/instance-resolution.mjs"); return await discoverOrStandalone(ctx.local, { deployment: ctx.deploymentDir, remoteOptions: ctx.remoteOptions }); }
|
|
1039
1062
|
catch (e) {
|
|
1040
1063
|
if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance);
|
|
1041
1064
|
throw e;
|
|
@@ -1064,6 +1087,9 @@ function workspaceItems(discovery, lock) {
|
|
|
1064
1087
|
const s = ext.soul;
|
|
1065
1088
|
souls.push({ name: s.name, origin: `external ${s.repoKey} @ ${short(s.commit)}`, kind: "external", repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? (s.team ? [s.team] : []))], private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null });
|
|
1066
1089
|
}
|
|
1090
|
+
for (const s of discovery.packageSouls || []) {
|
|
1091
|
+
souls.push({ name: s.name, qualifiedName: s.qualifiedName, origin: originOf(s), kind: "package", package: s.package, version: s.version, repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? [])], private: false, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null });
|
|
1092
|
+
}
|
|
1067
1093
|
for (const [id, entry] of Object.entries(lock?.packages || {})) {
|
|
1068
1094
|
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 });
|
|
1069
1095
|
}
|
|
@@ -1083,7 +1109,7 @@ function memberRows(discovery) {
|
|
|
1083
1109
|
|
|
1084
1110
|
/** Package rows for `sync` / `workspace status` from the lock. */
|
|
1085
1111
|
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 }));
|
|
1112
|
+
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
1113
|
}
|
|
1088
1114
|
|
|
1089
1115
|
/** Print a padded table: rows are arrays of strings. */
|
|
@@ -1124,6 +1150,17 @@ async function performSync(ctx, bail, { onDiscovered } = {}) {
|
|
|
1124
1150
|
const lock = resolved.lock;
|
|
1125
1151
|
let lockFile;
|
|
1126
1152
|
try { lockFile = writeLock(ctx.deploymentDir, lock); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
|
|
1153
|
+
// Package souls come from the lock: list what THIS sync locked, not what the previous lock held.
|
|
1154
|
+
if (discovery.standalone !== true) {
|
|
1155
|
+
let pkg;
|
|
1156
|
+
try { pkg = await discoverPackageSouls(discovery.workspace, lock, { remoteOptions: ctx.remoteOptions }); }
|
|
1157
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance); throw e; }
|
|
1158
|
+
const kept = (list) => list.filter((p) => typeof p.package !== "string");
|
|
1159
|
+
discovery.packageSouls = pkg.souls;
|
|
1160
|
+
discovery.problems = [...kept(discovery.problems), ...pkg.problems];
|
|
1161
|
+
discovery.warnings = workspaceWarnings(discovery.workspace, discovery.members, discovery.external || [], pkg.souls);
|
|
1162
|
+
problems.splice(0, problems.length, ...kept(problems), ...pkg.problems);
|
|
1163
|
+
}
|
|
1127
1164
|
// The deployment's instance root: <deployment>/agents/ (findRoot's marker). A hand-written
|
|
1128
1165
|
// oats-local.yaml + sync is a complete deployment; spawn must not answer E_NO_DEPLOYMENT after it.
|
|
1129
1166
|
try { mkdirSync(join(ctx.deploymentDir, "agents"), { recursive: true }); } catch { /* reported by spawn's E_NO_DEPLOYMENT remedy if it matters */ }
|
|
@@ -1152,6 +1189,7 @@ function printSyncReport(ctx, synced) {
|
|
|
1152
1189
|
const { report, discovery, items, lockFile } = synced;
|
|
1153
1190
|
const { members, packages, changes } = report;
|
|
1154
1191
|
const disabled = new Set(ctx.local.souls?.disabled || []);
|
|
1192
|
+
const isDisabled = (s) => disabled.has(s.name) || disabled.has(s.qualifiedName ?? `${memberLabel(s.repoKey)}/${s.name}`);
|
|
1155
1193
|
console.log(`workspace ${discovery.workspace?.name ?? `(${standaloneNote(discovery)})`} (${discovery.key} @ ${short(discovery.commit)})`);
|
|
1156
1194
|
console.log(`members ${members.map((m) => m.confirmed ? `${m.name} ✓↔ (@ ${short(m.commit)})` : `${m.name} ✗ (${m.status})`).join(" ") || "(none)"}`);
|
|
1157
1195
|
console.log(`packages ${packages.map((p) => `${p.id} ${p.version} ✓ (@ ${short(p.commit)})`).join(" ") || "(none)"}`);
|
|
@@ -1162,8 +1200,9 @@ function printSyncReport(ctx, synced) {
|
|
|
1162
1200
|
const externalSouls = items.souls.filter((s) => s.kind === "external");
|
|
1163
1201
|
// Souls have no private mode (0.26.0); the private count is of repo-owned member capabilities.
|
|
1164
1202
|
const repoOwned = items.capabilities.filter((c) => c.kind === "member" && c.private);
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1203
|
+
const packageSouls = items.souls.filter((s) => s.kind === "package");
|
|
1204
|
+
const disabledHere = items.souls.filter(isDisabled);
|
|
1205
|
+
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
1206
|
const teams = new Map();
|
|
1168
1207
|
for (const s of items.souls) { const t = teams.get(s.team) || { souls: 0, capabilities: 0 }; t.souls++; teams.set(s.team, t); }
|
|
1169
1208
|
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); }
|
|
@@ -1294,7 +1333,7 @@ async function workspaceCmd() {
|
|
|
1294
1333
|
for (const m of members.filter((m) => !m.confirmed)) console.log(` ${m.name}: ${m.detail}`);
|
|
1295
1334
|
console.log("\nPackages:");
|
|
1296
1335
|
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(",")]));
|
|
1336
|
+
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
1337
|
if (packages.length && unsynced.length) console.log(` declared but not locked (run \`oats sync\`): ${unsynced.join(", ")}`);
|
|
1299
1338
|
if (stale.length) console.log(` locked but no longer declared (run \`oats sync\`): ${stale.join(", ")}`);
|
|
1300
1339
|
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(" ")}`);
|
|
@@ -1343,7 +1382,7 @@ async function statusDrift(data) {
|
|
|
1343
1382
|
const souls = new Map();
|
|
1344
1383
|
for (const a of data) {
|
|
1345
1384
|
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 }); }
|
|
1385
|
+
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 } : {}) }); }
|
|
1347
1386
|
catch { /* not a workspace soul (classic, capability agent, or unreadable stamp) */ }
|
|
1348
1387
|
}
|
|
1349
1388
|
const anything = data.some((a) => (a.instances || []).some((i) => hasModules(i) || hasSoul(i)));
|
|
@@ -1354,7 +1393,7 @@ async function statusDrift(data) {
|
|
|
1354
1393
|
let discovery;
|
|
1355
1394
|
// The standalone view (decisions 10/25) is a discovery too: drift of a standalone
|
|
1356
1395
|
// 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() }); }
|
|
1396
|
+
try { const { discoverOrStandalone } = await import("../lib/instance-resolution.mjs"); discovery = await discoverOrStandalone(ctx.local, { lock, remoteOptions: remoteOptionsFromEnv() }); }
|
|
1358
1397
|
catch (e) {
|
|
1359
1398
|
const reason = e?.details?.reason ? `${e.code}: ${e.details.reason}` : (e?.code || e?.message || "unknown");
|
|
1360
1399
|
return { drift: new Map(), soul: new Map(), souls, unreachable: { code: e?.code ?? null, reason, message: e?.message ?? String(e) } };
|
|
@@ -1369,6 +1408,7 @@ async function statusDrift(data) {
|
|
|
1369
1408
|
}
|
|
1370
1409
|
// The roster's soul row reflects the CURRENT member commit too (the pointer may lag a moved member).
|
|
1371
1410
|
for (const [, stamp] of souls) {
|
|
1411
|
+
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
1412
|
const member = discovery.members.find((m) => m && m.key === stamp.repoKey);
|
|
1373
1413
|
if (member && typeof member.commit === "string" && (member.confirmed || (discovery.standalone === true && member.key === discovery.key))) stamp.current = member.commit;
|
|
1374
1414
|
}
|
|
@@ -1385,6 +1425,12 @@ function driftLine(row) {
|
|
|
1385
1425
|
}
|
|
1386
1426
|
/** The `soul:` line of an instance — decision 17 for the soul source. */
|
|
1387
1427
|
function soulDriftLine(row, agentName) {
|
|
1428
|
+
if (row.package) {
|
|
1429
|
+
const base = `soul: ${row.name ?? agentName} from package ${row.package} v${row.version} @ ${short7(row.commit)}`;
|
|
1430
|
+
if (row.status === "moved") return `${base} [package moved since (now v${row.current?.version} @ ${short7(row.current?.commit)})]`;
|
|
1431
|
+
if (row.status === "missing") return `${base} [${row.reason === "soul-absent" ? "soul no longer shipped by the package" : "package no longer locked"}]`;
|
|
1432
|
+
return base;
|
|
1433
|
+
}
|
|
1388
1434
|
const base = `soul: ${row.name ?? agentName} from ${memberLabel(row.repoKey)} @ ${short7(row.commit)}`;
|
|
1389
1435
|
if (row.status === "moved") return `${base} [member moved since (now @ ${short7(row.current?.commit)})]`;
|
|
1390
1436
|
if (row.status === "missing") return `${base} [${row.reason === "soul-absent" ? "soul no longer present" : `member ${row.reason || "unconfirmed"}`}]`;
|
|
@@ -1394,6 +1440,7 @@ function soulDriftLine(row, agentName) {
|
|
|
1394
1440
|
function soulRepoLabel(a, ws) {
|
|
1395
1441
|
const stamp = ws?.souls?.get(a.name);
|
|
1396
1442
|
if (!stamp) return a.repo || "?";
|
|
1443
|
+
if (typeof stamp.package === "string") return `package ${stamp.package} v${stamp.version} @ ${short7(stamp.commit)}`;
|
|
1397
1444
|
const moved = stamp.current && stamp.commit && stamp.current !== stamp.commit ? ` (member now @ ${short7(stamp.current)})` : "";
|
|
1398
1445
|
return `${memberLabel(stamp.repoKey)} @ ${short7(stamp.commit)}${moved}`;
|
|
1399
1446
|
}
|
|
@@ -1417,7 +1464,7 @@ async function status() {
|
|
|
1417
1464
|
const rows = ws.drift.get(key);
|
|
1418
1465
|
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
1466
|
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 } : {}) };
|
|
1467
|
+
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
1468
|
}
|
|
1422
1469
|
}
|
|
1423
1470
|
console.log(JSON.stringify({ root, agents: data, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}), ...(problems.length ? { problems } : {}), ...envelopeWarnings() }, null, 2)); return;
|
|
@@ -1503,12 +1550,12 @@ async function spawnCmd() {
|
|
|
1503
1550
|
{
|
|
1504
1551
|
let discovery = null;
|
|
1505
1552
|
try {
|
|
1506
|
-
const { prepareInstance, ensureWorkspaceSoul, previewWorkspaceSoul, parseProviderFlags, discoverOrStandalone } = await import("../lib/instance-resolution.mjs");
|
|
1553
|
+
const { prepareInstance, ensureWorkspaceSoul, previewWorkspaceSoul, parseProviderFlags, discoverOrStandalone, agentDirOf } = await import("../lib/instance-resolution.mjs");
|
|
1507
1554
|
const remoteOptions = remoteOptionsFromEnv();
|
|
1508
|
-
const { local } = loadLocal(dirFlag());
|
|
1509
|
-
discovery = wsDiscovery = await discoverOrStandalone(local, { remoteOptions });
|
|
1555
|
+
const { local, path: localPath } = loadLocal(dirFlag());
|
|
1556
|
+
discovery = wsDiscovery = await discoverOrStandalone(local, { deployment: dirname(localPath), remoteOptions });
|
|
1510
1557
|
wsPrepared = await prepareInstance(dirFlag(), name, { spawn: { providers: parseProviderFlags(providerPairs) }, remoteOptions, discovery });
|
|
1511
|
-
const soulName = wsPrepared.soulEntry
|
|
1558
|
+
const soulName = agentDirOf(wsPrepared.soulEntry);
|
|
1512
1559
|
let soulDir;
|
|
1513
1560
|
if (isPreview) {
|
|
1514
1561
|
// A preview writes nothing in the deployment: the soul comes from the
|
|
@@ -1616,6 +1663,17 @@ async function spawnCmd() {
|
|
|
1616
1663
|
const taskFileFlag = flag("task-file");
|
|
1617
1664
|
if (taskFileFlag === true) bail("E_BAD_ARGS", "--task-file needs a path");
|
|
1618
1665
|
if (taskFileFlag && !existsSync(taskFileFlag)) bail("E_BAD_ARGS", `--task-file not found: ${taskFileFlag}`);
|
|
1666
|
+
// --trigger-event <file>: the event a trigger spawns this instance for (lib/triggers.mjs). It is
|
|
1667
|
+
// copied to <home>/.oats/trigger-event.json (OATS_TRIGGER_EVENT_FILE) and recorded as instance.json.trigger.
|
|
1668
|
+
let triggerEvent;
|
|
1669
|
+
{ const f = flag("trigger-event");
|
|
1670
|
+
if (f === true) bail("E_BAD_ARGS", "--trigger-event needs a path");
|
|
1671
|
+
if (f !== undefined) {
|
|
1672
|
+
if (!existsSync(f)) bail("E_BAD_ARGS", `--trigger-event not found: ${f}`);
|
|
1673
|
+
try { triggerEvent = JSON.parse(readFileSync(f, "utf8")); } catch (e) { bail("E_BAD_ARGS", `--trigger-event is not valid JSON: ${e.message}`); }
|
|
1674
|
+
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";
|
|
1675
|
+
if (!ok) bail("E_BAD_ARGS", "--trigger-event must hold { trigger, source, repo, number, url, event, headSha, labels, observedAt }");
|
|
1676
|
+
} }
|
|
1619
1677
|
const relativeRoot = flag("relative-root");
|
|
1620
1678
|
if (relativeRoot !== undefined && (relativeRoot === true || !String(relativeRoot).trim())) bail("E_BAD_ARGS", "--relative-root needs an agents-root path");
|
|
1621
1679
|
if (relativeRoot && !relativeTo) bail("E_BAD_ARGS", "--relative-root only qualifies --relative-to/--parent");
|
|
@@ -1684,6 +1742,7 @@ async function spawnCmd() {
|
|
|
1684
1742
|
work: requestedWork, workDir, harness: harnessFlag(), backend, herdrSocket, yolo, model: flag("model"), branch,
|
|
1685
1743
|
launchConfig: valueFlag("launch-config"),
|
|
1686
1744
|
launch: !args.includes("--no-launch"),
|
|
1745
|
+
...(triggerEvent ? { triggerEvent } : {}),
|
|
1687
1746
|
// K6: --preview decides everything and touches nothing; --base <ref>
|
|
1688
1747
|
// selects a worktree's start point; --model @native-default is the
|
|
1689
1748
|
// explicit "harness's own default" (distinct from omitting --model).
|
|
@@ -1897,7 +1956,7 @@ function scheduleCmd() {
|
|
|
1897
1956
|
const needId = () => { if (!id) throw scheduleError("E_BAD_ARGS", `oats schedule ${sub} <id>`); return id; };
|
|
1898
1957
|
try {
|
|
1899
1958
|
switch (sub) {
|
|
1900
|
-
case "list":
|
|
1959
|
+
case "list": { const r = listSchedules(ws(), io); out(r); if (!JSON_MODE && r.triggers.count) console.log(`${r.triggers.count} trigger${r.triggers.count === 1 ? " is" : "s are"} not listed here: ${r.triggers.command}`); return; }
|
|
1901
1960
|
case "show": return out({ schedule: describeSchedule(ws(), needId(), io) });
|
|
1902
1961
|
case "add": { const spec = readSpec(); if (id && spec.id === undefined) spec.id = id; 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" }); return out({ schedule: addSchedule(ws(), spec, io) }); }
|
|
1903
1962
|
case "update": return out({ schedule: updateSchedule(ws(), needId(), readSpec(), io) });
|
|
@@ -1910,7 +1969,7 @@ function scheduleCmd() {
|
|
|
1910
1969
|
const dryRun = args.includes("--dry-run");
|
|
1911
1970
|
if (args.includes("--host")) return out(tickHost({ io, dryRun }));
|
|
1912
1971
|
const reg = readRegistry();
|
|
1913
|
-
const considered = withHostLock(() => tickWorkspace(ws(), { io, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()], dryRun }));
|
|
1972
|
+
const considered = withHostLock(() => [...tickWorkspace(ws(), { io, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()], dryRun }), ...tickTriggers(ws(), { io, dryRun })]);
|
|
1914
1973
|
return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws(), io) });
|
|
1915
1974
|
}
|
|
1916
1975
|
case "host": {
|
|
@@ -1929,6 +1988,98 @@ function scheduleCmd() {
|
|
|
1929
1988
|
}
|
|
1930
1989
|
}
|
|
1931
1990
|
|
|
1991
|
+
/** `oats trigger …` (lib/triggers.mjs): event-driven spawns stored beside the schedules of the
|
|
1992
|
+
* deployment scope and evaluated by the same host tick. Every subcommand answers the envelope. */
|
|
1993
|
+
async function triggerCmd() {
|
|
1994
|
+
const sub = args[1];
|
|
1995
|
+
const id = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
|
|
1996
|
+
const T = await import("../lib/triggers.mjs");
|
|
1997
|
+
let scope;
|
|
1998
|
+
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
1999
|
+
const out = (result, text) => { if (JSON_MODE) jsonOk(result); else console.log(text ? text(result) : JSON.stringify(result, null, 2)); };
|
|
2000
|
+
const needId = () => { if (!id) throw T.triggerError("E_BAD_ARGS", `oats trigger ${sub} <id>`); return id; };
|
|
2001
|
+
const usage = "usage: oats trigger add (--file <trigger.json> | --from <package>:<template> [--set <name>=<value>]… [--id <id>]) | list | show <id> | enable <id> | disable <id> | remove <id> | test <id> | status [<id>] [--dir <deployment>] [--json]";
|
|
2002
|
+
const line = (t) => `${t.id} ${t.enabled ? "enabled " : "disabled"} ${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}` : ""}`;
|
|
2003
|
+
try {
|
|
2004
|
+
switch (sub) {
|
|
2005
|
+
case "list": return out(T.listTriggers(ws()), (r) => r.triggers.length ? r.triggers.map(line).join("\n") : "(no triggers — oats trigger add --file <trigger.json> | --from <package>:<template>)");
|
|
2006
|
+
case "show": return out({ trigger: T.describeTrigger(ws(), needId()) });
|
|
2007
|
+
case "enable": return out({ trigger: T.setTriggerEnabled(ws(), needId(), true) }, (r) => line(r.trigger));
|
|
2008
|
+
case "disable": return out({ trigger: T.setTriggerEnabled(ws(), needId(), false) }, (r) => line(r.trigger));
|
|
2009
|
+
case "remove": return out(T.removeTrigger(ws(), needId()), (r) => `removed trigger ${r.removed}${r.live.length ? ` (its live instances keep running: ${r.live.join(", ")})` : ""}`);
|
|
2010
|
+
case "status": return out(T.triggerStatus(ws(), id), (r) => r.triggers.map((t) => `${t.id} last poll ${t.lastPoll ? `${t.lastPoll.at} ${t.lastPoll.ok ? `ok (${t.lastPoll.matching}/${t.lastPoll.prs} PRs match)` : `FAILED: ${t.lastPoll.error}`}` : "never"} pending ${t.pending.length} fired ${t.firedTotal} live ${t.live.map((l) => l.instance).join(",") || "none"}${t.lastError ? `\n last error ${t.lastError.at}: ${t.lastError.message}` : ""}`).join("\n") || "(no triggers)");
|
|
2011
|
+
case "test": {
|
|
2012
|
+
let workspaceTeams = null;
|
|
2013
|
+
try {
|
|
2014
|
+
const { observeWorkspace } = await import("../lib/workspace.mjs");
|
|
2015
|
+
const { local } = loadLocal(ws());
|
|
2016
|
+
if (typeof local.workspace === "string") workspaceTeams = Object.keys((await observeWorkspace(local.workspace, { remoteOptions: remoteOptionsFromEnv() })).workspace.teams || {});
|
|
2017
|
+
} catch { /* reported as unknown (null) */ }
|
|
2018
|
+
return out(T.testTrigger(ws(), needId(), { workspaceTeams }), (r) => [
|
|
2019
|
+
`trigger ${r.id}: ${r.ok ? "ready" : "NOT ready"} (nothing was spawned)`,
|
|
2020
|
+
` gh auth ${r.gh.ok ? `ok — ${r.gh.account ?? "?"} via ${r.gh.credentialSource}` : `FAILED${r.gh.detail ? ` — ${r.gh.detail}` : ""}`}`,
|
|
2021
|
+
` repo ${r.repo.key} ${r.repo.readable ? `readable; push ${r.repo.permissions.push} maintain ${r.repo.permissions.maintain} admin ${r.repo.permissions.admin}` : `NOT readable: ${r.repo.error}`}`,
|
|
2022
|
+
` 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}`}`,
|
|
2023
|
+
` teams ${r.teams.requested.join(", ") || "(none)"}${r.teams.undeclared?.length ? ` undeclared: ${r.teams.undeclared.join(", ")}` : ""}`,
|
|
2024
|
+
` would fire ${r.wouldFire.length ? r.wouldFire.map((w) => `${w.event} #${w.number}${w.held ? ` (held: ${w.held})` : ""}`).join(", ") : "nothing now"}`,
|
|
2025
|
+
...r.problems.map((p) => ` problem ${p}`),
|
|
2026
|
+
...(r.warnings ?? []).map((w) => ` warning ${w}`),
|
|
2027
|
+
].join("\n"));
|
|
2028
|
+
}
|
|
2029
|
+
case "add": {
|
|
2030
|
+
const file = flag("file"), from = flag("from");
|
|
2031
|
+
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}`);
|
|
2032
|
+
const sets = {};
|
|
2033
|
+
for (let i = 0; i < args.length; i++) if (args[i] === "--set") {
|
|
2034
|
+
const kv = args[i + 1];
|
|
2035
|
+
if (typeof kv !== "string" || !kv.includes("=")) throw T.triggerError("E_BAD_ARGS", "--set needs <name>=<value>");
|
|
2036
|
+
sets[kv.slice(0, kv.indexOf("="))] = kv.slice(kv.indexOf("=") + 1); i++;
|
|
2037
|
+
}
|
|
2038
|
+
const idFlag = flag("id");
|
|
2039
|
+
if (idFlag === true) throw T.triggerError("E_BAD_ARGS", "--id needs a trigger id");
|
|
2040
|
+
let spec;
|
|
2041
|
+
if (file !== undefined) {
|
|
2042
|
+
if (file === true || !existsSync(file)) throw T.triggerError("E_BAD_ARGS", `--file ${file === true ? "needs a path" : `not found: ${file}`}`);
|
|
2043
|
+
if (Object.keys(sets).length) throw T.triggerError("E_BAD_ARGS", "--set fills a package template's parameters; with --file, edit the file");
|
|
2044
|
+
try { spec = JSON.parse(readFileSync(file, "utf8")); } catch (e) { throw T.triggerError("E_TRIGGER_INVALID", `${file} is not valid JSON: ${e.message}`, { details: { field: "file" } }); }
|
|
2045
|
+
if (idFlag !== undefined) spec.id = idFlag;
|
|
2046
|
+
} else spec = await triggerFromPackage(T, String(from), sets, idFlag);
|
|
2047
|
+
return out({ trigger: T.addTrigger(ws(), spec) }, (r) => `added ${line(r.trigger)}\n(\`oats trigger test ${r.trigger.id}\` checks gh, the repository, the soul and the teams on this host)`);
|
|
2048
|
+
}
|
|
2049
|
+
default: throw T.triggerError("E_BAD_ARGS", usage);
|
|
2050
|
+
}
|
|
2051
|
+
} catch (e) {
|
|
2052
|
+
const details = { ...(e.details && typeof e.details === "object" ? e.details : {}), ...(e.field !== undefined ? { field: e.field } : {}) };
|
|
2053
|
+
if (JSON_MODE) jsonFail(e.code || "E_TRIGGER_FAILED", e.message, Object.keys(details).length ? details : undefined); else die(e.message);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
/** A package trigger template (`triggers: [{ id, file }]` in the locked package's oats-package.json),
|
|
2057
|
+
* read at the lock's commit and instantiated with the --set parameters. */
|
|
2058
|
+
async function triggerFromPackage(T, from, sets, idFlag) {
|
|
2059
|
+
const m = /^([a-z0-9][a-z0-9._-]*):([a-z0-9][a-z0-9._-]*)$/.exec(from);
|
|
2060
|
+
if (!m) throw T.triggerError("E_BAD_ARGS", `--from ${JSON.stringify(from)}: write <package>:<template>, e.g. oats.okf:harvest-review`);
|
|
2061
|
+
const [, pkg, template] = m;
|
|
2062
|
+
const deployment = scheduleScopeOf(dirFlag());
|
|
2063
|
+
const lock = readLock(deployment);
|
|
2064
|
+
const entry = lock.packages[pkg];
|
|
2065
|
+
if (!entry) throw T.triggerError("E_PACKAGE_MISSING", `package ${pkg} is not in ${LOCK_FILE} — declare it in packages: and run \`oats sync\``, { details: { package: pkg } });
|
|
2066
|
+
const { lockedPackageRef, bindRemote } = await import("../lib/packages.mjs");
|
|
2067
|
+
const ref = lockedPackageRef(entry);
|
|
2068
|
+
const remote = bindRemote(remoteModule, remoteOptionsFromEnv());
|
|
2069
|
+
const read = async (rel, what) => {
|
|
2070
|
+
let bytes;
|
|
2071
|
+
try { ({ bytes } = await remote.readRemoteFile(ref, entry.commit, `${entry.path}/${rel}`)); }
|
|
2072
|
+
catch (e) { throw T.triggerError(e?.code === "E_REMOTE_PATH_MISSING" ? "E_PACKAGE_MANIFEST" : (e?.code || "E_REMOTE_UNREADABLE"), `${pkg} v${entry.version}: ${what} ${entry.path}/${rel} cannot be read: ${e.message}`, { details: { package: pkg, path: rel } }); }
|
|
2073
|
+
try { return JSON.parse(Buffer.from(bytes).toString("utf8")); } catch (e) { throw T.triggerError("E_PACKAGE_MANIFEST", `${pkg} v${entry.version}: ${what} ${entry.path}/${rel} is not valid JSON: ${e.message}`, { details: { package: pkg, path: rel } }); }
|
|
2074
|
+
};
|
|
2075
|
+
const manifest = await read("oats-package.json", "the package manifest");
|
|
2076
|
+
const listed = Array.isArray(manifest.triggers) ? manifest.triggers : [];
|
|
2077
|
+
const row = listed.find((t) => t && t.id === template);
|
|
2078
|
+
if (!row || typeof row.file !== "string" || !row.file || row.file.split("/").includes("..") || row.file.startsWith("/")) throw T.triggerError("E_TRIGGER_UNKNOWN", `package ${pkg} v${entry.version} has no trigger template ${JSON.stringify(template)} (templates: ${listed.map((t) => t?.id).filter(Boolean).join(", ") || "none"})`, { details: { package: pkg, template, templates: listed.map((t) => t?.id).filter(Boolean) } });
|
|
2079
|
+
const doc = await read(row.file, `trigger template ${template}`);
|
|
2080
|
+
return T.instantiateTemplate(doc, sets, { id: idFlag, provenance: { package: pkg, version: entry.version, commit: entry.commit, template } });
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1932
2083
|
async function sessionCmd() {
|
|
1933
2084
|
try {
|
|
1934
2085
|
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" });
|
|
@@ -2279,7 +2430,7 @@ async function capabilityCommand() {
|
|
|
2279
2430
|
// OATS_SOUL is the recorded soul or nothing: an ambient value inherited from the
|
|
2280
2431
|
// invoking process names some other soul (a coordinator's own), never this one.
|
|
2281
2432
|
const { OATS_SOUL: _ambientSoul, ...inherited } = process.env;
|
|
2282
|
-
const r = spawnSync("node", [abs, ...rest, ...
|
|
2433
|
+
const r = spawnSync("node", [abs, ...rest, ...rawArgs.slice(2)], { stdio: "inherit", env: {
|
|
2283
2434
|
...inherited, OATS_CAPABILITY: m.capability,
|
|
2284
2435
|
// Package-runtime boundary: dispatched commands receive the active
|
|
2285
2436
|
// capability's EFFECTIVE settings (instance snapshot, resolved context, or
|
|
@@ -2356,7 +2507,7 @@ function versionCmd() {
|
|
|
2356
2507
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
2357
2508
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
2358
2509
|
// 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 }));
|
|
2510
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "teams", "settings-declared", "capabilities-private", "layers-from", "harness", "package-souls", "triggers"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
|
|
2360
2511
|
return;
|
|
2361
2512
|
}
|
|
2362
2513
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -2495,7 +2646,7 @@ async function serverRouteCmd() {
|
|
|
2495
2646
|
// so its explicit --dir travels; every other routed command takes its
|
|
2496
2647
|
// scope from the registration.
|
|
2497
2648
|
const explicitScopeOk = ["inspect", "operation", "launch-config"].includes(cmd);
|
|
2498
|
-
if (!explicitScopeOk &&
|
|
2649
|
+
if (!explicitScopeOk && flag("dir") !== undefined) bail("E_BAD_ARGS", "--dir cannot be combined with --server: the remote workspace comes from the server registration");
|
|
2499
2650
|
if (cmd === "launch-config") {
|
|
2500
2651
|
const action = args[1];
|
|
2501
2652
|
const value = (name) => { const v = flag(name); if (v === true) bail("E_BAD_ARGS", `--${name} needs a value`); return v; };
|
|
@@ -2715,13 +2866,16 @@ try {
|
|
|
2715
2866
|
// resolved against the current context instead. `version` answers regardless:
|
|
2716
2867
|
// host protocol negotiation describes this executable.
|
|
2717
2868
|
{
|
|
2869
|
+
// The kernel's own argv only: a capability command's flags are its provider's to parse.
|
|
2870
|
+
const kernelArgv = (KERNEL_COMMANDS.has(cmd) && !OWN_ARGV_COMMANDS.has(cmd)) || (ROUTED_COMMANDS.has(cmd) && flag("server") !== undefined);
|
|
2871
|
+
if (argvProblem && kernelArgv) cmdFail("E_BAD_ARGS", argvProblem);
|
|
2718
2872
|
const end = args.indexOf("--"), head = end < 0 ? args : args.slice(0, end);
|
|
2719
|
-
const selector = head.find((a) => /^--(deployment|resolution|artifact-set)
|
|
2873
|
+
const selector = head.find((a) => /^--(deployment|resolution|artifact-set)$/.test(a));
|
|
2720
2874
|
const refuse = (message, details) => { if (JSON_MODE) jsonFail("E_UNSUPPORTED_MODE", message, details); die(message); };
|
|
2721
|
-
if (selector) refuse(`${selector
|
|
2875
|
+
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
2876
|
const inherited = ["OATS_RESOLUTION", "OATS_DEPLOYMENT"].filter((k) => process.env[k]);
|
|
2723
2877
|
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.
|
|
2878
|
+
if (cmd === "inspect" && head.includes("--request")) {
|
|
2725
2879
|
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
2880
|
if (JSON_MODE) jsonFail("E_UNKNOWN_COMMAND", message, { removed: "inspect --request", replacement: "oats onboard / oats sync; oats spawn --preview" });
|
|
2727
2881
|
die(message);
|
|
@@ -2738,7 +2892,7 @@ else {
|
|
|
2738
2892
|
// `okf harvest --help` spawned a harvester (BeadHub, 2026-09-05).
|
|
2739
2893
|
const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
|
|
2740
2894
|
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 &&
|
|
2895
|
+
if (flag("server") !== undefined && ROUTED_COMMANDS.has(cmd)) await serverRouteCmd();
|
|
2742
2896
|
else if (cmd === "server") serverCmd();
|
|
2743
2897
|
else if (cmd === "inspect") await inspectCmd();
|
|
2744
2898
|
else if (cmd === "operation") await operationCmd();
|
|
@@ -2768,6 +2922,7 @@ else if (cmd === "version" || cmd === "--version" || cmd === "-v") versionCmd();
|
|
|
2768
2922
|
// through the shared boundary, never re-badged as a spawn-mechanism failure.
|
|
2769
2923
|
else if (cmd === "session") await sessionCmd();
|
|
2770
2924
|
else if (cmd === "schedule") scheduleCmd();
|
|
2925
|
+
else if (cmd === "trigger") await triggerCmd();
|
|
2771
2926
|
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
2927
|
else if (cmd === "retire") retireCmd();
|
|
2773
2928
|
else if (cmd === "capture" || cmd === "recall" || cmd === "setup") await recordCmd(cmd);
|
|
@@ -2861,6 +3016,10 @@ Usage:
|
|
|
2861
3016
|
tick [--dry-run] [--host] command or wake jobs on a five-field cron with an
|
|
2862
3017
|
host install|uninstall|status explicit IANA tz; see docs/schedules.md); --server
|
|
2863
3018
|
routes to that host's workspace
|
|
3019
|
+
oats trigger add (--file <json> | --from <package>:<template> [--set k=v]) | list | show | enable
|
|
3020
|
+
| disable | remove <id> | test <id> | status [<id>] event-driven spawns (github.pull_request
|
|
3021
|
+
polled with the host's gh by the schedule tick;
|
|
3022
|
+
see docs/schedules.md#triggers)
|
|
2864
3023
|
oats spawn ... --wake-file <json> | --wake-every <N> --wake-message <text> save a wake schedule
|
|
2865
3024
|
bound to the new instance's home (docs/schedules.md)
|
|
2866
3025
|
oats session upload --file <path> store a copy of a local file as a private attachment
|
|
@@ -10,9 +10,14 @@ worktree, or write accepted bases directly. Only the listed owned nodes and
|
|
|
10
10
|
base navigation are editable. Read other nodes as context.
|
|
11
11
|
|
|
12
12
|
You are a service: no STATE.md/log.md/notes upkeep and no recursive capture.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
No okf injection is composed for you (capability agents get none), so the
|
|
14
|
+
working-agent consultation protocol does not apply to you. Judge and edit the
|
|
15
|
+
owned nodes from your STAGED roots in ./work (listed in staging.json). Do NOT
|
|
16
|
+
use `oats okf index`, `cat` or `search` for those nodes: they serve the
|
|
17
|
+
accepted state, not your staging, and would hide the changes you are judging.
|
|
18
|
+
Use them, if at all, only for read-only context from nodes you do not edit.
|
|
19
|
+
Accepted bases remain read-only even for you: the completion command performs
|
|
20
|
+
provider publication.
|
|
16
21
|
|
|
17
22
|
Write the explicit judgment receipt, call the task's safely quoted completion
|
|
18
23
|
command, and inspect its result. A failed/uncertain command is NOT success:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { fs, join, resolve, readJSON, safePath, oats, fail, unlock } from '../lib/io.mjs';
|
|
3
|
+
import { loadBindings } from '../lib/config.mjs';
|
|
4
|
+
import { register, registerCaptured, loadInvocationSourceReceipt, homeSource, loadSource, loadStatus, saveStatus, updateStatus, capture, scheduleSource, settleRetiredSchedule, service, markerPath } from '../lib/sources.mjs';
|
|
5
|
+
import { CONSULT } from '../lib/consult.mjs';
|
|
6
6
|
import { runSource, complete, retry, readRun, requireQualifiedHelper } from '../lib/worker.mjs';
|
|
7
7
|
import { initBase, migrate, deliverMigration, cutoverMigration, migrateSource, forgetMigration } from '../lib/migration.mjs';
|
|
8
8
|
import { inspect } from '../lib/inspection.mjs';
|
|
@@ -13,8 +13,16 @@ oats okf harvest [--home PATH] [--no-launch] [--json]
|
|
|
13
13
|
oats okf run-source --source FILE [--manual] [--no-launch] [--json]
|
|
14
14
|
oats okf complete --source FILE --run ID --judgment FILE [--json]
|
|
15
15
|
oats okf retry --source FILE [--run ID --rejudge | --rejudge | --launch | --adopt-home PATH] [--json]
|
|
16
|
-
oats okf
|
|
17
|
-
oats okf
|
|
16
|
+
oats okf bases [--fresh] [--json]
|
|
17
|
+
oats okf index [--base ALIAS] [NODE | ALIAS/NODE] [--fresh] [--json]
|
|
18
|
+
oats okf cat --base ALIAS PATH [--from PATH] [--fresh] [--json]
|
|
19
|
+
oats okf ls --base ALIAS [DIR] [--fresh] [--json]
|
|
20
|
+
oats okf links --base ALIAS PATH [--fresh] [--json]
|
|
21
|
+
oats okf search [--base ALIAS | --all] [--node NODE] [--regex] [--case-sensitive] TEXT [--fresh] [--json]
|
|
22
|
+
oats okf read --base ALIAS [--path node/index.md] [--json] (2.x alias of cat)
|
|
23
|
+
Consult commands read the accepted state remotely (host cache, no local copy);
|
|
24
|
+
also accept --home PATH | --source FILE. PATH is /node/x.md from the base root,
|
|
25
|
+
relative to --from's directory, or bare node/x.md from the root.
|
|
18
26
|
oats okf setup --source FILE [--enable | --disable] [--install-host] [--json]
|
|
19
27
|
oats okf init --base ALIAS --nodes FILE [--output PATH | --confirm] [--json]
|
|
20
28
|
oats okf migrate --legacy PATH --base ALIAS --node NODE --output PATH [--json]
|
|
@@ -36,11 +44,13 @@ if(args.includes('--help') || args.includes('-h')) {process.stdout.write(HELP);}
|
|
|
36
44
|
else {
|
|
37
45
|
const event=process.env.OATS_EVENT || args[0];
|
|
38
46
|
const hook=['spawn','retire','soul-scaffold'].includes(event);
|
|
39
|
-
|
|
47
|
+
const consult=Object.hasOwn(CONSULT,event);
|
|
48
|
+
let exit=0,answer,text,textMode=consult && !args.includes('--json');
|
|
40
49
|
try {
|
|
41
|
-
const flags={}; const boolean=new Set(['json','no-launch','manual','rejudge','launch','enable','disable','install-host','confirm']);
|
|
50
|
+
const flags={},positionals=[]; const boolean=new Set(['json','no-launch','manual','rejudge','launch','enable','disable','install-host','confirm','fresh','all','regex','case-sensitive']);
|
|
42
51
|
for(let i=1;i<args.length;i++) {
|
|
43
|
-
if(
|
|
52
|
+
if(consult && args[i]==='--') {positionals.push(...args.slice(i+1));break;}
|
|
53
|
+
if(!args[i].startsWith('--')) {if(!consult || event==='read') fail('E_USAGE',`unexpected argument ${args[i]}`);positionals.push(args[i]);continue;}
|
|
44
54
|
const k=args[i].slice(2);if(k in flags) fail('E_USAGE',`duplicate --${k}`);
|
|
45
55
|
if(boolean.has(k)) flags[k]=true;
|
|
46
56
|
else {if(!args[i+1] || args[i+1].startsWith('--')) fail('E_USAGE',`--${k} needs a value`);flags[k]=args[++i];}
|
|
@@ -49,7 +59,9 @@ else {
|
|
|
49
59
|
spawn:[],retire:['home'], 'soul-scaffold':[],
|
|
50
60
|
harvest:['home','no-launch','native-request','worker-mode'],inspect:['home','source'],
|
|
51
61
|
'run-source':['source','manual','no-launch'],complete:['source','run','judgment'],
|
|
52
|
-
retry:['source','run','rejudge','launch','adopt-home'],read:['home','source','base','path'],refresh:['home','source'],
|
|
62
|
+
retry:['source','run','rejudge','launch','adopt-home'],read:['home','source','base','path','fresh'],refresh:['home','source'],
|
|
63
|
+
bases:['home','source','fresh'],index:['home','source','base','fresh'],cat:['home','source','base','from','fresh'],ls:['home','source','base','fresh'],
|
|
64
|
+
links:['home','source','base','fresh'],search:['home','source','base','all','node','regex','case-sensitive','fresh'],
|
|
53
65
|
setup:['source','enable','disable','install-host'],init:['base','nodes','output','confirm'],
|
|
54
66
|
migrate:['source-home','legacy','base','node','output','deliver','cutover','soul-dir'],unlock:['lock','token']
|
|
55
67
|
};
|
|
@@ -107,7 +119,9 @@ else {
|
|
|
107
119
|
readRun(s,id);return s;
|
|
108
120
|
};
|
|
109
121
|
let result;
|
|
110
|
-
if(
|
|
122
|
+
if(consult) {const answer=CONSULT[event](src(),flags,positionals);result=answer.result;text=answer.text;}
|
|
123
|
+
else if(event==='refresh') fail('E_REMOVED','okf 3.0.0 has no per-instance views; index/cat always read the accepted state: run `oats okf index`, then `oats okf cat --base ALIAS PATH`');
|
|
124
|
+
else if(event==='soul-scaffold') {
|
|
111
125
|
// Souls are portable declarations, never an implicit knowledge store.
|
|
112
126
|
result={meta:{scaffolded:false},brief:'OKF requires explicit external bindings and soul/okf.json before a working instance can spawn. Use init or migrate; no knowledge was created in this soul.'};
|
|
113
127
|
} else if(event==='spawn') {
|
|
@@ -115,7 +129,8 @@ else {
|
|
|
115
129
|
if(s.skipped) result={meta:{memory:'none'},brief:'Service agent: follow your own task; no working-memory upkeep.'};
|
|
116
130
|
else {
|
|
117
131
|
const schedule=loadStatus(s).schedule.result;
|
|
118
|
-
|
|
132
|
+
const nodes=(list)=>list.join(', ') || 'none';
|
|
133
|
+
result={meta:{memory:'okf-v2',source:s.file,schedule},brief:`Your knowledge is read remotely at its accepted state; there is no local copy. Start every task with \`oats okf index\` (owns: ${nodes(s.decl.owns)}; reads: ${nodes(s.decl.reads.filter(r=>!s.decl.owns.includes(r)))}), then \`oats okf cat --base ALIAS PATH\` for the concepts the task needs; \`oats okf search\` before re-deriving a decision. Load the okf-consultation skill for the procedure. Keep STATE.md/log.md/notes/ current; never edit knowledge.`};
|
|
119
134
|
}
|
|
120
135
|
} else if(event==='retire') {
|
|
121
136
|
if(captured) {
|
|
@@ -151,20 +166,6 @@ else {
|
|
|
151
166
|
if(flags.enable || flags.disable) {oats(['schedule',flags.enable?'enable':'disable',`okf-${s.id}`,'--dir',s.context,'--json'],s.context);updateStatus(s,current=>{current.auto=!!flags.enable;});}
|
|
152
167
|
if(flags['install-host']) oats(['schedule','host','install','--dir',s.context,'--json'],s.context);
|
|
153
168
|
result={source:s.file,scheduler:oats(['schedule','list','--dir',s.context,'--json'],s.context).scheduler};
|
|
154
|
-
} else if(event==='read' || event==='refresh') {
|
|
155
|
-
const s=src();
|
|
156
|
-
// A descriptor-selected read is independent of any invoking/source home.
|
|
157
|
-
// In particular, retired sources must not leave caches in context/repo.
|
|
158
|
-
const target=join(flags.source?join(dirname(s.file),'views'):home,`knowledge-view-${randomUUID()}`);
|
|
159
|
-
const receipts=views(s.bindings,s.decl,target);
|
|
160
|
-
if(event==='refresh') result={path:target,receipts};
|
|
161
|
-
else {
|
|
162
|
-
if(!Object.hasOwn(s.bindings.bases,flags.base || '')) fail('E_CONFIG','unknown --base');
|
|
163
|
-
const basePath=join(target,receipts[flags.base].path);
|
|
164
|
-
const p=safePath(join(basePath,flags.path || 'index.md'));
|
|
165
|
-
if(!p.startsWith(basePath+'/') || !p.endsWith('.md')) fail('E_PATH','read only contained Markdown');
|
|
166
|
-
result={path:p,text:fs.readFileSync(p,'utf8'),receipt:receipts[flags.base]};
|
|
167
|
-
}
|
|
168
169
|
} else if(event==='init') result=initBase(loadBindings(),flags.base,flags.nodes,flags.output,{confirm:!!flags.confirm});
|
|
169
170
|
else if(event==='migrate') {
|
|
170
171
|
if(flags['source-home']) result=migrateSource(loadBindings(),flags['source-home']);
|
|
@@ -176,6 +177,10 @@ else {
|
|
|
176
177
|
else fail('E_USAGE',`unknown command ${event}; see --help`);
|
|
177
178
|
answer=hook?result:{schemaVersion:1,ok:true,result};
|
|
178
179
|
} catch(e) {const code=e.code || 'E_OKF';exit=1;answer=hook?{meta:{...(event==='retire'?{retired:false,reason:e.message}:{})},warning:`oats-okf ${code}: ${e.message}`}:{schemaVersion:1,ok:false,error:{code,message:e.message}};}
|
|
179
|
-
//
|
|
180
|
-
process.
|
|
180
|
+
// Consult commands print text unless --json; every other answer is JSON.
|
|
181
|
+
// Let Node drain the pipe; no process.exit after a possibly large answer.
|
|
182
|
+
if(textMode && exit) process.stderr.write(`oats okf ${event}: ${answer.error.code}: ${answer.error.message}\n`);
|
|
183
|
+
else if(textMode) process.stdout.write(text+'\n');
|
|
184
|
+
else process.stdout.write(JSON.stringify(answer)+'\n');
|
|
185
|
+
process.exitCode=exit;
|
|
181
186
|
}
|