@awebai/oats 0.29.3 → 0.30.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 +12 -6
- package/bin/oats.mjs +203 -54
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +538 -204
- package/capabilities/oats-aweb/injects/aweb.md +1 -1
- package/capabilities/oats-aweb/lib/binding-wire.mjs +31 -22
- package/capabilities/oats-aweb/oats.json +5 -12
- package/capabilities/oats-aweb/skills/VENDORED.md +4 -4
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +1 -1
- package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +83 -13
- package/capabilities/oats-code-review/injects/reviewer.md +26 -0
- package/capabilities/oats-code-review/oats.json +16 -0
- package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +66 -0
- package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +30 -0
- package/capabilities/oats-code-review/skills/security-review/SKILL.md +56 -0
- package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +34 -0
- package/capabilities/oats-developer/injects/developer.md +38 -0
- package/capabilities/oats-developer/oats.json +17 -0
- package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +43 -0
- package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +47 -0
- package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +65 -0
- package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +37 -0
- package/capabilities/oats-developer/skills/worktrees/SKILL.md +36 -0
- package/capabilities/oats-engineering-expert/injects/expert.md +37 -0
- package/capabilities/oats-engineering-expert/oats.json +17 -0
- package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +37 -0
- package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +52 -0
- package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +50 -0
- package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +53 -0
- package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +49 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +16 -9
- package/capabilities/oats-okf/lib/binding-wire.mjs +47 -15
- package/capabilities/oats-okf/lib/config.mjs +2 -1
- package/capabilities/oats-okf/lib/consult.mjs +26 -4
- package/capabilities/oats-okf/lib/harvest-switch.mjs +16 -3
- package/capabilities/oats-okf/lib/inspection.mjs +26 -7
- package/capabilities/oats-okf/lib/io.mjs +9 -1
- package/capabilities/oats-okf/lib/sources.mjs +34 -3
- package/capabilities/oats-okf/lib/stores.mjs +8 -6
- package/capabilities/oats-okf/lib/worker.mjs +7 -17
- package/capabilities/oats-okf/oats.json +6 -3
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +2 -2
- package/capabilities/oats-okf-harvest/oats.json +3 -3
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +6 -6
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +37 -16
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +1 -1
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +6 -1
- package/capabilities/oats-okf-maintenance/oats.json +2 -2
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +17 -2
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +11 -23
- package/capabilities/oats-workspace-experts/injects/oats-experts.md +26 -0
- package/capabilities/oats-workspace-experts/oats.json +9 -0
- package/docs/capabilities.md +160 -171
- package/docs/capability-manifest.schema.json +7 -10
- package/docs/configuration.md +213 -64
- package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
- package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
- package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
- package/docs/design/2026-09-27-team-model-v2.md +116 -0
- package/docs/design/2026-09-28-automations-trust.md +38 -0
- package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
- package/docs/design/HISTORY.md +65 -0
- package/docs/design/README.md +23 -54
- package/docs/desktop-cli-api.md +1787 -1777
- package/docs/desktop.md +30 -91
- package/docs/execution-targets.md +146 -292
- package/docs/first-team.md +31 -17
- package/docs/implementation.md +76 -288
- package/docs/integrations.md +118 -320
- package/docs/knowledge-capability-authoring.md +25 -52
- package/docs/knowledge-reference/acceptance.md +3 -3
- package/docs/knowledge-reference/adoption.md +1 -1
- package/docs/knowledge-reference/harvester.md +2 -2
- package/docs/knowledge-reference/package-craft.md +3 -3
- package/docs/knowledge-reference/provider-mapping.md +3 -6
- package/docs/knowledge-reference/reader-capture.md +3 -3
- package/docs/knowledge-theory.md +62 -166
- package/docs/knowledge.md +225 -404
- package/docs/layers.md +42 -97
- package/docs/oats-local.schema.json +58 -5
- package/docs/oats-membership.schema.json +1 -8
- package/docs/oats-package.schema.json +5 -5
- package/docs/oats-workspace.schema.json +8 -22
- package/docs/official-catalog.md +25 -28
- package/docs/packages.md +45 -63
- package/docs/plans/0.30-close-out.md +61 -0
- package/docs/release-lane.md +77 -0
- package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
- package/docs/release-notes/v0.19.0.md +48 -147
- package/docs/release-notes/v0.19.1.md +2 -3
- package/docs/release-notes/v0.19.3.md +2 -15
- package/docs/release-notes/v0.20.0.md +0 -15
- package/docs/release-notes/v0.22.0.md +71 -138
- package/docs/release-notes/v0.22.1.md +42 -90
- package/docs/release-notes/v0.22.10.md +1 -1
- package/docs/release-notes/v0.22.11.md +1 -47
- package/docs/release-notes/v0.22.12.md +4 -13
- package/docs/release-notes/v0.22.13.md +1 -42
- package/docs/release-notes/v0.22.14.md +3 -11
- package/docs/release-notes/v0.22.15.md +1 -46
- package/docs/release-notes/v0.22.16.md +6 -8
- package/docs/release-notes/v0.22.18.md +1 -99
- package/docs/release-notes/v0.22.19.md +3 -14
- package/docs/release-notes/v0.22.2.md +6 -15
- package/docs/release-notes/v0.22.3.md +0 -1
- package/docs/release-notes/v0.22.4.md +1 -14
- package/docs/release-notes/v0.22.5.md +2 -12
- package/docs/release-notes/v0.22.6.md +0 -3
- package/docs/release-notes/v0.23.0.md +9 -25
- package/docs/release-notes/v0.23.1.md +9 -25
- package/docs/release-notes/v0.23.2.md +2 -4
- package/docs/release-notes/v0.24.0.md +56 -97
- package/docs/release-notes/v0.24.1.md +7 -11
- package/docs/release-notes/v0.24.10.md +34 -45
- package/docs/release-notes/v0.24.11.md +12 -20
- package/docs/release-notes/v0.24.12.md +35 -48
- package/docs/release-notes/v0.24.13.md +34 -41
- package/docs/release-notes/v0.24.2.md +9 -13
- package/docs/release-notes/v0.24.3.md +7 -11
- package/docs/release-notes/v0.24.4.md +6 -6
- package/docs/release-notes/v0.24.5.md +6 -10
- package/docs/release-notes/v0.24.6.md +2 -5
- package/docs/release-notes/v0.24.7.md +46 -75
- package/docs/release-notes/v0.24.8.md +58 -96
- package/docs/release-notes/v0.24.9.md +38 -54
- package/docs/release-notes/v0.25.0.md +59 -76
- package/docs/release-notes/v0.25.1.md +57 -81
- package/docs/release-notes/v0.25.2.md +51 -70
- package/docs/release-notes/v0.25.3.md +11 -13
- package/docs/release-notes/v0.25.4.md +9 -13
- package/docs/release-notes/v0.25.5.md +3 -5
- package/docs/release-notes/v0.25.6.md +20 -29
- package/docs/release-notes/v0.25.7.md +5 -7
- package/docs/release-notes/v0.25.8.md +26 -39
- package/docs/release-notes/v0.26.0.md +175 -646
- package/docs/release-notes/v0.27.0.md +4 -5
- package/docs/release-notes/v0.27.1.md +4 -6
- package/docs/release-notes/v0.27.2.md +1 -1
- package/docs/release-notes/v0.28.0.md +57 -124
- package/docs/release-notes/v0.29.0.md +89 -208
- package/docs/release-notes/v0.29.1.md +1 -1
- package/docs/release-notes/v0.29.2.md +3 -4
- package/docs/release-notes/v0.29.4.md +90 -0
- package/docs/release-notes/v0.30.0.md +205 -0
- package/docs/schedules.md +280 -349
- package/docs/servers.md +99 -117
- package/docs/soul.schema.json +2 -9
- package/docs/souls-and-instances.md +145 -158
- package/docs/workspaces.md +132 -215
- package/lib/automations.mjs +28 -6
- package/lib/core.mjs +226 -74
- package/lib/instance-events.mjs +1 -1
- package/lib/instance-inspect.mjs +109 -34
- package/lib/instance-lifecycle.mjs +14 -1
- package/lib/instance-resolution.mjs +26 -27
- package/lib/launch-preference.mjs +87 -0
- package/lib/materialize.mjs +3 -3
- package/lib/packages.mjs +2 -5
- package/lib/resolve.mjs +29 -87
- package/lib/schedule.mjs +32 -18
- package/lib/teams-verbs.mjs +195 -0
- package/lib/teams.mjs +190 -0
- package/lib/triggers.mjs +53 -17
- package/lib/workspace.mjs +54 -147
- package/package-catalog.json +9 -15
- package/package.json +1 -1
- package/skills/oats-getting-started/SKILL.md +25 -13
- package/capabilities/oats-review/injects/review.md +0 -69
- package/capabilities/oats-review/oats.json +0 -10
- package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
- package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
- package/docs/conventions.md +0 -90
- package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
- package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
- package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
- package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
- package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
- package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
- package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
- package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
- package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
- package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
- package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
- package/docs/design/2026-09-15-captured-dispatch.md +0 -127
- package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
- package/docs/design/2026-09-15-package-preparation.md +0 -100
- package/docs/design/2026-09-15-portable-data-contract.md +0 -121
- package/docs/design/2026-09-15-portable-declarations.md +0 -189
- package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
- package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
- package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
- package/docs/design/2026-09-15-source-observation.md +0 -119
- package/docs/design/2026-09-16-captured-admission.md +0 -77
- package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
- package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
- package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
- package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
- package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
- package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
- package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
- package/docs/design/2026-09-16-portable-onboarding.md +0 -179
- package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
- package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
- package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
- package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
- package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
- package/docs/design/2026-09-17-captured-native-start.md +0 -58
- package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
- package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
- package/docs/design/2026-09-17-public-captured-start.md +0 -108
- package/docs/design/2026-09-17-public-prepare-request.md +0 -90
- package/docs/design/2026-09-18-captured-pi-host.md +0 -205
- package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
- package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
- package/docs/design/2026-09-20-redesign-program-board.md +0 -142
- package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
- package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
- package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
- package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
- package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
- package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
- package/docs/design/2026-09-24-phase-d-plan.md +0 -305
- package/docs/design/2026-09-25-teams-contract.md +0 -258
- package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
- package/docs/design/desktop-ux-plan.md +0 -362
- package/docs/design/launch-configurations.md +0 -168
- package/docs/design/okf-mirror-provenance.md +0 -105
- package/docs/design/operations-contract.md +0 -141
- package/docs/oats-member.schema.json +0 -38
- package/skills/integration-authoring/SKILL.md +0 -84
- package/skills/oats-support/SKILL.md +0 -79
- package/skills/skill-craft/SKILL.md +0 -109
- package/skills/soul-craft/SKILL.md +0 -116
package/lib/instance-inspect.mjs
CHANGED
|
@@ -14,10 +14,12 @@ import { spawnSync } from "node:child_process";
|
|
|
14
14
|
import { accessSync, constants as fsConstants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
15
15
|
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { capabilityManifests, instanceSoulDir, manifestOperations, parseYamlNested, servedIdentityOf, teamEnv, upgradeHomeMeta, withConfigFile } from "./core.mjs";
|
|
18
|
-
import { agentDirOf, discoverOrStandalone, findSoulEntry, liveTeams, prepareInstance } from "./instance-resolution.mjs";
|
|
17
|
+
import { capabilityManifests, homeLaunchLayers, instanceSoulDir, launchConfigsAt, launchReportFor, manifestOperations, parseYamlNested, servedIdentityOf, teamEnv, upgradeHomeMeta, withConfigFile } from "./core.mjs";
|
|
18
|
+
import { agentDirOf, discoverOrStandalone, ensureWorkspaceSoul, findSoulEntry, liveTeams, prepareInstance } from "./instance-resolution.mjs";
|
|
19
19
|
import { declaredSettings } from "./capability-contract.mjs";
|
|
20
|
-
import { kernelCompatibility
|
|
20
|
+
import { kernelCompatibility } from "./resolve.mjs";
|
|
21
|
+
import { recordedTeams, reportRows, soulKeyOf, soulTeams, teamModel, teamProblems } from "./teams.mjs";
|
|
22
|
+
import { launchLayers, launchReport } from "./launch-preference.mjs";
|
|
21
23
|
import { loadLocal } from "./workspace.mjs";
|
|
22
24
|
import { ensureModuleTree } from "./operator-dispatch.mjs";
|
|
23
25
|
/** Bounds of a provider check's answer (bytes, nesting, entries), as the released binding wire set them. */
|
|
@@ -34,7 +36,9 @@ const real = (p) => { try { return realpathSync(p); } catch { return resolve(p);
|
|
|
34
36
|
|
|
35
37
|
/** A resolution refusal that is a readiness FACT (the soul cannot resolve as the
|
|
36
38
|
* lock stands), not a failure to answer: reported as a failing `installed` item. */
|
|
37
|
-
export const RESOLUTION_FACTS = new Set(["E_PACKAGE_MISSING", "E_PACKAGE_INTEGRITY", "E_CAPABILITY_MISSING", "E_LOCK_SCHEMA", "E_REQUIREMENT_INACTIVE"]);
|
|
39
|
+
export const RESOLUTION_FACTS = new Set(["E_PACKAGE_MISSING", "E_PACKAGE_INTEGRITY", "E_CAPABILITY_MISSING", "E_LOCK_SCHEMA", "E_REQUIREMENT_INACTIVE", "E_TEAM_UNKNOWN", "E_TEAM_NOT_ELIGIBLE"]);
|
|
40
|
+
/** A resolution refusal that readiness reports as a team item (checks.configured), not an install failure. */
|
|
41
|
+
const TEAM_FACTS = new Set(["E_TEAM_UNKNOWN", "E_TEAM_NOT_ELIGIBLE"]);
|
|
38
42
|
|
|
39
43
|
/** Is `name` an executable on PATH? (manifest `requires`, no shell). */
|
|
40
44
|
function onPath(name) {
|
|
@@ -54,7 +58,7 @@ const missingRequires = (manifest) => manifestRequires(manifest).filter((r) => !
|
|
|
54
58
|
function declarationsOf(definition) {
|
|
55
59
|
const d = obj(definition) ? definition : {};
|
|
56
60
|
const section = (key) => (d[key] === undefined ? null : d[key]);
|
|
57
|
-
return { requires: section("requires"), defaults: section("defaults"), knowledge: section("knowledge"),
|
|
61
|
+
return { requires: section("requires"), defaults: section("defaults"), knowledge: section("knowledge"), resources: section("resources"), children: section("children"), capabilities: section("capabilities") };
|
|
58
62
|
}
|
|
59
63
|
function readSoulYaml(soulDir) {
|
|
60
64
|
const file = soulDir && join(soulDir, "soul.yaml");
|
|
@@ -68,9 +72,9 @@ function readTextCapped(file, cap = 200_000) {
|
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
/** The instance target: `home` is absolute and holds instance.json with modules.
|
|
71
|
-
* `teams`
|
|
72
|
-
*
|
|
73
|
-
*
|
|
75
|
+
* `teams` / `defaultTeam` are the home's LIVE teams (team model v2), read from this target's
|
|
76
|
+
* discovery when it discovers, else by liveTeams; `teamsSource` says which ("live" |
|
|
77
|
+
* "recorded": the spawn-time record, when the workspace cannot answer now).
|
|
74
78
|
* `live: false` skips the live read — a caller that never hands the teams to the
|
|
75
79
|
* messaging provider (a non-messaging operation) gets the record at no remote cost. */
|
|
76
80
|
export async function homeTarget(home, meta, { remoteOptions, discover = true, live: readLive = true } = {}) {
|
|
@@ -88,12 +92,13 @@ export async function homeTarget(home, meta, { remoteOptions, discover = true, l
|
|
|
88
92
|
const ws = obj(meta.workspace) ? meta.workspace : {};
|
|
89
93
|
const soulDir = instanceSoulDir(realHome, meta) ?? null;
|
|
90
94
|
const { definition, problems } = readSoulYaml(soulDir);
|
|
91
|
-
let discovery = null, discoveryError = null;
|
|
95
|
+
let discovery = null, discoveryError = null, local = null;
|
|
92
96
|
if (discover) {
|
|
93
97
|
try {
|
|
94
98
|
// The derived deployment exactly — never an oats-local.yaml found further up.
|
|
95
99
|
const found = loadLocal(deployment);
|
|
96
100
|
if (real(dirname(found.path)) !== real(deployment)) throw Object.assign(new Error(`${deployment} has no oats-local.yaml`), { code: "E_HOME_MISMATCH" });
|
|
101
|
+
local = found.local;
|
|
97
102
|
discovery = await discoverOrStandalone(found.local, { deployment, remoteOptions });
|
|
98
103
|
}
|
|
99
104
|
catch (e) { discoveryError = { code: e.code || "E_REMOTE_UNREADABLE", message: e.message }; }
|
|
@@ -106,18 +111,26 @@ export async function homeTarget(home, meta, { remoteOptions, discover = true, l
|
|
|
106
111
|
if (external === null && (discovery.members || []).some((m) => m.key === ws.soul?.repoKey)) external = false;
|
|
107
112
|
}
|
|
108
113
|
const slots = Object.fromEntries(LAYERS.map((l) => [l, Object.keys(modules).find((n) => manifests[n]?.layer === l) ?? null]));
|
|
109
|
-
|
|
114
|
+
// The home's soul key (souls.teams / souls.default): its name, or <package>/<soul> for a package soul.
|
|
115
|
+
const teamKey = ws.soul?.package && typeof ws.soul.qualifiedName === "string" ? ws.soul.qualifiedName : meta.agent;
|
|
116
|
+
let live, model = null;
|
|
110
117
|
if (discovery) {
|
|
111
|
-
|
|
112
|
-
live =
|
|
113
|
-
|
|
114
|
-
: { teams: Array.isArray(meta.teams) ? meta.teams : null, source: "recorded" };
|
|
118
|
+
model = teamModel(discovery.standalone === true ? null : discovery.workspace, local, { workspaceKey: discovery.key ?? null });
|
|
119
|
+
try { const t = soulTeams(model, teamKey); live = { teams: reportRows(t.teams), defaultTeam: t.defaultTeam, source: "live" }; }
|
|
120
|
+
catch (e) { if (!String(e?.code).startsWith("E_TEAM_")) throw e; live = { ...recordedTeams(meta), source: "recorded" }; }
|
|
115
121
|
} else if (readLive) live = await liveTeams(realHome, meta, { remoteOptions });
|
|
116
|
-
else live = {
|
|
122
|
+
else live = { ...recordedTeams(meta), source: "recorded" };
|
|
123
|
+
// Feature launch-preference: the recorded launch (frozen), and what --reselect-launch would choose now —
|
|
124
|
+
// from the home's recorded soul and this deployment's oats-local.yaml (null when it cannot be read).
|
|
125
|
+
const launch = recordedLaunch(meta);
|
|
126
|
+
let launchCurrent = null;
|
|
127
|
+
try { launchCurrent = launchReportFor({ layers: homeLaunchLayers(realHome, meta), launchConfigs: launchConfigsAt(deployment), contextDir: deployment }); }
|
|
128
|
+
catch { launchCurrent = null; }
|
|
117
129
|
return {
|
|
118
|
-
kind: "instance", home: realHome, meta, deployment, agentsRoot, teams: live.teams, teamsSource: live.source,
|
|
130
|
+
kind: "instance", home: realHome, meta, deployment, agentsRoot, teams: live.teams, defaultTeam: live.defaultTeam ?? null, teamsSource: live.source, launch, launchCurrent,
|
|
131
|
+
recordedDefaultTeam: recordedTeams(meta).defaultTeam, teamModel: model, teamKey,
|
|
119
132
|
subject: { kind: "instance", instance: meta.instance, home, soul: meta.agent },
|
|
120
|
-
soul: { name: meta.agent, repoKey: ws.soul?.repoKey ?? null, commit: ws.soul?.commit ?? null,
|
|
133
|
+
soul: { name: meta.agent, repoKey: ws.soul?.repoKey ?? null, commit: ws.soul?.commit ?? null, external, path: null, soulDir, definition, problems: [...problems, ...loadProblems] },
|
|
121
134
|
workspace: { key: ws.key ?? null, name: typeof ws.name === "string" ? ws.name : discovery?.workspace?.name ?? null, deployment, commit: ws.commit ?? null, standalone: ws.standalone === true },
|
|
122
135
|
modules: Object.keys(modules).sort().map((name) => ({ name, from: modules[name]?.from ?? null, manifest: manifests[name] ?? null, dir: join(realHome, ".oats", "modules", name) })),
|
|
123
136
|
payloads: obj(meta.providers) ? meta.providers : {},
|
|
@@ -146,14 +159,34 @@ export async function soulTarget(contextDir, soul, { remoteOptions } = {}) {
|
|
|
146
159
|
soulEntry = findSoulEntry(discovery, soul);
|
|
147
160
|
}
|
|
148
161
|
const res = prepared?.resolution;
|
|
149
|
-
// A refused resolution still names
|
|
150
|
-
|
|
151
|
-
const
|
|
162
|
+
// A refused resolution still names the soul's teams when they resolve (a package refusal, …); a
|
|
163
|
+
// refusal of the teams themselves (E_TEAM_UNKNOWN / E_TEAM_NOT_ELIGIBLE) names none.
|
|
164
|
+
const model = teamModel(discovery?.standalone === true ? null : discovery?.workspace ?? null, prepared?.local ?? found.local, { workspaceKey: discovery?.key ?? null });
|
|
165
|
+
const teamKey = soulKeyOf(soulEntry);
|
|
166
|
+
let teams = res?.teams ?? null, defaultTeam = res ? res.defaultTeam ?? null : null;
|
|
167
|
+
if (!res) { try { const t = soulTeams(model, teamKey); teams = reportRows(t.teams); defaultTeam = t.defaultTeam; } catch (e) { if (!String(e?.code).startsWith("E_TEAM_")) throw e; } }
|
|
168
|
+
// The soul being asked about is materialised as a spawn would (ensureWorkspaceSoul: idempotent per
|
|
169
|
+
// commit, never touching a directory an instance links), so its checks always get OATS_SOUL; only a
|
|
170
|
+
// spawn wrote the per-commit copy before, and a provider asked about a soul not yet spawned at this
|
|
171
|
+
// commit answered a false negative. A failure is a readiness fact (soulCopyError), never a silent null.
|
|
172
|
+
// A refused resolution has nothing to materialise: the cached copy, when there is one.
|
|
173
|
+
let soulDir = null, soulCopyError = null;
|
|
174
|
+
if (prepared) {
|
|
175
|
+
try { soulDir = await ensureWorkspaceSoul(prepared, join(deployment, "agents")); }
|
|
176
|
+
catch (e) { soulCopyError = { code: e?.code || "E_SOUL_COPY_FAILED", message: e?.message || String(e) }; }
|
|
177
|
+
} else {
|
|
178
|
+
const cached = soulEntry?.commit ? join(deployment, "agents", agentDirOf(soulEntry), "souls", String(soulEntry.commit).slice(0, 12)) : null;
|
|
179
|
+
soulDir = cached && existsSync(join(cached, "soul.yaml")) ? cached : null;
|
|
180
|
+
}
|
|
181
|
+
// Feature launch-preference: what a spawn with no flags would launch here.
|
|
182
|
+
let launchConfigs = {};
|
|
183
|
+
try { launchConfigs = launchConfigsAt(deployment); } catch { launchConfigs = {}; }
|
|
184
|
+
const launch = launchReportFor({ layers: prepared?.launch ?? launchLayers({ definition: soulEntry.definition, entry: soulEntry, key: teamKey, local: found.local }), launchConfigs, contextDir: deployment });
|
|
152
185
|
return {
|
|
153
|
-
kind: "soul", home: null, meta: null, deployment, agentsRoot: join(deployment, "agents"), teams, teamsSource: "live",
|
|
154
|
-
subject: { kind: "soul", soul: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null
|
|
155
|
-
soul: { name: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null,
|
|
156
|
-
soulDir
|
|
186
|
+
kind: "soul", home: null, meta: null, deployment, agentsRoot: join(deployment, "agents"), teams, defaultTeam, teamsSource: "live", teamModel: model, teamKey, launch,
|
|
187
|
+
subject: { kind: "soul", soul: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null },
|
|
188
|
+
soul: { name: soulEntry.name, repoKey: soulEntry.repoKey ?? null, commit: soulEntry.commit ?? null, external: soulEntry.external === true, path: soulEntry.path ?? null,
|
|
189
|
+
soulDir, definition: soulEntry.definition ?? null, problems: [], ...(soulCopyError ? { copyError: soulCopyError } : {}) },
|
|
157
190
|
workspace: { key: discovery?.key ?? null, name: discovery?.workspace?.name ?? null, deployment, commit: discovery?.commit ?? null, standalone: discovery?.standalone === true },
|
|
158
191
|
modules: (res?.modules || []).map((m) => ({ name: m.name, from: m.from ?? null, manifest: m.manifest ?? null, dir: null, module: m })).sort((a, b) => a.name.localeCompare(b.name)),
|
|
159
192
|
payloads: obj(res?.payloads) ? res.payloads : {}, payloadOrigins: obj(res?.payloadOrigins) ? res.payloadOrigins : {}, slots: obj(res?.slots) ? res.slots : Object.fromEntries(LAYERS.map((l) => [l, null])), slotsFrom: obj(res?.slotsFrom) ? res.slotsFrom : {},
|
|
@@ -195,7 +228,7 @@ function capabilityRows(t) {
|
|
|
195
228
|
}
|
|
196
229
|
function soulRow(t) {
|
|
197
230
|
const s = t.soul, def = obj(s.definition) ? s.definition : {};
|
|
198
|
-
return { soulsApi: SOULS_API, name: s.name, repoKey: s.repoKey, commit: s.commit,
|
|
231
|
+
return { soulsApi: SOULS_API, name: s.name, repoKey: s.repoKey, commit: s.commit, kind: s.external === true ? "external" : s.external === false ? "member" : null, path: s.path,
|
|
199
232
|
description: def.description ?? null, work: def.work ?? null, harness: def.harness ?? null, model: def.model ?? null,
|
|
200
233
|
declarations: declarationsOf(s.definition), declarationProblems: s.problems,
|
|
201
234
|
instructions: s.soulDir ? readTextCapped(join(s.soulDir, "AGENTS.md")) : null };
|
|
@@ -215,20 +248,57 @@ export function inspectDocument(t, { kernel }) {
|
|
|
215
248
|
// The capabilities the soul turned off (feature desktop-facts): its `off` over a workspace/team entry
|
|
216
249
|
// (reason "off"), or a `<slot>: none` that dropped a workspace default (reason "slot-none", slot).
|
|
217
250
|
capabilitiesOff: (t.turnedOff || []).map(({ name, reason, slot, overrides }) => ({ id: name, off: true, from: "soul", reason, ...(slot ? { slot } : {}), overrides })),
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
251
|
+
// Team model v2 (feature team-model-2): the soul's teams here and its default — live for a home
|
|
252
|
+
// (`teamsSource: "recorded"` when its workspace could not be read now), which also answers the
|
|
253
|
+
// default it was spawned with.
|
|
254
|
+
teams: t.teams ?? null, defaultTeam: t.defaultTeam ?? null, teamsSource: t.teamsSource ?? null,
|
|
255
|
+
...(t.home ? { recordedDefaultTeam: t.recordedDefaultTeam ?? null } : {}),
|
|
256
|
+
// Feature launch-preference: a soul's launch here; a home's recorded launch and launchCurrent.
|
|
257
|
+
launch: t.launch ?? null,
|
|
258
|
+
...(t.home ? { launchCurrent: t.launchCurrent ?? null } : {}),
|
|
221
259
|
knowledge: kcap ? { provider: kcap.id, version: kcap.version, operations: kcap.operations.map((o) => ({ name: o.name, kind: o.kind, available: o.available, reason: o.reason })) } : { provider: null, version: null, operations: [] },
|
|
222
260
|
instance: m ? { home: t.home, instance: m.instance, agent: m.agent, harness: m.harness || null, model: m.model ?? null, yolo: m.yolo ?? null, launched: !!m.launched, createdAt: m.createdAt || null,
|
|
223
261
|
resolution: m.workspace?.resolution ?? null, soulDir: t.soul.soulDir, instructions: { ...readTextCapped(join(t.home, "AGENTS.md")), sources: m.instructions || [] } } : null,
|
|
224
262
|
...(m ? { identity: servedIdentityOf(m) } : {}),
|
|
225
|
-
problems: [...t.soul.problems, ...(t.discoveryError ? [t.discoveryError] : []),
|
|
263
|
+
problems: [...t.soul.problems, ...(t.soul.copyError ? [t.soul.copyError] : []), ...(t.discoveryError ? [t.discoveryError] : []),
|
|
226
264
|
...t.modules.filter((x) => !x.manifest).map((x) => ({ code: "module-missing", message: `${x.name} is recorded but its module copy has no readable oats.json`, capability: x.name })),
|
|
227
265
|
...capabilities.filter((c) => !c.compatibility.ok).map((c) => ({ code: "capability-incompatible", message: `${c.id} requires oats ${c.compatibility.range}; this kernel is ${c.compatibility.kernel} (re-spawn from the deployment with a release whose range admits it)`, capability: c.id, range: c.compatibility.range, kernel: c.compatibility.kernel }))],
|
|
228
266
|
};
|
|
229
267
|
}
|
|
230
268
|
|
|
231
269
|
// ---------- readiness ----------
|
|
270
|
+
/** The soul's team problems as readiness items in `checks.configured` (team model v2; no fifth check:
|
|
271
|
+
* released Desktops recompute the summary from the four). A failure blocks `ready`; a warning is
|
|
272
|
+
* `required: false`. A home also compares its spawn-time default with the live one. */
|
|
273
|
+
function teamItems(t) {
|
|
274
|
+
if (!t.teamModel) return [];
|
|
275
|
+
const problems = teamProblems(t.teamModel, { key: t.teamKey, messaging: !!t.slots?.messaging });
|
|
276
|
+
const same = (a, b) => (a?.label ?? null) === (b?.label ?? null) && (a?.team ?? null) === (b?.team ?? null);
|
|
277
|
+
if (t.home && t.teamsSource === "live" && t.meta?.defaultTeam !== undefined && !same(t.recordedDefaultTeam, t.defaultTeam)) {
|
|
278
|
+
problems.push({ code: "default-team-changed", severity: "warning", recorded: t.recordedDefaultTeam, current: t.defaultTeam,
|
|
279
|
+
message: `this instance was spawned in ${t.recordedDefaultTeam?.label ?? "no default team"}; the default is now ${t.defaultTeam?.label ?? "none"}`, fix: "respawn" });
|
|
280
|
+
}
|
|
281
|
+
return problems.map(({ code, severity, message, fix, ...rest }) => item(rest.label ? `team ${rest.label}` : "teams", "fail",
|
|
282
|
+
{ required: severity === "failure", producer: "team model", code, reason: message, remedy: fix, ...rest }));
|
|
283
|
+
}
|
|
284
|
+
/** The home's recorded launch as a `Launch` (from instance.json; `from: "recorded"` for a home from before 0.30). */
|
|
285
|
+
function recordedLaunch(meta) {
|
|
286
|
+
const recipe = obj(meta?.launch) ? meta.launch : {};
|
|
287
|
+
return launchReport({ declared: obj(meta?.launchDeclared) ? meta.launchDeclared : null, harness: meta?.harness || recipe.harness || null, model: meta?.model || null,
|
|
288
|
+
launchConfig: recipe.launchConfig ?? null, from: typeof meta?.launchFrom === "string" ? meta.launchFrom : "recorded", at: meta?.launchAt ?? null });
|
|
289
|
+
}
|
|
290
|
+
/** `launch-changed` (feature launch-preference; `--home` only): a home whose launch a LAYER chose (not explicit
|
|
291
|
+
* flags, not a pre-0.30 record) now reads a different launch from the layers. A warning: the home keeps its
|
|
292
|
+
* recorded launch until `--reselect-launch` or a respawn. */
|
|
293
|
+
function launchItems(t) {
|
|
294
|
+
if (!t.home || !t.launch || !t.launchCurrent || !["local", "local-default", "soul", "host"].includes(t.launch.from)) return [];
|
|
295
|
+
const a = t.launch.effective, b = t.launchCurrent.effective;
|
|
296
|
+
if (a.harness === b.harness && a.model === b.model && a.launchConfig === b.launchConfig) return [];
|
|
297
|
+
const show = (e) => `${e.harness ?? "?"}${e.model ? ` ${e.model}` : ""}${e.launchConfig ? ` (launch configuration ${e.launchConfig})` : ""}`;
|
|
298
|
+
return [item("launch", "fail", { required: false, producer: "launch preference", code: "launch-changed",
|
|
299
|
+
reason: `this instance runs ${show(a)}; its launch preference now says ${show(b)} (${t.launchCurrent.at ?? t.launchCurrent.from})`,
|
|
300
|
+
remedy: "`oats session restart --reselect-launch`, or respawn", recorded: a, current: b, from: t.launchCurrent.from, at: t.launchCurrent.at })];
|
|
301
|
+
}
|
|
232
302
|
function roll(items) {
|
|
233
303
|
const req = items.filter((i) => i.required !== false);
|
|
234
304
|
if (!items.length) return "not-applicable";
|
|
@@ -241,7 +311,7 @@ const item = (subject, status, { required = true, reason = null, producer = "ker
|
|
|
241
311
|
/** The team/workspace facts a provider receives, as hooks get them (teamEnv). */
|
|
242
312
|
function providerEnv(t, capability, settings) {
|
|
243
313
|
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^(OATS_|OAS_|PI_)/.test(k)));
|
|
244
|
-
Object.assign(env, teamEnv({ workspace: { key: t.workspace.key, name: t.workspace.name, deployment: t.deployment
|
|
314
|
+
Object.assign(env, teamEnv({ workspace: { key: t.workspace.key, name: t.workspace.name, deployment: t.deployment }, teams: t.teams, defaultTeam: t.defaultTeam, teamsSource: t.teamsSource }));
|
|
245
315
|
return Object.assign(env, { OATS_CAPABILITY: capability, OATS_SETTINGS: JSON.stringify(settings), OATS_SETTINGS_ORIGINS: JSON.stringify(t.payloadOrigins?.[capability] ?? {}), OATS_CLI_BIN: CLI_BIN, OATS_WORKSPACE: t.deployment,
|
|
246
316
|
...(t.home ? { OATS_INSTANCE: t.meta.instance, OATS_INSTANCE_HOME: t.home } : {}), OATS_AGENT: t.soul.name, ...(t.soul.soulDir ? { OATS_SOUL: t.soul.soulDir } : {}) });
|
|
247
317
|
}
|
|
@@ -276,10 +346,10 @@ export function runProviderCheck(t, mod, dir, { timeoutMs = 30000 } = {}) {
|
|
|
276
346
|
if (!file) return unavailable("resource-not-found", `${mod.name}: the check executable ${script} is unavailable`);
|
|
277
347
|
const settings = obj(t.payloads[mod.name]) ? t.payloads[mod.name] : {};
|
|
278
348
|
// The released binding wire, key for key: providers decode it strictly (oats.aweb 1.13.1
|
|
279
|
-
// refuses any other key). The
|
|
280
|
-
// (
|
|
349
|
+
// refuses any other key). The teams reach the check through its environment
|
|
350
|
+
// (OATS_DEFAULT_TEAM* / OATS_TEAMS / OATS_TEAMS_SOURCE, providerEnv), never on stdin.
|
|
281
351
|
const request = { schemaVersion: 1, phase: "check", slot: m.layer ?? null, capability: mod.name, settings,
|
|
282
|
-
input: { context: { kind: "workspace", workspace: t.workspace.key, deployment: t.deployment, soul: t.soul.name,
|
|
352
|
+
input: { context: { kind: "workspace", workspace: t.workspace.key, deployment: t.deployment, soul: t.soul.name, instance: t.meta?.instance ?? null, home: t.home }, action: { kind: "readiness" } } };
|
|
283
353
|
// The environment is providerEnv's: ambient OATS_/OAS_/PI_ identity removed, the rest
|
|
284
354
|
// inherited.
|
|
285
355
|
const r = spawnSync(process.execPath, [file, ...args], { cwd: dir, env: providerEnv(t, mod.name, settings), input: JSON.stringify(request), encoding: "utf8", timeout: Math.max(1, timeoutMs), killSignal: "SIGKILL", maxBuffer: BINDING_LIMITS.maxBytes, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -315,9 +385,13 @@ export const PROVIDER_CHECK_BUDGET_MS = 60_000;
|
|
|
315
385
|
export async function readinessDocument(t, { selector = null, remoteOptions, catalog = null, budgetMs = PROVIDER_CHECK_BUDGET_MS } = {}) {
|
|
316
386
|
const installed = [], configured = [], member = [], providers = [];
|
|
317
387
|
const cap = (id) => ({ capability: { id } });
|
|
318
|
-
if (t.resolutionError) {
|
|
388
|
+
if (t.resolutionError && !TEAM_FACTS.has(t.resolutionError.code)) {
|
|
319
389
|
installed.push(item(t.soul.name, "fail", { producer: "workspace resolution", code: t.resolutionError.code, reason: t.resolutionError.message, evidence: t.resolutionError.details, remedy: "oats sync (the lock must provide every package capability the soul resolves)" }));
|
|
320
390
|
}
|
|
391
|
+
if (t.soul.copyError) {
|
|
392
|
+
installed.push(item(`soul ${t.soul.name}`, "fail", { producer: "soul copy", code: t.soul.copyError.code, reason: `the soul could not be copied into ${join(t.agentsRoot, "…", "souls")}: ${t.soul.copyError.message}`,
|
|
393
|
+
evidence: { repoKey: t.soul.repoKey, commit: t.soul.commit }, remedy: "check access to the soul's repository at the locked commit (oats sync), then retry" }));
|
|
394
|
+
}
|
|
321
395
|
for (const mod of t.modules) {
|
|
322
396
|
const present = t.home ? existsSync(join(mod.dir, "oats.json")) && !!mod.manifest : !!mod.manifest;
|
|
323
397
|
installed.push(item(mod.name, present ? "pass" : "fail", { producer: t.home ? "instance modules" : "workspace resolution", evidence: { from: mod.from },
|
|
@@ -328,6 +402,7 @@ export async function readinessDocument(t, { selector = null, remoteOptions, cat
|
|
|
328
402
|
evidence: { command: req.command }, remedy: found ? null : req.install, ...cap(mod.name) }));
|
|
329
403
|
}
|
|
330
404
|
}
|
|
405
|
+
configured.push(...teamItems(t), ...launchItems(t));
|
|
331
406
|
// member — the soul's member repository is a confirmed member of the workspace
|
|
332
407
|
// (oats-membership.yaml backlink observed over the remotes).
|
|
333
408
|
const d = t.discovery;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { basename, join } from "node:path";
|
|
13
|
-
import { findInstanceHomes, inspectInstanceSession, listAgents, listInstances, retirePendingMarkerPath, stopInstanceSession } from "./core.mjs";
|
|
13
|
+
import { findInstanceHomes, inspectInstanceSession, listAgents, listInstances, observeSessionWithoutReceipt, retirePendingMarkerPath, stopInstanceSession } from "./core.mjs";
|
|
14
14
|
import { observeInstanceGit } from "./instance-git.mjs";
|
|
15
15
|
import { appendEvent } from "./instance-events.mjs";
|
|
16
16
|
import { oatsError } from "./errors.mjs";
|
|
@@ -74,6 +74,17 @@ function sessionFacts(home) {
|
|
|
74
74
|
try { const s = inspectInstanceSession(home); return { state: s.state, present: s.present, backend: s.backend ?? null, established: true }; }
|
|
75
75
|
catch (e) { return { state: "unestablished", present: null, backend: null, established: false, reason: e.code || "E_SESSION_UNAVAILABLE" }; }
|
|
76
76
|
}
|
|
77
|
+
/** Retire's view of a home without its session receipt (before 0.25.9): the
|
|
78
|
+
* retirement needs only absence, which it can observe — `{state: "absent",
|
|
79
|
+
* present: false, established: true, note}`; live or ambiguous stays
|
|
80
|
+
* unestablished, with the note saying why (retire refuses it). */
|
|
81
|
+
function retireSessionFacts(home, session) {
|
|
82
|
+
if (session.established || session.reason !== "E_RUNTIME_ENDPOINT_UNKNOWN") return session;
|
|
83
|
+
const meta = readJson(join(home, "instance.json"));
|
|
84
|
+
if (!meta) return session;
|
|
85
|
+
const seen = observeSessionWithoutReceipt(home, meta);
|
|
86
|
+
return seen.absent ? { state: "absent", present: false, backend: seen.backend, established: true, note: seen.note } : { ...session, note: seen.note };
|
|
87
|
+
}
|
|
77
88
|
const running = (s) => s.established && s.present === true && !["shell", "stopped", "not-launched"].includes(s.state);
|
|
78
89
|
function workFacts(home) {
|
|
79
90
|
if (!existsSync(join(home, "work"))) return { observed: false, reason: "no-worktree" };
|
|
@@ -161,6 +172,7 @@ export function planRetire(ctx, root, name, { home } = {}) {
|
|
|
161
172
|
const me = resolveInstance(ctx, root, name, { home });
|
|
162
173
|
const kids = descendantsOf(me.root, name);
|
|
163
174
|
const target = targetFacts({ ...me, instance: name, depth: 0 });
|
|
175
|
+
target.session = retireSessionFacts(me.home, target.session);
|
|
164
176
|
const meta = readJson(join(me.home, "instance.json")) || {};
|
|
165
177
|
const facts = { session: target.session, work: target.work, workMode: meta.work ?? null, repo: meta.repo ?? null, recordedBranch: meta.branch ?? null,
|
|
166
178
|
children: kids.map((c) => ({ instance: c.instance, agent: c.agent, home: c.home, session: sessionFacts(c.home) })),
|
|
@@ -175,6 +187,7 @@ export function planRetire(ctx, root, name, { home } = {}) {
|
|
|
175
187
|
...(facts.work.observed && facts.work.changed + facts.work.untracked > 0 ? [`${facts.work.changed} changed and ${facts.work.untracked} untracked file(s) would be retained with the worktree`] : []),
|
|
176
188
|
...(kids.length ? [`${kids.length} recorded child instance(s) are stopped first (bounded SIGTERM, never escalated) and retained (their homes are not removed); a child still running after the grace refuses the retirement`] : []),
|
|
177
189
|
...((kids.ambiguous || []).length ? [`${kids.ambiguous.length} instance(s) record this name as parent but the name is not unique under this root; they are listed under ambiguous and NOT acted on`] : []),
|
|
190
|
+
...(target.session.note ? [target.session.established ? `session absent: ${target.session.note}; nothing needs quiescing` : `session not observably absent: ${target.session.note}; retire refuses until it is stopped`] : []),
|
|
178
191
|
"pull request state is unknown to the kernel; the ADE reports it when a forge connection exists",
|
|
179
192
|
] };
|
|
180
193
|
}
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
|
|
20
20
|
import { join, resolve as resolvePath, dirname, relative, isAbsolute, sep } from "node:path";
|
|
21
21
|
import { oatsError } from "./errors.mjs";
|
|
22
|
-
import { loadLocal, discoverWorkspace, discoverRepo, standaloneRepo, observeWorkspace
|
|
23
|
-
import { resolveSoul,
|
|
22
|
+
import { loadLocal, discoverWorkspace, discoverRepo, standaloneRepo, observeWorkspace } from "./workspace.mjs";
|
|
23
|
+
import { resolveSoul, kernelCompatibility } from "./resolve.mjs";
|
|
24
|
+
import { BY_TEAM_REMOVED, recordedTeams, reportRows, soulKeyOf, soulTeams, teamModel } from "./teams.mjs";
|
|
25
|
+
import { launchLayers } from "./launch-preference.mjs";
|
|
24
26
|
import { declaredSettings } from "./capability-contract.mjs";
|
|
25
27
|
import { materialize, MODULES_DIR, SKILLS_DIR } from "./materialize.mjs";
|
|
26
28
|
import { fetchRemoteTree } from "./remote.mjs";
|
|
@@ -39,14 +41,13 @@ function err(code, message, details) { const e = oatsError(code, message, detail
|
|
|
39
41
|
* Same set as lib/resolve.mjs POISON_KEYS; refused with E_WORKSPACE_SCHEMA reason
|
|
40
42
|
* "poison-key" so a `--provider constructor x=1` never reaches Object.prototype. */
|
|
41
43
|
const POISON_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
42
|
-
/** `byTeam`
|
|
43
|
-
* a spawn payload may never smuggle it in. */
|
|
44
|
+
/** `byTeam` was removed in 0.30 (team model v2): a spawn payload may not carry it. */
|
|
44
45
|
const RESERVED_KEY = "byTeam";
|
|
45
46
|
|
|
46
47
|
/** Parse repeated `--provider <cap> k=v` occurrences into { <cap>: { k: v } }.
|
|
47
48
|
* Values are strings; `k=v=w` keeps everything after the first `=`. Poison keys
|
|
48
49
|
* (capability OR any path segment) are E_WORKSPACE_SCHEMA reason "poison-key";
|
|
49
|
-
* `byTeam` (any path segment) is E_WORKSPACE_SCHEMA reason "
|
|
50
|
+
* `byTeam` (any path segment) is E_WORKSPACE_SCHEMA reason "removed-key". Both are
|
|
50
51
|
* refused again in resolve (defense in depth). The accumulators are built with
|
|
51
52
|
* Object.create(null) and only OWN keys are ever reused, so a name that happens to
|
|
52
53
|
* exist on Object.prototype (`toString`, `hasOwnProperty`) never writes through to it. */
|
|
@@ -63,7 +64,7 @@ export function parseProviderFlags(pairs) {
|
|
|
63
64
|
const parts = key.split(".");
|
|
64
65
|
for (const p of parts) {
|
|
65
66
|
if (POISON_KEYS.has(p)) throw err("E_WORKSPACE_SCHEMA", `--provider ${cap}: key ${JSON.stringify(key)} is refused (segment ${JSON.stringify(p)} would poison the payload's prototype)`, { path: `/${cap}/${parts.join("/")}`, key: p, reason: "poison-key" });
|
|
66
|
-
if (p === RESERVED_KEY) throw err("E_WORKSPACE_SCHEMA", `--provider ${cap}: key ${JSON.stringify(key)} is refused (${
|
|
67
|
+
if (p === RESERVED_KEY) throw err("E_WORKSPACE_SCHEMA", `--provider ${cap}: key ${JSON.stringify(key)} is refused (${BY_TEAM_REMOVED})`, { path: `/${cap}/${parts.join("/")}`, key: p, reason: "removed-key" });
|
|
67
68
|
}
|
|
68
69
|
if (!Object.hasOwn(out, cap)) out[cap] = Object.create(null);
|
|
69
70
|
let cur = out[cap];
|
|
@@ -130,33 +131,29 @@ export function disabledEntry(local, soulEntry) {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
/**
|
|
133
|
-
* A home's LIVE
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* unreachable, the soul is no longer listed, a standalone deployment), the spawn-time record:
|
|
140
|
-
* { teams: <record, or null when none>, source: "recorded", reason }.
|
|
134
|
+
* A home's LIVE teams (team model v2): the committed shared teams (the workspace host read now) and the
|
|
135
|
+
* deployment's oats-local.yaml as it is now, resolved for the home's soul key (its name, or
|
|
136
|
+
* `<package>/<soul>`). The home's modules and skills are untouched. One repository read, the workspace
|
|
137
|
+
* host; a standalone deployment has local teams only and reads none.
|
|
138
|
+
* → { teams, defaultTeam, source: "live" } — or, when that cannot answer (the host is unreachable, the
|
|
139
|
+
* deployment or its teams are unreadable), the spawn-time record: { teams, defaultTeam, source: "recorded", reason }.
|
|
141
140
|
* `source` travels as OATS_TEAMS_SOURCE: a provider leaves a joined team only on a `live` answer.
|
|
142
141
|
*/
|
|
143
142
|
export async function liveTeams(home, meta, { remoteOptions, remote } = {}) {
|
|
144
|
-
const recorded = (reason, error) => ({
|
|
143
|
+
const recorded = (reason, error) => ({ ...recordedTeams(meta), source: "recorded", reason, ...(error ? { error } : {}) });
|
|
145
144
|
const soul = meta?.workspace?.soul;
|
|
146
|
-
// A home without a workspace soul record
|
|
147
|
-
if (!soul || typeof
|
|
148
|
-
|
|
149
|
-
if (soul.package && typeof soul.package === "object") return recorded("package-soul");
|
|
145
|
+
// A home without a workspace soul record has nothing to resolve.
|
|
146
|
+
if (!soul || typeof meta.agent !== "string") return recorded("no-workspace-soul");
|
|
147
|
+
const key = soul.package && typeof soul.qualifiedName === "string" ? soul.qualifiedName : meta.agent;
|
|
150
148
|
const deployment = dirname(dirname(dirname(dirname(resolvePath(home)))));
|
|
151
149
|
try {
|
|
152
150
|
const found = loadLocal(deployment);
|
|
153
151
|
if (!found.path || resolvePath(dirname(found.path)) !== resolvePath(deployment)) return recorded("no-deployment");
|
|
154
|
-
|
|
155
|
-
const wsObs = await observeWorkspace(found.local.workspace, { remoteOptions, remote });
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} catch (e) { return recorded("unreadable", { code: e?.code || "E_REMOTE_UNREADABLE", message: e?.message ?? String(e) }); }
|
|
152
|
+
const standalone = typeof found.local.standalone === "string" && found.local.standalone;
|
|
153
|
+
const wsObs = standalone ? null : await observeWorkspace(found.local.workspace, { remoteOptions, remote });
|
|
154
|
+
const t = soulTeams(teamModel(wsObs?.workspace ?? null, found.local, { workspaceKey: wsObs?.key ?? null }), key);
|
|
155
|
+
return { teams: reportRows(t.teams), defaultTeam: t.defaultTeam, source: "live" };
|
|
156
|
+
} catch (e) { return recorded(String(e?.code).startsWith("E_TEAM_") ? "invalid-teams" : "unreadable", { code: e?.code || "E_REMOTE_UNREADABLE", message: e?.message ?? String(e) }); }
|
|
160
157
|
}
|
|
161
158
|
|
|
162
159
|
/** The per-commit soul cache under an agent directory: `<agentDir>/souls/<commit12>/`.
|
|
@@ -425,7 +422,7 @@ export function requireMemberClone(prepared, { explicit } = {}) {
|
|
|
425
422
|
}
|
|
426
423
|
|
|
427
424
|
/** Would a spawn of this discovered soul be refused before it touches this machine (feature desktop-facts)?
|
|
428
|
-
* The same checks prepareInstance makes — souls.disabled, then the soul's resolution (
|
|
425
|
+
* The same checks prepareInstance makes — souls.disabled, then the soul's resolution (its teams here, slot
|
|
429
426
|
* conflicts, missing or private capabilities, compatibility …) — without spawning anything.
|
|
430
427
|
* → { spawnable: true, problem: null } | { spawnable: false, problem: { code, message } } */
|
|
431
428
|
export async function soulSpawnability(local, discovery, lock, soulEntry, { remoteOptions, remote } = {}) {
|
|
@@ -447,7 +444,9 @@ export async function prepareInstance(contextDir, soulName, { spawn = {}, remote
|
|
|
447
444
|
const disabled = disabledEntry(local, soulEntry);
|
|
448
445
|
if (disabled !== null) throw err("E_SOUL_DISABLED", `soul ${qualifiedSoulName(soulEntry)} is disabled on this machine (oats-local.yaml souls.disabled: ${disabled}) — remove it from that list to spawn it here`, { name: soulEntry.name, qualifiedName: qualifiedSoulName(soulEntry), entry: disabled });
|
|
449
446
|
const resolution = await resolveSoul(discovery, soulEntry, { local, lock, spawn, remoteOptions, remote });
|
|
450
|
-
|
|
447
|
+
// The launch layers a new selection reads (feature launch-preference): souls.launch, the soul's own launch.
|
|
448
|
+
const launch = launchLayers({ definition: soulEntry.definition, entry: soulEntry, key: soulKeyOf(soulEntry), local });
|
|
449
|
+
return { local, deployment, lock, discovery, soulEntry, resolution, remoteOptions, spawn, launch };
|
|
451
450
|
}
|
|
452
451
|
|
|
453
452
|
/** E_REMOTE_UNREADABLE reasons (lib/remote.mjs classifyRemoteFailure) that mean "the
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soul launch preferences (0.30, feature `launch-preference`; docs/design/2026-09-28-soul-launch-preference.md,
|
|
3
|
+
* docs/desktop-cli-api.md "Launch preferences"). PURE: no I/O. A soul may declare `launch: {harness, model?}`;
|
|
4
|
+
* a machine overrides it in oats-local.yaml `souls.launch` (a soul key or "*" → a launch configuration's name
|
|
5
|
+
* or an inline {harness, model?}). For a NEW selection the first layer with a value decides: the flags, then
|
|
6
|
+
* souls.launch.<key>, souls.launch."*", the soul's launch, the host default. A home's recorded launch stays
|
|
7
|
+
* frozen: a plain start/restart never re-reads these layers (only --reselect-launch or a respawn does).
|
|
8
|
+
*/
|
|
9
|
+
import { oatsError } from "./errors.mjs";
|
|
10
|
+
|
|
11
|
+
export const LOCAL_FILE = "oats-local.yaml";
|
|
12
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
13
|
+
/** A coded error whose details also sit on `details` (the CLI boundary renders them). */
|
|
14
|
+
function fail(code, message, details) { const e = oatsError(code, message, details); e.details = details; return e; }
|
|
15
|
+
const pointerKey = (k) => String(k).replace(/~/g, "~0").replace(/\//g, "~1");
|
|
16
|
+
|
|
17
|
+
/** A preference value as `{harness, model}` (`model` null when absent); anything else → null. */
|
|
18
|
+
export function preferenceOf(v) {
|
|
19
|
+
return isObject(v) && typeof v.harness === "string" ? { harness: v.harness, model: typeof v.model === "string" && v.model ? v.model : null } : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Where a soul's own `launch` lives: `<repoKey>:<path>/soul.yaml#/launch`, or `package:<id>:<path>/…` for a package soul. */
|
|
23
|
+
export function soulLaunchAt(entry) {
|
|
24
|
+
const file = `${entry?.path ? `${entry.path}/` : ""}soul.yaml#/launch`;
|
|
25
|
+
const pkg = typeof entry?.package === "string" ? entry.package : entry?.package?.id;
|
|
26
|
+
return pkg ? `package:${pkg}:${file}` : `${entry?.repoKey ?? "?"}:${file}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The layers a new selection reads, without flags: `declared` (the soul's own launch, or null) and `layer`
|
|
31
|
+
* — the deciding one, `{ from: "local" | "local-default" | "soul", at, value }` (`value` a launch
|
|
32
|
+
* configuration's name or `{harness, model}`) — or null (the host default).
|
|
33
|
+
* `definition` is the soul.yaml content; `key` its soul key (soulKeyOf); `local` the oats-local.yaml value.
|
|
34
|
+
*/
|
|
35
|
+
export function launchLayers({ definition, entry, key, local }) {
|
|
36
|
+
const declared = preferenceOf(definition?.launch);
|
|
37
|
+
const byKey = isObject(local?.souls?.launch) ? local.souls.launch : {};
|
|
38
|
+
const pick = (k, from) => {
|
|
39
|
+
if (!Object.hasOwn(byKey, k)) return null;
|
|
40
|
+
const v = byKey[k];
|
|
41
|
+
return { from, at: `${LOCAL_FILE}#/souls/launch/${pointerKey(k)}`, value: typeof v === "string" ? v : preferenceOf(v) };
|
|
42
|
+
};
|
|
43
|
+
const layer = (key !== undefined && key !== "*" ? pick(key, "local") : null) ?? pick("*", "local-default")
|
|
44
|
+
?? (declared ? { from: "soul", at: soulLaunchAt(entry), value: declared } : null);
|
|
45
|
+
return { declared, layer };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The selection a NEW launch makes from the flags and the layers: `{ selection, preference, from, at }` for
|
|
50
|
+
* resolveLaunchSelection. `--launch-config` or `--harness` decides (`from: "flag"`); otherwise the layer does
|
|
51
|
+
* (a name selects that configuration; an inline or soul preference is `preference`), and `--model` alone
|
|
52
|
+
* replaces only the model. No layer: the host default (`from: "host"`).
|
|
53
|
+
*/
|
|
54
|
+
export function selectionFrom({ flags = {}, layers }) {
|
|
55
|
+
const selection = { launchConfig: flags.launchConfig, harness: flags.harness, model: flags.model };
|
|
56
|
+
if (flags.launchConfig !== undefined || flags.harness !== undefined) return { selection, preference: null, from: "flag", at: null };
|
|
57
|
+
const layer = layers?.layer;
|
|
58
|
+
if (!layer) return { selection, preference: null, from: "host", at: null };
|
|
59
|
+
if (typeof layer.value === "string") return { selection: { ...selection, launchConfig: layer.value }, preference: null, from: layer.from, at: layer.at };
|
|
60
|
+
return { selection: { ...selection, launchConfig: "none" }, preference: { ...layer.value, from: layer.from }, from: layer.from, at: layer.at };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A launch configuration the layer names but this oats-local.yaml does not declare. */
|
|
64
|
+
export function launchConfigUnknown({ name, from, at }) {
|
|
65
|
+
return fail("E_LAUNCH_CONFIG_UNKNOWN", `${at ?? "the launch preference"} names launch configuration ${JSON.stringify(name)}, which ${LOCAL_FILE} does not declare (launch-configs:); oats launch-config list shows what is`, { name, from, at });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The fix for a missing harness, by the layer that chose it. */
|
|
69
|
+
export function harnessFix(harness, from) {
|
|
70
|
+
if (from === "flag") return `install ${harness}, or choose another --harness / --launch-config`;
|
|
71
|
+
if (from === "local" || from === "local-default") return `install ${harness}, or change ${LOCAL_FILE} souls.launch`;
|
|
72
|
+
return `install ${harness}, or override it on this machine in ${LOCAL_FILE} souls.launch`;
|
|
73
|
+
}
|
|
74
|
+
/** E_HARNESS_UNAVAILABLE: the chosen harness has no executable here. Never a fallback to another harness. */
|
|
75
|
+
export function harnessUnavailable({ harness, from, at, why }) {
|
|
76
|
+
const source = from === "flag" ? "the spawn flags" : from === "host" ? "the host default" : at ?? from;
|
|
77
|
+
const fix = harnessFix(harness, from);
|
|
78
|
+
return fail("E_HARNESS_UNAVAILABLE", `${harness} is not installed on this machine (${why}); ${source} chose it — ${fix}`, { harness, from, at: at ?? null, fix });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The closed `Launch` report: `{declared, effective: {harness, model, launchConfig}, from, at, problem}`. */
|
|
82
|
+
export function launchReport({ declared = null, harness, model = null, launchConfig = null, from, at = null, problem = null }) {
|
|
83
|
+
return { declared: declared ? { harness: declared.harness, model: declared.model ?? null } : null, effective: { harness, model: model || null, launchConfig: launchConfig || null }, from, at: at ?? null, problem };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `modelFrom` for a model a preference layer supplied. */
|
|
87
|
+
export const MODEL_SOURCE = { soul: "soul preference", local: "local preference", "local-default": "local-default preference" };
|
package/lib/materialize.mjs
CHANGED
|
@@ -576,7 +576,7 @@ export function driftOf(instanceJson, discovery, { lock } = {}) {
|
|
|
576
576
|
* → { name, repoKey, commit, current: { commit } | null, status: "current"|"moved"|"missing", reason? } | null
|
|
577
577
|
*
|
|
578
578
|
* Decision 17 for the SOUL SOURCE: a workspace spawn records `instance.json.workspace.soul =
|
|
579
|
-
* { repoKey, commit
|
|
579
|
+
* { repoKey, commit }` (the member and commit the soul was fetched from). The member row
|
|
580
580
|
* for `repoKey` in `discovery.members` is the current state: a different commit → "moved", the
|
|
581
581
|
* soul gone from the member → "missing" (reason "soul-absent"), no usable row → "missing"
|
|
582
582
|
* (reason "unconfirmed" / the row's reason). A standalone view's own member row is unconfirmed
|
|
@@ -595,7 +595,7 @@ export function soulDriftOf(instanceJson, discovery) {
|
|
|
595
595
|
// A package soul: "moved" means the package pin moved (another version/commit is locked now).
|
|
596
596
|
const pkg = { package: soul.package.id, version: soul.package.version ?? null };
|
|
597
597
|
const soulName = typeof soul.name === "string" ? soul.name : name;
|
|
598
|
-
const base = { name: soulName, repoKey: soul.repoKey, commit,
|
|
598
|
+
const base = { name: soulName, repoKey: soul.repoKey, commit, ...pkg };
|
|
599
599
|
const listed = Array.isArray(discovery?.packageSouls) ? discovery.packageSouls.filter((s) => s && s.package === soul.package.id) : [];
|
|
600
600
|
const now = listed.find((s) => s.name === soulName) || null;
|
|
601
601
|
if (!now) return { ...base, current: null, status: "missing", reason: listed.length ? "soul-absent" : "package-absent" };
|
|
@@ -604,7 +604,7 @@ export function soulDriftOf(instanceJson, discovery) {
|
|
|
604
604
|
const members = Array.isArray(discovery?.members) ? discovery.members : [];
|
|
605
605
|
const member = members.find((m) => m && m.key === soul.repoKey) || null;
|
|
606
606
|
const standaloneOwn = discovery?.standalone === true && member && member.key === discovery.key && typeof member.commit === "string";
|
|
607
|
-
const base = { name, repoKey: soul.repoKey, commit
|
|
607
|
+
const base = { name, repoKey: soul.repoKey, commit };
|
|
608
608
|
if (!member || (!member.confirmed && !standaloneOwn) || typeof member.commit !== "string") {
|
|
609
609
|
return { ...base, current: null, status: "missing", reason: member ? (member.reason || "unconfirmed") : "unconfirmed" };
|
|
610
610
|
}
|
package/lib/packages.mjs
CHANGED
|
@@ -372,11 +372,8 @@ export async function packageSoulDigests(remote, remoteRef, commit, souls, detai
|
|
|
372
372
|
if (e?.code === "E_REMOTE_PATH_MISSING") throw oatsError("E_PACKAGE_MANIFEST", `package soul ${soul.path} is missing at ${String(commit).slice(0, 12)}`, { ...details, soul: soul.name, path: soul.dir });
|
|
373
373
|
throw e;
|
|
374
374
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
for (const file of ["soul.yaml", "AGENTS.md"]) {
|
|
378
|
-
if (!existsSync(join(dest, file))) throw oatsError("E_PACKAGE_MANIFEST", `package soul ${soul.path} lacks ${file} at ${String(commit).slice(0, 12)} — a soul directory carries soul.yaml and AGENTS.md`, { ...details, soul: soul.name, path: soul.dir, missing: file });
|
|
379
|
-
}
|
|
375
|
+
for (const file of ["soul.yaml", "AGENTS.md"]) {
|
|
376
|
+
if (!existsSync(join(dest, file))) throw oatsError("E_PACKAGE_MANIFEST", `package soul ${soul.path} lacks ${file} at ${String(commit).slice(0, 12)} — a soul directory carries soul.yaml and AGENTS.md`, { ...details, soul: soul.name, path: soul.dir, missing: file });
|
|
380
377
|
}
|
|
381
378
|
out.push({ name: soul.name, path: soul.path, digest: assertDigest(`package soul ${soul.path} digest`, digest, details) });
|
|
382
379
|
}
|