@awebai/oats 0.25.8 → 0.26.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/README.md +8 -6
- package/bin/oats.mjs +576 -1714
- package/capabilities/oats-authoring/oats-package.json +2 -2
- package/capabilities/oats-authoring/oats.json +2 -2
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +334 -72
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +107 -5
- package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
- package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
- package/capabilities/oats-aweb/oats.json +14 -4
- package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
- package/capabilities/oats-jira/oats.json +2 -2
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
- package/capabilities/oats-linear/oats.json +2 -2
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +1 -1
- package/capabilities/oats-okf/lib/binding-wire.mjs +1 -1
- package/capabilities/oats-okf/lib/migration.mjs +2 -2
- package/capabilities/oats-okf/lib/sources.mjs +5 -4
- package/capabilities/oats-okf/lib/stores.mjs +40 -9
- package/capabilities/oats-okf/lib/worker.mjs +3 -3
- package/capabilities/oats-okf/oats.json +1 -1
- package/capabilities/oats-review/oats.json +3 -2
- package/docs/capabilities.md +218 -47
- package/docs/capability-manifest.schema.json +13 -4
- package/docs/configuration.md +17 -5
- package/docs/conventions.md +16 -26
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
- package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
- package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
- package/docs/design/2026-09-20-redesign-program-board.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +54 -10
- package/docs/design/2026-09-24-phase-d-plan.md +77 -0
- package/docs/design/2026-09-25-teams-contract.md +226 -0
- package/docs/design/README.md +3 -3
- package/docs/design/launch-configurations.md +20 -16
- package/docs/design/operations-contract.md +27 -10
- package/docs/desktop-cli-api.md +546 -264
- package/docs/desktop-instance-start.md +1 -1
- package/docs/desktop.md +7 -13
- package/docs/execution-targets.md +16 -18
- package/docs/first-team.md +14 -17
- package/docs/implementation.md +28 -59
- package/docs/integrations.md +88 -32
- package/docs/knowledge-capability-authoring.md +1 -1
- package/docs/knowledge-reference/package-craft.md +10 -8
- package/docs/knowledge-theory.md +1 -1
- package/docs/knowledge.md +10 -11
- package/docs/layers.md +16 -17
- package/docs/oats-local.schema.json +29 -1
- package/docs/oats-membership.schema.json +5 -3
- package/docs/oats-package.schema.json +2 -2
- package/docs/oats-workspace.schema.json +1 -1
- package/docs/{official-marketplace.md → official-catalog.md} +15 -16
- package/docs/packages.md +75 -52
- package/docs/release-notes/v0.22.0.md +1 -1
- package/docs/release-notes/v0.23.1.md +1 -1
- package/docs/release-notes/v0.25.9.md +23 -0
- package/docs/release-notes/v0.26.0.md +670 -0
- package/docs/schedules.md +48 -126
- package/docs/soul.schema.json +11 -4
- package/docs/souls-and-instances.md +56 -43
- package/docs/workspaces.md +80 -58
- package/injects/instance-boundary.md +1 -1
- package/injects/work-attached.md +1 -1
- package/injects/work-workspace.md +2 -2
- package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
- package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
- package/lib/capability-contract.mjs +110 -0
- package/lib/config-data.mjs +2 -2
- package/lib/core.mjs +700 -4824
- package/lib/digest.mjs +12 -0
- package/lib/instance-inspect.mjs +396 -0
- package/lib/instance-lifecycle.mjs +3 -4
- package/lib/instance-resolution.mjs +212 -26
- package/lib/instruction-composition.mjs +0 -20
- package/lib/materialize.mjs +6 -4
- package/lib/operator-dispatch.mjs +33 -13
- package/lib/packages.mjs +25 -190
- package/lib/provider-binding.mjs +4 -2
- package/lib/provider-reasons.mjs +3 -68
- package/lib/resolve.mjs +204 -68
- package/lib/schedule.mjs +97 -272
- package/lib/servers.mjs +13 -13
- package/lib/{portable-shape.mjs → shape.mjs} +4 -3
- package/lib/tree-copy.mjs +44 -0
- package/lib/workspace.mjs +125 -20
- package/package-catalog.json +7 -7
- package/package.json +1 -1
- package/skills/integration-authoring/SKILL.md +48 -40
- package/skills/oats-getting-started/SKILL.md +105 -110
- package/skills/oats-support/SKILL.md +2 -2
- package/skills/soul-craft/SKILL.md +13 -6
- package/bin/oats-pi-sdk-host.mjs +0 -17
- package/docs/2026-09-03-architecture-proposal.md +0 -642
- package/docs/artifact-approvals.schema.json +0 -7
- package/docs/captured-invocation-context.schema.json +0 -7
- package/docs/captured-resolution.schema.json +0 -7
- package/docs/design/package-engine-contract.md +0 -813
- package/docs/design/package-runtime-api.md +0 -588
- package/docs/desktop-succession.md +0 -57
- package/docs/execution-capsule.schema.json +0 -108
- package/docs/first-team-demo.md +0 -92
- package/docs/knowledge-migration.md +0 -147
- package/docs/migration-from-oas.md +0 -103
- package/docs/oats-config.schema.json +0 -172
- package/docs/oats-lock-v3.schema.json +0 -7
- package/docs/oats-lock.schema.json +0 -175
- package/docs/operating-team-migration.md +0 -470
- package/docs/portable.schema.json +0 -2512
- package/docs/provider-check-input.schema.json +0 -7
- package/docs/rebuild-to-v2.md +0 -511
- package/docs/workspace-adoption.md +0 -74
- package/injects/framework-workspace.md +0 -7
- package/injects/local-soul.md +0 -19
- package/injects/oats-portable.md +0 -20
- package/injects/oats.md +0 -11
- package/injects/portable-instance-boundary.md +0 -39
- package/injects/portable-work-directory.md +0 -29
- package/lib/artifact-approvals.mjs +0 -120
- package/lib/artifact-tree.mjs +0 -141
- package/lib/capability-artifacts.mjs +0 -179
- package/lib/capability-execution.mjs +0 -15
- package/lib/capability-inputs.mjs +0 -39
- package/lib/capability-provenance.mjs +0 -231
- package/lib/captured-action-shape.mjs +0 -21
- package/lib/captured-admission-shape.mjs +0 -20
- package/lib/captured-binding-file.mjs +0 -36
- package/lib/captured-dispatch.mjs +0 -66
- package/lib/captured-instance-index.mjs +0 -277
- package/lib/captured-invocation-context.mjs +0 -130
- package/lib/captured-launch-request.mjs +0 -66
- package/lib/captured-operation-process.mjs +0 -15
- package/lib/captured-pi-custody.mjs +0 -29
- package/lib/captured-pi-host.mjs +0 -167
- package/lib/captured-pi-outcome.mjs +0 -172
- package/lib/captured-resolutions.mjs +0 -275
- package/lib/captured-scaffold.mjs +0 -87
- package/lib/captured-selector.mjs +0 -28
- package/lib/captured-session-backend.mjs +0 -52
- package/lib/captured-source-receipt-file.mjs +0 -72
- package/lib/helper-injection-policy.mjs +0 -104
- package/lib/legacy-lock-codec.mjs +0 -106
- package/lib/manifest-settings.mjs +0 -84
- package/lib/package-closure.mjs +0 -48
- package/lib/package-materialization.mjs +0 -83
- package/lib/pi-sdk-host.mjs +0 -229
- package/lib/portable-artifacts.mjs +0 -115
- package/lib/portable-choices.mjs +0 -82
- package/lib/portable-composition.mjs +0 -136
- package/lib/portable-digest.mjs +0 -105
- package/lib/portable-identity.mjs +0 -40
- package/lib/portable-lock.mjs +0 -117
- package/lib/portable-onboarding-request.mjs +0 -49
- package/lib/portable-onboarding.mjs +0 -256
- package/lib/portable-package-preparation.mjs +0 -188
- package/lib/portable-policy.mjs +0 -44
- package/lib/portable-soul.mjs +0 -42
- package/lib/portable-state.mjs +0 -80
- package/lib/prepare-composition.mjs +0 -170
- package/lib/prepared-bindings.mjs +0 -92
- package/lib/prepared-resources.mjs +0 -127
- package/lib/provider-binding-broker.mjs +0 -65
- package/lib/provider-binding-wire.mjs +0 -116
- package/lib/readiness.mjs +0 -225
- package/lib/repository-observation.mjs +0 -226
- package/lib/resolution-shape.mjs +0 -393
- package/lib/schedule-capsule.mjs +0 -206
- package/lib/soul-constraints.mjs +0 -40
- package/lib/source-projection.mjs +0 -84
- package/lib/source-spec.mjs +0 -189
- package/lib/workspace-definition.mjs +0 -126
- package/lib/workspace-discovery.mjs +0 -146
- package/skills/oats/SKILL.md +0 -162
- package/skills/oats-config/SKILL.md +0 -164
- package/skills/oats-packages/SKILL.md +0 -184
- package/skills/oats-portable/SKILL.md +0 -115
- package/skills/oats-portable-artifacts/SKILL.md +0 -63
package/bin/oats.mjs
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* oats — the OATS command line.
|
|
4
4
|
*
|
|
5
|
-
* oats doctor [dir] [--json]
|
|
5
|
+
* oats doctor [dir] [--soul <s>] [--json] show the deployment; --soul: the instructions an instance of <s> would carry
|
|
6
6
|
* oats onboard [<dir>] --workspace <ref> realize a workspace here (oats-local.yaml +
|
|
7
7
|
* agents/), then sync
|
|
8
8
|
* oats sync [--dir <d>] [--json] discover the workspace, confirm membership,
|
|
9
|
-
* resolve packages,
|
|
9
|
+
* resolve packages, write the lock
|
|
10
10
|
* oats package add|remove ... edit `packages:` in the workspace file
|
|
11
|
-
* oats workspace status membership table, packages
|
|
11
|
+
* oats workspace status membership table, packages
|
|
12
12
|
* oats capabilities | oats souls every visible item of the workspace
|
|
13
13
|
*
|
|
14
14
|
* Workspace model v2 (docs/design/2026-09-23-workspace-module-contracts.md §6):
|
|
@@ -17,30 +17,23 @@
|
|
|
17
17
|
* `init` / `use` / `install` / `restore` / `list` / `catalog` / `remove` /
|
|
18
18
|
* `migrate` / `trust` / `inject` are gone with the installed-capability tier.
|
|
19
19
|
*/
|
|
20
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readSync, realpathSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readSync, realpathSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
22
|
-
import { homedir } from "node:os";
|
|
22
|
+
import { homedir, tmpdir } from "node:os";
|
|
23
23
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
24
|
-
import { createHash } from "node:crypto";
|
|
25
24
|
import { fileURLToPath } from "node:url";
|
|
26
25
|
import {
|
|
27
|
-
LAYERS,
|
|
28
|
-
capabilityManifests,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir,
|
|
32
|
-
resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, parseYamlNested, assertSafeConfigValue, stripInternalAnnotations, withConfigFile, teamAgentRoots,
|
|
33
|
-
findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, findInstanceHomes, listCapabilityAgents, workspaceOf, stopInstanceSession, recomposeInstanceInstructions,
|
|
34
|
-
ensureRoot, findRoot, findAgent, listAgents, listInstances, servedIdentityLine, servedIdentityOf, listAgentDefs, createAgent as coreCreateAgent,
|
|
35
|
-
spawnInstance, spawnInstanceAsync, findModuleCapabilityAgent, capabilityAgentFromDir, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, upsertLocalAgent, defaultRepo, RELATIONS, validateLaunchConfig, resolveLaunchSelection, resolveLaunchExecutable, checkLaunchExecutable, missingLaunchEnvRefs, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_RUNTIMES, LAUNCH_RECIPE_VERSION, parseLaunchCommand, resolveYolo, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
26
|
+
LAYERS, OATS_VERSION, manifestOperations,
|
|
27
|
+
capabilityManifests, capabilityTrust, capabilityExecutablePath,
|
|
28
|
+
officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
|
|
29
|
+
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, explicitInstanceName, findModuleCapabilityAgent, capabilityAgentFromDir, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_RUNTIMES, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
36
30
|
} from "../lib/core.mjs";
|
|
37
31
|
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
import {
|
|
32
|
+
writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages,
|
|
33
|
+
classifyPackageValue, parsePackageRequest } from "../lib/packages.mjs";
|
|
34
|
+
import { loadLocal, validateWorkspace, validateLocal } from "../lib/workspace.mjs";
|
|
35
|
+
import { parseConfigData } from "../lib/config-data.mjs";
|
|
42
36
|
import * as remoteModule from "../lib/remote.mjs";
|
|
43
|
-
import { createInterface } from "node:readline/promises";
|
|
44
37
|
import YAML from "yaml";
|
|
45
38
|
import { attachArgv, checkRemote, forgetSnapshot, getServer, inspectRemote, startRemote, restartRemote, launchConfigRemote, scheduleRemote, listSnapshots, readServers, rosterGroups, routeCommand, targetOf, validateServer, writeServers, SERVERS_FILE } from "../lib/servers.mjs";
|
|
46
39
|
import { spawnSync as spawnSyncProc } from "node:child_process";
|
|
@@ -48,25 +41,16 @@ import { parseEnvelopeText, scheduleScopeOf, listSchedules, describe as describe
|
|
|
48
41
|
import { hostUnitStatus, installHostUnit, uninstallHostUnit } from "../lib/schedule-host.mjs";
|
|
49
42
|
import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_BYTES } from "../lib/attachments.mjs";
|
|
50
43
|
|
|
51
|
-
import { capturedSelector } from "../lib/captured-selector.mjs";
|
|
52
|
-
import { inspectCapturedPiOutcome } from "../lib/captured-pi-host.mjs";
|
|
53
|
-
import { readCapturedResolution } from "../lib/captured-resolutions.mjs";
|
|
54
|
-
import { oatsError } from "../lib/errors.mjs";
|
|
55
|
-
import { canonicalJson } from "../lib/portable-values.mjs";
|
|
56
|
-
import { readPortablePreparationRequest } from "../lib/portable-onboarding-request.mjs";
|
|
57
|
-
import { portableScope } from "../lib/portable-state.mjs";
|
|
58
|
-
import { CAPTURED_OPERATION_TIMEOUT_MS, runCapturedOperationProcess } from "../lib/captured-operation-process.mjs";
|
|
59
|
-
import { approveCapturedCapability } from "../lib/artifact-approvals.mjs";
|
|
60
44
|
import { observeInstanceGit, diffInstanceFile } from "../lib/instance-git.mjs";
|
|
61
45
|
import { planStop, applyStop, planRetire, resolveInstance as resolveInstanceForCli } from "../lib/instance-lifecycle.mjs";
|
|
62
46
|
const await_import_lifecycle = () => ({ resolveInstance: resolveInstanceForCli });
|
|
63
|
-
import {
|
|
47
|
+
import { homeTarget, soulTarget, isWorkspaceContext, inspectDocument, readinessDocument, policyOf, policySoul, manifestMissingRequires, INSPECT_OPERATIONS_API } from "../lib/instance-inspect.mjs";
|
|
64
48
|
import { readEvents } from "../lib/instance-events.mjs";
|
|
65
49
|
|
|
66
50
|
const args = process.argv.slice(2);
|
|
67
51
|
let cmd = args[0];
|
|
68
52
|
const HELP_WORDS = new Set(["help", "--help", "-h"]);
|
|
69
|
-
const KERNEL_COMMANDS = new Set(["
|
|
53
|
+
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"]);
|
|
70
54
|
const flag = (name) => {
|
|
71
55
|
const i = args.indexOf(`--${name}`);
|
|
72
56
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -81,7 +65,7 @@ function valueFlag(name) {
|
|
|
81
65
|
return value;
|
|
82
66
|
}
|
|
83
67
|
const die = (msg) => { console.error(`oats: ${msg}`); process.exit(1); };
|
|
84
|
-
const cmdFail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
68
|
+
const cmdFail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
85
69
|
/** Resolve the --dir flag with central validation: a value-taking flag given
|
|
86
70
|
* no value (flag() → true) is E_BAD_ARGS inside the JSON boundary, never an
|
|
87
71
|
* uncaught resolve(true) TypeError (reviewer-6f0a3bd). */
|
|
@@ -104,382 +88,14 @@ const JSON_MODE = args.includes("--json");
|
|
|
104
88
|
const CLI_BIN = realpathSync(fileURLToPath(import.meta.url));
|
|
105
89
|
const jsonFail = (code, message, details) => { console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message), ...(details !== undefined ? { details } : {}) } })); process.exit(1); };
|
|
106
90
|
const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok: true, result })); };
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
values.set(key, args[++index]);
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
const output = values.get("--emit-prepare-request");
|
|
119
|
-
let parent;
|
|
120
|
-
const checkOutput = () => {
|
|
121
|
-
if (!isAbsolute(output) || resolve(output) !== output || output.includes("\0")) throw oatsError("E_BAD_ARGS", "prepare-request output needs a normalized absolute path");
|
|
122
|
-
let stat;
|
|
123
|
-
try {
|
|
124
|
-
stat = lstatSync(dirname(output));
|
|
125
|
-
if (!stat.isDirectory() || realpathSync(dirname(output)) !== dirname(output)) throw new Error();
|
|
126
|
-
} catch { throw oatsError("E_BAD_ARGS", "prepare-request output parent must be an existing real directory"); }
|
|
127
|
-
if (parent && (parent.dev !== stat.dev || parent.ino !== stat.ino)) throw oatsError("selection-changed", "prepare-request output parent changed during inspection");
|
|
128
|
-
try { lstatSync(output); }
|
|
129
|
-
catch (error) { if (error.code === "ENOENT") return stat; throw oatsError("E_BAD_ARGS", "prepare-request output could not be checked"); }
|
|
130
|
-
throw oatsError("E_BAD_ARGS", "prepare-request output already exists; choose a new file (nothing overwritten)");
|
|
131
|
-
};
|
|
132
|
-
if (output !== undefined) parent = checkOutput();
|
|
133
|
-
const input = readPortablePreparationRequest({ file: values.get("--request") });
|
|
134
|
-
const { prepareRequest, ...view } = inspectPortableOnboarding(input, { includePrepareRequest: output !== undefined });
|
|
135
|
-
let result = view;
|
|
136
|
-
if (output !== undefined) {
|
|
137
|
-
checkOutput();
|
|
138
|
-
try { writeFileSync(output, canonicalJson(prepareRequest) + "\n", { flag: "wx", mode: 0o600 }); }
|
|
139
|
-
catch { throw oatsError("E_BAD_ARGS", "prepare-request output could not be created exclusively; no existing file was overwritten"); }
|
|
140
|
-
const deployment = view.deployment.deployment.path;
|
|
141
|
-
result = { ...view, prepareRequestFile: output, effects: { ...view.effects, requestFileWrite: true,
|
|
142
|
-
deploymentWrites: output === deployment || output.startsWith(deployment + sep) } };
|
|
143
|
-
}
|
|
144
|
-
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
145
|
-
} catch (error) { fail(error.code || "E_INSPECT_FAILED", error.message); }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function prepareCmd() {
|
|
149
|
-
const fail = (code, message, details) => JSON_MODE ? jsonFail(code, message, details) : die(message);
|
|
150
|
-
const values = new Map(), allowed = new Set(["request", "dir", "source", "revision", "export", "alias", "workspace", "workspace-revision", "work"]);
|
|
151
|
-
for (let index = 1; index < args.length; index++) {
|
|
152
|
-
if (args[index] === "--json") continue;
|
|
153
|
-
const key = args[index].startsWith("--") ? args[index].slice(2) : "";
|
|
154
|
-
if (!allowed.has(key) || values.has(key) || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "prepare needs unique named source/context arguments; use prepare --help");
|
|
155
|
-
values.set(key, args[++index]);
|
|
156
|
-
}
|
|
157
|
-
try {
|
|
158
|
-
let input;
|
|
159
|
-
if (values.has("request")) {
|
|
160
|
-
// The shared leaf owns request bytes/exclusivity; this router alone owns
|
|
161
|
-
// argv and has already refused explicit captured selectors.
|
|
162
|
-
input = readPortablePreparationRequest({ file: values.get("request"),
|
|
163
|
-
inputFlags: Object.fromEntries([...values].filter(([key]) => key !== "request")) });
|
|
164
|
-
} else {
|
|
165
|
-
const deployment = values.get("dir"), alias = values.get("alias"), source = values.get("source");
|
|
166
|
-
if (!deployment || !isAbsolute(deployment) || !alias) fail("E_BAD_ARGS", "prepare needs --dir <absolute deployment> and --alias <name>");
|
|
167
|
-
if (source ? !values.get("revision") || !values.get("export") : !values.get("workspace") || values.has("revision") || values.has("export")) fail("E_BAD_ARGS", "choose a complete source/revision/export reference or a workspace-advertised alias");
|
|
168
|
-
if (values.has("workspace-revision") && !values.has("workspace")) fail("E_BAD_ARGS", "--workspace-revision requires --workspace");
|
|
169
|
-
const origin = { kind: "operator", document: { kind: "operator", id: "oats-prepare" }, pointer: "/source" };
|
|
170
|
-
input = { deployment, source: source ? { source, revision: values.get("revision"), soul: values.get("export"), alias } : alias, origin,
|
|
171
|
-
...(values.has("work") ? { mode: values.get("work") } : {}),
|
|
172
|
-
...(values.has("workspace") ? { workspace: { source: values.get("workspace"), origin: { ...origin, pointer: "/workspace" },
|
|
173
|
-
...(values.has("workspace-revision") ? { revision: values.get("workspace-revision") } : {}) } } : {}) };
|
|
174
|
-
}
|
|
175
|
-
// Pass the whole request to the one public validator/resolver. Unknown
|
|
176
|
-
// fields are refused there, never filtered or filled from ambient state.
|
|
177
|
-
const result = prepareCapturedComposition(input);
|
|
178
|
-
if (!result.resolution) {
|
|
179
|
-
const summary = "preparation is incomplete; no executable resolution was published";
|
|
180
|
-
const reasons = (result.problems ?? []).filter(p => p.key !== undefined || (p.slot && p.capability))
|
|
181
|
-
.map(p => `[${p.slot && p.capability ? `${p.capability}/${p.slot}` : p.code}] ${p.key === undefined ? "" : `${JSON.stringify(p.key)}: `}${p.message}`);
|
|
182
|
-
fail("needs-configuration", JSON_MODE ? summary : [summary, ...reasons].join("\n"), result);
|
|
183
|
-
}
|
|
184
|
-
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
185
|
-
} catch (error) { fail(error.code || "E_PREPARE_FAILED", error.message); }
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Run one provider operation from immutable captured authority. A home is an
|
|
189
|
-
* explicit target only: its stored binding must name this exact record. */
|
|
190
|
-
function capturedOperation(selector, load, bail) {
|
|
191
|
-
if (args[1] !== "run") bail("E_USAGE", "usage: oats operation run <layer>:<name> --deployment <abs> --resolution <id> [--home <abs>] [--arg k=v ...] [--retry-intent <saved-id>] [--json]");
|
|
192
|
-
const address = args[2], match = typeof address === "string" ? OPERATION_ADDRESS_RE.exec(address) : null;
|
|
193
|
-
if (!match) bail("E_BAD_ARGS", `operation address must be <layer>:<name> with layer one of ${LAYERS.join(", ")} (got ${JSON.stringify(address)})`);
|
|
194
|
-
const [, slot, name] = match, given = Object.create(null);
|
|
195
|
-
let home, retryExecutionId;
|
|
196
|
-
for (let index = 3; index < args.length; index++) {
|
|
197
|
-
const token = args[index];
|
|
198
|
-
if (token === "--json") continue;
|
|
199
|
-
if (token === "--home") {
|
|
200
|
-
const value = args[++index];
|
|
201
|
-
if (home !== undefined || !value || value.startsWith("--") || !isAbsolute(value)) bail("E_BAD_ARGS", "--home needs one absolute instance home");
|
|
202
|
-
home = resolve(value); continue;
|
|
203
|
-
}
|
|
204
|
-
if (token === "--retry-intent") {
|
|
205
|
-
const value = args[++index];
|
|
206
|
-
if (retryExecutionId !== undefined || !value || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) bail("E_BAD_ARGS", "--retry-intent requires one saved executionId");
|
|
207
|
-
retryExecutionId = value; continue;
|
|
208
|
-
}
|
|
209
|
-
if (token === "--arg") {
|
|
210
|
-
const value = args[++index], eq = value?.indexOf("=") ?? -1;
|
|
211
|
-
if (eq < 1) bail("E_BAD_ARGS", "--arg expects name=value");
|
|
212
|
-
const key = value.slice(0, eq);
|
|
213
|
-
if (Object.hasOwn(given, key)) bail("E_BAD_ARGS", `duplicate operation arg ${JSON.stringify(key)}`);
|
|
214
|
-
given[key] = value.slice(eq + 1); continue;
|
|
215
|
-
}
|
|
216
|
-
bail("E_BAD_ARGS", `unsupported captured operation argument ${JSON.stringify(token)}`);
|
|
217
|
-
}
|
|
218
|
-
let meta;
|
|
219
|
-
if (home) {
|
|
220
|
-
const metaFile = join(home, "instance.json");
|
|
221
|
-
if (!existsSync(metaFile)) bail("E_SESSION_UNKNOWN", `${home} is not an OATS instance home (no instance.json)`);
|
|
222
|
-
try { meta = JSON.parse(readFileSync(metaFile, "utf8")); } catch (error) { bail("E_SESSION_UNKNOWN", `${metaFile}: ${error.message}`); }
|
|
223
|
-
if (!meta || typeof meta !== "object" || Array.isArray(meta) || typeof meta.instance !== "string" || !meta.instance) bail("E_SESSION_UNKNOWN", `${metaFile}: invalid instance metadata`);
|
|
224
|
-
const binding = meta.executionBinding;
|
|
225
|
-
if (!binding) bail("migration-required", `${home} has no captured executionBinding; current configuration was not used`);
|
|
226
|
-
if (binding.schemaVersion !== 1 || typeof binding.deployment !== "string" || !isAbsolute(binding.deployment)
|
|
227
|
-
|| realOrResolved(binding.deployment) !== realOrResolved(selector.deployment)
|
|
228
|
-
|| binding.resolution?.schemaVersion !== 1 || binding.resolution.id !== selector.resolution.id) {
|
|
229
|
-
bail("E_HOME_MISMATCH", `${home} is not bound to captured resolution ${selector.resolution.id} in ${selector.deployment}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
// Inspect is static: validate target and arguments before the action load runs
|
|
233
|
-
// the provider's mutable readiness check.
|
|
234
|
-
const inspected = load({ kind: "inspect" }), providerId = inspected.record.bindings[slot]?.capability;
|
|
235
|
-
const provider = providerId ? inspected.manifests.get(providerId) : undefined;
|
|
236
|
-
if (!provider) bail("capability-not-selected", `no captured ${slot} provider is selected`);
|
|
237
|
-
const operation = manifestOperations(provider).find((entry) => entry.name === name);
|
|
238
|
-
if (!operation) bail("operation-not-found", "captured provider does not declare this operation");
|
|
239
|
-
if (operation.context === "home" && !meta) bail("E_OPERATION_UNAVAILABLE", `${address} runs in an instance home; pass --home <abs>`);
|
|
240
|
-
if (operation.context === "scope" && meta) bail("E_BAD_ARGS", `${address} is a scope operation and does not accept --home`);
|
|
241
|
-
const declared = new Map(operation.args.map((entry) => [entry.name, entry]));
|
|
242
|
-
for (const key of Object.keys(given)) if (!declared.has(key)) bail("E_BAD_ARGS", `${address} takes no arg ${JSON.stringify(key)} (declared: ${[...declared.keys()].join(", ") || "none"})`);
|
|
243
|
-
for (const entry of operation.args) if (entry.required && given[entry.name] === undefined) bail("E_BAD_ARGS", `${address} needs --arg ${entry.name}=<value>: ${entry.description || "required"}`);
|
|
244
|
-
const action = { kind: "operation", slot, name };
|
|
245
|
-
const argFlags = operation.args.flatMap((entry) => given[entry.name] === undefined ? [] : [entry.flag, given[entry.name]]), cwd = home || selector.deployment;
|
|
246
|
-
if (operation.kind === "action" && !home) bail("admission-required", "scope mutation has no qualified incarnation/admission path; use an instance-scoped operation");
|
|
247
|
-
if (retryExecutionId !== undefined && operation.kind !== "action") bail("E_BAD_ARGS", "read-only operations do not retry mutation intents");
|
|
248
|
-
const admission = operation.kind === "action" ? admitCapturedAction({ deployment: selector.deployment, resolution: selector.resolution, home, action, input: { arguments: argFlags },
|
|
249
|
-
...(retryExecutionId !== undefined ? { retryExecutionId } : {}) }) : null;
|
|
250
|
-
let settlement;
|
|
251
|
-
const admittedBail = (code, message, details) => bail(code, message, { ...details, ...(admission ? { intent: admission.intent } : {}),
|
|
252
|
-
...(settlement ? { settlement, unconfirmed: settlement.state === "unconfirmed" } : {}) });
|
|
253
|
-
if (admission?.replayed) {
|
|
254
|
-
settlement = { state: "completed", receipt: admission.receipt };
|
|
255
|
-
if (!admission.replayable) admittedBail("needs-configuration", "completed operation cannot replay its retained outcome");
|
|
256
|
-
finishOperation({ r: { status: 0, stdout: JSON.stringify(admission.receipt) }, bail: admittedBail, address, provider, op: operation, argFlags, cwd, home, meta, intent: admission.intent }); return;
|
|
257
|
-
}
|
|
258
|
-
let loaded;
|
|
259
|
-
try { loaded = load(action, { invocationTarget: meta ? { home, work: join(home, "work"), name: meta.instance, agent: meta.agent } : null,
|
|
260
|
-
...(admission ? { intent: admission.intent, priorReceipt: admission.receipt } : {}) }); }
|
|
261
|
-
catch (error) {
|
|
262
|
-
if (admission) {
|
|
263
|
-
settleCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action, state: "blocked", receipt: admission.receipt });
|
|
264
|
-
settlement = { state: "blocked", receipt: admission.receipt };
|
|
265
|
-
}
|
|
266
|
-
admittedBail(error.code || "provider-unavailable", error.message);
|
|
267
|
-
}
|
|
268
|
-
const { capability, executable } = loaded;
|
|
269
|
-
const env = { ...process.env };
|
|
270
|
-
for (const key of Object.keys(env)) if (key.startsWith("OATS_") || key.startsWith("PI_AGENT_") || key === "PI_AGENTS_ROOT") delete env[key];
|
|
271
|
-
Object.assign(env, {
|
|
272
|
-
OATS_DEPLOYMENT: selector.deployment, OATS_RESOLUTION: selector.resolution.id,
|
|
273
|
-
OATS_CAPABILITY: capability.id, OATS_CAPABILITY_ROOT: capability.manifest._dir,
|
|
274
|
-
OATS_SETTINGS: JSON.stringify(capability.settings), OATS_CLI_BIN: CLI_BIN,
|
|
275
|
-
OATS_OPERATION: address, OATS_CONTEXT: selector.deployment, OATS_LEVEL: selector.deployment,
|
|
276
|
-
OATS_WORKSPACE: selector.deployment,
|
|
277
|
-
});
|
|
278
|
-
if (meta) Object.assign(env, { OATS_INSTANCE: meta.instance, OATS_INSTANCE_HOME: home, OATS_HOME: home,
|
|
279
|
-
PI_AGENT_INSTANCE: meta.instance, PI_AGENT_HOME: home, ...(meta.agent ? { OATS_AGENT: meta.agent } : {}) });
|
|
280
|
-
const invocation = loaded.invocation;
|
|
281
|
-
let child, cleanupError, started = false;
|
|
282
|
-
try {
|
|
283
|
-
child = withCapturedInvocationContextFile(invocation, contextEnv => withCapturedBindingFile(loaded, bindingEnv => {
|
|
284
|
-
if (admission) { beginCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action }); started = true; }
|
|
285
|
-
return runCapturedOperationProcess({ file: executable.file, args: [...executable.args, ...argFlags, "--json"], cwd, env: { ...env, ...contextEnv, ...bindingEnv } });
|
|
286
|
-
}));
|
|
287
|
-
} catch (error) {
|
|
288
|
-
if (!error?.invocationCompleted) {
|
|
289
|
-
if (admission) {
|
|
290
|
-
settleCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action, state: started ? "unconfirmed" : "blocked", receipt: admission.receipt });
|
|
291
|
-
settlement = { state: started ? "unconfirmed" : "blocked", receipt: admission.receipt };
|
|
292
|
-
}
|
|
293
|
-
admittedBail(error.code || "E_OPERATION_RESULT", error.message, { unconfirmed: started });
|
|
294
|
-
}
|
|
295
|
-
child = error.invocationResult; cleanupError = error;
|
|
296
|
-
}
|
|
297
|
-
if (admission) {
|
|
298
|
-
let envelope; try { envelope = JSON.parse(String(child.stdout || "").trim()); } catch { /* unconfirmed below */ }
|
|
299
|
-
const completed = !cleanupError && !child.error && child.status === 0 && envelope?.schemaVersion === 1 && envelope.ok === true;
|
|
300
|
-
const state = completed ? "completed" : "unconfirmed", receipt = envelope ?? parseEnvelopeText(String(child.stdout || "")) ?? admission.receipt;
|
|
301
|
-
try {
|
|
302
|
-
settleCapturedIntent({ deployment: selector.deployment, home, intent: admission.intent, action, state, receipt, replayable: completed });
|
|
303
|
-
settlement = { state, receipt };
|
|
304
|
-
}
|
|
305
|
-
catch (error) { admittedBail("E_OPERATION_RESULT", "operation ran but outcome custody could not be confirmed", { unconfirmed: true, envelope, custody: { code: error.code, message: error.message } }); }
|
|
306
|
-
}
|
|
307
|
-
finishOperation({ r: child, bail: admittedBail, address, provider: capability.manifest, op: operation, argFlags, cwd, home, meta, cleanupError, settlement, ...(admission ? { intent: admission.intent } : {}) });
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** Fresh explicit captured scaffold + spawn hooks. Placement is supplied by
|
|
311
|
-
* the operator; launch and non-directory work remain unsupported. */
|
|
312
|
-
function capturedSpawn(selector, load, bail) {
|
|
313
|
-
const subject = args[1]; let home; let noLaunch = false;
|
|
314
|
-
if (!subject || subject.startsWith("-")) bail("E_BAD_ARGS", "captured spawn needs the retained subject name");
|
|
315
|
-
for (let index = 2; index < args.length; index++) {
|
|
316
|
-
const token = args[index];
|
|
317
|
-
if (token === "--json") continue;
|
|
318
|
-
if (token === "--no-launch") { if (noLaunch) bail("E_BAD_ARGS", "duplicate --no-launch"); noLaunch = true; continue; }
|
|
319
|
-
if (token === "--home") {
|
|
320
|
-
const value = args[++index];
|
|
321
|
-
if (home !== undefined || !value || value.startsWith("--") || !isAbsolute(value)) bail("E_BAD_ARGS", "--home needs one absolute new instance home");
|
|
322
|
-
home = resolve(value); continue;
|
|
323
|
-
}
|
|
324
|
-
bail("E_BAD_ARGS", `unsupported captured spawn argument ${JSON.stringify(token)}`);
|
|
325
|
-
}
|
|
326
|
-
if (!home || !noLaunch) bail("E_BAD_ARGS", "captured spawn currently requires --home <absolute new home> and --no-launch");
|
|
327
|
-
const inspected = load({ kind: "inspect" }), expected = inspected.record.subject.kind === "persistent" ? inspected.record.subject.soul.alias : inspected.record.subject.name;
|
|
328
|
-
if (subject !== expected) bail("E_HOME_MISMATCH", `captured resolution subject is ${expected}, not ${subject}`);
|
|
329
|
-
const scaffold = scaffoldCapturedInstance({ deployment: selector.deployment, resolution: selector.resolution, home, instance: basename(home) });
|
|
330
|
-
let activated;
|
|
331
|
-
try {
|
|
332
|
-
activated = activateCapturedScaffold({ deployment: selector.deployment, resolution: selector.resolution, home,
|
|
333
|
-
extraEnv: process.env.OATS_HOME_DIR ? { OATS_HOME_DIR: process.env.OATS_HOME_DIR } : {} });
|
|
334
|
-
} catch (error) {
|
|
335
|
-
if (error?.home) bail(error.code || "E_SPAWN_FAILED", error.message, { home: error.home, cleanupRequired: true, failures: error.provenance || [],
|
|
336
|
-
...(error.capturedCustody ? { unconfirmed: true, custody: error.capturedCustody } : {}) });
|
|
337
|
-
throw error;
|
|
338
|
-
}
|
|
339
|
-
const result = { ...scaffold, hooksPending: activated.hooksPending, cleanupRequired: activated.cleanupRequired, launchPending: true,
|
|
340
|
-
hookIntents: activated.hooks.intents, hookOrder: activated.hooks.order, warnings: activated.hooks.warnings };
|
|
341
|
-
if (JSON_MODE) jsonOk(result); else console.log(`Scaffolded ${result.instance} at ${result.home}; ${result.hooksPending ? "captured hook custody requires retry/reconciliation" : "captured hooks complete"}, launch pending`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/** Native continuation of an already owned captured home. A helper selector is
|
|
345
|
-
* an exact edge from the SOURCE record, not a name/current-config resolver. */
|
|
346
|
-
function capturedSession(selector, bail) {
|
|
347
|
-
const inspecting = args[1] === "inspect";
|
|
348
|
-
if (!["start", "restart", "inspect"].includes(args[1])) bail("unsupported-action", "captured session supports start/restart or explicit Pi outcome inspect; no current-context fallback was used");
|
|
349
|
-
const values = new Map(), allowed = new Set(inspecting ? ["home", "helper", "native-record"] : ["home", "helper", "request", "retry-intent"]);
|
|
350
|
-
for (let index = 2; index < args.length; index++) {
|
|
351
|
-
if (args[index] === "--json") continue;
|
|
352
|
-
const key = args[index].startsWith("--") ? args[index].slice(2) : "", value = args[index + 1];
|
|
353
|
-
if (!allowed.has(key) || values.has(key) || !value || value.startsWith("--")) bail("E_BAD_ARGS", "captured session needs unique named home/helper/request/retry arguments");
|
|
354
|
-
values.set(key, value); index++;
|
|
355
|
-
}
|
|
356
|
-
const home = values.get("home"), retry = values.get("retry-intent");
|
|
357
|
-
if (!home || !isAbsolute(home) || resolve(home) !== home || home.includes("\0")) bail("E_BAD_ARGS", "captured session needs a normalized absolute --home");
|
|
358
|
-
if (retry !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(retry)) bail("E_BAD_ARGS", "--retry-intent requires one saved executionId");
|
|
359
|
-
if (inspecting && !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(values.get("native-record") ?? "")) bail("E_BAD_ARGS", "captured Pi outcome inspect requires one explicit --native-record UUID");
|
|
360
|
-
const sourceExecutionBinding = { schemaVersion: 1, deployment: portableScope(selector.deployment), resolution: selector.resolution };
|
|
361
|
-
// Pure retained-subject check, before request files, provider or native calls.
|
|
362
|
-
// Dedicated helper IDs remain valid for scaffolding, not edge-less dispatch.
|
|
363
|
-
if (!values.has("helper") && readCapturedResolution(sourceExecutionBinding.deployment, sourceExecutionBinding.resolution).subject.kind === "helper") {
|
|
364
|
-
bail("helper-not-selected", "captured helper session needs SOURCE selectors plus --helper EXACT_SOURCE_HELPER_KEY");
|
|
365
|
-
}
|
|
366
|
-
// Reuse the same bounded strict object-file transport. Preparation and native
|
|
367
|
-
// request schemas remain separate; reject unknown fields before projection.
|
|
368
|
-
let request = {};
|
|
369
|
-
if (values.has("request")) {
|
|
370
|
-
const input = readPortablePreparationRequest({ file: values.get("request") });
|
|
371
|
-
if (input.schemaVersion !== 1 || Object.keys(input).some(key => !["schemaVersion", "backend", "task", "stopGraceMs"].includes(key))) bail("E_BAD_ARGS", "native request must be version1 with only backend/task/stopGraceMs");
|
|
372
|
-
if (Object.hasOwn(input, "backend") && (!input.backend || typeof input.backend !== "object" || Array.isArray(input.backend))) bail("E_BAD_ARGS", "a supplied native backend must be an explicit object, not omission");
|
|
373
|
-
const { schemaVersion, ...fields } = input; request = fields;
|
|
374
|
-
}
|
|
375
|
-
const helperSelection = values.has("helper") ? resolveCapturedHelper({ executionBinding: sourceExecutionBinding, helper: values.get("helper") }) : null;
|
|
376
|
-
const executionBinding = helperSelection?.executionBinding ?? sourceExecutionBinding;
|
|
377
|
-
try {
|
|
378
|
-
if (inspecting) {
|
|
379
|
-
const outcome = inspectCapturedPiOutcome(home, { ...executionBinding, nativeRecordId: values.get("native-record") });
|
|
380
|
-
const result = { outcome, executionBinding, ...(helperSelection ? { sourceExecutionBinding: helperSelection.sourceExecutionBinding, helper: helperSelection.helper } : {}) };
|
|
381
|
-
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
const native = startCapturedInstanceSession(home, { ...request, deployment: executionBinding.deployment, resolution: executionBinding.resolution,
|
|
385
|
-
restart: args[1] === "restart", ...(retry !== undefined ? { retryExecutionId: retry } : {}) });
|
|
386
|
-
const result = { ...native, executionBinding, ...(helperSelection ? { sourceExecutionBinding: helperSelection.sourceExecutionBinding, helper: helperSelection.helper } : {}) };
|
|
387
|
-
if (JSON_MODE) jsonOk(result); else console.log(`${result.replayed ? "Replayed captured dispatch receipt for" : "Dispatched captured native session for"} ${home}`);
|
|
388
|
-
} catch (error) {
|
|
389
|
-
bail(error.code || "E_SESSION_START_FAILED", error.message, { home: error.home ?? home, executionBinding,
|
|
390
|
-
...(helperSelection ? { sourceExecutionBinding: helperSelection.sourceExecutionBinding, helper: helperSelection.helper } : {}),
|
|
391
|
-
...(error.capturedCustody ? { custody: error.capturedCustody } : {}),
|
|
392
|
-
...(error.nativeCustody ? { unconfirmed: true, nativeCustody: error.nativeCustody } : {}) });
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/** Exact-selector dispatch enters before any current-context resolver. Its
|
|
397
|
-
* child receives the same selector, never an invoking agent's ambient identity. */
|
|
398
|
-
function capturedCommand(selector) {
|
|
399
|
-
const fail = (code, message, details) => JSON_MODE ? jsonFail(code, message, details) : die(message);
|
|
400
|
-
try {
|
|
401
|
-
const end = args.indexOf("--"), head = end < 0 ? args : args.slice(0, end);
|
|
402
|
-
const permitsHome = cmd === "operation" || cmd === "spawn" || cmd === "session";
|
|
403
|
-
const forbiddenContext = permitsHome ? ["--dir", "--server", "--soul", "--agents-root"] : ["--dir", "--home", "--server", "--soul", "--agents-root"];
|
|
404
|
-
if (head.some((arg) => forbiddenContext.includes(arg.split("=")[0]))) {
|
|
405
|
-
fail("E_BAD_ARGS", "captured selectors cannot be mixed with current-context selectors");
|
|
406
|
-
}
|
|
407
|
-
if (selector.artifactSet !== undefined) {
|
|
408
|
-
if (cmd !== "trust" || !args[1] || args[1].startsWith("-") || args.slice(2).some((arg) => arg !== "--json")) fail("E_BAD_ARGS", "artifact-set selectors support only explicit trust of one capability");
|
|
409
|
-
const result = approveAvailableCapability(selector.deployment, selector.artifactSet, args[1], {
|
|
410
|
-
kind: "operator", document: { kind: "operator", id: "oats-trust-artifact-set" }, pointer: "/capability",
|
|
411
|
-
});
|
|
412
|
-
if (JSON_MODE) jsonOk(result); else console.log(`${args[1]}: ${result.status}`);
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
const target = { deployment: selector.deployment, resolution: selector.resolution };
|
|
416
|
-
const load = (action, extra = {}) => loadCapturedDispatch({ ...target, action, ...extra });
|
|
417
|
-
if (cmd === "inspect") {
|
|
418
|
-
let helperKey;
|
|
419
|
-
for (let index = 1; index < args.length; index++) {
|
|
420
|
-
if (["--json", "--composition"].includes(args[index])) continue;
|
|
421
|
-
if (args[index] !== "--helper" || helperKey !== undefined || !args[index + 1] || args[index + 1].startsWith("--")) fail("E_BAD_ARGS", "captured inspect accepts --json, --composition and one --helper <exact-map-key>");
|
|
422
|
-
helperKey = args[++index];
|
|
423
|
-
}
|
|
424
|
-
const helperSelection = helperKey === undefined ? null : resolveCapturedHelper({ executionBinding: { schemaVersion: 1, ...target }, helper: helperKey });
|
|
425
|
-
const selected = helperSelection?.executionBinding ?? target;
|
|
426
|
-
const loaded = loadCapturedDispatch({ deployment: selected.deployment, resolution: selected.resolution, action: { kind: args.includes("--composition") ? "compose" : "inspect" } });
|
|
427
|
-
const result = { resolution: loaded.resolution, capture: loaded.record.capture, nativeSession: capturedNativeSessionAvailability(),
|
|
428
|
-
launchSelection: loaded.record.dispatch.launch === null ? null : {
|
|
429
|
-
runtime: loaded.record.dispatch.launch.runtime, model: loaded.record.dispatch.launch.model,
|
|
430
|
-
},
|
|
431
|
-
...(helperSelection ? { helperSelection } : {}),
|
|
432
|
-
capabilities: [...loaded.capabilities.values()].map(({ id, manifest }) => ({ id, version: manifest.version,
|
|
433
|
-
approval: loaded.approvals.find((entry) => entry.artifact.capability === id).status })),
|
|
434
|
-
helpers: Object.entries(loaded.record.helpers).map(([key, resolution]) => ({ key, resolution })),
|
|
435
|
-
hasComposition: !!loaded.record.dispatch.composition,
|
|
436
|
-
...(loaded.composition ? { composition: loaded.composition } : {}) };
|
|
437
|
-
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
if (cmd === "trust") {
|
|
441
|
-
if (!args[1] || args[1].startsWith("-") || args.slice(2).some((arg) => arg !== "--json")) fail("E_BAD_ARGS", "captured trust needs one capability ID");
|
|
442
|
-
load({ kind: "inspect" }); // complete manifest validation before approval
|
|
443
|
-
const result = approveCapturedCapability(selector.deployment, selector.resolution, args[1], {
|
|
444
|
-
kind: "operator", document: { kind: "operator", id: "oats-trust" }, pointer: "/capability",
|
|
445
|
-
});
|
|
446
|
-
if (JSON_MODE) jsonOk(result); else console.log(`${args[1]}: ${result.status}`);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
if (cmd === "operation") { capturedOperation(selector, load, fail); return; }
|
|
450
|
-
if (cmd === "spawn") { capturedSpawn(selector, load, fail); return; }
|
|
451
|
-
if (cmd === "session") { capturedSession(selector, fail); return; }
|
|
452
|
-
if (!cmd || cmd.startsWith("-") || KERNEL_COMMANDS.has(cmd)) fail("unsupported-action", "this kernel command has not yet adopted captured selectors; no current-context fallback was used");
|
|
453
|
-
if (!args[1] || args[1] === "--json" || head.some((arg) => HELP_WORDS.has(arg))) {
|
|
454
|
-
const loaded = load({ kind: "inspect" });
|
|
455
|
-
const matches = [...loaded.manifests.values()].filter((manifest) => manifest.command === cmd);
|
|
456
|
-
if (matches.length !== 1) fail("capability-not-selected", "captured command namespace is absent or ambiguous");
|
|
457
|
-
const result = { capability: matches[0].capability, commands: Object.keys(matches[0].commands || {}), help: "manifest only; no executable ran" };
|
|
458
|
-
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
const loaded = load({ kind: "command", namespace: cmd, name: args[1] });
|
|
462
|
-
const env = { ...process.env };
|
|
463
|
-
for (const key of Object.keys(env)) if (key.startsWith("OATS_") || key.startsWith("PI_AGENT_") || key === "PI_AGENTS_ROOT") delete env[key];
|
|
464
|
-
Object.assign(env, { OATS_DEPLOYMENT: selector.deployment, OATS_RESOLUTION: selector.resolution.id,
|
|
465
|
-
OATS_CAPABILITY: loaded.capability.id, OATS_CAPABILITY_ROOT: loaded.capability.manifest._dir,
|
|
466
|
-
OATS_SETTINGS: JSON.stringify(loaded.capability.settings),
|
|
467
|
-
OATS_CLI_BIN: CLI_BIN, OATS_CONTEXT: selector.deployment, OATS_LEVEL: selector.deployment });
|
|
468
|
-
const forwarded = args.slice(2); if (forwarded[0] === "--") forwarded.shift();
|
|
469
|
-
const child = withCapturedInvocationContextFile(loaded.invocation, contextEnv => withCapturedBindingFile(loaded, bindingEnv => spawnSync(process.execPath, [loaded.executable.file, ...loaded.executable.args, ...forwarded], {
|
|
470
|
-
cwd: selector.deployment, env: { ...env, ...contextEnv, ...bindingEnv }, stdio: "inherit",
|
|
471
|
-
})));
|
|
472
|
-
if (child.error) fail("E_CAPABILITY_BROKEN", child.error.message);
|
|
473
|
-
process.exit(child.status ?? 1);
|
|
474
|
-
} catch (error) { fail(error.code || "E_CAPABILITY_BROKEN", error.message); }
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/** Level of a directory: laptop (home), repo (.git), else workspace. */
|
|
478
|
-
function levelOf(dir) {
|
|
479
|
-
const d = resolve(dir);
|
|
480
|
-
if (d === homedir()) return "laptop";
|
|
481
|
-
if (existsSync(join(d, ".git"))) return "repo";
|
|
482
|
-
return "workspace";
|
|
91
|
+
const formatBytes = (n) => n < 1024 ? `${n} B` : n < 1024 ** 2 ? `${(n / 1024).toFixed(1)} KiB` : n < 1024 ** 3 ? `${(n / 1024 ** 2).toFixed(1)} MiB` : `${(n / 1024 ** 3).toFixed(1)} GiB`;
|
|
92
|
+
/** A retire recovery's copied outputs (untracked/ignored or directory work), named with their size. */
|
|
93
|
+
function preservedOutputLines(recovery) {
|
|
94
|
+
const outputs = recovery?.outputs;
|
|
95
|
+
if (!outputs?.paths?.length) return [];
|
|
96
|
+
const shown = outputs.paths.slice(0, 8).map((p) => `${p.path} (${formatBytes(p.bytes)})`);
|
|
97
|
+
const more = outputs.paths.length > 8 ? `, and ${outputs.paths.length - 8} more` : "";
|
|
98
|
+
return [` copied outputs: ${shown.join(", ")}${more} — ${formatBytes(outputs.bytes)} in total`];
|
|
483
99
|
}
|
|
484
100
|
|
|
485
101
|
function shortPath(p) {
|
|
@@ -493,56 +109,63 @@ function shellQuote(s) {
|
|
|
493
109
|
return /^[A-Za-z0-9._/~-]+$/.test(s) ? s : `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
494
110
|
}
|
|
495
111
|
|
|
496
|
-
/** The scaffolded `name:` value — the target directory's basename — held to the
|
|
497
|
-
* SAME write refusal as every other value this CLI renders into a config line.
|
|
498
|
-
*
|
|
499
|
-
* A basename is filesystem input, not a literal: a directory whose name embeds
|
|
500
|
-
* a newline turned one scaffolded `name:` line into arbitrary top-level config
|
|
501
|
-
* blocks (a live `team:` block smuggled through `oats init`), and a `#`-leading
|
|
502
|
-
* basename wrote a value that reads back as an empty map. Refusing names the
|
|
503
|
-
* offending basename and writes nothing — the operator renames the directory. */
|
|
504
|
-
function scaffoldConfigName(dir) {
|
|
505
|
-
return assertSafeConfigValue(basename(dir), `the scaffolded name from the directory basename ${JSON.stringify(basename(dir))}`);
|
|
506
|
-
}
|
|
507
112
|
|
|
508
113
|
// ---------- doctor ----------
|
|
509
114
|
/** Doctor must diagnose, not crash: a stale activation of a retired
|
|
510
115
|
* capability fails config resolution — surface the cleanup instruction
|
|
511
116
|
* cleanly (text or JSON) instead of an uncaught stack trace. */
|
|
512
|
-
function resolveForDoctor(ctx, soulName, { json } = {}) {
|
|
513
|
-
try { return resolveOatsConfig(ctx, soulName); }
|
|
514
|
-
catch (e) {
|
|
515
|
-
// Doctor is THE diagnosis surface: it alone catches the typed fail-closed
|
|
516
|
-
// invalid-lock error and continues to render actionable state.
|
|
517
|
-
if (e.code === "invalid-lock") {
|
|
518
|
-
const prov = Array.isArray(e.provenance) ? e.provenance[0] : undefined;
|
|
519
|
-
if (json) { console.log(JSON.stringify({ context: ctx, error: { code: "invalid-lock", message: e.message, provenance: e.provenance || null } }, null, 2)); process.exit(1); }
|
|
520
|
-
console.log(`oats doctor — resolved from ${shortPath(ctx)}\n`);
|
|
521
|
-
console.log(`ERROR: ${e.message} [invalid-lock]`);
|
|
522
|
-
if (prov?.file) console.log(` fix or remove the offending entry in ${shortPath(prov.file)} — the lock is never auto-repaired; all package operations fail closed until it is valid`);
|
|
523
|
-
process.exit(0); // doctor DIAGNOSED successfully; the lock is the problem
|
|
524
|
-
}
|
|
525
|
-
const retiredId = Object.keys(RETIRED_CAPABILITIES).find((id) => String(e.message).includes(`"${id}"`) && String(e.message).includes("retired"));
|
|
526
|
-
if (!retiredId) throw e;
|
|
527
|
-
if (json) { console.log(JSON.stringify({ schemaVersion: 1, context: ctx, error: e.message, retired: [retiredId] }, null, 2)); process.exit(1); }
|
|
528
|
-
die(`${e.message}`);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
117
|
function operationalKnowledgeNote(composition, soulName) {
|
|
532
118
|
return composition && !composition.oatsCoreDeclared
|
|
533
|
-
? `soul ${soulName} has no oats.core capability;
|
|
534
|
-
}
|
|
535
|
-
|
|
119
|
+
? `soul ${soulName} has no oats.core capability (the workspace default); it gets no OATS operating instructions` : null;
|
|
120
|
+
}
|
|
121
|
+
/** `doctor --soul`: the instructions an instance of that soul would carry. The soul
|
|
122
|
+
* is resolved over the workspace remotes exactly as a spawn preview resolves it,
|
|
123
|
+
* the kernel half composed, and the modules materialized into a scratch home
|
|
124
|
+
* OUTSIDE the deployment (removed after), so module injects are part of the text.
|
|
125
|
+
* Nothing in the deployment is written. Block files of module injects are named
|
|
126
|
+
* home-relative (`.oats/modules/<cap>/<inject>`), where an instance carries them. */
|
|
127
|
+
async function doctorComposition(ctx, soulName, ws, bail) {
|
|
536
128
|
if (!soulName) return undefined;
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
129
|
+
const { prepareInstance, previewWorkspaceSoul, materializePrepared, discoverOrStandalone } = await import("../lib/instance-resolution.mjs");
|
|
130
|
+
const deployment = dirname(ws.local.path);
|
|
131
|
+
const root = join(deployment, "agents");
|
|
132
|
+
const remoteOptions = remoteOptionsFromEnv();
|
|
133
|
+
const cleanups = [];
|
|
134
|
+
// A bail exits the process without unwinding: the temporary copies are removed at exit too.
|
|
135
|
+
process.once("exit", () => { for (const c of cleanups) { try { c(); } catch { /* best effort */ } } });
|
|
136
|
+
try {
|
|
137
|
+
const discovery = await discoverOrStandalone(loadLocal(deployment).local, { remoteOptions });
|
|
138
|
+
const prepared = await prepareInstance(deployment, soulName, { remoteOptions, discovery });
|
|
139
|
+
const pv = await previewWorkspaceSoul(prepared, root);
|
|
140
|
+
cleanups.push(pv.cleanup);
|
|
141
|
+
const agent = findAgentAt(root, prepared.soulEntry.name, pv.soulDir);
|
|
142
|
+
if (!agent) bail("E_SOUL_UNKNOWN", `soul "${soulName}" was fetched but is not readable as a soul`);
|
|
143
|
+
const composition = composeInstanceAgentsMd(pv.soulDir, deployment, agent.name, agent.work || "checkout", agent.kind, prepared);
|
|
144
|
+
const scratch = realpathSync(mkdtempSync(join(tmpdir(), "oats-doctor-home-")));
|
|
145
|
+
cleanups.push(() => rmSync(scratch, { recursive: true, force: true }));
|
|
146
|
+
const outcome = await materializePrepared({ ...prepared, soulAgentsMd: composition.text, soulDir: pv.soulDir }, scratch);
|
|
147
|
+
const text = readFileSync(join(scratch, "AGENTS.md"), "utf8");
|
|
148
|
+
const known = new Set(composition.blocks.map((b) => b.source));
|
|
149
|
+
const outcomeBlocks = Array.isArray(outcome?.blocks) ? outcome.blocks : [];
|
|
150
|
+
for (const m of text.matchAll(/^<!-- oats:(capability:[^\s]+) src=(.+?) -->$/gm)) {
|
|
151
|
+
const [, source, file] = m;
|
|
152
|
+
if (known.has(source)) continue;
|
|
153
|
+
known.add(source);
|
|
154
|
+
const content = outcomeBlocks.find((b) => b.source === source && b.file === file)?.content ?? (existsSync(file) ? readFileSync(file, "utf8").trim() : "");
|
|
155
|
+
const rel = file.startsWith(scratch + sep) ? file.slice(scratch.length + 1) : file;
|
|
156
|
+
composition.blocks.push({ source, file: rel, content, materialized: true });
|
|
157
|
+
}
|
|
158
|
+
return { ...composition, text };
|
|
159
|
+
} catch (e) {
|
|
160
|
+
if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details);
|
|
161
|
+
throw e;
|
|
162
|
+
} finally { for (const c of cleanups) { try { c(); } catch { /* best effort: temporary copies only */ } } }
|
|
541
163
|
}
|
|
542
164
|
|
|
543
165
|
/** Workspace-model v2 doctor data, OFFLINE: the deployment declaration found
|
|
544
166
|
* walking up from ctx (oats-local.yaml) and the lock v3 beside it. Doctor never
|
|
545
|
-
* goes to the network
|
|
167
|
+
* goes to the network for this view (only `--soul`, which resolves the soul like a
|
|
168
|
+
* spawn preview); membership and discovery are `oats sync` / `oats workspace status`. */
|
|
546
169
|
function doctorLockData(ctx) {
|
|
547
170
|
const out = { local: null, localError: null, lockFile: null, packages: [], lockError: null };
|
|
548
171
|
let lockDir = ctx;
|
|
@@ -551,7 +174,9 @@ function doctorLockData(ctx) {
|
|
|
551
174
|
out.local = { path: found.path, workspace: found.local.workspace };
|
|
552
175
|
lockDir = dirname(found.path);
|
|
553
176
|
} catch (e) {
|
|
554
|
-
|
|
177
|
+
// An unreadable oats-local.yaml, or a 0.25 oats-config.yaml inside the deployment
|
|
178
|
+
// (E_CONFIG_BROKEN reason legacy-config): doctor answers it as its typed error.
|
|
179
|
+
if (e?.code === "E_WORKSPACE_SCHEMA" || e?.code === "E_CONFIG_BROKEN") out.localError = { code: e.code, message: e.message, details: e.details };
|
|
555
180
|
else if (e?.code !== "E_LOCAL_MISSING") throw e;
|
|
556
181
|
}
|
|
557
182
|
const file = join(lockDir, LOCK_FILE);
|
|
@@ -559,7 +184,7 @@ function doctorLockData(ctx) {
|
|
|
559
184
|
out.lockFile = file;
|
|
560
185
|
try {
|
|
561
186
|
const lock = readLock(lockDir);
|
|
562
|
-
out.packages = Object.entries(lock.packages).map(([id, p]) => ({ id, version: p.version, source: p.source, path: p.path, commit: p.commit, integrity: p.integrity, capabilities: p.capabilities
|
|
187
|
+
out.packages = Object.entries(lock.packages).map(([id, p]) => ({ id, version: p.version, source: p.source, path: p.path, commit: p.commit, integrity: p.integrity, capabilities: p.capabilities }));
|
|
563
188
|
} catch (e) {
|
|
564
189
|
if (e?.code !== "E_LOCK_SCHEMA") throw e;
|
|
565
190
|
out.lockError = { code: e.code, message: e.message, file: e.details?.file ?? file };
|
|
@@ -567,45 +192,6 @@ function doctorLockData(ctx) {
|
|
|
567
192
|
return out;
|
|
568
193
|
}
|
|
569
194
|
|
|
570
|
-
/** The health of ONE materialized capability, against the rows it was projected
|
|
571
|
-
* from. Shared by doctor and list so both name the same states with the same
|
|
572
|
-
* codes — and so the `.oats-installation.json` provenance is checked in BOTH,
|
|
573
|
-
* not only deep inside trust resolution where it surfaces as a bare "untrusted".
|
|
574
|
-
*
|
|
575
|
-
* Order matters: a missing artifact cannot be hashed, drifted bytes make an
|
|
576
|
-
* approval meaningless (so trust is not ALSO reported), and provenance is only
|
|
577
|
-
* worth reading once the bytes are the locked ones. */
|
|
578
|
-
/** The lock rows AT one level. Never the merged maps: those resolve each
|
|
579
|
-
* identity independently, so an outer scope's capability can be paired with a
|
|
580
|
-
* nearer scope's package of the same id — a provider that never exported it. */
|
|
581
|
-
const levelRows = (locks, level) => locks.levels.find((l) => l.level === level) || { packages: Object.create(null), capabilities: Object.create(null) };
|
|
582
|
-
|
|
583
|
-
/** A capability has an executable surface when its manifest declares commands,
|
|
584
|
-
* hooks or launch environment — the things `oats trust` approves. A
|
|
585
|
-
* data-only capability (skills/injects) has none, and trust is not-applicable. */
|
|
586
|
-
function hasExecutableSurface(manifest) {
|
|
587
|
-
return !!(Object.keys(manifest?.commands || {}).length || Object.keys(manifest?.hooks || {}).length || (manifest?.environment?.length || 0));
|
|
588
|
-
}
|
|
589
|
-
function capabilityHealth(level, cap, capRow, pkgRow) {
|
|
590
|
-
const dir = installedCapabilityDir(level, cap.id);
|
|
591
|
-
if (!cap.installed) return { status: "missing", code: "missing-capability-artifact", dir, detail: `capability ${cap.id} is locked but not materialized — run \`oats sync\` to re-materialize it` };
|
|
592
|
-
let integrity;
|
|
593
|
-
try { integrity = capabilityArtifactIntegrity(dir); }
|
|
594
|
-
catch (e) { return { status: "broken", code: e.code || "invalid-capability-artifact", dir, detail: `capability ${cap.id}: ${e.message}` }; }
|
|
595
|
-
if (integrity !== cap.integrity) {
|
|
596
|
-
return { status: "drifted", code: "integrity-drift", dir, integrity, detail: `capability ${cap.id}: artifact integrity drift — installed ${integrity}, locked ${cap.integrity}; its executable approval is invalid` };
|
|
597
|
-
}
|
|
598
|
-
// The artifact's own provenance and the lock must tell the SAME story before
|
|
599
|
-
// either is believed. Neither silently wins; the disagreement is the finding.
|
|
600
|
-
if (capRow && pkgRow) {
|
|
601
|
-
try { verifyCapabilityInstallation(dir, cap.id, capRow, pkgRow); }
|
|
602
|
-
catch (e) { return { status: "provenance-mismatch", code: e.code || "invalid-lock", dir, integrity, detail: `capability ${cap.id}: ${e.message}` }; }
|
|
603
|
-
}
|
|
604
|
-
const executable = hasExecutableSurface(cap.manifest);
|
|
605
|
-
if (executable && !cap.trusted) return { status: "untrusted", code: "untrusted-surface", dir, integrity, detail: `capability ${cap.id}: executable surface UNTRUSTED — approve it in \`oats sync\`` };
|
|
606
|
-
return { status: "ok", code: null, dir, integrity, detail: null };
|
|
607
|
-
}
|
|
608
|
-
|
|
609
195
|
// ---------- inspect: one authoritative answer for GUIs ----------
|
|
610
196
|
/** Souls, capabilities (installed state and health, separately from
|
|
611
197
|
* activation), effective layer bindings and declared operations for a
|
|
@@ -614,35 +200,19 @@ function capabilityHealth(level, cap, capRow, pkgRow) {
|
|
|
614
200
|
* from its roster poll). Nothing here is provider-specific: what a
|
|
615
201
|
* knowledge provider offers is what its manifest declares. */
|
|
616
202
|
const INSPECT_TEXT_CAP = 256 * 1024;
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
catch (e) { return { file, text: null, sha256: null, truncated: false, error: `${e.code || "EIO"}: ${e.message}` }; }
|
|
621
|
-
const truncated = bytes.length > INSPECT_TEXT_CAP;
|
|
622
|
-
// The bound is bytes; a cut inside a multi-byte sequence is dropped, never
|
|
623
|
-
// rendered as a replacement character.
|
|
624
|
-
let text = truncated ? bytes.subarray(0, INSPECT_TEXT_CAP).toString("utf8") : bytes.toString("utf8");
|
|
625
|
-
if (truncated && text.endsWith("\uFFFD")) text = text.slice(0, -1);
|
|
626
|
-
return { file, text, sha256: createHash("sha256").update(bytes).digest("hex"), bytes: bytes.length, truncated, error: null };
|
|
627
|
-
}
|
|
628
|
-
/** The canonical agents root a home belongs to, from its path alone:
|
|
629
|
-
* <root>/<agent>/instances/<instance>, or <workspace>/local-agents/<agent>/
|
|
630
|
-
* instances/<instance> whose canonical root is the sibling agents/. */
|
|
631
|
-
function agentsRootOfHome(home) {
|
|
632
|
-
const agentDir = dirname(dirname(home));
|
|
633
|
-
const base = dirname(agentDir);
|
|
634
|
-
return basename(base) === "local-agents" ? join(dirname(base), "agents") : base;
|
|
635
|
-
}
|
|
203
|
+
/** The agents root a home belongs to, from its path alone:
|
|
204
|
+
* <root>/<agent>/instances/<instance>. */
|
|
205
|
+
function agentsRootOfHome(home) { return dirname(dirname(dirname(home))); }
|
|
636
206
|
const SOUL_FIELDS = ["runtime", "model", "yolo", "backend", "description", "launch-config"];
|
|
637
207
|
const realOrResolved = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
|
|
638
|
-
/** Every soul of a scope: persistent
|
|
208
|
+
/** Every soul of a scope: the persistent souls of every agents root in
|
|
639
209
|
* scope, plus packaged souls (read-only). One enumeration for inspect and
|
|
640
210
|
* operation run, so both address souls the same way. */
|
|
641
|
-
function scopeSouls(ctx,
|
|
211
|
+
function scopeSouls(ctx, { extraRoots = [] } = {}) {
|
|
642
212
|
// A home's own agents root is always in scope for that home: its recorded
|
|
643
213
|
// work repository may be another repository entirely (repo overrides), and
|
|
644
214
|
// the config context resolves there while the soul lives with its owner.
|
|
645
|
-
const roots = [...new Set([
|
|
215
|
+
const roots = [...new Set([findRoot(ctx), ...extraRoots].filter(Boolean).map((p) => realOrResolved(resolve(p))))];
|
|
646
216
|
const souls = [];
|
|
647
217
|
for (const root of roots) {
|
|
648
218
|
for (const a of listInstances(root)) {
|
|
@@ -652,17 +222,7 @@ function scopeSouls(ctx, r, { extraRoots = [] } = {}) {
|
|
|
652
222
|
souls.push(e);
|
|
653
223
|
}
|
|
654
224
|
}
|
|
655
|
-
|
|
656
|
-
try {
|
|
657
|
-
const packaged = listCapabilityAgents(ctx);
|
|
658
|
-
diagnostics.push(...(packaged.diagnostics || []));
|
|
659
|
-
for (const pa of packaged) {
|
|
660
|
-
let soul = {};
|
|
661
|
-
try { soul = stripInternalAnnotations(withConfigFile(join(pa.soulDir, "soul.yaml"), () => parseYamlNested(readFileSync(join(pa.soulDir, "soul.yaml"), "utf8")))); } catch { /* reported by name only */ }
|
|
662
|
-
souls.push(soulEntry({ ...soul, name: pa.name, description: pa.description ?? soul.description, soulDir: pa.soulDir }, roots[0] || ctx, { capability: pa.capability }));
|
|
663
|
-
}
|
|
664
|
-
} catch (e) { diagnostics.push({ code: e.code || "E_CAPABILITY_BROKEN", message: e.message }); }
|
|
665
|
-
return { roots, souls, diagnostics };
|
|
225
|
+
return { roots, souls, diagnostics: [] };
|
|
666
226
|
}
|
|
667
227
|
/** The one soul a name (and optional agents root) addresses; throws with a
|
|
668
228
|
* code when none or several match. */
|
|
@@ -745,215 +305,71 @@ function soulEntry(soul, root, { capability } = {}) {
|
|
|
745
305
|
* invoking process's ambient agents-root override must not redirect them
|
|
746
306
|
* to its own deployment. */
|
|
747
307
|
function dropAmbientRoot() { delete process.env.PI_AGENTS_ROOT; }
|
|
748
|
-
function inspectCmd() {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
308
|
+
async function inspectCmd() {
|
|
309
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
310
|
+
const t = await workspaceTarget(bail, { command: "inspect" });
|
|
311
|
+
if (t) {
|
|
312
|
+
if (t.resolutionError) return bail(t.resolutionError.code, t.resolutionError.message, t.resolutionError.details ?? undefined);
|
|
313
|
+
const doc = inspectDocument(t, { kernel: OATS_VERSION });
|
|
314
|
+
if (JSON_MODE) { jsonOk(doc); return; }
|
|
315
|
+
printWorkspaceInspect(doc); return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** The workspace-model target of inspect / readiness / operation run (lead
|
|
319
|
+
* decision 4): an instance home with materialized modules (`--home`), or a soul
|
|
320
|
+
* of a workspace deployment (`--soul`, resolved as its spawn would be). Anything
|
|
321
|
+
* else is a typed refusal: there is no classic scope answer. */
|
|
322
|
+
async function workspaceTarget(bail, { command, liveTeams = true }) {
|
|
754
323
|
dropAmbientRoot();
|
|
755
|
-
const homeFlag = flag("home");
|
|
756
|
-
|
|
324
|
+
const homeFlag = flag("home"), soulFlag = flag("soul"), rootFlag = flag("agents-root");
|
|
325
|
+
if (homeFlag === true) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
326
|
+
if (soulFlag === true) return bail("E_BAD_ARGS", "--soul needs a soul name");
|
|
327
|
+
if (rootFlag === true) return bail("E_BAD_ARGS", "--agents-root needs an absolute agents directory");
|
|
328
|
+
const remoteOptions = remoteOptionsFromEnv();
|
|
329
|
+
if (homeFlag) {
|
|
330
|
+
if (!isAbsolute(homeFlag)) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
331
|
+
let meta = null;
|
|
332
|
+
try { meta = JSON.parse(readFileSync(join(homeFlag, "instance.json"), "utf8")); } catch (e) { return bail("E_SESSION_UNKNOWN", `${homeFlag} is not an OATS instance home (${e.code === "ENOENT" ? "no instance.json" : e.message})`); }
|
|
333
|
+
if (isCapturedHome(meta)) { const e = capturedHomeRefusal(homeFlag, "nothing was read"); return bail(e.code, e.message, e.details); }
|
|
334
|
+
if (!meta || typeof meta.modules !== "object" || meta.modules === null) return bail("E_UNSUPPORTED_MODE", `${homeFlag} is not a workspace-model home (it records no modules): it was spawned by an earlier kernel — re-spawn it from the deployment`);
|
|
335
|
+
if (soulFlag && soulFlag !== meta.agent) return bail("E_HOME_MISMATCH", `--soul ${soulFlag} is not the soul of ${homeFlag} (${meta.agent})`);
|
|
336
|
+
const deployment = dirname(dirname(dirname(dirname(realOrResolved(homeFlag)))));
|
|
337
|
+
// A v2 home lives at <deployment>/agents/<soul>/instances/<name>: its deployment is
|
|
338
|
+
// derived, so it must hold oats-local.yaml EXACTLY there (never found by walking up).
|
|
339
|
+
if (!existsSync(join(deployment, "oats-local.yaml"))) return bail("E_HOME_MISMATCH", `${homeFlag} is not at <deployment>/agents/<soul>/instances/<name>: ${deployment} has no oats-local.yaml`, { home: homeFlag, expected: join(deployment, "oats-local.yaml") });
|
|
340
|
+
if (flag("dir") !== undefined) { let given = null; try { given = dirname(loadLocal(dirFlag()).path); } catch { given = dirFlag(); } if (realOrResolved(given) !== realOrResolved(deployment)) return bail("E_HOME_MISMATCH", `--dir ${dirFlag()} is not the deployment of ${homeFlag} (${deployment}); omit --dir for a home`); }
|
|
341
|
+
if (rootFlag && realOrResolved(rootFlag) !== realOrResolved(join(deployment, "agents"))) return bail("E_HOME_MISMATCH", `--agents-root ${rootFlag} is not the agents root of ${homeFlag}`);
|
|
342
|
+
return homeTarget(homeFlag, meta, { remoteOptions, discover: command === "readiness", live: liveTeams });
|
|
343
|
+
}
|
|
344
|
+
try { if (!isWorkspaceContext(dirFlag())) return bail("E_LOCAL_MISSING", `${command} reads a workspace deployment, and none is in reach of ${dirFlag()} (no oats-local.yaml walking up; \`oats onboard\` creates one) — or pass --home <abs> of a workspace instance`); }
|
|
345
|
+
catch (e) { return bail(e?.code || "E_WORKSPACE_SCHEMA", e?.message || String(e), e?.details); }
|
|
346
|
+
if (!soulFlag) return bail("E_BAD_ARGS", `${command} on a workspace deployment needs --soul <name> or --home <abs>${command === "inspect" ? " (the deployment's souls and capabilities: oats souls / oats capabilities)" : ""}`);
|
|
347
|
+
const deployment = dirname(loadLocal(dirFlag()).path);
|
|
348
|
+
if (rootFlag && realOrResolved(rootFlag) !== realOrResolved(join(deployment, "agents"))) return bail("E_SOUL_UNKNOWN", `soul "${soulFlag}" is not at agents root ${rootFlag} (this deployment's is ${join(deployment, "agents")})`);
|
|
349
|
+
try { return await soulTarget(dirFlag(), String(soulFlag), { remoteOptions }); }
|
|
350
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
351
|
+
}
|
|
352
|
+
function printWorkspaceInspect(doc) {
|
|
353
|
+
const s = doc.subject;
|
|
354
|
+
console.log(`oats inspect — ${s.kind === "instance" ? `instance ${s.instance} (soul ${s.soul}) ${shortPath(s.home)}` : `soul ${s.soul} from ${s.repoKey}`}`);
|
|
355
|
+
if (doc.identity) console.log(` identity: ${servedIdentityLine(doc.identity)}`);
|
|
356
|
+
for (const l of LAYERS) console.log(` ${l} capability: ${doc.layers[l].id || "none"}`);
|
|
357
|
+
for (const c of doc.capabilities) console.log(` ${c.id}@${c.version || "?"} ${c.from?.kind === "package" ? `package ${c.from.package}` : c.from?.kind === "member" ? `member ${c.from.repoKey}` : ""}${c.operations.length ? ` ops: ${c.operations.map((o) => `${o.name}${o.available ? "" : "(unavailable)"}`).join(", ")}` : ""}`);
|
|
358
|
+
for (const p of doc.problems) console.log(` ! ${p.code}: ${p.message}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** `{ teams, teamsSource }` for a session start of a workspace home: its eligible teams read
|
|
362
|
+
* live (two repository reads), which the launch hook re-checks joined memberships against
|
|
363
|
+
* (teams contract decision 6) — or the spawn record, marked `recorded`, when the read cannot
|
|
364
|
+
* answer. Anything that is not a readable workspace home gets nothing here — the start
|
|
365
|
+
* itself refuses it. */
|
|
366
|
+
async function homeLiveTeams(home) {
|
|
757
367
|
let meta;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
const real = realOrResolved;
|
|
765
|
-
let ctx;
|
|
766
|
-
if (meta) {
|
|
767
|
-
// The home is the identity and its recorded repository is ALWAYS its
|
|
768
|
-
// context (that is what composed it); an explicit --dir is accepted only
|
|
769
|
-
// as an alias naming that repository or the workspace of the home's
|
|
770
|
-
// agents root, and never replaces the context.
|
|
771
|
-
const contexts = homeContexts(home, meta);
|
|
772
|
-
if (flag("dir") !== undefined) { const given = dirFlag(); if (!contexts.some((c) => real(c) === real(given))) bail("E_HOME_MISMATCH", `--dir ${given} is not the context of ${home} (${contexts.join(" or ")}); omit --dir for a home`); }
|
|
773
|
-
ctx = contexts[0];
|
|
774
|
-
} else ctx = dirFlag();
|
|
775
|
-
const soulFlag = flag("soul");
|
|
776
|
-
if (soulFlag === true) bail("E_BAD_ARGS", "--soul needs a soul name");
|
|
777
|
-
if (meta && soulFlag && soulFlag !== meta.agent) bail("E_HOME_MISMATCH", `--soul ${soulFlag} is not the soul of ${home} (${meta.agent})`);
|
|
778
|
-
const soulName = soulFlag || meta?.agent || undefined;
|
|
779
|
-
let agentsRootFlag = flag("agents-root");
|
|
780
|
-
if (agentsRootFlag === true) bail("E_BAD_ARGS", "--agents-root needs an absolute agents directory");
|
|
781
|
-
if (meta) {
|
|
782
|
-
// The soul is the home's own, under the home's own root; same-named souls
|
|
783
|
-
// in other member repositories are ordinary and never ambiguous here.
|
|
784
|
-
const homeRoot = agentsRootOfHome(real(home));
|
|
785
|
-
if (agentsRootFlag && real(agentsRootFlag) !== real(homeRoot)) bail("E_HOME_MISMATCH", `--agents-root ${agentsRootFlag} is not the agents root of ${home} (${homeRoot})`);
|
|
786
|
-
agentsRootFlag = homeRoot;
|
|
787
|
-
}
|
|
788
|
-
let r;
|
|
789
|
-
try { r = resolveOatsConfig(ctx, soulName); } catch (e) { bail(e.code || "E_CONFIG_BROKEN", e.message); }
|
|
790
|
-
let chain = configChain(ctx);
|
|
791
|
-
const enumerated = scopeSouls(ctx, r, { extraRoots: meta ? [agentsRootOfHome(realOrResolved(home))] : [] });
|
|
792
|
-
const roots = enumerated.roots;
|
|
793
|
-
let souls = enumerated.souls;
|
|
794
|
-
const packagedDiagnostics = enumerated.diagnostics;
|
|
795
|
-
let selectedSoul = null;
|
|
796
|
-
const requestedContext = ctx;
|
|
797
|
-
if (soulName) {
|
|
798
|
-
try { selectedSoul = selectSoul(souls, soulName, agentsRootFlag, ctx); } catch (e) { bail(e.code || "E_SOUL_UNKNOWN", e.message); }
|
|
799
|
-
selectedSoul.instructions = readTextCapped(selectedSoul.instructionsFile);
|
|
800
|
-
souls = [selectedSoul];
|
|
801
|
-
// A soul's effective bindings are its own member's: a team root or
|
|
802
|
-
// another member's --dir must not be applied to it. (A home keeps its
|
|
803
|
-
// recorded repository as its context; that is what composed it.)
|
|
804
|
-
if (!meta) {
|
|
805
|
-
const member = memberContextOf(selectedSoul, ctx, flag("dir") !== undefined, bail);
|
|
806
|
-
if (member !== realOrResolved(ctx)) {
|
|
807
|
-
ctx = member;
|
|
808
|
-
try { r = resolveOatsConfig(ctx, soulName); } catch (e) { bail(e.code || "E_CONFIG_BROKEN", e.message); }
|
|
809
|
-
chain = configChain(ctx);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Capabilities: installed state and health from the package engine (exactly
|
|
815
|
-
// what `oats list` reports), owned/path manifests beside them, and the
|
|
816
|
-
// ACTIVATION for the selected soul (or global) from the resolver.
|
|
817
|
-
const mans = capabilityManifests(manifestSource(meta, home, ctx));
|
|
818
|
-
let lockError = null;
|
|
819
|
-
const byId = new Map();
|
|
820
|
-
try {
|
|
821
|
-
const pkgs = listInstalledPackages(ctx), locks = readPackageLocks(ctx);
|
|
822
|
-
for (const p of pkgs) {
|
|
823
|
-
const rows = levelRows(locks, p.level);
|
|
824
|
-
for (const c of p.capabilities) {
|
|
825
|
-
const h = capabilityHealth(p.level, c, rows.capabilities[c.id], rows.packages[p.package]);
|
|
826
|
-
byId.set(c.id, {
|
|
827
|
-
id: c.id, package: p.package, version: c.version || null, layer: c.manifest?.layer || null, command: c.manifest?.command || null,
|
|
828
|
-
origin: "installed", level: p.level, source: p.source || null, commit: p.commit ?? rows.packages[p.package]?.commit ?? null, dir: h.dir,
|
|
829
|
-
health: { status: h.status, code: h.code, detail: h.detail, installed: !!c.installed, locked: true, trusted: c.trusted === true, executableSurface: hasExecutableSurface(c.manifest), integrity: c.integrity || null, installedIntegrity: h.integrity ?? null },
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
} catch (e) { lockError = { code: e.code || "invalid-lock", message: e.message }; }
|
|
834
|
-
for (const [id, m] of Object.entries(mans)) {
|
|
835
|
-
if (byId.has(id)) continue;
|
|
836
|
-
const trust = capabilityTrust(m, ctx);
|
|
837
|
-
const executable = hasExecutableSurface(m);
|
|
838
|
-
let integrity = trust.integrity || null;
|
|
839
|
-
if (!integrity) { try { integrity = capabilityArtifactIntegrity(m._dir); } catch { integrity = null; } }
|
|
840
|
-
byId.set(id, {
|
|
841
|
-
id, package: m._package || null, version: m.version || null, layer: m.layer || null, command: m.command || null,
|
|
842
|
-
origin: String(m._origin || "").split(":")[0] || "unknown", level: String(m._origin || "").split(":").slice(1).join(":") || null, source: null, dir: m._dir,
|
|
843
|
-
health: { status: executable && !trust.trusted ? "untrusted" : "ok", code: executable && !trust.trusted ? "untrusted-surface" : null, detail: executable && !trust.trusted ? (trust.reason || null) : null, executableSurface: executable, installed: true, locked: !!trust.lock, trusted: !!trust.trusted, integrity, installedIntegrity: integrity },
|
|
844
|
-
});
|
|
845
|
-
}
|
|
846
|
-
// What is EFFECTIVE for the answer: a home's captured bindings and settings
|
|
847
|
-
// (with the currently acquired manifests and current trust); a soul's or
|
|
848
|
-
// scope's current config otherwise. The current config is reported
|
|
849
|
-
// separately for a home so a GUI can show both without confusing them.
|
|
850
|
-
const snapshotCaps = meta ? (meta.capabilities || []) : null;
|
|
851
|
-
const layerIdOf = (rec) => { const m = typeof rec === "string" ? /^([a-z0-9][a-z0-9._-]*)(?:\s|$)/.exec(rec) : null; return m && m[1] !== "none" ? m[1] : null; };
|
|
852
|
-
const effectiveLayers = meta
|
|
853
|
-
? Object.fromEntries(LAYERS.map((l) => { const id = layerIdOf(meta.layers?.[l]) || snapshotCaps.find((c) => mans[c.id]?.layer === l)?.id || null; const rec = typeof meta.layers?.[l] === "string" ? meta.layers[l] : null; return [l, { id, level: snapshotCaps.find((c) => c.id === id)?.level || null, provenance: rec, disabled: !id && !!rec && rec.startsWith("none") }]; }))
|
|
854
|
-
: Object.fromEntries(LAYERS.map((l) => [l, r.layers[l]
|
|
855
|
-
? { id: r.layers[l].id, level: r.layers[l].level, provenance: r.provenance[l] || null, disabled: false }
|
|
856
|
-
: { id: null, level: r.layerDisabled?.[l]?.level || null, provenance: r.provenance[l] || null, disabled: !!r.layerDisabled?.[l] }]));
|
|
857
|
-
const effectiveActive = (id) => meta
|
|
858
|
-
? (() => { const c = snapshotCaps.find((x) => x.id === id); return c ? { id, level: c.level || null, provenance: c.provenance || [], settings: c.settings || {} } : undefined; })()
|
|
859
|
-
: r.capabilities.find((c) => c.id === id);
|
|
860
|
-
const declaredAt = (id) => chain.flatMap((cfg) => configCapabilityEntries(cfg).filter((e) => e.id === id).map((e) => ({ level: cfg._level, slot: e.slot || null, targets: [
|
|
861
|
-
...(e.spec.global !== undefined ? [`global`] : []),
|
|
862
|
-
...Object.keys(e.spec["agent-types"] || {}).map((t) => `type:${t}`),
|
|
863
|
-
...Object.keys(e.spec.souls || {}).map((sn) => `soul:${sn}`),
|
|
864
|
-
] })));
|
|
865
|
-
const targetOf = (provenance) => [...provenance].map((p) => p.split(" @ ")[0]).sort((a, b) => (b.startsWith("soul:") ? 2 : b.startsWith("type:") ? 1 : 0) - (a.startsWith("soul:") ? 2 : a.startsWith("type:") ? 1 : 0))[0] || (meta ? "snapshot" : "global");
|
|
866
|
-
const capabilities = [...byId.values()].sort((a, b) => a.id.localeCompare(b.id)).map((entry) => {
|
|
867
|
-
const active = effectiveActive(entry.id);
|
|
868
|
-
const m = mans[entry.id];
|
|
869
|
-
const missingRequires = (() => { try { return capabilityMissingRequires(entry.id, ctx).map((x) => ({ command: x.command, why: x.why || null, install: x.install || null })); } catch { return []; } })();
|
|
870
|
-
const disabledLayer = entry.layer && (meta ? (effectiveLayers[entry.layer]?.disabled ? { level: null } : null) : r.layerDisabled?.[entry.layer]);
|
|
871
|
-
const declared = declaredAt(entry.id);
|
|
872
|
-
const activation = active
|
|
873
|
-
? { enabled: true, source: meta ? "snapshot" : "config", target: targetOf(active.provenance || []), level: active.level, provenance: active.provenance || [], settings: active.settings || {}, declaredAt: declared }
|
|
874
|
-
: { enabled: false, source: meta ? "snapshot" : "config", target: declared.length ? "declared" : "none", level: declared[0]?.level || null, provenance: [], settings: {}, declaredAt: declared, ...(disabledLayer ? { reason: `layer ${entry.layer} is disabled${disabledLayer.level ? ` at ${disabledLayer.level}` : " for this home"}` } : {}) };
|
|
875
|
-
const operations = manifestOperations(m).map((op) => {
|
|
876
|
-
let reason = null;
|
|
877
|
-
if (!active) reason = disabledLayer ? `layer ${entry.layer} is disabled${disabledLayer.level ? ` at ${disabledLayer.level}` : " for this home"}` : `${entry.id} is not activated for ${meta ? `home ${basename(home)}` : soulName ? `soul ${soulName}` : "this scope"}`;
|
|
878
|
-
else if (!entry.health.trusted) reason = `${entry.id} executable surface is not trusted (approve it in oats sync)`;
|
|
879
|
-
else if (entry.health.status !== "ok") reason = entry.health.detail || entry.health.status;
|
|
880
|
-
else if (missingRequires.length) reason = `${entry.id} requires ${missingRequires.map((x) => `"${x.command}" on PATH${x.why ? ` (${x.why})` : ""}`).join(", ")}`;
|
|
881
|
-
else if (op.context === "home" && !home) reason = "needs a running home (--home)";
|
|
882
|
-
return { ...op, argv: [entry.command, op.command], available: !reason, reason };
|
|
883
|
-
});
|
|
884
|
-
return { ...entry, missingRequires, activation, operations };
|
|
885
|
-
});
|
|
886
|
-
const layers = effectiveLayers;
|
|
887
|
-
// For a home, the CURRENT config beside the captured bindings, so a GUI can
|
|
888
|
-
// show what future instances would get without mistaking it for the home's.
|
|
889
|
-
const currentConfig = meta ? {
|
|
890
|
-
layers: Object.fromEntries(LAYERS.map((l) => [l, r.layers[l] ? { id: r.layers[l].id, level: r.layers[l].level, provenance: r.provenance[l] || null, disabled: false } : { id: null, level: r.layerDisabled?.[l]?.level || null, provenance: r.provenance[l] || null, disabled: !!r.layerDisabled?.[l] }])),
|
|
891
|
-
activations: r.capabilities.map((c) => ({ id: c.id, target: targetOf(c.provenance), level: c.level, settings: c.settings || {} })),
|
|
892
|
-
} : null;
|
|
893
|
-
const knowledgeCap = layers.knowledge.id ? capabilities.find((c) => c.id === layers.knowledge.id) : null;
|
|
894
|
-
const knowledge = knowledgeCap ? { provider: knowledgeCap.id, version: knowledgeCap.version, operations: knowledgeCap.operations.map((o) => ({ name: o.name, kind: o.kind, available: o.available, reason: o.reason })) } : { provider: null, version: null, operations: [] };
|
|
895
|
-
|
|
896
|
-
let snapshot = null;
|
|
897
|
-
if (meta) {
|
|
898
|
-
const runtimeById = new Map((meta.capabilityRuntime || []).map((c) => [c.id, c]));
|
|
899
|
-
const drift = [];
|
|
900
|
-
for (const c of meta.capabilities || []) {
|
|
901
|
-
const now = r.capabilities.find((x) => x.id === c.id);
|
|
902
|
-
if (!now) { drift.push({ id: c.id, field: "activation", snapshot: true, config: false }); continue; }
|
|
903
|
-
if (JSON.stringify(c.settings || {}) !== JSON.stringify(now.settings || {})) drift.push({ id: c.id, field: "settings", snapshot: c.settings || {}, config: now.settings || {} });
|
|
904
|
-
const then = runtimeById.get(c.id)?.trust?.integrity, cur = byId.get(c.id)?.health?.integrity;
|
|
905
|
-
if (then && cur && then !== cur) drift.push({ id: c.id, field: "integrity", snapshot: then, config: cur });
|
|
906
|
-
}
|
|
907
|
-
for (const now of r.capabilities) if (!(meta.capabilities || []).some((c) => c.id === now.id)) drift.push({ id: now.id, field: "activation", snapshot: false, config: true });
|
|
908
|
-
snapshot = {
|
|
909
|
-
home, instance: meta.instance, agent: meta.agent, runtime: meta.runtime || null, model: meta.model ?? null, yolo: meta.yolo ?? null, launched: !!meta.launched, createdAt: meta.createdAt || null,
|
|
910
|
-
layers: meta.layers || {}, capabilities: (meta.capabilities || []).map((c) => ({ id: c.id, level: c.level, settings: c.settings || {}, trusted: runtimeById.get(c.id)?.trust?.trusted ?? null })),
|
|
911
|
-
instructions: { ...readTextCapped(join(home, "AGENTS.md")), sources: meta.instructions || [] }, drift,
|
|
912
|
-
};
|
|
913
|
-
}
|
|
914
|
-
// K4: the deployment's portable source context and each soul's declared
|
|
915
|
-
// requirements joined against the capability inventory in this same payload.
|
|
916
|
-
// Readiness is about the soul's declared sources, kept apart from
|
|
917
|
-
// launchability (spawn) and adoption (prepare); unobservable = null.
|
|
918
|
-
const capabilityById = new Map(capabilities.map((c) => [c.id, c]));
|
|
919
|
-
for (const s of souls) {
|
|
920
|
-
const required = s.declarations?.requires?.capabilities;
|
|
921
|
-
const requirements = required && typeof required === "object" ? Object.entries(required).map(([id, spec]) => {
|
|
922
|
-
const cap = capabilityById.get(id) || null;
|
|
923
|
-
return { capability: id, source: spec && typeof spec === "object" ? spec.source ?? null : null,
|
|
924
|
-
installed: cap ? cap.health?.installed ?? null : false, approved: cap ? cap.health?.trusted ?? null : null,
|
|
925
|
-
active: cap ? !!cap.activation?.enabled : null, version: cap?.version ?? null };
|
|
926
|
-
}) : null;
|
|
927
|
-
s.readiness = { source: s.provenance ? "recorded" : "unrecorded", requirements,
|
|
928
|
-
status: requirements === null ? "undeclared" : requirements.every((q) => q.installed === true) ? "sources-installed" : requirements.some((q) => q.installed === false) ? "sources-missing" : "unknown" };
|
|
929
|
-
}
|
|
930
|
-
const sourceKey = (p) => JSON.stringify([p.source, p.revision, p.path]);
|
|
931
|
-
const sourceItems = [...new Map(souls.filter((s) => s.provenance?.source).map((s) => [sourceKey(s.provenance), { ...s.provenance, souls: [] }])).values()];
|
|
932
|
-
for (const s of souls) if (s.provenance?.source) sourceItems.find((i) => sourceKey(i) === sourceKey(s.provenance)).souls.push(s.name);
|
|
933
|
-
const sources = { soulsApi: 1, kind: sourceItems.length ? "recorded-provenance" : "none-recorded", items: sourceItems,
|
|
934
|
-
note: sourceItems.length ? null : "no soul in this scope records a portable source address" };
|
|
935
|
-
const result = {
|
|
936
|
-
operationsApi: 1, kernel: OATS_VERSION,
|
|
937
|
-
scope: { context: ctx, requestedContext: requestedContext === ctx ? null : requestedContext, workspace: roots.length ? workspaceOf(roots[0]) : ctx, team: r.team || null, chain: chain.map((c) => ({ file: c._file, level: c._level, levelKind: levelOf(c._level) })), agentsRoots: roots },
|
|
938
|
-
selected: { soul: selectedSoul?.name || null, agentsRoot: selectedSoul?.agentsRoot || null, home: home || null, source: meta ? "snapshot" : "config",
|
|
939
|
-
// Decision 27 (K2): the principal this home acts as, from its messaging provider's hook meta.
|
|
940
|
-
...(meta ? { identity: servedIdentityOf(meta) } : {}) },
|
|
941
|
-
souls, sources, layers, capabilities, knowledge, snapshot, currentConfig,
|
|
942
|
-
problems: [...(lockError ? [lockError] : []), ...packagedDiagnostics.map((d) => ({ code: d.code, message: d.message, capability: d.capability })),
|
|
943
|
-
...(meta ? snapshotCaps.filter((c) => !mans[c.id]).map((c) => ({ code: "captured-capability-missing", message: `${c.id} was active when this home was composed but no manifest for it is acquired now`, capability: c.id })) : [])],
|
|
944
|
-
};
|
|
945
|
-
result._print = { ctx, selectedSoul, home };
|
|
946
|
-
return result;
|
|
947
|
-
}
|
|
948
|
-
function printInspect(result) {
|
|
949
|
-
const { ctx, selectedSoul, home } = result._print; delete result._print;
|
|
950
|
-
const { souls, layers, capabilities } = result;
|
|
951
|
-
console.log(`oats inspect — ${shortPath(ctx)}${selectedSoul ? ` soul ${selectedSoul.name}` : ""}${home ? ` home ${shortPath(home)}` : ""}`);
|
|
952
|
-
if (result.selected?.identity) console.log(` identity: ${servedIdentityLine(result.selected.identity)}`);
|
|
953
|
-
for (const s of souls) console.log(` soul ${s.name} [${s.kind}${s.capability ? ` ${s.capability}` : ""}] runtime ${s.runtime}${s.model ? ` model ${s.model}` : ""} work ${s.work}${s.editable.fields.length ? "" : " (read-only)"}`);
|
|
954
|
-
for (const l of LAYERS) console.log(` layer ${l}: ${layers[l].id || (layers[l].disabled ? "disabled" : "none")}${layers[l].provenance ? ` (${layers[l].provenance})` : ""}`);
|
|
955
|
-
for (const c of capabilities) console.log(` ${c.id}@${c.version || "?"} ${c.health.status}${c.activation.enabled ? ` active:${c.activation.target}` : " inactive"}${c.operations.length ? ` ops: ${c.operations.map((o) => `${o.name}${o.available ? "" : "(unavailable)"}`).join(", ")}` : ""}`);
|
|
956
|
-
for (const p of result.problems) console.log(` ! ${p.code}: ${p.message}`);
|
|
368
|
+
try { meta = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); } catch { return {}; }
|
|
369
|
+
if (!isWorkspaceHome(meta)) return {};
|
|
370
|
+
const { liveTeams } = await import("../lib/instance-resolution.mjs");
|
|
371
|
+
const { teams, source } = await liveTeams(home, meta, { remoteOptions: remoteOptionsFromEnv() });
|
|
372
|
+
return Array.isArray(teams) ? { teams, teamsSource: source } : {};
|
|
957
373
|
}
|
|
958
374
|
|
|
959
375
|
// ---------- operation run: generic invoke through the capability engine ----------
|
|
@@ -969,11 +385,11 @@ const OPERATION_ADDRESS_RE = /^(knowledge|messaging|tasks):([a-z][a-z0-9-]*)$/;
|
|
|
969
385
|
const reportsRetainedEffectsText = (message) => /INCOMPLETE|quarantin|retain|could not (?:be )?(?:verif|confirm)/i.test(String(message || ""));
|
|
970
386
|
// Comfortably below the scheduler's 5-minute command bound and any GUI
|
|
971
387
|
// proxy, so the receipt always reaches the caller before a wrapper gives up.
|
|
972
|
-
const OPERATION_TIMEOUT_MS =
|
|
973
|
-
function finishOperation({ r, bail, address, provider, op, argFlags, cwd, home, meta, cleanupError, intent, settlement }) {
|
|
388
|
+
const OPERATION_TIMEOUT_MS = 4 * 60 * 1000;
|
|
389
|
+
function finishOperation({ r, bail, address, provider, op, argFlags, cwd, home, meta, cleanupError, intent, settlement, api }) {
|
|
974
390
|
const stderr = String(r.stderr || "").trim();
|
|
975
391
|
const timedOut = r.error?.code === "ETIMEDOUT" || (r.status === null && ["SIGTERM", "SIGKILL"].includes(r.signal) && (!settlement || !r.error));
|
|
976
|
-
const base = { operation: address, capability: provider.capability, version: provider.version || null, argv: [provider.command, op.command, ...argFlags], cwd, target: home ? { home, instance: meta.instance } : null };
|
|
392
|
+
const base = { ...(api ? { operationsApi: api } : {}), operation: address, capability: provider.capability, version: provider.version || null, argv: [provider.command, op.command, ...argFlags], cwd, target: home ? { home, instance: meta.instance } : null };
|
|
977
393
|
// Unconfirmed outcomes (a timeout, no valid receipt, a receipt contradicted
|
|
978
394
|
// by the exit status) carry what WAS observed in error.details, so a
|
|
979
395
|
// scheduler can keep the slot as unknown and reconcile by any name the
|
|
@@ -1018,58 +434,8 @@ function finishOperation({ r, bail, address, provider, op, argFlags, cwd, home,
|
|
|
1018
434
|
else console.log(JSON.stringify(result, null, 2));
|
|
1019
435
|
if (stderr) console.error(stderr);
|
|
1020
436
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
dropAmbientRoot();
|
|
1024
|
-
if (args[1] !== "run") bail("E_USAGE", "usage: oats operation run <layer>:<name> (--home <abs> | --soul <name> [--dir <scope>] [--agents-root <abs>]) [--arg k=v ...] [--json]");
|
|
1025
|
-
const address = args[2];
|
|
1026
|
-
const m0 = typeof address === "string" ? OPERATION_ADDRESS_RE.exec(address) : null;
|
|
1027
|
-
if (!m0) bail("E_BAD_ARGS", `operation address must be <layer>:<name> with layer one of ${LAYERS.join(", ")} (got ${JSON.stringify(address)})`);
|
|
1028
|
-
const [, layer, opName] = m0;
|
|
1029
|
-
const homeFlag = flag("home");
|
|
1030
|
-
if (homeFlag === true) bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
1031
|
-
const home = homeFlag ? resolve(homeFlag) : undefined;
|
|
1032
|
-
let meta;
|
|
1033
|
-
if (home) {
|
|
1034
|
-
if (!isAbsolute(homeFlag)) bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
1035
|
-
const metaFile = join(home, "instance.json");
|
|
1036
|
-
if (!existsSync(metaFile)) bail("E_SESSION_UNKNOWN", `${home} is not an OATS instance home (no instance.json)`);
|
|
1037
|
-
try { meta = JSON.parse(readFileSync(metaFile, "utf8")); } catch (e) { bail("E_SESSION_UNKNOWN", `${metaFile}: ${e.message}`); }
|
|
1038
|
-
}
|
|
1039
|
-
// The home is the identity: an explicit --dir must be one of its own
|
|
1040
|
-
// contexts, never a different scope's config applied to it.
|
|
1041
|
-
let ctx;
|
|
1042
|
-
if (meta) {
|
|
1043
|
-
// The recorded repository is always a home's context; --dir is only an
|
|
1044
|
-
// alias to validate (the repository or the workspace of the home's root).
|
|
1045
|
-
const contexts = homeContexts(home, meta);
|
|
1046
|
-
if (flag("dir") !== undefined) { const given = dirFlag(); if (!contexts.some((c) => realOrResolved(c) === realOrResolved(given))) bail("E_HOME_MISMATCH", `--dir ${given} is not the context of ${home} (${contexts.join(" or ")}); omit --dir for a home`); }
|
|
1047
|
-
ctx = contexts[0];
|
|
1048
|
-
} else ctx = dirFlag();
|
|
1049
|
-
const soulFlag = flag("soul");
|
|
1050
|
-
if (soulFlag === true) bail("E_BAD_ARGS", "--soul needs a soul name");
|
|
1051
|
-
if (meta && soulFlag && soulFlag !== meta.agent) bail("E_HOME_MISMATCH", `--soul ${soulFlag} is not the soul of ${home} (${meta.agent})`);
|
|
1052
|
-
const soulName = soulFlag || meta?.agent || undefined;
|
|
1053
|
-
let agentsRootFlag = flag("agents-root");
|
|
1054
|
-
if (agentsRootFlag === true) bail("E_BAD_ARGS", "--agents-root needs an absolute agents directory");
|
|
1055
|
-
if (meta) {
|
|
1056
|
-
const homeRoot = agentsRootOfHome(realOrResolved(home));
|
|
1057
|
-
if (agentsRootFlag && realOrResolved(agentsRootFlag) !== realOrResolved(homeRoot)) bail("E_HOME_MISMATCH", `--agents-root ${agentsRootFlag} is not the agents root of ${home} (${homeRoot})`);
|
|
1058
|
-
agentsRootFlag = homeRoot;
|
|
1059
|
-
}
|
|
1060
|
-
// The same soul selection as inspect: name plus agents root, refused when
|
|
1061
|
-
// ambiguous, never silently the first match.
|
|
1062
|
-
let selectedSoul;
|
|
1063
|
-
if (soulName) {
|
|
1064
|
-
let rSel;
|
|
1065
|
-
try { rSel = resolveOatsConfig(ctx, meta ? undefined : soulName); } catch (e) { bail(e.code || "E_CONFIG_BROKEN", e.message); }
|
|
1066
|
-
try { selectedSoul = selectSoul(scopeSouls(ctx, rSel, { extraRoots: meta ? [agentsRootOfHome(realOrResolved(home))] : [] }).souls, soulName, agentsRootFlag, ctx); } catch (e) { bail(e.code || "E_SOUL_UNKNOWN", e.message); }
|
|
1067
|
-
// The provider and its settings are the selected soul's own member's,
|
|
1068
|
-
// never a team root's or another member's (a home keeps its recorded
|
|
1069
|
-
// repository as its context).
|
|
1070
|
-
if (!meta) ctx = memberContextOf(selectedSoul, ctx, flag("dir") !== undefined, bail);
|
|
1071
|
-
}
|
|
1072
|
-
// --arg k=v pairs, matched against the operation's declared args below.
|
|
437
|
+
/** --arg k=v pairs of `oats operation run`. */
|
|
438
|
+
function operationArgs(bail) {
|
|
1073
439
|
const given = Object.create(null);
|
|
1074
440
|
for (let i = 3; i < args.length; i++) {
|
|
1075
441
|
if (args[i] !== "--arg") continue;
|
|
@@ -1079,210 +445,92 @@ function operationCmd() {
|
|
|
1079
445
|
given[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
1080
446
|
i++;
|
|
1081
447
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
let r;
|
|
1095
|
-
try { r = resolveOatsConfig(ctx, soulName); } catch (e) { bail(e.code || "E_CONFIG_BROKEN", e.message); }
|
|
1096
|
-
provider = r.layers[layer] ? mans[r.layers[layer].id] : undefined;
|
|
1097
|
-
settings = r.layers[layer]?.settings || {};
|
|
1098
|
-
team = r.team;
|
|
1099
|
-
if (!provider && r.layerDisabled?.[layer]) disabled = `none @ ${r.layerDisabled[layer].level}`;
|
|
1100
|
-
}
|
|
1101
|
-
if (!provider) bail("E_OPERATION_UNAVAILABLE", disabled ? `layer ${layer} is explicitly disabled (${disabled}); no provider can run ${address}` : `no ${layer} provider is active for ${meta ? home : soulName ? `soul ${soulName} in ${ctx}` : ctx}`);
|
|
448
|
+
return given;
|
|
449
|
+
}
|
|
450
|
+
/** `oats operation run` on the workspace model (operationsApi 2): the provider is
|
|
451
|
+
* the module filling <layer> — the home's own copy, or the soul's resolved module
|
|
452
|
+
* fetched into the deployment's module store — with its merged payload as
|
|
453
|
+
* OATS_SETTINGS and the team facts hooks get. */
|
|
454
|
+
async function workspaceOperation(t, { bail, address, layer, opName }) {
|
|
455
|
+
if (t.resolutionError) return bail(t.resolutionError.code, t.resolutionError.message, t.resolutionError.details ?? undefined);
|
|
456
|
+
const name = t.slots[layer];
|
|
457
|
+
const mod = name ? t.modules.find((x) => x.name === name) : null;
|
|
458
|
+
if (!mod?.manifest) return bail("E_OPERATION_UNAVAILABLE", `no ${layer} provider is resolved for ${t.home ? t.home : `soul ${t.soul.name}`}`);
|
|
459
|
+
const provider = { ...mod.manifest, capability: mod.name };
|
|
1102
460
|
const op = manifestOperations(provider).find((o) => o.name === opName);
|
|
1103
|
-
if (!op) bail("E_OPERATION_UNKNOWN", `${
|
|
1104
|
-
const
|
|
1105
|
-
if (
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
if (op.context === "home" && !meta) bail("E_OPERATION_UNAVAILABLE", `${address} runs in an instance home; pass --home <abs>`);
|
|
461
|
+
if (!op) return bail("E_OPERATION_UNKNOWN", `${mod.name} declares no operation ${JSON.stringify(opName)} (declared: ${manifestOperations(provider).map((o) => o.name).join(", ") || "none"})`);
|
|
462
|
+
const missingReq = manifestMissingRequires(provider);
|
|
463
|
+
if (missingReq.length) return bail("E_CAPABILITY_REQUIRES", `${mod.name} requires ${missingReq.map((m) => `"${m.command}" on PATH${m.why ? ` (${m.why})` : ""}${m.install ? ` [install: ${m.install}]` : ""}`).join(", ")}; ${address} was not run`);
|
|
464
|
+
if (op.context === "home" && !t.home) return bail("E_OPERATION_UNAVAILABLE", `${address} runs in an instance home; pass --home <abs>`);
|
|
465
|
+
const given = operationArgs(bail);
|
|
1109
466
|
const declared = new Map(op.args.map((a) => [a.name, a]));
|
|
1110
|
-
for (const
|
|
1111
|
-
for (const a of op.args) if (a.required && given[a.name] === undefined) bail("E_BAD_ARGS", `${address} needs --arg ${a.name}=<value>: ${a.description || "required"}`);
|
|
467
|
+
for (const n of Object.keys(given)) if (!declared.has(n)) return bail("E_BAD_ARGS", `${address} takes no arg ${JSON.stringify(n)} (declared: ${[...declared.keys()].join(", ") || "none"})`);
|
|
468
|
+
for (const a of op.args) if (a.required && given[a.name] === undefined) return bail("E_BAD_ARGS", `${address} needs --arg ${a.name}=<value>: ${a.description || "required"}`);
|
|
1112
469
|
const argFlags = op.args.flatMap((a) => (given[a.name] === undefined ? [] : [a.flag, given[a.name]]));
|
|
1113
|
-
const spec = provider.commands[op.command];
|
|
1114
|
-
if (typeof spec !== "string" || !spec.trim()) bail("E_CAPABILITY_BROKEN", `${
|
|
470
|
+
const spec = provider.commands?.[op.command];
|
|
471
|
+
if (typeof spec !== "string" || !spec.trim()) return bail("E_CAPABILITY_BROKEN", `${mod.name}: command ${op.command} is not a non-empty string`);
|
|
472
|
+
let catalog = null; try { catalog = officialPackageCatalog(); } catch { catalog = null; }
|
|
473
|
+
const { layerProvider } = await import("../lib/instance-inspect.mjs");
|
|
474
|
+
let lp;
|
|
475
|
+
try { lp = await layerProvider(t, layer, { catalog, remoteOptions: remoteOptionsFromEnv() }); } catch (e) { return bail(e.code || "E_CAPABILITY_BROKEN", e.message, e.details); }
|
|
1115
476
|
const [script, ...rest] = spec.trim().split(/\s+/);
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
const cwd = op.context === "home" ? home :
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
// running this for another home never steers the provider to its own.
|
|
1124
|
-
const env = { ...process.env };
|
|
1125
|
-
for (const k of ["OATS_EVENT", "OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "OATS_AGENT", "OATS_SOUL", "OATS_CONTEXT", "OATS_ROOT", "OATS_WORKSPACE", "OATS_LEVEL", "OATS_META", "OATS_KIND", "PI_AGENT_INSTANCE", "PI_AGENT_HOME", "PI_AGENTS_ROOT"]) delete env[k];
|
|
1126
|
-
const targetRoot = meta ? agentsRootOfHome(realOrResolved(home)) : selectedSoul?.agentsRoot;
|
|
1127
|
-
const soulDir = meta ? join(dirname(dirname(realOrResolved(home))), "soul") : selectedSoul ? dirname(selectedSoul.soulFile) : undefined;
|
|
1128
|
-
Object.assign(env, {
|
|
1129
|
-
OATS_CAPABILITY: provider.capability, OATS_SETTINGS: JSON.stringify(settings || {}), OATS_CLI_BIN: CLI_BIN, OATS_OPERATION: address,
|
|
1130
|
-
OATS_CONTEXT: ctx, OATS_WORKSPACE: targetRoot ? workspaceOf(targetRoot) : workspaceOf(findRoot(ctx) || ctx),
|
|
1131
|
-
OATS_TEAM_NAME: team?.name || "", OATS_TEAM_ID: team?.id || "", OATS_TEAM_SCOPE: team?.scope || "",
|
|
1132
|
-
...(targetRoot ? { OATS_ROOT: targetRoot, PI_AGENTS_ROOT: targetRoot } : {}),
|
|
1133
|
-
...(soulName ? { OATS_AGENT: soulName } : {}), ...(soulDir ? { OATS_SOUL: soulDir } : {}),
|
|
1134
|
-
});
|
|
1135
|
-
if (op.context === "home") Object.assign(env, { OATS_INSTANCE: meta.instance, OATS_INSTANCE_HOME: home, OATS_HOME: home, PI_AGENT_INSTANCE: meta.instance, PI_AGENT_HOME: home });
|
|
477
|
+
const abs = lp?.executable(script);
|
|
478
|
+
if (!abs) return bail("E_CAPABILITY_BROKEN", `${mod.name} ${op.command}: script not found (${script})`);
|
|
479
|
+
const settings = lp.settings;
|
|
480
|
+
const cwd = op.context === "home" ? t.home : t.deployment;
|
|
481
|
+
const env = { ...lp.env(mod.name, settings), OATS_OPERATION: address, OATS_CONTEXT: t.deployment, OATS_ROOT: t.agentsRoot, PI_AGENTS_ROOT: t.agentsRoot };
|
|
482
|
+
if (op.context === "home") Object.assign(env, { OATS_INSTANCE: t.meta.instance, OATS_INSTANCE_HOME: t.home, OATS_HOME: t.home, PI_AGENT_INSTANCE: t.meta.instance, PI_AGENT_HOME: t.home });
|
|
483
|
+
else for (const k of ["OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "PI_AGENT_INSTANCE", "PI_AGENT_HOME"]) delete env[k];
|
|
1136
484
|
const r = spawnSync("node", [abs, ...rest, ...argFlags, "--json"], { cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 16 * 1024 * 1024, timeout: OPERATION_TIMEOUT_MS, killSignal: "SIGTERM" });
|
|
1137
|
-
finishOperation({ r, bail, address, provider, op, argFlags, cwd, home, meta });
|
|
485
|
+
finishOperation({ r, bail, address, provider, op, argFlags, cwd, home: t.home, meta: t.meta, api: INSPECT_OPERATIONS_API });
|
|
1138
486
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
/** Rewrites only the given soul.yaml fields, preserving every other line
|
|
1142
|
-
* (unknown keys, comments, order), and replaces AGENTS.md when asked.
|
|
1143
|
-
* Packaged souls are read-only (their source is the package). */
|
|
1144
|
-
async function soulCmd() {
|
|
1145
|
-
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
487
|
+
async function operationCmd() {
|
|
488
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1146
489
|
dropAmbientRoot();
|
|
1147
|
-
if (args[1] !== "
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
let soul;
|
|
1156
|
-
try { soul = selectSoul(scopeSouls(ctx, r).souls, name, agentsRootFlag, ctx); } catch (e) { bail(e.code || "E_SOUL_UNKNOWN", e.message); }
|
|
1157
|
-
if (!soul.editable.fields.length) bail("E_SOUL_READONLY", `${name} is a ${soul.kind} soul: ${soul.editable.reason}`);
|
|
1158
|
-
// Field changes, validated before anything is written.
|
|
1159
|
-
const changes = {};
|
|
1160
|
-
const has = (f) => args.includes(`--${f}`);
|
|
1161
|
-
const val = (f) => { const v = flag(f); if (v === true) bail("E_BAD_ARGS", `--${f} needs a value`); return v; };
|
|
1162
|
-
if (has("runtime")) { const v = val("runtime"); if (!["pi", "claude", "codex"].includes(v)) bail("E_BAD_ARGS", "--runtime must be pi, claude or codex"); changes.runtime = v; }
|
|
1163
|
-
if (has("model") && has("no-model")) bail("E_BAD_ARGS", "choose --model <m> or --no-model, not both");
|
|
1164
|
-
if (has("model")) { const v = val("model"); if (!v.trim()) bail("E_BAD_ARGS", "--model needs a model id (use --no-model to clear)"); changes.model = assertSafeConfigValue(v, "--model"); }
|
|
1165
|
-
if (has("no-model")) changes.model = null;
|
|
1166
|
-
if (has("yolo") && has("no-yolo")) bail("E_BAD_ARGS", "choose --yolo or --no-yolo, not both");
|
|
1167
|
-
if (has("yolo")) changes.yolo = true;
|
|
1168
|
-
if (has("no-yolo")) changes.yolo = false;
|
|
1169
|
-
if (has("backend")) { const v = val("backend"); if (!["tmux", "herdr"].includes(v)) bail("E_BAD_ARGS", "--backend must be tmux or herdr"); changes.backend = v; }
|
|
1170
|
-
if (has("launch-config") && has("no-launch-config")) bail("E_BAD_ARGS", "choose --launch-config <name> or --no-launch-config, not both");
|
|
1171
|
-
if (has("launch-config")) { const v = val("launch-config"); if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(v)) bail("E_BAD_ARGS", "--launch-config needs a configuration name (letters, digits, dot, underscore, dash)"); changes["launch-config"] = v; }
|
|
1172
|
-
if (has("no-launch-config")) changes["launch-config"] = null;
|
|
1173
|
-
if (has("description") && has("no-description")) bail("E_BAD_ARGS", "choose --description <d> or --no-description, not both");
|
|
1174
|
-
if (has("description")) changes.description = assertSafeConfigValue(val("description"), "--description");
|
|
1175
|
-
if (has("no-description")) changes.description = null;
|
|
1176
|
-
let instructions;
|
|
1177
|
-
if (has("instructions-file") && has("instructions-stdin")) bail("E_BAD_ARGS", "choose --instructions-file or --instructions-stdin, not both");
|
|
1178
|
-
if (has("instructions-file")) {
|
|
1179
|
-
const file = val("instructions-file");
|
|
1180
|
-
let bytes;
|
|
1181
|
-
try { bytes = readFileSync(file); } catch (e) { bail("E_BAD_ARGS", `--instructions-file ${file}: ${e.message}`); }
|
|
1182
|
-
if (bytes.includes(0)) bail("E_BAD_ARGS", "--instructions-file must be text without NUL bytes");
|
|
1183
|
-
if (bytes.length > INSPECT_TEXT_CAP) bail("E_BAD_ARGS", `--instructions-file is ${bytes.length} bytes; the bound is ${INSPECT_TEXT_CAP} (what inspect can answer whole)`);
|
|
1184
|
-
instructions = bytes;
|
|
1185
|
-
}
|
|
1186
|
-
if (has("instructions-stdin")) {
|
|
1187
|
-
// The routed form: bytes arrive on stdin (the ssh transport), bounded
|
|
1188
|
-
// while reading, exactly like session receive.
|
|
1189
|
-
if (process.stdin.isTTY) bail("E_BAD_ARGS", "--instructions-stdin reads the instructions from stdin");
|
|
1190
|
-
let bytes;
|
|
1191
|
-
try { bytes = await readStreamBounded(process.stdin, INSPECT_TEXT_CAP); } catch (e) { bail(e.code || "E_BAD_ARGS", e.message); }
|
|
1192
|
-
if (bytes.includes(0)) bail("E_BAD_ARGS", "instructions must be text without NUL bytes");
|
|
1193
|
-
// An empty completed stream is a deliberate replacement with nothing,
|
|
1194
|
-
// exactly like an empty --instructions-file: the option itself states
|
|
1195
|
-
// the intent, and a TTY was refused above.
|
|
1196
|
-
instructions = bytes;
|
|
1197
|
-
}
|
|
1198
|
-
if (!Object.keys(changes).length && !instructions) bail("E_BAD_ARGS", "nothing to set: pass at least one of --runtime, --model/--no-model, --yolo/--no-yolo, --backend, --launch-config/--no-launch-config, --description/--no-description, --instructions-file");
|
|
1199
|
-
for (const f of Object.keys(changes)) if (!soul.editable.fields.includes(f)) bail("E_BAD_ARGS", `${f} is not an editable field of ${name}`);
|
|
1200
|
-
const before = { runtime: soul.runtime, model: soul.model, yolo: soul.yolo, backend: soul.backend, description: soul.description, launchConfig: soul.launchConfig };
|
|
1201
|
-
// soul.yaml: replace or append `key: value` lines in place; a cleared
|
|
1202
|
-
// field's line is removed; nothing else in the file moves.
|
|
1203
|
-
let yamlText = "";
|
|
1204
|
-
try { yamlText = readFileSync(soul.soulFile, "utf8"); } catch (e) { bail("E_SOUL_UNKNOWN", `${soul.soulFile}: ${e.message}`); }
|
|
1205
|
-
const lines = yamlText.replace(/\n*$/, "").split("\n");
|
|
1206
|
-
for (const [key, value] of Object.entries(changes)) {
|
|
1207
|
-
const idx = lines.findIndex((l) => new RegExp(`^${key}:\\s`).test(l) || l === `${key}:`);
|
|
1208
|
-
if (value === null) { if (idx >= 0) lines.splice(idx, 1); continue; }
|
|
1209
|
-
const line = `${key}: ${value}`;
|
|
1210
|
-
if (idx >= 0) lines[idx] = line; else lines.push(line);
|
|
1211
|
-
}
|
|
1212
|
-
const receipt = { soul: name, kind: soul.kind, agentsRoot: soul.agentsRoot, file: soul.soulFile, instructionsFile: soul.instructionsFile, changed: Object.keys(changes), before, instructions: null };
|
|
1213
|
-
if (Object.keys(changes).length) writeFileAtomic(soul.soulFile, lines.join("\n") + "\n");
|
|
1214
|
-
if (instructions) {
|
|
1215
|
-
const prev = (() => { try { return createHash("sha256").update(readFileSync(soul.instructionsFile)).digest("hex"); } catch { return null; } })();
|
|
1216
|
-
writeFileAtomic(soul.instructionsFile, instructions);
|
|
1217
|
-
receipt.instructions = { before: prev, after: createHash("sha256").update(instructions).digest("hex"), bytes: instructions.length };
|
|
1218
|
-
}
|
|
1219
|
-
let after;
|
|
1220
|
-
try { after = selectSoul(scopeSouls(ctx, r).souls, name, soul.agentsRoot, ctx); } catch { after = soul; }
|
|
1221
|
-
receipt.after = { runtime: after.runtime, model: after.model, yolo: after.yolo, backend: after.backend, description: after.description };
|
|
1222
|
-
if (JSON_MODE) { jsonOk(receipt); return; }
|
|
1223
|
-
console.log(`Updated soul ${name} (${shortPath(soul.soulFile)})${instructions ? ` and its instructions (${shortPath(soul.instructionsFile)})` : ""}: ${Object.keys(changes).map((k) => `${k}=${changes[k] === null ? "(cleared)" : changes[k]}`).join(", ") || "instructions only"}`);
|
|
1224
|
-
console.log("Future instances use these defaults; existing homes keep what they were composed with.");
|
|
490
|
+
if (args[1] !== "run") bail("E_USAGE", "usage: oats operation run <layer>:<name> (--home <abs> | --soul <name> [--dir <scope>] [--agents-root <abs>]) [--arg k=v ...] [--json]");
|
|
491
|
+
const address = args[2];
|
|
492
|
+
const m0 = typeof address === "string" ? OPERATION_ADDRESS_RE.exec(address) : null;
|
|
493
|
+
if (!m0) bail("E_BAD_ARGS", `operation address must be <layer>:<name> with layer one of ${LAYERS.join(", ")} (got ${JSON.stringify(address)})`);
|
|
494
|
+
const [, layer, opName] = m0;
|
|
495
|
+
// Only the messaging provider's operations act on the teams: others get the record, no remote read.
|
|
496
|
+
const target = await workspaceTarget(bail, { command: "operation run", liveTeams: layer === "messaging" });
|
|
497
|
+
return workspaceOperation(target, { bail, address, layer, opName });
|
|
1225
498
|
}
|
|
1226
499
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
500
|
+
|
|
501
|
+
/** Doctor answers on a workspace deployment only (lead decision c3-6): the
|
|
502
|
+
* deployment found walking up from the given directory (positional or --dir),
|
|
503
|
+
* else E_LOCAL_MISSING; an unreadable oats-local.yaml is its own error. */
|
|
504
|
+
function doctorDeployment(dir) {
|
|
505
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
506
|
+
const ctx = resolve(dir || dirFlag());
|
|
1230
507
|
const ws = doctorLockData(ctx);
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
const oasScopes = detectOasScopes(ctx);
|
|
1239
|
-
console.log(JSON.stringify({
|
|
1240
|
-
schemaVersion: 1,
|
|
1241
|
-
context: ctx,
|
|
1242
|
-
team: r.team || null,
|
|
1243
|
-
chain: r.chain.map((c) => ({ file: c._file, level: c._level, levelKind: levelOf(c._level) })),
|
|
1244
|
-
oasScopes,
|
|
1245
|
-
oasRemedy: oasScopes.length ? OAS_SCOPE_REMEDY : null,
|
|
1246
|
-
layers: Object.fromEntries(LAYERS.map((l) => [l, r.layers[l] ? {
|
|
1247
|
-
integration: r.layers[l].id, level: r.layers[l].level, inject: r.layers[l].inject,
|
|
1248
|
-
skills: [...(Array.isArray(r.layers[l].skills) ? r.layers[l].skills : (r.layers[l].skills ? [r.layers[l].skills] : []))],
|
|
1249
|
-
hooks: Object.keys(r.layers[l].hooks || {}), missingRequires: r.layers[l].missingRequires,
|
|
1250
|
-
provenance: r.provenance[l],
|
|
1251
|
-
} : { provenance: r.provenance[l] || null }])),
|
|
1252
|
-
kernelInjection: composition?.resolved.kernelInjection ?? r.kernelInjection,
|
|
1253
|
-
information: operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : [],
|
|
1254
|
-
injects: r.injects,
|
|
1255
|
-
capabilities: r.capabilities.map((c) => ({ id: c.id, layer: c.layer, command: c.command, origin: c.origin, provenance: c.provenance, settings: c.settings, skills: c.skills, inject: c.inject, hooks: Object.keys(c.hooks || {}), trust: c.trust })),
|
|
1256
|
-
acquired: Object.fromEntries(Object.entries(mans).map(([n, m]) => [n, { layer: m.layer, command: m.command, version: m.version, dir: m._dir, origin: m._origin, description: m.description }])),
|
|
1257
|
-
retiredLocks: (() => { try { return Object.entries(readCapabilityLocks(ctx)); } catch { return []; } })()
|
|
1258
|
-
.filter(([id]) => retiredCapabilityReason(id))
|
|
1259
|
-
.map(([id, lock]) => ({ id, file: lock._file, reason: retiredCapabilityReason(id) })),
|
|
1260
|
-
retiredArtifacts: Object.entries(mans)
|
|
1261
|
-
.filter(([id]) => retiredCapabilityReason(id))
|
|
1262
|
-
.map(([id, m]) => ({ id, dir: m._dir, origin: m._origin, reason: retiredCapabilityReason(id) })),
|
|
1263
|
-
// Workspace model v2 (offline view): the lock (v3) and the deployment
|
|
1264
|
-
// declaration. Human and JSON doctor derive from ONE computation; a
|
|
1265
|
-
// fail-closed lock read is diagnosed via lockError, never consumed as data.
|
|
1266
|
-
workspace: ws.local ? { file: ws.local.path, ref: ws.local.workspace } : null,
|
|
1267
|
-
workspaceError: ws.localError,
|
|
1268
|
-
lockFile: ws.lockFile,
|
|
1269
|
-
packages: ws.packages,
|
|
1270
|
-
lockError: ws.lockError,
|
|
1271
|
-
composedInstructions: composition?.text,
|
|
1272
|
-
instructionBlocks: composition?.blocks,
|
|
1273
|
-
}, null, 2));
|
|
508
|
+
if (ws.localError) bail(ws.localError.code, ws.localError.message, ws.localError.details);
|
|
509
|
+
if (!ws.local) bail("E_LOCAL_MISSING", `oats doctor reads a workspace deployment, and none is in reach of ${ctx} (no oats-local.yaml walking up; \`oats onboard\` creates one)`, { dir: ctx });
|
|
510
|
+
return { ctx, ws };
|
|
511
|
+
}
|
|
512
|
+
async function doctorJson(dir) {
|
|
513
|
+
const { ctx, ws } = doctorDeployment(dir);
|
|
514
|
+
console.log(JSON.stringify(await doctorWorkspaceJson(ctx, flag("soul"), ws), null, 2));
|
|
1274
515
|
}
|
|
1275
516
|
|
|
1276
517
|
/** The v2 doctor payload: the deployment declaration + lock (offline) and, with
|
|
1277
518
|
* --soul, the composed instructions. No v1 keys (chain/layers/acquired/injects…). */
|
|
1278
|
-
|
|
1279
|
-
|
|
519
|
+
/** The problems doctor and status share for a v2 deployment: what OATS 0.25 left
|
|
520
|
+
* under local-agents/ (named, never read), and the captured homes under the agents root. */
|
|
521
|
+
function legacyLayoutProblems(root) {
|
|
522
|
+
return [legacyLocalAgents(root), legacyCapturedHomes(root)].filter(Boolean);
|
|
523
|
+
}
|
|
524
|
+
async function doctorWorkspaceJson(ctx, soulName, ws) {
|
|
525
|
+
const composition = await doctorComposition(ctx, soulName, ws, (code, msg, details) => jsonFail(code, msg, details));
|
|
526
|
+
const problems = legacyLayoutProblems(join(dirname(ws.local.path), "agents"));
|
|
1280
527
|
return {
|
|
1281
528
|
schemaVersion: 1, workspaceApi: 2, context: ctx,
|
|
1282
529
|
workspace: { file: ws.local.path, ref: ws.local.workspace },
|
|
1283
530
|
workspaceError: ws.localError, lockFile: ws.lockFile, packages: ws.packages, lockError: ws.lockError,
|
|
1284
531
|
information: operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : [],
|
|
1285
532
|
composedInstructions: composition?.text, instructionBlocks: composition?.blocks,
|
|
533
|
+
...(problems.length ? { problems } : {}),
|
|
1286
534
|
};
|
|
1287
535
|
}
|
|
1288
536
|
/** Kernel/bridge version skew (published in lockstep from one tag). */
|
|
@@ -1295,142 +543,26 @@ function doctorVersionSkew() {
|
|
|
1295
543
|
/** The "Workspace (v2, offline view)" + "Locked packages" sections, shared by both doctor shapes. */
|
|
1296
544
|
function printDoctorWorkspace(ws) {
|
|
1297
545
|
console.log("\nWorkspace (v2, offline view):");
|
|
1298
|
-
|
|
1299
|
-
else if (ws.localError) console.log(` ERROR: ${ws.localError.message} [${ws.localError.code}]`);
|
|
1300
|
-
else console.log(" (no oats-local.yaml found walking up — this scope realizes no v2 workspace; run `oats sync` from one that does)");
|
|
546
|
+
console.log(` oats-local.yaml ${shortPath(ws.local.path)} → workspace ${ws.local.workspace}`);
|
|
1301
547
|
console.log("\nLocked packages (oats-lock.json v3):");
|
|
1302
548
|
if (ws.lockError) {
|
|
1303
549
|
console.log(` ERROR: ${ws.lockError.message} [${ws.lockError.code}]`);
|
|
1304
550
|
if (ws.lockError.file) console.log(` the lock is never auto-repaired; delete ${shortPath(ws.lockError.file)} and run \`oats sync\``);
|
|
1305
551
|
} else if (!ws.packages.length) console.log(ws.lockFile ? " (none)" : " (no lock yet — run `oats sync`)");
|
|
1306
552
|
for (const p of ws.packages) {
|
|
1307
|
-
console.log(` ${p.id} ${p.version} ${p.source} @ ${p.commit.slice(0, 12)} ${p.
|
|
553
|
+
console.log(` ${p.id} ${p.version} ${p.source} @ ${p.commit.slice(0, 12)} ${p.integrity}`);
|
|
1308
554
|
if (p.capabilities.length) console.log(` capabilities: ${p.capabilities.join(", ")}`);
|
|
1309
555
|
}
|
|
1310
556
|
console.log(" membership, discovery and drift need the remotes: `oats workspace status`, `oats sync`.");
|
|
1311
557
|
}
|
|
1312
|
-
function doctor(dir) {
|
|
1313
|
-
const ctx =
|
|
558
|
+
async function doctor(dir) {
|
|
559
|
+
const { ctx, ws } = doctorDeployment(dir);
|
|
1314
560
|
const soulName = flag("soul");
|
|
1315
|
-
const ws = doctorLockData(ctx);
|
|
1316
561
|
console.log(`oats doctor — resolved from ${shortPath(ctx)}\n`);
|
|
1317
562
|
doctorVersionSkew();
|
|
1318
|
-
|
|
1319
|
-
// A v2 deployment: nothing is installed and there is no config chain — the v1
|
|
1320
|
-
// sections (Config chain / Layers / Kernel injection / Acquired packages / lock
|
|
1321
|
-
// warnings) would describe a tier this deployment does not have.
|
|
1322
|
-
const composition = doctorComposition(ctx, soulName);
|
|
1323
|
-
printDoctorWorkspace(ws);
|
|
1324
|
-
if (soulName) {
|
|
1325
|
-
const information = operationalKnowledgeNote(composition, soulName);
|
|
1326
|
-
if (information) console.log(`\nINFO: ${information}`);
|
|
1327
|
-
console.log(`\nFinal composed AGENTS.md for ${soulName}:\n\n${composition.text}`);
|
|
1328
|
-
} else console.log("\nPass --soul <name> to inspect final composed AGENTS.md.");
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1331
|
-
const chain = configChain(ctx);
|
|
1332
|
-
const r = resolveForDoctor(ctx, soulName);
|
|
1333
|
-
const composition = doctorComposition(ctx, soulName);
|
|
1334
|
-
|
|
1335
|
-
console.log("Config chain (closest first):");
|
|
1336
|
-
if (chain.length === 0) console.log(" (none — no oats-config.yaml found walking up)");
|
|
1337
|
-
for (const c of chain) {
|
|
1338
|
-
console.log(` ${shortPath(c._file)} [${levelOf(c._level)}]`);
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
// An empty-looking chain over oas-* files is not an empty scope: it is a
|
|
1342
|
-
// pre-rename OAS deployment this kernel cannot read (aweb-abfy.1).
|
|
1343
|
-
const oasScopes = detectOasScopes(ctx);
|
|
1344
|
-
if (oasScopes.length) {
|
|
1345
|
-
console.log("");
|
|
1346
|
-
for (const f of oasScopes) console.log(`UN-MIGRATED OAS SCOPE: ${shortPath(f.dir)} (${f.files.join(", ")})`);
|
|
1347
|
-
console.log(` ${OAS_SCOPE_REMEDY}`);
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
if (r.team) console.log(`\nTeam: ${r.team.name}${r.team.id ? ` (id: ${r.team.id})` : ""} [scope: ${shortPath(r.team.scope)}]`);
|
|
1351
|
-
|
|
1352
|
-
console.log("\nLayers:");
|
|
1353
|
-
for (const layer of LAYERS) {
|
|
1354
|
-
const l = r.layers[layer];
|
|
1355
|
-
const prov = r.provenance[layer];
|
|
1356
|
-
if (!prov) { console.log(` ${layer.padEnd(10)} (unresolved — no declaration in chain)`); continue; }
|
|
1357
|
-
if (!l) { console.log(` ${layer.padEnd(10)} none [${prov}]`); continue; }
|
|
1358
|
-
console.log(` ${layer.padEnd(10)} ${l.id} [${prov}]`);
|
|
1359
|
-
if (l.inject) console.log(` inject: ${shortPath(l.inject)}`);
|
|
1360
|
-
const skills = Array.isArray(l.skills) ? l.skills : (l.skills ? [l.skills] : []);
|
|
1361
|
-
if (skills.length) console.log(` skills: ${skills.map(shortPath).join(", ")}`);
|
|
1362
|
-
const hooks = Object.keys(l.hooks || {});
|
|
1363
|
-
if (hooks.length) console.log(` hooks: ${hooks.join(", ")}`);
|
|
1364
|
-
for (const miss of l.missingRequires || []) {
|
|
1365
|
-
console.log(` MISSING REQUIREMENT: ${miss.command} — ${miss.why || ""}${miss.install ? ` (install: ${miss.install})` : ""}`);
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
console.log("\nKernel injection:");
|
|
1370
|
-
const kernelInjection = composition?.resolved.kernelInjection ?? r.kernelInjection;
|
|
1371
|
-
console.log(` oats: ${kernelInjection?.inject ? shortPath(kernelInjection.inject) : "none"} [${kernelInjection?.provenance || "default"}]`);
|
|
1372
|
-
|
|
1373
|
-
console.log("\nUnconditional injections (outermost→innermost):");
|
|
1374
|
-
if (r.injects.length === 0) console.log(" (none)");
|
|
1375
|
-
for (const inj of r.injects) console.log(` ${inj.source}: ${shortPath(inj.file)}`);
|
|
1376
|
-
|
|
1377
|
-
for (const mode of WORK_MODES) {
|
|
1378
|
-
const wm = resolveWorkMode(ctx, mode);
|
|
1379
|
-
console.log(`\nWork mode ${mode}: inject ${wm.inject ? shortPath(wm.inject) : "none"}${wm.setup ? `, setup ${shortPath(wm.setup)}` : ""}`);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
console.log("\nActive capabilities:");
|
|
1383
|
-
if (!r.capabilities.length) console.log(" (none)");
|
|
1384
|
-
for (const cap of r.capabilities) {
|
|
1385
|
-
console.log(` ${cap.id}${cap.layer ? ` layer: ${cap.layer}` : ""} [${cap.provenance.join(" + ")}]`);
|
|
1386
|
-
console.log(` trust: ${cap.trust.trusted ? "approved" : `BLOCKED (${cap.trust.reason})`}`);
|
|
1387
|
-
if (cap.inject) console.log(` inject: ${shortPath(cap.inject)}`);
|
|
1388
|
-
if (cap.skills.length) console.log(` skills: ${cap.skills.map(shortPath).join(", ")}`);
|
|
1389
|
-
}
|
|
1390
|
-
console.log("\nAcquired capability packages:");
|
|
1391
|
-
for (const [name, m] of Object.entries(capabilityManifests(ctx))) {
|
|
1392
|
-
const missing = capabilityMissingRequires(name, ctx);
|
|
1393
|
-
console.log(` ${name.padEnd(16)} layer: ${(m.layer || "additive").padEnd(10)} origin: ${m._origin}${missing.length ? ` (missing: ${missing.map((x) => x.command).join(", ")})` : ""}`);
|
|
1394
|
-
const retiredReason = retiredCapabilityReason(name);
|
|
1395
|
-
if (retiredReason) {
|
|
1396
|
-
const installed = String(m._origin).startsWith("installed:");
|
|
1397
|
-
console.log(` WARNING: artifact of a retired capability — ${retiredReason}${installed ? `; also delete ${shortPath(m._dir)}` : ` (origin ${m._origin}: remove its declaration; the source tree at ${shortPath(m._dir)} is yours to keep or drop)`}`);
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
// readCapabilityLocks fails closed on invalid legacy entries — doctor is the
|
|
1401
|
-
// diagnosis surface, so catch the typed error and render it (never using the data).
|
|
1402
|
-
let locks = {};
|
|
1403
|
-
try { locks = readCapabilityLocks(ctx); }
|
|
1404
|
-
catch (e) {
|
|
1405
|
-
if (e.code !== "invalid-lock") throw e;
|
|
1406
|
-
const prov = Array.isArray(e.provenance) ? e.provenance[0] : undefined;
|
|
1407
|
-
console.log(` ERROR: ${e.message} [invalid-lock]`);
|
|
1408
|
-
if (prov?.file) console.log(` fix or remove the entry in ${shortPath(prov.file)} — never auto-repaired; legacy trust/restore fail closed until it is valid`);
|
|
1409
|
-
}
|
|
1410
|
-
const mans = capabilityManifests(ctx);
|
|
1411
|
-
for (const [id, lock] of Object.entries(locks)) {
|
|
1412
|
-
const retiredReason = retiredCapabilityReason(id);
|
|
1413
|
-
if (retiredReason) { console.log(` WARNING: ${id} is locked in ${shortPath(lock._file)} but ${retiredReason}`); continue; }
|
|
1414
|
-
if (!mans[id]) console.log(` WARNING: ${id} is locked in ${shortPath(lock._file)} but not acquired — run \`oats sync\``);
|
|
1415
|
-
}
|
|
1416
|
-
for (const [id, m] of Object.entries(mans)) {
|
|
1417
|
-
if (!String(m._origin).startsWith("installed:")) continue;
|
|
1418
|
-
// SCOPE-EXACT on the v2 side. `m._capabilityLock` is the row from the
|
|
1419
|
-
// artifact's OWN scope's lock (capabilityManifests annotates it there), and
|
|
1420
|
-
// that is the only row that can lock this artifact: the merged chain would
|
|
1421
|
-
// let an outer scope's lock — or a lock-only ancestor with no config at all
|
|
1422
|
-
// — silence an unlocked inner copy that WINS discovery precedence and
|
|
1423
|
-
// activates. The legacy arm stays chain-merged: v1 parity is unchanged.
|
|
1424
|
-
if (m._capabilityLock || Object.hasOwn(locks, id)) continue;
|
|
1425
|
-
console.log(` WARNING: ${id} at ${shortPath(m._dir)} is in installed/ but has no lock entry — reacquire it or move it to owned/`);
|
|
1426
|
-
}
|
|
1427
|
-
if (existsSync(LEGACY_HOME_CAPABILITIES_DIR)) console.log(` WARNING: legacy ~/.oats/capabilities exists and is no longer discovered — reinstall its packages at a config scope and remove it`);
|
|
1428
|
-
|
|
1429
|
-
// Workspace model v2: nothing is installed. Doctor reports the lock (v3) and
|
|
1430
|
-
// the deployment declaration OFFLINE — membership, discovery and package
|
|
1431
|
-
// resolution go to the remotes and belong to `oats sync` / `oats workspace status`.
|
|
563
|
+
const composition = await doctorComposition(ctx, soulName, ws, (code, msg) => die(`${msg} [${code}]`));
|
|
1432
564
|
printDoctorWorkspace(ws);
|
|
1433
|
-
|
|
565
|
+
for (const p of legacyLayoutProblems(join(dirname(ws.local.path), "agents"))) console.log(`\n! ${p.code}: ${p.message}`);
|
|
1434
566
|
if (soulName) {
|
|
1435
567
|
const information = operationalKnowledgeNote(composition, soulName);
|
|
1436
568
|
if (information) console.log(`\nINFO: ${information}`);
|
|
@@ -1448,7 +580,7 @@ function replaceLaunchConfigsBlock(text, serialized) {
|
|
|
1448
580
|
const keyLine = /^(["']?)launch-configs\1:(\s*(?:#.*)?|\s+\S.*)?$/;
|
|
1449
581
|
const lines = text.split("\n");
|
|
1450
582
|
const starts = lines.map((l, i) => keyLine.test(l) ? i : -1).filter((i) => i >= 0);
|
|
1451
|
-
if (starts.length > 1) throw Object.assign(new Error(`oats-
|
|
583
|
+
if (starts.length > 1) throw Object.assign(new Error(`oats-local.yaml declares launch-configs ${starts.length} times (lines ${starts.map((i) => i + 1).join(", ")}); keep one`), { code: "E_CONFIG_BROKEN" });
|
|
1452
584
|
const block = serialized ? serialized.replace(/\n$/, "").split("\n") : [];
|
|
1453
585
|
if (!starts.length) {
|
|
1454
586
|
if (!serialized) return text;
|
|
@@ -1513,11 +645,9 @@ function normalizeLaunchConfig(e) {
|
|
|
1513
645
|
...(e.yolo !== undefined ? { yolo: e.yolo } : {}),
|
|
1514
646
|
};
|
|
1515
647
|
}
|
|
1516
|
-
function readLaunchConfigsModel(
|
|
1517
|
-
|
|
1518
|
-
const
|
|
1519
|
-
const map = cfg["launch-configs"] || {};
|
|
1520
|
-
for (const [name, entry] of Object.entries(map)) validateLaunchConfig(name, entry, file);
|
|
648
|
+
function readLaunchConfigsModel(local) {
|
|
649
|
+
const map = local["launch-configs"] || {};
|
|
650
|
+
for (const [name, entry] of Object.entries(map)) validateLaunchConfig(name, entry, "oats-local.yaml");
|
|
1521
651
|
const out = Object.create(null); // a name may be "constructor": membership is own only
|
|
1522
652
|
for (const [n, e] of Object.entries(map)) out[n] = normalizeLaunchConfig(e);
|
|
1523
653
|
return out;
|
|
@@ -1551,10 +681,8 @@ function launchConfigContext(bail) {
|
|
|
1551
681
|
if (soulFlag === true) bail("E_BAD_ARGS", "--soul needs a soul name");
|
|
1552
682
|
const agentsRootFlag = flag("agents-root");
|
|
1553
683
|
if (agentsRootFlag === true) bail("E_BAD_ARGS", "--agents-root needs an absolute agents directory");
|
|
1554
|
-
let r;
|
|
1555
|
-
try { r = resolveOatsConfig(ctx); } catch (e) { bail(e.code || "E_CONFIG_BROKEN", e.message); }
|
|
1556
684
|
let soul;
|
|
1557
|
-
try { soul = selectSoul(scopeSouls(ctx
|
|
685
|
+
try { soul = selectSoul(scopeSouls(ctx).souls, String(soulFlag), agentsRootFlag, ctx); } catch (e) { bail(e.code || "E_SOUL_UNKNOWN", e.message); }
|
|
1558
686
|
return { context: memberContextOf(soul, ctx, flag("dir") !== undefined, bail), selected: { soul: soul.name, agentsRoot: soul.agentsRoot } };
|
|
1559
687
|
}
|
|
1560
688
|
return { context: ctx, selected: null };
|
|
@@ -1577,25 +705,25 @@ function launchPreview(bail) {
|
|
|
1577
705
|
instance = meta.instance || basename(home);
|
|
1578
706
|
if (!(meta.launch && typeof meta.launch === "object") && !selectionGiven) {
|
|
1579
707
|
// A home that predates recipes, asked nothing: its frozen command is
|
|
1580
|
-
// described as is. Under a selection
|
|
1581
|
-
//
|
|
708
|
+
// described as is. Under a selection the planner refuses it
|
|
709
|
+
// (E_LAUNCH_LEGACY: re-spawn it from the deployment).
|
|
1582
710
|
let d;
|
|
1583
711
|
try { d = describeLaunchCommand(meta.command); } catch (e) { bail(e.code || "E_LAUNCH_COMMAND_UNSUPPORTED", e.message); }
|
|
1584
|
-
jsonOk({ context, selected, selection: { source: "frozen-command", launchConfig: null, runtime: null, model: null, yolo: null }, runtime: meta.runtime, model: meta.model || null, modelSource: meta.model ? "recorded" : "native default", yolo: meta.yolo ?? null, launchConfig: null, launchConfigSource: null, executable: { path: d.executable, declared: null, resolvedFrom: "recorded" }, argv: d.argv, environment: d.environment, command: redactLaunchCommand(meta.command), prompt: { kind: "task-file", file: "TASK.md" }, hooks: null, preflight: [{ check: "recipe", ok: true, detail: "frozen command;
|
|
712
|
+
jsonOk({ context, selected, selection: { source: "frozen-command", launchConfig: null, runtime: null, model: null, yolo: null }, runtime: meta.runtime, model: meta.model || null, modelSource: meta.model ? "recorded" : "native default", yolo: meta.yolo ?? null, launchConfig: null, launchConfigSource: null, executable: { path: d.executable, declared: null, resolvedFrom: "recorded" }, argv: d.argv, environment: d.environment, command: redactLaunchCommand(meta.command), prompt: { kind: "task-file", file: "TASK.md" }, hooks: null, preflight: [{ check: "recipe", ok: true, detail: "frozen command; a selection is refused (E_LAUNCH_LEGACY): re-spawn it" }], ok: true });
|
|
1585
713
|
return;
|
|
1586
714
|
}
|
|
1587
715
|
const agentsRoot = agentsRootOfHome(home);
|
|
1588
716
|
const agent = (() => { try { return findAgent(agentsRoot, meta.agent); } catch { return undefined; } })();
|
|
1589
717
|
agentLike = agent || { runtime: meta.runtime, model: meta.model, yolo: meta.yolo };
|
|
1590
718
|
} else {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
const soul = scopeSouls(context, r0).souls.find((x) => x.name === selected.soul && x.agentsRoot === selected.agentsRoot);
|
|
1594
|
-
agentLike = { runtime: soul.runtime, model: soul.model, yolo: soul.yolo, "launch-config": soul.launchConfig };
|
|
719
|
+
const soul = scopeSouls(context).souls.find((x) => x.name === selected.soul && x.agentsRoot === selected.agentsRoot);
|
|
720
|
+
agentLike = { runtime: soul.runtime, model: soul.model };
|
|
1595
721
|
instance = `${soul.name}-<purpose>`; home = join(selected.agentsRoot, soul.name, "instances", instance);
|
|
1596
722
|
}
|
|
723
|
+
// A home's recorded capabilities; a new instance's are its spawn's resolution,
|
|
724
|
+
// which a preview of a soul does not prepare (spawn --preview does).
|
|
1597
725
|
let r;
|
|
1598
|
-
try { r =
|
|
726
|
+
try { r = meta ? resolvedFromHome(home, meta) : { capabilities: [], launchConfigs: launchConfigsAt(context) }; } catch (e) { bail(e.code || "E_CONFIG_BROKEN", e.message, e.details); }
|
|
1599
727
|
// The same planner a start uses, in preview mode: failed checks are listed, nothing is touched.
|
|
1600
728
|
let plan;
|
|
1601
729
|
try { plan = planLaunch({ home, instance, meta, contextDir: context, agentLike, selection: sel, resolvedCfg: r, preview: true }); } catch (e) { bail(e.code || "E_BAD_ARGS", e.message); }
|
|
@@ -1606,40 +734,46 @@ function launchPreview(bail) {
|
|
|
1606
734
|
jsonOk({ context, selected, selection: { source: plan.selectionSource, launchConfig: recipe.launchConfig, runtime: sel.runtime ?? null, model: sel.model ?? null, yolo: sel.yolo ?? null }, runtime: plan.runtime, model: recipe.model, modelSource: plan.modelSource, yolo: recipe.yolo ?? null, launchConfig: recipe.launchConfig, launchConfigSource: recipe.launchConfigSource, executable: { path: plan.executable.path, declared: plan.executable.declared ?? null, resolvedFrom: plan.executable.resolvedFrom }, argv: d.argv, environment, command, prompt: recipe.prompt, hooks: redactLaunchRecipe(recipe).hooks, preflight: plan.preflight, ok: plan.ok });
|
|
1607
735
|
}
|
|
1608
736
|
async function launchConfigCmd() {
|
|
1609
|
-
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
737
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1610
738
|
dropAmbientRoot();
|
|
1611
739
|
const sub = args[1];
|
|
1612
740
|
const usage = "usage: oats launch-config list [--dir <scope> | --home <abs> | --soul <name> [--dir <scope>] [--agents-root <abs>]] [--json] | set <name> --file <json> [--keep-env] [--dir <scope>] [--json] | remove <name> [--dir <scope>] [--json] | preview (--home <abs> | --soul <name> [--dir <scope>]) [--launch-config <name>|none] [--runtime r] [--model m] [--yolo|--no-yolo] --json";
|
|
1613
741
|
if (sub === "preview") { launchPreview(bail); return; }
|
|
1614
742
|
if (!["list", "set", "remove"].includes(sub)) bail("E_USAGE", usage);
|
|
1615
743
|
const { context: dir, selected } = sub === "list" ? launchConfigContext(bail) : { context: dirFlag(), selected: null };
|
|
1616
|
-
if (sub !== "list" && (flag("home") !== undefined || flag("soul") !== undefined)) bail("E_BAD_ARGS", `launch-config ${sub} writes
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
744
|
+
if (sub !== "list" && (flag("home") !== undefined || flag("soul") !== undefined)) bail("E_BAD_ARGS", `launch-config ${sub} writes the deployment's oats-local.yaml: address it with --dir, not --home or --soul`);
|
|
745
|
+
// Lead decision 2: launch configurations are a HOST choice, declared in the
|
|
746
|
+
// deployment's oats-local.yaml (found walking up; a home's own deployment).
|
|
747
|
+
const at = selected?.home ?? dir;
|
|
748
|
+
// A 0.25 oats-config.yaml in reach is refused by loadLocal (E_CONFIG_BROKEN, naming
|
|
749
|
+
// the move), never read as "no configurations".
|
|
750
|
+
let found = null;
|
|
751
|
+
// No deployment in reach answers the (empty) effective set — unless what is in
|
|
752
|
+
// reach is a 0.25 oats-config.yaml, whose launch-configs nothing reads any more.
|
|
753
|
+
try { found = loadLocal(at); } catch (e) { if (e?.code !== "E_LOCAL_MISSING" || e.details?.legacy) bail(e.code || "E_WORKSPACE_SCHEMA", e.message, e.details); }
|
|
754
|
+
const file = found?.path ?? null, level = file ? dirname(file) : null;
|
|
755
|
+
const effective = () => Object.values(launchConfigsAt(at)).sort((a, b) => a.name.localeCompare(b.name)).map((e) => ({ name: e.name, ...publicLaunchConfig(e, { source: e.source, shadows: e.shadows }) }));
|
|
1623
756
|
if (sub === "list") {
|
|
1624
757
|
let configurations;
|
|
1625
|
-
try { configurations = effective(); } catch (e) { bail(e.code || "
|
|
1626
|
-
if (JSON_MODE) { jsonOk({ context: dir, level, file
|
|
1627
|
-
if (!configurations.length) { console.log(`No launch configurations are
|
|
758
|
+
try { configurations = effective(); } catch (e) { bail(e.code || "E_LAUNCH_CONFIG_INVALID", e.message); }
|
|
759
|
+
if (JSON_MODE) { jsonOk({ context: dir, level, file, selected, configurations }); return; }
|
|
760
|
+
if (!configurations.length) { console.log(`No launch configurations are declared${file ? ` in ${shortPath(file)}` : ` (no oats-local.yaml in reach of ${dir})`}`); return; }
|
|
1628
761
|
for (const c of configurations) {
|
|
1629
762
|
const env = Object.entries(c.env).map(([n, v]) => v.fromEnv ? `${n}=$${v.fromEnv}` : `${n}=<redacted>`).join(" ");
|
|
1630
|
-
console.log(`${c.name}: ${c.runtime}${c.executable ? ` ${c.executable}` : ""}${c.args.length ? ` ${c.args.map((a) => JSON.stringify(a)).join(" ")}` : ""}${env ? ` [${env}]` : ""}${c.model ? ` model ${c.model}` : ""}${c.yolo !== null ? ` yolo ${c.yolo}` : ""}
|
|
763
|
+
console.log(`${c.name}: ${c.runtime}${c.executable ? ` ${c.executable}` : ""}${c.args.length ? ` ${c.args.map((a) => JSON.stringify(a)).join(" ")}` : ""}${env ? ` [${env}]` : ""}${c.model ? ` model ${c.model}` : ""}${c.yolo !== null ? ` yolo ${c.yolo}` : ""}`);
|
|
1631
764
|
}
|
|
1632
765
|
return;
|
|
1633
766
|
}
|
|
767
|
+
if (!file) bail("E_LOCAL_MISSING", `launch-config ${sub} writes the deployment's oats-local.yaml, and none is in reach of ${dir} — run it from the deployment (\`oats onboard\` creates one)`);
|
|
1634
768
|
const name = args[2];
|
|
1635
769
|
if (!name || name.startsWith("--")) bail("E_BAD_ARGS", `launch-config ${sub} needs a configuration name`);
|
|
1636
|
-
const text =
|
|
770
|
+
const text = readFileSync(file, "utf8");
|
|
1637
771
|
let model;
|
|
1638
|
-
try { model = readLaunchConfigsModel(
|
|
772
|
+
try { model = readLaunchConfigsModel(found.local); } catch (e) { bail(e.code || "E_LAUNCH_CONFIG_INVALID", e.message); }
|
|
1639
773
|
const declaredHere = Object.hasOwn(model, name);
|
|
1640
774
|
const before = declaredHere ? publicLaunchConfig(model[name]) : null;
|
|
1641
775
|
if (sub === "remove") {
|
|
1642
|
-
if (!declaredHere) bail("E_LAUNCH_CONFIG_UNKNOWN", `${name} is not declared
|
|
776
|
+
if (!declaredHere) bail("E_LAUNCH_CONFIG_UNKNOWN", `${name} is not declared in ${shortPath(file)}`);
|
|
1643
777
|
delete model[name];
|
|
1644
778
|
} else {
|
|
1645
779
|
const f = flag("file");
|
|
@@ -1659,13 +793,11 @@ async function launchConfigCmd() {
|
|
|
1659
793
|
try { entry = JSON.parse(raw); } catch { bail("E_BAD_ARGS", `--file ${f} is not valid JSON (one object with runtime and optional executable, args, env, model, yolo)`); }
|
|
1660
794
|
if (args.includes("--keep-env")) {
|
|
1661
795
|
// An editor that saw only redacted values keeps the environment of the
|
|
1662
|
-
// definition
|
|
1663
|
-
//
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
try { const all = resolveOatsConfig(dir).launchConfigs || {}; current = Object.hasOwn(all, name) ? all[name] : undefined; } catch (e) { bail(e.code || "E_CONFIG_BROKEN", e.message); }
|
|
1668
|
-
if (!current) bail("E_LAUNCH_CONFIG_UNKNOWN", `--keep-env: no launch configuration ${name} is effective at ${dir}, so there is no environment to keep; declare it with env`);
|
|
796
|
+
// declared definition of that name: a one-time copy into the complete
|
|
797
|
+
// replacement entry.
|
|
798
|
+
if (entry && typeof entry === "object" && entry.env !== undefined) bail("E_BAD_ARGS", "--keep-env keeps the environment of the declared definition; omit env from --file");
|
|
799
|
+
const current = declaredHere ? model[name] : undefined;
|
|
800
|
+
if (!current) bail("E_LAUNCH_CONFIG_UNKNOWN", `--keep-env: no launch configuration ${name} is declared in ${shortPath(file)}, so there is no environment to keep; declare it with env`);
|
|
1669
801
|
if (entry && typeof entry === "object" && Object.keys(current.env || {}).length) entry.env = { ...current.env };
|
|
1670
802
|
}
|
|
1671
803
|
try { validateLaunchConfig(name, entry, `--file ${f}`); } catch (e) { bail(e.code || "E_LAUNCH_CONFIG_INVALID", e.message); }
|
|
@@ -1676,7 +808,10 @@ async function launchConfigCmd() {
|
|
|
1676
808
|
// What is written must read back as exactly what was asked, by the kernel's
|
|
1677
809
|
// own reader, before a byte of the file changes.
|
|
1678
810
|
let readBack;
|
|
1679
|
-
|
|
811
|
+
let nextLocal;
|
|
812
|
+
try { nextLocal = parseConfigData(next, { origin: { kind: "local", path: file } }).value; readBack = nextLocal["launch-configs"] || {}; } catch (e) { bail("E_LAUNCH_CONFIG_INVALID", `the rewritten block does not parse: ${e.message}; nothing was written`); }
|
|
813
|
+
const schemaProblems = validateLocal(nextLocal);
|
|
814
|
+
if (schemaProblems.length) bail("E_LAUNCH_CONFIG_INVALID", `the rewritten oats-local.yaml would be invalid (${schemaProblems.map((p) => `${p.path || "/"}: ${p.message}`).join("; ")}); nothing was written`);
|
|
1680
815
|
const canonical = (m) => JSON.stringify(Object.keys(m).sort().map((n) => [n, normalizeLaunchConfig(m[n])]));
|
|
1681
816
|
const same = canonical(readBack) === canonical(model);
|
|
1682
817
|
if (!same) bail("E_LAUNCH_CONFIG_INVALID", `${name} would not read back as written; nothing was written`);
|
|
@@ -1685,7 +820,7 @@ async function launchConfigCmd() {
|
|
|
1685
820
|
try { eff = effective().find((c) => c.name === name) || null; } catch (e) { eff = { error: e.message }; }
|
|
1686
821
|
const receipt = { name, action: sub, level, file, before, after: Object.hasOwn(model, name) ? publicLaunchConfig(model[name]) : null, effective: eff };
|
|
1687
822
|
if (JSON_MODE) { jsonOk(receipt); return; }
|
|
1688
|
-
console.log(sub === "set" ? `Declared launch configuration ${name}
|
|
823
|
+
console.log(sub === "set" ? `Declared launch configuration ${name} in ${shortPath(file)}` : `Removed launch configuration ${name} from ${shortPath(file)}`);
|
|
1689
824
|
}
|
|
1690
825
|
|
|
1691
826
|
|
|
@@ -1752,8 +887,7 @@ function instanceCmd() {
|
|
|
1752
887
|
if (home === undefined) {
|
|
1753
888
|
let root;
|
|
1754
889
|
try { root = ensureRoot(dirFlag()); } catch (e) { return bail(e.code || "E_NO_ROOT", e.message); }
|
|
1755
|
-
|
|
1756
|
-
const roots = [...new Set([root, ...(r.team ? teamAgentRoots(r.team.scope) : [])].map((p) => realOrResolved(p)))];
|
|
890
|
+
const roots = [realOrResolved(root)];
|
|
1757
891
|
const candidates = [];
|
|
1758
892
|
for (const rt of roots) for (const hit of findInstanceHomes(rt, name)) candidates.push({ root: rt, agent: hit.agent?.name ?? null, home: hit.home });
|
|
1759
893
|
if (!candidates.length) return bail("E_SESSION_UNKNOWN", `no instance ${JSON.stringify(name)} under ${roots.join(", ")}`);
|
|
@@ -1783,48 +917,36 @@ function instanceCmd() {
|
|
|
1783
917
|
bail(e.code || "E_GIT_FAILED", e.message, e.observation ? { observation: e.observation } : undefined);
|
|
1784
918
|
}
|
|
1785
919
|
}
|
|
1786
|
-
/** `oats readiness
|
|
1787
|
-
function readinessCmd() {
|
|
920
|
+
/** `oats readiness (--soul <name> [--agents-root <abs>] [--dir <d>] | --home <abs>) [--policy] --json` — readinessApi 2. */
|
|
921
|
+
async function readinessCmd() {
|
|
1788
922
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1789
923
|
dropAmbientRoot();
|
|
1790
|
-
// A captured incarnation's readiness comes from its retained resolution, not
|
|
1791
|
-
// from the current configuration this command reads; refuse before inspecting.
|
|
1792
924
|
const homeArg = flag("home");
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
readiness.policy = policyOf({ instanceMeta: meta, soul: soul ? inspect.souls.find((s) => s.name === soul) : null }).policy;
|
|
1818
|
-
readiness.notes.push("policy: a lifecycle-authority claim enforced by the spawn route, not an OS sandbox");
|
|
1819
|
-
}
|
|
1820
|
-
if (JSON_MODE) { jsonOk(readiness); return; }
|
|
1821
|
-
console.log(`readiness — ${readiness.subject.kind === "soul" ? `soul ${readiness.subject.name}` : shortPath(readiness.subject.context)}: ${readiness.summary.ready ? "READY" : `${readiness.summary.fail} failing, ${readiness.summary.unknown} unknown of ${readiness.summary.required} required`}`);
|
|
1822
|
-
for (const [name, check] of Object.entries(readiness.checks)) {
|
|
1823
|
-
console.log(` ${name}: ${check.status}`);
|
|
1824
|
-
for (const i of check.items) console.log(` ${i.status.padEnd(14)} ${i.subject}${i.required ? "" : " (optional)"}${i.reason ? ` — ${i.reason}` : ""}${i.signature ? ` · signature ${i.signature.status}${i.signature.signer?.label ? ` by ${i.signature.signer.label}` : ""}` : ""}${i.remedy ? ` → ${i.remedy}` : ""}`);
|
|
925
|
+
// Workspace model (readinessApi 2): an instance or soul subject; checks
|
|
926
|
+
// installed | configured | member | providers. No trusted check.
|
|
927
|
+
if (args.includes("--verify-signatures")) return bail("E_BAD_ARGS", "--verify-signatures was removed with the trusted check (readinessApi 2): declaring a package in packages: is the trust decision, and the lock pins commit + integrity");
|
|
928
|
+
const target = await workspaceTarget(bail, { command: "readiness" });
|
|
929
|
+
{
|
|
930
|
+
const given = (name) => { const v = flag(name); return v && v !== true ? String(v) : null; };
|
|
931
|
+
const selector = homeArg && homeArg !== true ? { kind: "home", home: String(homeArg), soul: given("soul"), agentsRoot: given("agents-root") }
|
|
932
|
+
: { kind: "soul", soul: String(flag("soul")), agentsRoot: given("agents-root"), dir: given("dir") };
|
|
933
|
+
let catalog = null; try { catalog = officialPackageCatalog(); } catch { catalog = null; }
|
|
934
|
+
const doc = await readinessDocument(target, { selector, remoteOptions: remoteOptionsFromEnv(), catalog });
|
|
935
|
+
if (args.includes("--policy")) {
|
|
936
|
+
doc.policy = policyOf({ instanceMeta: target.meta, soul: target.meta ? null : policySoul(target) }).policy;
|
|
937
|
+
doc.notes.push("policy: a lifecycle-authority claim enforced by the spawn route, not an OS sandbox");
|
|
938
|
+
}
|
|
939
|
+
if (JSON_MODE) { jsonOk(doc); return; }
|
|
940
|
+
const s = doc.subject;
|
|
941
|
+
console.log(`readiness — ${s.kind === "instance" ? `instance ${s.instance} (soul ${s.soul})` : `soul ${s.soul}`}: ${doc.summary.ready ? "READY" : `${doc.summary.fail} failing, ${doc.summary.unknown} unknown of ${doc.summary.required} required`}`);
|
|
942
|
+
for (const [name, check] of Object.entries(doc.checks)) {
|
|
943
|
+
console.log(` ${name}: ${check.status}`);
|
|
944
|
+
for (const i of check.items) console.log(` ${i.status.padEnd(14)} ${i.subject}${i.required ? "" : " (optional)"}${i.reason ? ` — ${i.reason}` : ""}${i.remedy ? ` → ${i.remedy}` : ""}`);
|
|
945
|
+
}
|
|
946
|
+
if (doc.policy) console.log(` policy: child spawns ${doc.policy.childSpawns.allowed ? "allowed" : "disabled"} (${doc.policy.childSpawns.origin.kind}${doc.policy.childSpawns.enforced ? ", enforced" : ""}); worktrees ${doc.policy.worktrees.allowed === null ? "unknown" : doc.policy.worktrees.allowed ? "allowed" : "not in this work mode"}`);
|
|
947
|
+
for (const n of doc.notes) console.log(` note: ${n}`);
|
|
948
|
+
return;
|
|
1825
949
|
}
|
|
1826
|
-
if (readiness.policy) console.log(` policy: child spawns ${readiness.policy.childSpawns.allowed ? "allowed" : "disabled"} (${readiness.policy.childSpawns.origin.kind}${readiness.policy.childSpawns.enforced ? ", enforced" : ""}); worktrees ${readiness.policy.worktrees.allowed === null ? "unknown" : readiness.policy.worktrees.allowed ? "allowed" : "not in this work mode"}`);
|
|
1827
|
-
for (const n of readiness.notes) console.log(` note: ${n}`);
|
|
1828
950
|
}
|
|
1829
951
|
// ---------- workspace model v2: sync / package / workspace status / capabilities / souls ----------
|
|
1830
952
|
// Contract: docs/design/2026-09-23-workspace-module-contracts.md §6. Nothing is
|
|
@@ -1862,12 +984,13 @@ function catalogForSync(bail) {
|
|
|
1862
984
|
* recomposes the tag from the catalog's own convention.
|
|
1863
985
|
* → { packages: { <id>: <version> }, problems: [ { code: "E_PACKAGE_MISSING", … } ] } */
|
|
1864
986
|
function standalonePackages(catalog) {
|
|
1865
|
-
let id = "oats.framework"
|
|
1866
|
-
|
|
987
|
+
let id = "oats.framework";
|
|
988
|
+
const file = officialCatalogFile();
|
|
989
|
+
try { const alias = officialCapabilityAliases()["oats.core"]; id = (typeof alias === "string" ? alias : alias?.package) ?? id; } catch { /* the catalog is diagnosed below */ }
|
|
1867
990
|
const version = standaloneCatalogVersion(catalog?.[id]?.ref);
|
|
1868
991
|
if (version) return { packages: { [id]: version }, problems: [] };
|
|
1869
992
|
const why = catalog?.[id] ? `its ref ${JSON.stringify(catalog[id].ref)} carries no version` : `it has no entry ${JSON.stringify(id)}`;
|
|
1870
|
-
return { packages: {}, problems: [{ code: "E_PACKAGE_MISSING", id, reason: "no-catalog", catalog: file, path: `/packages/${id}`, message: `the catalog has no package providing oats.core (OATS_PACKAGE_CATALOG=${file
|
|
993
|
+
return { packages: {}, problems: [{ code: "E_PACKAGE_MISSING", id, reason: "no-catalog", catalog: file, path: `/packages/${id}`, message: `the catalog has no package providing oats.core (OATS_PACKAGE_CATALOG=${file}): ${why}; standalone spawns will be refused until it does` }] };
|
|
1871
994
|
}
|
|
1872
995
|
/** The bare version of a catalog ref: its LAST path segment when that is a version
|
|
1873
996
|
* (`v1.1.3` → `v1.1.3`; `oats-framework/v1.1.3` → `v1.1.3`; `main` → null). */
|
|
@@ -1889,31 +1012,29 @@ async function discoverForCli(ctx, bail) {
|
|
|
1889
1012
|
}
|
|
1890
1013
|
|
|
1891
1014
|
const short = (oid) => (typeof oid === "string" ? oid.slice(0, 8) : "?");
|
|
1892
|
-
/** Where a reader takes capability manifests from: the instance home's own
|
|
1893
|
-
* materialized modules when the home has them (workspace model), else the
|
|
1894
|
-
* context directory (classic chain). */
|
|
1895
|
-
const manifestSource = (meta, home, ctx) => (meta && meta.modules && typeof meta.modules === "object" && home ? realOrResolved(home) : ctx);
|
|
1896
1015
|
/** Display name of a discovery: the workspace's name, or the standalone label (decision 10). */
|
|
1897
1016
|
const workspaceName = (discovery) => discovery.workspace?.name ?? `standalone:${memberLabel(discovery.key)}`;
|
|
1898
1017
|
const memberLabel = (key) => String(key).split("/").filter(Boolean).pop()?.replace(/\.git$/, "") || String(key);
|
|
1899
1018
|
const teamLabel = (team) => team ?? "unassigned";
|
|
1900
1019
|
const originOf = (item) => (item.package ? `package ${item.package} v${item.version}` : `member ${item.repoKey} @ ${short(item.commit)}`);
|
|
1901
1020
|
|
|
1902
|
-
/** Rows of every
|
|
1903
|
-
|
|
1021
|
+
/** Rows of every soul and capability of confirmed members (+ external souls) + locked package
|
|
1022
|
+
* capabilities. Souls have no private mode (0.26.0); a private member capability is listed with
|
|
1023
|
+
* `private: true` — repo-owned: usable only by its own repo's souls (E_CAPABILITY_PRIVATE). */
|
|
1024
|
+
function workspaceItems(discovery, lock) {
|
|
1904
1025
|
const souls = [];
|
|
1905
1026
|
const capabilities = [];
|
|
1906
1027
|
for (const m of discovery.members) {
|
|
1907
1028
|
if (!m.confirmed && !(discovery.standalone === true && m.key === discovery.key)) continue;
|
|
1908
|
-
for (const s of m.souls)
|
|
1909
|
-
for (const c of m.capabilities)
|
|
1029
|
+
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 });
|
|
1030
|
+
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 });
|
|
1910
1031
|
}
|
|
1911
1032
|
for (const ext of discovery.external || []) {
|
|
1912
1033
|
const s = ext.soul;
|
|
1913
|
-
|
|
1034
|
+
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 });
|
|
1914
1035
|
}
|
|
1915
1036
|
for (const [id, entry] of Object.entries(lock?.packages || {})) {
|
|
1916
|
-
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
|
|
1037
|
+
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 });
|
|
1917
1038
|
}
|
|
1918
1039
|
const byName = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0);
|
|
1919
1040
|
souls.sort(byName);
|
|
@@ -1931,7 +1052,7 @@ function memberRows(discovery) {
|
|
|
1931
1052
|
|
|
1932
1053
|
/** Package rows for `sync` / `workspace status` from the lock. */
|
|
1933
1054
|
function packageRows(lock) {
|
|
1934
|
-
return Object.entries(lock.packages).map(([id, p]) => ({ id, version: p.version, source: p.source, commit: p.commit, integrity: p.integrity, capabilities: p.capabilities
|
|
1055
|
+
return Object.entries(lock.packages).map(([id, p]) => ({ id, version: p.version, source: p.source, commit: p.commit, integrity: p.integrity, capabilities: p.capabilities }));
|
|
1935
1056
|
}
|
|
1936
1057
|
|
|
1937
1058
|
/** Print a padded table: rows are arrays of strings. */
|
|
@@ -1941,55 +1062,17 @@ function printTable(header, rows) {
|
|
|
1941
1062
|
for (const r of all) console.log(" " + r.map((c, i) => String(c ?? "").padEnd(widths[i])).join(" ").trimEnd());
|
|
1942
1063
|
}
|
|
1943
1064
|
|
|
1944
|
-
/**
|
|
1945
|
-
|
|
1946
|
-
// ONE definition of the approval digest (lib/packages.mjs executablesDigestAt) — the
|
|
1947
|
-
// same function resolveSoul re-runs at spawn (M3), so sync and spawn can never disagree.
|
|
1948
|
-
const req = parsePackageRequest(id, workspace.packages[id], catalog);
|
|
1949
|
-
const { digest, executables } = await executablesDigestAt(remoteModule, req.remoteRef, entry.commit, entry.path, entry.capabilities ?? null, { remoteOptions });
|
|
1950
|
-
const targets = executables.map((x) => `${x.capability}: ${x.kind} ${x.name} → ${x.target}`);
|
|
1951
|
-
return { digest, targets };
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
/** One yes/no question on the terminal (TTY only; the caller checks). Ctrl+D / a closed
|
|
1955
|
-
* stdin at the prompt is "no" (readline rejects the question with AbortError, or simply
|
|
1956
|
-
* closes without an answer — neither is a crash; both are the operator declining). */
|
|
1957
|
-
async function askYesNo(question) {
|
|
1958
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1959
|
-
try {
|
|
1960
|
-
const closed = new Promise((resolveClosed) => rl.once("close", () => resolveClosed(null)));
|
|
1961
|
-
const answer = await Promise.race([rl.question(question).catch((e) => { if (e?.code === "ABORT_ERR" || e?.name === "AbortError") return null; throw e; }), closed]);
|
|
1962
|
-
if (answer === null) { process.stderr.write("\n"); return false; }
|
|
1963
|
-
const a = String(answer).trim().toLowerCase();
|
|
1964
|
-
return a === "y" || a === "yes";
|
|
1965
|
-
} finally { rl.close(); }
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
/** `--approve <id>@<version>` (repeatable) → Map<id, version>. Exactly one "@" splits the two;
|
|
1969
|
-
* the digest is never taken from the operator — it is computed over the locked tree. */
|
|
1970
|
-
function approveFlags(bail) {
|
|
1971
|
-
const out = new Map();
|
|
1972
|
-
for (let i = 0; i < args.length; i++) {
|
|
1973
|
-
if (args[i] !== "--approve") continue;
|
|
1974
|
-
const v = args[i + 1];
|
|
1975
|
-
if (v === undefined || v.startsWith("--")) return bail("E_BAD_ARGS", "--approve needs <id>@<version> (the package id and the version exactly as `packages:` / the lock spell them)");
|
|
1976
|
-
const at = v.indexOf("@");
|
|
1977
|
-
const id = at > 0 ? v.slice(0, at) : "";
|
|
1978
|
-
const version = at > 0 ? v.slice(at + 1) : "";
|
|
1979
|
-
if (!id || !version || version.includes("@")) return bail("E_BAD_ARGS", `--approve ${JSON.stringify(v)}: expected <id>@<version>`, { value: v });
|
|
1980
|
-
if (out.has(id) && out.get(id) !== version) return bail("E_BAD_ARGS", `--approve names ${id} twice with different versions (${out.get(id)}, ${version})`, { id, versions: [out.get(id), version] });
|
|
1981
|
-
out.set(id, version);
|
|
1982
|
-
i++;
|
|
1983
|
-
}
|
|
1984
|
-
return out;
|
|
1985
|
-
}
|
|
1065
|
+
/** `--approve` left with package approval (human decision 2026-09-24). */
|
|
1066
|
+
const APPROVAL_REMOVED = "package approval was removed; declaring a package in packages: is the trust decision";
|
|
1986
1067
|
|
|
1987
1068
|
/** The body of `oats sync` — shared by `sync` and `onboard` (which onboards, then syncs the same
|
|
1988
1069
|
* way). Given a v2 deployment context: discover over the remotes, confirm membership, resolve
|
|
1989
|
-
* `packages:` against the lock
|
|
1070
|
+
* `packages:` against the lock (commit + integrity), write the lock. There is no approval step:
|
|
1071
|
+
* declaring a package in `packages:` is the trust decision (human decision 2026-09-24).
|
|
1990
1072
|
* `bail` never returns (it exits the process with the caller's error shape).
|
|
1991
|
-
* → { report, lock, discovery,
|
|
1073
|
+
* → { report, lock, discovery, items, lockFile, problems } */
|
|
1992
1074
|
async function performSync(ctx, bail, { onDiscovered } = {}) {
|
|
1075
|
+
if (args.includes("--approve")) return bail("E_BAD_ARGS", `--approve: ${APPROVAL_REMOVED}`, { flag: "--approve" });
|
|
1993
1076
|
const catalog = catalogForSync(bail);
|
|
1994
1077
|
const discovery = await discoverForCli(ctx, bail);
|
|
1995
1078
|
onDiscovered?.(discovery);
|
|
@@ -2007,37 +1090,7 @@ async function performSync(ctx, bail, { onDiscovered } = {}) {
|
|
|
2007
1090
|
if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance);
|
|
2008
1091
|
throw e;
|
|
2009
1092
|
}
|
|
2010
|
-
|
|
2011
|
-
const approvalNeeded = [];
|
|
2012
|
-
const interactive = !JSON_MODE && process.stdin.isTTY && process.stdout.isTTY;
|
|
2013
|
-
// `--approve <id>@<version>`: approve what is THERE — every named pair must be a locked entry
|
|
2014
|
-
// at exactly that version (E_BAD_ARGS otherwise); the digest recorded is the one computed over
|
|
2015
|
-
// the locked tree, never anything the operator typed. Works without a terminal.
|
|
2016
|
-
const approveWanted = approveFlags(bail);
|
|
2017
|
-
for (const [id, version] of approveWanted) {
|
|
2018
|
-
const entry = lock.packages[id];
|
|
2019
|
-
if (!entry) return bail("E_BAD_ARGS", `--approve ${id}@${version}: ${id} is not a package of this ${discovery.standalone === true ? "standalone view" : "workspace"} (locked: ${Object.keys(lock.packages).sort().join(", ") || "none"})`, { id, version, locked: Object.keys(lock.packages).sort() });
|
|
2020
|
-
if (entry.version !== version) return bail("E_BAD_ARGS", `--approve ${id}@${version}: the lock resolves ${id} to version ${entry.version} (@ ${short(entry.commit)}) — approve what is there: --approve ${id}@${entry.version}`, { id, version, locked: entry.version, commit: entry.commit });
|
|
2021
|
-
}
|
|
2022
|
-
const approvedNow = [];
|
|
2023
|
-
for (const id of Object.keys(lock.packages)) {
|
|
2024
|
-
const entry = lock.packages[id];
|
|
2025
|
-
if (entry.approved) continue;
|
|
2026
|
-
let digest, targets;
|
|
2027
|
-
try { ({ digest, targets } = await lockedExecutablesDigest(id, entry, packageSource, catalog, ctx.remoteOptions)); }
|
|
2028
|
-
catch (e) {
|
|
2029
|
-
if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance);
|
|
2030
|
-
throw e;
|
|
2031
|
-
}
|
|
2032
|
-
if (approveWanted.has(id)) { lock = approvePackage(lock, id, digest); approvedNow.push({ id, version: entry.version, commit: entry.commit, executables: digest, targets }); continue; }
|
|
2033
|
-
if (interactive) {
|
|
2034
|
-
console.error(`\n${id} ${entry.version} @ ${short(entry.commit)} needs executable approval (${targets.length} executable${targets.length === 1 ? "" : "s"}, digest ${digest}):`);
|
|
2035
|
-
for (const t of targets) console.error(` ${t}`);
|
|
2036
|
-
if (targets.length === 0) console.error(" (no commands or hooks — nothing runs unattended)");
|
|
2037
|
-
if (await askYesNo(`approve ${id} ${entry.version}? [y/N] `)) { lock = approvePackage(lock, id, digest); continue; }
|
|
2038
|
-
}
|
|
2039
|
-
approvalNeeded.push({ id, version: entry.version, commit: entry.commit, executables: digest, targets });
|
|
2040
|
-
}
|
|
1093
|
+
const lock = resolved.lock;
|
|
2041
1094
|
let lockFile;
|
|
2042
1095
|
try { lockFile = writeLock(ctx.deploymentDir, lock); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
|
|
2043
1096
|
// The deployment's instance root: <deployment>/agents/ (findRoot's marker). A hand-written
|
|
@@ -2046,9 +1099,9 @@ async function performSync(ctx, bail, { onDiscovered } = {}) {
|
|
|
2046
1099
|
const members = memberRows(discovery);
|
|
2047
1100
|
const packages = packageRows(lock);
|
|
2048
1101
|
const changes = resolved.changes;
|
|
2049
|
-
const items = workspaceItems(discovery, lock
|
|
2050
|
-
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,
|
|
2051
|
-
return { report, lock, discovery,
|
|
1102
|
+
const items = workspaceItems(discovery, lock);
|
|
1103
|
+
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 ?? [] };
|
|
1104
|
+
return { report, lock, discovery, items, lockFile, problems };
|
|
2052
1105
|
}
|
|
2053
1106
|
|
|
2054
1107
|
/** The one-line standalone explanation (decision 10). `standaloneReason` says WHY the view is
|
|
@@ -2065,32 +1118,28 @@ function standaloneNote(discovery, { from = "" } = {}) {
|
|
|
2065
1118
|
|
|
2066
1119
|
/** The human §8 report of a sync (text mode). */
|
|
2067
1120
|
function printSyncReport(ctx, synced) {
|
|
2068
|
-
const { report, discovery,
|
|
1121
|
+
const { report, discovery, items, lockFile } = synced;
|
|
2069
1122
|
const { members, packages, changes } = report;
|
|
2070
1123
|
const disabled = new Set(ctx.local.souls?.disabled || []);
|
|
2071
1124
|
console.log(`workspace ${discovery.workspace?.name ?? `(${standaloneNote(discovery)})`} (${discovery.key} @ ${short(discovery.commit)})`);
|
|
2072
1125
|
console.log(`members ${members.map((m) => m.confirmed ? `${m.name} ✓↔ (@ ${short(m.commit)})` : `${m.name} ✗ (${m.status})`).join(" ") || "(none)"}`);
|
|
2073
|
-
console.log(`packages ${packages.map((p) => {
|
|
2074
|
-
const need = approvalNeeded.find((a) => a.id === p.id);
|
|
2075
|
-
return `${p.id} ${p.version} ✓ (${p.approved ? "approved" : need ? "approval needed" : "unapproved"})`;
|
|
2076
|
-
}).join(" ") || "(none)"}`);
|
|
1126
|
+
console.log(`packages ${packages.map((p) => `${p.id} ${p.version} ✓ (@ ${short(p.commit)})`).join(" ") || "(none)"}`);
|
|
2077
1127
|
const changed = changes.filter((c) => c.to !== null && c.from !== c.to).map((c) => `${c.id} ${c.from ?? "—"} → ${c.to} (@ ${short(c.commit)})`);
|
|
2078
1128
|
const removed = changes.filter((c) => c.to === null).map((c) => `${c.id} ${c.from} → removed`);
|
|
2079
1129
|
console.log(`changed ${[...changed, ...removed].join(" ") || "(nothing — the lock already described this workspace)"}`);
|
|
2080
1130
|
const memberSouls = items.souls.filter((s) => s.kind === "member");
|
|
2081
1131
|
const externalSouls = items.souls.filter((s) => s.kind === "external");
|
|
2082
|
-
|
|
1132
|
+
// Souls have no private mode (0.26.0); the private count is of repo-owned member capabilities.
|
|
1133
|
+
const repoOwned = items.capabilities.filter((c) => c.kind === "member" && c.private);
|
|
2083
1134
|
const disabledHere = items.souls.filter((s) => disabled.has(s.name));
|
|
2084
|
-
console.log(`souls ${items.souls.length} discovered (${memberSouls.length} members, ${externalSouls.length} external, ${disabledHere.length} disabled here) · ${
|
|
1135
|
+
console.log(`souls ${items.souls.length} discovered (${memberSouls.length} members, ${externalSouls.length} external, ${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("; ")})` : ""}`);
|
|
2085
1136
|
const teams = new Map();
|
|
2086
1137
|
for (const s of items.souls) { const t = teams.get(s.team) || { souls: 0, capabilities: 0 }; t.souls++; teams.set(s.team, t); }
|
|
2087
1138
|
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); }
|
|
2088
1139
|
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)"}`);
|
|
2089
1140
|
for (const p of synced.problems ?? discovery.problems) console.log(`problem ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`);
|
|
2090
|
-
for (const
|
|
2091
|
-
|
|
2092
|
-
console.log(`\nApproval needed for ${approvalNeeded.map((a) => `${a.id} ${a.version}`).join(", ")} — ${interactive ? "declined; " : "not a terminal; "}the lock records them unapproved. Run \`oats sync\` in a terminal to approve their executables (spawns of souls using them are refused until then).`);
|
|
2093
|
-
} else console.log(`\nlock ${shortPath(lockFile)}`);
|
|
1141
|
+
for (const w of discovery.warnings ?? []) console.log(`warning ${w.code} ${w.message}`);
|
|
1142
|
+
console.log(`\nlock ${shortPath(lockFile)}`);
|
|
2094
1143
|
}
|
|
2095
1144
|
|
|
2096
1145
|
/** `oats sync [--dir] [--json]` — contract §6. */
|
|
@@ -2098,10 +1147,8 @@ async function syncCmd() {
|
|
|
2098
1147
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
2099
1148
|
const ctx = workspaceContext(bail);
|
|
2100
1149
|
const synced = await performSync(ctx, bail);
|
|
2101
|
-
// One envelope (or the §8 report)
|
|
2102
|
-
// are pending. exitCode (not process.exit) lets stdout drain when it is a pipe.
|
|
1150
|
+
// One envelope (or the §8 report); success is exit 0 (a failure bails with its code).
|
|
2103
1151
|
if (JSON_MODE) jsonOk(synced.report); else printSyncReport(ctx, synced);
|
|
2104
|
-
process.exitCode = synced.approvalNeeded.length ? 2 : 0;
|
|
2105
1152
|
}
|
|
2106
1153
|
|
|
2107
1154
|
/** Walk up from dir for oats-workspace.yaml INSIDE a Git checkout → { file, root } | null. */
|
|
@@ -2186,7 +1233,7 @@ async function packageCmd() {
|
|
|
2186
1233
|
const receipt = { action: sub, id, value: value ?? null, previous: had ?? null, edited: true, file: checkout.file };
|
|
2187
1234
|
if (JSON_MODE) { jsonOk(receipt); return; }
|
|
2188
1235
|
console.log(sub === "add"
|
|
2189
|
-
? `${had === undefined ? "Added" : `Changed (${had} →)`} packages.${id}: ${value} in ${shortPath(checkout.file)}. Commit it, then \`oats sync\` (the lock resolves the version to a commit
|
|
1236
|
+
? `${had === undefined ? "Added" : `Changed (${had} →)`} packages.${id}: ${value} in ${shortPath(checkout.file)}. Commit it, then \`oats sync\` (the lock resolves the version to a commit).`
|
|
2190
1237
|
: `Removed packages.${id} (${had}) from ${shortPath(checkout.file)}. Commit it, then \`oats sync\`.`);
|
|
2191
1238
|
}
|
|
2192
1239
|
|
|
@@ -2207,8 +1254,7 @@ async function workspaceCmd() {
|
|
|
2207
1254
|
const locked = new Set(packages.map((p) => p.id));
|
|
2208
1255
|
const unsynced = declared.filter((id) => !locked.has(id));
|
|
2209
1256
|
const stale = packages.filter((p) => !declared.includes(p.id)).map((p) => p.id);
|
|
2210
|
-
const
|
|
2211
|
-
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, approval, external: (discovery.external || []).map((e) => ({ source: e.source, soul: e.soul.name, team: teamLabel(e.soul.team) })), problems: discovery.problems };
|
|
1257
|
+
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 ?? [] };
|
|
2212
1258
|
if (JSON_MODE) { jsonOk(result); return; }
|
|
2213
1259
|
console.log(`workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)}) local ${shortPath(ctx.localPath)}\n`);
|
|
2214
1260
|
if (standalone) console.log(` (${standaloneNote(discovery)})\n`);
|
|
@@ -2217,11 +1263,12 @@ async function workspaceCmd() {
|
|
|
2217
1263
|
for (const m of members.filter((m) => !m.confirmed)) console.log(` ${m.name}: ${m.detail}`);
|
|
2218
1264
|
console.log("\nPackages:");
|
|
2219
1265
|
if (!packages.length) console.log(unsynced.length ? ` (none locked yet — \`oats sync\` resolves ${unsynced.join(", ")})` : " (none)");
|
|
2220
|
-
else printTable(["package", "version", "source", "commit", "
|
|
1266
|
+
else printTable(["package", "version", "source", "commit", "capabilities"], packages.map((p) => [p.id, p.version, p.source, short(p.commit), p.capabilities.join(",")]));
|
|
2221
1267
|
if (packages.length && unsynced.length) console.log(` declared but not locked (run \`oats sync\`): ${unsynced.join(", ")}`);
|
|
2222
1268
|
if (stale.length) console.log(` locked but no longer declared (run \`oats sync\`): ${stale.join(", ")}`);
|
|
2223
1269
|
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(" ")}`);
|
|
2224
1270
|
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}`); }
|
|
1271
|
+
if (discovery.warnings?.length) { console.log("\nWarnings:"); for (const w of discovery.warnings) console.log(` ${w.code} ${w.message}`); }
|
|
2225
1272
|
}
|
|
2226
1273
|
|
|
2227
1274
|
/** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
|
|
@@ -2236,8 +1283,8 @@ async function itemsCmd(kind) {
|
|
|
2236
1283
|
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; }
|
|
2237
1284
|
console.log(`${kind} of workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)})${standalone ? ` — ${standaloneNote(discovery)}` : ""}\n`);
|
|
2238
1285
|
if (!items.length) console.log(" (none)");
|
|
2239
|
-
else if (kind === "souls") printTable(["name", "origin", "team", "work"], items.map((s) => [s.name, s.origin, s.team, s.work ?? "—"]));
|
|
2240
|
-
else printTable(["name", "origin", "team", "layer"], items.map((c) => [c.name, c.origin, c.team, c.layer ?? "—"]));
|
|
1286
|
+
else if (kind === "souls") printTable(["name", "origin", "team", "work"], items.map((s) => [s.name, s.origin, s.labels?.length > 1 ? s.labels.join(",") : s.team, s.work ?? "—"]));
|
|
1287
|
+
else printTable(["name", "origin", "team", "layer"], items.map((c) => [c.private ? `${c.name} (repo-owned)` : c.name, c.origin, c.team, c.layer ?? "—"]));
|
|
2241
1288
|
const unsynced = Object.keys(discovery.workspace?.packages || {}).filter((id) => !lock.packages[id]);
|
|
2242
1289
|
if (kind === "capabilities" && unsynced.length) console.log(`\n package capabilities of ${unsynced.join(", ")} appear after \`oats sync\``);
|
|
2243
1290
|
}
|
|
@@ -2252,7 +1299,13 @@ async function itemsCmd(kind) {
|
|
|
2252
1299
|
* present) to its stamp — the roster's `repo:` column for a workspace soul. */
|
|
2253
1300
|
async function statusDrift(data) {
|
|
2254
1301
|
let ctx;
|
|
2255
|
-
try { ctx = loadLocal(dirFlag()); }
|
|
1302
|
+
try { ctx = loadLocal(dirFlag()); }
|
|
1303
|
+
catch (e) {
|
|
1304
|
+
if (e?.code === "E_LOCAL_MISSING") return null;
|
|
1305
|
+
// A 0.25 oats-config.yaml inside the deployment: the typed migration error, never a stack.
|
|
1306
|
+
if (e?.code === "E_CONFIG_BROKEN") { if (JSON_MODE) jsonFail(e.code, e.message, e.details); die(e.message); }
|
|
1307
|
+
throw e;
|
|
1308
|
+
}
|
|
2256
1309
|
const hasModules = (i) => i.modules && typeof i.modules === "object" && Object.keys(i.modules).length > 0;
|
|
2257
1310
|
const hasSoul = (i) => i.workspace && typeof i.workspace === "object" && i.workspace.soul && typeof i.workspace.soul === "object" && typeof i.workspace.soul.repoKey === "string";
|
|
2258
1311
|
// Workspace souls of this roster: the stamp ensureWorkspaceSoul leaves beside the soul pointer.
|
|
@@ -2316,13 +1369,14 @@ function soulRepoLabel(a, ws) {
|
|
|
2316
1369
|
const short7 = (oid) => (typeof oid === "string" ? oid.slice(0, 7) : "?");
|
|
2317
1370
|
|
|
2318
1371
|
async function status() {
|
|
2319
|
-
if (args.includes("--team"))
|
|
1372
|
+
if (args.includes("--team")) { const msg = "status --team was removed with the classic team scope: `oats status` in the deployment lists every instance, and `oats workspace status` shows the members"; if (JSON_MODE) jsonFail("E_BAD_ARGS", msg); die(msg); }
|
|
2320
1373
|
let root;
|
|
2321
1374
|
try { root = ensureRoot(dirFlag()); }
|
|
2322
1375
|
catch (e) { if (e?.code === "E_NO_DEPLOYMENT") { if (JSON_MODE) jsonFail("E_NO_DEPLOYMENT", e.message, e.details ?? e.provenance); die(e.message); } throw e; }
|
|
2323
1376
|
const data = listInstances(root);
|
|
2324
1377
|
const ws = await statusDrift(data);
|
|
2325
1378
|
const verbose = args.includes("--verbose");
|
|
1379
|
+
const problems = legacyLayoutProblems(root);
|
|
2326
1380
|
if (args.includes("--json")) {
|
|
2327
1381
|
if (ws) for (const a of data) {
|
|
2328
1382
|
const stamp = ws.souls.get(a.name);
|
|
@@ -2335,13 +1389,14 @@ async function status() {
|
|
|
2335
1389
|
if (s) i.soul = { repoKey: s.repoKey, commit: s.commit, current: s.current?.commit ?? null, status: s.status, ...(s.reason ? { reason: s.reason } : {}) };
|
|
2336
1390
|
}
|
|
2337
1391
|
}
|
|
2338
|
-
console.log(JSON.stringify({ root, agents: data, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}) }, null, 2)); return;
|
|
1392
|
+
console.log(JSON.stringify({ root, agents: data, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}), ...(problems.length ? { problems } : {}) }, null, 2)); return;
|
|
2339
1393
|
}
|
|
2340
1394
|
console.log(`oats status — agents root ${shortPath(root)}\n`);
|
|
2341
1395
|
if (ws?.unreachable) console.log(` workspace: unreachable (${ws.unreachable.reason}) — drift unknown\n`);
|
|
2342
|
-
|
|
1396
|
+
for (const p of problems) console.log(` ! ${p.code}: ${p.message}\n`);
|
|
1397
|
+
if (data.length === 0) { console.log(" (no agents — a soul is a member repository's souls/<name>; `oats souls` lists the workspace's)"); return; }
|
|
2343
1398
|
for (const a of data) {
|
|
2344
|
-
console.log(` ${a.name}
|
|
1399
|
+
console.log(` ${a.name} [work: ${a.work || "checkout"}, repo: ${soulRepoLabel(a, ws)}]`);
|
|
2345
1400
|
if (a.description) console.log(` ${a.description}`);
|
|
2346
1401
|
for (const i of a.instances) {
|
|
2347
1402
|
console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"} (branch ${i.branch || "?"}, ${i.work || "?"})`);
|
|
@@ -2356,29 +1411,6 @@ async function status() {
|
|
|
2356
1411
|
console.log(` ! deferred retirement of ${f.instance} FAILED${f.completedAt ? ` at ${f.completedAt}` : ""}: ${f.error || (f.incomplete || []).join("; ") || "see result file"} — retry with \`oats retire ${f.instance}\``);
|
|
2357
1412
|
}
|
|
2358
1413
|
}
|
|
2359
|
-
const defs = listAgentDefs(process.cwd());
|
|
2360
|
-
if (defs.length) console.log(`\n importable defs: ${defs.map((d) => d.name).join(", ")}`);
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
function statusTeam() {
|
|
2364
|
-
const ctx = dirFlag();
|
|
2365
|
-
const r = resolveOatsConfig(ctx);
|
|
2366
|
-
if (!r.team) die(`no team declared in the config chain from ${shortPath(ctx)} — add a "team:" block (name, optional id) at the deployment scope`);
|
|
2367
|
-
const roots = teamAgentRoots(r.team.scope);
|
|
2368
|
-
const payload = { team: r.team, roots: [] };
|
|
2369
|
-
for (const root of roots) payload.roots.push({ root, agents: listInstances(root) });
|
|
2370
|
-
if (args.includes("--json")) { console.log(JSON.stringify(payload, null, 2)); return; }
|
|
2371
|
-
console.log(`oats status — team ${r.team.name}${r.team.id ? ` (${r.team.id})` : ""} [scope: ${shortPath(r.team.scope)}]\n`);
|
|
2372
|
-
if (!roots.length) { console.log(" (no agents/ directories in the team scope)"); return; }
|
|
2373
|
-
for (const { root, agents } of payload.roots) {
|
|
2374
|
-
console.log(` ${shortPath(root)}`);
|
|
2375
|
-
if (!agents.length) { console.log(" (no agents)"); continue; }
|
|
2376
|
-
for (const a of agents) {
|
|
2377
|
-
console.log(` ${a.name}${a.kind === "local" ? " (local)" : ""}${a.description ? ` — ${a.description}` : ""}`);
|
|
2378
|
-
for (const i of a.instances) console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"}`);
|
|
2379
|
-
for (const f of a.retireFailures || []) console.log(` ! deferred retirement of ${f.instance} FAILED: ${f.error || (f.incomplete || []).join("; ") || "see result file"} — retry with \`oats retire ${f.instance}\``);
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
1414
|
}
|
|
2383
1415
|
|
|
2384
1416
|
async function spawnCmd() {
|
|
@@ -2393,90 +1425,96 @@ async function spawnCmd() {
|
|
|
2393
1425
|
const checkDirectoryOptions = (work) => {
|
|
2394
1426
|
if (work === "directory" && (workDir !== undefined || branch !== undefined)) bail("E_BAD_ARGS", "--work directory owns only <home>/work; --work-dir and --branch are not allowed");
|
|
2395
1427
|
};
|
|
2396
|
-
checkDirectoryOptions(requestedWork); // before
|
|
1428
|
+
checkDirectoryOptions(requestedWork); // before anything is resolved or written
|
|
2397
1429
|
const name = args[1];
|
|
2398
|
-
if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>] [--preview] [--base <ref>] [--model <id>|@native-default] [--allow-child-spawns|--no-child-spawns] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--
|
|
1430
|
+
if (!name || name.startsWith("--")) bail("E_USAGE", "usage: oats spawn <agent> [--task <text>|--task-file <f>] [--purpose <slug>|--name <slug>] [--preview] [--base <ref>] [--model <id>|@native-default] [--allow-child-spawns|--no-child-spawns] [--relation child|sibling|parent|unrelated --relative-to <instance> [--relative-root <agents-root>]] [--parent <instance>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--no-launch] [--json]");
|
|
2399
1431
|
// Retired boundary flags (maintainer transport ruling): fail LOUDLY before
|
|
2400
|
-
// ANY side effect
|
|
2401
|
-
//
|
|
2402
|
-
//
|
|
2403
|
-
|
|
1432
|
+
// ANY side effect, including root discovery.
|
|
1433
|
+
// Local souls (local-agents/) are gone with the workspace model: a soul is a member
|
|
1434
|
+
// repository's souls/<name>, so spawn never writes one.
|
|
1435
|
+
for (const removed of ["instructions-file", "def-file"]) if (flag(removed) !== undefined) bail("E_BAD_ARGS", `--${removed} created a local soul under local-agents/, which the workspace model removed — author souls/<name>/soul.yaml + AGENTS.md in a member repository, then \`oats sync\``);
|
|
1436
|
+
if (args.includes("--instance")) bail("E_BAD_ARGS", "--instance was removed by the runtime-boundary ruling — use --purpose <slug> (deterministic <agent>-<purpose> naming) or --name <slug> (the exact instance name)");
|
|
1437
|
+
// --name <slug>: the exact, unprefixed instance name (human decision 2026-09-24).
|
|
1438
|
+
const nameFlag = flag("name");
|
|
1439
|
+
if (nameFlag === true) bail("E_BAD_ARGS", "--name needs an instance name (a slug: lowercase letters and digits, single dashes between them)");
|
|
1440
|
+
if (nameFlag !== undefined && flag("purpose") !== undefined) bail("E_BAD_ARGS", "--name and --purpose are mutually exclusive: --name is the exact instance name, --purpose derives <agent>-<purpose>");
|
|
1441
|
+
// The slug rule is checked here too, before any side effect (soul fetch).
|
|
1442
|
+
if (nameFlag !== undefined) { try { explicitInstanceName(String(nameFlag)); } catch (e) { bail(e.code, e.message); throw e; } }
|
|
2404
1443
|
if (args.includes("--ephemeral")) bail("E_BAD_ARGS", "--ephemeral was removed by the runtime-boundary ruling — declare the agent in a capability manifest (agents:) for automatic ephemeral semantics");
|
|
2405
1444
|
let root;
|
|
1445
|
+
// A spawn needs a workspace deployment (lead decision c3-1): no oats-local.yaml
|
|
1446
|
+
// in reach is E_LOCAL_MISSING, before anything else is read. The agents root is
|
|
1447
|
+
// then <deployment>/agents; an ambient root (the invoking agent's own
|
|
1448
|
+
// PI_AGENTS_ROOT / OATS_ROOT) never redirects it — a home outside
|
|
1449
|
+
// <deployment>/agents would have no derivable deployment.
|
|
1450
|
+
try { loadLocal(dirFlag()); } catch (e) { if (e?.code === "E_LOCAL_MISSING") bail("E_LOCAL_MISSING", `${e.message}: a spawn needs a workspace deployment — \`oats onboard\` creates one`, e.details); else bail(e.code || "E_WORKSPACE_SCHEMA", e.message, e.details); }
|
|
1451
|
+
delete process.env.PI_AGENTS_ROOT; delete process.env.OATS_ROOT;
|
|
1452
|
+
// A deployment without its agents/ root is E_NO_DEPLOYMENT, naming the remedy.
|
|
2406
1453
|
try { root = ensureRoot(dirFlag()); }
|
|
2407
1454
|
catch (e) { bail("E_NO_DEPLOYMENT", e.message || e); throw e; }
|
|
2408
1455
|
const isPreview = args.includes("--preview");
|
|
2409
1456
|
// --agents-root <abs>: the exact root the soul must live in (as inspect and
|
|
2410
|
-
// readiness take it)
|
|
2411
|
-
// def fallback: the soul is there or the spawn refuses E_SOUL_UNKNOWN.
|
|
1457
|
+
// readiness take it) — the deployment's one agents root, or E_SOUL_UNKNOWN.
|
|
2412
1458
|
const agentsRootFlag = flag("agents-root");
|
|
2413
1459
|
if (agentsRootFlag !== undefined && (agentsRootFlag === true || !isAbsolute(String(agentsRootFlag)))) bail("E_BAD_ARGS", "--agents-root needs an absolute agents root");
|
|
2414
|
-
if (agentsRootFlag !== undefined && realOrResolved(String(agentsRootFlag)) !== realOrResolved(root)) {
|
|
2415
|
-
const teamHit = findTeamAgent(dirFlag(), name), hit = (teamHit?.matches || []).find((m) => realOrResolved(m.root) === realOrResolved(String(agentsRootFlag)));
|
|
2416
|
-
if (!hit) bail("E_SOUL_UNKNOWN", `soul "${name}" is not at agents root ${String(agentsRootFlag)} (this scope's root is ${shortPath(root)})`);
|
|
2417
|
-
root = hit.root;
|
|
2418
|
-
}
|
|
1460
|
+
if (agentsRootFlag !== undefined && realOrResolved(String(agentsRootFlag)) !== realOrResolved(root)) bail("E_SOUL_UNKNOWN", `soul "${name}" is not at agents root ${String(agentsRootFlag)} (this scope's root is ${shortPath(root)})`);
|
|
2419
1461
|
let agent = findAgent(root, name);
|
|
2420
1462
|
// Workspace model: with an oats-local.yaml the soul is ALWAYS discovered over the
|
|
2421
|
-
// remotes and resolved (member = latest state, package = locked
|
|
1463
|
+
// remotes and resolved (member = latest state, package = locked) — never
|
|
2422
1464
|
// "whatever <agents-root>/<name>/soul/ happens to hold": that copy is a per-commit
|
|
2423
1465
|
// cache (ensureWorkspaceSoul refreshes it when the member moved), so a second
|
|
2424
1466
|
// spawn sees the member's CURRENT soul, not the first spawn's. A preview runs the
|
|
2425
|
-
// same read-only discovery+resolution
|
|
2426
|
-
//
|
|
1467
|
+
// same read-only discovery+resolution and writes nothing: it reads the soul from
|
|
1468
|
+
// the per-commit cache, or fetches it to a temporary copy (reported as soulFetched).
|
|
2427
1469
|
const providerPairs = [];
|
|
2428
1470
|
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; }
|
|
2429
|
-
let wsPrepared, soulFetched = false, wsSoulUnknown = null;
|
|
2430
|
-
let hasLocal = true;
|
|
1471
|
+
let wsPrepared, soulFetched = false, wsSoulUnknown = null, wsDiscovery;
|
|
2431
1472
|
{
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
1473
|
+
let discovery = null;
|
|
1474
|
+
try {
|
|
1475
|
+
const { prepareInstance, ensureWorkspaceSoul, previewWorkspaceSoul, parseProviderFlags, discoverOrStandalone } = await import("../lib/instance-resolution.mjs");
|
|
1476
|
+
const remoteOptions = remoteOptionsFromEnv();
|
|
1477
|
+
const { local } = loadLocal(dirFlag());
|
|
1478
|
+
discovery = wsDiscovery = await discoverOrStandalone(local, { remoteOptions });
|
|
1479
|
+
wsPrepared = await prepareInstance(dirFlag(), name, { spawn: { providers: parseProviderFlags(providerPairs) }, remoteOptions, discovery });
|
|
1480
|
+
const soulName = wsPrepared.soulEntry.name;
|
|
1481
|
+
let soulDir;
|
|
1482
|
+
if (isPreview) {
|
|
1483
|
+
// A preview writes nothing in the deployment: the soul comes from the
|
|
1484
|
+
// per-commit cache when complete, else from a temporary fetch removed at exit.
|
|
1485
|
+
const pv = await previewWorkspaceSoul(wsPrepared, root);
|
|
1486
|
+
process.once("exit", pv.cleanup);
|
|
1487
|
+
soulDir = pv.soulDir; soulFetched = pv.fetched;
|
|
1488
|
+
agent = findAgentAt(root, soulName, soulDir);
|
|
1489
|
+
} else {
|
|
2442
1490
|
const stampFile = join(root, soulName, ".oats-soul-source.json");
|
|
2443
1491
|
const stampBefore = (() => { try { return JSON.parse(readFileSync(stampFile, "utf8")); } catch { return null; } })();
|
|
2444
|
-
|
|
1492
|
+
soulDir = await ensureWorkspaceSoul(wsPrepared, root);
|
|
2445
1493
|
soulFetched = !stampBefore || stampBefore.commit !== wsPrepared.soulEntry.commit || stampBefore.repoKey !== wsPrepared.soulEntry.repoKey;
|
|
2446
1494
|
if (!agent || soulFetched || agent._dir !== dirname(soulDir)) agent = findAgent(root, soulName);
|
|
2447
|
-
if (!agent) bail("E_SOUL_UNKNOWN", `soul "${name}" was fetched to ${shortPath(soulDir)} but is not readable as a soul there`);
|
|
2448
|
-
note(`(workspace soul: "${name}" from ${wsPrepared.soulEntry.repoKey} @ ${String(wsPrepared.soulEntry.commit).slice(0, 12)}${wsPrepared.soulEntry.team ? `, team ${wsPrepared.soulEntry.team}` : ""}${soulFetched ? "; soul source fetched" : ""})`);
|
|
2449
|
-
} catch (e) {
|
|
2450
|
-
// Standalone (decisions 10/25): the ONLY package request is the kernel's own
|
|
2451
|
-
// default; when the catalog cannot name it, say so instead of "add it to packages:"
|
|
2452
|
-
// (there is no workspace file to add it to).
|
|
2453
|
-
if (e?.code === "E_PACKAGE_MISSING" && discovery?.standalone === true) {
|
|
2454
|
-
let file = process.env.OATS_PACKAGE_CATALOG || null; try { file = describeOfficialCatalog().catalog.file; } catch { /* keep the env value */ }
|
|
2455
|
-
bail(e.code, `${e.details?.capability ?? "oats.core"}: the catalog has no package providing oats.core (OATS_PACKAGE_CATALOG=${file ?? "<bundled>"}) — standalone spawns resolve only the kernel's default package from the catalog`, { ...(e.details ?? {}), standalone: true, reason: "no-catalog", catalog: file });
|
|
2456
|
-
}
|
|
2457
|
-
// Not a workspace soul: a capability-defined agent (a module's `agents:`
|
|
2458
|
-
// soul, resolved below from a materialized copy) or a local-only soul
|
|
2459
|
-
// (--instructions-file/--def-file) may still answer to this name.
|
|
2460
|
-
if (e?.code === "E_SOUL_UNKNOWN" && !isPreview) { wsSoulUnknown = e; }
|
|
2461
|
-
else if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details);
|
|
2462
|
-
else throw e;
|
|
2463
1495
|
}
|
|
2464
|
-
|
|
1496
|
+
if (!agent) bail("E_SOUL_UNKNOWN", `soul "${name}" was fetched to ${shortPath(soulDir)} but is not readable as a soul there`);
|
|
1497
|
+
note(`(workspace soul: "${name}" from ${wsPrepared.soulEntry.repoKey} @ ${String(wsPrepared.soulEntry.commit).slice(0, 12)}${wsPrepared.soulEntry.team ? `, team ${wsPrepared.soulEntry.team}` : ""}${soulFetched ? "; soul source fetched" : ""})`);
|
|
1498
|
+
} catch (e) {
|
|
1499
|
+
// Standalone (decisions 10/25): the ONLY package request is the kernel's own
|
|
1500
|
+
// default; when the catalog cannot name it, say so instead of "add it to packages:"
|
|
1501
|
+
// (there is no workspace file to add it to).
|
|
1502
|
+
if (e?.code === "E_PACKAGE_MISSING" && discovery?.standalone === true) {
|
|
1503
|
+
const file = officialCatalogFile();
|
|
1504
|
+
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 });
|
|
1505
|
+
}
|
|
1506
|
+
// Not a workspace soul: a capability-defined agent (a module's `agents:`
|
|
1507
|
+
// soul, resolved below from a materialized copy) may still answer to this name.
|
|
1508
|
+
if (e?.code === "E_SOUL_UNKNOWN" && !isPreview) { wsSoulUnknown = e; }
|
|
1509
|
+
else if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details);
|
|
1510
|
+
else throw e;
|
|
1511
|
+
}
|
|
2465
1512
|
}
|
|
2466
1513
|
if (agentsRootFlag !== undefined && !agent) bail("E_SOUL_UNKNOWN", `soul "${name}" is not at agents root ${String(agentsRootFlag)}`);
|
|
2467
1514
|
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"})`);
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
if (!agent && !instrFile && !defFile) {
|
|
2472
|
-
// Capability-defined agent: a package's `agents:` soul, active in this context.
|
|
2473
|
-
const capAgent = findCapabilityAgent(dirFlag(), root, name);
|
|
2474
|
-
if (capAgent) {
|
|
2475
|
-
agent = capAgent;
|
|
2476
|
-
note(`(capability agent: "${name}" from ${capAgent.capability} — fresh soul, instances home locally)`);
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
if (!agent && !instrFile && !defFile && hasLocal) {
|
|
1515
|
+
// Capability agent (lead decision c3 Q1): a prepared spawn of its providing module only.
|
|
1516
|
+
let capabilityPrepared, capabilityPkg = null;
|
|
1517
|
+
if (!agent) {
|
|
2480
1518
|
// Workspace model: the agent is declared by a capability some INSTANCE already
|
|
2481
1519
|
// materialized (the --parent home first, then any home under this root) —
|
|
2482
1520
|
// OKF's memory-harvest worker spawned by a knowledge source, for example.
|
|
@@ -2487,50 +1525,30 @@ async function spawnCmd() {
|
|
|
2487
1525
|
catch (e) { bail(e.code || "E_CAPABILITY_BROKEN", e.message, e.details); }
|
|
2488
1526
|
if (modAgent) {
|
|
2489
1527
|
agent = modAgent;
|
|
2490
|
-
note(`(capability agent: "${name}" from ${modAgent.capability}, materialized in ${shortPath(modAgent._manifestSource)} — fresh soul, instances home
|
|
1528
|
+
note(`(capability agent: "${name}" from ${modAgent.capability}, materialized in ${shortPath(modAgent._manifestSource)} — fresh soul, instances home under ${shortPath(join(root, name, "instances"))})`);
|
|
2491
1529
|
} else {
|
|
2492
|
-
// No instance carries it: resolve from the deployment's LOCK —
|
|
1530
|
+
// No instance carries it: resolve from the deployment's LOCK — a locked
|
|
2493
1531
|
// package whose capability declares agents/<name> is fetched into the
|
|
2494
1532
|
// deployment's module store and read from there.
|
|
2495
1533
|
try {
|
|
2496
1534
|
const { resolvePackageCapabilityAgent } = await import("../lib/instance-resolution.mjs");
|
|
2497
|
-
const hit = await resolvePackageCapabilityAgent(dirFlag(), name, { remoteOptions: remoteOptionsFromEnv(), catalog: (() => { try { return officialPackageCatalog(); } catch { return null; } })() });
|
|
1535
|
+
const hit = await resolvePackageCapabilityAgent(dirFlag(), name, { remoteOptions: remoteOptionsFromEnv(), discovery: wsDiscovery, catalog: (() => { try { return officialPackageCatalog(); } catch { return null; } })() });
|
|
2498
1536
|
if (hit) {
|
|
2499
1537
|
agent = capabilityAgentFromDir(hit.dir, name, root, { module: { from: { kind: "package", package: hit.package, version: hit.version, commit: hit.commit } } });
|
|
2500
|
-
if (agent) note(`(capability agent: "${name}" from ${hit.capability} — package ${hit.package} v${hit.version}, fetched to ${shortPath(hit.dir)} — fresh soul, instances home
|
|
1538
|
+
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"))})`); }
|
|
2501
1539
|
}
|
|
2502
1540
|
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
2503
1541
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const teamHit = findTeamAgent(dirFlag(), name);
|
|
2510
|
-
const remote = (teamHit?.matches || []).filter((m) => resolve(m.root) !== resolve(root));
|
|
2511
|
-
if (remote.length > 1) bail("E_AMBIGUOUS_SOUL", `soul "${name}" found in multiple team repos: ${remote.map((m) => shortPath(m.root)).join(", ")} — re-run with --dir <that repo>`);
|
|
2512
|
-
if (remote.length === 1) {
|
|
2513
|
-
root = remote[0].root;
|
|
2514
|
-
agent = remote[0].agent;
|
|
2515
|
-
note(`(cross-repo: soul "${name}" found at ${shortPath(root)} — instance homes there)`);
|
|
1542
|
+
if (agent) {
|
|
1543
|
+
try {
|
|
1544
|
+
const { prepareCapabilityAgent } = await import("../lib/instance-resolution.mjs");
|
|
1545
|
+
capabilityPrepared = await prepareCapabilityAgent(dirFlag(), agent, { discovery: wsDiscovery, pkg: capabilityPkg, remoteOptions: remoteOptionsFromEnv(), catalog: (() => { try { return officialPackageCatalog(); } catch { return null; } })() });
|
|
1546
|
+
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
2516
1547
|
}
|
|
2517
1548
|
}
|
|
1549
|
+
if (!agent && wsSoulUnknown) bail(wsSoulUnknown.code, wsSoulUnknown.message, wsSoulUnknown.details);
|
|
2518
1550
|
checkDirectoryOptions(requestedWork || agent?.work);
|
|
2519
|
-
|
|
2520
|
-
if (instrFile || defFile || !agent) {
|
|
2521
|
-
if (!agent && !instrFile && !defFile) {
|
|
2522
|
-
const def = listAgentDefs(process.cwd()).find((d) => d.name === name);
|
|
2523
|
-
if (!def) bail("E_UNKNOWN_AGENT", `unknown agent "${name}" (known: ${listAgents(root).map((a) => a.name).join(", ") || "none"}; importable defs: ${listAgentDefs(process.cwd()).map((d) => d.name).join(", ") || "none"}) — pass --instructions-file or --def-file to create a local agent`);
|
|
2524
|
-
agent = upsertLocalAgent(root, { name: def.name, file: def.path, repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"), oatsCore: !args.includes("--no-oats-core") });
|
|
2525
|
-
} else if (!agent || agent.kind === "local") {
|
|
2526
|
-
agent = upsertLocalAgent(root, {
|
|
2527
|
-
name, file: defFile, instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined, oatsCore: !args.includes("--no-oats-core"),
|
|
2528
|
-
repo: flag("repo"), work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo: yoloFlag(),
|
|
2529
|
-
});
|
|
2530
|
-
} else {
|
|
2531
|
-
bail("E_BAD_ARGS", `"${name}" is a persistent agent — spawn it without --instructions-file/--def-file`);
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
1551
|
+
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\``);
|
|
2534
1552
|
for (const information of agent.notes || []) note(`[${information.code}] ${information.message}`);
|
|
2535
1553
|
// Lineage is explicit: --relation child|sibling|parent|unrelated anchors the new
|
|
2536
1554
|
// instance to --relative-to <instance>. --parent X is sugar for
|
|
@@ -2558,9 +1576,9 @@ async function spawnCmd() {
|
|
|
2558
1576
|
// NOTE: explicit "unrelated" is passed through to the kernel.
|
|
2559
1577
|
if (relativeTo && relation !== "unrelated") {
|
|
2560
1578
|
// findInstanceHome also sees capability-defined agents' instance homes
|
|
2561
|
-
// (
|
|
1579
|
+
// (<root>/<name>/ without a soul) — e.g. a reviewer passing
|
|
2562
1580
|
// --parent "$OATS_INSTANCE" from a capability agent.
|
|
2563
|
-
if (!findInstanceHome(root, relativeTo)
|
|
1581
|
+
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`);
|
|
2564
1582
|
}
|
|
2565
1583
|
const taskText = flag("task");
|
|
2566
1584
|
if (taskText === true) bail("E_BAD_ARGS", "--task needs a value (use --task-file for long tasks)");
|
|
@@ -2600,7 +1618,7 @@ async function spawnCmd() {
|
|
|
2600
1618
|
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message); throw e; }
|
|
2601
1619
|
// Workspace model: when this deployment has an oats-local.yaml, the soul's
|
|
2602
1620
|
// capabilities are resolved over the workspace's remotes (member = latest,
|
|
2603
|
-
// package = locked
|
|
1621
|
+
// package = locked) and copied whole into the new home. Without one
|
|
2604
1622
|
// (a bare agents root, tests) the classic soul-directory spawn proceeds.
|
|
2605
1623
|
let prepared;
|
|
2606
1624
|
// Workspace model: a `work: worktree|checkout` soul works IN its member's clone on
|
|
@@ -2608,10 +1626,10 @@ async function spawnCmd() {
|
|
|
2608
1626
|
// convention <deployment>/<member name>. Never an ambient Git checkout around the
|
|
2609
1627
|
// deployment. Resolved once here so a preview sees exactly what the apply would.
|
|
2610
1628
|
let preparedRepo;
|
|
2611
|
-
if (wsPrepared) {
|
|
1629
|
+
if (wsPrepared || capabilityPrepared) {
|
|
2612
1630
|
try {
|
|
2613
1631
|
const { toCapabilityRows, modulesPreview, requireMemberClone } = await import("../lib/instance-resolution.mjs");
|
|
2614
|
-
prepared = wsPrepared;
|
|
1632
|
+
prepared = wsPrepared ?? capabilityPrepared;
|
|
2615
1633
|
prepared.capabilityRows = []; // filled after materialization (paths live in the home); preview uses modulesPreview
|
|
2616
1634
|
prepared.preview = modulesPreview(prepared.resolution, root, agent.name);
|
|
2617
1635
|
prepared.toCapabilityRows = toCapabilityRows;
|
|
@@ -2625,12 +1643,13 @@ async function spawnCmd() {
|
|
|
2625
1643
|
if (flag("base") === true) bail("E_BAD_ARGS", "--base needs a ref");
|
|
2626
1644
|
{ const spawnOpts = {
|
|
2627
1645
|
prepared,
|
|
2628
|
-
purpose: flag("purpose"), task: taskText, taskFile: taskFileFlag, relation, relativeTo, relativeRoot,
|
|
1646
|
+
purpose: flag("purpose"), ...(nameFlag !== undefined ? { name: String(nameFlag) } : {}), task: taskText, taskFile: taskFileFlag, relation, relativeTo, relativeRoot,
|
|
2629
1647
|
...(args.includes("--allow-child-spawns") ? { allowChildSpawns: true } : args.includes("--no-child-spawns") ? { allowChildSpawns: false } : {}),
|
|
2630
1648
|
// Directory execution uses deployment configuration, not an ambient Git
|
|
2631
1649
|
// checkout (especially when invoked via --dir from a source instance).
|
|
2632
|
-
|
|
2633
|
-
|
|
1650
|
+
// An attached instance's repository is its work tree owner's (derived by the kernel).
|
|
1651
|
+
repo: preparedRepo !== undefined ? preparedRepo : ["directory", "attached"].includes(requestedWork || agent.work)
|
|
1652
|
+
? repo : repo || defaultRepo(workspaceOf(root)) || defaultRepo(process.cwd()),
|
|
2634
1653
|
work: requestedWork, workDir, runtime: flag("runtime"), backend, herdrSocket, yolo, model: flag("model"), branch,
|
|
2635
1654
|
launchConfig: valueFlag("launch-config"),
|
|
2636
1655
|
launch: !args.includes("--no-launch"),
|
|
@@ -2644,13 +1663,13 @@ async function spawnCmd() {
|
|
|
2644
1663
|
// K6c: with --idempotency-key, a retry of the SAME confirmed decision replays the recorded home instead of spawning twice.
|
|
2645
1664
|
...(flag("idempotency-key") !== undefined && flag("idempotency-key") !== true ? { idempotencyKey: String(flag("idempotency-key")) } : {}),
|
|
2646
1665
|
};
|
|
2647
|
-
r =
|
|
1666
|
+
r = await spawnInstanceAsync(root, agent, spawnOpts); }
|
|
2648
1667
|
if (args.includes("--preview")) {
|
|
2649
|
-
// A workspace preview may have fetched the soul's SOURCE
|
|
2650
|
-
// (
|
|
1668
|
+
// A workspace preview may have fetched the soul's SOURCE to a temporary copy
|
|
1669
|
+
// (the deployment's cache had no entry for its commit): the result says so.
|
|
2651
1670
|
if (prepared) r.soulFetched = soulFetched;
|
|
2652
1671
|
if (JSON_MODE) { jsonOk(r); return; }
|
|
2653
|
-
console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""}) runtime ${r.runtime}${r.model ? ` model ${r.model}` : ` (${r.modelSource})`}; nothing was created${soulFetched ?
|
|
1672
|
+
console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""}) runtime ${r.runtime}${r.model ? ` model ${r.model}` : ` (${r.modelSource})`}; nothing was created${soulFetched ? " (the soul source was fetched to a temporary copy, not kept)" : ""}`);
|
|
2654
1673
|
return;
|
|
2655
1674
|
}
|
|
2656
1675
|
} catch (e) {
|
|
@@ -2672,6 +1691,8 @@ async function spawnCmd() {
|
|
|
2672
1691
|
if (e?.code === "E_DECISION_STALE") { bail(e.code, e.message, { decision: e.decision }); throw e; }
|
|
2673
1692
|
if (e?.code === "E_IDEMPOTENCY_CONFLICT") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
|
|
2674
1693
|
if (e?.code === "E_PLACEMENT_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
|
|
1694
|
+
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; }
|
|
1695
|
+
if (e?.code === "E_INSTANCE_NAME_INVALID") { bail(e.code, e.message); throw e; }
|
|
2675
1696
|
if (e?.code === "E_SPAWN_INCOMPLETE") { bail(e.code, e.message, { instance: e.instance, home: e.home, launched: e.launched }); throw e; }
|
|
2676
1697
|
bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
|
|
2677
1698
|
}
|
|
@@ -2734,7 +1755,7 @@ function retireCmd() {
|
|
|
2734
1755
|
console.log(` defaults: retain worktree ${plan.defaults.retainWorktree}, delete branch ${plan.defaults.deleteBranch}, stop children ${plan.defaults.stopChildren}`);
|
|
2735
1756
|
for (const n of plan.notes) console.log(` note: ${n}`);
|
|
2736
1757
|
return;
|
|
2737
|
-
} catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message, e.candidates ? { candidates: e.candidates } :
|
|
1758
|
+
} catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message, e.candidates ? { ...e.details, candidates: e.candidates } : e.details) : die(e.message); }
|
|
2738
1759
|
}
|
|
2739
1760
|
// The calling instance knows its own home: self-retire never needs to
|
|
2740
1761
|
// disambiguate a same-named twin by hand.
|
|
@@ -2742,13 +1763,7 @@ function retireCmd() {
|
|
|
2742
1763
|
const isSelf = process.env.PI_AGENT_INSTANCE === name || process.env.OATS_INSTANCE === name;
|
|
2743
1764
|
if (isSelf && !args.includes("--self")) die(`"${name}" is the calling instance — self-retire is irreversible; if your task is complete and you were told to retire, re-run with --self (finish your memory files FIRST; your session dies ~8s after)`);
|
|
2744
1765
|
if (!isSelf && args.includes("--self")) die(`--self given but "${name}" is not the calling instance`);
|
|
2745
|
-
|
|
2746
|
-
// Cross-repo: the instance may home in a sibling repo of the team scope.
|
|
2747
|
-
if (!listAgents(root).some((a) => existsSync(join(a._dir, "instances", name)))) {
|
|
2748
|
-
const hit = findTeamInstance(dirFlag(), name);
|
|
2749
|
-
// Stdout carries only the envelope in JSON mode (the Desktop parses it).
|
|
2750
|
-
if (hit && resolve(hit.root) !== resolve(root)) { root = hit.root; (args.includes("--json") ? console.error : console.log)(`(cross-repo: instance homes at ${shortPath(root)})`); }
|
|
2751
|
-
}
|
|
1766
|
+
const root = ensureRoot(dirFlag());
|
|
2752
1767
|
const retiringHome = homeFlag || findInstanceHome(root, name);
|
|
2753
1768
|
// K3: a GUI-driven Remove carries the plan revision it showed and an
|
|
2754
1769
|
// idempotency key. The revision is revalidated against a fresh plan
|
|
@@ -2765,7 +1780,7 @@ function retireCmd() {
|
|
|
2765
1780
|
const replay = (dir) => { const p = join(dir, `.oats-retire-receipt.${idemKey}.json`); if (!existsSync(p)) return false; try { const prior = JSON.parse(readFileSync(p, "utf8")); if (prior.retired !== name) return false; if (args.includes("--json")) jsonOk({ ...prior, replayed: true }); else console.log(`retire ${name}: replayed receipt for key ${idemKey}`); return true; } catch { return false; } };
|
|
2766
1781
|
for (const a of listAgents(root)) if (replay(join(a._dir, "instances"))) return;
|
|
2767
1782
|
let fresh;
|
|
2768
|
-
try { fresh = planRetire(dirFlag(), root, name, { home: homeFlag }); } catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message) : die(e.message); }
|
|
1783
|
+
try { fresh = planRetire(dirFlag(), root, name, { home: homeFlag }); } catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message, e.details) : die(e.message); }
|
|
2769
1784
|
replayPath = join(dirname(fresh.home), `.oats-retire-receipt.${idemKey}.json`);
|
|
2770
1785
|
if (fresh.planRevision !== planRev) return args.includes("--json") ? jsonFail("E_PLAN_STALE", `the retire plan changed since it was shown (${planRev} → ${fresh.planRevision}); review the fresh plan`, { plan: fresh }) : die(`the retire plan changed since it was shown; re-run oats retire ${name} --plan`);
|
|
2771
1786
|
// The plan promised: recorded children are STOPPED first (bounded, never
|
|
@@ -2780,7 +1795,9 @@ function retireCmd() {
|
|
|
2780
1795
|
if (running.length) return args.includes("--json") ? jsonFail("E_CHILDREN_RUNNING", `${running.map((k) => k.instance).join(", ")} ${running.length === 1 ? "is" : "are"} still running after a bounded stop; nothing was retired and nothing was escalated`, { childrenStopped, plan: fresh }) : die(`children still running: ${running.map((k) => k.instance).join(", ")}; nothing retired`);
|
|
2781
1796
|
expectedBranch = fresh.facts.work.observed ? fresh.facts.work.branch : undefined;
|
|
2782
1797
|
}
|
|
2783
|
-
|
|
1798
|
+
let r;
|
|
1799
|
+
try { r = retireInstance(root, name, { home: homeFlag, self: isSelf, deleteBranch: args.includes("--delete-branch"), discardWorktree: args.includes("--discard-worktree"), keepDir: args.includes("--keep-dir"), force: args.includes("--force"), ...(expectedBranch !== undefined ? { expectedBranch } : {}) }); }
|
|
1800
|
+
catch (e) { if (!e?.code) throw e; return args.includes("--json") ? jsonFail(e.code, e.message, e.candidates ? { ...e.details, candidates: e.candidates } : e.details) : die(e.message); }
|
|
2784
1801
|
if (childrenStopped) r.childrenStopped = childrenStopped;
|
|
2785
1802
|
if (replayPath) { r.planRevision = planRev; r.idempotencyKey = idemKey; r.replayed = false; try { writeFileAtomic(replayPath, JSON.stringify(r, null, 2)); } catch { /* receipt is evidence, not authority */ } }
|
|
2786
1803
|
// A retired home's wake jobs are forgotten (definitions only; nothing is
|
|
@@ -2818,8 +1835,10 @@ function retireCmd() {
|
|
|
2818
1835
|
// which is most of the harm of deleting it. Name the classes and the path.
|
|
2819
1836
|
for (const recovery of r.workRecoveries || (r.workRecovery ? [r.workRecovery] : [])) {
|
|
2820
1837
|
console.log(`Work that was not committed has been preserved: ${recovery.classes.join(", ")}`);
|
|
2821
|
-
console.log(` ${recovery.path}`);
|
|
1838
|
+
console.log(` ${recovery.path}${typeof recovery.bytes === "number" ? ` (${formatBytes(recovery.bytes)})` : ""}`);
|
|
1839
|
+
for (const line of preservedOutputLines(recovery)) console.log(line);
|
|
2822
1840
|
}
|
|
1841
|
+
for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
|
|
2823
1842
|
if (isSelf) console.log("This window dies in ~8s — say any goodbyes now.");
|
|
2824
1843
|
}
|
|
2825
1844
|
|
|
@@ -2829,9 +1848,11 @@ function retireCmd() {
|
|
|
2829
1848
|
function scheduleCmd() {
|
|
2830
1849
|
const sub = args[1];
|
|
2831
1850
|
const id = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
|
|
2832
|
-
// One schedule-owning scope for a directory:
|
|
2833
|
-
//
|
|
2834
|
-
|
|
1851
|
+
// One schedule-owning scope for a directory: its deployment (the directory
|
|
1852
|
+
// holding oats-local.yaml), resolved when a subcommand needs it — inside the
|
|
1853
|
+
// try, so no deployment in reach is a typed refusal (E_LOCAL_MISSING).
|
|
1854
|
+
let scope;
|
|
1855
|
+
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
2835
1856
|
const io = { hostStatus: () => hostUnitStatus() };
|
|
2836
1857
|
const out = (result) => { if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2)); };
|
|
2837
1858
|
const readSpec = () => {
|
|
@@ -2845,41 +1866,41 @@ function scheduleCmd() {
|
|
|
2845
1866
|
const needId = () => { if (!id) throw scheduleError("E_BAD_ARGS", `oats schedule ${sub} <id>`); return id; };
|
|
2846
1867
|
try {
|
|
2847
1868
|
switch (sub) {
|
|
2848
|
-
case "list": return out(listSchedules(ws, io));
|
|
2849
|
-
case "show": return out({ schedule: describeSchedule(ws, needId(), io) });
|
|
2850
|
-
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) }); }
|
|
2851
|
-
case "update": return out({ schedule: updateSchedule(ws, needId(), readSpec(), io) });
|
|
2852
|
-
case "enable": return out({ schedule: setScheduleEnabled(ws, needId(), true, io) });
|
|
2853
|
-
case "disable": return out({ schedule: setScheduleEnabled(ws, needId(), false, io) });
|
|
2854
|
-
case "run": return out(runScheduleNow(ws, needId(), { io, force: args.includes("--force") }));
|
|
2855
|
-
case "remove": return out(removeSchedule(ws, needId(), { force: args.includes("--force") }));
|
|
2856
|
-
case "reconcile": return out(reconcileSchedule(ws, needId(), { io, clear: args.includes("--clear") }));
|
|
1869
|
+
case "list": return out(listSchedules(ws(), io));
|
|
1870
|
+
case "show": return out({ schedule: describeSchedule(ws(), needId(), io) });
|
|
1871
|
+
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) }); }
|
|
1872
|
+
case "update": return out({ schedule: updateSchedule(ws(), needId(), readSpec(), io) });
|
|
1873
|
+
case "enable": return out({ schedule: setScheduleEnabled(ws(), needId(), true, io) });
|
|
1874
|
+
case "disable": return out({ schedule: setScheduleEnabled(ws(), needId(), false, io) });
|
|
1875
|
+
case "run": return out(runScheduleNow(ws(), needId(), { io, force: args.includes("--force") }));
|
|
1876
|
+
case "remove": return out(removeSchedule(ws(), needId(), { force: args.includes("--force") }));
|
|
1877
|
+
case "reconcile": return out(reconcileSchedule(ws(), needId(), { io, clear: args.includes("--clear") }));
|
|
2857
1878
|
case "tick": {
|
|
2858
1879
|
const dryRun = args.includes("--dry-run");
|
|
2859
1880
|
if (args.includes("--host")) return out(tickHost({ io, dryRun }));
|
|
2860
1881
|
const reg = readRegistry();
|
|
2861
|
-
const considered = withHostLock(() => tickWorkspace(ws, { io, reg, wsList: reg.workspaces.includes(ws) ? reg.workspaces : [...reg.workspaces, ws], dryRun }));
|
|
2862
|
-
return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws, io) });
|
|
1882
|
+
const considered = withHostLock(() => tickWorkspace(ws(), { io, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()], dryRun }));
|
|
1883
|
+
return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws(), io) });
|
|
2863
1884
|
}
|
|
2864
1885
|
case "host": {
|
|
2865
1886
|
const op = args[2];
|
|
2866
|
-
if (op === "install") { registerWorkspace(ws); installHostUnit(); return out({ scheduler: schedulerStatus(ws, io) }); }
|
|
2867
|
-
if (op === "uninstall") { unregisterWorkspace(ws); if (!readRegistry().workspaces.length) uninstallHostUnit(); return out({ scheduler: schedulerStatus(ws, io) }); }
|
|
2868
|
-
if (op === "status") return out({ scheduler: schedulerStatus(ws, io) });
|
|
1887
|
+
if (op === "install") { registerWorkspace(ws()); installHostUnit(); return out({ scheduler: schedulerStatus(ws(), io) }); }
|
|
1888
|
+
if (op === "uninstall") { unregisterWorkspace(ws()); if (!readRegistry().workspaces.length) uninstallHostUnit(); return out({ scheduler: schedulerStatus(ws(), io) }); }
|
|
1889
|
+
if (op === "status") return out({ scheduler: schedulerStatus(ws(), io) });
|
|
2869
1890
|
throw scheduleError("E_BAD_ARGS", "oats schedule host install|uninstall|status");
|
|
2870
1891
|
}
|
|
2871
1892
|
default: throw scheduleError("E_BAD_ARGS", "usage: oats schedule list|show <id>|add <id> --file <spec.json>|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]");
|
|
2872
1893
|
}
|
|
2873
1894
|
} catch (e) {
|
|
2874
1895
|
// K8b: typed refusal details travel (identity mismatch: key/declared; a refused file: its integrity source).
|
|
2875
|
-
const details = Object.fromEntries(["key", "declared", "source", "field"].filter((k) => e[k] !== undefined).map((k) => [k, e[k]]));
|
|
1896
|
+
const details = { ...(e.details && typeof e.details === "object" ? e.details : {}), ...Object.fromEntries(["key", "declared", "source", "field"].filter((k) => e[k] !== undefined).map((k) => [k, e[k]])) };
|
|
2876
1897
|
if (JSON_MODE) jsonFail(e.code || "E_SCHEDULE_FAILED", e.message, Object.keys(details).length ? details : undefined); else die(e.message);
|
|
2877
1898
|
}
|
|
2878
1899
|
}
|
|
2879
1900
|
|
|
2880
1901
|
async function sessionCmd() {
|
|
2881
1902
|
try {
|
|
2882
|
-
if (flag("native-record") !== undefined) throw Object.assign(new Error("--native-record outcome inspection
|
|
1903
|
+
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" });
|
|
2883
1904
|
const home = flag("home");
|
|
2884
1905
|
let result;
|
|
2885
1906
|
if (args[1] === "attach") {
|
|
@@ -2888,7 +1909,7 @@ async function sessionCmd() {
|
|
|
2888
1909
|
return;
|
|
2889
1910
|
}
|
|
2890
1911
|
if (args[1] === "inspect") result = inspectInstanceSession(home);
|
|
2891
|
-
else if (args[1] === "recompose")
|
|
1912
|
+
else if (args[1] === "recompose") throw Object.assign(new Error('unknown command "session recompose" — removed by the workspace model v2; use a re-spawn: an instance never changes under itself'), { code: "E_UNKNOWN_COMMAND" });
|
|
2892
1913
|
else if (args[1] === "start" || args[1] === "restart") {
|
|
2893
1914
|
const bad = (msg) => { throw Object.assign(new Error(msg), { code: "E_BAD_ARGS" }); };
|
|
2894
1915
|
const model = flag("model");
|
|
@@ -2897,7 +1918,7 @@ async function sessionCmd() {
|
|
|
2897
1918
|
if (launchConfig === true) bad("--launch-config needs a configuration name, or none");
|
|
2898
1919
|
const runtime = flag("runtime");
|
|
2899
1920
|
if (runtime === true || (runtime !== undefined && !LAUNCH_RUNTIMES.includes(runtime))) bad(`--runtime must be one of ${LAUNCH_RUNTIMES.join(", ")}`);
|
|
2900
|
-
const opts = { model: model || undefined, launchConfig, runtime, yolo: yoloFlag(), env: process.env };
|
|
1921
|
+
const opts = { model: model || undefined, launchConfig, runtime, yolo: yoloFlag(), env: process.env, ...(await homeLiveTeams(home)) };
|
|
2901
1922
|
if (args[1] === "restart") {
|
|
2902
1923
|
const grace = flag("stop-grace");
|
|
2903
1924
|
if (grace !== undefined) { if (grace === true || !/^\d+$/.test(String(grace)) || Number(grace) < 1 || Number(grace) > 300) bad("--stop-grace needs a number of seconds (1..300) to wait for the harness after SIGTERM"); opts.stopGraceMs = Number(grace) * 1000; }
|
|
@@ -2922,7 +1943,7 @@ async function sessionCmd() {
|
|
|
2922
1943
|
result = uploadAttachment({ file, home: home === true ? undefined : home });
|
|
2923
1944
|
} else throw Object.assign(new Error("usage: oats session inspect|input|attach|start|restart|receive|upload --home /absolute/home [--text-file path] [--model id] [--launch-config name|none] [--runtime pi|claude|codex] [--yolo|--no-yolo] [--stop-grace seconds] [--name file] [--file path] [--json]"), { code: "E_BAD_ARGS" });
|
|
2924
1945
|
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
2925
|
-
} catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message); }
|
|
1946
|
+
} catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message, e.details); }
|
|
2926
1947
|
}
|
|
2927
1948
|
|
|
2928
1949
|
async function paneCmd() {
|
|
@@ -2935,8 +1956,7 @@ async function paneCmd() {
|
|
|
2935
1956
|
* (docs/design/2026-09-23-simplified-workspace-model.md §4): writes
|
|
2936
1957
|
* `<dir>/oats-local.yaml` naming the workspace, creates `<dir>/agents/` (the
|
|
2937
1958
|
* instance homes), then runs exactly the `oats sync` path — discover over the
|
|
2938
|
-
* remotes, confirm membership, resolve `packages:`,
|
|
2939
|
-
* needs approval (exit 2), write `oats-lock.json`. Nothing is installed, no soul
|
|
1959
|
+
* remotes, confirm membership, resolve `packages:`, write `oats-lock.json`. Nothing is installed, no soul
|
|
2940
1960
|
* is created, nothing is spawned, no `oats-config.yaml` is written: the member
|
|
2941
1961
|
* clones and the operator expert are the operator's next steps, printed here. */
|
|
2942
1962
|
async function onboardCmd() {
|
|
@@ -3043,14 +2063,14 @@ async function onboardCmd() {
|
|
|
3043
2063
|
const hostIsMember = members.some((m) => m.key === synced.discovery.key);
|
|
3044
2064
|
const hosting = { host: synced.discovery.key, hostIsMember, rule: "If any member is private, host oats-workspace.yaml in a private repo that is not a public member (a dedicated <org>/workspace repo); public contributors then use the standalone case (from: here capabilities + oats.core)." };
|
|
3045
2065
|
const result = { onboardApi: 2, standalone: standalone || undefined, local: localFile, dir, agents: join(dir, "agents"), lock: synced.lockFile, sync: synced.report, hosting, next: { clone: clones, spawn: spawnHint, souls: soulNames.slice(0, 3) } };
|
|
3046
|
-
if (JSON_MODE) { jsonOk(result);
|
|
2066
|
+
if (JSON_MODE) { jsonOk(result); return; }
|
|
3047
2067
|
|
|
3048
2068
|
console.log(`Onboarded ${shortPath(dir)} into workspace ${workspaceName(synced.discovery)} (${synced.discovery.key} @ ${short(synced.discovery.commit)}).${standalone ? `\n (${standaloneNote(synced.discovery, { from: " from here" })})` : ""}\n`);
|
|
3049
2069
|
printSyncReport(ctx, synced);
|
|
3050
2070
|
console.log(`
|
|
3051
2071
|
This directory (${shortPath(dir)}) is your deployment — any layout works; it now holds what the kernel needs:
|
|
3052
2072
|
├── oats-local.yaml which workspace this machine realizes (+ host settings, disabled souls)
|
|
3053
|
-
├── oats-lock.json exact commit + integrity
|
|
2073
|
+
├── oats-lock.json exact commit + integrity per package
|
|
3054
2074
|
└── agents/ instance homes, each self-contained
|
|
3055
2075
|
Member clones live wherever you keep them (here, or anywhere named in oats-local.yaml clones:).
|
|
3056
2076
|
|
|
@@ -3060,9 +2080,8 @@ Next:
|
|
|
3060
2080
|
2. Check who may read the host: ${synced.discovery.key}${hostIsMember ? " is itself a member" : " is a dedicated host"}. The workspace file
|
|
3061
2081
|
names every member, so if any member is private the host must be a private repo that is not
|
|
3062
2082
|
a public member; public contributors then get the standalone case (from: here + oats.core).
|
|
3063
|
-
3. ${setupExpert ? "Spawn the operator expert to guide the rest (souls, teams, provider settings
|
|
3064
|
-
${spawnHint ?? anySoulHint}
|
|
3065
|
-
process.exitCode = synced.approvalNeeded.length ? 2 : 0;
|
|
2083
|
+
3. ${setupExpert ? "Spawn the operator expert to guide the rest (souls, teams, provider settings):" : "No soul named oats-operator-expert is listed here —"}
|
|
2084
|
+
${spawnHint ?? anySoulHint}`);
|
|
3066
2085
|
}
|
|
3067
2086
|
|
|
3068
2087
|
/** The clone URL of a member row: what the remote observed (from the workspace's members: refs;
|
|
@@ -3075,45 +2094,6 @@ function memberUrlOf(discovery, key) {
|
|
|
3075
2094
|
return null;
|
|
3076
2095
|
}
|
|
3077
2096
|
|
|
3078
|
-
function createCmd() {
|
|
3079
|
-
const yolo = yoloFlag();
|
|
3080
|
-
const name = args[1];
|
|
3081
|
-
if (!name || name.startsWith("--")) die("usage: oats create <name> [--local] [--no-oats-core] [--description <d>] [--type <agent-type>] [--repo <r>] [--work worktree|checkout|attached|workspace|directory] [--runtime pi|claude|codex] [--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]");
|
|
3082
|
-
const local = args.includes("--local");
|
|
3083
|
-
const startDir = dirFlag();
|
|
3084
|
-
// `create` BOOTSTRAPS a deployment: with no agents/ or local-agents/ yet,
|
|
3085
|
-
// anchor at the enclosing git repo (else the start dir). It is the command
|
|
3086
|
-
// that populates the roster root, so it must not demand that the root
|
|
3087
|
-
// already exist — that demand was the first thing a new user hit after
|
|
3088
|
-
// `oats init` (a raw stack trace from ensureRoot). Local and committed souls
|
|
3089
|
-
// anchor the same way; writeSoul creates the directories.
|
|
3090
|
-
let root = findRoot(startDir);
|
|
3091
|
-
// A configured package-only scope may resolve its future agents root before
|
|
3092
|
-
// that directory exists. Preserve create's bootstrap receipt/message.
|
|
3093
|
-
let bootstrapped = !!root && !existsSync(root) && !existsSync(join(dirname(root), "local-agents"));
|
|
3094
|
-
if (!root) {
|
|
3095
|
-
root = join(defaultRepo(startDir) || resolve(startDir), "agents");
|
|
3096
|
-
bootstrapped = true;
|
|
3097
|
-
}
|
|
3098
|
-
const instrFile = flag("instructions-file");
|
|
3099
|
-
const r = coreCreateAgent(root, {
|
|
3100
|
-
name, local, oatsCore: !args.includes("--no-oats-core"), description: flag("description"), type: flag("type"), repo: flag("repo") || (flag("work") === "directory" ? undefined : defaultRepo(process.cwd())),
|
|
3101
|
-
work: flag("work"), runtime: flag("runtime"), model: flag("model"), yolo,
|
|
3102
|
-
instructions: instrFile ? readFileSync(instrFile, "utf8") : undefined,
|
|
3103
|
-
});
|
|
3104
|
-
// A declared oats.core is a requirement spawn will enforce: say the next
|
|
3105
|
-
// step here, not only at the refusal.
|
|
3106
|
-
const declared = r.declaredCapabilities || [];
|
|
3107
|
-
const inactive = declared.filter((id) => !(resolveOatsConfig(workspaceOf(root), name).capabilities || []).some((c) => c.id === id));
|
|
3108
|
-
const next = inactive.length ? [{ code: "next-step", message: `${name} declares ${inactive.join(", ")}; before spawning, pin their packages in oats-workspace.yaml packages: and declare them in soul.yaml capabilities: { <cap>: { from } } (workspace model v2)` }] : [];
|
|
3109
|
-
const notes = [...(r.notes || []), ...next];
|
|
3110
|
-
if (args.includes("--json")) { console.log(JSON.stringify({ ...r, ...(notes.length ? { notes } : {}), ...(bootstrapped ? { agentsRoot: root } : {}) }, null, 2)); return; }
|
|
3111
|
-
for (const information of notes) console.error(`[${information.code}] ${information.message}`);
|
|
3112
|
-
if (bootstrapped) console.log(`Created deployment root ${shortPath(root)} (this scope had no agents/ yet)`);
|
|
3113
|
-
console.log(`Created ${r.kind === "local" ? "LOCAL agent (uncommitted — soul lives in local-agents/, gitignored)" : "agent"} "${r.agent}" — soul at ${shortPath(r.soul)}`);
|
|
3114
|
-
console.log(`Edit ${shortPath(join(r.soul, "AGENTS.md"))} to define its role, then${inactive.length ? ` activate ${inactive.join(", ")} (above) and` : ":"} oats spawn ${r.agent} --task "..."`);
|
|
3115
|
-
}
|
|
3116
|
-
|
|
3117
2097
|
// ---------- capability command dispatch ----------
|
|
3118
2098
|
/**
|
|
3119
2099
|
* oats <namespace> <command> [args…] — run a command an active capability
|
|
@@ -3127,7 +2107,7 @@ function createCmd() {
|
|
|
3127
2107
|
* namespace's capability into <deployment>/.oats/modules/<cap>@<commit12>/
|
|
3128
2108
|
* and run THAT copy with the soul's merged payload (lib/operator-dispatch.mjs;
|
|
3129
2109
|
* contracts doc, "Post-0.25.0 clarifications");
|
|
3130
|
-
* - otherwise the
|
|
2110
|
+
* - otherwise no namespace is active (the help fallthrough answers).
|
|
3131
2111
|
*/
|
|
3132
2112
|
async function capabilityCommand() {
|
|
3133
2113
|
// JSON-aware boundary: in --json mode every dispatch failure — inactive or
|
|
@@ -3159,64 +2139,79 @@ async function capabilityCommand() {
|
|
|
3159
2139
|
throw e;
|
|
3160
2140
|
}
|
|
3161
2141
|
if (!hit) return NOT_DISPATCHED;
|
|
3162
|
-
|
|
3163
|
-
|
|
2142
|
+
// The same team/workspace facts a spawn hook receives (lead decision c3-7).
|
|
2143
|
+
const teamCtx = teamEnv(resolvedFromPrepared(hit.prepared, hit.deployment));
|
|
2144
|
+
// No home, so no recorded soul: the soul's per-commit copy is OATS_SOUL when a spawn
|
|
2145
|
+
// already fetched exactly this commit; otherwise the command gets none (never ambient).
|
|
2146
|
+
const cachedSoul = hit.soul?.commit ? join(hit.deployment, "agents", hit.soul.name, "souls", String(hit.soul.commit).slice(0, 12)) : null;
|
|
2147
|
+
return runManifestCommand({ capability: hit.module.name, ...hit.manifest }, hit.settings, teamCtx, hit.ensureTree, cachedSoul && existsSync(join(cachedSoul, "soul.yaml")) ? realpathSync(cachedSoul) : undefined);
|
|
3164
2148
|
}
|
|
3165
2149
|
|
|
3166
2150
|
async function dispatch() {
|
|
3167
2151
|
let activeIds;
|
|
3168
2152
|
let context = process.cwd();
|
|
3169
|
-
let teamCtx;
|
|
3170
|
-
|
|
2153
|
+
let teamCtx, homeMeta, homeTeamCtx;
|
|
2154
|
+
// OATS_INSTANCE_HOME is the canonical identity; the older names still count.
|
|
2155
|
+
const instanceHome = process.env.OATS_INSTANCE_HOME || process.env.PI_AGENT_HOME || process.env.OATS_HOME;
|
|
3171
2156
|
const metaFile = instanceHome && join(instanceHome, "instance.json");
|
|
3172
2157
|
// Capability-id keyed — never answer for `constructor`/`toString`. Belt and
|
|
3173
2158
|
// braces: the ids come from instance.json, which spawn wrote from resolved
|
|
3174
2159
|
// manifests. Null-prototype because the dispatcher indexes it with the
|
|
3175
2160
|
// namespace the operator typed on the command line.
|
|
3176
2161
|
let capSettings = Object.create(null);
|
|
3177
|
-
let instanceModules = false;
|
|
3178
2162
|
let deployment = null;
|
|
2163
|
+
let soulDir;
|
|
3179
2164
|
try {
|
|
3180
2165
|
if (metaFile && existsSync(metaFile)) {
|
|
3181
2166
|
const meta = JSON.parse(readFileSync(metaFile, "utf8"));
|
|
3182
|
-
instanceModules = !!(meta.modules && typeof meta.modules === "object");
|
|
3183
2167
|
activeIds = (meta.capabilities || []).map((c) => c.id);
|
|
3184
2168
|
for (const c of meta.capabilities || []) capSettings[c.id] = c.settings || {};
|
|
2169
|
+
// Workspace-model homes only (lead decision c3 Q2).
|
|
2170
|
+
if (isCapturedHome(meta)) { const e = capturedHomeRefusal(instanceHome, "nothing was dispatched"); bail(e.code, e.message, e.details); }
|
|
2171
|
+
if (!isWorkspaceHome(meta)) { const e = preWorkspaceHome(instanceHome, "nothing was dispatched"); bail(e.code, e.message); }
|
|
3185
2172
|
context = meta.repo || context;
|
|
3186
|
-
|
|
3187
|
-
//
|
|
3188
|
-
|
|
2173
|
+
soulDir = instanceSoulDir(instanceHome, meta);
|
|
2174
|
+
// The team/workspace facts the home recorded at spawn, as its hooks got them, with the
|
|
2175
|
+
// recorded eligible teams (OATS_TEAMS_SOURCE=recorded). Only the home's MESSAGING module
|
|
2176
|
+
// gets them live (below): its team verbs (join/leave/teams) must see what the workspace
|
|
2177
|
+
// allows now, and no other command pays a remote read for them.
|
|
2178
|
+
const ws = meta.workspace && typeof meta.workspace === "object" ? meta.workspace : {};
|
|
2179
|
+
const messaging = (meta.capabilities || []).find((c) => c.layer === "messaging")?.id;
|
|
2180
|
+
homeMeta = { meta, messaging };
|
|
2181
|
+
homeTeamCtx = (teams, teamsSource) => teamEnv({ workspace: { key: ws.key, name: ws.name, deployment: ws.deployment, team: ws.soul?.team, slots: { messaging } }, payloads: meta.providers, teams, teamsSource });
|
|
2182
|
+
teamCtx = homeTeamCtx(meta.teams, "recorded");
|
|
3189
2183
|
} else {
|
|
3190
2184
|
// Not inside a home: a v2 deployment (oats-local.yaml in reach) resolves
|
|
3191
2185
|
// through the workspace, exactly as a spawn of --soul would (below).
|
|
3192
2186
|
try { const { deploymentOf } = await import("../lib/operator-dispatch.mjs"); deployment = deploymentOf(context); }
|
|
3193
2187
|
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
activeIds = resolved.capabilities.map((c) => c.id);
|
|
3197
|
-
for (const c of resolved.capabilities) capSettings[c.id] = c.settings || {};
|
|
3198
|
-
teamCtx = resolved.team;
|
|
3199
|
-
}
|
|
2188
|
+
// No home and no deployment in reach: no capability namespace is active.
|
|
2189
|
+
if (!deployment) return NOT_DISPATCHED;
|
|
3200
2190
|
}
|
|
3201
2191
|
} catch (e) { bail("E_CONFIG_BROKEN", e.message || e); throw e; }
|
|
3202
2192
|
if (deployment) return operatorDispatch();
|
|
3203
2193
|
// Workspace model: an instance's own materialized modules are the command
|
|
3204
2194
|
// namespaces available to it (instance.json.modules → <home>/.oats/modules).
|
|
3205
|
-
const mans = Object.values(capabilityManifests(
|
|
2195
|
+
const mans = Object.values(capabilityManifests(instanceHome)).filter((m) => m.command === cmd && m.commands);
|
|
3206
2196
|
if (!mans.length) return NOT_DISPATCHED;
|
|
3207
2197
|
if (mans.length > 1) bail("E_DUPLICATE_NAMESPACE", `duplicate operational command namespace "${cmd}": ${mans.map((m) => m.capability).join(", ")}`);
|
|
3208
2198
|
const m = mans[0];
|
|
3209
2199
|
if (!activeIds.includes(m.capability)) bail("E_CAPABILITY_INACTIVE", `${m.capability} command namespace is not active in the current context/instance`);
|
|
3210
|
-
const trust = capabilityTrust(m
|
|
2200
|
+
const trust = capabilityTrust(m);
|
|
3211
2201
|
if (!trust.trusted) bail("E_CAPABILITY_BLOCKED", `${m.capability} executable command is blocked: ${trust.reason}`);
|
|
3212
|
-
|
|
2202
|
+
if (homeMeta && m.capability === homeMeta.messaging) {
|
|
2203
|
+
const { liveTeams } = await import("../lib/instance-resolution.mjs");
|
|
2204
|
+
const live = await liveTeams(instanceHome, homeMeta.meta, { remoteOptions: remoteOptionsFromEnv() });
|
|
2205
|
+
teamCtx = homeTeamCtx(live.teams, live.source);
|
|
2206
|
+
}
|
|
2207
|
+
return runManifestCommand(m, capSettings[m.capability] || {}, teamCtx, () => m._dir, soulDir);
|
|
3213
2208
|
}
|
|
3214
2209
|
|
|
3215
2210
|
/** Help / unknown-command / spec validation / exec — shared by every context.
|
|
3216
2211
|
* `m` is the manifest (with `capability`; `_dir` may be absent until `ensureDir`
|
|
3217
2212
|
* resolves the directory holding the executable — the operator branch fetches
|
|
3218
2213
|
* the module tree only when a command is actually going to run). */
|
|
3219
|
-
async function runManifestCommand(m, settings, teamCtx, ensureDir) {
|
|
2214
|
+
async function runManifestCommand(m, settings, teamCtx, ensureDir, soulDir) {
|
|
3220
2215
|
const sub = args[1];
|
|
3221
2216
|
const cmds = Object.keys(m.commands);
|
|
3222
2217
|
// `oats <ns> --help` and `oats <ns> <cmd> --help` answer from the manifest
|
|
@@ -3250,8 +2245,11 @@ async function capabilityCommand() {
|
|
|
3250
2245
|
try { abs = capabilityExecutablePath(withDir, script); }
|
|
3251
2246
|
catch (e) { bail("E_CAPABILITY_BROKEN", e.message); }
|
|
3252
2247
|
if (!abs) bail("E_CAPABILITY_BROKEN", `${cmd} ${sub}: script not found (${join(dir, script)})`);
|
|
2248
|
+
// OATS_SOUL is the recorded soul or nothing: an ambient value inherited from the
|
|
2249
|
+
// invoking process names some other soul (a coordinator's own), never this one.
|
|
2250
|
+
const { OATS_SOUL: _ambientSoul, ...inherited } = process.env;
|
|
3253
2251
|
const r = spawnSync("node", [abs, ...rest, ...args.slice(2)], { stdio: "inherit", env: {
|
|
3254
|
-
...
|
|
2252
|
+
...inherited, OATS_CAPABILITY: m.capability,
|
|
3255
2253
|
// Package-runtime boundary: dispatched commands receive the active
|
|
3256
2254
|
// capability's EFFECTIVE settings (instance snapshot, resolved context, or
|
|
3257
2255
|
// the soul's merged payload on operator-level dispatch), same contract as
|
|
@@ -3262,7 +2260,10 @@ async function capabilityCommand() {
|
|
|
3262
2260
|
// canonical absolute executable of THIS CLI; official consumers execFile
|
|
3263
2261
|
// it directly and never resolve `oats` from PATH or a shell.
|
|
3264
2262
|
OATS_CLI_BIN: CLI_BIN,
|
|
3265
|
-
|
|
2263
|
+
...teamEnv(null), ...(teamCtx || {}),
|
|
2264
|
+
// The soul the command acts for (an instance home's recorded soul directory):
|
|
2265
|
+
// homes carry no soul link, so providers read it here.
|
|
2266
|
+
...(soulDir ? { OATS_SOUL: soulDir } : {}),
|
|
3266
2267
|
} });
|
|
3267
2268
|
// Child never ran (spawn error): nothing reached stdout — keep the envelope contract.
|
|
3268
2269
|
if (r.error) bail("E_CAPABILITY_BROKEN", `oats ${cmd} ${sub}: ${r.error.message || r.error}`);
|
|
@@ -3270,52 +2271,6 @@ async function capabilityCommand() {
|
|
|
3270
2271
|
}
|
|
3271
2272
|
}
|
|
3272
2273
|
|
|
3273
|
-
// ---------- agent types ----------
|
|
3274
|
-
function typeCmd() {
|
|
3275
|
-
const sub = args[1];
|
|
3276
|
-
const dir = dirFlag();
|
|
3277
|
-
const file = join(dir, "oats-config.yaml");
|
|
3278
|
-
if (sub === "list") {
|
|
3279
|
-
const seen = new Map();
|
|
3280
|
-
for (const cfg of configChain(dir)) for (const [name, spec] of Object.entries(cfg["agent-types"] || {})) if (!seen.has(name)) seen.set(name, { desc: spec?.description, level: cfg._level });
|
|
3281
|
-
if (!seen.size) { console.log("No agent types declared in the config chain."); return; }
|
|
3282
|
-
for (const [name, { desc, level }] of seen) console.log(`${name} ${desc ? `— ${desc} ` : ""}[${shortPath(level)}]`);
|
|
3283
|
-
return;
|
|
3284
|
-
}
|
|
3285
|
-
if (sub !== "add" || !args[2] || args[2].startsWith("--")) die("usage: oats type add <name> [--description <d>] [--dir <dir>] | oats type list [--dir <dir>]");
|
|
3286
|
-
const name = args[2];
|
|
3287
|
-
if (!/^[a-z][a-z0-9-]*$/.test(name)) die(`agent type "${name}" must be lowercase alphanumeric/hyphens`);
|
|
3288
|
-
const description = flag("description");
|
|
3289
|
-
let text = existsSync(file) ? readFileSync(file, "utf8") : `name: ${scaffoldConfigName(dir)}\n`;
|
|
3290
|
-
const cfg = existsSync(file) ? withConfigFile(file, () => parseYamlNested(text)) : {};
|
|
3291
|
-
// Own-property: `constructor` is a legal agent-type name, and a plain lookup
|
|
3292
|
-
// would report it as already declared in a config that never mentions it.
|
|
3293
|
-
const declaredTypes = cfg["agent-types"];
|
|
3294
|
-
if (declaredTypes && typeof declaredTypes === "object" && Object.hasOwn(declaredTypes, name)) die(`agent type "${name}" already declared in ${shortPath(file)}`);
|
|
3295
|
-
// The NAME is already held to a strict grammar above; the DESCRIPTION was
|
|
3296
|
-
// written verbatim onto its own line, so it could inject document the same
|
|
3297
|
-
// way a `--settings` value could.
|
|
3298
|
-
const block = [` ${name}:`, ...(description ? [` description: ${assertSafeConfigValue(description, "--description")}`] : [])];
|
|
3299
|
-
const lines = text.replace(/\n*$/, "\n").split("\n");
|
|
3300
|
-
// Drop the scaffold comment block once a real agent-types block exists.
|
|
3301
|
-
const scaffold = lines.findIndex((l) => /^# ── Agent types/.test(l));
|
|
3302
|
-
if (scaffold >= 0) {
|
|
3303
|
-
let e = scaffold;
|
|
3304
|
-
while (e < lines.length && (/^#/.test(lines[e]) || lines[e] === "")) { if (lines[e] === "" && !/^#/.test(lines[e + 1] || "x")) break; e++; }
|
|
3305
|
-
lines.splice(scaffold, e - scaffold);
|
|
3306
|
-
}
|
|
3307
|
-
const start = lines.findIndex((l) => /^agent-types:\s*(#.*)?$/.test(l));
|
|
3308
|
-
if (start >= 0) {
|
|
3309
|
-
let end = start + 1;
|
|
3310
|
-
while (end < lines.length && (/^\s/.test(lines[end]) || lines[end] === "")) { if (lines[end] === "" && !/^\s/.test(lines[end + 1] || "x")) break; end++; }
|
|
3311
|
-
lines.splice(end, 0, ...block);
|
|
3312
|
-
} else {
|
|
3313
|
-
lines.splice(1, 0, "", "agent-types:", ...block);
|
|
3314
|
-
}
|
|
3315
|
-
writeFileSync(file, lines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "\n"));
|
|
3316
|
-
console.log(`Declared agent type "${name}" at ${levelOf(dir)} level (${shortPath(file)})`);
|
|
3317
|
-
console.log(`Souls join it with: oats create <agent> --type ${name} (or type: ${name} in soul.yaml)`);
|
|
3318
|
-
}
|
|
3319
2274
|
|
|
3320
2275
|
// ---------- update ----------
|
|
3321
2276
|
function updateCmd() {
|
|
@@ -3370,7 +2325,7 @@ function versionCmd() {
|
|
|
3370
2325
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
3371
2326
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
3372
2327
|
// never listed.
|
|
3373
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["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", "
|
|
2328
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, runtimes: ["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"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
|
|
3374
2329
|
return;
|
|
3375
2330
|
}
|
|
3376
2331
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3508,7 +2463,7 @@ async function serverRouteCmd() {
|
|
|
3508
2463
|
// The operations contract addresses an exact member context on the host,
|
|
3509
2464
|
// so its explicit --dir travels; every other routed command takes its
|
|
3510
2465
|
// scope from the registration.
|
|
3511
|
-
const explicitScopeOk = ["inspect", "operation", "
|
|
2466
|
+
const explicitScopeOk = ["inspect", "operation", "launch-config"].includes(cmd);
|
|
3512
2467
|
if (!explicitScopeOk && (flag("dir") !== undefined || args.some((a) => a.startsWith("--dir=")))) bail("E_BAD_ARGS", "--dir cannot be combined with --server: the remote workspace comes from the server registration");
|
|
3513
2468
|
if (cmd === "launch-config") {
|
|
3514
2469
|
const action = args[1];
|
|
@@ -3635,7 +2590,6 @@ async function serverRouteCmd() {
|
|
|
3635
2590
|
// local --task-file is read here and travels as --task text, since the
|
|
3636
2591
|
// remote cannot read this machine's files.
|
|
3637
2592
|
const rest = [];
|
|
3638
|
-
let routedInput;
|
|
3639
2593
|
for (let i = 1; i < args.length; i++) {
|
|
3640
2594
|
const a = args[i];
|
|
3641
2595
|
if (a === "--server") { i++; continue; }
|
|
@@ -3665,23 +2619,10 @@ async function serverRouteCmd() {
|
|
|
3665
2619
|
rest.push("--wake-message", readFileSync(f, "utf8"));
|
|
3666
2620
|
continue;
|
|
3667
2621
|
}
|
|
3668
|
-
// Soul instructions travel as BYTES on the ssh stdin (the same transport
|
|
3669
|
-
// as session upload), never as a path the host cannot read nor as a
|
|
3670
|
-
// command-line argument.
|
|
3671
|
-
if (a === "--instructions-file") {
|
|
3672
|
-
const f = args[++i];
|
|
3673
|
-
if (!f || f.startsWith("--")) bail("E_BAD_ARGS", "--instructions-file needs a path");
|
|
3674
|
-
let bytes; try { bytes = readFileSync(f); } catch (e) { bail("E_BAD_ARGS", `instructions file not readable: ${f}: ${e.message}`); }
|
|
3675
|
-
if (bytes.includes(0)) bail("E_BAD_ARGS", "--instructions-file must be text without NUL bytes");
|
|
3676
|
-
if (bytes.length > INSPECT_TEXT_CAP) bail("E_BAD_ARGS", `--instructions-file is ${bytes.length} bytes; the bound is ${INSPECT_TEXT_CAP}`);
|
|
3677
|
-
routedInput = bytes;
|
|
3678
|
-
rest.push("--instructions-stdin");
|
|
3679
|
-
continue;
|
|
3680
|
-
}
|
|
3681
2622
|
rest.push(a);
|
|
3682
2623
|
}
|
|
3683
2624
|
let routed;
|
|
3684
|
-
try { routed = routeCommand(id, cmd, rest
|
|
2625
|
+
try { routed = routeCommand(id, cmd, rest); }
|
|
3685
2626
|
catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3686
2627
|
const { envelope, stderr } = routed;
|
|
3687
2628
|
if (stderr && stderr.trim()) process.stderr.write(stderr.endsWith("\n") ? stderr : stderr + "\n");
|
|
@@ -3706,7 +2647,8 @@ async function serverRouteCmd() {
|
|
|
3706
2647
|
console.log(`Retired ${r.retired} on ${id}${r.deferred ? " (deferred completion scheduled there)" : ""}${r.rollbackIncomplete ? " — cleanup INCOMPLETE on the server, home retained there" : ""}`);
|
|
3707
2648
|
for (const recovery of r.workRecoveries || (r.workRecovery ? [r.workRecovery] : [])) {
|
|
3708
2649
|
console.log(`Work that was not committed has been preserved on ${target.sshHost}: ${(recovery.classes || []).join(", ")}`);
|
|
3709
|
-
console.log(` ${recovery.path}`);
|
|
2650
|
+
console.log(` ${recovery.path}${typeof recovery.bytes === "number" ? ` (${formatBytes(recovery.bytes)})` : ""}`);
|
|
2651
|
+
for (const line of preservedOutputLines(recovery)) console.log(line);
|
|
3710
2652
|
}
|
|
3711
2653
|
if (r.rollbackIncomplete) { for (const f of r.rollbackIncomplete) console.error(` ${f}`); console.error(`Fix the cause there and re-run \`oats retire ${r.retired} --server ${id}\`.`); process.exit(1); }
|
|
3712
2654
|
} else {
|
|
@@ -3735,70 +2677,44 @@ async function serverRouteCmd() {
|
|
|
3735
2677
|
// blame` pointing at the commit that last changed each command.
|
|
3736
2678
|
const TYPED_CLI_FAILURES = new Set(["unsafe-config-key", "unsafe-config-value"]);
|
|
3737
2679
|
/** Removed 0.24 verbs → their v2 replacement (workspace model v2, decision 5). Checked before capability dispatch. */
|
|
3738
|
-
const REMOVED_VERBS = { install: "oats sync", restore: "oats sync", init: "oats-local.yaml + oats sync", use: "soul.yaml capabilities: { <cap>: { from } } + workspace defaults", trust: "
|
|
2680
|
+
const REMOVED_VERBS = { prepare: "`oats onboard` / `oats sync` to set up a workspace, and `oats spawn <soul> --preview` to see what a spawn would resolve (the captured/portable path was removed in 0.26)", create: "author souls/<name>/soul.yaml + AGENTS.md in a member repository, then `oats sync`", type: "the soul's own soul.yaml in its member repository (agent types were a classic config block)", soul: "the soul's soul.yaml / AGENTS.md in its member repository, then `oats sync` (per-spawn choices: spawn flags or a launch configuration)", install: "oats sync", restore: "oats sync", init: "oats-local.yaml + oats sync", use: "soul.yaml capabilities: { <cap>: { from } } + workspace defaults", trust: "declaring the package in packages: (package approval was removed; oats sync locks commit + integrity)", list: "oats workspace status | oats capabilities", catalog: "oats package add <id> <version> (bare versions resolve through package-catalog.json)", remove: "oats package remove <id>", migrate: "a rebuild (no migration: docs/design/2026-09-23-workspace-module-contracts.md)", config: "oats-local.yaml (host settings) and oats-workspace.yaml (shared)", inject: "injection overrides are not part of the workspace model yet; edit the capability inject in its member repo" };
|
|
3739
2681
|
try {
|
|
3740
|
-
//
|
|
3741
|
-
//
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
}
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
if (
|
|
3751
|
-
if (
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
if (cmd === "prepare" || captured?.args[0] === "prepare") {
|
|
3759
|
-
if (captured) {
|
|
3760
|
-
if (JSON_MODE) jsonFail("E_BAD_ARGS", "prepare is explicit new work and cannot use captured selectors");
|
|
3761
|
-
die("prepare is explicit new work and cannot use captured selectors");
|
|
3762
|
-
}
|
|
3763
|
-
if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
|
|
3764
|
-
prepareCmd(); process.exit(0);
|
|
3765
|
-
}
|
|
3766
|
-
if (cmd === "onboard" || captured?.args[0] === "onboard") {
|
|
3767
|
-
if (captured) {
|
|
3768
|
-
if (JSON_MODE) jsonFail("E_BAD_ARGS", "onboard is explicit workspace bootstrap and cannot use captured selectors");
|
|
3769
|
-
die("onboard is explicit workspace bootstrap and cannot use captured selectors");
|
|
3770
|
-
}
|
|
2682
|
+
// The captured/portable path was removed in 0.26 (lead decisions on (e), D2/D3):
|
|
2683
|
+
// its selectors and an inherited captured context are refused, never quietly
|
|
2684
|
+
// resolved against the current context instead. `version` answers regardless:
|
|
2685
|
+
// host protocol negotiation describes this executable.
|
|
2686
|
+
{
|
|
2687
|
+
const end = args.indexOf("--"), head = end < 0 ? args : args.slice(0, end);
|
|
2688
|
+
const selector = head.find((a) => /^--(deployment|resolution|artifact-set)(=|$)/.test(a));
|
|
2689
|
+
const refuse = (message, details) => { if (JSON_MODE) jsonFail("E_UNSUPPORTED_MODE", message, details); die(message); };
|
|
2690
|
+
if (selector) refuse(`${selector.split("=")[0]}: 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: selector.split("=")[0] });
|
|
2691
|
+
const inherited = ["OATS_RESOLUTION", "OATS_DEPLOYMENT"].filter((k) => process.env[k]);
|
|
2692
|
+
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 });
|
|
2693
|
+
if (cmd === "inspect" && head.some((a) => a === "--request" || a.startsWith("--request="))) {
|
|
2694
|
+
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";
|
|
2695
|
+
if (JSON_MODE) jsonFail("E_UNKNOWN_COMMAND", message, { removed: "inspect --request", replacement: "oats onboard / oats sync; oats spawn --preview" });
|
|
2696
|
+
die(message);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
if (cmd === "onboard") {
|
|
3771
2700
|
if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
|
|
3772
2701
|
await onboardCmd();
|
|
3773
2702
|
}
|
|
3774
2703
|
else {
|
|
3775
|
-
// Other commands retain their existing explicit/inherited selection rules.
|
|
3776
|
-
try { captured ??= capturedSelector(args); }
|
|
3777
|
-
catch (error) {
|
|
3778
|
-
if (JSON_MODE) jsonFail(error.code || "E_BAD_ARGS", error.message);
|
|
3779
|
-
die(error.message);
|
|
3780
|
-
}
|
|
3781
|
-
if (captured) {
|
|
3782
|
-
args.splice(0, args.length, ...captured.args); cmd = args[0];
|
|
3783
|
-
// Host protocol negotiation describes this executable, not a mutable
|
|
3784
|
-
// configuration. An inherited capture must not break `oats version` probes.
|
|
3785
|
-
if (captured.explicit || cmd !== "version") { capturedCommand(captured); process.exit(0); }
|
|
3786
|
-
}
|
|
3787
2704
|
// `--help`/`-h` anywhere after a kernel command prints that command's usage
|
|
3788
2705
|
// and exits 0 BEFORE any dispatch: a fresh operator inspects --help before
|
|
3789
2706
|
// using a command, and `install --help` once ran the bare restore while
|
|
3790
2707
|
// `okf harvest --help` spawned a harvester (BeadHub, 2026-09-05).
|
|
3791
2708
|
const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
|
|
3792
2709
|
if (cmd && KERNEL_COMMANDS.has(cmd) && wantsHelp) { if (JSON_MODE) { jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); process.exit(0); } usageFor(cmd); process.exit(0); }
|
|
3793
|
-
if (flag("server") !== undefined && ["spawn", "retire", "status", "session", "okf", "schedule", "inspect", "operation", "
|
|
2710
|
+
if (flag("server") !== undefined && ["spawn", "retire", "status", "session", "okf", "schedule", "inspect", "operation", "launch-config"].includes(cmd)) await serverRouteCmd();
|
|
3794
2711
|
else if (cmd === "server") serverCmd();
|
|
3795
|
-
else if (cmd === "inspect") inspectCmd();
|
|
3796
|
-
else if (cmd === "operation") operationCmd();
|
|
3797
|
-
else if (cmd === "soul") await soulCmd();
|
|
2712
|
+
else if (cmd === "inspect") await inspectCmd();
|
|
2713
|
+
else if (cmd === "operation") await operationCmd();
|
|
3798
2714
|
else if (cmd === "launch-config") await launchConfigCmd();
|
|
3799
2715
|
else if (cmd === "doctor") {
|
|
3800
2716
|
const doctorDir = args[1] && !args[1].startsWith("--") ? args[1] : undefined;
|
|
3801
|
-
args.includes("--json") ? doctorJson(doctorDir) : doctor(doctorDir);
|
|
2717
|
+
await (args.includes("--json") ? doctorJson(doctorDir) : doctor(doctorDir));
|
|
3802
2718
|
}
|
|
3803
2719
|
else if (cmd === "update") {
|
|
3804
2720
|
// `oats update <package>` left with the installed tier (packages are pinned in
|
|
@@ -3807,8 +2723,7 @@ else if (cmd === "update") {
|
|
|
3807
2723
|
if (t || flag("to") !== undefined) { cmdFail("E_BAD_ARGS", "oats update takes no package: pin package versions in oats-workspace.yaml (`oats package add <id> <version>`), then `oats sync`; bare `oats update [--check] [--yes]` updates the kernel"); process.exit(1); }
|
|
3808
2724
|
updateCmd();
|
|
3809
2725
|
}
|
|
3810
|
-
else if (cmd === "
|
|
3811
|
-
else if (cmd === "readiness") readinessCmd();
|
|
2726
|
+
else if (cmd === "readiness") await readinessCmd();
|
|
3812
2727
|
else if (cmd === "instance") instanceCmd();
|
|
3813
2728
|
else if (cmd === "root") console.log(resolve(new URL("..", import.meta.url).pathname));
|
|
3814
2729
|
else if (cmd === "sync") await syncCmd();
|
|
@@ -3824,7 +2739,6 @@ else if (cmd === "session") await sessionCmd();
|
|
|
3824
2739
|
else if (cmd === "schedule") scheduleCmd();
|
|
3825
2740
|
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; } }
|
|
3826
2741
|
else if (cmd === "retire") retireCmd();
|
|
3827
|
-
else if (cmd === "create") createCmd();
|
|
3828
2742
|
else if (cmd === "capture" || cmd === "recall" || cmd === "setup") await recordCmd(cmd);
|
|
3829
2743
|
else if (cmd === "experimental") await experimentalCmd();
|
|
3830
2744
|
// `!HELP_WORDS.has(cmd)`: usage NEVER depends on deployment state. `help` is a
|
|
@@ -3874,7 +2788,6 @@ Usage:
|
|
|
3874
2788
|
oats version [--json] kernel version; --json emits the
|
|
3875
2789
|
Desktop CLI API v1 probe payload
|
|
3876
2790
|
oats status [--json] agents, souls, running instances
|
|
3877
|
-
oats status --team [--json] whole-team roster across the team scope's repos
|
|
3878
2791
|
oats server add <id> --ssh <alias> register another machine's OATS (OpenSSH alias,
|
|
3879
2792
|
--workspace </abs/path> [--oats <p>] remote workspace, remote oats path; no keys stored;
|
|
3880
2793
|
[--path <dir:dir>] --path = dirs prepended to the remote PATH, e.g. ~/.local/bin)
|
|
@@ -3898,24 +2811,19 @@ Usage:
|
|
|
3898
2811
|
oats session start --server <id> start a stopped remote instance in its existing home
|
|
3899
2812
|
--instance <name> | --home <abs> over its saved route; the server must advertise
|
|
3900
2813
|
[--model <m>] [--json] session-start (oats 0.22.9 or later)
|
|
3901
|
-
oats inspect|operation
|
|
2814
|
+
oats inspect|operation --server <id> the same commands on a registered server over its
|
|
3902
2815
|
... [--dir <remote member>] [--home <abs>] saved route (an explicit --dir travels as is; a --home
|
|
3903
2816
|
is its own context; else the registered workspace);
|
|
3904
|
-
|
|
3905
|
-
server must advertise operations (oats 0.22.16 or later)
|
|
2817
|
+
the server must advertise operations (oats 0.22.16 or later)
|
|
3906
2818
|
oats session upload --server <id> copy a local file into a remote instance's private
|
|
3907
2819
|
--instance <name> | --home <abs> attachments over its saved route (bytes stream on
|
|
3908
2820
|
--file <path> [--json] ssh stdin; sha256 verified); the server must
|
|
3909
2821
|
advertise session-upload (oats 0.22.13 or later)
|
|
3910
2822
|
oats onboard [<dir>] --workspace <repo ref> realize a workspace here: writes <dir>/oats-local.yaml
|
|
3911
|
-
[--json] and agents/, then runs the oats sync path (lock v3
|
|
3912
|
-
|
|
2823
|
+
[--json] and agents/, then runs the oats sync path (lock v3)
|
|
2824
|
+
and prints the
|
|
3913
2825
|
next steps (clone members you work IN, spawn
|
|
3914
2826
|
oats-operator-expert); creates no soul, spawns nothing
|
|
3915
|
-
oats create <name> [--local] [--no-oats-core] create an agent soul; --local = full
|
|
3916
|
-
[--description <d>] [--repo <r>] soul under local-agents/ (uncommitted,
|
|
3917
|
-
[--work <mode>] [--runtime pi|claude|codex] gitignored; same memory + lifecycle)
|
|
3918
|
-
[--model <m>] [--yolo|--no-yolo] [--instructions-file <f>]
|
|
3919
2827
|
oats session inspect|input|attach --home <absolute-home> [--text-file <path>] [--json]
|
|
3920
2828
|
oats schedule list|show <id>|add <id> --file <spec.json>|update <id> --file <spec.json>
|
|
3921
2829
|
enable|disable|run|remove|reconcile <id> workspace-scoped, host-owned schedules (spawn,
|
|
@@ -3940,19 +2848,21 @@ Usage:
|
|
|
3940
2848
|
spawn hooks); --model replaces the recorded model
|
|
3941
2849
|
for this and later starts; a live harness is refused
|
|
3942
2850
|
oats spawn <agent> [--task <text>] spawn an instance (tmux/Herdr; --no-launch
|
|
3943
|
-
[--purpose <slug>] [--repo <r>] = scaffold only);
|
|
3944
|
-
[--parent <instance>]
|
|
3945
|
-
[--no-oats-core] omit the default only on NEW local souls;
|
|
2851
|
+
[--purpose <slug>] [--repo <r>] = scaffold only); the agent is a workspace
|
|
2852
|
+
[--parent <instance>] soul or a capability-defined agent
|
|
3946
2853
|
[--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
|
|
3947
2854
|
[--relative-to <instance>] new instance to an existing one; --parent X
|
|
3948
2855
|
[--relative-root <agents-root>] disambiguates same-named team anchors
|
|
3949
2856
|
[--work worktree|checkout|attached|workspace|directory] = sugar for --relative-to X --relation
|
|
3950
2857
|
[--work-dir <owner-work>] [--runtime pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
|
|
3951
|
-
[--
|
|
3952
|
-
with team: declared, unknown
|
|
2858
|
+
[--no-launch] [--json]
|
|
2859
|
+
with team: declared, unknown souls
|
|
3953
2860
|
resolve across the team scope's repos
|
|
3954
2861
|
directory: owned home/work, config context may
|
|
3955
2862
|
be non-Git; rejects --work-dir and --branch
|
|
2863
|
+
[--name <slug>] the exact instance name (no <agent>- prefix;
|
|
2864
|
+
not with --purpose); must be a slug, not a
|
|
2865
|
+
soul name, and unused in the deployment
|
|
3956
2866
|
oats retire <instance> [--force] retire an instance (window, hooks,
|
|
3957
2867
|
[--self] [--delete-branch] worktree, home); --self = retire the
|
|
3958
2868
|
[--keep-dir] [--json] CALLING instance: the window dies, then
|
|
@@ -3962,20 +2872,14 @@ Usage:
|
|
|
3962
2872
|
[--json] installed capabilities with health, effective
|
|
3963
2873
|
layer bindings and activation, declared
|
|
3964
2874
|
operations with availability; --home answers the
|
|
3965
|
-
running home's
|
|
3966
|
-
from the
|
|
3967
|
-
oats operation run <layer>:<name> run an operation the
|
|
2875
|
+
running home's recorded modules and their drift
|
|
2876
|
+
from the deployment
|
|
2877
|
+
oats operation run <layer>:<name> run an operation the soul's core capability for that
|
|
3968
2878
|
(--home <abs> | --soul <name> [--dir <d>]) layer declares (knowledge:harvest, knowledge:
|
|
3969
|
-
[--arg k=v ...] [--json] inspect ...): resolved from the home's
|
|
3970
|
-
|
|
2879
|
+
[--arg k=v ...] [--json] inspect ...): resolved from the home's recorded
|
|
2880
|
+
modules or the deployment's soul, trust checked, the provider's
|
|
3971
2881
|
own command run in the home or scope, envelope
|
|
3972
2882
|
relayed; a view answers {documents: [...]}
|
|
3973
|
-
oats soul set <name> [--dir <scope>] change an editable soul's launch defaults and/or
|
|
3974
|
-
[--agents-root <abs>] [--runtime r] instructions in place (soul.yaml lines replaced,
|
|
3975
|
-
[--model m | --no-model] everything else kept; AGENTS.md replaced from
|
|
3976
|
-
[--yolo | --no-yolo] [--backend b] --instructions-file); packaged souls are refused;
|
|
3977
|
-
[--description d | --no-description] the receipt carries before/after and sha256s
|
|
3978
|
-
[--instructions-file <path>] [--json]
|
|
3979
2883
|
oats launch-config list [--dir <scope> named launch configurations effective at a scope,
|
|
3980
2884
|
| --home <abs> | --soul <name>] a home's recorded context or a soul's own scope:
|
|
3981
2885
|
[--agents-root <abs>] [--json] runtime, executable, args, env (values redacted,
|
|
@@ -3996,22 +2900,22 @@ Usage:
|
|
|
3996
2900
|
oats update [--check] [--yes] check npm for a newer kernel+pi bridge and
|
|
3997
2901
|
optionally run the update; then run oats doctor
|
|
3998
2902
|
oats sync [--dir <d>] [--json] workspace model v2: observe the workspace named by
|
|
3999
|
-
|
|
2903
|
+
oats-local.yaml over its Git remote, confirm every
|
|
4000
2904
|
member (reciprocal oats-membership.yaml), resolve
|
|
4001
|
-
packages: to exact commits
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
package at exactly that locked version without a
|
|
4006
|
-
terminal (the digest is computed over the locked
|
|
4007
|
-
tree; an id@version not in the lock is E_BAD_ARGS)
|
|
2905
|
+
packages: to exact commits + integrity, write
|
|
2906
|
+
oats-lock.json (v3) and report the diff. No approval
|
|
2907
|
+
step: declaring a package in packages: is the trust
|
|
2908
|
+
decision (--approve is E_BAD_ARGS)
|
|
4008
2909
|
oats package add <id> <version|git:<repo>@<ref>> edit packages: in oats-workspace.yaml when the
|
|
4009
2910
|
| remove <id> [--dir <d>] workspace repo is the current checkout; otherwise
|
|
4010
2911
|
print the line to add (the file travels through Git)
|
|
4011
2912
|
oats workspace status [--dir <d>] [--json] membership table (confirmed / no-backlink /
|
|
4012
|
-
cannot-read / backlink-elsewhere), packages
|
|
4013
|
-
oats capabilities [--dir <d>] [--json] every
|
|
4014
|
-
|
|
2913
|
+
cannot-read / backlink-elsewhere), locked packages
|
|
2914
|
+
oats capabilities [--dir <d>] [--json] every capability of every confirmed member (a
|
|
2915
|
+
private one is listed as repo-owned: usable only by
|
|
2916
|
+
its own repo's souls) + the locked packages
|
|
2917
|
+
oats souls [--dir <d>] [--json] every soul of every confirmed member + external souls
|
|
2918
|
+
(souls have no private mode), with origin
|
|
4015
2919
|
(member <key> @ <commit> | package <id> v<ver>) and team
|
|
4016
2920
|
oats instance git <instance> [--home <abs>] [--dir <d>] [--json]
|
|
4017
2921
|
read-only Git observation of the instance's work
|
|
@@ -4020,10 +2924,6 @@ Usage:
|
|
|
4020
2924
|
oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json]
|
|
4021
2925
|
bounded diff of one observed file; refuses when
|
|
4022
2926
|
the tree moved since the observation
|
|
4023
|
-
oats session recompose --home <abs> [--dry-run] [--json]
|
|
4024
|
-
refresh a LIVE home's AGENTS.md from its current
|
|
4025
|
-
canonical soul + context (same composer as spawn);
|
|
4026
|
-
previous text retained; nothing restarted
|
|
4027
2927
|
oats instance events <instance> [--limit <n>] [--since <iso>] [--json]
|
|
4028
2928
|
typed lifecycle events (spawned, launched, stopped,
|
|
4029
2929
|
restarted, retired, worktree-retained…) written by
|
|
@@ -4034,14 +2934,15 @@ Usage:
|
|
|
4034
2934
|
oats instance stop <instance> --apply --plan-revision <rev> --idempotency-key <key>
|
|
4035
2935
|
quiesce (SIGTERM, bounded, never escalated),
|
|
4036
2936
|
children first; home/work/launch retained
|
|
4037
|
-
oats readiness
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
their readiness is the retained resolution's)
|
|
4041
|
-
installed | trusted | configured | enrolled, each
|
|
2937
|
+
oats readiness (--soul <n> | --home <abs>) [--policy] [--json]
|
|
2938
|
+
readinessApi 2 for a soul or an instance home:
|
|
2939
|
+
installed | configured | member | providers, each
|
|
4042
2940
|
pass|fail|unknown|not-applicable with items and
|
|
4043
|
-
remedies;
|
|
4044
|
-
|
|
2941
|
+
remedies; providers relays each bound provider's
|
|
2942
|
+
own check ({status, problems, warnings});
|
|
2943
|
+
--policy: enforced child-spawn / worktree policy
|
|
2944
|
+
with origins (captured homes refuse: the
|
|
2945
|
+
captured/portable path was removed in 0.26)
|
|
4045
2946
|
oats retire <instance> --plan [--json] what Remove would touch, with retention defaults
|
|
4046
2947
|
oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree] [--delete-branch]
|
|
4047
2948
|
with a plan revision: refuses E_PLAN_STALE (fresh plan
|
|
@@ -4050,8 +2951,6 @@ Usage:
|
|
|
4050
2951
|
<workspace>/.agents/worktrees/<repo>/<branch>)
|
|
4051
2952
|
unless discarded; --delete-branch deletes the
|
|
4052
2953
|
worktree's verified branch and implies discard
|
|
4053
|
-
oats type add <name> [--description <d>] declare an agent type (family) in config;
|
|
4054
|
-
oats type list souls join via create --type / soul.yaml
|
|
4055
2954
|
oats root print this package's install root
|
|
4056
2955
|
(adapters resolve the kernel from it)
|
|
4057
2956
|
|
|
@@ -4069,43 +2968,6 @@ The turn record (core — every conversation captured, searchable, replicated):
|
|
|
4069
2968
|
design, repo checkout only; see
|
|
4070
2969
|
packages/experimental/README.md
|
|
4071
2970
|
|
|
4072
|
-
oats inspect --request <absolute-json-file> [--json]
|
|
4073
|
-
[--emit-prepare-request <new-absolute-json-file>]
|
|
4074
|
-
fresh source/workspace/member metadata; optional private
|
|
4075
|
-
request export uses the existing fresh-request builder;
|
|
4076
|
-
no provider execution or preparation authority
|
|
4077
|
-
oats prepare --request <absolute-json-file> [--json]
|
|
4078
|
-
complete public preparation input; no mixed flags,
|
|
4079
|
-
inherited binding, implicit setup or launch authority
|
|
4080
|
-
oats prepare --dir <abs> --source <git repo> --revision <ref> --export <path>
|
|
4081
|
-
--alias <name> [--work <mode>] [--json] prepare retained commands and curriculum,
|
|
4082
|
-
no launch; provider gaps report incomplete
|
|
4083
|
-
oats prepare --dir <abs> --workspace <git repo> --alias <advertised alias>
|
|
4084
|
-
[--workspace-revision <ref>] [--work <mode>] [--json]
|
|
4085
|
-
same preparation through workspace imports
|
|
4086
|
-
oats inspect --deployment <abs> --resolution <id> [--composition] [--json]
|
|
4087
|
-
[--helper <exact-map-key>] inspect retained source/helper inputs, not today's configuration
|
|
4088
|
-
oats trust <capability> --deployment <abs> --artifact-set <sha256-…> [--json]
|
|
4089
|
-
approve one exact prepared artifact; use each
|
|
4090
|
-
selections[].artifactSet for capability ids
|
|
4091
|
-
in prepare's approvalRequired[] at that selection
|
|
4092
|
-
oats trust <capability> --deployment <abs> --resolution <id> [--json]
|
|
4093
|
-
explicitly approve that exact captured artifact
|
|
4094
|
-
oats <namespace> <command> --deployment <abs> --resolution <id> -- [args…]
|
|
4095
|
-
run the approved retained command; no ambient fallback
|
|
4096
|
-
oats operation run <layer>:<name> --deployment <abs> --resolution <id>
|
|
4097
|
-
[--home <abs>] [--arg k=v ...] [--retry-intent <saved-id>] [--json]
|
|
4098
|
-
home actions admit distinct requests; explicit retry reuses intent
|
|
4099
|
-
scope views stay read-only; scope mutation is not yet qualified
|
|
4100
|
-
oats spawn <captured subject> --deployment <abs> --resolution <id>
|
|
4101
|
-
--home <abs> --no-launch [--json] create a fresh directory scaffold and run captured hooks
|
|
4102
|
-
oats session inspect --deployment <abs> --resolution <id> --home <abs> --native-record <UUID> [--helper <exact-key>] --json
|
|
4103
|
-
read-only Pi completion observation; not admission or readiness
|
|
4104
|
-
oats session start|restart --deployment <abs> --resolution <id> --home <abs>
|
|
4105
|
-
[--helper <exact-source-map-key>] [--request <abs-json>] [--retry-intent <saved-id>] [--json]
|
|
4106
|
-
dispatch the owned captured home via existing native custody;
|
|
4107
|
-
version1 request: backend/task/stopGraceMs, no model override
|
|
4108
|
-
|
|
4109
2971
|
oats <namespace> <command> [args…] run an operational command only when its
|
|
4110
2972
|
capability is active (e.g. oats okf harvest)
|
|
4111
2973
|
|