@awebai/oats 0.29.4 → 0.30.1
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 +194 -50
- package/docs/capabilities.md +160 -171
- package/docs/capability-manifest.schema.json +6 -11
- 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 +97 -117
- 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 +77 -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 +83 -0
- package/docs/release-lane.md +82 -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.30.0.md +205 -0
- package/docs/release-notes/v0.30.1.md +123 -0
- package/docs/schedules.md +280 -363
- 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 +137 -215
- package/lib/automations.mjs +21 -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 +1 -1
- package/lib/resolve.mjs +30 -88
- package/lib/schedule.mjs +1 -1
- package/lib/teams-verbs.mjs +195 -0
- package/lib/teams.mjs +190 -0
- package/lib/triggers.mjs +2 -2
- package/lib/workspace.mjs +54 -147
- package/package-catalog.json +10 -16
- package/package.json +1 -3
- package/skills/oats-getting-started/SKILL.md +25 -13
- package/capabilities/oats-authoring/LICENSE +0 -21
- package/capabilities/oats-authoring/oats-package.json +0 -11
- package/capabilities/oats-authoring/oats.json +0 -12
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +0 -84
- package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +0 -109
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +0 -116
- package/capabilities/oats-aweb/bin/oats-aweb-binding.mjs +0 -11
- package/capabilities/oats-aweb/bin/oats-aweb.mjs +0 -1338
- package/capabilities/oats-aweb/injects/aweb.md +0 -47
- package/capabilities/oats-aweb/lib/binding-wire.mjs +0 -356
- package/capabilities/oats-aweb/lib/captured-execution.mjs +0 -91
- package/capabilities/oats-aweb/lib/captured-native.mjs +0 -91
- package/capabilities/oats-aweb/lib/grant-custody.mjs +0 -38
- package/capabilities/oats-aweb/lib/invocation-shape.mjs +0 -135
- package/capabilities/oats-aweb/lib/portable-binding.mjs +0 -146
- package/capabilities/oats-aweb/lib/session-readiness.mjs +0 -56
- package/capabilities/oats-aweb/lib/wake-receive.mjs +0 -56
- package/capabilities/oats-aweb/oats.json +0 -208
- package/capabilities/oats-aweb/skills/LICENSE +0 -21
- package/capabilities/oats-aweb/skills/VENDORED.md +0 -31
- package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +0 -201
- package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +0 -161
- package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +0 -61
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +0 -116
- package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +0 -74
- package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +0 -216
- package/capabilities/oats-jira/bin/oats-jira.mjs +0 -40
- package/capabilities/oats-jira/injects/jira.md +0 -10
- package/capabilities/oats-jira/oats.json +0 -22
- package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +0 -179
- package/capabilities/oats-linear/bin/oats-linear-hook.mjs +0 -34
- package/capabilities/oats-linear/bin/oats-linear.mjs +0 -344
- package/capabilities/oats-linear/injects/linear.md +0 -8
- package/capabilities/oats-linear/oats.json +0 -24
- package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +0 -223
- package/capabilities/oats-okf/bin/oats-okf-binding.mjs +0 -14
- package/capabilities/oats-okf/bin/oats-okf.mjs +0 -209
- package/capabilities/oats-okf/injects/okf.md +0 -42
- package/capabilities/oats-okf/lib/binding-wire.mjs +0 -348
- package/capabilities/oats-okf/lib/captured-worker.mjs +0 -109
- package/capabilities/oats-okf/lib/config.mjs +0 -124
- package/capabilities/oats-okf/lib/consult.mjs +0 -518
- package/capabilities/oats-okf/lib/harvest-status.mjs +0 -88
- package/capabilities/oats-okf/lib/harvest-switch.mjs +0 -94
- package/capabilities/oats-okf/lib/inspection.mjs +0 -119
- package/capabilities/oats-okf/lib/invocation-context.mjs +0 -111
- package/capabilities/oats-okf/lib/invocation-shape.mjs +0 -135
- package/capabilities/oats-okf/lib/io.mjs +0 -118
- package/capabilities/oats-okf/lib/migration.mjs +0 -137
- package/capabilities/oats-okf/lib/okf-validate.mjs +0 -123
- package/capabilities/oats-okf/lib/portable-binding.mjs +0 -199
- package/capabilities/oats-okf/lib/source-contract.mjs +0 -46
- package/capabilities/oats-okf/lib/sources.mjs +0 -424
- package/capabilities/oats-okf/lib/stores.mjs +0 -473
- package/capabilities/oats-okf/lib/worker.mjs +0 -497
- package/capabilities/oats-okf/oats.json +0 -148
- package/capabilities/oats-okf/schemas/okf-base.schema.json +0 -46
- package/capabilities/oats-okf/schemas/okf-bindings.schema.json +0 -112
- package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +0 -87
- package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +0 -113
- package/capabilities/oats-okf/schemas/okf-soul.schema.json +0 -37
- package/capabilities/oats-okf/skills/okf-consultation/SKILL.md +0 -144
- package/capabilities/oats-okf/skills/okf-consultation/references/consult.md +0 -86
- package/capabilities/oats-okf/skills/okf-instance-knowledge/SKILL.md +0 -104
- package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +0 -140
- package/capabilities/oats-okf-harvest/injects/harvester.md +0 -12
- package/capabilities/oats-okf-harvest/oats.json +0 -26
- package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +0 -168
- package/capabilities/oats-okf-harvest/skills/knowledge-theory/SKILL.md +0 -192
- package/capabilities/oats-okf-harvest/skills/okf-authoring/SKILL.md +0 -151
- package/capabilities/oats-okf-harvest/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
- package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +0 -170
- package/capabilities/oats-okf-maintenance/injects/maintainer.md +0 -12
- package/capabilities/oats-okf-maintenance/lib/provenance.mjs +0 -50
- package/capabilities/oats-okf-maintenance/oats.json +0 -21
- package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +0 -159
- package/capabilities/oats-okf-maintenance/skills/knowledge-theory/SKILL.md +0 -192
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/SKILL.md +0 -151
- package/capabilities/oats-okf-maintenance/skills/okf-authoring/scripts/okf-validate.mjs +0 -123
- package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +0 -146
- 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/core.mjs
CHANGED
|
@@ -44,6 +44,8 @@ import { killGroup } from "./process-group.mjs";
|
|
|
44
44
|
import { ensureHerdr, allocateHerdr, launchHerdr, inspectHerdr, stopHerdr, validHerdrTarget, herdrSnapshot, herdrCommand } from "./herdr.mjs";
|
|
45
45
|
|
|
46
46
|
import { oatsError } from "./errors.mjs";
|
|
47
|
+
import { envRows, recordedTeams, teamsEnv } from "./teams.mjs";
|
|
48
|
+
import { harnessUnavailable, launchConfigUnknown, launchLayers, launchReport, selectionFrom } from "./launch-preference.mjs";
|
|
47
49
|
async function materializePreparedDefault(prepared, home) { const m = await import("./instance-resolution.mjs"); return m.materializePrepared(prepared, home); }
|
|
48
50
|
// Capability rows for a PREPARED spawn (workspace model): the one function that
|
|
49
51
|
// turns a Resolution's modules into the row shape hooks/environment/requirements/
|
|
@@ -856,7 +858,7 @@ function readCatalogFile() {
|
|
|
856
858
|
// Capability aliases (0.19.1 migration contract): legacy capability id →
|
|
857
859
|
// official package id. Identity mappings (package id == capability id) need
|
|
858
860
|
// no entry; an alias exists for capabilities a package exports under another
|
|
859
|
-
// package identity (oats.
|
|
861
|
+
// package identity (oats.developer is exported by package oats.engineering).
|
|
860
862
|
//
|
|
861
863
|
// The object form may also carry "capability": the id the package exports
|
|
862
864
|
// TODAY in place of the legacy id (a RENAMING alias, the oas.* → oats.*
|
|
@@ -1009,9 +1011,9 @@ function layerRows(resolution) {
|
|
|
1009
1011
|
export function resolvedFromPrepared(prepared, contextDir) {
|
|
1010
1012
|
return {
|
|
1011
1013
|
capabilities: Array.isArray(prepared.capabilityRows) && prepared.capabilityRows.length ? prepared.capabilityRows : plannedCapabilityRows(prepared.resolution),
|
|
1012
|
-
workspace: { key: prepared.discovery?.key ?? null, name: prepared.discovery?.workspace?.name ?? null, deployment: prepared.deployment ?? null, commit: prepared.discovery?.commit ?? null, standalone: prepared.discovery?.standalone === true, revision: prepared.resolution.revision,
|
|
1014
|
+
workspace: { key: prepared.discovery?.key ?? null, name: prepared.discovery?.workspace?.name ?? null, deployment: prepared.deployment ?? null, commit: prepared.discovery?.commit ?? null, standalone: prepared.discovery?.standalone === true, revision: prepared.resolution.revision, slots: prepared.resolution.slots },
|
|
1013
1015
|
payloads: prepared.resolution.payloads ?? {},
|
|
1014
|
-
teams: prepared.resolution.teams ?? [], teamsSource: "live",
|
|
1016
|
+
teams: prepared.resolution.teams ?? [], defaultTeam: prepared.resolution.defaultTeam ?? null, teamsSource: "live",
|
|
1015
1017
|
layers: layerRows(prepared.resolution),
|
|
1016
1018
|
launchConfigs: launchConfigsAt(contextDir || prepared.deployment),
|
|
1017
1019
|
};
|
|
@@ -1020,20 +1022,17 @@ export function resolvedFromPrepared(prepared, contextDir) {
|
|
|
1020
1022
|
/** The resolved view of an EXISTING home: what it recorded at spawn (its
|
|
1021
1023
|
* capability rows, workspace and payloads) and its deployment's launch
|
|
1022
1024
|
* configurations. Never re-resolved against the scope. The one live part is
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
export function resolvedFromHome(home, meta, { teams, teamsSource } = {}) {
|
|
1026
|
-
// The team facts read `workspace.team` and `workspace.slots.messaging` (teamEnv); a home records
|
|
1027
|
-
// the soul's primary label under `workspace.soul.team` and its messaging module among its
|
|
1028
|
-
// capabilities — shaped here exactly as the in-home command dispatch shapes them.
|
|
1025
|
+
* the teams (team model v2): a caller that computed the home's live teams
|
|
1026
|
+
* (liveTeams) passes them; otherwise the spawn-time record. */
|
|
1027
|
+
export function resolvedFromHome(home, meta, { teams, defaultTeam, teamsSource } = {}) {
|
|
1029
1028
|
const ws = meta?.workspace && typeof meta.workspace === "object" ? meta.workspace : null;
|
|
1030
1029
|
const messaging = (Array.isArray(meta?.capabilities) ? meta.capabilities : []).find((c) => c?.layer === "messaging")?.id ?? null;
|
|
1030
|
+
const t = Array.isArray(teams) ? { teams, defaultTeam: defaultTeam ?? null, teamsSource: teamsSource === "live" ? "live" : "recorded" } : { ...recordedTeams(meta), teamsSource: "recorded" };
|
|
1031
1031
|
return {
|
|
1032
1032
|
capabilities: Array.isArray(meta?.capabilityRuntime) ? meta.capabilityRuntime : [],
|
|
1033
|
-
workspace: ws ? { ...ws,
|
|
1033
|
+
workspace: ws ? { ...ws, slots: { messaging } } : null,
|
|
1034
1034
|
payloads: meta?.providers ?? {},
|
|
1035
|
-
|
|
1036
|
-
teamsSource: Array.isArray(teams) ? (teamsSource === "live" ? "live" : "recorded") : "recorded",
|
|
1035
|
+
...t,
|
|
1037
1036
|
layers: ws && typeof ws.layers === "object" && ws.layers ? ws.layers : {}, // recorded at spawn; none before layers-from
|
|
1038
1037
|
launchConfigs: launchConfigsAt(home),
|
|
1039
1038
|
};
|
|
@@ -1377,7 +1376,7 @@ export function safeHarnessSourceRef(ref) {
|
|
|
1377
1376
|
/**
|
|
1378
1377
|
* Run a lifecycle event's hooks for every active capability. Env contract:
|
|
1379
1378
|
* OATS_EVENT/OATS_INSTANCE/OATS_HOME/OATS_AGENT/OATS_CONTEXT/OATS_LEVEL/OATS_SETTINGS/OATS_META,
|
|
1380
|
-
* plus the team/workspace facts (teamEnv: OATS_TEAM_SCOPE
|
|
1379
|
+
* plus the team/workspace facts (teamEnv: OATS_TEAM_SCOPE, OATS_DEFAULT_TEAM*, OATS_TEAMS, OATS_WORKSPACE_NAME/_KEY);
|
|
1381
1380
|
* cwd = the instance home. A hook may print JSON { meta, brief, warning, launch, env } — meta
|
|
1382
1381
|
* is persisted per capability in instance.json (and fed back as OATS_META at retire), brief
|
|
1383
1382
|
* is added to TASK.md, warning surfaces in the spawn result; launch maps harness → extra
|
|
@@ -1482,37 +1481,22 @@ export function instanceSoulDir(home, meta = undefined) {
|
|
|
1482
1481
|
return typeof meta?.soulDir === "string" && isAbsolute(meta.soulDir) ? meta.soulDir : undefined;
|
|
1483
1482
|
}
|
|
1484
1483
|
|
|
1485
|
-
/** The team facts a lifecycle hook
|
|
1486
|
-
* c3-7):
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
* provider's default (for oats.aweb, the root's active team). The provider also gets
|
|
1490
|
-
* the soul's primary team LABEL and the workspace's name and canonical key, so it can
|
|
1491
|
-
* derive the workspace's default team; each label's mapped payload is in
|
|
1492
|
-
* OATS_TEAMS.
|
|
1493
|
-
* OATS_TEAM_NAME (the 0.25 `team:` block's name) is always empty, so an ambient
|
|
1494
|
-
* value never reaches a hook. Human decision 2026-09-24 (messaging default).
|
|
1484
|
+
/** The team and workspace facts a lifecycle hook, command or provider check receives (workspace
|
|
1485
|
+
* model only, lead decision c3-7): OATS_TEAM_SCOPE is the deployment directory; OATS_WORKSPACE_NAME /
|
|
1486
|
+
* _KEY the workspace's name and canonical key; OATS_TEAM_NAME (the 0.25 `team:` block's name) is always
|
|
1487
|
+
* empty, so an ambient value never reaches a hook.
|
|
1495
1488
|
*
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1500
|
-
*
|
|
1501
|
-
* OATS_TEAMS_SOURCE says where the list came from: `live` (the workspace read now,
|
|
1502
|
-
* or a fresh resolution) or `recorded` (the spawn-time list, when a live read could
|
|
1503
|
-
* not answer or was not made); empty with OATS_TEAMS. A provider LEAVES a joined
|
|
1504
|
-
* team only on a `live` list — a recorded one lacks teams mapped since the spawn. */
|
|
1489
|
+
* Team model v2 (docs/desktop-cli-api.md, The provider environment): `resolved.teams` are the soul's
|
|
1490
|
+
* TeamRows here and `resolved.defaultTeam` its DefaultTeam; teamsEnv sets OATS_DEFAULT_TEAM(_ID,_FROM),
|
|
1491
|
+
* OATS_TEAMS (mapped rows only) and OATS_TEAMS_SOURCE (`live`: the workspace + oats-local.yaml read now,
|
|
1492
|
+
* or a fresh resolution; `recorded`: the spawn-time record). A provider LEAVES a joined team only on a
|
|
1493
|
+
* `live` list. Teams not known (`resolved.teams` not an array): every team name unset. */
|
|
1505
1494
|
export function teamEnv(resolved) {
|
|
1506
1495
|
const ws = resolved?.workspace && typeof resolved.workspace === "object" ? resolved.workspace : {};
|
|
1507
|
-
const
|
|
1508
|
-
const payload = messaging ? resolved.payloads?.[messaging] : null;
|
|
1509
|
-
const teamId = payload && typeof payload === "object" && typeof payload.team === "string" ? payload.team : "";
|
|
1496
|
+
const known = Array.isArray(resolved?.teams);
|
|
1510
1497
|
return {
|
|
1511
|
-
OATS_TEAM_NAME: "",
|
|
1512
|
-
|
|
1513
|
-
OATS_TEAM_LABELS: Array.isArray(resolved?.teams) ? resolved.teams.map((t) => t.label).join(",") : "",
|
|
1514
|
-
OATS_TEAMS: Array.isArray(resolved?.teams) ? JSON.stringify(resolved.teams) : "",
|
|
1515
|
-
OATS_TEAMS_SOURCE: Array.isArray(resolved?.teams) && (resolved.teamsSource === "live" || resolved.teamsSource === "recorded") ? resolved.teamsSource : "",
|
|
1498
|
+
OATS_TEAM_NAME: "", OATS_TEAM_SCOPE: typeof ws.deployment === "string" ? ws.deployment : "",
|
|
1499
|
+
...teamsEnv(known ? { teams: resolved.teams, defaultTeam: resolved.defaultTeam ?? null, source: resolved.teamsSource } : null),
|
|
1516
1500
|
OATS_WORKSPACE_NAME: typeof ws.name === "string" ? ws.name : "", OATS_WORKSPACE_KEY: typeof ws.key === "string" ? ws.key : "",
|
|
1517
1501
|
};
|
|
1518
1502
|
}
|
|
@@ -1881,7 +1865,9 @@ export function resolveClaudeBinary(contextDir) {
|
|
|
1881
1865
|
export function modelFromOf(modelSource, { at = "spawn", prior = null } = {}) {
|
|
1882
1866
|
if (typeof modelSource !== "string") return prior ?? null;
|
|
1883
1867
|
if (modelSource === "explicit") return at;
|
|
1884
|
-
if (modelSource === "soul default") return "soul";
|
|
1868
|
+
if (modelSource === "soul default" || modelSource === "soul preference") return "soul";
|
|
1869
|
+
if (modelSource === "local preference") return "local";
|
|
1870
|
+
if (modelSource === "local-default preference") return "local-default";
|
|
1885
1871
|
if (modelSource.startsWith("launch-config ")) return "launch-config";
|
|
1886
1872
|
if (modelSource === "recorded") return prior ?? null;
|
|
1887
1873
|
if (modelSource.startsWith("native default")) return "harness-default";
|
|
@@ -2096,7 +2082,7 @@ export function missingLaunchEnvRefs(configEnv, env = process.env) {
|
|
|
2096
2082
|
* frozen model on the same harness, else the harness's native default. */
|
|
2097
2083
|
/** Sentinel a caller passes as `model` to mean "the harness's own default, not the configured/soul preference". */
|
|
2098
2084
|
export const NATIVE_DEFAULT_MODEL = "@native-default";
|
|
2099
|
-
export function resolveLaunchSelection({ launchConfigs = {}, agent, frozen, selection = {} }) {
|
|
2085
|
+
export function resolveLaunchSelection({ launchConfigs = {}, agent, frozen, selection = {}, preference = null }) {
|
|
2100
2086
|
const bad = (code, msg) => { throw oatsError(code, msg); };
|
|
2101
2087
|
let wanted = selection.launchConfig;
|
|
2102
2088
|
let config = null;
|
|
@@ -2116,7 +2102,9 @@ export function resolveLaunchSelection({ launchConfigs = {}, agent, frozen, sele
|
|
|
2116
2102
|
config = launchConfigs[wanted];
|
|
2117
2103
|
if (selection.harness && selection.harness !== config.harness) bad("E_LAUNCH_CONFIG_MISMATCH", `launch configuration ${wanted} starts ${config.harness}; --harness ${selection.harness} disagrees with it (select another configuration, or --launch-config none with --harness)`);
|
|
2118
2104
|
}
|
|
2119
|
-
|
|
2105
|
+
// A launch preference (feature launch-preference: an inline or soul {harness, model}) stands where the
|
|
2106
|
+
// host default would: below the flags and a selected configuration.
|
|
2107
|
+
const harness = config?.harness || selection.harness || (frozen ? frozen.harness : preference?.harness || agent?.harness || "pi");
|
|
2120
2108
|
if (!LAUNCH_HARNESSES.includes(harness)) bad("E_UNSUPPORTED_HARNESS", `unknown harness "${harness}" (pi|claude|codex)`);
|
|
2121
2109
|
let model, modelSource;
|
|
2122
2110
|
// K6: an EXPLICIT "use the harness's native default" is distinct from an
|
|
@@ -2131,6 +2119,10 @@ export function resolveLaunchSelection({ launchConfigs = {}, agent, frozen, sele
|
|
|
2131
2119
|
} else if (config?.model) {
|
|
2132
2120
|
model = config.frozen ? config.model : resolveModelPreference(config.model, harness); modelSource = config.frozen ? "recorded" : `launch-config ${config.name}`;
|
|
2133
2121
|
if (!model) bad("E_MODEL_UNKNOWN", `launch configuration ${config.name} names model ${JSON.stringify(config.model)}, which has no entry usable by harness ${harness}`);
|
|
2122
|
+
} else if (preference && preference.harness === harness) {
|
|
2123
|
+
// A preference is a unit: its model, or the harness's own — never a lower layer's.
|
|
2124
|
+
model = preference.model ? resolveModelPreference(preference.model, harness) : ""; modelSource = model ? `${preference.from} preference` : "native default";
|
|
2125
|
+
if (preference.model && !model) bad("E_MODEL_UNKNOWN", `the ${preference.from} launch preference names model ${JSON.stringify(preference.model)}, which has no entry usable by harness ${harness}`);
|
|
2134
2126
|
} else if (frozen) {
|
|
2135
2127
|
if (frozen.harness === harness) { model = frozen.model || ""; modelSource = model ? "recorded" : "native default"; }
|
|
2136
2128
|
else { model = ""; modelSource = "native default (harness changed)"; }
|
|
@@ -2141,6 +2133,50 @@ export function resolveLaunchSelection({ launchConfigs = {}, agent, frozen, sele
|
|
|
2141
2133
|
} else { model = resolveModelPreference(agent?.model || "", harness); modelSource = model ? "soul default" : "native default"; }
|
|
2142
2134
|
return { config, harness, model, modelSource, configuredYolo: config?.yolo };
|
|
2143
2135
|
}
|
|
2136
|
+
/** A home's launch layers now (feature launch-preference; --reselect-launch and inspect's launchCurrent): its
|
|
2137
|
+
* RECORDED soul's launch (the soul copy it runs) and its deployment's oats-local.yaml souls.launch. */
|
|
2138
|
+
export function homeLaunchLayers(realHome, meta) {
|
|
2139
|
+
const deployment = dirname(dirname(dirname(dirname(realHome))));
|
|
2140
|
+
const found = loadLocal(deployment);
|
|
2141
|
+
if (!found.path || resolve(dirname(found.path)) !== resolve(deployment)) throw oatsError("E_LOCAL_MISSING", `${deployment} has no oats-local.yaml: --reselect-launch reads its souls.launch`);
|
|
2142
|
+
const soul = isPlainObject(meta?.workspace?.soul) ? meta.workspace.soul : {};
|
|
2143
|
+
const soulDir = instanceSoulDir(realHome, meta);
|
|
2144
|
+
const file = soulDir ? join(soulDir, "soul.yaml") : null;
|
|
2145
|
+
const definition = file && existsSync(file) ? withConfigFile(file, () => parseYamlNested(readFileSync(file, "utf8"))) : null;
|
|
2146
|
+
const key = soul.package && typeof soul.qualifiedName === "string" ? soul.qualifiedName : meta.agent;
|
|
2147
|
+
return launchLayers({ definition, entry: { repoKey: soul.repoKey ?? null, path: typeof soul.path === "string" ? soul.path : null, package: soul.package?.id ?? null }, key, local: found.local });
|
|
2148
|
+
}
|
|
2149
|
+
/** A NEW launch selection (a spawn, or a start/restart that reselects) from the flags and the launch
|
|
2150
|
+
* layers (lib/launch-preference.mjs). → { launchSelection (resolveLaunchSelection's), launchChoice:
|
|
2151
|
+
* { from, at, declared } }. A configuration a layer names but the host lacks is E_LAUNCH_CONFIG_UNKNOWN
|
|
2152
|
+
* naming the layer. Never reads a frozen recipe. */
|
|
2153
|
+
export function selectNewLaunch({ launchConfigs = {}, agent, flags = {}, layers = null }) {
|
|
2154
|
+
const choice = selectionFrom({ flags, layers });
|
|
2155
|
+
const named = choice.from !== "flag" && choice.selection.launchConfig !== undefined && choice.selection.launchConfig !== "none" ? choice.selection.launchConfig : null;
|
|
2156
|
+
if (named !== null && !Object.hasOwn(launchConfigs, named)) throw launchConfigUnknown({ name: named, from: choice.from, at: choice.at });
|
|
2157
|
+
const launchSelection = resolveLaunchSelection({ launchConfigs, agent: layers ? {} : agent, selection: choice.selection, preference: choice.preference });
|
|
2158
|
+
return { launchSelection, launchChoice: { from: choice.from, at: choice.at, declared: layers?.declared ?? null } };
|
|
2159
|
+
}
|
|
2160
|
+
/** The `Launch` report (feature launch-preference) a spawn with no flags would decide HERE, for listings
|
|
2161
|
+
* (`oats souls`, `inspect --soul`): the same layers as selectNewLaunch, but no model catalogue is probed
|
|
2162
|
+
* (`model` is the configured id) and a spawn's refusal becomes `problem` {code, message, fix}. `contextDir`
|
|
2163
|
+
* is the deployment (where a harness executable resolves). */
|
|
2164
|
+
export function launchReportFor({ layers, launchConfigs = {}, contextDir }) {
|
|
2165
|
+
const choice = selectionFrom({ layers });
|
|
2166
|
+
const report = (harness, model, launchConfig, problem = null) => launchReport({ declared: layers?.declared ?? null, harness, model, launchConfig, from: choice.from, at: choice.at, problem });
|
|
2167
|
+
let harness = "pi", model = null, config = null;
|
|
2168
|
+
if (choice.selection.launchConfig !== undefined && choice.selection.launchConfig !== "none") {
|
|
2169
|
+
// An unknown configuration launches nothing (`problem`); `effective` stays a real launch — the host
|
|
2170
|
+
// default — so a reader's closed decoder never meets a null harness.
|
|
2171
|
+
const name = choice.selection.launchConfig;
|
|
2172
|
+
if (!Object.hasOwn(launchConfigs, name)) { const e = launchConfigUnknown({ name, from: choice.from, at: choice.at }); return report("pi", null, null, { code: e.code, message: e.message, fix: `declare launch configuration ${name} (oats launch-config set), or change oats-local.yaml souls.launch` }); }
|
|
2173
|
+
config = launchConfigs[name]; harness = config.harness; model = typeof config.model === "string" && config.model ? config.model : null;
|
|
2174
|
+
} else if (choice.preference) { harness = choice.preference.harness; model = choice.preference.model ?? null; }
|
|
2175
|
+
const exe = resolveLaunchExecutable({ harness, declared: config?.executable, declaringDir: config?.source, contextDir });
|
|
2176
|
+
if (!exe.path && !config?.executable) { const e = harnessUnavailable({ harness, from: choice.from, at: choice.at, why: exe.missing }); return report(harness, model, config?.name, { code: e.code, message: e.message, fix: e.details.fix }); }
|
|
2177
|
+
if (!exe.path) return report(harness, model, config.name, { code: "E_LAUNCH_EXECUTABLE", message: `launch configuration ${config.name}: ${exe.missing}`, fix: `fix the executable of launch configuration ${config.name} (oats launch-config set)` });
|
|
2178
|
+
return report(harness, model, config?.name);
|
|
2179
|
+
}
|
|
2144
2180
|
/** The pane environment carrying each reference's value under a
|
|
2145
2181
|
* kernel-owned alias (OATS_LAUNCH_REF_<NAME>), which no configuration can
|
|
2146
2182
|
* name: the command says NAME="$OATS_LAUNCH_REF_NAME", so no source
|
|
@@ -2245,7 +2281,7 @@ function requirementsWithArgsMessage(harness, providers, config) {
|
|
|
2245
2281
|
* collects every failed check into `preflight` instead of throwing. A home
|
|
2246
2282
|
* that records no launch recipe (an earlier kernel spawned it) is not planned:
|
|
2247
2283
|
* E_LAUNCH_LEGACY, re-spawn it from the deployment. */
|
|
2248
|
-
export function planLaunch({ home, instance, meta, contextDir, agentLike, selection = {}, launchConfigs, resolvedCfg, env = process.env, preview = false, assertRoots }) {
|
|
2284
|
+
export function planLaunch({ home, instance, meta, contextDir, agentLike, selection = {}, launchConfigs, resolvedCfg, env = process.env, preview = false, assertRoots, reselect = null }) {
|
|
2249
2285
|
assertRoots?.();
|
|
2250
2286
|
const problems = [];
|
|
2251
2287
|
const fail = (check, code, detail) => { if (!preview) throw oatsError(code, detail); problems.push({ check, ok: false, detail, code }); };
|
|
@@ -2256,13 +2292,27 @@ export function planLaunch({ home, instance, meta, contextDir, agentLike, select
|
|
|
2256
2292
|
// The home's deployment declares the launch configurations (its oats-local.yaml,
|
|
2257
2293
|
// found walking up from the home) — never the context directory, which may be a
|
|
2258
2294
|
// member clone outside the deployment.
|
|
2259
|
-
const
|
|
2295
|
+
const scopeConfigs = launchConfigs || (home ? launchConfigsAt(home) : resolvedCfg?.launchConfigs) || {};
|
|
2296
|
+
// A NEW selection (feature launch-preference) — `reselect` (the home's launch layers, --reselect-launch)
|
|
2297
|
+
// or explicit --launch-config / --harness — records which layer decided it (`launchChoice`); otherwise
|
|
2298
|
+
// the recorded launch stays frozen (launchChoice null).
|
|
2299
|
+
let chosen, launchChoice = null;
|
|
2300
|
+
if (reselect) ({ launchSelection: chosen, launchChoice } = selectNewLaunch({ launchConfigs: scopeConfigs, agent: {}, flags: { launchConfig: selection.launchConfig, harness: selection.harness, model: selection.model }, layers: reselect }));
|
|
2301
|
+
else {
|
|
2302
|
+
chosen = resolveLaunchSelection({ launchConfigs: scopeConfigs, agent: agentLike, frozen, selection });
|
|
2303
|
+
if (selection.launchConfig !== undefined || selection.harness !== undefined) launchChoice = { from: "flag", at: null, declared: meta?.launchDeclared ?? null };
|
|
2304
|
+
}
|
|
2260
2305
|
const { config, harness, model, modelSource } = chosen;
|
|
2261
2306
|
const yolo = resolveYolo(selection.yolo ?? chosen.configuredYolo ?? (frozen ? frozen.yolo : agentLike?.yolo ?? resolvedCfg?.yolo));
|
|
2262
2307
|
const executable = config?.frozen
|
|
2263
2308
|
? { path: config.executablePath, declared: frozen.executableDeclared ?? null, resolvedFrom: frozen.executableResolvedFrom || "recorded", missing: existsSync(config.executablePath) ? undefined : `${config.executablePath} (recorded) does not exist` }
|
|
2264
2309
|
: resolveLaunchExecutable({ harness, declared: config?.executable, declaringDir: config?.source, contextDir });
|
|
2265
|
-
|
|
2310
|
+
if (!executable.path && !config?.executable && launchChoice) {
|
|
2311
|
+
const e = harnessUnavailable({ harness, from: launchChoice.from, at: launchChoice.at, why: executable.missing });
|
|
2312
|
+
if (!preview) throw e;
|
|
2313
|
+
problems.push({ check: "executable", ok: false, detail: e.message, code: e.code });
|
|
2314
|
+
}
|
|
2315
|
+
const exeProblem = executable.path ? checkLaunchExecutable(executable.path) : !config?.executable && launchChoice ? null : executable.missing;
|
|
2266
2316
|
if (exeProblem) fail("executable", "E_LAUNCH_EXECUTABLE", `launch configuration ${config?.name || "(harness default)"}: ${exeProblem}`); else problems.push({ check: "executable", ok: true, detail: `${executable.path} (${executable.resolvedFrom})` });
|
|
2267
2317
|
// Capability contributions: recorded at spawn with provenance; a harness
|
|
2268
2318
|
// switch needs the new harness's launch args from the same capabilities.
|
|
@@ -2330,7 +2380,7 @@ export function planLaunch({ home, instance, meta, contextDir, agentLike, select
|
|
|
2330
2380
|
const inst = instance || meta?.instance || basename(home);
|
|
2331
2381
|
const command = renderLaunchRecipe(recipe, { home, instance: inst });
|
|
2332
2382
|
const selectionSource = frozen ? (config?.frozen || (!config && !selection.launchConfig && !selection.harness) ? "frozen" : "config") : "config";
|
|
2333
|
-
return { recipe, command, harness, model: model || undefined, modelSource, yolo, config, executable, preflight: problems, ok: problems.every((c) => c.ok), selectionSource, frozen, ...(hooks.meta ? { hookMeta: hooks.meta } : {}) };
|
|
2383
|
+
return { recipe, command, harness, model: model || undefined, modelSource, yolo, config, executable, preflight: problems, ok: problems.every((c) => c.ok), selectionSource, frozen, launchChoice, warnings: hooks.warnings || [], ...(hooks.meta ? { hookMeta: hooks.meta } : {}) };
|
|
2334
2384
|
}
|
|
2335
2385
|
/** The environment a planned launch runs under: the host's base, the
|
|
2336
2386
|
* capabilities' validated env, the configuration's literals and its
|
|
@@ -2429,8 +2479,10 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
2429
2479
|
const preflightStarted = Date.now(), preflightBudgetMs = o.preview === true ? (Number(process.env.OATS_PREVIEW_PREFLIGHT_BUDGET_MS) || 20000) : undefined;
|
|
2430
2480
|
let preflight = { status: "complete", budgetMs: preflightBudgetMs ?? null };
|
|
2431
2481
|
if (o.preview === true) previewPreflightBudget = { deadline: preflightStarted + preflightBudgetMs };
|
|
2432
|
-
|
|
2433
|
-
|
|
2482
|
+
// Feature launch-preference: the flags, else the machine's souls.launch, else the soul's own launch,
|
|
2483
|
+
// else the host default — `launchChoice` says which layer decided (instance.json launchFrom/launchAt).
|
|
2484
|
+
let launchSelection, launchChoice;
|
|
2485
|
+
try { ({ launchSelection, launchChoice } = selectNewLaunch({ launchConfigs: launchConfigsAt(root), agent, flags: { launchConfig: o.launchConfig, harness: o.harness, model: o.model }, layers: o.prepared.launch })); }
|
|
2434
2486
|
catch (e) { previewPreflightBudget = null; throw e; }
|
|
2435
2487
|
const launchConfig = launchSelection.config;
|
|
2436
2488
|
const harness = launchSelection.harness;
|
|
@@ -2781,7 +2833,10 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
2781
2833
|
|
|
2782
2834
|
// Prerequisites must fail before creating a home, worktree, or identity.
|
|
2783
2835
|
const executable = resolveLaunchExecutable({ harness, declared: launchConfig?.executable, declaringDir: launchConfig?.source, contextDir: repoAbs });
|
|
2784
|
-
|
|
2836
|
+
// No executable for the chosen harness (none declared by a configuration): E_HARNESS_UNAVAILABLE names the
|
|
2837
|
+
// layer that chose it and the fix. Never a fallback to another harness.
|
|
2838
|
+
if (!executable.path && !launchConfig?.executable) { previewPreflightBudget = null; throw harnessUnavailable({ harness, from: launchChoice.from, at: launchChoice.at, why: executable.missing }); }
|
|
2839
|
+
if (!executable.path) throw oatsError("E_LAUNCH_EXECUTABLE", `launch configuration ${launchConfig.name}: ${executable.missing}`);
|
|
2785
2840
|
{ const bad = checkLaunchExecutable(executable.path); if (bad) throw oatsError("E_LAUNCH_EXECUTABLE", `launch configuration ${launchConfig?.name || "(harness default)"}: ${bad}`); }
|
|
2786
2841
|
const bin = executable.path;
|
|
2787
2842
|
const missingRefs = missingLaunchEnvRefs(launchConfig?.env || {}, process.env);
|
|
@@ -2848,24 +2903,24 @@ function* spawnBody(root, agent, o = {}) {
|
|
|
2848
2903
|
// R5 (0.25.2, decision 14): the preview shows the provider payloads the apply
|
|
2849
2904
|
// will record — `providers` is the --provider map exactly as parsed (what the
|
|
2850
2905
|
// operator typed; prepareInstance keeps it on prepared.spawn.providers) and
|
|
2851
|
-
// `settings.<cap>` is the MERGED payload per module (
|
|
2906
|
+
// `settings.<cap>` is the MERGED payload per module (workspace ⊕ soul ⊕
|
|
2852
2907
|
// local.settings ⊕ --provider), i.e. what the provider receives. Reserved and
|
|
2853
2908
|
// poison keys were refused by resolveSoul before this point (E_WORKSPACE_SCHEMA).
|
|
2854
2909
|
const preparedProviders = structuredClone(o.prepared.spawn?.providers && typeof o.prepared.spawn.providers === "object" ? o.prepared.spawn.providers : {});
|
|
2855
2910
|
// Addendum 5: where each leaf of `settings.<cap>` came from (JSON pointer → { kind, at }):
|
|
2856
|
-
// manifest-default | workspace | soul | host | spawn, the last layer that set it
|
|
2857
|
-
// entry is merged since teams amendment K, so no `workspace-team` origin appears).
|
|
2911
|
+
// manifest-default | workspace | soul | host | spawn, the last layer that set it.
|
|
2858
2912
|
const preparedSettingsOrigins = Object.fromEntries(o.prepared.resolution.modules.map((m) => [m.name, structuredClone(o.prepared.resolution.payloadOrigins?.[m.name] ?? {})]));
|
|
2859
2913
|
const preparedSettings = Object.fromEntries(o.prepared.resolution.modules.map((m) => [m.name, structuredClone(o.prepared.resolution.payloads?.[m.name] && typeof o.prepared.resolution.payloads[m.name] === "object" ? o.prepared.resolution.payloads[m.name] : {})]));
|
|
2860
2914
|
return deliver({
|
|
2861
|
-
//
|
|
2862
|
-
// Desktop offers before anything is minted; joining is the
|
|
2863
|
-
...{ modules: o.prepared.preview ?? null,
|
|
2915
|
+
// Team model v2: the soul's teams here (unmapped rows included) and its default — the choice a
|
|
2916
|
+
// Desktop offers before anything is minted; joining any but the default is the provider's explicit act.
|
|
2917
|
+
...{ modules: o.prepared.preview ?? null, teams: structuredClone(o.prepared.resolution.teams ?? []), defaultTeam: structuredClone(o.prepared.resolution.defaultTeam ?? null), resolution: o.prepared.resolution.revision, declRevision: o.prepared.resolution.declRevision ?? null, payloadRevision: o.prepared.resolution.payloadRevision ?? null, workspace: o.prepared.discovery?.key ?? null, standalone: o.prepared.discovery?.standalone === true, providers: preparedProviders, settings: preparedSettings, settingsOrigins: preparedSettingsOrigins },
|
|
2864
2918
|
spawnPreviewApi: 2, preview: true, agent: agent.name, kind: agent.kind || "persistent", instance, home, repo: repoAbs, work,
|
|
2865
2919
|
subject: o.subject ?? { soul: agent.name, agentsRoot: root, context: null },
|
|
2866
2920
|
decision, preflight,
|
|
2867
2921
|
backendStatus: launch ? { name: backend, installed: !!which(backend), started: false } : null,
|
|
2868
2922
|
harness, model: model || null, modelSource: launchSelection.modelSource ?? null, launchConfig: launchConfig?.name ?? null, yolo, backend,
|
|
2923
|
+
launch: launchReport({ declared: launchChoice.declared, harness, model, launchConfig: launchConfig?.name, from: launchChoice.from, at: launchChoice.at }),
|
|
2869
2924
|
branch: plannedBranch, base: plannedBase, worktree: work === "worktree" ? join(home, "work") : null,
|
|
2870
2925
|
relation: relation || null, parentInstance: parentInstance && parentInstance !== instance ? parentInstance : null,
|
|
2871
2926
|
policy: { childSpawns: ownChildPolicy }, executable: bin, capabilities: preparedCapabilities,
|
|
@@ -3427,6 +3482,8 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3427
3482
|
const meta = {
|
|
3428
3483
|
agent: agent.name, kind: agent.kind || "persistent", instance, home, soulDir: homeSoulTarget,
|
|
3429
3484
|
repo: repoAbs, work, branch, harness, model: model || undefined, modelFrom: modelFromOf(launchSelection.modelSource, { at: "spawn" }) ?? undefined,
|
|
3485
|
+
// Feature launch-preference: the layer that decided this launch, and the soul's own preference then.
|
|
3486
|
+
launchFrom: launchChoice.from, launchAt: launchChoice.at, launchDeclared: launchChoice.declared,
|
|
3430
3487
|
...(yolo !== undefined ? { yolo } : {}),
|
|
3431
3488
|
parentInstance: parentInstance && parentInstance !== instance ? parentInstance : undefined,
|
|
3432
3489
|
siblingInstance: siblingInstance && siblingInstance !== instance ? siblingInstance : undefined,
|
|
@@ -3503,11 +3560,13 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
|
|
|
3503
3560
|
try { const prior = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")); if (prior.modules) meta.modules = prior.modules; if (prior.providers) meta.providers = prior.providers; } catch { /* materialize wrote it; absent means nothing to carry */ }
|
|
3504
3561
|
// M5/3a: the workspace's name and the deployment directory are recorded, so a home
|
|
3505
3562
|
// answers them (inspect/operation run --home, OATS_WORKSPACE_NAME) without discovery.
|
|
3506
|
-
meta.workspace = { key: o.prepared.discovery?.key ?? null, name: o.prepared.discovery?.workspace?.name ?? null, deployment: o.prepared.deployment ?? null, commit: o.prepared.discovery?.commit ?? null, resolution: o.prepared.resolution.revision, standalone: o.prepared.discovery?.standalone === true, soul: { id: preparedSoulIdOf(o.prepared.soulEntry), repoKey: o.prepared.soulEntry.repoKey, commit: o.prepared.soulEntry.commit,
|
|
3507
|
-
//
|
|
3508
|
-
//
|
|
3509
|
-
// the LIVE set (liveTeams); this is
|
|
3510
|
-
|
|
3563
|
+
meta.workspace = { key: o.prepared.discovery?.key ?? null, name: o.prepared.discovery?.workspace?.name ?? null, deployment: o.prepared.deployment ?? null, commit: o.prepared.discovery?.commit ?? null, resolution: o.prepared.resolution.revision, standalone: o.prepared.discovery?.standalone === true, soul: { id: preparedSoulIdOf(o.prepared.soulEntry), repoKey: o.prepared.soulEntry.repoKey, commit: o.prepared.soulEntry.commit, ...(typeof o.prepared.soulEntry.package === "string" ? { name: o.prepared.soulEntry.name, qualifiedName: o.prepared.soulEntry.qualifiedName, package: { id: o.prepared.soulEntry.package, version: o.prepared.soulEntry.version, commit: o.prepared.soulEntry.commit, digest: o.prepared.soulEntry.digest, path: o.prepared.soulEntry.path } } : {}) }, layers: layerRows(o.prepared.resolution) };
|
|
3564
|
+
// Team model v2: the teams at spawn, exactly as the providers received them (OATS_TEAMS: mapped
|
|
3565
|
+
// rows only) and the default, recorded as EVIDENCE beside `providers` (never inside that
|
|
3566
|
+
// capability-keyed map). A home's hooks and operations get the LIVE set (liveTeams); this is
|
|
3567
|
+
// what they fall back to when the workspace cannot answer.
|
|
3568
|
+
meta.teams = envRows(o.prepared.resolution.teams ?? []);
|
|
3569
|
+
meta.defaultTeam = structuredClone(o.prepared.resolution.defaultTeam ?? null);
|
|
3511
3570
|
}
|
|
3512
3571
|
const spawnWarnings = warnings;
|
|
3513
3572
|
|
|
@@ -4202,6 +4261,77 @@ export function inspectInstanceSession(home) {
|
|
|
4202
4261
|
* there after the grace is reported still running, nothing is killed harder.
|
|
4203
4262
|
* A no-launch or already-idle instance is a no-op that says so. */
|
|
4204
4263
|
function tmuxServerLost(e) { return /no server running on |(?:error connecting to|failed to connect to) .*(?:No such file or directory|Connection refused)/i.test(String(e.stderr ?? e.message ?? "")); }
|
|
4264
|
+
/**
|
|
4265
|
+
* The live processes (other than this one and its children) whose working
|
|
4266
|
+
* directory is inside `home`: `lsof -a -d cwd` over the host, one call.
|
|
4267
|
+
* → { ok: true, processes: [{pid, command}] } | { ok: false, error } (the scan
|
|
4268
|
+
* could not run: a caller must treat that as unknown, never as none).
|
|
4269
|
+
*/
|
|
4270
|
+
export function processesInHome(home, io) {
|
|
4271
|
+
let out;
|
|
4272
|
+
try { out = (io?.exec || execFileSync)("lsof", ["-a", "-d", "cwd", "-F", "pRcn", "-w"], { encoding: "utf8", timeout: 20000, maxBuffer: 16 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }); }
|
|
4273
|
+
catch (e) {
|
|
4274
|
+
// lsof exits 1 with a full listing when some process could not be read; only no output is a failed scan.
|
|
4275
|
+
if (typeof e?.stdout === "string" && /^p\d+$/m.test(e.stdout)) out = e.stdout;
|
|
4276
|
+
else return { ok: false, error: e?.code === "ENOENT" ? "lsof is not on PATH" : String(e?.stderr || e?.message || "lsof failed").trim().split("\n")[0] };
|
|
4277
|
+
}
|
|
4278
|
+
const realHome = realPathOrNearest(home);
|
|
4279
|
+
const processes = [];
|
|
4280
|
+
let cur = {};
|
|
4281
|
+
for (const line of String(out).split("\n")) {
|
|
4282
|
+
const tag = line[0], value = line.slice(1);
|
|
4283
|
+
if (tag === "p") cur = { pid: Number(value) };
|
|
4284
|
+
else if (tag === "R") cur.ppid = Number(value);
|
|
4285
|
+
else if (tag === "c") cur.command = value;
|
|
4286
|
+
else if (tag === "n" && cur.pid && cur.pid !== process.pid && cur.ppid !== process.pid) {
|
|
4287
|
+
const cwd = realPathOrNearest(value);
|
|
4288
|
+
if (cwd === realHome || cwd.startsWith(`${realHome}/`)) processes.push({ pid: cur.pid, command: cur.command ?? "?" });
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
return { ok: true, processes };
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
/**
|
|
4295
|
+
* A home without a valid independent session receipt (spawned before 0.25.9)
|
|
4296
|
+
* has no endpoint OATS may quiesce. It can still be observed ABSENT, which is
|
|
4297
|
+
* all a retirement needs: `{ absent: true, backend, note }` only when
|
|
4298
|
+
* instance.json records no launch, or the recorded tmux server is gone, or it
|
|
4299
|
+
* no longer has the recorded window and no pane on it works in this home —
|
|
4300
|
+
* AND no live process on the host works in the home (a harness started by
|
|
4301
|
+
* hand on another server is invisible to the recorded endpoint).
|
|
4302
|
+
* Anything else (the window is there, a pane or a process works in the home,
|
|
4303
|
+
* the process scan cannot run, a Herdr target, a launch without an endpoint,
|
|
4304
|
+
* tmux failing otherwise) is `{ absent: false, note }`: live or ambiguous,
|
|
4305
|
+
* never killed on mutable metadata.
|
|
4306
|
+
*/
|
|
4307
|
+
export function observeSessionWithoutReceipt(home, meta, io) {
|
|
4308
|
+
const recorded = recordedSessionAbsence(home, meta, io);
|
|
4309
|
+
if (!recorded.absent) return recorded;
|
|
4310
|
+
const scan = processesInHome(home, io);
|
|
4311
|
+
if (!scan.ok) return { absent: false, backend: recorded.backend, note: `could not scan for a process working in this home (${scan.error})` };
|
|
4312
|
+
if (scan.processes.length) return { absent: false, backend: recorded.backend, note: `a process works in this home (${scan.processes.slice(0, 5).map((p) => `pid ${p.pid} ${p.command}`).join(", ")}${scan.processes.length > 5 ? `, and ${scan.processes.length - 5} more` : ""}); stop it, then retire` };
|
|
4313
|
+
return recorded;
|
|
4314
|
+
}
|
|
4315
|
+
function recordedSessionAbsence(home, meta, io) {
|
|
4316
|
+
const tmux = meta?.tmux;
|
|
4317
|
+
if (meta?.sessionTarget) return { absent: false, backend: "herdr", note: "instance.json records a Herdr session, which cannot be observed without the receipt" };
|
|
4318
|
+
// A --no-launch home keeps its planned tmux names with `launched: false`: nothing was started there.
|
|
4319
|
+
if (meta?.launched !== true) return { absent: true, backend: null, note: "no session receipt (a home from before 0.25.9); instance.json records no launch" };
|
|
4320
|
+
if (!tmux) return { absent: false, backend: null, note: "instance.json records a launch but no session endpoint" };
|
|
4321
|
+
if (![tmux.session, tmux.window, tmux.socket].every((v) => typeof v === "string" && v.length > 0)) return { absent: false, backend: "tmux", note: "instance.json records an incomplete tmux endpoint" };
|
|
4322
|
+
const where = `${tmux.session}:${tmux.window} on ${tmux.socket}`;
|
|
4323
|
+
let rows;
|
|
4324
|
+
try { rows = tmuxOn(tmux.socket, ["list-panes", "-a", "-F", "#{session_name}\t#{window_name}\t#{pane_current_path}"], io).split("\n").filter(Boolean).map((l) => l.split("\t")); }
|
|
4325
|
+
catch (e) {
|
|
4326
|
+
if (tmuxServerLost(e)) return { absent: true, backend: "tmux", note: `no session receipt (a home from before 0.25.9); the recorded tmux server ${tmux.socket} is not running` };
|
|
4327
|
+
return { absent: false, backend: "tmux", note: `tmux could not list ${tmux.socket}: ${String(e.stderr ?? e.message ?? "").trim() || "tmux failed"}` };
|
|
4328
|
+
}
|
|
4329
|
+
if (rows.some(([s, w]) => s === tmux.session && w === tmux.window)) return { absent: false, backend: "tmux", note: `the recorded window ${where} is still present` };
|
|
4330
|
+
const realHome = realPathOrNearest(home);
|
|
4331
|
+
const inHome = rows.find(([, , cwd]) => { const p = cwd ? realPathOrNearest(cwd) : ""; return p === realHome || p.startsWith(`${realHome}/`); });
|
|
4332
|
+
if (inHome) return { absent: false, backend: "tmux", note: `a tmux pane (${inHome[0]}:${inHome[1]}) on ${tmux.socket} works in this home` };
|
|
4333
|
+
return { absent: true, backend: "tmux", note: `no session receipt (a home from before 0.25.9); the recorded window ${where} is gone` };
|
|
4334
|
+
}
|
|
4205
4335
|
export function stopInstanceSession(home, o = {}) {
|
|
4206
4336
|
if (typeof home !== "string" || !isAbsolute(home)) throw oatsError("E_BAD_ARGS", "session stop needs an absolute instance home");
|
|
4207
4337
|
if (o.graceMs !== undefined && (!Number.isSafeInteger(o.graceMs) || o.graceMs < 1 || o.graceMs > 300000)) throw oatsError("E_BAD_ARGS", "stop grace must be 1-300000 ms");
|
|
@@ -4464,6 +4594,7 @@ export function prepareLaunchHooks({ frozen, harness, resolvedCfg, home, meta, c
|
|
|
4464
4594
|
const contributions = (frozen.hooks?.contributions || []).map((c) => ({ ...c }));
|
|
4465
4595
|
const env = { ...(frozen.hooks?.env || {}) };
|
|
4466
4596
|
const refreshed = [];
|
|
4597
|
+
let warnings = [];
|
|
4467
4598
|
// The providers that took part in this home's launch are the CAPTURED ones
|
|
4468
4599
|
// (its recorded contributions). Each is resolved by id through the scope's
|
|
4469
4600
|
// current installed manifest and trust, whatever the scope's bindings say
|
|
@@ -4512,12 +4643,15 @@ export function prepareLaunchHooks({ frozen, harness, resolvedCfg, home, meta, c
|
|
|
4512
4643
|
// renewed session grant, for instance — left the ORIGINAL id on record and
|
|
4513
4644
|
// retire undid the wrong one. Carry it to the caller; the start records it.
|
|
4514
4645
|
hookMeta = res.meta && Object.keys(res.meta).length ? res.meta : undefined;
|
|
4646
|
+
// The hooks' advisory warnings go to the start's answer and events, as
|
|
4647
|
+
// spawn's do; they were dropped here before 0.30.
|
|
4648
|
+
warnings = [...(res.warnings || [])];
|
|
4515
4649
|
}
|
|
4516
4650
|
const unprepared = contributions.filter((c) => !refreshed.includes(c.capability) && c.launch && c.launch[frozen.harness] !== undefined && c.launch[harness] === undefined).map((c) => c.capability);
|
|
4517
4651
|
if (unprepared.length) throw oatsError("E_LAUNCH_PREPARATION", `${unprepared.join(", ")} contributed ${frozen.harness} launch arguments at spawn and none for ${harness}; change that capability's setting (for example its delivery mode), or the provider must declare a launch hook; nothing was stopped`);
|
|
4518
4652
|
const launch = {};
|
|
4519
4653
|
for (const c of contributions) { for (const [rt, args] of Object.entries(c.launch || {})) if (args) launch[rt] = `${launch[rt] ? `${launch[rt]} ` : ""}${args}`; }
|
|
4520
|
-
return { launch, env, contributions, refreshed, ...(hookMeta ? { meta: hookMeta } : {}) };
|
|
4654
|
+
return { launch, env, contributions, refreshed, warnings, ...(hookMeta ? { meta: hookMeta } : {}) };
|
|
4521
4655
|
}
|
|
4522
4656
|
|
|
4523
4657
|
|
|
@@ -4582,7 +4716,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4582
4716
|
// The independent receipt first (retire and session consult it), then the
|
|
4583
4717
|
// mutable metadata; both tmp+rename. A failure between them is what the
|
|
4584
4718
|
// pending receipt exists for.
|
|
4585
|
-
const record = (meta, { id, backend, target, model, command, startedAt, reused, launch, harness: newHarness, yolo: newYolo, stop, nativeRecordId, hookMeta, modelFrom }, clearPending = true) => {
|
|
4719
|
+
const record = (meta, { id, backend, target, model, command, startedAt, reused, launch, harness: newHarness, yolo: newYolo, stop, nativeRecordId, hookMeta, modelFrom, launchFrom, launchAt, launchDeclared }, clearPending = true) => {
|
|
4586
4720
|
checkRoots();
|
|
4587
4721
|
const baselinePath = retirementBaselinePath(realHome);
|
|
4588
4722
|
let baseline;
|
|
@@ -4598,6 +4732,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4598
4732
|
if (!recorded) restarts.push({ startedAt, model: model ?? null, reused });
|
|
4599
4733
|
const next = { ...meta, model, command, launched: true, startId: id, restarts, restartCount: (meta.restartCount || 0) + (recorded ? 0 : 1), ...(modelFrom ? { modelFrom } : {}),
|
|
4600
4734
|
...(launch ? { launch } : {}), ...(newHarness ? { harness: newHarness } : {}), ...(newYolo !== undefined ? { yolo: newYolo } : {}),
|
|
4735
|
+
...(launchFrom ? { launchFrom, launchAt: launchAt ?? null, launchDeclared: launchDeclared ?? null } : {}),
|
|
4601
4736
|
// Launch-hook meta lands per capability over the spawn's record; a hook
|
|
4602
4737
|
// that answered without meta keeps its previous entry (retire reads it).
|
|
4603
4738
|
...(hookMeta ? { capabilityMeta: { ...(meta.capabilityMeta || {}), ...hookMeta } } : {}) };
|
|
@@ -4605,7 +4740,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4605
4740
|
else { next.tmux = { session: target.session, window: target.window, socket: resolve(target.socket) }; delete next.sessionTarget; }
|
|
4606
4741
|
writeJsonAtomic(metaPath, next);
|
|
4607
4742
|
if (clearPending) { checkRoots(); rmSync(pendingPath, { force: true }); }
|
|
4608
|
-
return { instance: meta.instance, agent: meta.agent, home: realHome, harness: next.harness, backend, model: model ?? null, launchConfig: next.launch?.launchConfig ?? null, yolo: next.yolo ?? null, target, startedAt, restartCount: next.restartCount, reused, ...(nativeRecordId ? { nativeRecordId } : {}), ...(stop ? { stop } : {}) };
|
|
4743
|
+
return { instance: meta.instance, agent: meta.agent, home: realHome, harness: next.harness, backend, model: model ?? null, launchConfig: next.launch?.launchConfig ?? null, yolo: next.yolo ?? null, target, startedAt, restartCount: next.restartCount, reused, warnings: [], ...(nativeRecordId ? { nativeRecordId } : {}), ...(stop ? { stop } : {}) };
|
|
4609
4744
|
};
|
|
4610
4745
|
try { mkdirSync(lock); }
|
|
4611
4746
|
catch (e) {
|
|
@@ -4679,9 +4814,12 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4679
4814
|
// model, re-rendered in place), or, under a selection, the recipe
|
|
4680
4815
|
// re-resolved against the home's current scoped configuration. Every
|
|
4681
4816
|
// preflight happens here, before anything is observed or stopped.
|
|
4682
|
-
|
|
4817
|
+
// --reselect-launch (feature launch-preference): apply the home's launch layers now (souls.launch,
|
|
4818
|
+
// the recorded soul's launch, the host default) instead of the frozen recipe.
|
|
4819
|
+
const reselect = o.reselectLaunch === true ? homeLaunchLayers(realHome, meta) : null;
|
|
4820
|
+
const selected = reselect !== null || o.launchConfig !== undefined || o.harness !== undefined || o.yolo !== undefined;
|
|
4683
4821
|
const hasRecipe = meta.launch && typeof meta.launch === "object";
|
|
4684
|
-
let launchPlan = null, explicitModelFrom = null;
|
|
4822
|
+
let launchPlan = null, explicitModelFrom = null, warnings = [];
|
|
4685
4823
|
if (selected || hasRecipe) {
|
|
4686
4824
|
// Every start of a home with a recipe (ordinary, model-only, or under a
|
|
4687
4825
|
// selection) goes through the one planner: recipe shape, the recorded
|
|
@@ -4689,13 +4827,18 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4689
4827
|
// the captured settings and current trust. A home without a recipe that
|
|
4690
4828
|
// is asked for a selection is refused (E_LAUNCH_LEGACY).
|
|
4691
4829
|
const context = meta.repo && existsSync(meta.repo) ? resolve(meta.repo) : dirname(dirname(dirname(dirname(realHome))));
|
|
4692
|
-
// o.teams: the home's LIVE
|
|
4693
|
-
// launch hook re-checks joined memberships against them
|
|
4694
|
-
const resolvedCfg = resolvedFromHome(realHome, meta, { teams: o.teams, teamsSource: o.teamsSource });
|
|
4830
|
+
// o.teams / o.defaultTeam: the home's LIVE teams, computed by an async caller (liveTeams) —
|
|
4831
|
+
// the launch hook re-checks joined memberships against them.
|
|
4832
|
+
const resolvedCfg = resolvedFromHome(realHome, meta, { teams: o.teams, defaultTeam: o.defaultTeam, teamsSource: o.teamsSource });
|
|
4695
4833
|
let agent; try { agent = findAgent(dirname(dirname(dirname(realHome))), meta.agent); } catch { agent = undefined; }
|
|
4696
|
-
const plan = planLaunch({ home: realHome, instance: meta.instance, meta, contextDir: context, agentLike: agent || { harness: meta.harness, model: meta.model, yolo: meta.yolo }, selection: { launchConfig: o.launchConfig, harness: o.harness, model: o.model, yolo: o.yolo }, resolvedCfg, env: o.env || process.env, assertRoots: checkRoots });
|
|
4697
|
-
launchPlan = { recipe: plan.recipe, command: plan.command, harness: plan.harness, model: plan.model, yolo: plan.yolo, modelFrom: modelFromOf(plan.modelSource, { at: "start", prior: meta.modelFrom ?? null }), ...(plan.hookMeta ? { hookMeta: plan.hookMeta } : {})
|
|
4834
|
+
const plan = planLaunch({ home: realHome, instance: meta.instance, meta, contextDir: context, agentLike: agent || { harness: meta.harness, model: meta.model, yolo: meta.yolo }, selection: { launchConfig: o.launchConfig, harness: o.harness, model: o.model, yolo: o.yolo }, reselect, resolvedCfg, env: o.env || process.env, assertRoots: checkRoots });
|
|
4835
|
+
launchPlan = { recipe: plan.recipe, command: plan.command, harness: plan.harness, model: plan.model, yolo: plan.yolo, modelFrom: modelFromOf(plan.modelSource, { at: "start", prior: meta.modelFrom ?? null }), ...(plan.hookMeta ? { hookMeta: plan.hookMeta } : {}),
|
|
4836
|
+
...(plan.launchChoice ? { launchFrom: plan.launchChoice.from, launchAt: plan.launchChoice.at, launchDeclared: plan.launchChoice.declared } : {}) };
|
|
4698
4837
|
command = launchPlan.command; model = launchPlan.model;
|
|
4838
|
+
// The launch hooks have run: their warnings are events now, whatever
|
|
4839
|
+
// the start does next, and the answer carries them.
|
|
4840
|
+
warnings = plan.warnings;
|
|
4841
|
+
for (const message of warnings) appendEvent(realHome, { kind: "launch-warning", data: { message } });
|
|
4699
4842
|
} else if (o.model !== undefined && o.model !== null && String(o.model).trim() !== "") {
|
|
4700
4843
|
const resolved = resolveModelPreference(String(o.model), harness);
|
|
4701
4844
|
if (!resolved) throw oatsError("E_MODEL_UNKNOWN", `model preference ${JSON.stringify(o.model)} has no entry usable by harness ${harness}; give a ${harness} model id`);
|
|
@@ -4711,6 +4854,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4711
4854
|
const paneEnvExports = paneEnv.map((r) => `export ${r.name}=${shq(r.value)}; `).join("");
|
|
4712
4855
|
checkRoots(); // launch hooks/preparation have run; no backend has been observed
|
|
4713
4856
|
const planExtra = launchPlan ? { launch: launchPlan.recipe, harness: launchPlan.harness, yolo: launchPlan.yolo, ...(launchPlan.modelFrom ? { modelFrom: launchPlan.modelFrom } : {}),
|
|
4857
|
+
...(launchPlan.launchFrom ? { launchFrom: launchPlan.launchFrom, launchAt: launchPlan.launchAt, launchDeclared: launchPlan.launchDeclared } : {}),
|
|
4714
4858
|
...(launchPlan.hookMeta ? { hookMeta: launchPlan.hookMeta } : {}) } : (explicitModelFrom ? { modelFrom: explicitModelFrom } : {});
|
|
4715
4859
|
let target = receipt.target;
|
|
4716
4860
|
let state = { present: false, state: "not-launched" };
|
|
@@ -4810,7 +4954,7 @@ export function startInstanceSession(home, o = {}) {
|
|
|
4810
4954
|
// Keep launch evidence until the command exits or the target disappears.
|
|
4811
4955
|
// A transient child (for example cat TASK.md) is not proof that startup
|
|
4812
4956
|
// has finished. A later start reconciles the receipt without a watcher.
|
|
4813
|
-
try { return record(meta, { id, backend, target, model, command, startedAt, reused, ...planExtra, ...(stopReceipt ? { stop: stopReceipt } : {}) }, false); }
|
|
4957
|
+
try { return { ...record(meta, { id, backend, target, model, command, startedAt, reused, ...planExtra, ...(stopReceipt ? { stop: stopReceipt } : {}) }, false), warnings }; }
|
|
4814
4958
|
catch (e) {
|
|
4815
4959
|
if (e.code && String(e.code).startsWith("E_")) throw e;
|
|
4816
4960
|
throw oatsError("E_SESSION_START_INCOMPLETE", `${meta.instance} was started (${backend === "herdr" ? `Herdr pane ${target.paneId}` : `tmux ${target.session}:${target.window} on ${target.socket}`}) but its metadata could not be recorded: ${e.message}; the actual target is kept in ${pendingPath} and the next start adopts it instead of allocating another`);
|
|
@@ -5384,9 +5528,17 @@ export function retireInstance(root, name, o = {}) {
|
|
|
5384
5528
|
let runtimeAuthority;
|
|
5385
5529
|
if (liveMeta || quarantine) {
|
|
5386
5530
|
runtimeAuthority = initialObservation.runtimeAuthority;
|
|
5387
|
-
|
|
5388
|
-
|
|
5531
|
+
// A home without its receipt (spawned before 0.25.9) has nothing OATS may
|
|
5532
|
+
// quiesce; when its session is observably absent, quiescing is vacuous and
|
|
5533
|
+
// the retirement goes on (hooks, work preservation). Live or ambiguous
|
|
5534
|
+
// still refuses, --force included: a harness is never left running under
|
|
5535
|
+
// a removed home.
|
|
5536
|
+
const unowned = runtimeAuthority ? null : observeSessionWithoutReceipt(found.home, meta);
|
|
5537
|
+
if (unowned && !unowned.absent) {
|
|
5538
|
+
throw oatsError("E_RUNTIME_ENDPOINT_UNKNOWN", `cannot quiesce ${name}: independent runtime endpoint authority is missing or invalid, and its session is not observably absent (${unowned.note}); stop that session yourself, then retire again`);
|
|
5389
5539
|
}
|
|
5540
|
+
}
|
|
5541
|
+
if (runtimeAuthority) {
|
|
5390
5542
|
const endpointAgrees = runtimeAuthority.sessionTarget
|
|
5391
5543
|
? !meta.tmux && ["backend", "binary", "socket", "workspaceId", "paneId", "terminalId", "protocol"].every((key) => meta.sessionTarget?.[key] === runtimeAuthority.sessionTarget[key])
|
|
5392
5544
|
: !meta.sessionTarget && meta.tmux?.session === runtimeAuthority.tmux?.session
|
package/lib/instance-events.mjs
CHANGED
|
@@ -26,7 +26,7 @@ export const EVENTS_API = 2;
|
|
|
26
26
|
const HOME_LOG = ".oats-events.jsonl";
|
|
27
27
|
const MAX_BYTES = 4 * 1024 * 1024;
|
|
28
28
|
|
|
29
|
-
export const EVENT_KINDS = ["spawned", "launched", "restarted", "stopped", "stop-refused", "retire-planned", "retired", "worktree-retained", "worktree-removed", "branch-deleted", "child-spawn-refused", "recomposed"];
|
|
29
|
+
export const EVENT_KINDS = ["spawned", "launched", "restarted", "stopped", "stop-refused", "retire-planned", "retired", "worktree-retained", "worktree-removed", "branch-deleted", "child-spawn-refused", "launch-warning", "recomposed"];
|
|
30
30
|
|
|
31
31
|
function workspaceLogPath(home) {
|
|
32
32
|
// <workspace>/.agents/events/<agent>--<instance>.jsonl — deployment-private
|