@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
@@ -1,13 +1,8 @@
1
- # Workspaces — one workspace per organisation, members are trust, nothing is installed
1
+ # Workspaces: one workspace per organisation, members are trust, nothing is installed
2
2
 
3
- This is the OATS workspace model (v2, the 0.25 line). It replaces the per-soul
4
- `source:` grammar, the installed-capability tier and `oats-config.yaml`. The
5
- normative record is the Decision concept
6
- `agents/oats-expert/soul/knowledge/decisions/workspace-model-v2.md`; the module
7
- contracts the kernel is built against are in
8
- [design/2026-09-23-workspace-module-contracts.md](design/2026-09-23-workspace-module-contracts.md);
9
- a full worked example (an imaginary company with three teams) is in
10
- [design/2026-09-23-simplified-workspace-model.md](design/2026-09-23-simplified-workspace-model.md).
3
+ This is the OATS workspace model. The module contracts the kernel is built
4
+ against are in
5
+ [design/2026-09-23-workspace-module-contracts.md](design/2026-09-23-workspace-module-contracts.md).
11
6
 
12
7
  ## The rule
13
8
 
@@ -44,7 +39,7 @@ shapes; domain rules (declared teams, duplicate members, canonical `from:` keys,
44
39
  the two `packages:` value forms) live in the kernel's `validateWorkspace` /
45
40
  `validateSoul`, which are the authority.
46
41
 
47
- ### `oats-workspace.yaml` — the one shared declaration
42
+ ### `oats-workspace.yaml`: the one shared declaration
48
43
 
49
44
  Lives in the repository that **hosts** the workspace (often a dedicated
50
45
  `agents` repo, but any member can host it). One per organisation.
@@ -59,13 +54,13 @@ members: # repo refs, NO @revision (E_WORKSPAC
59
54
  - git:github.com/acme/tools # a member that ALSO publishes a package (see below)
60
55
 
61
56
  packages: # the ONLY versioned things
62
- oats.framework: v1.3.1 # bare version → resolves through the official catalog
63
- oats.okf: v4.0.0
57
+ oats.framework: v1.4.0 # bare version → resolves through the official catalog
58
+ oats.okf: v4.0.4
64
59
  acme.tools: git:github.com/acme/tools@v0.4.0 # outside the catalog → git:<repo>@<tag|OID>; still a package
65
60
 
66
- teams: # labels, declared once so they cannot drift
67
- global: { description: Org-wide souls and house capabilities }
68
- engineering: { description: Platform and release automation }
61
+ teams: # SHARED teams: the same provider team for everyone
62
+ engineering: { description: Platform and release automation, team: "engineering:acme.aweb.ai" }
63
+ reviewers: { description: Code review } # declared, not created yet (no `team` id): readiness team-unmapped
69
64
 
70
65
  defaults:
71
66
  capabilities:
@@ -74,9 +69,6 @@ defaults:
74
69
  knowledge: { oats.okf: { from: package } } # one slot default at most; a soul may say `none`
75
70
  messaging: none
76
71
  tasks: none
77
- byTeam:
78
- engineering:
79
- capabilities: { acme-release-tooling: { from: github.com/acme/agents } }
80
72
 
81
73
  stores: # knowledge stores, declared once
82
74
  org: git:github.com/acme/knowledge
@@ -97,92 +89,68 @@ exactly as the kernel spells it (`parseRepoRef(ref).key`: lowercase host,
97
89
  file/bare-directory remote). Any other spelling is a schema error at
98
90
  validation, not a late membership error.
99
91
 
100
- ### `oats-membership.yaml` — the backlink, in every member
92
+ ### `oats-membership.yaml`: the backlink, in every member
101
93
 
102
94
  ```yaml
103
95
  schemaVersion: 2
104
96
  workspace: git:github.com/acme/agents # "I am a member of acme"
105
- team: engineering # optional: default team label for this repo's items
106
97
  ```
107
98
 
108
- Nothing else. It replaces `oats.yaml`; there are no export lists.
99
+ Nothing else: there are no export lists, and team membership is local to each
100
+ deployment ([Teams](#teams)).
109
101
 
110
- ### `souls/<name>/soul.yaml` — where each capability comes from
102
+ ### `souls/<name>/soul.yaml`: where each capability comes from
111
103
 
112
104
  ```yaml
113
105
  schemaVersion: 2
114
106
  name: release-manager
115
107
  description: Cuts, verifies and announces releases.
116
108
  work: worktree # worktree | checkout | directory | workspace
117
- team: engineering # optional; else the repo's default; else "unassigned"
118
109
 
119
110
  capabilities:
120
111
  acme-release-tooling: { from: here } # `here` = the repo this soul.yaml lives in
121
112
  acme-deploy: { from: package } # provided by acme.tools, pinned in packages:
122
113
  acme-house-style: off # removes a workspace default
123
114
 
124
- knowledge: # provider payload, opaque to the kernel — the slot capability's BINDING keys
125
- harvest-runtime: claude # (oats.okf 2.1.3: what this soul owns/reads is in souls/<name>/okf.json, not here — see below)
115
+ knowledge: # provider payload, opaque to the kernel: the slot capability's settings
116
+ harvest: off # (what this soul owns and reads is in souls/<name>/okf.json, not here)
126
117
  messaging:
127
118
  channels: [acme-eng]
128
119
  tasks: none # empties the slot
129
120
 
130
- compatibility: # optional FLOORS on package versions — constraints, not sources
131
- oats.okf: ">=2.1"
121
+ compatibility: # optional FLOORS on package versions: constraints, not sources
122
+ oats.okf: ">=4.0"
132
123
  ```
133
124
 
134
125
  Beside it: `AGENTS.md` (canonical), `CLAUDE.md → AGENTS.md`, `skills/`, and
135
- whatever the slot providers read from the soul directory — for `oats.okf`
136
- 2.1.3 that is **`okf.json`** (`{ version: 1, owner, owns: ["<base>/<node>"],
137
- reads: […] }`, written by `oats okf init|migrate`), the soul's knowledge
138
- declaration; it travels with the soul into the per-commit soul cache. The
139
- `knowledge:` payload on `soul.yaml` reaches OKF as `OATS_SETTINGS` and may
140
- carry only the binding's settings keys (`bindings-file`, `state-dir`,
141
- `harvest-runtime`, `harvest-model`); `owns`/`reads`/`root` there are refused by
142
- the provider, not read (a soul payload grammar is an OKF follow-up). Every
143
- `souls/*/soul.yaml` in a member is listed and spawnable — souls have no
144
- private mode (`private:` in a soul.yaml is ignored since 0.26.0, with a
145
- `soul-private-ignored` warning). A soul's
146
- `name` must equal its directory name; the first of two souls declaring one
147
- name (by path) is listed, the second is a problem.
148
-
149
- ### `capabilities/<name>/oats.json` — the manifest, unchanged shape
150
-
151
- The capability manifest is the one file that did not change (see
152
- [capabilities.md](capabilities.md)). Discovery relies on `capability` (the same
126
+ whatever the slot providers read from the soul directory. For `oats.okf` that
127
+ is **`okf.json`** (`{ version: 1, owner, owns: ["<base>/<node>"], reads: […] }`),
128
+ the soul's knowledge declaration; it travels with the soul into the per-commit
129
+ soul cache ([knowledge.md](knowledge.md)). A slot payload on `soul.yaml` reaches
130
+ the provider as `OATS_SETTINGS` and may carry only the settings the provider's
131
+ manifest declares; the provider refuses any other key.
132
+
133
+ Every `souls/*/soul.yaml` in a member is listed and spawnable; souls have no
134
+ private mode. A soul's `name` must equal its directory name; the first of two
135
+ souls declaring one name (by path) is listed, the second is a problem.
136
+
137
+ ### `capabilities/<name>/oats.json`: the manifest
138
+
139
+ The capability manifest is described in [capabilities.md](capabilities.md).
140
+ Discovery relies on `capability` (the same
153
141
  `^[a-z0-9][a-z0-9._-]*$` grammar every `capabilities:` key uses), `version`,
154
- `layer`, and may read `private: true` (a **repo-owned** capability) and
155
- `team: <label>`. `version` is
142
+ `layer`, and may read `private: true` (a **repo-owned** capability). `version` is
156
143
  informational for member capabilities — a materialized copy is identified by
157
144
  its content digest.
158
145
 
159
- ### `oats-local.yaml` — the only per-machine file
146
+ ### `oats-local.yaml`: the only per-machine file
160
147
 
161
- ```yaml
162
- schemaVersion: 2
163
- workspace: git:github.com/acme/agents # observed over the remote; need not be cloned
164
- clones: # optional: where member clones live, if not the convention
165
- github.com/acme/platform: /Users/ana/src/acme-platform
166
- settings: # host-owned values the manifests ask for
167
- oats.okf:
168
- bindings-file: /Users/ana/.oats/okf-bindings.json
169
- state-dir: /Users/ana/.oats/okf
170
- souls:
171
- disabled: [data-analyst] # not run on this machine (E_SOUL_DISABLED); oats.okf/knowledge-harvester names a package soul
172
- host:
173
- name: ana-laptop # this machine's name: runs the workspace triggers/schedules whose runsOn names it
174
- triggers:
175
- disabled: [knowledge/okf-harvest-review] # workspace triggers this host does not run (oats trigger disable)
176
- schedules:
177
- disabled: [platform/nightly-digest] # workspace schedules this host does not run (oats schedule disable)
178
- ```
148
+ Which workspace this machine realizes, where member clones live, host-owned
149
+ provider settings, this deployment's local teams and team membership, host
150
+ facts for automations, and launch configurations. The full reference is
151
+ [configuration.md](configuration.md).
179
152
 
180
- `host`, `triggers.disabled` and `schedules.disabled` (0.29.0) are machine facts:
181
- see [schedules.md#workspace-triggers-and-schedules](schedules.md#workspace-triggers-and-schedules).
182
-
183
- See [configuration.md](configuration.md). `oats-config.yaml` no longer exists.
184
-
185
- ### `oats-lock.json` — lock v3
153
+ ### `oats-lock.json`: lock v3
186
154
 
187
155
  Written by `oats sync`; the only persisted state at the deployment besides
188
156
  `oats-local.yaml`. See [packages.md](packages.md).
@@ -194,16 +162,11 @@ Written by `oats sync`; the only persisted state at the deployment besides
194
162
  the workspace back. One file per side; a copied backlink in a fork, or a folder
195
163
  with the right name, is not admission.
196
164
 
197
- **Membership is the whole trust decision for member capabilities** — the same
165
+ **Membership is the whole trust decision for member capabilities**, the same
198
166
  model as a repo's committed `.agents/skills/`: whoever can push to the repo
199
- decides what runs, and the branch's latest state is what runs. No per-operator
200
- trust lists, no per-capability approval for members. Packages come from
201
- *outside* that boundary, and **declaring one in the workspace's `packages:` is
202
- the trust decision** (human decision, 2026-09-24): people install a package only
203
- when they trust it, so there is no second, per-version approval step. The lock
204
- is reproducibility, not approval — it pins the exact commit and content
205
- integrity, and `oats sync` refuses drift (a moved tag, changed content, an
206
- edited capability list). A spawn admits only a locked package the workspace
167
+ decides what runs, and the branch's latest state is what runs. Packages come
168
+ from *outside* that boundary, and **declaring one in the workspace's
169
+ `packages:` is the trust decision** ([packages.md](packages.md#trust)). A spawn admits only a locked package the workspace
207
170
  **still declares**: one removed from `packages:` but left in a stale lock is
208
171
  `E_PACKAGE_MISSING { reason: "undeclared" }` until `oats sync` drops it.
209
172
 
@@ -237,15 +200,15 @@ no private mode: every soul of a confirmed member is listed and spawnable.
237
200
  **not** a member, pinned to a full commit. No handshake is asked for and none is
238
201
  read; the soul gets no member-tier capabilities of its own repo; it is
239
202
  "source-complete" (its skills travel with it) and the workspace's defaults fill
240
- its slots. An `external[].team` overrides the soul's own `team`.
203
+ its slots.
241
204
 
242
- **Package souls.** A package may ship souls (`souls:` in `oats-package.json`,
243
- 0.28.0): they are listed from the lock for each package the workspace declares,
205
+ **Package souls.** A package may ship souls (`souls:` in `oats-package.json`):
206
+ they are listed from the lock for each package the workspace declares,
244
207
  named `<package>/<soul>` (a bare name when unique), resolved like any soul
245
208
  (`from: here` = their own package at the locked commit) and trusted as the
246
209
  package is. See [packages](packages.md#package-souls).
247
210
 
248
- ## Member tier vs package tier — the non-collapse rule
211
+ ## Member tier vs package tier: the non-collapse rule
249
212
 
250
213
  A repository may be a **member** (it completed the handshake; its `souls/*` and
251
214
  `capabilities/*` are member-tier: latest state, trusted by membership) **and** a
@@ -264,44 +227,24 @@ A repository may be a **member** (it completed the handshake; its `souls/*` and
264
227
  package's capabilities as member capabilities.
265
228
 
266
229
  So the framework's own souls say `oats.okf: { from: package }` even though
267
- `oats-okf` is a member of the OATS workspace — and every package repo carries a
268
- member soul that is the expert in that capability (`okf-expert`, `aweb-expert`,
269
- …), discoverable at latest state like any member soul.
230
+ `oats-okf` is a member of the OATS workspace, and every package repo carries a
231
+ member soul that is the expert in that capability (`oats-okf-expert`,
232
+ `oats-aweb-expert`, …), discoverable at latest state like any member soul.
270
233
 
271
234
  ## Packages, lock, catalog
272
235
 
273
- `packages:` values have exactly two forms:
274
-
275
- - a **bare version** (`v2.1.3`, `2.1.3`, `v1.0.0-rc.1`) — resolved through the
276
- official catalog (`package-catalog.json` in the `oats` repo; the reviewed
277
- list, see [official-catalog.md](official-catalog.md)). This is
278
- the only way a package becomes *pinnable by id*.
279
- - **`git:<repo>@<ref>`** — a direct package ref: `<repo>` is any ref the kernel
280
- understands (`github.com/org/repo`, `https://…`, `git@host:…`, `/abs/bare.git`,
281
- `file:///…`), `<ref>` a tag name or a full commit OID. The package is read at
282
- `oats-package/` inside that repo.
283
-
284
- Both are packages: versioned and locked. A ref that resolves to a **branch** is
285
- refused (`E_PACKAGE_INTEGRITY { why: "branch" }`) — versions are immutable. A
286
- tag that moved (same version string, different commit), or content that no
287
- longer matches the locked integrity, fails with `E_PACKAGE_INTEGRITY` on the
288
- next `oats sync`.
289
-
290
- **There is no package approval** (human decision, 2026-09-24). Declaring a
291
- package in `packages:` is the trust decision; `oats sync` asks nothing and
292
- `--approve` is `E_BAD_ARGS`. `oats sync` confirms membership, resolves every
293
- `packages:` entry to a commit + content digest, writes `oats-lock.json`
294
- (lockfileVersion 3), creates `agents/` if absent, reports what changed and
295
- exits `0`. A lock written by an earlier kernel may still carry an `approved`
296
- record per entry: it is ignored, and the next write drops it. `oats package add <id> <version|git:…@…>` / `oats package remove <id>` edit
297
- `packages:` in the workspace file when it is tracked by the current checkout,
298
- else print the line to add — the workspace file is shared through Git. Details:
299
- [packages.md](packages.md).
236
+ `packages:` values have two forms, a **bare version** resolved through the
237
+ official catalog and a **`git:<repo>@<ref>`** direct ref; both are versioned
238
+ and locked, and a ref that resolves to a branch is refused. `oats sync`
239
+ confirms membership, resolves every entry to a commit and content digest,
240
+ writes `oats-lock.json`, creates `agents/` if absent and reports what changed.
241
+ `oats package add | remove` edit `packages:`. The grammar, the lock, trust and
242
+ the catalog are in [packages.md](packages.md).
300
243
 
301
244
  ## Resolution, spelled out
302
245
 
303
246
  For each `(name, from)` in
304
- `defaults.<slot>` ⊕ `defaults.capabilities` ⊕ `defaults.byTeam[<soul team>]` ⊕
247
+ `defaults.<slot>` ⊕ `defaults.capabilities` ⊕
305
248
  `soul.capabilities` (later wins; `off` removes; a soul `<slot>: none` drops
306
249
  the workspace's slot default):
307
250
 
@@ -326,30 +269,13 @@ preview and apply is `E_DECISION_STALE`, not a silent drift.
326
269
 
327
270
  ## Materialization and the instance home
328
271
 
329
- At spawn every resolved capability is **copied whole** into the instance home —
330
- skills, injects, scripts, hooks — from the remote at the recorded commit.
331
- Nothing is symlinked, nothing is shared between instances.
332
-
333
- ```
334
- <agents-root>/<soul>/instances/<instance>/
335
- ├── AGENTS.md # composed: soul AGENTS.md + kernel/work-mode blocks + each module's inject
336
- │ # (the "You run on OATS" block is oats.core's inject; the kernel ships no copy)
337
- ├── CLAUDE.md → AGENTS.md
338
- ├── .agents/skills/<capability>/<skill>/SKILL.md # full copies; where pi/codex look
339
- ├── .claude/skills → ../.agents/skills
340
- ├── .oats/modules/<capability>/ # the full capability copy: oats.json, bin/, injects/, skills/
341
- ├── instance.json # modules{}, providers{}, workspace{} recorded here
342
- ├── TASK.md
343
- └── work/
344
- ```
345
-
346
- `instance.json.modules.<cap>` records `from` (`{ kind: "member", repoKey,
347
- commit }` or `{ kind: "package", package, version, commit, integrity, repoKey }`),
348
- `commit`, `digest` (sha256 of the copied tree) and `materializedAt`;
349
- `instance.json.providers.<cap>` records the merged provider payload. A running
350
- instance never changes under itself: a member moving or `packages:` being
351
- bumped affects only new spawns. Details and DTOs:
352
- [souls-and-instances.md](souls-and-instances.md), [desktop-cli-api.md](desktop-cli-api.md).
272
+ At spawn every resolved capability is **copied whole** into the instance home
273
+ (skills, injects, scripts, hooks) from the remote at the recorded commit, and
274
+ `instance.json` records each module's source, commit and digest. Nothing is
275
+ symlinked or shared between instances, and a running instance never changes
276
+ under itself: a member moving or `packages:` being bumped affects only new
277
+ spawns. The home's layout and records are in
278
+ [souls-and-instances.md](souls-and-instances.md).
353
279
 
354
280
  **Drift is shown, not prevented.** `oats status` compares each instance's
355
281
  recorded modules — and its recorded **soul source** (`instance.json.workspace.soul`)
@@ -361,29 +287,61 @@ carries `instances[].soul`. `oats spawn --preview` lists `changedSince` the
361
287
  newest previous instance of the same soul, plus `providers` (the `--provider`
362
288
  map as given) and `settings.<cap>` (the merged payload each provider receives).
363
289
 
364
- **Harnesses start normally.** OATS is a skill contributor, not a skill sandbox:
365
- cwd = the instance home, the harness's own skill discovery intact
366
- (`~/.pi/agent/skills`, `.agents/skills` up the tree, `.claude/`, …); machine-
367
- and repo-level skills resolve exactly as they would without OATS. OATS
368
- composes instructions (`AGENTS.md`) and pins model/provider settings; it does
369
- not exclude anything.
290
+ Harnesses start normally, with their own skill discovery intact
291
+ ([souls-and-instances.md](souls-and-instances.md)).
370
292
 
371
293
  ## Teams
372
294
 
373
- `teams:` declares labels once (`global`, `engineering`, …) so they cannot drift
374
- into typos. A soul carries `team:` — one label or a list (`team: [engineering,
375
- reviewers]`, the first the primary) — else its repo's default from
376
- `oats-membership.yaml` (same shape), else `unassigned`; a capability carries one
377
- label. A label not declared in `teams:` is `E_TEAM_UNKNOWN` (the item is still
378
- listed); a declared label without a `messaging.byTeam` entry is the
379
- `unmapped-team-label` warning. `defaults.byTeam.<team>.capabilities` adds
380
- capabilities additively for souls with that label, for each label in order
381
- (`off` removes; two labels that disagree are `E_TEAM_CONFLICT`). Each label is
382
- an *eligible* messaging team the provider may join on request — see
383
- [capabilities.md](capabilities.md#several-team-labels). **A
384
- label never gates, restricts, changes trust or partitions the knowledge
385
- store** — it organises and can supply defaults. The messaging provider's payload
386
- (private teams, channels) lives under `messaging:`, so "team" means one thing.
295
+ A team is a messaging-provider team (for oats.aweb, an
296
+ aweb team id `<team>:<namespace>`) under a **label**. Two files declare them:
297
+
298
+ - **Shared teams**: the committed `oats-workspace.yaml` `teams.<label> =
299
+ { description?, team? }`: the same provider team for everyone, edited by a PR.
300
+ A shared team without `team` is declared but not created yet (readiness
301
+ `team-unmapped`): its owner creates it with the messaging provider, then
302
+ commits the id.
303
+ - **Local teams**: the deployment's `oats-local.yaml` `teams.<label> = { team,
304
+ description? }`: a team only this deployment uses (a personal team). A label in
305
+ both files is `team-label-collision` (a warning); the **shared** definition
306
+ wins, and the fix is renaming the local label.
307
+
308
+ `oats-local.yaml` also says which teams each soul belongs to **here**:
309
+
310
+ - `defaultTeam: <label>`: the team every instance of this deployment lives in
311
+ (its default-team identity);
312
+ - `souls.teams`: `"*"` for every soul, and a soul's own entry (its bare name, or
313
+ `<package>/<soul>` for a package soul) adds to it;
314
+ - `souls.default`: a per-soul override of `defaultTeam`; it must be one of that
315
+ soul's teams (`E_TEAM_NOT_ELIGIBLE`).
316
+
317
+ A soul's default is `souls.default[soul] ?? defaultTeam`; its teams are that
318
+ default ∪ `souls.teams["*"]` ∪ `souls.teams[soul]`. A label no file declares is
319
+ `E_TEAM_UNKNOWN` (a spawn, preview or `inspect --soul` of that soul is
320
+ refused). At spawn an instance joins its **default** only; the others are
321
+ eligible: offered, and joined on request through the provider (`join=` at
322
+ spawn, or its own verbs later). Nothing committed besides the shared `teams:`
323
+ says anything about teams, and capabilities compose from the workspace defaults
324
+ and the soul only, the same for everyone. **A label never gates, restricts,
325
+ changes trust or partitions the knowledge store.**
326
+
327
+ The verbs edit `oats-local.yaml` in place; they never call a provider:
328
+
329
+ ```
330
+ oats teams [--json] # this deployment's teams, ids, the default, problems
331
+ oats teams add <label> --team <id> [--description d] # declare a local team (the first one becomes the default)
332
+ oats teams remove <label> # refused while referenced (E_TEAM_IN_USE) or shared (E_TEAM_SHARED)
333
+ oats teams default <label>
334
+ oats soul teams <soul>|'*' [--add a,b] [--remove a,b] [--default <label> | --clear-default] [--json]
335
+ ```
336
+
337
+ The messaging provider's own setup creates provider teams and records them with
338
+ `oats teams add` (see the provider's documentation). The spawn preview, `inspect` and `oats souls` report a soul's
339
+ `teams` and `defaultTeam`; readiness reports the team problems in
340
+ `checks.configured` (`E_TEAM_UNCONFIGURED` when a messaging layer is active and
341
+ there is no default; `team-unmapped`, blocking when it is the default;
342
+ `default-team-changed` for a running instance). The provider receives them in
343
+ its environment — see [capabilities.md](capabilities.md#teams-in-the-provider-environment).
344
+ Exact shapes: [desktop-cli-api.md](desktop-cli-api.md#team-model-v2-feature-team-model-2-oats-0300-replaces-feature-teams).
387
345
 
388
346
  ## Provider payloads have three homes
389
347
 
@@ -393,46 +351,17 @@ store** — it organises and can supply defaults. The messaging provider's paylo
393
351
  | A fact about this machine | `oats-local.yaml` → `settings.<cap>.<key>` (absolute paths are refused in the workspace file) | `settings.oats.okf.state-dir: /Users/ana/.oats/okf` |
394
352
  | A fact about **this spawn** | `oats spawn … --provider <cap> key=value` (repeatable; dotted keys nest) → `instance.json.providers.<cap>` | `--provider oats.aweb identity.source=/abs/path/to/retained/.aw` |
395
353
 
396
- The merged payload is `workspace.messaging` (messaging slot only; its base
397
- keys, with `byTeam` stripped) ⊕ soul slot payload ⊕ `local.settings[cap]` ⊕
398
- `spawn.providers[cap]` — objects deep-merge, later wins on scalars and arrays.
399
- **No `byTeam[<label>]` is merged into it, the primary's included** (teams
400
- amendment K): each label's `base ⊕ byTeam[label]` reaches the provider only as
401
- that label's entry in `OATS_TEAMS` (the preview's `teams`). So `settings.team`
402
- (and `OATS_TEAM_ID`) is the workspace's default team if the host, the soul or the spawn
403
- set one; empty means the provider's own default. The provider's own `binding` contract
404
- (`normalize → bind → check`) runs over the merged payload exactly as before.
405
- Two teams, two messaging identities, one workspace:
354
+ The merged payload is `workspace.messaging` (messaging slot only) ⊕ soul slot
355
+ payload ⊕ `local.settings[cap]` ⊕ `spawn.providers[cap]` — objects deep-merge,
356
+ later wins on scalars and arrays. The provider's own `binding` contract
357
+ (its readiness check) runs over the merged payload. Teams are **not** settings:
358
+ they reach the provider beside them, in its environment ([Teams](#teams)).
359
+ Where a provider keeps its own state (for oats.aweb, its identity roots) is the
360
+ provider's concern; see its documentation.
406
361
 
407
- ```yaml
408
- teams: { oss: { description: Open protocol }, cloud: { description: Hosted application } }
409
- messaging:
410
- byTeam:
411
- oss: { team: aweb:example.oss }
412
- cloud: { team: aweb:example.cloud }
413
- ```
414
-
415
- A soul with `team: cloud` hands its messaging provider the eligible team
416
- `{ label: cloud, team: aweb:example.cloud, mapped: true, payload: { team: aweb:example.cloud, … } }`
417
- in `OATS_TEAMS`; its settings carry no `team` unless the host, soul or spawn set
418
- one. A label under `byTeam` that is not declared in `teams:` is
419
- `E_WORKSPACE_SCHEMA`. **Joining an eligible team is the provider's explicit
420
- act.** `spawn --preview` shows the merged `settings.<cap>` and the `teams`, so
421
- the delivery is verifiable, and `instance.json` records both. With oats.aweb
422
- 1.13.1 (which reads `team` from its settings and ignores `OATS_TEAMS`) the
423
- primary identity therefore mints into the workspace's default team: the `.aw` root's
424
- active team, or the one the host set. What the payload does not change is
425
- **where the `.aw` root is found**: the hook still searches
426
- bounded candidates, first hit wins — the instance home, the Git repository
427
- containing it, the soul's work repository and the Git repository containing
428
- it, then the deployment directory (`OATS_WORKSPACE`); never the user home or
429
- above the deployment — and that root must hold a membership of the named team (the deployment's `.aw`
430
- joined to every team its labels name is the simple layout). On oats.aweb
431
- 1.11.2 `team` was ignored (the root's active team won), so `byTeam` there is
432
- a recorded intent only.
433
362
  A store (`stores: { <name>: <repo ref> }`) names a repository; where a
434
- knowledge base lives inside it is the knowledge provider's own concern — for
435
- OKF 2.1.3 that is the **bindings file** (`bases.<alias>.repository` + `root`,
363
+ knowledge base lives inside it is the knowledge provider's own concern. For
364
+ oats.okf that is the **bindings file** (`bases.<alias>.repository` + `root`,
436
365
  `oats-local.yaml settings.oats.okf.bindings-file`), not a soul payload key; a
437
366
  repo ref never carries a `#path`.
438
367
 
@@ -460,11 +389,11 @@ different repo → `E_CLONE_MISMATCH`. Spawning a soul whose repo is not yet
460
389
  cloned is a guided clone-then-spawn, a job for the onboarding skill, not the
461
390
  kernel.
462
391
 
463
- The deployment directory is **yours to choose** (decision 9) — an existing folder that already holds your member clones is the usual case; `oats onboard <dir>` adds what the kernel needs and nothing else:
392
+ The deployment directory is **yours to choose**: an existing folder that already holds your member clones is the usual case, and `oats onboard <dir>` adds what the kernel needs and nothing else ([configuration.md](configuration.md#the-deployment-directory)):
464
393
 
465
394
  ```
466
395
  ~/acme/ ← the directory you chose
467
- ├── oats-local.yaml ← which workspace this machine realizes + host paths + disabled souls
396
+ ├── oats-local.yaml ← which workspace this machine realizes, and host facts
468
397
  ├── oats-lock.json ← exact commit + integrity per package
469
398
  ├── agents/ ← instance homes (each self-contained) + fetched soul sources
470
399
  ├── platform/ ← clone of github.com/acme/platform (only if someone works IN it; may live elsewhere — see clones:)
@@ -500,7 +429,7 @@ that view explicitly). `oats onboard` lists the host among the clones to make
500
429
  like any member (the host is a member; its souls may need a work clone); under
501
430
  an explicit `standalone:` header its next steps say so and name that one repo.
502
431
 
503
- **Executables from public members.** Membership is the trust (decision 2): a
432
+ **Executables from public members.** Membership is the trust: a
504
433
  member capability's hooks and command scripts run on every operator's machine at
505
434
  spawn, gated by nothing but the handshake. In a mixed public/private
506
435
  organisation keep **souls only** in public members and let executable
@@ -520,27 +449,15 @@ onboarding skill asks this question first.
520
449
  - **Members.** A member is always its latest state; there is no `@revision`
521
450
  on `members:`. A team that wants frozen capabilities publishes them as a
522
451
  package and pins that.
523
- - **Member capabilities' `version` field** — informational; the content digest
452
+ - **Member capabilities' `version` field**: informational; the content digest
524
453
  recorded at spawn identifies a copy.
525
454
  - **Souls in members.** A soul is spawned from its repo's current state; the
526
455
  commit is recorded in `instance.json.workspace.soul`.
527
- - **The deployment layout** — the operator's; only the convention is taught.
456
+ - **The deployment layout**: the operator's; only the convention is taught.
528
457
 
529
458
  What **is** versioned: `packages:` (the workspace's one list), the lock's exact
530
459
  commits and digests, and `external:` pins (a stranger's repo is never "latest").
531
460
 
532
- ## Removed
533
-
534
- Per-soul `source: git:…@v#…` lines and the `git:`/`repo:`/`path:` grammar;
535
- `imports:` of member souls; `exports:` lists; `oats.yaml`; the
536
- installed-capability tier (`.agents/capabilities/installed/`) and
537
- `oats-config.yaml` entirely; `oats init` / `use` / `install` / `restore` /
538
- `trust` / `list` / `catalog` / `remove` / `migrate` / `config` (each answers
539
- `E_UNKNOWN_COMMAND` naming its replacement); per-soul `stores.<x>.inherit`;
540
- ambient-skill exclusion at launch. Lock v1/v2 files are `E_LOCK_SCHEMA`.
541
- There is no converter and no dual-schema reader: a 0.24.x kernel keeps
542
- spawning 0.24.x deployments.
543
-
544
461
  ## Related
545
462
 
546
463
  - [Souls and instances](souls-and-instances.md) · [Packages](packages.md) ·
@@ -21,9 +21,10 @@
21
21
  * E_AUTOMATION_SCHEMA), `id:` or the filename stem, `runsOn` (a host name), `owner` (a GitHub
22
22
  * account, `<host>/<login>`), `description?`, `enabled?`. A duplicate id within one member and
23
23
  * one kind is E_AUTOMATION_DUPLICATE.
24
- * - PLACEMENT: a host runs one ONLY when `runsOn` is its `host.name` (oats-local.yaml) AND its
25
- * authenticated `gh` account is `owner`; otherwise it is listed with the reason
26
- * (`host-unnamed`, `assigned-elsewhere`, `owner-mismatch`). Each kind's own list in
24
+ * - PLACEMENT: a host runs one ONLY when `runsOn` is its `host.name` (oats-local.yaml), its
25
+ * authenticated `gh` account is `owner` AND its `automations.trust` admits it (0.30: both
26
+ * names come from a commit; trust is the operator's own yes); otherwise it is listed with the
27
+ * reason (`host-unnamed`, `assigned-elsewhere`, `owner-mismatch`, `untrusted`). Each kind's own list in
27
28
  * oats-local.yaml (`triggers.disabled`, `schedules.disabled`) stops a named host from running
28
29
  * one.
29
30
  * - THE SNAPSHOT: discovery reads the remotes (async), the host tick is synchronous; so
@@ -38,11 +39,15 @@ import { parseConfigData } from "./config-data.mjs";
38
39
 
39
40
  export const AUTOMATIONS_API = 1;
40
41
  export const SNAPSHOT_MAX_AGE_MS = 10 * 60_000;
41
- export const PLACEMENT_REASONS = Object.freeze(["host-unnamed", "assigned-elsewhere", "owner-mismatch"]);
42
+ export const PLACEMENT_REASONS = Object.freeze(["host-unnamed", "assigned-elsewhere", "owner-mismatch", "untrusted"]);
42
43
  /** The kinds, in the order the snapshot keeps them (`triggers`, `schedules`). */
43
44
  export const KIND_NAMES = Object.freeze(["trigger", "schedule"]);
44
45
  const NEVER_SCANNED = new Set(["oats-package", ".git", "node_modules"]);
45
46
  export const AUTOMATION_ID_RE = /^[a-z0-9-]{1,40}$/;
47
+ /** A model id an automation may pass to `oats spawn --model`: a provider/model id, never an option.
48
+ * The first character is alphanumeric (or the spawn CLI's own `@native-default`), so no value can
49
+ * be read as a flag (re-review B #1). `[` `]` admit a harness's context-size alias (`opus[1m]`). */
50
+ export const MODEL_RE = /^(?:@native-default|[A-Za-z0-9][A-Za-z0-9._:/@+\[\]-]{0,127})$/;
46
51
  export const HOST_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
47
52
  const OWNER_RE = /^([a-z0-9-]+(?:\.[a-z0-9-]+)+)\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))$/;
48
53
  const HEADER_KEYS = ["kind", "schemaVersion", "id", "description", "runsOn", "owner", "enabled"];
@@ -123,6 +128,9 @@ export async function discoverAutomations(discovery, { remote, memberName, kinds
123
128
  const names = new Map();
124
129
  for (const m of (discovery?.members || []).filter((x) => x.confirmed && x.commit)) {
125
130
  const name = memberName(m.key);
131
+ // `local/<id>` names this host's own automations (and their state, live counts and CLI edits):
132
+ // a member labelled `local` would collide with them (re-review B #3).
133
+ if (name === LOCAL) { problems.push({ code: "E_AUTOMATION_DUPLICATE", repoKey: m.key, path: "", message: `member name ${JSON.stringify(name)} is reserved for this host's own triggers and schedules (local/<id>), so ${m.key}'s are not listed — rename the repository to list them` }); continue; }
126
134
  if (names.has(name)) { problems.push({ code: "E_AUTOMATION_DUPLICATE", repoKey: m.key, path: "", message: `member name ${JSON.stringify(name)} is also ${names.get(name)}'s: triggers and schedules are named <member>/<id>, so ${m.key}'s are not listed` }); continue; }
127
135
  names.set(name, m.key);
128
136
  let candidates;
@@ -218,7 +226,20 @@ const snapshotAge = (snap, now) => (snap?.takenAt ? now.getTime() - Date.parse(s
218
226
  export function hostOf(local) {
219
227
  const name = isObject(local?.host) && typeof local.host.name === "string" ? local.host.name : null;
220
228
  const disabled = Object.fromEntries(KIND_NAMES.map((k) => [k, new Set(Array.isArray(local?.[`${k}s`]?.disabled) ? local[`${k}s`].disabled : [])]));
221
- return { name, disabled };
229
+ const t = isObject(local?.automations) ? local.automations.trust : undefined;
230
+ const trust = t === "*" ? "*" : new Set(Array.isArray(t) ? t : []);
231
+ return { name, disabled, trust };
232
+ }
233
+ /** Whether this host's `automations.trust` admits a workspace automation (`<member>/<id>`). */
234
+ export const trusted = (host, id) => host.trust === "*" || !!host.trust?.has(id);
235
+ /** What an operator adds to trust `id`: the exact list line under `automations:` `trust:`. */
236
+ export const trustRemedy = (id) => `add the line "- ${id}" under automations: trust: in oats-local.yaml`;
237
+ /** `automations.trust` entries that name no workspace trigger or schedule in `automations`
238
+ * (the snapshot's): stale, or a member not synced yet. A warning, never an error. */
239
+ export function staleTrust(host, automations) {
240
+ if (host.trust === "*") return [];
241
+ const known = new Set(automations.map((a) => a.id));
242
+ return [...host.trust].filter((id) => !known.has(id));
222
243
  }
223
244
  /** The account the host's `gh` is logged in as on one GitHub host (`gh api user`), memoized in
224
245
  * `cache` (one per tick). `io.gh(args)` is the seam. → { ok, login } | { ok: false, error } */
@@ -237,7 +258,7 @@ export function ghLogin(ghHost, { io, cache } = {}) {
237
258
  return out;
238
259
  }
239
260
  /** Where a workspace trigger or schedule runs, from this host's point of view.
240
- * → { runsHere, reason: null | host-unnamed | assigned-elsewhere | owner-mismatch, detail?, enabledHere } */
261
+ * → { runsHere, reason: null | host-unnamed | assigned-elsewhere | owner-mismatch | untrusted, detail?, enabledHere } */
241
262
  export function placementOf(a, { host, io, cache }) {
242
263
  const enabledHere = a.enabled !== false && !host.disabled[a.kind]?.has(a.id);
243
264
  let reason = null, detail;
@@ -248,6 +269,7 @@ export function placementOf(a, { host, io, cache }) {
248
269
  const who = owner ? ghLogin(owner.host, { io, cache }) : { ok: false, error: "owner is not <host>/<login>" };
249
270
  if (!who.ok) { reason = "owner-mismatch"; detail = `acts as ${a.owner}, but this host's gh is not logged in on ${owner?.host ?? "?"}: ${who.error}`; }
250
271
  else if (who.login.toLowerCase() !== owner.login.toLowerCase()) { reason = "owner-mismatch"; detail = `acts as ${a.owner}; this host's gh is logged in as ${owner.host}/${who.login}`; }
272
+ else if (!trusted(host, a.id)) { reason = "untrusted"; detail = `declared for this host, not trusted here; to run it, ${trustRemedy(a.id)}`; }
251
273
  }
252
274
  return { runsHere: reason === null && enabledHere && !a.invalid && !!a.definition, reason, ...(detail ? { detail } : {}), enabledHere };
253
275
  }