@awebai/oats 0.25.9 → 0.27.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 +648 -1755
- 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 +279 -93
- package/capabilities/oats-aweb/injects/aweb.md +7 -2
- package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
- 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 +8 -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-review/oats.json +3 -2
- package/docs/capabilities.md +229 -58
- package/docs/capability-manifest.schema.json +29 -9
- package/docs/configuration.md +17 -5
- package/docs/conventions.md +18 -28
- 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 +34 -1
- package/docs/design/2026-09-24-phase-d-plan.md +57 -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 +604 -271
- package/docs/desktop-instance-start.md +3 -3
- package/docs/desktop.md +7 -13
- package/docs/execution-targets.md +16 -18
- package/docs/first-team.md +15 -18
- package/docs/implementation.md +31 -62
- package/docs/integrations.md +64 -33
- 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 +30 -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 +76 -53
- 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.26.0.md +670 -0
- package/docs/release-notes/v0.27.0.md +100 -0
- package/docs/schedules.md +54 -132
- package/docs/servers.md +4 -4
- package/docs/soul.schema.json +11 -4
- package/docs/souls-and-instances.md +60 -47
- package/docs/workspaces.md +80 -58
- package/injects/instance-boundary.md +2 -2
- 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 +947 -5023
- package/lib/deprecation.mjs +24 -0
- package/lib/digest.mjs +12 -0
- package/lib/instance-inspect.mjs +397 -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/process-group.mjs +1 -1
- package/lib/provider-binding.mjs +4 -2
- package/lib/provider-reasons.mjs +3 -68
- package/lib/remote.mjs +1 -1
- package/lib/resolve.mjs +204 -68
- package/lib/schedule.mjs +136 -292
- package/lib/servers.mjs +70 -38
- package/lib/{portable-shape.mjs → shape.mjs} +4 -3
- package/lib/tree-copy.mjs +44 -0
- package/lib/workspace.mjs +132 -20
- package/package-catalog.json +6 -6
- package/package.json +1 -1
- package/packages/record/lib/session-roots.mjs +8 -6
- 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,24 @@
|
|
|
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";
|
|
25
|
+
import { runtimeNameWarning, noteRuntimeName } from "../lib/deprecation.mjs";
|
|
26
26
|
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,
|
|
27
|
+
LAYERS, OATS_VERSION, manifestOperations, upgradeHomeMeta,
|
|
28
|
+
capabilityManifests, capabilityTrust, capabilityExecutablePath,
|
|
29
|
+
officialPackageCatalog, officialCatalogFile, officialCapabilityAliases, resolvedFromHome, resolvedFromPrepared, teamEnv, isWorkspaceHome, preWorkspaceHome, isCapturedHome, capturedHomeRefusal, composeInstanceAgentsMd, parseYamlNested, withConfigFile,
|
|
30
|
+
findInstanceHome, findInstanceHomes, workspaceOf, stopInstanceSession, ensureRoot, findRoot, findAgent, findAgentAt, legacyLocalAgents, legacyCapturedHomes, listAgents, listInstances, servedIdentityLine, spawnInstanceAsync, instanceSoulDir, launchConfigsAt, explicitInstanceName, findModuleCapabilityAgent, capabilityAgentFromDir, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, defaultRepo, RELATIONS, validateLaunchConfig, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_HARNESSES, planLaunch, redactLaunchCommand, restartInstanceSession,
|
|
36
31
|
} from "../lib/core.mjs";
|
|
37
32
|
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
import {
|
|
33
|
+
writeFileAtomic, LOCK_FILE, readLock, writeLock, resolvePackages,
|
|
34
|
+
classifyPackageValue, parsePackageRequest } from "../lib/packages.mjs";
|
|
35
|
+
import { loadLocal, validateWorkspace, validateLocal } from "../lib/workspace.mjs";
|
|
36
|
+
import { parseConfigData } from "../lib/config-data.mjs";
|
|
42
37
|
import * as remoteModule from "../lib/remote.mjs";
|
|
43
|
-
import { createInterface } from "node:readline/promises";
|
|
44
38
|
import YAML from "yaml";
|
|
45
39
|
import { attachArgv, checkRemote, forgetSnapshot, getServer, inspectRemote, startRemote, restartRemote, launchConfigRemote, scheduleRemote, listSnapshots, readServers, rosterGroups, routeCommand, targetOf, validateServer, writeServers, SERVERS_FILE } from "../lib/servers.mjs";
|
|
46
40
|
import { spawnSync as spawnSyncProc } from "node:child_process";
|
|
@@ -48,25 +42,16 @@ import { parseEnvelopeText, scheduleScopeOf, listSchedules, describe as describe
|
|
|
48
42
|
import { hostUnitStatus, installHostUnit, uninstallHostUnit } from "../lib/schedule-host.mjs";
|
|
49
43
|
import { receiveAttachment, uploadAttachment, readStreamBounded, MAX_ATTACHMENT_BYTES } from "../lib/attachments.mjs";
|
|
50
44
|
|
|
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
45
|
import { observeInstanceGit, diffInstanceFile } from "../lib/instance-git.mjs";
|
|
61
46
|
import { planStop, applyStop, planRetire, resolveInstance as resolveInstanceForCli } from "../lib/instance-lifecycle.mjs";
|
|
62
47
|
const await_import_lifecycle = () => ({ resolveInstance: resolveInstanceForCli });
|
|
63
|
-
import {
|
|
48
|
+
import { homeTarget, soulTarget, isWorkspaceContext, inspectDocument, readinessDocument, policyOf, policySoul, manifestMissingRequires, INSPECT_OPERATIONS_API } from "../lib/instance-inspect.mjs";
|
|
64
49
|
import { readEvents } from "../lib/instance-events.mjs";
|
|
65
50
|
|
|
66
51
|
const args = process.argv.slice(2);
|
|
67
52
|
let cmd = args[0];
|
|
68
53
|
const HELP_WORDS = new Set(["help", "--help", "-h"]);
|
|
69
|
-
const KERNEL_COMMANDS = new Set(["
|
|
54
|
+
const KERNEL_COMMANDS = new Set(["capture", "capabilities", "doctor", "inspect", "instance", "operation", "package", "readiness", "souls", "launch-config", "experimental", "onboard", "pane", "recall", "retire", "root", "schedule", "server", "session", "setup", "spawn", "status", "sync", "update", "version", "workspace"]);
|
|
70
55
|
const flag = (name) => {
|
|
71
56
|
const i = args.indexOf(`--${name}`);
|
|
72
57
|
return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true) : undefined;
|
|
@@ -81,7 +66,17 @@ function valueFlag(name) {
|
|
|
81
66
|
return value;
|
|
82
67
|
}
|
|
83
68
|
const die = (msg) => { console.error(`oats: ${msg}`); process.exit(1); };
|
|
84
|
-
|
|
69
|
+
/** A command's harness: --harness, or --runtime, its pre-0.27 name (the released okf worker and
|
|
70
|
+
* a 0.26-era Desktop pass it) — read either, with the deprecation warning. Both, disagreeing,
|
|
71
|
+
* are refused. `get` reads one flag (the command's own reader where it has one). */
|
|
72
|
+
function harnessFlag(get = flag) {
|
|
73
|
+
const harness = get("harness"), runtime = get("runtime");
|
|
74
|
+
if (runtime === undefined) return harness;
|
|
75
|
+
if (harness !== undefined && harness !== runtime) cmdFail("E_BAD_ARGS", `--harness ${harness} and --runtime ${runtime} disagree; --runtime is the pre-0.27 name of --harness — give one`);
|
|
76
|
+
noteRuntimeName("the --runtime flag (use --harness)");
|
|
77
|
+
return runtime;
|
|
78
|
+
}
|
|
79
|
+
const cmdFail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
85
80
|
/** Resolve the --dir flag with central validation: a value-taking flag given
|
|
86
81
|
* no value (flag() → true) is E_BAD_ARGS inside the JSON boundary, never an
|
|
87
82
|
* uncaught resolve(true) TypeError (reviewer-6f0a3bd). */
|
|
@@ -102,384 +97,33 @@ const JSON_MODE = args.includes("--json");
|
|
|
102
97
|
// Canonical absolute path of this CLI executable — the versioned OATS_CLI_BIN
|
|
103
98
|
// env contract for dispatched package commands (never resolved via PATH).
|
|
104
99
|
const CLI_BIN = realpathSync(fileURLToPath(import.meta.url));
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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";
|
|
100
|
+
// The one deprecated-name warning (lib/deprecation.mjs) rides the envelope, only when there is one.
|
|
101
|
+
const envelopeWarnings = () => { const w = runtimeNameWarning(); if (w) warningDelivered = true; return w ? { warnings: [w] } : {}; };
|
|
102
|
+
let warningDelivered = false;
|
|
103
|
+
/** A forwarded envelope keeps the host's warnings; this command's own deprecated-name
|
|
104
|
+
* note (a `--runtime` given here) joins the host's into the one warning. */
|
|
105
|
+
const withLocalWarnings = (envelope) => {
|
|
106
|
+
const mine = runtimeNameWarning();
|
|
107
|
+
if (!mine || !envelope || typeof envelope !== "object") return envelope;
|
|
108
|
+
warningDelivered = true;
|
|
109
|
+
const theirs = Array.isArray(envelope.warnings) ? envelope.warnings : [];
|
|
110
|
+
const same = theirs.find((w) => w?.code === mine.code);
|
|
111
|
+
if (!same) return { ...envelope, warnings: [...theirs, mine] };
|
|
112
|
+
const sources = [...new Set([...(Array.isArray(same.sources) ? same.sources : []), ...mine.sources])];
|
|
113
|
+
return { ...envelope, warnings: theirs.map((w) => (w === same ? { ...mine, sources, message: mine.message.replace(/\(.*\)/, `(${sources.join("; ")})`) } : w)) };
|
|
114
|
+
};
|
|
115
|
+
const jsonFail = (code, message, details) => { console.log(JSON.stringify({ schemaVersion: 1, ok: false, error: { code, message: String(message), ...(details !== undefined ? { details } : {}) }, ...envelopeWarnings() })); process.exit(1); };
|
|
116
|
+
const jsonOk = (result) => { console.log(JSON.stringify({ schemaVersion: 1, ok: true, result, ...envelopeWarnings() })); };
|
|
117
|
+
// Text mode (or a JSON answer printed before the read): the warning goes to stderr, never stdout.
|
|
118
|
+
process.on("exit", () => { const w = runtimeNameWarning(); if (w && !warningDelivered) process.stderr.write(`oats: warning: ${w.message}\n`); });
|
|
119
|
+
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`;
|
|
120
|
+
/** A retire recovery's copied outputs (untracked/ignored or directory work), named with their size. */
|
|
121
|
+
function preservedOutputLines(recovery) {
|
|
122
|
+
const outputs = recovery?.outputs;
|
|
123
|
+
if (!outputs?.paths?.length) return [];
|
|
124
|
+
const shown = outputs.paths.slice(0, 8).map((p) => `${p.path} (${formatBytes(p.bytes)})`);
|
|
125
|
+
const more = outputs.paths.length > 8 ? `, and ${outputs.paths.length - 8} more` : "";
|
|
126
|
+
return [` copied outputs: ${shown.join(", ")}${more} — ${formatBytes(outputs.bytes)} in total`];
|
|
483
127
|
}
|
|
484
128
|
|
|
485
129
|
function shortPath(p) {
|
|
@@ -493,56 +137,63 @@ function shellQuote(s) {
|
|
|
493
137
|
return /^[A-Za-z0-9._/~-]+$/.test(s) ? s : `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
494
138
|
}
|
|
495
139
|
|
|
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
140
|
|
|
508
141
|
// ---------- doctor ----------
|
|
509
142
|
/** Doctor must diagnose, not crash: a stale activation of a retired
|
|
510
143
|
* capability fails config resolution — surface the cleanup instruction
|
|
511
144
|
* 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
145
|
function operationalKnowledgeNote(composition, soulName) {
|
|
532
146
|
return composition && !composition.oatsCoreDeclared
|
|
533
|
-
? `soul ${soulName} has no oats.core capability;
|
|
534
|
-
}
|
|
535
|
-
|
|
147
|
+
? `soul ${soulName} has no oats.core capability (the workspace default); it gets no OATS operating instructions` : null;
|
|
148
|
+
}
|
|
149
|
+
/** `doctor --soul`: the instructions an instance of that soul would carry. The soul
|
|
150
|
+
* is resolved over the workspace remotes exactly as a spawn preview resolves it,
|
|
151
|
+
* the kernel half composed, and the modules materialized into a scratch home
|
|
152
|
+
* OUTSIDE the deployment (removed after), so module injects are part of the text.
|
|
153
|
+
* Nothing in the deployment is written. Block files of module injects are named
|
|
154
|
+
* home-relative (`.oats/modules/<cap>/<inject>`), where an instance carries them. */
|
|
155
|
+
async function doctorComposition(ctx, soulName, ws, bail) {
|
|
536
156
|
if (!soulName) return undefined;
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
157
|
+
const { prepareInstance, previewWorkspaceSoul, materializePrepared, discoverOrStandalone } = await import("../lib/instance-resolution.mjs");
|
|
158
|
+
const deployment = dirname(ws.local.path);
|
|
159
|
+
const root = join(deployment, "agents");
|
|
160
|
+
const remoteOptions = remoteOptionsFromEnv();
|
|
161
|
+
const cleanups = [];
|
|
162
|
+
// A bail exits the process without unwinding: the temporary copies are removed at exit too.
|
|
163
|
+
process.once("exit", () => { for (const c of cleanups) { try { c(); } catch { /* best effort */ } } });
|
|
164
|
+
try {
|
|
165
|
+
const discovery = await discoverOrStandalone(loadLocal(deployment).local, { remoteOptions });
|
|
166
|
+
const prepared = await prepareInstance(deployment, soulName, { remoteOptions, discovery });
|
|
167
|
+
const pv = await previewWorkspaceSoul(prepared, root);
|
|
168
|
+
cleanups.push(pv.cleanup);
|
|
169
|
+
const agent = findAgentAt(root, prepared.soulEntry.name, pv.soulDir);
|
|
170
|
+
if (!agent) bail("E_SOUL_UNKNOWN", `soul "${soulName}" was fetched but is not readable as a soul`);
|
|
171
|
+
const composition = composeInstanceAgentsMd(pv.soulDir, deployment, agent.name, agent.work || "checkout", agent.kind, prepared);
|
|
172
|
+
const scratch = realpathSync(mkdtempSync(join(tmpdir(), "oats-doctor-home-")));
|
|
173
|
+
cleanups.push(() => rmSync(scratch, { recursive: true, force: true }));
|
|
174
|
+
const outcome = await materializePrepared({ ...prepared, soulAgentsMd: composition.text, soulDir: pv.soulDir }, scratch);
|
|
175
|
+
const text = readFileSync(join(scratch, "AGENTS.md"), "utf8");
|
|
176
|
+
const known = new Set(composition.blocks.map((b) => b.source));
|
|
177
|
+
const outcomeBlocks = Array.isArray(outcome?.blocks) ? outcome.blocks : [];
|
|
178
|
+
for (const m of text.matchAll(/^<!-- oats:(capability:[^\s]+) src=(.+?) -->$/gm)) {
|
|
179
|
+
const [, source, file] = m;
|
|
180
|
+
if (known.has(source)) continue;
|
|
181
|
+
known.add(source);
|
|
182
|
+
const content = outcomeBlocks.find((b) => b.source === source && b.file === file)?.content ?? (existsSync(file) ? readFileSync(file, "utf8").trim() : "");
|
|
183
|
+
const rel = file.startsWith(scratch + sep) ? file.slice(scratch.length + 1) : file;
|
|
184
|
+
composition.blocks.push({ source, file: rel, content, materialized: true });
|
|
185
|
+
}
|
|
186
|
+
return { ...composition, text };
|
|
187
|
+
} catch (e) {
|
|
188
|
+
if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details);
|
|
189
|
+
throw e;
|
|
190
|
+
} finally { for (const c of cleanups) { try { c(); } catch { /* best effort: temporary copies only */ } } }
|
|
541
191
|
}
|
|
542
192
|
|
|
543
193
|
/** Workspace-model v2 doctor data, OFFLINE: the deployment declaration found
|
|
544
194
|
* walking up from ctx (oats-local.yaml) and the lock v3 beside it. Doctor never
|
|
545
|
-
* goes to the network
|
|
195
|
+
* goes to the network for this view (only `--soul`, which resolves the soul like a
|
|
196
|
+
* spawn preview); membership and discovery are `oats sync` / `oats workspace status`. */
|
|
546
197
|
function doctorLockData(ctx) {
|
|
547
198
|
const out = { local: null, localError: null, lockFile: null, packages: [], lockError: null };
|
|
548
199
|
let lockDir = ctx;
|
|
@@ -551,7 +202,9 @@ function doctorLockData(ctx) {
|
|
|
551
202
|
out.local = { path: found.path, workspace: found.local.workspace };
|
|
552
203
|
lockDir = dirname(found.path);
|
|
553
204
|
} catch (e) {
|
|
554
|
-
|
|
205
|
+
// An unreadable oats-local.yaml, or a 0.25 oats-config.yaml inside the deployment
|
|
206
|
+
// (E_CONFIG_BROKEN reason legacy-config): doctor answers it as its typed error.
|
|
207
|
+
if (e?.code === "E_WORKSPACE_SCHEMA" || e?.code === "E_CONFIG_BROKEN") out.localError = { code: e.code, message: e.message, details: e.details };
|
|
555
208
|
else if (e?.code !== "E_LOCAL_MISSING") throw e;
|
|
556
209
|
}
|
|
557
210
|
const file = join(lockDir, LOCK_FILE);
|
|
@@ -559,7 +212,7 @@ function doctorLockData(ctx) {
|
|
|
559
212
|
out.lockFile = file;
|
|
560
213
|
try {
|
|
561
214
|
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
|
|
215
|
+
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
216
|
} catch (e) {
|
|
564
217
|
if (e?.code !== "E_LOCK_SCHEMA") throw e;
|
|
565
218
|
out.lockError = { code: e.code, message: e.message, file: e.details?.file ?? file };
|
|
@@ -567,45 +220,6 @@ function doctorLockData(ctx) {
|
|
|
567
220
|
return out;
|
|
568
221
|
}
|
|
569
222
|
|
|
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
223
|
// ---------- inspect: one authoritative answer for GUIs ----------
|
|
610
224
|
/** Souls, capabilities (installed state and health, separately from
|
|
611
225
|
* activation), effective layer bindings and declared operations for a
|
|
@@ -614,35 +228,19 @@ function capabilityHealth(level, cap, capRow, pkgRow) {
|
|
|
614
228
|
* from its roster poll). Nothing here is provider-specific: what a
|
|
615
229
|
* knowledge provider offers is what its manifest declares. */
|
|
616
230
|
const INSPECT_TEXT_CAP = 256 * 1024;
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
}
|
|
636
|
-
const SOUL_FIELDS = ["runtime", "model", "yolo", "backend", "description", "launch-config"];
|
|
231
|
+
/** The agents root a home belongs to, from its path alone:
|
|
232
|
+
* <root>/<agent>/instances/<instance>. */
|
|
233
|
+
function agentsRootOfHome(home) { return dirname(dirname(dirname(home))); }
|
|
234
|
+
const SOUL_FIELDS = ["harness", "model", "yolo", "backend", "description", "launch-config"];
|
|
637
235
|
const realOrResolved = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
|
|
638
|
-
/** Every soul of a scope: persistent
|
|
236
|
+
/** Every soul of a scope: the persistent souls of every agents root in
|
|
639
237
|
* scope, plus packaged souls (read-only). One enumeration for inspect and
|
|
640
238
|
* operation run, so both address souls the same way. */
|
|
641
|
-
function scopeSouls(ctx,
|
|
239
|
+
function scopeSouls(ctx, { extraRoots = [] } = {}) {
|
|
642
240
|
// A home's own agents root is always in scope for that home: its recorded
|
|
643
241
|
// work repository may be another repository entirely (repo overrides), and
|
|
644
242
|
// the config context resolves there while the soul lives with its owner.
|
|
645
|
-
const roots = [...new Set([
|
|
243
|
+
const roots = [...new Set([findRoot(ctx), ...extraRoots].filter(Boolean).map((p) => realOrResolved(resolve(p))))];
|
|
646
244
|
const souls = [];
|
|
647
245
|
for (const root of roots) {
|
|
648
246
|
for (const a of listInstances(root)) {
|
|
@@ -652,17 +250,7 @@ function scopeSouls(ctx, r, { extraRoots = [] } = {}) {
|
|
|
652
250
|
souls.push(e);
|
|
653
251
|
}
|
|
654
252
|
}
|
|
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 };
|
|
253
|
+
return { roots, souls, diagnostics: [] };
|
|
666
254
|
}
|
|
667
255
|
/** The one soul a name (and optional agents root) addresses; throws with a
|
|
668
256
|
* code when none or several match. */
|
|
@@ -733,7 +321,7 @@ function soulEntry(soul, root, { capability } = {}) {
|
|
|
733
321
|
declarationProblems: declared.problems,
|
|
734
322
|
name: soul.name, kind: packaged ? "capability" : (soul.kind || "persistent"), capability: capability || null,
|
|
735
323
|
type: soul.type ?? null, description: soul.description ?? null, repo: soul.repo ?? null, work: soul.work || "checkout",
|
|
736
|
-
|
|
324
|
+
harness: soul.harness || "pi", model: soul.model ?? null, yolo: soul.yolo === true || soul.yolo === "true" ? true : soul.yolo === false || soul.yolo === "false" ? false : null, launchConfig: soul["launch-config"] ?? null, backend: soul.backend ?? null,
|
|
737
325
|
agentsRoot: root, dir: packaged ? soulDir : dir, soulFile: join(soulDir, "soul.yaml"), instructionsFile: join(soulDir, "AGENTS.md"),
|
|
738
326
|
editable: packaged
|
|
739
327
|
? { fields: [], instructions: false, reason: `packaged soul from capability ${capability}: edit the package and update it; scoped bindings still apply through oats use` }
|
|
@@ -745,215 +333,71 @@ function soulEntry(soul, root, { capability } = {}) {
|
|
|
745
333
|
* invoking process's ambient agents-root override must not redirect them
|
|
746
334
|
* to its own deployment. */
|
|
747
335
|
function dropAmbientRoot() { delete process.env.PI_AGENTS_ROOT; }
|
|
748
|
-
function inspectCmd() {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
336
|
+
async function inspectCmd() {
|
|
337
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
338
|
+
const t = await workspaceTarget(bail, { command: "inspect" });
|
|
339
|
+
if (t) {
|
|
340
|
+
if (t.resolutionError) return bail(t.resolutionError.code, t.resolutionError.message, t.resolutionError.details ?? undefined);
|
|
341
|
+
const doc = inspectDocument(t, { kernel: OATS_VERSION });
|
|
342
|
+
if (JSON_MODE) { jsonOk(doc); return; }
|
|
343
|
+
printWorkspaceInspect(doc); return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** The workspace-model target of inspect / readiness / operation run (lead
|
|
347
|
+
* decision 4): an instance home with materialized modules (`--home`), or a soul
|
|
348
|
+
* of a workspace deployment (`--soul`, resolved as its spawn would be). Anything
|
|
349
|
+
* else is a typed refusal: there is no classic scope answer. */
|
|
350
|
+
async function workspaceTarget(bail, { command, liveTeams = true }) {
|
|
754
351
|
dropAmbientRoot();
|
|
755
|
-
const homeFlag = flag("home");
|
|
756
|
-
|
|
352
|
+
const homeFlag = flag("home"), soulFlag = flag("soul"), rootFlag = flag("agents-root");
|
|
353
|
+
if (homeFlag === true) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
354
|
+
if (soulFlag === true) return bail("E_BAD_ARGS", "--soul needs a soul name");
|
|
355
|
+
if (rootFlag === true) return bail("E_BAD_ARGS", "--agents-root needs an absolute agents directory");
|
|
356
|
+
const remoteOptions = remoteOptionsFromEnv();
|
|
357
|
+
if (homeFlag) {
|
|
358
|
+
if (!isAbsolute(homeFlag)) return bail("E_BAD_ARGS", "--home needs an absolute instance home");
|
|
359
|
+
let meta = null;
|
|
360
|
+
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})`); }
|
|
361
|
+
if (isCapturedHome(meta)) { const e = capturedHomeRefusal(homeFlag, "nothing was read"); return bail(e.code, e.message, e.details); }
|
|
362
|
+
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`);
|
|
363
|
+
if (soulFlag && soulFlag !== meta.agent) return bail("E_HOME_MISMATCH", `--soul ${soulFlag} is not the soul of ${homeFlag} (${meta.agent})`);
|
|
364
|
+
const deployment = dirname(dirname(dirname(dirname(realOrResolved(homeFlag)))));
|
|
365
|
+
// A v2 home lives at <deployment>/agents/<soul>/instances/<name>: its deployment is
|
|
366
|
+
// derived, so it must hold oats-local.yaml EXACTLY there (never found by walking up).
|
|
367
|
+
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") });
|
|
368
|
+
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`); }
|
|
369
|
+
if (rootFlag && realOrResolved(rootFlag) !== realOrResolved(join(deployment, "agents"))) return bail("E_HOME_MISMATCH", `--agents-root ${rootFlag} is not the agents root of ${homeFlag}`);
|
|
370
|
+
return homeTarget(homeFlag, meta, { remoteOptions, discover: command === "readiness", live: liveTeams });
|
|
371
|
+
}
|
|
372
|
+
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`); }
|
|
373
|
+
catch (e) { return bail(e?.code || "E_WORKSPACE_SCHEMA", e?.message || String(e), e?.details); }
|
|
374
|
+
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)" : ""}`);
|
|
375
|
+
const deployment = dirname(loadLocal(dirFlag()).path);
|
|
376
|
+
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")})`);
|
|
377
|
+
try { return await soulTarget(dirFlag(), String(soulFlag), { remoteOptions }); }
|
|
378
|
+
catch (e) { if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details); throw e; }
|
|
379
|
+
}
|
|
380
|
+
function printWorkspaceInspect(doc) {
|
|
381
|
+
const s = doc.subject;
|
|
382
|
+
console.log(`oats inspect — ${s.kind === "instance" ? `instance ${s.instance} (soul ${s.soul}) ${shortPath(s.home)}` : `soul ${s.soul} from ${s.repoKey}`}`);
|
|
383
|
+
if (doc.identity) console.log(` identity: ${servedIdentityLine(doc.identity)}`);
|
|
384
|
+
for (const l of LAYERS) console.log(` ${l} capability: ${doc.layers[l].id || "none"}`);
|
|
385
|
+
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(", ")}` : ""}`);
|
|
386
|
+
for (const p of doc.problems) console.log(` ! ${p.code}: ${p.message}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** `{ teams, teamsSource }` for a session start of a workspace home: its eligible teams read
|
|
390
|
+
* live (two repository reads), which the launch hook re-checks joined memberships against
|
|
391
|
+
* (teams contract decision 6) — or the spawn record, marked `recorded`, when the read cannot
|
|
392
|
+
* answer. Anything that is not a readable workspace home gets nothing here — the start
|
|
393
|
+
* itself refuses it. */
|
|
394
|
+
async function homeLiveTeams(home) {
|
|
757
395
|
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}`);
|
|
396
|
+
try { meta = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); } catch { return {}; }
|
|
397
|
+
if (!isWorkspaceHome(meta)) return {};
|
|
398
|
+
const { liveTeams } = await import("../lib/instance-resolution.mjs");
|
|
399
|
+
const { teams, source } = await liveTeams(home, meta, { remoteOptions: remoteOptionsFromEnv() });
|
|
400
|
+
return Array.isArray(teams) ? { teams, teamsSource: source } : {};
|
|
957
401
|
}
|
|
958
402
|
|
|
959
403
|
// ---------- operation run: generic invoke through the capability engine ----------
|
|
@@ -969,11 +413,11 @@ const OPERATION_ADDRESS_RE = /^(knowledge|messaging|tasks):([a-z][a-z0-9-]*)$/;
|
|
|
969
413
|
const reportsRetainedEffectsText = (message) => /INCOMPLETE|quarantin|retain|could not (?:be )?(?:verif|confirm)/i.test(String(message || ""));
|
|
970
414
|
// Comfortably below the scheduler's 5-minute command bound and any GUI
|
|
971
415
|
// 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 }) {
|
|
416
|
+
const OPERATION_TIMEOUT_MS = 4 * 60 * 1000;
|
|
417
|
+
function finishOperation({ r, bail, address, provider, op, argFlags, cwd, home, meta, cleanupError, intent, settlement, api }) {
|
|
974
418
|
const stderr = String(r.stderr || "").trim();
|
|
975
419
|
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 };
|
|
420
|
+
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
421
|
// Unconfirmed outcomes (a timeout, no valid receipt, a receipt contradicted
|
|
978
422
|
// by the exit status) carry what WAS observed in error.details, so a
|
|
979
423
|
// scheduler can keep the slot as unknown and reconcile by any name the
|
|
@@ -1018,58 +462,8 @@ function finishOperation({ r, bail, address, provider, op, argFlags, cwd, home,
|
|
|
1018
462
|
else console.log(JSON.stringify(result, null, 2));
|
|
1019
463
|
if (stderr) console.error(stderr);
|
|
1020
464
|
}
|
|
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.
|
|
465
|
+
/** --arg k=v pairs of `oats operation run`. */
|
|
466
|
+
function operationArgs(bail) {
|
|
1073
467
|
const given = Object.create(null);
|
|
1074
468
|
for (let i = 3; i < args.length; i++) {
|
|
1075
469
|
if (args[i] !== "--arg") continue;
|
|
@@ -1079,210 +473,92 @@ function operationCmd() {
|
|
|
1079
473
|
given[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
1080
474
|
i++;
|
|
1081
475
|
}
|
|
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}`);
|
|
476
|
+
return given;
|
|
477
|
+
}
|
|
478
|
+
/** `oats operation run` on the workspace model (operationsApi 2): the provider is
|
|
479
|
+
* the module filling <layer> — the home's own copy, or the soul's resolved module
|
|
480
|
+
* fetched into the deployment's module store — with its merged payload as
|
|
481
|
+
* OATS_SETTINGS and the team facts hooks get. */
|
|
482
|
+
async function workspaceOperation(t, { bail, address, layer, opName }) {
|
|
483
|
+
if (t.resolutionError) return bail(t.resolutionError.code, t.resolutionError.message, t.resolutionError.details ?? undefined);
|
|
484
|
+
const name = t.slots[layer];
|
|
485
|
+
const mod = name ? t.modules.find((x) => x.name === name) : null;
|
|
486
|
+
if (!mod?.manifest) return bail("E_OPERATION_UNAVAILABLE", `no ${layer} provider is resolved for ${t.home ? t.home : `soul ${t.soul.name}`}`);
|
|
487
|
+
const provider = { ...mod.manifest, capability: mod.name };
|
|
1102
488
|
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>`);
|
|
489
|
+
if (!op) return bail("E_OPERATION_UNKNOWN", `${mod.name} declares no operation ${JSON.stringify(opName)} (declared: ${manifestOperations(provider).map((o) => o.name).join(", ") || "none"})`);
|
|
490
|
+
const missingReq = manifestMissingRequires(provider);
|
|
491
|
+
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`);
|
|
492
|
+
if (op.context === "home" && !t.home) return bail("E_OPERATION_UNAVAILABLE", `${address} runs in an instance home; pass --home <abs>`);
|
|
493
|
+
const given = operationArgs(bail);
|
|
1109
494
|
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"}`);
|
|
495
|
+
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"})`);
|
|
496
|
+
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
497
|
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", `${
|
|
498
|
+
const spec = provider.commands?.[op.command];
|
|
499
|
+
if (typeof spec !== "string" || !spec.trim()) return bail("E_CAPABILITY_BROKEN", `${mod.name}: command ${op.command} is not a non-empty string`);
|
|
500
|
+
let catalog = null; try { catalog = officialPackageCatalog(); } catch { catalog = null; }
|
|
501
|
+
const { layerProvider } = await import("../lib/instance-inspect.mjs");
|
|
502
|
+
let lp;
|
|
503
|
+
try { lp = await layerProvider(t, layer, { catalog, remoteOptions: remoteOptionsFromEnv() }); } catch (e) { return bail(e.code || "E_CAPABILITY_BROKEN", e.message, e.details); }
|
|
1115
504
|
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 });
|
|
505
|
+
const abs = lp?.executable(script);
|
|
506
|
+
if (!abs) return bail("E_CAPABILITY_BROKEN", `${mod.name} ${op.command}: script not found (${script})`);
|
|
507
|
+
const settings = lp.settings;
|
|
508
|
+
const cwd = op.context === "home" ? t.home : t.deployment;
|
|
509
|
+
const env = { ...lp.env(mod.name, settings), OATS_OPERATION: address, OATS_CONTEXT: t.deployment, OATS_ROOT: t.agentsRoot, PI_AGENTS_ROOT: t.agentsRoot };
|
|
510
|
+
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 });
|
|
511
|
+
else for (const k of ["OATS_INSTANCE", "OATS_INSTANCE_HOME", "OATS_HOME", "PI_AGENT_INSTANCE", "PI_AGENT_HOME"]) delete env[k];
|
|
1136
512
|
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 });
|
|
513
|
+
finishOperation({ r, bail, address, provider, op, argFlags, cwd, home: t.home, meta: t.meta, api: INSPECT_OPERATIONS_API });
|
|
1138
514
|
}
|
|
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));
|
|
515
|
+
async function operationCmd() {
|
|
516
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1146
517
|
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.");
|
|
518
|
+
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]");
|
|
519
|
+
const address = args[2];
|
|
520
|
+
const m0 = typeof address === "string" ? OPERATION_ADDRESS_RE.exec(address) : null;
|
|
521
|
+
if (!m0) bail("E_BAD_ARGS", `operation address must be <layer>:<name> with layer one of ${LAYERS.join(", ")} (got ${JSON.stringify(address)})`);
|
|
522
|
+
const [, layer, opName] = m0;
|
|
523
|
+
// Only the messaging provider's operations act on the teams: others get the record, no remote read.
|
|
524
|
+
const target = await workspaceTarget(bail, { command: "operation run", liveTeams: layer === "messaging" });
|
|
525
|
+
return workspaceOperation(target, { bail, address, layer, opName });
|
|
1225
526
|
}
|
|
1226
527
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
528
|
+
|
|
529
|
+
/** Doctor answers on a workspace deployment only (lead decision c3-6): the
|
|
530
|
+
* deployment found walking up from the given directory (positional or --dir),
|
|
531
|
+
* else E_LOCAL_MISSING; an unreadable oats-local.yaml is its own error. */
|
|
532
|
+
function doctorDeployment(dir) {
|
|
533
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
534
|
+
const ctx = resolve(dir || dirFlag());
|
|
1230
535
|
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));
|
|
536
|
+
if (ws.localError) bail(ws.localError.code, ws.localError.message, ws.localError.details);
|
|
537
|
+
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 });
|
|
538
|
+
return { ctx, ws };
|
|
539
|
+
}
|
|
540
|
+
async function doctorJson(dir) {
|
|
541
|
+
const { ctx, ws } = doctorDeployment(dir);
|
|
542
|
+
console.log(JSON.stringify(await doctorWorkspaceJson(ctx, flag("soul"), ws), null, 2));
|
|
1274
543
|
}
|
|
1275
544
|
|
|
1276
545
|
/** The v2 doctor payload: the deployment declaration + lock (offline) and, with
|
|
1277
546
|
* --soul, the composed instructions. No v1 keys (chain/layers/acquired/injects…). */
|
|
1278
|
-
|
|
1279
|
-
|
|
547
|
+
/** The problems doctor and status share for a v2 deployment: what OATS 0.25 left
|
|
548
|
+
* under local-agents/ (named, never read), and the captured homes under the agents root. */
|
|
549
|
+
function legacyLayoutProblems(root) {
|
|
550
|
+
return [legacyLocalAgents(root), legacyCapturedHomes(root)].filter(Boolean);
|
|
551
|
+
}
|
|
552
|
+
async function doctorWorkspaceJson(ctx, soulName, ws) {
|
|
553
|
+
const composition = await doctorComposition(ctx, soulName, ws, (code, msg, details) => jsonFail(code, msg, details));
|
|
554
|
+
const problems = legacyLayoutProblems(join(dirname(ws.local.path), "agents"));
|
|
1280
555
|
return {
|
|
1281
556
|
schemaVersion: 1, workspaceApi: 2, context: ctx,
|
|
1282
557
|
workspace: { file: ws.local.path, ref: ws.local.workspace },
|
|
1283
558
|
workspaceError: ws.localError, lockFile: ws.lockFile, packages: ws.packages, lockError: ws.lockError,
|
|
1284
559
|
information: operationalKnowledgeNote(composition, soulName) ? [operationalKnowledgeNote(composition, soulName)] : [],
|
|
1285
560
|
composedInstructions: composition?.text, instructionBlocks: composition?.blocks,
|
|
561
|
+
...(problems.length ? { problems } : {}),
|
|
1286
562
|
};
|
|
1287
563
|
}
|
|
1288
564
|
/** Kernel/bridge version skew (published in lockstep from one tag). */
|
|
@@ -1295,142 +571,26 @@ function doctorVersionSkew() {
|
|
|
1295
571
|
/** The "Workspace (v2, offline view)" + "Locked packages" sections, shared by both doctor shapes. */
|
|
1296
572
|
function printDoctorWorkspace(ws) {
|
|
1297
573
|
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)");
|
|
574
|
+
console.log(` oats-local.yaml ${shortPath(ws.local.path)} → workspace ${ws.local.workspace}`);
|
|
1301
575
|
console.log("\nLocked packages (oats-lock.json v3):");
|
|
1302
576
|
if (ws.lockError) {
|
|
1303
577
|
console.log(` ERROR: ${ws.lockError.message} [${ws.lockError.code}]`);
|
|
1304
578
|
if (ws.lockError.file) console.log(` the lock is never auto-repaired; delete ${shortPath(ws.lockError.file)} and run \`oats sync\``);
|
|
1305
579
|
} else if (!ws.packages.length) console.log(ws.lockFile ? " (none)" : " (no lock yet — run `oats sync`)");
|
|
1306
580
|
for (const p of ws.packages) {
|
|
1307
|
-
console.log(` ${p.id} ${p.version} ${p.source} @ ${p.commit.slice(0, 12)} ${p.
|
|
581
|
+
console.log(` ${p.id} ${p.version} ${p.source} @ ${p.commit.slice(0, 12)} ${p.integrity}`);
|
|
1308
582
|
if (p.capabilities.length) console.log(` capabilities: ${p.capabilities.join(", ")}`);
|
|
1309
583
|
}
|
|
1310
584
|
console.log(" membership, discovery and drift need the remotes: `oats workspace status`, `oats sync`.");
|
|
1311
585
|
}
|
|
1312
|
-
function doctor(dir) {
|
|
1313
|
-
const ctx =
|
|
586
|
+
async function doctor(dir) {
|
|
587
|
+
const { ctx, ws } = doctorDeployment(dir);
|
|
1314
588
|
const soulName = flag("soul");
|
|
1315
|
-
const ws = doctorLockData(ctx);
|
|
1316
589
|
console.log(`oats doctor — resolved from ${shortPath(ctx)}\n`);
|
|
1317
590
|
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`.
|
|
591
|
+
const composition = await doctorComposition(ctx, soulName, ws, (code, msg) => die(`${msg} [${code}]`));
|
|
1432
592
|
printDoctorWorkspace(ws);
|
|
1433
|
-
|
|
593
|
+
for (const p of legacyLayoutProblems(join(dirname(ws.local.path), "agents"))) console.log(`\n! ${p.code}: ${p.message}`);
|
|
1434
594
|
if (soulName) {
|
|
1435
595
|
const information = operationalKnowledgeNote(composition, soulName);
|
|
1436
596
|
if (information) console.log(`\nINFO: ${information}`);
|
|
@@ -1448,7 +608,7 @@ function replaceLaunchConfigsBlock(text, serialized) {
|
|
|
1448
608
|
const keyLine = /^(["']?)launch-configs\1:(\s*(?:#.*)?|\s+\S.*)?$/;
|
|
1449
609
|
const lines = text.split("\n");
|
|
1450
610
|
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-
|
|
611
|
+
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
612
|
const block = serialized ? serialized.replace(/\n$/, "").split("\n") : [];
|
|
1453
613
|
if (!starts.length) {
|
|
1454
614
|
if (!serialized) return text;
|
|
@@ -1489,7 +649,7 @@ function serializeLaunchConfigs(map) {
|
|
|
1489
649
|
const lines = ["launch-configs:"];
|
|
1490
650
|
for (const name of names) {
|
|
1491
651
|
const e = map[name];
|
|
1492
|
-
lines.push(` ${name}:`, `
|
|
652
|
+
lines.push(` ${name}:`, ` harness: ${e.harness}`);
|
|
1493
653
|
if (e.executable !== undefined) lines.push(` executable: ${yamlQuoted(e.executable)}`);
|
|
1494
654
|
if (e.args?.length) { lines.push(" args:"); for (const a of e.args) lines.push(` - ${yamlQuoted(a)}`); }
|
|
1495
655
|
const envNames = Object.keys(e.env || {}).sort();
|
|
@@ -1503,9 +663,11 @@ function serializeLaunchConfigs(map) {
|
|
|
1503
663
|
return lines.join("\n") + "\n";
|
|
1504
664
|
}
|
|
1505
665
|
/** Only the declared keys, in canonical order, from a validated entry. */
|
|
666
|
+
/** A configuration as `launch-config set` writes it: `harness` always — a `runtime` (the
|
|
667
|
+
* pre-0.27 name, read either) is written back under its new name (lead call 6). */
|
|
1506
668
|
function normalizeLaunchConfig(e) {
|
|
1507
669
|
return {
|
|
1508
|
-
|
|
670
|
+
harness: Object.hasOwn(e, "harness") ? e.harness : e.runtime,
|
|
1509
671
|
...(e.executable !== undefined ? { executable: e.executable } : {}),
|
|
1510
672
|
...(e.args?.length ? { args: [...e.args] } : {}),
|
|
1511
673
|
...(e.env && Object.keys(e.env).length ? { env: Object.fromEntries(Object.keys(e.env).sort().map((n) => [n, typeof e.env[n] === "string" ? e.env[n] : { fromEnv: e.env[n].fromEnv }])) } : {}),
|
|
@@ -1513,11 +675,9 @@ function normalizeLaunchConfig(e) {
|
|
|
1513
675
|
...(e.yolo !== undefined ? { yolo: e.yolo } : {}),
|
|
1514
676
|
};
|
|
1515
677
|
}
|
|
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);
|
|
678
|
+
function readLaunchConfigsModel(local) {
|
|
679
|
+
const map = local["launch-configs"] || {};
|
|
680
|
+
for (const [name, entry] of Object.entries(map)) validateLaunchConfig(name, entry, "oats-local.yaml");
|
|
1521
681
|
const out = Object.create(null); // a name may be "constructor": membership is own only
|
|
1522
682
|
for (const [n, e] of Object.entries(map)) out[n] = normalizeLaunchConfig(e);
|
|
1523
683
|
return out;
|
|
@@ -1529,7 +689,7 @@ function readLaunchConfigsModel(file) {
|
|
|
1529
689
|
* `set --keep-env`. */
|
|
1530
690
|
function publicLaunchConfig(e, extra = {}) {
|
|
1531
691
|
const env = Object.fromEntries(Object.keys(e.env || {}).sort().map((n) => [n, typeof e.env[n] === "string" ? { redacted: true } : { fromEnv: e.env[n].fromEnv }]));
|
|
1532
|
-
return {
|
|
692
|
+
return { harness: e.harness, executable: e.executable ?? null, args: [...(e.args || [])], env, model: e.model ?? null, yolo: e.yolo ?? null, ...extra };
|
|
1533
693
|
}
|
|
1534
694
|
/** The scope a launch-config command reads: --dir (or cwd), a running
|
|
1535
695
|
* home's recorded context (--home), or a soul's own member context
|
|
@@ -1551,10 +711,8 @@ function launchConfigContext(bail) {
|
|
|
1551
711
|
if (soulFlag === true) bail("E_BAD_ARGS", "--soul needs a soul name");
|
|
1552
712
|
const agentsRootFlag = flag("agents-root");
|
|
1553
713
|
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
714
|
let soul;
|
|
1557
|
-
try { soul = selectSoul(scopeSouls(ctx
|
|
715
|
+
try { soul = selectSoul(scopeSouls(ctx).souls, String(soulFlag), agentsRootFlag, ctx); } catch (e) { bail(e.code || "E_SOUL_UNKNOWN", e.message); }
|
|
1558
716
|
return { context: memberContextOf(soul, ctx, flag("dir") !== undefined, bail), selected: { soul: soul.name, agentsRoot: soul.agentsRoot } };
|
|
1559
717
|
}
|
|
1560
718
|
return { context: ctx, selected: null };
|
|
@@ -1564,38 +722,38 @@ function launchConfigContext(bail) {
|
|
|
1564
722
|
* configuration, preflighted, read-only; environment values withheld and
|
|
1565
723
|
* the prompt named, never the TASK body. */
|
|
1566
724
|
function launchPreview(bail) {
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
if (sel.
|
|
725
|
+
for (const k of ["launch-config", "harness", "runtime", "model"]) if (flag(k) === true) bail("E_BAD_ARGS", `--${k} needs a value`);
|
|
726
|
+
const sel = { launchConfig: flag("launch-config"), harness: harnessFlag(), model: flag("model"), yolo: yoloFlag() };
|
|
727
|
+
if (sel.harness !== undefined && !LAUNCH_HARNESSES.includes(sel.harness)) bail("E_BAD_ARGS", `--harness must be one of ${LAUNCH_HARNESSES.join(", ")}`);
|
|
1570
728
|
const { context, selected } = launchConfigContext(bail);
|
|
1571
729
|
if (!selected) bail("E_BAD_ARGS", "preview needs --home <abs> (an existing instance) or --soul <name> [--dir <scope>] (a new instance)");
|
|
1572
|
-
const selectionGiven = sel.launchConfig !== undefined || sel.
|
|
730
|
+
const selectionGiven = sel.launchConfig !== undefined || sel.harness !== undefined || sel.model !== undefined || sel.yolo !== undefined;
|
|
1573
731
|
let meta = null, agentLike, home, instance;
|
|
1574
732
|
if (selected.home) {
|
|
1575
733
|
home = selected.home;
|
|
1576
|
-
try { meta = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); } catch (e) { bail("E_HOME_UNKNOWN", `${home}: ${e.message}`); }
|
|
734
|
+
try { meta = upgradeHomeMeta(JSON.parse(readFileSync(join(home, "instance.json"), "utf8")), home); } catch (e) { bail("E_HOME_UNKNOWN", `${home}: ${e.message}`); }
|
|
1577
735
|
instance = meta.instance || basename(home);
|
|
1578
736
|
if (!(meta.launch && typeof meta.launch === "object") && !selectionGiven) {
|
|
1579
737
|
// A home that predates recipes, asked nothing: its frozen command is
|
|
1580
|
-
// described as is. Under a selection
|
|
1581
|
-
//
|
|
738
|
+
// described as is. Under a selection the planner refuses it
|
|
739
|
+
// (E_LAUNCH_LEGACY: re-spawn it from the deployment).
|
|
1582
740
|
let d;
|
|
1583
741
|
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,
|
|
742
|
+
jsonOk({ context, selected, selection: { source: "frozen-command", launchConfig: null, harness: null, model: null, yolo: null }, harness: meta.harness, 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
743
|
return;
|
|
1586
744
|
}
|
|
1587
745
|
const agentsRoot = agentsRootOfHome(home);
|
|
1588
746
|
const agent = (() => { try { return findAgent(agentsRoot, meta.agent); } catch { return undefined; } })();
|
|
1589
|
-
agentLike = agent || {
|
|
747
|
+
agentLike = agent || { harness: meta.harness, model: meta.model, yolo: meta.yolo };
|
|
1590
748
|
} 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 };
|
|
749
|
+
const soul = scopeSouls(context).souls.find((x) => x.name === selected.soul && x.agentsRoot === selected.agentsRoot);
|
|
750
|
+
agentLike = { harness: soul.harness, model: soul.model };
|
|
1595
751
|
instance = `${soul.name}-<purpose>`; home = join(selected.agentsRoot, soul.name, "instances", instance);
|
|
1596
752
|
}
|
|
753
|
+
// A home's recorded capabilities; a new instance's are its spawn's resolution,
|
|
754
|
+
// which a preview of a soul does not prepare (spawn --preview does).
|
|
1597
755
|
let r;
|
|
1598
|
-
try { r =
|
|
756
|
+
try { r = meta ? resolvedFromHome(home, meta) : { capabilities: [], launchConfigs: launchConfigsAt(context) }; } catch (e) { bail(e.code || "E_CONFIG_BROKEN", e.message, e.details); }
|
|
1599
757
|
// The same planner a start uses, in preview mode: failed checks are listed, nothing is touched.
|
|
1600
758
|
let plan;
|
|
1601
759
|
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); }
|
|
@@ -1603,47 +761,53 @@ function launchPreview(bail) {
|
|
|
1603
761
|
const command = renderLaunchRecipe(recipe, { home, instance, redact: true });
|
|
1604
762
|
const d = describeLaunchCommand(command);
|
|
1605
763
|
const environment = d.environment.map((e) => e.reference && recipe.env[e.name]?.fromEnv ? { name: e.name, fromEnv: recipe.env[e.name].fromEnv } : e);
|
|
1606
|
-
jsonOk({ context, selected, selection: { source: plan.selectionSource, launchConfig: recipe.launchConfig,
|
|
764
|
+
jsonOk({ context, selected, selection: { source: plan.selectionSource, launchConfig: recipe.launchConfig, harness: sel.harness ?? null, model: sel.model ?? null, yolo: sel.yolo ?? null }, harness: plan.harness, 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
765
|
}
|
|
1608
766
|
async function launchConfigCmd() {
|
|
1609
|
-
const bail = (code, msg) => (JSON_MODE ? jsonFail(code, msg) : die(msg));
|
|
767
|
+
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1610
768
|
dropAmbientRoot();
|
|
1611
769
|
const sub = args[1];
|
|
1612
|
-
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] [--
|
|
770
|
+
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] [--harness r] [--model m] [--yolo|--no-yolo] --json";
|
|
1613
771
|
if (sub === "preview") { launchPreview(bail); return; }
|
|
1614
772
|
if (!["list", "set", "remove"].includes(sub)) bail("E_USAGE", usage);
|
|
1615
773
|
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
|
-
|
|
774
|
+
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`);
|
|
775
|
+
// Lead decision 2: launch configurations are a HOST choice, declared in the
|
|
776
|
+
// deployment's oats-local.yaml (found walking up; a home's own deployment).
|
|
777
|
+
const at = selected?.home ?? dir;
|
|
778
|
+
// A 0.25 oats-config.yaml in reach is refused by loadLocal (E_CONFIG_BROKEN, naming
|
|
779
|
+
// the move), never read as "no configurations".
|
|
780
|
+
let found = null;
|
|
781
|
+
// No deployment in reach answers the (empty) effective set — unless what is in
|
|
782
|
+
// reach is a 0.25 oats-config.yaml, whose launch-configs nothing reads any more.
|
|
783
|
+
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); }
|
|
784
|
+
const file = found?.path ?? null, level = file ? dirname(file) : null;
|
|
785
|
+
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
786
|
if (sub === "list") {
|
|
1624
787
|
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
|
|
788
|
+
try { configurations = effective(); } catch (e) { bail(e.code || "E_LAUNCH_CONFIG_INVALID", e.message); }
|
|
789
|
+
if (JSON_MODE) { jsonOk({ context: dir, level, file, selected, configurations }); return; }
|
|
790
|
+
if (!configurations.length) { console.log(`No launch configurations are declared${file ? ` in ${shortPath(file)}` : ` (no oats-local.yaml in reach of ${dir})`}`); return; }
|
|
1628
791
|
for (const c of configurations) {
|
|
1629
792
|
const env = Object.entries(c.env).map(([n, v]) => v.fromEnv ? `${n}=$${v.fromEnv}` : `${n}=<redacted>`).join(" ");
|
|
1630
|
-
console.log(`${c.name}: ${c.
|
|
793
|
+
console.log(`${c.name}: ${c.harness}${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
794
|
}
|
|
1632
795
|
return;
|
|
1633
796
|
}
|
|
797
|
+
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
798
|
const name = args[2];
|
|
1635
799
|
if (!name || name.startsWith("--")) bail("E_BAD_ARGS", `launch-config ${sub} needs a configuration name`);
|
|
1636
|
-
const text =
|
|
800
|
+
const text = readFileSync(file, "utf8");
|
|
1637
801
|
let model;
|
|
1638
|
-
try { model = readLaunchConfigsModel(
|
|
802
|
+
try { model = readLaunchConfigsModel(found.local); } catch (e) { bail(e.code || "E_LAUNCH_CONFIG_INVALID", e.message); }
|
|
1639
803
|
const declaredHere = Object.hasOwn(model, name);
|
|
1640
804
|
const before = declaredHere ? publicLaunchConfig(model[name]) : null;
|
|
1641
805
|
if (sub === "remove") {
|
|
1642
|
-
if (!declaredHere) bail("E_LAUNCH_CONFIG_UNKNOWN", `${name} is not declared
|
|
806
|
+
if (!declaredHere) bail("E_LAUNCH_CONFIG_UNKNOWN", `${name} is not declared in ${shortPath(file)}`);
|
|
1643
807
|
delete model[name];
|
|
1644
808
|
} else {
|
|
1645
809
|
const f = flag("file");
|
|
1646
|
-
if (!f || f === true) bail("E_BAD_ARGS", "launch-config set needs --file <json> (an object with
|
|
810
|
+
if (!f || f === true) bail("E_BAD_ARGS", "launch-config set needs --file <json> (an object with harness and optional executable, args, env, model, yolo)");
|
|
1647
811
|
let entry;
|
|
1648
812
|
// A parse error is reported without the parser's text: its message can
|
|
1649
813
|
// quote the document, and a definition may carry environment literals.
|
|
@@ -1656,19 +820,18 @@ async function launchConfigCmd() {
|
|
|
1656
820
|
} else {
|
|
1657
821
|
try { raw = readFileSync(f, "utf8"); } catch (e) { bail("E_BAD_ARGS", `--file ${f}: ${e.code === "ENOENT" ? "no such file" : e.code || "cannot read"}`); }
|
|
1658
822
|
}
|
|
1659
|
-
try { entry = JSON.parse(raw); } catch { bail("E_BAD_ARGS", `--file ${f} is not valid JSON (one object with
|
|
823
|
+
try { entry = JSON.parse(raw); } catch { bail("E_BAD_ARGS", `--file ${f} is not valid JSON (one object with harness and optional executable, args, env, model, yolo)`); }
|
|
1660
824
|
if (args.includes("--keep-env")) {
|
|
1661
825
|
// 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`);
|
|
826
|
+
// declared definition of that name: a one-time copy into the complete
|
|
827
|
+
// replacement entry.
|
|
828
|
+
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");
|
|
829
|
+
const current = declaredHere ? model[name] : undefined;
|
|
830
|
+
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
831
|
if (entry && typeof entry === "object" && Object.keys(current.env || {}).length) entry.env = { ...current.env };
|
|
1670
832
|
}
|
|
1671
833
|
try { validateLaunchConfig(name, entry, `--file ${f}`); } catch (e) { bail(e.code || "E_LAUNCH_CONFIG_INVALID", e.message); }
|
|
834
|
+
if (!Object.hasOwn(entry, "harness")) noteRuntimeName(`runtime in the --file definition (written as harness)`);
|
|
1672
835
|
model[name] = normalizeLaunchConfig(entry);
|
|
1673
836
|
}
|
|
1674
837
|
let next;
|
|
@@ -1676,7 +839,10 @@ async function launchConfigCmd() {
|
|
|
1676
839
|
// What is written must read back as exactly what was asked, by the kernel's
|
|
1677
840
|
// own reader, before a byte of the file changes.
|
|
1678
841
|
let readBack;
|
|
1679
|
-
|
|
842
|
+
let nextLocal;
|
|
843
|
+
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`); }
|
|
844
|
+
const schemaProblems = validateLocal(nextLocal);
|
|
845
|
+
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
846
|
const canonical = (m) => JSON.stringify(Object.keys(m).sort().map((n) => [n, normalizeLaunchConfig(m[n])]));
|
|
1681
847
|
const same = canonical(readBack) === canonical(model);
|
|
1682
848
|
if (!same) bail("E_LAUNCH_CONFIG_INVALID", `${name} would not read back as written; nothing was written`);
|
|
@@ -1685,7 +851,7 @@ async function launchConfigCmd() {
|
|
|
1685
851
|
try { eff = effective().find((c) => c.name === name) || null; } catch (e) { eff = { error: e.message }; }
|
|
1686
852
|
const receipt = { name, action: sub, level, file, before, after: Object.hasOwn(model, name) ? publicLaunchConfig(model[name]) : null, effective: eff };
|
|
1687
853
|
if (JSON_MODE) { jsonOk(receipt); return; }
|
|
1688
|
-
console.log(sub === "set" ? `Declared launch configuration ${name}
|
|
854
|
+
console.log(sub === "set" ? `Declared launch configuration ${name} in ${shortPath(file)}` : `Removed launch configuration ${name} from ${shortPath(file)}`);
|
|
1689
855
|
}
|
|
1690
856
|
|
|
1691
857
|
|
|
@@ -1752,8 +918,7 @@ function instanceCmd() {
|
|
|
1752
918
|
if (home === undefined) {
|
|
1753
919
|
let root;
|
|
1754
920
|
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)))];
|
|
921
|
+
const roots = [realOrResolved(root)];
|
|
1757
922
|
const candidates = [];
|
|
1758
923
|
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
924
|
if (!candidates.length) return bail("E_SESSION_UNKNOWN", `no instance ${JSON.stringify(name)} under ${roots.join(", ")}`);
|
|
@@ -1783,48 +948,36 @@ function instanceCmd() {
|
|
|
1783
948
|
bail(e.code || "E_GIT_FAILED", e.message, e.observation ? { observation: e.observation } : undefined);
|
|
1784
949
|
}
|
|
1785
950
|
}
|
|
1786
|
-
/** `oats readiness
|
|
1787
|
-
function readinessCmd() {
|
|
951
|
+
/** `oats readiness (--soul <name> [--agents-root <abs>] [--dir <d>] | --home <abs>) [--policy] --json` — readinessApi 2. */
|
|
952
|
+
async function readinessCmd() {
|
|
1788
953
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
1789
954
|
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
955
|
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}` : ""}`);
|
|
956
|
+
// Workspace model (readinessApi 2): an instance or soul subject; checks
|
|
957
|
+
// installed | configured | member | providers. No trusted check.
|
|
958
|
+
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");
|
|
959
|
+
const target = await workspaceTarget(bail, { command: "readiness" });
|
|
960
|
+
{
|
|
961
|
+
const given = (name) => { const v = flag(name); return v && v !== true ? String(v) : null; };
|
|
962
|
+
const selector = homeArg && homeArg !== true ? { kind: "home", home: String(homeArg), soul: given("soul"), agentsRoot: given("agents-root") }
|
|
963
|
+
: { kind: "soul", soul: String(flag("soul")), agentsRoot: given("agents-root"), dir: given("dir") };
|
|
964
|
+
let catalog = null; try { catalog = officialPackageCatalog(); } catch { catalog = null; }
|
|
965
|
+
const doc = await readinessDocument(target, { selector, remoteOptions: remoteOptionsFromEnv(), catalog });
|
|
966
|
+
if (args.includes("--policy")) {
|
|
967
|
+
doc.policy = policyOf({ instanceMeta: target.meta, soul: target.meta ? null : policySoul(target) }).policy;
|
|
968
|
+
doc.notes.push("policy: a lifecycle-authority claim enforced by the spawn route, not an OS sandbox");
|
|
969
|
+
}
|
|
970
|
+
if (JSON_MODE) { jsonOk(doc); return; }
|
|
971
|
+
const s = doc.subject;
|
|
972
|
+
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`}`);
|
|
973
|
+
for (const [name, check] of Object.entries(doc.checks)) {
|
|
974
|
+
console.log(` ${name}: ${check.status}`);
|
|
975
|
+
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}` : ""}`);
|
|
976
|
+
}
|
|
977
|
+
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"}`);
|
|
978
|
+
for (const n of doc.notes) console.log(` note: ${n}`);
|
|
979
|
+
return;
|
|
1825
980
|
}
|
|
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
981
|
}
|
|
1829
982
|
// ---------- workspace model v2: sync / package / workspace status / capabilities / souls ----------
|
|
1830
983
|
// Contract: docs/design/2026-09-23-workspace-module-contracts.md §6. Nothing is
|
|
@@ -1862,12 +1015,13 @@ function catalogForSync(bail) {
|
|
|
1862
1015
|
* recomposes the tag from the catalog's own convention.
|
|
1863
1016
|
* → { packages: { <id>: <version> }, problems: [ { code: "E_PACKAGE_MISSING", … } ] } */
|
|
1864
1017
|
function standalonePackages(catalog) {
|
|
1865
|
-
let id = "oats.framework"
|
|
1866
|
-
|
|
1018
|
+
let id = "oats.framework";
|
|
1019
|
+
const file = officialCatalogFile();
|
|
1020
|
+
try { const alias = officialCapabilityAliases()["oats.core"]; id = (typeof alias === "string" ? alias : alias?.package) ?? id; } catch { /* the catalog is diagnosed below */ }
|
|
1867
1021
|
const version = standaloneCatalogVersion(catalog?.[id]?.ref);
|
|
1868
1022
|
if (version) return { packages: { [id]: version }, problems: [] };
|
|
1869
1023
|
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
|
|
1024
|
+
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
1025
|
}
|
|
1872
1026
|
/** The bare version of a catalog ref: its LAST path segment when that is a version
|
|
1873
1027
|
* (`v1.1.3` → `v1.1.3`; `oats-framework/v1.1.3` → `v1.1.3`; `main` → null). */
|
|
@@ -1889,31 +1043,29 @@ async function discoverForCli(ctx, bail) {
|
|
|
1889
1043
|
}
|
|
1890
1044
|
|
|
1891
1045
|
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
1046
|
/** Display name of a discovery: the workspace's name, or the standalone label (decision 10). */
|
|
1897
1047
|
const workspaceName = (discovery) => discovery.workspace?.name ?? `standalone:${memberLabel(discovery.key)}`;
|
|
1898
1048
|
const memberLabel = (key) => String(key).split("/").filter(Boolean).pop()?.replace(/\.git$/, "") || String(key);
|
|
1899
1049
|
const teamLabel = (team) => team ?? "unassigned";
|
|
1900
1050
|
const originOf = (item) => (item.package ? `package ${item.package} v${item.version}` : `member ${item.repoKey} @ ${short(item.commit)}`);
|
|
1901
1051
|
|
|
1902
|
-
/** Rows of every
|
|
1903
|
-
|
|
1052
|
+
/** Rows of every soul and capability of confirmed members (+ external souls) + locked package
|
|
1053
|
+
* capabilities. Souls have no private mode (0.26.0); a private member capability is listed with
|
|
1054
|
+
* `private: true` — repo-owned: usable only by its own repo's souls (E_CAPABILITY_PRIVATE). */
|
|
1055
|
+
function workspaceItems(discovery, lock) {
|
|
1904
1056
|
const souls = [];
|
|
1905
1057
|
const capabilities = [];
|
|
1906
1058
|
for (const m of discovery.members) {
|
|
1907
1059
|
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)
|
|
1060
|
+
for (const s of m.souls) souls.push({ name: s.name, origin: originOf(s), kind: "member", repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? (s.team ? [s.team] : []))], private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null });
|
|
1061
|
+
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
1062
|
}
|
|
1911
1063
|
for (const ext of discovery.external || []) {
|
|
1912
1064
|
const s = ext.soul;
|
|
1913
|
-
|
|
1065
|
+
souls.push({ name: s.name, origin: `external ${s.repoKey} @ ${short(s.commit)}`, kind: "external", repoKey: s.repoKey, commit: s.commit, team: teamLabel(s.team), labels: [...(s.labels ?? (s.team ? [s.team] : []))], private: s.private, path: s.path, work: s.definition.work ?? null, description: s.definition.description ?? null });
|
|
1914
1066
|
}
|
|
1915
1067
|
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
|
|
1068
|
+
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
1069
|
}
|
|
1918
1070
|
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
1071
|
souls.sort(byName);
|
|
@@ -1931,7 +1083,7 @@ function memberRows(discovery) {
|
|
|
1931
1083
|
|
|
1932
1084
|
/** Package rows for `sync` / `workspace status` from the lock. */
|
|
1933
1085
|
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
|
|
1086
|
+
return Object.entries(lock.packages).map(([id, p]) => ({ id, version: p.version, source: p.source, commit: p.commit, integrity: p.integrity, capabilities: p.capabilities }));
|
|
1935
1087
|
}
|
|
1936
1088
|
|
|
1937
1089
|
/** Print a padded table: rows are arrays of strings. */
|
|
@@ -1941,55 +1093,17 @@ function printTable(header, rows) {
|
|
|
1941
1093
|
for (const r of all) console.log(" " + r.map((c, i) => String(c ?? "").padEnd(widths[i])).join(" ").trimEnd());
|
|
1942
1094
|
}
|
|
1943
1095
|
|
|
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
|
-
}
|
|
1096
|
+
/** `--approve` left with package approval (human decision 2026-09-24). */
|
|
1097
|
+
const APPROVAL_REMOVED = "package approval was removed; declaring a package in packages: is the trust decision";
|
|
1986
1098
|
|
|
1987
1099
|
/** The body of `oats sync` — shared by `sync` and `onboard` (which onboards, then syncs the same
|
|
1988
1100
|
* way). Given a v2 deployment context: discover over the remotes, confirm membership, resolve
|
|
1989
|
-
* `packages:` against the lock
|
|
1101
|
+
* `packages:` against the lock (commit + integrity), write the lock. There is no approval step:
|
|
1102
|
+
* declaring a package in `packages:` is the trust decision (human decision 2026-09-24).
|
|
1990
1103
|
* `bail` never returns (it exits the process with the caller's error shape).
|
|
1991
|
-
* → { report, lock, discovery,
|
|
1104
|
+
* → { report, lock, discovery, items, lockFile, problems } */
|
|
1992
1105
|
async function performSync(ctx, bail, { onDiscovered } = {}) {
|
|
1106
|
+
if (args.includes("--approve")) return bail("E_BAD_ARGS", `--approve: ${APPROVAL_REMOVED}`, { flag: "--approve" });
|
|
1993
1107
|
const catalog = catalogForSync(bail);
|
|
1994
1108
|
const discovery = await discoverForCli(ctx, bail);
|
|
1995
1109
|
onDiscovered?.(discovery);
|
|
@@ -2007,37 +1121,7 @@ async function performSync(ctx, bail, { onDiscovered } = {}) {
|
|
|
2007
1121
|
if (typeof e?.code === "string" && e.code.startsWith("E_")) return bail(e.code, e.message, e.details ?? e.provenance);
|
|
2008
1122
|
throw e;
|
|
2009
1123
|
}
|
|
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
|
-
}
|
|
1124
|
+
const lock = resolved.lock;
|
|
2041
1125
|
let lockFile;
|
|
2042
1126
|
try { lockFile = writeLock(ctx.deploymentDir, lock); } catch (e) { return bail(e.code || "E_LOCK_SCHEMA", e.message, e.details); }
|
|
2043
1127
|
// The deployment's instance root: <deployment>/agents/ (findRoot's marker). A hand-written
|
|
@@ -2046,9 +1130,9 @@ async function performSync(ctx, bail, { onDiscovered } = {}) {
|
|
|
2046
1130
|
const members = memberRows(discovery);
|
|
2047
1131
|
const packages = packageRows(lock);
|
|
2048
1132
|
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,
|
|
1133
|
+
const items = workspaceItems(discovery, lock);
|
|
1134
|
+
const report = { syncApi: 1, standalone: discovery.standalone === true || undefined, workspace: { name: workspaceName(discovery), key: discovery.key, url: discovery.url, commit: discovery.commit, observedAt: discovery.observedAt, local: ctx.localPath, lock: lockFile }, members, packages, changes, problems, warnings: discovery.warnings ?? [] };
|
|
1135
|
+
return { report, lock, discovery, items, lockFile, problems };
|
|
2052
1136
|
}
|
|
2053
1137
|
|
|
2054
1138
|
/** The one-line standalone explanation (decision 10). `standaloneReason` says WHY the view is
|
|
@@ -2065,32 +1149,28 @@ function standaloneNote(discovery, { from = "" } = {}) {
|
|
|
2065
1149
|
|
|
2066
1150
|
/** The human §8 report of a sync (text mode). */
|
|
2067
1151
|
function printSyncReport(ctx, synced) {
|
|
2068
|
-
const { report, discovery,
|
|
1152
|
+
const { report, discovery, items, lockFile } = synced;
|
|
2069
1153
|
const { members, packages, changes } = report;
|
|
2070
1154
|
const disabled = new Set(ctx.local.souls?.disabled || []);
|
|
2071
1155
|
console.log(`workspace ${discovery.workspace?.name ?? `(${standaloneNote(discovery)})`} (${discovery.key} @ ${short(discovery.commit)})`);
|
|
2072
1156
|
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)"}`);
|
|
1157
|
+
console.log(`packages ${packages.map((p) => `${p.id} ${p.version} ✓ (@ ${short(p.commit)})`).join(" ") || "(none)"}`);
|
|
2077
1158
|
const changed = changes.filter((c) => c.to !== null && c.from !== c.to).map((c) => `${c.id} ${c.from ?? "—"} → ${c.to} (@ ${short(c.commit)})`);
|
|
2078
1159
|
const removed = changes.filter((c) => c.to === null).map((c) => `${c.id} ${c.from} → removed`);
|
|
2079
1160
|
console.log(`changed ${[...changed, ...removed].join(" ") || "(nothing — the lock already described this workspace)"}`);
|
|
2080
1161
|
const memberSouls = items.souls.filter((s) => s.kind === "member");
|
|
2081
1162
|
const externalSouls = items.souls.filter((s) => s.kind === "external");
|
|
2082
|
-
|
|
1163
|
+
// Souls have no private mode (0.26.0); the private count is of repo-owned member capabilities.
|
|
1164
|
+
const repoOwned = items.capabilities.filter((c) => c.kind === "member" && c.private);
|
|
2083
1165
|
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) · ${
|
|
1166
|
+
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
1167
|
const teams = new Map();
|
|
2086
1168
|
for (const s of items.souls) { const t = teams.get(s.team) || { souls: 0, capabilities: 0 }; t.souls++; teams.set(s.team, t); }
|
|
2087
1169
|
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
1170
|
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
1171
|
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)}`);
|
|
1172
|
+
for (const w of discovery.warnings ?? []) console.log(`warning ${w.code} ${w.message}`);
|
|
1173
|
+
console.log(`\nlock ${shortPath(lockFile)}`);
|
|
2094
1174
|
}
|
|
2095
1175
|
|
|
2096
1176
|
/** `oats sync [--dir] [--json]` — contract §6. */
|
|
@@ -2098,10 +1178,8 @@ async function syncCmd() {
|
|
|
2098
1178
|
const bail = (code, msg, details) => (JSON_MODE ? jsonFail(code, msg, details) : die(msg));
|
|
2099
1179
|
const ctx = workspaceContext(bail);
|
|
2100
1180
|
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.
|
|
1181
|
+
// One envelope (or the §8 report); success is exit 0 (a failure bails with its code).
|
|
2103
1182
|
if (JSON_MODE) jsonOk(synced.report); else printSyncReport(ctx, synced);
|
|
2104
|
-
process.exitCode = synced.approvalNeeded.length ? 2 : 0;
|
|
2105
1183
|
}
|
|
2106
1184
|
|
|
2107
1185
|
/** Walk up from dir for oats-workspace.yaml INSIDE a Git checkout → { file, root } | null. */
|
|
@@ -2186,7 +1264,7 @@ async function packageCmd() {
|
|
|
2186
1264
|
const receipt = { action: sub, id, value: value ?? null, previous: had ?? null, edited: true, file: checkout.file };
|
|
2187
1265
|
if (JSON_MODE) { jsonOk(receipt); return; }
|
|
2188
1266
|
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
|
|
1267
|
+
? `${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
1268
|
: `Removed packages.${id} (${had}) from ${shortPath(checkout.file)}. Commit it, then \`oats sync\`.`);
|
|
2191
1269
|
}
|
|
2192
1270
|
|
|
@@ -2207,8 +1285,7 @@ async function workspaceCmd() {
|
|
|
2207
1285
|
const locked = new Set(packages.map((p) => p.id));
|
|
2208
1286
|
const unsynced = declared.filter((id) => !locked.has(id));
|
|
2209
1287
|
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 };
|
|
1288
|
+
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
1289
|
if (JSON_MODE) { jsonOk(result); return; }
|
|
2213
1290
|
console.log(`workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)}) local ${shortPath(ctx.localPath)}\n`);
|
|
2214
1291
|
if (standalone) console.log(` (${standaloneNote(discovery)})\n`);
|
|
@@ -2217,11 +1294,12 @@ async function workspaceCmd() {
|
|
|
2217
1294
|
for (const m of members.filter((m) => !m.confirmed)) console.log(` ${m.name}: ${m.detail}`);
|
|
2218
1295
|
console.log("\nPackages:");
|
|
2219
1296
|
if (!packages.length) console.log(unsynced.length ? ` (none locked yet — \`oats sync\` resolves ${unsynced.join(", ")})` : " (none)");
|
|
2220
|
-
else printTable(["package", "version", "source", "commit", "
|
|
1297
|
+
else printTable(["package", "version", "source", "commit", "capabilities"], packages.map((p) => [p.id, p.version, p.source, short(p.commit), p.capabilities.join(",")]));
|
|
2221
1298
|
if (packages.length && unsynced.length) console.log(` declared but not locked (run \`oats sync\`): ${unsynced.join(", ")}`);
|
|
2222
1299
|
if (stale.length) console.log(` locked but no longer declared (run \`oats sync\`): ${stale.join(", ")}`);
|
|
2223
1300
|
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
1301
|
if (discovery.problems.length) { console.log("\nProblems:"); for (const p of discovery.problems) console.log(` ${p.code} ${p.repoKey ? `${memberLabel(p.repoKey)}:` : ""}${p.path} ${p.message}`); }
|
|
1302
|
+
if (discovery.warnings?.length) { console.log("\nWarnings:"); for (const w of discovery.warnings) console.log(` ${w.code} ${w.message}`); }
|
|
2225
1303
|
}
|
|
2226
1304
|
|
|
2227
1305
|
/** `oats capabilities` / `oats souls` [--dir] [--json] — contract §6. */
|
|
@@ -2236,8 +1314,8 @@ async function itemsCmd(kind) {
|
|
|
2236
1314
|
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
1315
|
console.log(`${kind} of workspace ${workspaceName(discovery)} (${discovery.key} @ ${short(discovery.commit)})${standalone ? ` — ${standaloneNote(discovery)}` : ""}\n`);
|
|
2238
1316
|
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 ?? "—"]));
|
|
1317
|
+
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 ?? "—"]));
|
|
1318
|
+
else printTable(["name", "origin", "team", "layer"], items.map((c) => [c.private ? `${c.name} (repo-owned)` : c.name, c.origin, c.team, c.layer ?? "—"]));
|
|
2241
1319
|
const unsynced = Object.keys(discovery.workspace?.packages || {}).filter((id) => !lock.packages[id]);
|
|
2242
1320
|
if (kind === "capabilities" && unsynced.length) console.log(`\n package capabilities of ${unsynced.join(", ")} appear after \`oats sync\``);
|
|
2243
1321
|
}
|
|
@@ -2252,7 +1330,13 @@ async function itemsCmd(kind) {
|
|
|
2252
1330
|
* present) to its stamp — the roster's `repo:` column for a workspace soul. */
|
|
2253
1331
|
async function statusDrift(data) {
|
|
2254
1332
|
let ctx;
|
|
2255
|
-
try { ctx = loadLocal(dirFlag()); }
|
|
1333
|
+
try { ctx = loadLocal(dirFlag()); }
|
|
1334
|
+
catch (e) {
|
|
1335
|
+
if (e?.code === "E_LOCAL_MISSING") return null;
|
|
1336
|
+
// A 0.25 oats-config.yaml inside the deployment: the typed migration error, never a stack.
|
|
1337
|
+
if (e?.code === "E_CONFIG_BROKEN") { if (JSON_MODE) jsonFail(e.code, e.message, e.details); die(e.message); }
|
|
1338
|
+
throw e;
|
|
1339
|
+
}
|
|
2256
1340
|
const hasModules = (i) => i.modules && typeof i.modules === "object" && Object.keys(i.modules).length > 0;
|
|
2257
1341
|
const hasSoul = (i) => i.workspace && typeof i.workspace === "object" && i.workspace.soul && typeof i.workspace.soul === "object" && typeof i.workspace.soul.repoKey === "string";
|
|
2258
1342
|
// Workspace souls of this roster: the stamp ensureWorkspaceSoul leaves beside the soul pointer.
|
|
@@ -2316,13 +1400,14 @@ function soulRepoLabel(a, ws) {
|
|
|
2316
1400
|
const short7 = (oid) => (typeof oid === "string" ? oid.slice(0, 7) : "?");
|
|
2317
1401
|
|
|
2318
1402
|
async function status() {
|
|
2319
|
-
if (args.includes("--team"))
|
|
1403
|
+
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
1404
|
let root;
|
|
2321
1405
|
try { root = ensureRoot(dirFlag()); }
|
|
2322
1406
|
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
1407
|
const data = listInstances(root);
|
|
2324
1408
|
const ws = await statusDrift(data);
|
|
2325
1409
|
const verbose = args.includes("--verbose");
|
|
1410
|
+
const problems = legacyLayoutProblems(root);
|
|
2326
1411
|
if (args.includes("--json")) {
|
|
2327
1412
|
if (ws) for (const a of data) {
|
|
2328
1413
|
const stamp = ws.souls.get(a.name);
|
|
@@ -2335,13 +1420,14 @@ async function status() {
|
|
|
2335
1420
|
if (s) i.soul = { repoKey: s.repoKey, commit: s.commit, current: s.current?.commit ?? null, status: s.status, ...(s.reason ? { reason: s.reason } : {}) };
|
|
2336
1421
|
}
|
|
2337
1422
|
}
|
|
2338
|
-
console.log(JSON.stringify({ root, agents: data, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}) }, null, 2)); return;
|
|
1423
|
+
console.log(JSON.stringify({ root, agents: data, ...(ws ? { workspace: ws.unreachable ? { reachable: false, ...ws.unreachable } : { reachable: true } } : {}), ...(problems.length ? { problems } : {}), ...envelopeWarnings() }, null, 2)); return;
|
|
2339
1424
|
}
|
|
2340
1425
|
console.log(`oats status — agents root ${shortPath(root)}\n`);
|
|
2341
1426
|
if (ws?.unreachable) console.log(` workspace: unreachable (${ws.unreachable.reason}) — drift unknown\n`);
|
|
2342
|
-
|
|
1427
|
+
for (const p of problems) console.log(` ! ${p.code}: ${p.message}\n`);
|
|
1428
|
+
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
1429
|
for (const a of data) {
|
|
2344
|
-
console.log(` ${a.name}
|
|
1430
|
+
console.log(` ${a.name} [work: ${a.work || "checkout"}, repo: ${soulRepoLabel(a, ws)}]`);
|
|
2345
1431
|
if (a.description) console.log(` ${a.description}`);
|
|
2346
1432
|
for (const i of a.instances) {
|
|
2347
1433
|
console.log(` • ${i.instance} ${i.retirePending ? "RETIRING" : i.running ? "RUNNING" : "idle"} (branch ${i.branch || "?"}, ${i.work || "?"})`);
|
|
@@ -2356,29 +1442,6 @@ async function status() {
|
|
|
2356
1442
|
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
1443
|
}
|
|
2358
1444
|
}
|
|
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
1445
|
}
|
|
2383
1446
|
|
|
2384
1447
|
async function spawnCmd() {
|
|
@@ -2393,90 +1456,96 @@ async function spawnCmd() {
|
|
|
2393
1456
|
const checkDirectoryOptions = (work) => {
|
|
2394
1457
|
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
1458
|
};
|
|
2396
|
-
checkDirectoryOptions(requestedWork); // before
|
|
1459
|
+
checkDirectoryOptions(requestedWork); // before anything is resolved or written
|
|
2397
1460
|
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>] [--
|
|
1461
|
+
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>] [--harness pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] [--no-launch] [--json]");
|
|
2399
1462
|
// Retired boundary flags (maintainer transport ruling): fail LOUDLY before
|
|
2400
|
-
// ANY side effect
|
|
2401
|
-
//
|
|
2402
|
-
//
|
|
2403
|
-
|
|
1463
|
+
// ANY side effect, including root discovery.
|
|
1464
|
+
// Local souls (local-agents/) are gone with the workspace model: a soul is a member
|
|
1465
|
+
// repository's souls/<name>, so spawn never writes one.
|
|
1466
|
+
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\``);
|
|
1467
|
+
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)");
|
|
1468
|
+
// --name <slug>: the exact, unprefixed instance name (human decision 2026-09-24).
|
|
1469
|
+
const nameFlag = flag("name");
|
|
1470
|
+
if (nameFlag === true) bail("E_BAD_ARGS", "--name needs an instance name (a slug: lowercase letters and digits, single dashes between them)");
|
|
1471
|
+
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>");
|
|
1472
|
+
// The slug rule is checked here too, before any side effect (soul fetch).
|
|
1473
|
+
if (nameFlag !== undefined) { try { explicitInstanceName(String(nameFlag)); } catch (e) { bail(e.code, e.message); throw e; } }
|
|
2404
1474
|
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
1475
|
let root;
|
|
1476
|
+
// A spawn needs a workspace deployment (lead decision c3-1): no oats-local.yaml
|
|
1477
|
+
// in reach is E_LOCAL_MISSING, before anything else is read. The agents root is
|
|
1478
|
+
// then <deployment>/agents; an ambient root (the invoking agent's own
|
|
1479
|
+
// PI_AGENTS_ROOT / OATS_ROOT) never redirects it — a home outside
|
|
1480
|
+
// <deployment>/agents would have no derivable deployment.
|
|
1481
|
+
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); }
|
|
1482
|
+
delete process.env.PI_AGENTS_ROOT; delete process.env.OATS_ROOT;
|
|
1483
|
+
// A deployment without its agents/ root is E_NO_DEPLOYMENT, naming the remedy.
|
|
2406
1484
|
try { root = ensureRoot(dirFlag()); }
|
|
2407
1485
|
catch (e) { bail("E_NO_DEPLOYMENT", e.message || e); throw e; }
|
|
2408
1486
|
const isPreview = args.includes("--preview");
|
|
2409
1487
|
// --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.
|
|
1488
|
+
// readiness take it) — the deployment's one agents root, or E_SOUL_UNKNOWN.
|
|
2412
1489
|
const agentsRootFlag = flag("agents-root");
|
|
2413
1490
|
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
|
-
}
|
|
1491
|
+
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
1492
|
let agent = findAgent(root, name);
|
|
2420
1493
|
// Workspace model: with an oats-local.yaml the soul is ALWAYS discovered over the
|
|
2421
|
-
// remotes and resolved (member = latest state, package = locked
|
|
1494
|
+
// remotes and resolved (member = latest state, package = locked) — never
|
|
2422
1495
|
// "whatever <agents-root>/<name>/soul/ happens to hold": that copy is a per-commit
|
|
2423
1496
|
// cache (ensureWorkspaceSoul refreshes it when the member moved), so a second
|
|
2424
1497
|
// spawn sees the member's CURRENT soul, not the first spawn's. A preview runs the
|
|
2425
|
-
// same read-only discovery+resolution
|
|
2426
|
-
//
|
|
1498
|
+
// same read-only discovery+resolution and writes nothing: it reads the soul from
|
|
1499
|
+
// the per-commit cache, or fetches it to a temporary copy (reported as soulFetched).
|
|
2427
1500
|
const providerPairs = [];
|
|
2428
1501
|
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;
|
|
1502
|
+
let wsPrepared, soulFetched = false, wsSoulUnknown = null, wsDiscovery;
|
|
2431
1503
|
{
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
1504
|
+
let discovery = null;
|
|
1505
|
+
try {
|
|
1506
|
+
const { prepareInstance, ensureWorkspaceSoul, previewWorkspaceSoul, parseProviderFlags, discoverOrStandalone } = await import("../lib/instance-resolution.mjs");
|
|
1507
|
+
const remoteOptions = remoteOptionsFromEnv();
|
|
1508
|
+
const { local } = loadLocal(dirFlag());
|
|
1509
|
+
discovery = wsDiscovery = await discoverOrStandalone(local, { remoteOptions });
|
|
1510
|
+
wsPrepared = await prepareInstance(dirFlag(), name, { spawn: { providers: parseProviderFlags(providerPairs) }, remoteOptions, discovery });
|
|
1511
|
+
const soulName = wsPrepared.soulEntry.name;
|
|
1512
|
+
let soulDir;
|
|
1513
|
+
if (isPreview) {
|
|
1514
|
+
// A preview writes nothing in the deployment: the soul comes from the
|
|
1515
|
+
// per-commit cache when complete, else from a temporary fetch removed at exit.
|
|
1516
|
+
const pv = await previewWorkspaceSoul(wsPrepared, root);
|
|
1517
|
+
process.once("exit", pv.cleanup);
|
|
1518
|
+
soulDir = pv.soulDir; soulFetched = pv.fetched;
|
|
1519
|
+
agent = findAgentAt(root, soulName, soulDir);
|
|
1520
|
+
} else {
|
|
2442
1521
|
const stampFile = join(root, soulName, ".oats-soul-source.json");
|
|
2443
1522
|
const stampBefore = (() => { try { return JSON.parse(readFileSync(stampFile, "utf8")); } catch { return null; } })();
|
|
2444
|
-
|
|
1523
|
+
soulDir = await ensureWorkspaceSoul(wsPrepared, root);
|
|
2445
1524
|
soulFetched = !stampBefore || stampBefore.commit !== wsPrepared.soulEntry.commit || stampBefore.repoKey !== wsPrepared.soulEntry.repoKey;
|
|
2446
1525
|
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
1526
|
}
|
|
2464
|
-
|
|
1527
|
+
if (!agent) bail("E_SOUL_UNKNOWN", `soul "${name}" was fetched to ${shortPath(soulDir)} but is not readable as a soul there`);
|
|
1528
|
+
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" : ""})`);
|
|
1529
|
+
} catch (e) {
|
|
1530
|
+
// Standalone (decisions 10/25): the ONLY package request is the kernel's own
|
|
1531
|
+
// default; when the catalog cannot name it, say so instead of "add it to packages:"
|
|
1532
|
+
// (there is no workspace file to add it to).
|
|
1533
|
+
if (e?.code === "E_PACKAGE_MISSING" && discovery?.standalone === true) {
|
|
1534
|
+
const file = officialCatalogFile();
|
|
1535
|
+
bail(e.code, `${e.details?.capability ?? "oats.core"}: the catalog has no package providing oats.core (OATS_PACKAGE_CATALOG=${file}) — standalone spawns resolve only the kernel's default package from the catalog`, { ...(e.details ?? {}), standalone: true, reason: "no-catalog", catalog: file });
|
|
1536
|
+
}
|
|
1537
|
+
// Not a workspace soul: a capability-defined agent (a module's `agents:`
|
|
1538
|
+
// soul, resolved below from a materialized copy) may still answer to this name.
|
|
1539
|
+
if (e?.code === "E_SOUL_UNKNOWN" && !isPreview) { wsSoulUnknown = e; }
|
|
1540
|
+
else if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details);
|
|
1541
|
+
else throw e;
|
|
1542
|
+
}
|
|
2465
1543
|
}
|
|
2466
1544
|
if (agentsRootFlag !== undefined && !agent) bail("E_SOUL_UNKNOWN", `soul "${name}" is not at agents root ${String(agentsRootFlag)}`);
|
|
2467
1545
|
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) {
|
|
1546
|
+
// Capability agent (lead decision c3 Q1): a prepared spawn of its providing module only.
|
|
1547
|
+
let capabilityPrepared, capabilityPkg = null;
|
|
1548
|
+
if (!agent) {
|
|
2480
1549
|
// Workspace model: the agent is declared by a capability some INSTANCE already
|
|
2481
1550
|
// materialized (the --parent home first, then any home under this root) —
|
|
2482
1551
|
// OKF's memory-harvest worker spawned by a knowledge source, for example.
|
|
@@ -2487,50 +1556,30 @@ async function spawnCmd() {
|
|
|
2487
1556
|
catch (e) { bail(e.code || "E_CAPABILITY_BROKEN", e.message, e.details); }
|
|
2488
1557
|
if (modAgent) {
|
|
2489
1558
|
agent = modAgent;
|
|
2490
|
-
note(`(capability agent: "${name}" from ${modAgent.capability}, materialized in ${shortPath(modAgent._manifestSource)} — fresh soul, instances home
|
|
1559
|
+
note(`(capability agent: "${name}" from ${modAgent.capability}, materialized in ${shortPath(modAgent._manifestSource)} — fresh soul, instances home under ${shortPath(join(root, name, "instances"))})`);
|
|
2491
1560
|
} else {
|
|
2492
|
-
// No instance carries it: resolve from the deployment's LOCK —
|
|
1561
|
+
// No instance carries it: resolve from the deployment's LOCK — a locked
|
|
2493
1562
|
// package whose capability declares agents/<name> is fetched into the
|
|
2494
1563
|
// deployment's module store and read from there.
|
|
2495
1564
|
try {
|
|
2496
1565
|
const { resolvePackageCapabilityAgent } = await import("../lib/instance-resolution.mjs");
|
|
2497
|
-
const hit = await resolvePackageCapabilityAgent(dirFlag(), name, { remoteOptions: remoteOptionsFromEnv(), catalog: (() => { try { return officialPackageCatalog(); } catch { return null; } })() });
|
|
1566
|
+
const hit = await resolvePackageCapabilityAgent(dirFlag(), name, { remoteOptions: remoteOptionsFromEnv(), discovery: wsDiscovery, catalog: (() => { try { return officialPackageCatalog(); } catch { return null; } })() });
|
|
2498
1567
|
if (hit) {
|
|
2499
1568
|
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
|
|
1569
|
+
if (agent) { capabilityPkg = hit; note(`(capability agent: "${name}" from ${hit.capability} — package ${hit.package} v${hit.version}, fetched to ${shortPath(hit.dir)} — fresh soul, instances home under ${shortPath(join(root, name, "instances"))})`); }
|
|
2501
1570
|
}
|
|
2502
1571
|
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
2503
1572
|
}
|
|
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)`);
|
|
1573
|
+
if (agent) {
|
|
1574
|
+
try {
|
|
1575
|
+
const { prepareCapabilityAgent } = await import("../lib/instance-resolution.mjs");
|
|
1576
|
+
capabilityPrepared = await prepareCapabilityAgent(dirFlag(), agent, { discovery: wsDiscovery, pkg: capabilityPkg, remoteOptions: remoteOptionsFromEnv(), catalog: (() => { try { return officialPackageCatalog(); } catch { return null; } })() });
|
|
1577
|
+
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message, e.details); throw e; }
|
|
2516
1578
|
}
|
|
2517
1579
|
}
|
|
1580
|
+
if (!agent && wsSoulUnknown) bail(wsSoulUnknown.code, wsSoulUnknown.message, wsSoulUnknown.details);
|
|
2518
1581
|
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
|
-
}
|
|
1582
|
+
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
1583
|
for (const information of agent.notes || []) note(`[${information.code}] ${information.message}`);
|
|
2535
1584
|
// Lineage is explicit: --relation child|sibling|parent|unrelated anchors the new
|
|
2536
1585
|
// instance to --relative-to <instance>. --parent X is sugar for
|
|
@@ -2558,9 +1607,9 @@ async function spawnCmd() {
|
|
|
2558
1607
|
// NOTE: explicit "unrelated" is passed through to the kernel.
|
|
2559
1608
|
if (relativeTo && relation !== "unrelated") {
|
|
2560
1609
|
// findInstanceHome also sees capability-defined agents' instance homes
|
|
2561
|
-
// (
|
|
1610
|
+
// (<root>/<name>/ without a soul) — e.g. a reviewer passing
|
|
2562
1611
|
// --parent "$OATS_INSTANCE" from a capability agent.
|
|
2563
|
-
if (!findInstanceHome(root, relativeTo)
|
|
1612
|
+
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
1613
|
}
|
|
2565
1614
|
const taskText = flag("task");
|
|
2566
1615
|
if (taskText === true) bail("E_BAD_ARGS", "--task needs a value (use --task-file for long tasks)");
|
|
@@ -2600,7 +1649,7 @@ async function spawnCmd() {
|
|
|
2600
1649
|
} catch (e) { if (e?.code?.startsWith?.("E_")) bail(e.code, e.message); throw e; }
|
|
2601
1650
|
// Workspace model: when this deployment has an oats-local.yaml, the soul's
|
|
2602
1651
|
// capabilities are resolved over the workspace's remotes (member = latest,
|
|
2603
|
-
// package = locked
|
|
1652
|
+
// package = locked) and copied whole into the new home. Without one
|
|
2604
1653
|
// (a bare agents root, tests) the classic soul-directory spawn proceeds.
|
|
2605
1654
|
let prepared;
|
|
2606
1655
|
// Workspace model: a `work: worktree|checkout` soul works IN its member's clone on
|
|
@@ -2608,10 +1657,10 @@ async function spawnCmd() {
|
|
|
2608
1657
|
// convention <deployment>/<member name>. Never an ambient Git checkout around the
|
|
2609
1658
|
// deployment. Resolved once here so a preview sees exactly what the apply would.
|
|
2610
1659
|
let preparedRepo;
|
|
2611
|
-
if (wsPrepared) {
|
|
1660
|
+
if (wsPrepared || capabilityPrepared) {
|
|
2612
1661
|
try {
|
|
2613
1662
|
const { toCapabilityRows, modulesPreview, requireMemberClone } = await import("../lib/instance-resolution.mjs");
|
|
2614
|
-
prepared = wsPrepared;
|
|
1663
|
+
prepared = wsPrepared ?? capabilityPrepared;
|
|
2615
1664
|
prepared.capabilityRows = []; // filled after materialization (paths live in the home); preview uses modulesPreview
|
|
2616
1665
|
prepared.preview = modulesPreview(prepared.resolution, root, agent.name);
|
|
2617
1666
|
prepared.toCapabilityRows = toCapabilityRows;
|
|
@@ -2625,18 +1674,19 @@ async function spawnCmd() {
|
|
|
2625
1674
|
if (flag("base") === true) bail("E_BAD_ARGS", "--base needs a ref");
|
|
2626
1675
|
{ const spawnOpts = {
|
|
2627
1676
|
prepared,
|
|
2628
|
-
purpose: flag("purpose"), task: taskText, taskFile: taskFileFlag, relation, relativeTo, relativeRoot,
|
|
1677
|
+
purpose: flag("purpose"), ...(nameFlag !== undefined ? { name: String(nameFlag) } : {}), task: taskText, taskFile: taskFileFlag, relation, relativeTo, relativeRoot,
|
|
2629
1678
|
...(args.includes("--allow-child-spawns") ? { allowChildSpawns: true } : args.includes("--no-child-spawns") ? { allowChildSpawns: false } : {}),
|
|
2630
1679
|
// Directory execution uses deployment configuration, not an ambient Git
|
|
2631
1680
|
// checkout (especially when invoked via --dir from a source instance).
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
1681
|
+
// An attached instance's repository is its work tree owner's (derived by the kernel).
|
|
1682
|
+
repo: preparedRepo !== undefined ? preparedRepo : ["directory", "attached"].includes(requestedWork || agent.work)
|
|
1683
|
+
? repo : repo || defaultRepo(workspaceOf(root)) || defaultRepo(process.cwd()),
|
|
1684
|
+
work: requestedWork, workDir, harness: harnessFlag(), backend, herdrSocket, yolo, model: flag("model"), branch,
|
|
2635
1685
|
launchConfig: valueFlag("launch-config"),
|
|
2636
1686
|
launch: !args.includes("--no-launch"),
|
|
2637
1687
|
// K6: --preview decides everything and touches nothing; --base <ref>
|
|
2638
1688
|
// selects a worktree's start point; --model @native-default is the
|
|
2639
|
-
// explicit "
|
|
1689
|
+
// explicit "harness's own default" (distinct from omitting --model).
|
|
2640
1690
|
...(args.includes("--preview") ? { preview: true, subject: { soul: name, agentsRoot: agentsRootFlag !== undefined ? String(agentsRootFlag) : null, dir: flag("dir") !== undefined && flag("dir") !== true ? String(flag("dir")) : null } } : {}),
|
|
2641
1691
|
...(flag("base") !== undefined && flag("base") !== true ? { baseRef: flag("base") } : {}),
|
|
2642
1692
|
// A confirmed preview binds this apply (K6b): drift → E_DECISION_STALE, nothing created.
|
|
@@ -2644,13 +1694,13 @@ async function spawnCmd() {
|
|
|
2644
1694
|
// K6c: with --idempotency-key, a retry of the SAME confirmed decision replays the recorded home instead of spawning twice.
|
|
2645
1695
|
...(flag("idempotency-key") !== undefined && flag("idempotency-key") !== true ? { idempotencyKey: String(flag("idempotency-key")) } : {}),
|
|
2646
1696
|
};
|
|
2647
|
-
r =
|
|
1697
|
+
r = await spawnInstanceAsync(root, agent, spawnOpts); }
|
|
2648
1698
|
if (args.includes("--preview")) {
|
|
2649
|
-
// A workspace preview may have fetched the soul's SOURCE
|
|
2650
|
-
// (
|
|
1699
|
+
// A workspace preview may have fetched the soul's SOURCE to a temporary copy
|
|
1700
|
+
// (the deployment's cache had no entry for its commit): the result says so.
|
|
2651
1701
|
if (prepared) r.soulFetched = soulFetched;
|
|
2652
1702
|
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)}` : ""})
|
|
1703
|
+
console.log(`preview ${r.agent} → ${r.instance} (${r.work}${r.branch ? `, branch ${r.branch} from ${r.base.ref}@${r.base.oid.slice(0, 12)}` : ""}) harness ${r.harness}${r.model ? ` model ${r.model}` : ` (${r.modelSource})`}; nothing was created${soulFetched ? " (the soul source was fetched to a temporary copy, not kept)" : ""}`);
|
|
2654
1704
|
return;
|
|
2655
1705
|
}
|
|
2656
1706
|
} catch (e) {
|
|
@@ -2660,9 +1710,9 @@ async function spawnCmd() {
|
|
|
2660
1710
|
// document the message already names. The shared boundary renders it.
|
|
2661
1711
|
if (TYPED_CLI_FAILURES.has(e?.code)) throw e;
|
|
2662
1712
|
// A launch refusal (configuration, executable, environment reference,
|
|
2663
|
-
// model,
|
|
1713
|
+
// model, harness) is a fact about the selection, not a spawn-mechanism
|
|
2664
1714
|
// failure: it keeps its own code so a GUI can act on it.
|
|
2665
|
-
if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^
|
|
1715
|
+
if (typeof e?.code === "string" && /^E_LAUNCH_|^E_MODEL_UNKNOWN$|^E_UNSUPPORTED_HARNESS$/.test(e.code)) { bail(e.code, e.message); throw e; }
|
|
2666
1716
|
// An unmet declared requirement is a fact about the soul's configuration
|
|
2667
1717
|
// (with a remedy), not a spawn-mechanism failure: keep its code and details.
|
|
2668
1718
|
if (e?.code === "E_REQUIREMENT_INACTIVE") { bail(e.code, e.message, { soul: e.soul, capabilities: e.capabilities, context: e.context, remedy: e.remedy }); throw e; }
|
|
@@ -2672,6 +1722,8 @@ async function spawnCmd() {
|
|
|
2672
1722
|
if (e?.code === "E_DECISION_STALE") { bail(e.code, e.message, { decision: e.decision }); throw e; }
|
|
2673
1723
|
if (e?.code === "E_IDEMPOTENCY_CONFLICT") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
|
|
2674
1724
|
if (e?.code === "E_PLACEMENT_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home }); throw e; }
|
|
1725
|
+
if (e?.code === "E_INSTANCE_NAME_TAKEN") { bail(e.code, e.message, { instance: e.instance, home: e.home ?? null, ...(e.session ? { session: e.session } : {}) }); throw e; }
|
|
1726
|
+
if (e?.code === "E_INSTANCE_NAME_INVALID") { bail(e.code, e.message); throw e; }
|
|
2675
1727
|
if (e?.code === "E_SPAWN_INCOMPLETE") { bail(e.code, e.message, { instance: e.instance, home: e.home, launched: e.launched }); throw e; }
|
|
2676
1728
|
bail(["E_BAD_ARGS", "E_RELATIVE_AMBIGUOUS"].includes(e.code) ? e.code : "E_SPAWN_FAILED", e.message || e); throw e;
|
|
2677
1729
|
}
|
|
@@ -2695,7 +1747,7 @@ async function spawnCmd() {
|
|
|
2695
1747
|
instance: r.instance, agent: r.agent, home: r.home, work: r.work,
|
|
2696
1748
|
branch: r.branch || null, launched: r.launched, warnings: r.warnings || [],
|
|
2697
1749
|
...(wakeSchedule ? { wakeSchedule } : {}), ...(wakeScheduleError ? { wakeScheduleError } : {}),
|
|
2698
|
-
tmux: r.tmux || null, repo: r.repo || null,
|
|
1750
|
+
tmux: r.tmux || null, repo: r.repo || null, harness: r.harness || null,
|
|
2699
1751
|
model: r.model || null, parent: r.parentInstance || null,
|
|
2700
1752
|
sibling: r.siblingInstance || null, relation: r.relation || null,
|
|
2701
1753
|
spawnOrigin: r.spawnOrigin, attach: r.attach,
|
|
@@ -2734,7 +1786,7 @@ function retireCmd() {
|
|
|
2734
1786
|
console.log(` defaults: retain worktree ${plan.defaults.retainWorktree}, delete branch ${plan.defaults.deleteBranch}, stop children ${plan.defaults.stopChildren}`);
|
|
2735
1787
|
for (const n of plan.notes) console.log(` note: ${n}`);
|
|
2736
1788
|
return;
|
|
2737
|
-
} catch (e) { return args.includes("--json") ? jsonFail(e.code || "E_LIFECYCLE_FAILED", e.message, e.candidates ? { candidates: e.candidates } :
|
|
1789
|
+
} 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
1790
|
}
|
|
2739
1791
|
// The calling instance knows its own home: self-retire never needs to
|
|
2740
1792
|
// disambiguate a same-named twin by hand.
|
|
@@ -2742,13 +1794,7 @@ function retireCmd() {
|
|
|
2742
1794
|
const isSelf = process.env.PI_AGENT_INSTANCE === name || process.env.OATS_INSTANCE === name;
|
|
2743
1795
|
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
1796
|
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
|
-
}
|
|
1797
|
+
const root = ensureRoot(dirFlag());
|
|
2752
1798
|
const retiringHome = homeFlag || findInstanceHome(root, name);
|
|
2753
1799
|
// K3: a GUI-driven Remove carries the plan revision it showed and an
|
|
2754
1800
|
// idempotency key. The revision is revalidated against a fresh plan
|
|
@@ -2765,7 +1811,7 @@ function retireCmd() {
|
|
|
2765
1811
|
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
1812
|
for (const a of listAgents(root)) if (replay(join(a._dir, "instances"))) return;
|
|
2767
1813
|
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); }
|
|
1814
|
+
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
1815
|
replayPath = join(dirname(fresh.home), `.oats-retire-receipt.${idemKey}.json`);
|
|
2770
1816
|
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
1817
|
// The plan promised: recorded children are STOPPED first (bounded, never
|
|
@@ -2780,7 +1826,9 @@ function retireCmd() {
|
|
|
2780
1826
|
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
1827
|
expectedBranch = fresh.facts.work.observed ? fresh.facts.work.branch : undefined;
|
|
2782
1828
|
}
|
|
2783
|
-
|
|
1829
|
+
let r;
|
|
1830
|
+
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 } : {}) }); }
|
|
1831
|
+
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
1832
|
if (childrenStopped) r.childrenStopped = childrenStopped;
|
|
2785
1833
|
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
1834
|
// A retired home's wake jobs are forgotten (definitions only; nothing is
|
|
@@ -2792,7 +1840,7 @@ function retireCmd() {
|
|
|
2792
1840
|
if (r.deferred) {
|
|
2793
1841
|
if (args.includes("--json")) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
2794
1842
|
console.log(`Retirement of ${r.retired} (agent ${r.agent}) is ${r.alreadyScheduled ? "already " : ""}scheduled — say any goodbyes now.`);
|
|
2795
|
-
console.log(` in ~${r.completesInSec}s a detached completion quiesces this
|
|
1843
|
+
console.log(` in ~${r.completesInSec}s a detached completion quiesces this harness (that is what ends this window), preserves work, runs retire hooks and removes the home`);
|
|
2796
1844
|
console.log(` if the completion fails, this window stays, the failure shows in \`oats status\` and at ${shortPath(r.resultPath)}, and \`oats retire ${r.retired}\` retries it`);
|
|
2797
1845
|
return;
|
|
2798
1846
|
}
|
|
@@ -2818,8 +1866,10 @@ function retireCmd() {
|
|
|
2818
1866
|
// which is most of the harm of deleting it. Name the classes and the path.
|
|
2819
1867
|
for (const recovery of r.workRecoveries || (r.workRecovery ? [r.workRecovery] : [])) {
|
|
2820
1868
|
console.log(`Work that was not committed has been preserved: ${recovery.classes.join(", ")}`);
|
|
2821
|
-
console.log(` ${recovery.path}`);
|
|
1869
|
+
console.log(` ${recovery.path}${typeof recovery.bytes === "number" ? ` (${formatBytes(recovery.bytes)})` : ""}`);
|
|
1870
|
+
for (const line of preservedOutputLines(recovery)) console.log(line);
|
|
2822
1871
|
}
|
|
1872
|
+
for (const w of r.warnings || []) console.log(` WARNING: ${w}`);
|
|
2823
1873
|
if (isSelf) console.log("This window dies in ~8s — say any goodbyes now.");
|
|
2824
1874
|
}
|
|
2825
1875
|
|
|
@@ -2829,9 +1879,11 @@ function retireCmd() {
|
|
|
2829
1879
|
function scheduleCmd() {
|
|
2830
1880
|
const sub = args[1];
|
|
2831
1881
|
const id = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
|
|
2832
|
-
// One schedule-owning scope for a directory:
|
|
2833
|
-
//
|
|
2834
|
-
|
|
1882
|
+
// One schedule-owning scope for a directory: its deployment (the directory
|
|
1883
|
+
// holding oats-local.yaml), resolved when a subcommand needs it — inside the
|
|
1884
|
+
// try, so no deployment in reach is a typed refusal (E_LOCAL_MISSING).
|
|
1885
|
+
let scope;
|
|
1886
|
+
const ws = () => (scope ??= scheduleScopeOf(dirFlag()));
|
|
2835
1887
|
const io = { hostStatus: () => hostUnitStatus() };
|
|
2836
1888
|
const out = (result) => { if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2)); };
|
|
2837
1889
|
const readSpec = () => {
|
|
@@ -2845,41 +1897,41 @@ function scheduleCmd() {
|
|
|
2845
1897
|
const needId = () => { if (!id) throw scheduleError("E_BAD_ARGS", `oats schedule ${sub} <id>`); return id; };
|
|
2846
1898
|
try {
|
|
2847
1899
|
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") }));
|
|
1900
|
+
case "list": return out(listSchedules(ws(), io));
|
|
1901
|
+
case "show": return out({ schedule: describeSchedule(ws(), needId(), io) });
|
|
1902
|
+
case "add": { const spec = readSpec(); if (id && spec.id === undefined) spec.id = id; if (id && spec.id !== id) throw scheduleError("E_SCHEDULE_INVALID", `id ${JSON.stringify(spec.id)} in the file does not match ${JSON.stringify(id)}`, { field: "id" }); return out({ schedule: addSchedule(ws(), spec, io) }); }
|
|
1903
|
+
case "update": return out({ schedule: updateSchedule(ws(), needId(), readSpec(), io) });
|
|
1904
|
+
case "enable": return out({ schedule: setScheduleEnabled(ws(), needId(), true, io) });
|
|
1905
|
+
case "disable": return out({ schedule: setScheduleEnabled(ws(), needId(), false, io) });
|
|
1906
|
+
case "run": return out(runScheduleNow(ws(), needId(), { io, force: args.includes("--force") }));
|
|
1907
|
+
case "remove": return out(removeSchedule(ws(), needId(), { force: args.includes("--force") }));
|
|
1908
|
+
case "reconcile": return out(reconcileSchedule(ws(), needId(), { io, clear: args.includes("--clear") }));
|
|
2857
1909
|
case "tick": {
|
|
2858
1910
|
const dryRun = args.includes("--dry-run");
|
|
2859
1911
|
if (args.includes("--host")) return out(tickHost({ io, dryRun }));
|
|
2860
1912
|
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) });
|
|
1913
|
+
const considered = withHostLock(() => tickWorkspace(ws(), { io, reg, wsList: reg.workspaces.includes(ws()) ? reg.workspaces : [...reg.workspaces, ws()], dryRun }));
|
|
1914
|
+
return out({ tickedAt: new Date().toISOString(), considered, scheduler: schedulerStatus(ws(), io) });
|
|
2863
1915
|
}
|
|
2864
1916
|
case "host": {
|
|
2865
1917
|
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) });
|
|
1918
|
+
if (op === "install") { registerWorkspace(ws()); installHostUnit(); return out({ scheduler: schedulerStatus(ws(), io) }); }
|
|
1919
|
+
if (op === "uninstall") { unregisterWorkspace(ws()); if (!readRegistry().workspaces.length) uninstallHostUnit(); return out({ scheduler: schedulerStatus(ws(), io) }); }
|
|
1920
|
+
if (op === "status") return out({ scheduler: schedulerStatus(ws(), io) });
|
|
2869
1921
|
throw scheduleError("E_BAD_ARGS", "oats schedule host install|uninstall|status");
|
|
2870
1922
|
}
|
|
2871
1923
|
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
1924
|
}
|
|
2873
1925
|
} catch (e) {
|
|
2874
1926
|
// 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]]));
|
|
1927
|
+
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
1928
|
if (JSON_MODE) jsonFail(e.code || "E_SCHEDULE_FAILED", e.message, Object.keys(details).length ? details : undefined); else die(e.message);
|
|
2877
1929
|
}
|
|
2878
1930
|
}
|
|
2879
1931
|
|
|
2880
1932
|
async function sessionCmd() {
|
|
2881
1933
|
try {
|
|
2882
|
-
if (flag("native-record") !== undefined) throw Object.assign(new Error("--native-record outcome inspection
|
|
1934
|
+
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
1935
|
const home = flag("home");
|
|
2884
1936
|
let result;
|
|
2885
1937
|
if (args[1] === "attach") {
|
|
@@ -2888,16 +1940,16 @@ async function sessionCmd() {
|
|
|
2888
1940
|
return;
|
|
2889
1941
|
}
|
|
2890
1942
|
if (args[1] === "inspect") result = inspectInstanceSession(home);
|
|
2891
|
-
else if (args[1] === "recompose")
|
|
1943
|
+
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
1944
|
else if (args[1] === "start" || args[1] === "restart") {
|
|
2893
1945
|
const bad = (msg) => { throw Object.assign(new Error(msg), { code: "E_BAD_ARGS" }); };
|
|
2894
1946
|
const model = flag("model");
|
|
2895
1947
|
if (model === true) bad("--model needs a model id; omit it to keep the recorded model");
|
|
2896
1948
|
const launchConfig = flag("launch-config");
|
|
2897
1949
|
if (launchConfig === true) bad("--launch-config needs a configuration name, or none");
|
|
2898
|
-
const
|
|
2899
|
-
if (
|
|
2900
|
-
const opts = { model: model || undefined, launchConfig,
|
|
1950
|
+
const harness = harnessFlag();
|
|
1951
|
+
if (harness === true || (harness !== undefined && !LAUNCH_HARNESSES.includes(harness))) bad(`--harness must be one of ${LAUNCH_HARNESSES.join(", ")}`);
|
|
1952
|
+
const opts = { model: model || undefined, launchConfig, harness, yolo: yoloFlag(), env: process.env, ...(await homeLiveTeams(home)) };
|
|
2901
1953
|
if (args[1] === "restart") {
|
|
2902
1954
|
const grace = flag("stop-grace");
|
|
2903
1955
|
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; }
|
|
@@ -2920,9 +1972,9 @@ async function sessionCmd() {
|
|
|
2920
1972
|
const file = flag("file");
|
|
2921
1973
|
if (!file || file === true) throw Object.assign(new Error("session upload needs --file <local path>"), { code: "E_BAD_ARGS" });
|
|
2922
1974
|
result = uploadAttachment({ file, home: home === true ? undefined : home });
|
|
2923
|
-
} 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] [--
|
|
1975
|
+
} 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] [--harness pi|claude|codex] [--yolo|--no-yolo] [--stop-grace seconds] [--name file] [--file path] [--json]"), { code: "E_BAD_ARGS" });
|
|
2924
1976
|
if (JSON_MODE) jsonOk(result); else console.log(JSON.stringify(result, null, 2));
|
|
2925
|
-
} catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message); }
|
|
1977
|
+
} catch (e) { cmdFail(e.code || "E_SESSION_FAILED", e.message, e.details); }
|
|
2926
1978
|
}
|
|
2927
1979
|
|
|
2928
1980
|
async function paneCmd() {
|
|
@@ -2935,8 +1987,7 @@ async function paneCmd() {
|
|
|
2935
1987
|
* (docs/design/2026-09-23-simplified-workspace-model.md §4): writes
|
|
2936
1988
|
* `<dir>/oats-local.yaml` naming the workspace, creates `<dir>/agents/` (the
|
|
2937
1989
|
* 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
|
|
1990
|
+
* remotes, confirm membership, resolve `packages:`, write `oats-lock.json`. Nothing is installed, no soul
|
|
2940
1991
|
* is created, nothing is spawned, no `oats-config.yaml` is written: the member
|
|
2941
1992
|
* clones and the operator expert are the operator's next steps, printed here. */
|
|
2942
1993
|
async function onboardCmd() {
|
|
@@ -3043,14 +2094,14 @@ async function onboardCmd() {
|
|
|
3043
2094
|
const hostIsMember = members.some((m) => m.key === synced.discovery.key);
|
|
3044
2095
|
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
2096
|
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);
|
|
2097
|
+
if (JSON_MODE) { jsonOk(result); return; }
|
|
3047
2098
|
|
|
3048
2099
|
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
2100
|
printSyncReport(ctx, synced);
|
|
3050
2101
|
console.log(`
|
|
3051
2102
|
This directory (${shortPath(dir)}) is your deployment — any layout works; it now holds what the kernel needs:
|
|
3052
2103
|
├── oats-local.yaml which workspace this machine realizes (+ host settings, disabled souls)
|
|
3053
|
-
├── oats-lock.json exact commit + integrity
|
|
2104
|
+
├── oats-lock.json exact commit + integrity per package
|
|
3054
2105
|
└── agents/ instance homes, each self-contained
|
|
3055
2106
|
Member clones live wherever you keep them (here, or anywhere named in oats-local.yaml clones:).
|
|
3056
2107
|
|
|
@@ -3060,9 +2111,8 @@ Next:
|
|
|
3060
2111
|
2. Check who may read the host: ${synced.discovery.key}${hostIsMember ? " is itself a member" : " is a dedicated host"}. The workspace file
|
|
3061
2112
|
names every member, so if any member is private the host must be a private repo that is not
|
|
3062
2113
|
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;
|
|
2114
|
+
3. ${setupExpert ? "Spawn the operator expert to guide the rest (souls, teams, provider settings):" : "No soul named oats-operator-expert is listed here —"}
|
|
2115
|
+
${spawnHint ?? anySoulHint}`);
|
|
3066
2116
|
}
|
|
3067
2117
|
|
|
3068
2118
|
/** The clone URL of a member row: what the remote observed (from the workspace's members: refs;
|
|
@@ -3075,45 +2125,6 @@ function memberUrlOf(discovery, key) {
|
|
|
3075
2125
|
return null;
|
|
3076
2126
|
}
|
|
3077
2127
|
|
|
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
2128
|
// ---------- capability command dispatch ----------
|
|
3118
2129
|
/**
|
|
3119
2130
|
* oats <namespace> <command> [args…] — run a command an active capability
|
|
@@ -3127,7 +2138,7 @@ function createCmd() {
|
|
|
3127
2138
|
* namespace's capability into <deployment>/.oats/modules/<cap>@<commit12>/
|
|
3128
2139
|
* and run THAT copy with the soul's merged payload (lib/operator-dispatch.mjs;
|
|
3129
2140
|
* contracts doc, "Post-0.25.0 clarifications");
|
|
3130
|
-
* - otherwise the
|
|
2141
|
+
* - otherwise no namespace is active (the help fallthrough answers).
|
|
3131
2142
|
*/
|
|
3132
2143
|
async function capabilityCommand() {
|
|
3133
2144
|
// JSON-aware boundary: in --json mode every dispatch failure — inactive or
|
|
@@ -3159,64 +2170,79 @@ async function capabilityCommand() {
|
|
|
3159
2170
|
throw e;
|
|
3160
2171
|
}
|
|
3161
2172
|
if (!hit) return NOT_DISPATCHED;
|
|
3162
|
-
|
|
3163
|
-
|
|
2173
|
+
// The same team/workspace facts a spawn hook receives (lead decision c3-7).
|
|
2174
|
+
const teamCtx = teamEnv(resolvedFromPrepared(hit.prepared, hit.deployment));
|
|
2175
|
+
// No home, so no recorded soul: the soul's per-commit copy is OATS_SOUL when a spawn
|
|
2176
|
+
// already fetched exactly this commit; otherwise the command gets none (never ambient).
|
|
2177
|
+
const cachedSoul = hit.soul?.commit ? join(hit.deployment, "agents", hit.soul.name, "souls", String(hit.soul.commit).slice(0, 12)) : null;
|
|
2178
|
+
return runManifestCommand({ capability: hit.module.name, ...hit.manifest }, hit.settings, teamCtx, hit.ensureTree, cachedSoul && existsSync(join(cachedSoul, "soul.yaml")) ? realpathSync(cachedSoul) : undefined);
|
|
3164
2179
|
}
|
|
3165
2180
|
|
|
3166
2181
|
async function dispatch() {
|
|
3167
2182
|
let activeIds;
|
|
3168
2183
|
let context = process.cwd();
|
|
3169
|
-
let teamCtx;
|
|
3170
|
-
|
|
2184
|
+
let teamCtx, homeMeta, homeTeamCtx;
|
|
2185
|
+
// OATS_INSTANCE_HOME is the canonical identity; the older names still count.
|
|
2186
|
+
const instanceHome = process.env.OATS_INSTANCE_HOME || process.env.PI_AGENT_HOME || process.env.OATS_HOME;
|
|
3171
2187
|
const metaFile = instanceHome && join(instanceHome, "instance.json");
|
|
3172
2188
|
// Capability-id keyed — never answer for `constructor`/`toString`. Belt and
|
|
3173
2189
|
// braces: the ids come from instance.json, which spawn wrote from resolved
|
|
3174
2190
|
// manifests. Null-prototype because the dispatcher indexes it with the
|
|
3175
2191
|
// namespace the operator typed on the command line.
|
|
3176
2192
|
let capSettings = Object.create(null);
|
|
3177
|
-
let instanceModules = false;
|
|
3178
2193
|
let deployment = null;
|
|
2194
|
+
let soulDir;
|
|
3179
2195
|
try {
|
|
3180
2196
|
if (metaFile && existsSync(metaFile)) {
|
|
3181
2197
|
const meta = JSON.parse(readFileSync(metaFile, "utf8"));
|
|
3182
|
-
instanceModules = !!(meta.modules && typeof meta.modules === "object");
|
|
3183
2198
|
activeIds = (meta.capabilities || []).map((c) => c.id);
|
|
3184
2199
|
for (const c of meta.capabilities || []) capSettings[c.id] = c.settings || {};
|
|
2200
|
+
// Workspace-model homes only (lead decision c3 Q2).
|
|
2201
|
+
if (isCapturedHome(meta)) { const e = capturedHomeRefusal(instanceHome, "nothing was dispatched"); bail(e.code, e.message, e.details); }
|
|
2202
|
+
if (!isWorkspaceHome(meta)) { const e = preWorkspaceHome(instanceHome, "nothing was dispatched"); bail(e.code, e.message); }
|
|
3185
2203
|
context = meta.repo || context;
|
|
3186
|
-
|
|
3187
|
-
//
|
|
3188
|
-
|
|
2204
|
+
soulDir = instanceSoulDir(instanceHome, meta);
|
|
2205
|
+
// The team/workspace facts the home recorded at spawn, as its hooks got them, with the
|
|
2206
|
+
// recorded eligible teams (OATS_TEAMS_SOURCE=recorded). Only the home's MESSAGING module
|
|
2207
|
+
// gets them live (below): its team verbs (join/leave/teams) must see what the workspace
|
|
2208
|
+
// allows now, and no other command pays a remote read for them.
|
|
2209
|
+
const ws = meta.workspace && typeof meta.workspace === "object" ? meta.workspace : {};
|
|
2210
|
+
const messaging = (meta.capabilities || []).find((c) => c.layer === "messaging")?.id;
|
|
2211
|
+
homeMeta = { meta, messaging };
|
|
2212
|
+
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 });
|
|
2213
|
+
teamCtx = homeTeamCtx(meta.teams, "recorded");
|
|
3189
2214
|
} else {
|
|
3190
2215
|
// Not inside a home: a v2 deployment (oats-local.yaml in reach) resolves
|
|
3191
2216
|
// through the workspace, exactly as a spawn of --soul would (below).
|
|
3192
2217
|
try { const { deploymentOf } = await import("../lib/operator-dispatch.mjs"); deployment = deploymentOf(context); }
|
|
3193
2218
|
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
|
-
}
|
|
2219
|
+
// No home and no deployment in reach: no capability namespace is active.
|
|
2220
|
+
if (!deployment) return NOT_DISPATCHED;
|
|
3200
2221
|
}
|
|
3201
2222
|
} catch (e) { bail("E_CONFIG_BROKEN", e.message || e); throw e; }
|
|
3202
2223
|
if (deployment) return operatorDispatch();
|
|
3203
2224
|
// Workspace model: an instance's own materialized modules are the command
|
|
3204
2225
|
// namespaces available to it (instance.json.modules → <home>/.oats/modules).
|
|
3205
|
-
const mans = Object.values(capabilityManifests(
|
|
2226
|
+
const mans = Object.values(capabilityManifests(instanceHome)).filter((m) => m.command === cmd && m.commands);
|
|
3206
2227
|
if (!mans.length) return NOT_DISPATCHED;
|
|
3207
2228
|
if (mans.length > 1) bail("E_DUPLICATE_NAMESPACE", `duplicate operational command namespace "${cmd}": ${mans.map((m) => m.capability).join(", ")}`);
|
|
3208
2229
|
const m = mans[0];
|
|
3209
2230
|
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
|
|
2231
|
+
const trust = capabilityTrust(m);
|
|
3211
2232
|
if (!trust.trusted) bail("E_CAPABILITY_BLOCKED", `${m.capability} executable command is blocked: ${trust.reason}`);
|
|
3212
|
-
|
|
2233
|
+
if (homeMeta && m.capability === homeMeta.messaging) {
|
|
2234
|
+
const { liveTeams } = await import("../lib/instance-resolution.mjs");
|
|
2235
|
+
const live = await liveTeams(instanceHome, homeMeta.meta, { remoteOptions: remoteOptionsFromEnv() });
|
|
2236
|
+
teamCtx = homeTeamCtx(live.teams, live.source);
|
|
2237
|
+
}
|
|
2238
|
+
return runManifestCommand(m, capSettings[m.capability] || {}, teamCtx, () => m._dir, soulDir);
|
|
3213
2239
|
}
|
|
3214
2240
|
|
|
3215
2241
|
/** Help / unknown-command / spec validation / exec — shared by every context.
|
|
3216
2242
|
* `m` is the manifest (with `capability`; `_dir` may be absent until `ensureDir`
|
|
3217
2243
|
* resolves the directory holding the executable — the operator branch fetches
|
|
3218
2244
|
* the module tree only when a command is actually going to run). */
|
|
3219
|
-
async function runManifestCommand(m, settings, teamCtx, ensureDir) {
|
|
2245
|
+
async function runManifestCommand(m, settings, teamCtx, ensureDir, soulDir) {
|
|
3220
2246
|
const sub = args[1];
|
|
3221
2247
|
const cmds = Object.keys(m.commands);
|
|
3222
2248
|
// `oats <ns> --help` and `oats <ns> <cmd> --help` answer from the manifest
|
|
@@ -3250,8 +2276,11 @@ async function capabilityCommand() {
|
|
|
3250
2276
|
try { abs = capabilityExecutablePath(withDir, script); }
|
|
3251
2277
|
catch (e) { bail("E_CAPABILITY_BROKEN", e.message); }
|
|
3252
2278
|
if (!abs) bail("E_CAPABILITY_BROKEN", `${cmd} ${sub}: script not found (${join(dir, script)})`);
|
|
2279
|
+
// OATS_SOUL is the recorded soul or nothing: an ambient value inherited from the
|
|
2280
|
+
// invoking process names some other soul (a coordinator's own), never this one.
|
|
2281
|
+
const { OATS_SOUL: _ambientSoul, ...inherited } = process.env;
|
|
3253
2282
|
const r = spawnSync("node", [abs, ...rest, ...args.slice(2)], { stdio: "inherit", env: {
|
|
3254
|
-
...
|
|
2283
|
+
...inherited, OATS_CAPABILITY: m.capability,
|
|
3255
2284
|
// Package-runtime boundary: dispatched commands receive the active
|
|
3256
2285
|
// capability's EFFECTIVE settings (instance snapshot, resolved context, or
|
|
3257
2286
|
// the soul's merged payload on operator-level dispatch), same contract as
|
|
@@ -3262,7 +2291,10 @@ async function capabilityCommand() {
|
|
|
3262
2291
|
// canonical absolute executable of THIS CLI; official consumers execFile
|
|
3263
2292
|
// it directly and never resolve `oats` from PATH or a shell.
|
|
3264
2293
|
OATS_CLI_BIN: CLI_BIN,
|
|
3265
|
-
|
|
2294
|
+
...teamEnv(null), ...(teamCtx || {}),
|
|
2295
|
+
// The soul the command acts for (an instance home's recorded soul directory):
|
|
2296
|
+
// homes carry no soul link, so providers read it here.
|
|
2297
|
+
...(soulDir ? { OATS_SOUL: soulDir } : {}),
|
|
3266
2298
|
} });
|
|
3267
2299
|
// Child never ran (spawn error): nothing reached stdout — keep the envelope contract.
|
|
3268
2300
|
if (r.error) bail("E_CAPABILITY_BROKEN", `oats ${cmd} ${sub}: ${r.error.message || r.error}`);
|
|
@@ -3270,52 +2302,6 @@ async function capabilityCommand() {
|
|
|
3270
2302
|
}
|
|
3271
2303
|
}
|
|
3272
2304
|
|
|
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
2305
|
|
|
3320
2306
|
// ---------- update ----------
|
|
3321
2307
|
function updateCmd() {
|
|
@@ -3370,7 +2356,7 @@ function versionCmd() {
|
|
|
3370
2356
|
// Phase B: `instance-modules` and `spawn-provider-payload` are advertised only once spawn
|
|
3371
2357
|
// runs on resolve/materialize (contract §6); a feature the binary does not implement is
|
|
3372
2358
|
// never listed.
|
|
3373
|
-
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1,
|
|
2359
|
+
console.log(JSON.stringify({ schemaVersion: 1, name: "@awebai/oats", version: OATS_VERSION, desktopApi: 1, harnesses: ["pi", "claude", "codex"], sessionBackends: ["tmux", "herdr"], launchOptions: ["yolo"], remote: ["spawn", "retire", "status", "session", "session-start", "session-restart", "launch-config", "roster", "harvest", "schedule", "session-upload", "operations"], features: ["retire-home", "session-start", "session-restart", "launch-config", "schedule", "session-upload", "operations", "instance-git", "instance-git-remote", "souls-declarations", "lifecycle-plans", "retire-retention", "readiness", "spawn-preview", "instance-events", "instance-events-2", "schedule-history", "schedule-read-2", "spawn-preview-2", "spawn-idempotency", "spawn-idempotency-2", "spawn-apply-2", "workspace-v2", "instance-modules", "spawn-provider-payload", "served-identity", "packages-no-approval", "spawn-name", "settings-origins", "teams", "settings-declared", "capabilities-private", "layers-from", "harness"], workspaceApi: 2, instanceGitApi: 1, spawnApplyApi: 1, soulsApi: 2, lifecycleApi: 1, readinessApi: 2, spawnPreviewApi: 2, eventsApi: 2, scheduleHistoryApi: 3, scheduleApi: SCHEDULE_API, operationsApi: 2 }));
|
|
3374
2360
|
return;
|
|
3375
2361
|
}
|
|
3376
2362
|
console.log(`@awebai/oats ${OATS_VERSION} (desktop API v1)`);
|
|
@@ -3508,14 +2494,14 @@ async function serverRouteCmd() {
|
|
|
3508
2494
|
// The operations contract addresses an exact member context on the host,
|
|
3509
2495
|
// so its explicit --dir travels; every other routed command takes its
|
|
3510
2496
|
// scope from the registration.
|
|
3511
|
-
const explicitScopeOk = ["inspect", "operation", "
|
|
2497
|
+
const explicitScopeOk = ["inspect", "operation", "launch-config"].includes(cmd);
|
|
3512
2498
|
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
2499
|
if (cmd === "launch-config") {
|
|
3514
2500
|
const action = args[1];
|
|
3515
2501
|
const value = (name) => { const v = flag(name); if (v === true) bail("E_BAD_ARGS", `--${name} needs a value`); return v; };
|
|
3516
2502
|
if (!["list", "set", "remove", "preview"].includes(action)) bail("E_BAD_ARGS", "launch-config --server supports list, set, remove and preview");
|
|
3517
2503
|
const options = { action, name: args[2], context: value("dir"), home: value("home"), instance: value("instance"), soul: value("soul"), agentsRoot: value("agents-root") };
|
|
3518
|
-
if (action === "preview") Object.assign(options, { launchConfig: value("launch-config"),
|
|
2504
|
+
if (action === "preview") Object.assign(options, { launchConfig: value("launch-config"), harness: harnessFlag(value), model: value("model"), yolo: yoloFlag() });
|
|
3519
2505
|
if (action === "set") {
|
|
3520
2506
|
const file = value("file");
|
|
3521
2507
|
if (!file) bail("E_BAD_ARGS", "launch-config set needs --file <local JSON file> (or - for stdin)");
|
|
@@ -3534,7 +2520,7 @@ async function serverRouteCmd() {
|
|
|
3534
2520
|
let out;
|
|
3535
2521
|
try { out = launchConfigRemote(id, options); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3536
2522
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3537
|
-
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2523
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3538
2524
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "launch configuration request failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
3539
2525
|
console.log(JSON.stringify(out.envelope.result, null, 2));
|
|
3540
2526
|
return;
|
|
@@ -3551,7 +2537,7 @@ async function serverRouteCmd() {
|
|
|
3551
2537
|
let routed;
|
|
3552
2538
|
try { routed = routeCommand(id, "harvest", [inst]); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3553
2539
|
if (routed.stderr?.trim()) process.stderr.write(routed.stderr.endsWith("\n") ? routed.stderr : routed.stderr + "\n");
|
|
3554
|
-
if (JSON_MODE) { console.log(JSON.stringify(routed.envelope, null, 2)); if (!routed.envelope.ok) process.exit(1); return; }
|
|
2540
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(routed.envelope), null, 2)); if (!routed.envelope.ok) process.exit(1); return; }
|
|
3555
2541
|
if (!routed.envelope.ok) die(`${id}: ${routed.envelope.error?.message || "harvest failed"} (${routed.envelope.error?.code || "E_REMOTE"})`);
|
|
3556
2542
|
const hr = routed.envelope.result;
|
|
3557
2543
|
console.log(`Harvest on ${id} for ${inst}: ${hr.harvest}${hr.reason ? ` (${hr.reason})` : ""}${hr.instance && hr.harvest === "spawned" ? ` — harvester ${hr.instance}` : ""}`);
|
|
@@ -3579,7 +2565,7 @@ async function serverRouteCmd() {
|
|
|
3579
2565
|
let out;
|
|
3580
2566
|
try { out = scheduleRemote(id, rest); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3581
2567
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3582
|
-
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2568
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3583
2569
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "schedule command failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
3584
2570
|
console.log(JSON.stringify(out.envelope.result, null, 2));
|
|
3585
2571
|
return;
|
|
@@ -3592,7 +2578,7 @@ async function serverRouteCmd() {
|
|
|
3592
2578
|
let out;
|
|
3593
2579
|
try { out = inspectRemote(id, addr); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3594
2580
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3595
|
-
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2581
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3596
2582
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "inspect failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
3597
2583
|
const r = out.envelope.result;
|
|
3598
2584
|
console.log(`${r.instance || r.home} on ${id}: ${r.present ? `present, ${r.state || "unknown"}` : "not present"}${r.backend ? ` (${r.backend})` : ""}`);
|
|
@@ -3600,12 +2586,12 @@ async function serverRouteCmd() {
|
|
|
3600
2586
|
}
|
|
3601
2587
|
if (args[1] === "start" || args[1] === "restart") {
|
|
3602
2588
|
const value = (name) => { const v = flag(name); if (v === true) bail("E_BAD_ARGS", `--${name} needs a value`); return v; };
|
|
3603
|
-
const choices = { ...addr, model: value("model"), launchConfig: value("launch-config"),
|
|
2589
|
+
const choices = { ...addr, model: value("model"), launchConfig: value("launch-config"), harness: harnessFlag(value), yolo: yoloFlag() };
|
|
3604
2590
|
if (flag("stop-grace") !== undefined) bail("E_BAD_ARGS", "--stop-grace is currently supported on the execution host; omit it to use the remote restart's default wait");
|
|
3605
2591
|
let out;
|
|
3606
2592
|
try { out = (args[1] === "restart" ? restartRemote : startRemote)(id, choices); } catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3607
2593
|
if (out.stderr?.trim()) process.stderr.write(out.stderr.endsWith("\n") ? out.stderr : out.stderr + "\n");
|
|
3608
|
-
if (JSON_MODE) { console.log(JSON.stringify(out.envelope, null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
2594
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(out.envelope), null, 2)); if (!out.envelope.ok) process.exit(1); return; }
|
|
3609
2595
|
if (!out.envelope.ok) die(`${id}: ${out.envelope.error?.message || "start failed"} (${out.envelope.error?.code || "E_REMOTE"})`);
|
|
3610
2596
|
const r = out.envelope.result;
|
|
3611
2597
|
console.log(`Started ${r.instance || r.home} on ${id} (${r.backend}${r.model ? `, model ${r.model}` : ""}, ${r.reused === "pane" ? "in its existing pane" : r.reused === "adopted" ? "adopted the pending session" : "new window"})`);
|
|
@@ -3635,7 +2621,6 @@ async function serverRouteCmd() {
|
|
|
3635
2621
|
// local --task-file is read here and travels as --task text, since the
|
|
3636
2622
|
// remote cannot read this machine's files.
|
|
3637
2623
|
const rest = [];
|
|
3638
|
-
let routedInput;
|
|
3639
2624
|
for (let i = 1; i < args.length; i++) {
|
|
3640
2625
|
const a = args[i];
|
|
3641
2626
|
if (a === "--server") { i++; continue; }
|
|
@@ -3665,27 +2650,14 @@ async function serverRouteCmd() {
|
|
|
3665
2650
|
rest.push("--wake-message", readFileSync(f, "utf8"));
|
|
3666
2651
|
continue;
|
|
3667
2652
|
}
|
|
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
2653
|
rest.push(a);
|
|
3682
2654
|
}
|
|
3683
2655
|
let routed;
|
|
3684
|
-
try { routed = routeCommand(id, cmd, rest
|
|
2656
|
+
try { routed = routeCommand(id, cmd, rest); }
|
|
3685
2657
|
catch (e) { bail(e.code || "E_SSH", e.message); }
|
|
3686
2658
|
const { envelope, stderr } = routed;
|
|
3687
2659
|
if (stderr && stderr.trim()) process.stderr.write(stderr.endsWith("\n") ? stderr : stderr + "\n");
|
|
3688
|
-
if (JSON_MODE) { console.log(JSON.stringify(envelope, null, 2)); if (!envelope.ok || envelope.result?.rollbackIncomplete) process.exit(1); return; }
|
|
2660
|
+
if (JSON_MODE) { console.log(JSON.stringify(withLocalWarnings(envelope), null, 2)); if (!envelope.ok || envelope.result?.rollbackIncomplete) process.exit(1); return; }
|
|
3689
2661
|
if (!envelope.ok && !(cmd === "retire" && envelope.result)) die(`${id}: ${envelope.error?.message || "remote command failed"} (${envelope.error?.code || "E_REMOTE"})`);
|
|
3690
2662
|
const r = envelope.result;
|
|
3691
2663
|
const target = r.target || {};
|
|
@@ -3706,7 +2678,8 @@ async function serverRouteCmd() {
|
|
|
3706
2678
|
console.log(`Retired ${r.retired} on ${id}${r.deferred ? " (deferred completion scheduled there)" : ""}${r.rollbackIncomplete ? " — cleanup INCOMPLETE on the server, home retained there" : ""}`);
|
|
3707
2679
|
for (const recovery of r.workRecoveries || (r.workRecovery ? [r.workRecovery] : [])) {
|
|
3708
2680
|
console.log(`Work that was not committed has been preserved on ${target.sshHost}: ${(recovery.classes || []).join(", ")}`);
|
|
3709
|
-
console.log(` ${recovery.path}`);
|
|
2681
|
+
console.log(` ${recovery.path}${typeof recovery.bytes === "number" ? ` (${formatBytes(recovery.bytes)})` : ""}`);
|
|
2682
|
+
for (const line of preservedOutputLines(recovery)) console.log(line);
|
|
3710
2683
|
}
|
|
3711
2684
|
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
2685
|
} else {
|
|
@@ -3735,70 +2708,44 @@ async function serverRouteCmd() {
|
|
|
3735
2708
|
// blame` pointing at the commit that last changed each command.
|
|
3736
2709
|
const TYPED_CLI_FAILURES = new Set(["unsafe-config-key", "unsafe-config-value"]);
|
|
3737
2710
|
/** 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: "
|
|
2711
|
+
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
2712
|
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
|
-
}
|
|
2713
|
+
// The captured/portable path was removed in 0.26 (lead decisions on (e), D2/D3):
|
|
2714
|
+
// its selectors and an inherited captured context are refused, never quietly
|
|
2715
|
+
// resolved against the current context instead. `version` answers regardless:
|
|
2716
|
+
// host protocol negotiation describes this executable.
|
|
2717
|
+
{
|
|
2718
|
+
const end = args.indexOf("--"), head = end < 0 ? args : args.slice(0, end);
|
|
2719
|
+
const selector = head.find((a) => /^--(deployment|resolution|artifact-set)(=|$)/.test(a));
|
|
2720
|
+
const refuse = (message, details) => { if (JSON_MODE) jsonFail("E_UNSUPPORTED_MODE", message, details); die(message); };
|
|
2721
|
+
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] });
|
|
2722
|
+
const inherited = ["OATS_RESOLUTION", "OATS_DEPLOYMENT"].filter((k) => process.env[k]);
|
|
2723
|
+
if (inherited.length && cmd !== "version") refuse(`this environment carries a captured context (${inherited.join(", ")}): the captured/portable path was removed in 0.26, and nothing is run against the current context in its place — retire the captured home and re-spawn it from the deployment`, { inherited });
|
|
2724
|
+
if (cmd === "inspect" && head.some((a) => a === "--request" || a.startsWith("--request="))) {
|
|
2725
|
+
const message = "oats inspect --request (portable onboarding inspection) is gone (the captured/portable path was removed in 0.26); use `oats onboard` / `oats sync` to set up a workspace and `oats spawn <soul> --preview` to see what a spawn would resolve";
|
|
2726
|
+
if (JSON_MODE) jsonFail("E_UNKNOWN_COMMAND", message, { removed: "inspect --request", replacement: "oats onboard / oats sync; oats spawn --preview" });
|
|
2727
|
+
die(message);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
if (cmd === "onboard") {
|
|
3771
2731
|
if (args.includes("--help") || args.includes("-h")) { if (JSON_MODE) jsonOk({ command: cmd, usage: usageLinesFor(cmd) }); else usageFor(cmd); process.exit(0); }
|
|
3772
2732
|
await onboardCmd();
|
|
3773
2733
|
}
|
|
3774
2734
|
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
2735
|
// `--help`/`-h` anywhere after a kernel command prints that command's usage
|
|
3788
2736
|
// and exits 0 BEFORE any dispatch: a fresh operator inspects --help before
|
|
3789
2737
|
// using a command, and `install --help` once ran the bare restore while
|
|
3790
2738
|
// `okf harvest --help` spawned a harvester (BeadHub, 2026-09-05).
|
|
3791
2739
|
const wantsHelp = args.slice(1).some((a) => a === "--help" || a === "-h");
|
|
3792
2740
|
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", "
|
|
2741
|
+
if (flag("server") !== undefined && ["spawn", "retire", "status", "session", "okf", "schedule", "inspect", "operation", "launch-config"].includes(cmd)) await serverRouteCmd();
|
|
3794
2742
|
else if (cmd === "server") serverCmd();
|
|
3795
|
-
else if (cmd === "inspect") inspectCmd();
|
|
3796
|
-
else if (cmd === "operation") operationCmd();
|
|
3797
|
-
else if (cmd === "soul") await soulCmd();
|
|
2743
|
+
else if (cmd === "inspect") await inspectCmd();
|
|
2744
|
+
else if (cmd === "operation") await operationCmd();
|
|
3798
2745
|
else if (cmd === "launch-config") await launchConfigCmd();
|
|
3799
2746
|
else if (cmd === "doctor") {
|
|
3800
2747
|
const doctorDir = args[1] && !args[1].startsWith("--") ? args[1] : undefined;
|
|
3801
|
-
args.includes("--json") ? doctorJson(doctorDir) : doctor(doctorDir);
|
|
2748
|
+
await (args.includes("--json") ? doctorJson(doctorDir) : doctor(doctorDir));
|
|
3802
2749
|
}
|
|
3803
2750
|
else if (cmd === "update") {
|
|
3804
2751
|
// `oats update <package>` left with the installed tier (packages are pinned in
|
|
@@ -3807,8 +2754,7 @@ else if (cmd === "update") {
|
|
|
3807
2754
|
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
2755
|
updateCmd();
|
|
3809
2756
|
}
|
|
3810
|
-
else if (cmd === "
|
|
3811
|
-
else if (cmd === "readiness") readinessCmd();
|
|
2757
|
+
else if (cmd === "readiness") await readinessCmd();
|
|
3812
2758
|
else if (cmd === "instance") instanceCmd();
|
|
3813
2759
|
else if (cmd === "root") console.log(resolve(new URL("..", import.meta.url).pathname));
|
|
3814
2760
|
else if (cmd === "sync") await syncCmd();
|
|
@@ -3824,7 +2770,6 @@ else if (cmd === "session") await sessionCmd();
|
|
|
3824
2770
|
else if (cmd === "schedule") scheduleCmd();
|
|
3825
2771
|
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
2772
|
else if (cmd === "retire") retireCmd();
|
|
3827
|
-
else if (cmd === "create") createCmd();
|
|
3828
2773
|
else if (cmd === "capture" || cmd === "recall" || cmd === "setup") await recordCmd(cmd);
|
|
3829
2774
|
else if (cmd === "experimental") await experimentalCmd();
|
|
3830
2775
|
// `!HELP_WORDS.has(cmd)`: usage NEVER depends on deployment state. `help` is a
|
|
@@ -3874,7 +2819,6 @@ Usage:
|
|
|
3874
2819
|
oats version [--json] kernel version; --json emits the
|
|
3875
2820
|
Desktop CLI API v1 probe payload
|
|
3876
2821
|
oats status [--json] agents, souls, running instances
|
|
3877
|
-
oats status --team [--json] whole-team roster across the team scope's repos
|
|
3878
2822
|
oats server add <id> --ssh <alias> register another machine's OATS (OpenSSH alias,
|
|
3879
2823
|
--workspace </abs/path> [--oats <p>] remote workspace, remote oats path; no keys stored;
|
|
3880
2824
|
[--path <dir:dir>] --path = dirs prepended to the remote PATH, e.g. ~/.local/bin)
|
|
@@ -3898,24 +2842,19 @@ Usage:
|
|
|
3898
2842
|
oats session start --server <id> start a stopped remote instance in its existing home
|
|
3899
2843
|
--instance <name> | --home <abs> over its saved route; the server must advertise
|
|
3900
2844
|
[--model <m>] [--json] session-start (oats 0.22.9 or later)
|
|
3901
|
-
oats inspect|operation
|
|
2845
|
+
oats inspect|operation --server <id> the same commands on a registered server over its
|
|
3902
2846
|
... [--dir <remote member>] [--home <abs>] saved route (an explicit --dir travels as is; a --home
|
|
3903
2847
|
is its own context; else the registered workspace);
|
|
3904
|
-
|
|
3905
|
-
server must advertise operations (oats 0.22.16 or later)
|
|
2848
|
+
the server must advertise operations (oats 0.22.16 or later)
|
|
3906
2849
|
oats session upload --server <id> copy a local file into a remote instance's private
|
|
3907
2850
|
--instance <name> | --home <abs> attachments over its saved route (bytes stream on
|
|
3908
2851
|
--file <path> [--json] ssh stdin; sha256 verified); the server must
|
|
3909
2852
|
advertise session-upload (oats 0.22.13 or later)
|
|
3910
2853
|
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
|
-
|
|
2854
|
+
[--json] and agents/, then runs the oats sync path (lock v3)
|
|
2855
|
+
and prints the
|
|
3913
2856
|
next steps (clone members you work IN, spawn
|
|
3914
2857
|
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
2858
|
oats session inspect|input|attach --home <absolute-home> [--text-file <path>] [--json]
|
|
3920
2859
|
oats schedule list|show <id>|add <id> --file <spec.json>|update <id> --file <spec.json>
|
|
3921
2860
|
enable|disable|run|remove|reconcile <id> workspace-scoped, host-owned schedules (spawn,
|
|
@@ -3931,7 +2870,7 @@ Usage:
|
|
|
3931
2870
|
<file> [--json] upload's remote half)
|
|
3932
2871
|
oats session start --home <absolute-home> start a STOPPED instance again in its existing home
|
|
3933
2872
|
[--model m] [--launch-config n|none] (recorded recipe as is; a selection re-resolves it
|
|
3934
|
-
[--
|
|
2873
|
+
[--harness r] [--yolo|--no-yolo] against the scope; a named configuration is a unit)
|
|
3935
2874
|
oats session restart --home <abs-home> stop the running harness (SIGTERM, bounded wait,
|
|
3936
2875
|
[same flags] [--stop-grace <s>] never escalated) and start it again in place under
|
|
3937
2876
|
the same lock; a stop that is not observed is
|
|
@@ -3940,45 +2879,41 @@ Usage:
|
|
|
3940
2879
|
spawn hooks); --model replaces the recorded model
|
|
3941
2880
|
for this and later starts; a live harness is refused
|
|
3942
2881
|
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;
|
|
2882
|
+
[--purpose <slug>] [--repo <r>] = scaffold only); the agent is a workspace
|
|
2883
|
+
[--parent <instance>] soul or a capability-defined agent
|
|
3946
2884
|
[--relation child|sibling|parent|unrelated] --relation + --relative-to anchor the
|
|
3947
2885
|
[--relative-to <instance>] new instance to an existing one; --parent X
|
|
3948
2886
|
[--relative-root <agents-root>] disambiguates same-named team anchors
|
|
3949
2887
|
[--work worktree|checkout|attached|workspace|directory] = sugar for --relative-to X --relation
|
|
3950
|
-
[--work-dir <owner-work>] [--
|
|
3951
|
-
[--
|
|
3952
|
-
with team: declared, unknown
|
|
2888
|
+
[--work-dir <owner-work>] [--harness pi|claude|codex] [--backend tmux|herdr] [--herdr-socket <path>] [--yolo|--no-yolo] [--model <m>] [--branch <b>] child (default: unrelated, top-level)
|
|
2889
|
+
[--no-launch] [--json]
|
|
2890
|
+
with team: declared, unknown souls
|
|
3953
2891
|
resolve across the team scope's repos
|
|
3954
2892
|
directory: owned home/work, config context may
|
|
3955
2893
|
be non-Git; rejects --work-dir and --branch
|
|
2894
|
+
[--name <slug>] the exact instance name (no <agent>- prefix;
|
|
2895
|
+
not with --purpose); must be a slug, not a
|
|
2896
|
+
soul name, and unused in the deployment
|
|
3956
2897
|
oats retire <instance> [--force] retire an instance (window, hooks,
|
|
3957
2898
|
[--self] [--delete-branch] worktree, home); --self = retire the
|
|
3958
2899
|
[--keep-dir] [--json] CALLING instance: the window dies, then
|
|
3959
2900
|
a detached external retirement runs
|
|
3960
2901
|
oats inspect [--dir <scope>] [--soul <name> one authoritative JSON answer for a GUI: souls
|
|
3961
|
-
[--agents-root <abs>]] [--home <abs>] (
|
|
2902
|
+
[--agents-root <abs>]] [--home <abs>] (harness defaults, editability, instructions),
|
|
3962
2903
|
[--json] installed capabilities with health, effective
|
|
3963
2904
|
layer bindings and activation, declared
|
|
3964
2905
|
operations with availability; --home answers the
|
|
3965
|
-
running home's
|
|
3966
|
-
from the
|
|
3967
|
-
oats operation run <layer>:<name> run an operation the
|
|
2906
|
+
running home's recorded modules and their drift
|
|
2907
|
+
from the deployment
|
|
2908
|
+
oats operation run <layer>:<name> run an operation the soul's core capability for that
|
|
3968
2909
|
(--home <abs> | --soul <name> [--dir <d>]) layer declares (knowledge:harvest, knowledge:
|
|
3969
|
-
[--arg k=v ...] [--json] inspect ...): resolved from the home's
|
|
3970
|
-
|
|
2910
|
+
[--arg k=v ...] [--json] inspect ...): resolved from the home's recorded
|
|
2911
|
+
modules or the deployment's soul, trust checked, the provider's
|
|
3971
2912
|
own command run in the home or scope, envelope
|
|
3972
2913
|
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
2914
|
oats launch-config list [--dir <scope> named launch configurations effective at a scope,
|
|
3980
2915
|
| --home <abs> | --soul <name>] a home's recorded context or a soul's own scope:
|
|
3981
|
-
[--agents-root <abs>] [--json]
|
|
2916
|
+
[--agents-root <abs>] [--json] harness, executable, args, env (values redacted,
|
|
3982
2917
|
references shown), model, yolo; the closest
|
|
3983
2918
|
declaring scope provides the whole entry
|
|
3984
2919
|
oats launch-config set <name> --file <j> declare or replace one at this scope from a JSON
|
|
@@ -3986,32 +2921,32 @@ Usage:
|
|
|
3986
2921
|
--keep-env copies the effective definition's env)
|
|
3987
2922
|
oats launch-config remove <name> remove this scope's declaration; an ancestor's,
|
|
3988
2923
|
[--dir <scope>] [--json] if any, becomes effective again
|
|
3989
|
-
oats launch-config preview what a start would run: resolved
|
|
2924
|
+
oats launch-config preview what a start would run: resolved harness, model,
|
|
3990
2925
|
(--home <abs> | --soul <name>) yolo, executable, argv, environment (redacted),
|
|
3991
2926
|
[--launch-config <name>|none] command and preflight; read-only, nothing
|
|
3992
|
-
[--
|
|
3993
|
-
[--yolo | --no-yolo] --json a disagreeing --
|
|
2927
|
+
[--harness r] [--model m] started; a named configuration is a unit, so
|
|
2928
|
+
[--yolo | --no-yolo] --json a disagreeing --harness is refused
|
|
3994
2929
|
oats doctor [dir] [--soul <name>] [--json] resolved targets, trust, requirements;
|
|
3995
2930
|
--soul shows final composed AGENTS.md
|
|
3996
2931
|
oats update [--check] [--yes] check npm for a newer kernel+pi bridge and
|
|
3997
2932
|
optionally run the update; then run oats doctor
|
|
3998
2933
|
oats sync [--dir <d>] [--json] workspace model v2: observe the workspace named by
|
|
3999
|
-
|
|
2934
|
+
oats-local.yaml over its Git remote, confirm every
|
|
4000
2935
|
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)
|
|
2936
|
+
packages: to exact commits + integrity, write
|
|
2937
|
+
oats-lock.json (v3) and report the diff. No approval
|
|
2938
|
+
step: declaring a package in packages: is the trust
|
|
2939
|
+
decision (--approve is E_BAD_ARGS)
|
|
4008
2940
|
oats package add <id> <version|git:<repo>@<ref>> edit packages: in oats-workspace.yaml when the
|
|
4009
2941
|
| remove <id> [--dir <d>] workspace repo is the current checkout; otherwise
|
|
4010
2942
|
print the line to add (the file travels through Git)
|
|
4011
2943
|
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
|
-
|
|
2944
|
+
cannot-read / backlink-elsewhere), locked packages
|
|
2945
|
+
oats capabilities [--dir <d>] [--json] every capability of every confirmed member (a
|
|
2946
|
+
private one is listed as repo-owned: usable only by
|
|
2947
|
+
its own repo's souls) + the locked packages
|
|
2948
|
+
oats souls [--dir <d>] [--json] every soul of every confirmed member + external souls
|
|
2949
|
+
(souls have no private mode), with origin
|
|
4015
2950
|
(member <key> @ <commit> | package <id> v<ver>) and team
|
|
4016
2951
|
oats instance git <instance> [--home <abs>] [--dir <d>] [--json]
|
|
4017
2952
|
read-only Git observation of the instance's work
|
|
@@ -4020,10 +2955,6 @@ Usage:
|
|
|
4020
2955
|
oats instance diff <instance> --file <id> --revision <rev> [--index-revision <rev>] [--home <abs>] [--dir <d>] [--json]
|
|
4021
2956
|
bounded diff of one observed file; refuses when
|
|
4022
2957
|
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
2958
|
oats instance events <instance> [--limit <n>] [--since <iso>] [--json]
|
|
4028
2959
|
typed lifecycle events (spawned, launched, stopped,
|
|
4029
2960
|
restarted, retired, worktree-retained…) written by
|
|
@@ -4034,14 +2965,15 @@ Usage:
|
|
|
4034
2965
|
oats instance stop <instance> --apply --plan-revision <rev> --idempotency-key <key>
|
|
4035
2966
|
quiesce (SIGTERM, bounded, never escalated),
|
|
4036
2967
|
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
|
|
2968
|
+
oats readiness (--soul <n> | --home <abs>) [--policy] [--json]
|
|
2969
|
+
readinessApi 2 for a soul or an instance home:
|
|
2970
|
+
installed | configured | member | providers, each
|
|
4042
2971
|
pass|fail|unknown|not-applicable with items and
|
|
4043
|
-
remedies;
|
|
4044
|
-
|
|
2972
|
+
remedies; providers relays each bound provider's
|
|
2973
|
+
own check ({status, problems, warnings});
|
|
2974
|
+
--policy: enforced child-spawn / worktree policy
|
|
2975
|
+
with origins (captured homes refuse: the
|
|
2976
|
+
captured/portable path was removed in 0.26)
|
|
4045
2977
|
oats retire <instance> --plan [--json] what Remove would touch, with retention defaults
|
|
4046
2978
|
oats retire <instance> [--plan-revision <rev> --idempotency-key <key>] [--discard-worktree] [--delete-branch]
|
|
4047
2979
|
with a plan revision: refuses E_PLAN_STALE (fresh plan
|
|
@@ -4050,8 +2982,6 @@ Usage:
|
|
|
4050
2982
|
<workspace>/.agents/worktrees/<repo>/<branch>)
|
|
4051
2983
|
unless discarded; --delete-branch deletes the
|
|
4052
2984
|
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
2985
|
oats root print this package's install root
|
|
4056
2986
|
(adapters resolve the kernel from it)
|
|
4057
2987
|
|
|
@@ -4069,43 +2999,6 @@ The turn record (core — every conversation captured, searchable, replicated):
|
|
|
4069
2999
|
design, repo checkout only; see
|
|
4070
3000
|
packages/experimental/README.md
|
|
4071
3001
|
|
|
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
3002
|
oats <namespace> <command> [args…] run an operational command only when its
|
|
4110
3003
|
capability is active (e.g. oats okf harvest)
|
|
4111
3004
|
|