@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.
Files changed (232) hide show
  1. package/README.md +12 -6
  2. package/bin/oats.mjs +203 -54
  3. package/capabilities/oats-aweb/bin/oats-aweb.mjs +538 -204
  4. package/capabilities/oats-aweb/injects/aweb.md +1 -1
  5. package/capabilities/oats-aweb/lib/binding-wire.mjs +31 -22
  6. package/capabilities/oats-aweb/oats.json +5 -12
  7. package/capabilities/oats-aweb/skills/VENDORED.md +4 -4
  8. package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +1 -1
  9. package/capabilities/oats-aweb/skills/oats-aweb/SKILL.md +83 -13
  10. package/capabilities/oats-code-review/injects/reviewer.md +26 -0
  11. package/capabilities/oats-code-review/oats.json +16 -0
  12. package/capabilities/oats-code-review/skills/adversarial-review/SKILL.md +66 -0
  13. package/capabilities/oats-code-review/skills/review-dev-docs/SKILL.md +30 -0
  14. package/capabilities/oats-code-review/skills/security-review/SKILL.md +56 -0
  15. package/capabilities/oats-code-review/skills/simplification-review/SKILL.md +34 -0
  16. package/capabilities/oats-developer/injects/developer.md +38 -0
  17. package/capabilities/oats-developer/oats.json +17 -0
  18. package/capabilities/oats-developer/skills/execution-strategy/SKILL.md +43 -0
  19. package/capabilities/oats-developer/skills/maintain-dev-docs/SKILL.md +47 -0
  20. package/capabilities/oats-developer/skills/run-the-review-loop/SKILL.md +65 -0
  21. package/capabilities/oats-developer/skills/understand-the-spec/SKILL.md +37 -0
  22. package/capabilities/oats-developer/skills/worktrees/SKILL.md +36 -0
  23. package/capabilities/oats-engineering-expert/injects/expert.md +37 -0
  24. package/capabilities/oats-engineering-expert/oats.json +17 -0
  25. package/capabilities/oats-engineering-expert/skills/coordinate-developers/SKILL.md +37 -0
  26. package/capabilities/oats-engineering-expert/skills/coordinate-experts/SKILL.md +52 -0
  27. package/capabilities/oats-engineering-expert/skills/land-your-prs/SKILL.md +50 -0
  28. package/capabilities/oats-engineering-expert/skills/plan-and-spec/SKILL.md +53 -0
  29. package/capabilities/oats-engineering-expert/skills/verify-developer-work/SKILL.md +49 -0
  30. package/capabilities/oats-okf/bin/oats-okf.mjs +16 -9
  31. package/capabilities/oats-okf/lib/binding-wire.mjs +47 -15
  32. package/capabilities/oats-okf/lib/config.mjs +2 -1
  33. package/capabilities/oats-okf/lib/consult.mjs +26 -4
  34. package/capabilities/oats-okf/lib/harvest-switch.mjs +16 -3
  35. package/capabilities/oats-okf/lib/inspection.mjs +26 -7
  36. package/capabilities/oats-okf/lib/io.mjs +9 -1
  37. package/capabilities/oats-okf/lib/sources.mjs +34 -3
  38. package/capabilities/oats-okf/lib/stores.mjs +8 -6
  39. package/capabilities/oats-okf/lib/worker.mjs +7 -17
  40. package/capabilities/oats-okf/oats.json +6 -3
  41. package/capabilities/oats-okf-harvest/bin/okf-harvest.mjs +2 -2
  42. package/capabilities/oats-okf-harvest/oats.json +3 -3
  43. package/capabilities/oats-okf-harvest/skills/knowledge-harvest/SKILL.md +6 -6
  44. package/capabilities/oats-okf-maintenance/bin/okf-maintenance.mjs +37 -16
  45. package/capabilities/oats-okf-maintenance/injects/maintainer.md +1 -1
  46. package/capabilities/oats-okf-maintenance/lib/provenance.mjs +6 -1
  47. package/capabilities/oats-okf-maintenance/oats.json +2 -2
  48. package/capabilities/oats-okf-maintenance/skills/knowledge-review/SKILL.md +17 -2
  49. package/capabilities/oats-okf-maintenance/skills/okf-trigger-setup/SKILL.md +11 -23
  50. package/capabilities/oats-workspace-experts/injects/oats-experts.md +26 -0
  51. package/capabilities/oats-workspace-experts/oats.json +9 -0
  52. package/docs/capabilities.md +160 -171
  53. package/docs/capability-manifest.schema.json +7 -10
  54. package/docs/configuration.md +213 -64
  55. package/docs/design/2026-09-16-knowledge-capability-contract.md +36 -50
  56. package/docs/design/2026-09-23-workspace-module-contracts.md +377 -544
  57. package/docs/design/2026-09-26-okf-knowledge-operations.md +132 -357
  58. package/docs/design/2026-09-27-team-model-v2.md +116 -0
  59. package/docs/design/2026-09-28-automations-trust.md +38 -0
  60. package/docs/design/2026-09-28-soul-launch-preference.md +63 -0
  61. package/docs/design/HISTORY.md +65 -0
  62. package/docs/design/README.md +23 -54
  63. package/docs/desktop-cli-api.md +1787 -1777
  64. package/docs/desktop.md +30 -91
  65. package/docs/execution-targets.md +146 -292
  66. package/docs/first-team.md +31 -17
  67. package/docs/implementation.md +76 -288
  68. package/docs/integrations.md +118 -320
  69. package/docs/knowledge-capability-authoring.md +25 -52
  70. package/docs/knowledge-reference/acceptance.md +3 -3
  71. package/docs/knowledge-reference/adoption.md +1 -1
  72. package/docs/knowledge-reference/harvester.md +2 -2
  73. package/docs/knowledge-reference/package-craft.md +3 -3
  74. package/docs/knowledge-reference/provider-mapping.md +3 -6
  75. package/docs/knowledge-reference/reader-capture.md +3 -3
  76. package/docs/knowledge-theory.md +62 -166
  77. package/docs/knowledge.md +225 -404
  78. package/docs/layers.md +42 -97
  79. package/docs/oats-local.schema.json +58 -5
  80. package/docs/oats-membership.schema.json +1 -8
  81. package/docs/oats-package.schema.json +5 -5
  82. package/docs/oats-workspace.schema.json +8 -22
  83. package/docs/official-catalog.md +25 -28
  84. package/docs/packages.md +45 -63
  85. package/docs/plans/0.30-close-out.md +61 -0
  86. package/docs/release-lane.md +77 -0
  87. package/docs/release-notes/oats-framework-v1.1.3.md +10 -8
  88. package/docs/release-notes/v0.19.0.md +48 -147
  89. package/docs/release-notes/v0.19.1.md +2 -3
  90. package/docs/release-notes/v0.19.3.md +2 -15
  91. package/docs/release-notes/v0.20.0.md +0 -15
  92. package/docs/release-notes/v0.22.0.md +71 -138
  93. package/docs/release-notes/v0.22.1.md +42 -90
  94. package/docs/release-notes/v0.22.10.md +1 -1
  95. package/docs/release-notes/v0.22.11.md +1 -47
  96. package/docs/release-notes/v0.22.12.md +4 -13
  97. package/docs/release-notes/v0.22.13.md +1 -42
  98. package/docs/release-notes/v0.22.14.md +3 -11
  99. package/docs/release-notes/v0.22.15.md +1 -46
  100. package/docs/release-notes/v0.22.16.md +6 -8
  101. package/docs/release-notes/v0.22.18.md +1 -99
  102. package/docs/release-notes/v0.22.19.md +3 -14
  103. package/docs/release-notes/v0.22.2.md +6 -15
  104. package/docs/release-notes/v0.22.3.md +0 -1
  105. package/docs/release-notes/v0.22.4.md +1 -14
  106. package/docs/release-notes/v0.22.5.md +2 -12
  107. package/docs/release-notes/v0.22.6.md +0 -3
  108. package/docs/release-notes/v0.23.0.md +9 -25
  109. package/docs/release-notes/v0.23.1.md +9 -25
  110. package/docs/release-notes/v0.23.2.md +2 -4
  111. package/docs/release-notes/v0.24.0.md +56 -97
  112. package/docs/release-notes/v0.24.1.md +7 -11
  113. package/docs/release-notes/v0.24.10.md +34 -45
  114. package/docs/release-notes/v0.24.11.md +12 -20
  115. package/docs/release-notes/v0.24.12.md +35 -48
  116. package/docs/release-notes/v0.24.13.md +34 -41
  117. package/docs/release-notes/v0.24.2.md +9 -13
  118. package/docs/release-notes/v0.24.3.md +7 -11
  119. package/docs/release-notes/v0.24.4.md +6 -6
  120. package/docs/release-notes/v0.24.5.md +6 -10
  121. package/docs/release-notes/v0.24.6.md +2 -5
  122. package/docs/release-notes/v0.24.7.md +46 -75
  123. package/docs/release-notes/v0.24.8.md +58 -96
  124. package/docs/release-notes/v0.24.9.md +38 -54
  125. package/docs/release-notes/v0.25.0.md +59 -76
  126. package/docs/release-notes/v0.25.1.md +57 -81
  127. package/docs/release-notes/v0.25.2.md +51 -70
  128. package/docs/release-notes/v0.25.3.md +11 -13
  129. package/docs/release-notes/v0.25.4.md +9 -13
  130. package/docs/release-notes/v0.25.5.md +3 -5
  131. package/docs/release-notes/v0.25.6.md +20 -29
  132. package/docs/release-notes/v0.25.7.md +5 -7
  133. package/docs/release-notes/v0.25.8.md +26 -39
  134. package/docs/release-notes/v0.26.0.md +175 -646
  135. package/docs/release-notes/v0.27.0.md +4 -5
  136. package/docs/release-notes/v0.27.1.md +4 -6
  137. package/docs/release-notes/v0.27.2.md +1 -1
  138. package/docs/release-notes/v0.28.0.md +57 -124
  139. package/docs/release-notes/v0.29.0.md +89 -208
  140. package/docs/release-notes/v0.29.1.md +1 -1
  141. package/docs/release-notes/v0.29.2.md +3 -4
  142. package/docs/release-notes/v0.29.4.md +90 -0
  143. package/docs/release-notes/v0.30.0.md +205 -0
  144. package/docs/schedules.md +280 -349
  145. package/docs/servers.md +99 -117
  146. package/docs/soul.schema.json +2 -9
  147. package/docs/souls-and-instances.md +145 -158
  148. package/docs/workspaces.md +132 -215
  149. package/lib/automations.mjs +28 -6
  150. package/lib/core.mjs +226 -74
  151. package/lib/instance-events.mjs +1 -1
  152. package/lib/instance-inspect.mjs +109 -34
  153. package/lib/instance-lifecycle.mjs +14 -1
  154. package/lib/instance-resolution.mjs +26 -27
  155. package/lib/launch-preference.mjs +87 -0
  156. package/lib/materialize.mjs +3 -3
  157. package/lib/packages.mjs +2 -5
  158. package/lib/resolve.mjs +29 -87
  159. package/lib/schedule.mjs +32 -18
  160. package/lib/teams-verbs.mjs +195 -0
  161. package/lib/teams.mjs +190 -0
  162. package/lib/triggers.mjs +53 -17
  163. package/lib/workspace.mjs +54 -147
  164. package/package-catalog.json +9 -15
  165. package/package.json +1 -1
  166. package/skills/oats-getting-started/SKILL.md +25 -13
  167. package/capabilities/oats-review/injects/review.md +0 -69
  168. package/capabilities/oats-review/oats.json +0 -10
  169. package/capabilities/oats-review/skills/code-review/SKILL.md +0 -44
  170. package/capabilities/oats-review/skills/security-review/SKILL.md +0 -59
  171. package/docs/conventions.md +0 -90
  172. package/docs/design/2026-09-07-architecture-reassessment.md +0 -131
  173. package/docs/design/2026-09-07-desktop-souls-capabilities.md +0 -50
  174. package/docs/design/2026-09-07-mobile-agent-management-proposal.md +0 -228
  175. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +0 -558
  176. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +0 -744
  177. package/docs/design/2026-09-13-knowledge-implementation.md +0 -127
  178. package/docs/design/2026-09-13-knowledge-location-contract.md +0 -340
  179. package/docs/design/2026-09-14-artifact-retention-contract.md +0 -190
  180. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +0 -708
  181. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +0 -85
  182. package/docs/design/2026-09-14-portable-souls-explainer.md +0 -750
  183. package/docs/design/2026-09-15-captured-dispatch.md +0 -127
  184. package/docs/design/2026-09-15-captured-resolution-records.md +0 -143
  185. package/docs/design/2026-09-15-package-preparation.md +0 -100
  186. package/docs/design/2026-09-15-portable-data-contract.md +0 -121
  187. package/docs/design/2026-09-15-portable-declarations.md +0 -189
  188. package/docs/design/2026-09-15-portable-souls-handoff.md +0 -150
  189. package/docs/design/2026-09-15-portable-souls-implementation.md +0 -417
  190. package/docs/design/2026-09-15-selection-lock-and-approval.md +0 -122
  191. package/docs/design/2026-09-15-source-observation.md +0 -119
  192. package/docs/design/2026-09-16-captured-admission.md +0 -77
  193. package/docs/design/2026-09-16-captured-helper-dispatch.md +0 -105
  194. package/docs/design/2026-09-16-captured-launch-inputs.md +0 -42
  195. package/docs/design/2026-09-16-command-profile-preparation.md +0 -86
  196. package/docs/design/2026-09-16-fresh-install-first-rollout.md +0 -47
  197. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +0 -282
  198. package/docs/design/2026-09-16-messaging-capability-contract.md +0 -59
  199. package/docs/design/2026-09-16-portable-migration-evidence.md +0 -158
  200. package/docs/design/2026-09-16-portable-onboarding.md +0 -179
  201. package/docs/design/2026-09-16-prepare-request-transport.md +0 -26
  202. package/docs/design/2026-09-16-provider-binding-codecs.md +0 -98
  203. package/docs/design/2026-09-16-provider-binding-wire.md +0 -274
  204. package/docs/design/2026-09-17-capability-helper-input-contract.md +0 -95
  205. package/docs/design/2026-09-17-captured-backend-parity.md +0 -53
  206. package/docs/design/2026-09-17-captured-native-start.md +0 -58
  207. package/docs/design/2026-09-17-portable-boundary-hookup.md +0 -19
  208. package/docs/design/2026-09-17-portable-boundary-resources.md +0 -52
  209. package/docs/design/2026-09-17-public-captured-start.md +0 -108
  210. package/docs/design/2026-09-17-public-prepare-request.md +0 -90
  211. package/docs/design/2026-09-18-captured-pi-host.md +0 -205
  212. package/docs/design/2026-09-18-first-cut-release-checklist.md +0 -131
  213. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +0 -60
  214. package/docs/design/2026-09-20-redesign-program-board.md +0 -142
  215. package/docs/design/2026-09-20-workspace-and-portable-adoption-plan.md +0 -289
  216. package/docs/design/2026-09-20-workspace-onboarding-public.md +0 -207
  217. package/docs/design/2026-09-22-desktop-parity-seams.md +0 -58
  218. package/docs/design/2026-09-23-simplified-workspace-model.md +0 -711
  219. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +0 -65
  220. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +0 -242
  221. package/docs/design/2026-09-24-phase-d-plan.md +0 -305
  222. package/docs/design/2026-09-25-teams-contract.md +0 -258
  223. package/docs/design/2026-09-26-desktop-design-brief-architecture.md +0 -241
  224. package/docs/design/desktop-ux-plan.md +0 -362
  225. package/docs/design/launch-configurations.md +0 -168
  226. package/docs/design/okf-mirror-provenance.md +0 -105
  227. package/docs/design/operations-contract.md +0 -141
  228. package/docs/oats-member.schema.json +0 -38
  229. package/skills/integration-authoring/SKILL.md +0 -84
  230. package/skills/oats-support/SKILL.md +0 -79
  231. package/skills/skill-craft/SKILL.md +0 -109
  232. 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.review is exported by package oats.dev).
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, team: prepared.resolution.soul?.team ?? null, slots: prepared.resolution.slots },
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
- * `teams` (teams contract decision 6): a caller that computed the home's live
1024
- * eligible teams (liveTeams) passes them; otherwise the spawn-time record. */
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, team: ws.soul?.team ?? null, slots: { messaging } } : null,
1033
+ workspace: ws ? { ...ws, slots: { messaging } } : null,
1034
1034
  payloads: meta?.providers ?? {},
1035
- teams: Array.isArray(teams) ? teams : Array.isArray(meta?.teams) ? meta.teams : null,
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/_ID/_LABEL, OATS_WORKSPACE_NAME/_KEY);
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 receives (workspace model only, lead decision
1486
- * c3-7): the team SCOPE is the deployment directory, the team ID is the messaging
1487
- * slot's merged payload `team` (workspace base ⊕ soul ⊕ host ⊕ spawn; no byTeam entry
1488
- * is merged since teams amendment K): the workspace's default team if one is set; empty = the
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
- * Teams contract 2026-09-25 (decision 4): `resolved.teams` is the ELIGIBLE teams,
1497
- * `[{ label, team, mapped, payload }]` in soul order. OATS_TEAM_LABELS is their
1498
- * labels comma-joined and OATS_TEAMS their JSON (`[]` = no label: the workspace's default team only).
1499
- * Both are empty when the teams are not known (a home spawned before 0.26.0 whose
1500
- * workspace cannot be read now) — empty means "unknown", never "none".
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 messaging = ws.slots?.messaging;
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: "", OATS_TEAM_ID: teamId, OATS_TEAM_SCOPE: typeof ws.deployment === "string" ? ws.deployment : "",
1512
- OATS_TEAM_LABEL: typeof ws.team === "string" ? ws.team : "",
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
- const harness = config?.harness || selection.harness || (frozen ? frozen.harness : agent?.harness || "pi");
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 chosen = resolveLaunchSelection({ launchConfigs: launchConfigs || (home ? launchConfigsAt(home) : resolvedCfg?.launchConfigs) || {}, agent: agentLike, frozen, selection });
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
- const exeProblem = executable.path ? checkLaunchExecutable(executable.path) : executable.missing;
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
- let launchSelection;
2433
- try { launchSelection = resolveLaunchSelection({ launchConfigs: launchConfigsAt(root), agent, selection: { launchConfig: o.launchConfig, harness: o.harness, model: o.model } }); }
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
- if (!executable.path) throw new Error(executable.missing);
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 (soul ⊕ workspace byTeam ⊕
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 (no byTeam
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
- // Teams contract (item 7): the ELIGIBLE teams (one per soul label, primary first) — the choice a
2862
- // Desktop offers before anything is minted; joining is the messaging provider's explicit act.
2863
- ...{ modules: o.prepared.preview ?? null, team: o.prepared.soulEntry?.team ?? null, teams: structuredClone(o.prepared.resolution.teams ?? []), 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 },
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, team: o.prepared.soulEntry.team ?? null, labels: [...(o.prepared.soulEntry.labels ?? (o.prepared.soulEntry.team ? [o.prepared.soulEntry.team] : []))], ...(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) };
3507
- // Teams contract (decision 6): the eligible teams at spawn, recorded as EVIDENCE beside
3508
- // `providers` (never inside that capability-keyed map). A home's hooks and operations get
3509
- // the LIVE set (liveTeams); this is what they fall back to when discovery cannot answer.
3510
- meta.teams = structuredClone(o.prepared.resolution.teams ?? []);
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
- const selected = o.launchConfig !== undefined || o.harness !== undefined || o.yolo !== undefined;
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 eligible teams, computed by an async caller (liveTeams) — the
4693
- // launch hook re-checks joined memberships against them (teams contract decision 6).
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
- if (!runtimeAuthority) {
5388
- throw oatsError("E_RUNTIME_ENDPOINT_UNKNOWN", `cannot quiesce ${name}: independent runtime endpoint authority is missing or invalid`);
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
@@ -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