@awebai/oats 0.24.3 → 0.24.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/packages.md CHANGED
@@ -402,7 +402,7 @@ sources; installing a kernel does not advance existing package locks:
402
402
  {
403
403
  "packages": {
404
404
  "oats.okf": { "url": "https://github.com/awebai/oats-okf.git", "ref": "v2.0.0", "path": "oats-package" },
405
- "oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.1.1", "path": "oats-package" },
405
+ "oats.framework": { "url": "https://github.com/awebai/oats.git", "ref": "oats-framework/v1.1.3", "path": "oats-package" },
406
406
  "oats.dev": { "url": "https://github.com/awebai/oats-dev.git", "ref": "v1.0.0", "path": "oats-package" }
407
407
  },
408
408
  "capabilities": { "oats.review": "oats.dev" }
@@ -429,7 +429,7 @@ integrity checks. Git transport preserves the canonical source alias.
429
429
 
430
430
  The `oats.framework` distribution package is a separate Git payload in this
431
431
  repository's `oats-package/`, excluded from the kernel npm tarball. The catalog
432
- entry selects the published `oats-framework/v1.1.1` tag, which exports three
432
+ entry selects the published `oats-framework/v1.1.3` tag, which exports three
433
433
  capabilities: `oats.core` (day-to-day operation: `oats-operate`, `oats-souls`
434
434
  and the "you run on OATS" briefing — declared explicitly on every soul by
435
435
  default at creation and removable), `oats.setup` (OATS Soul Setup: `oats-config`,
@@ -0,0 +1,11 @@
1
+ # oats.framework 1.1.3 · aweb 1.11.2 — helper composition for every edition
2
+
3
+ Packaging fix found by the independent second operator on the 0.24.4 wave: behind the operator's `responsibleHuman` requirement, preparation refused with `needs-configuration: new helper injection requires an explicit capability policy`. `oats.okf` had adopted the helper-injection contract (`omit`); its siblings **`oats.core`** and **`oats.aweb`** shipped an `inject` without a `helperInjection` policy, so no edition's OKF harvest helper could compose and **no edition could publish a resolution**. Both remaining blockers were packaging, not operator input.
4
+
5
+ - **`oats.core` 1.0.1** (in oats.framework 1.1.3): `helperInjection: {version: 1, mode: inherit}` — a helper is still an OATS instance and keeps the "you run on OATS" briefing.
6
+ - **aweb 1.11.2**: `helperInjection: {version: 1, mode: omit}` — a harvest helper has no messaging identity. Manifest-only; code identical to 1.11.0.
7
+ - **Release check**: `test/release-packaging.test.mjs` now asserts every framework-shipped capability with an `inject` declares a `helperInjection` policy; the theory-package check pins core's `inherit`. The framework's own packages must pass the contracts the kernel imposes.
8
+ - Catalog: `oats.aweb` → `v1.11.2`, `oats.framework` → `oats-framework/v1.1.3`; six editions and workspace imports repinned.
9
+ - Still open (kernel, 0.25 unless a 0.24.5 is cut): this early refusal is a bare top-level error with no `details`/attribution — it must route through the same problem shape as every other preparation problem. Also noted: `responsibleHuman` is a captured accountability claim the code validates only by shape.
10
+
11
+ Decision: `agents/oats-expert/soul/knowledge/decisions/helper-injection-policy-on-every-injecting-capability.md`.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Kernel/Pi/Desktop **0.24.3**. Fixes the five usability seams an independent second operator hit adopting the shared workspace definition on 0.24.1/0.24.2 (see the [program board](../design/2026-09-20-redesign-program-board.md)). No contract, schema or authority change.
4
4
 
5
- - **Attributed provider problems.** Every preparation problem now carries `slot`, `capability` and its `origins`, with kernel-fixed text that names the missing item (for example `oats.okf knowledge normalize binding could not be prepared` / `oats.aweb@1.10.3 declares no binding interface; messaging cannot be prepared`). One unqualified slot no longer masks another slot's diagnostics: every slot that has a binding interface is normalized, and the single resolver reports each invalid choice under its slot. Provider free text still never crosses the wire.
5
+ - **Attributed provider problems.** Every preparation problem now carries `slot`, `capability` and its `origins`, with kernel-fixed text (for example `oats.okf knowledge normalize binding could not be prepared` / `oats.aweb@1.10.3 declares no binding interface; messaging cannot be prepared`). The kernel names the missing item where it knows it (a missing binding interface); for a provider's own `needs-configuration` it attributes the slot/capability/phase but cannot name the missing setting unless the provider sends it — OKF 2.1.1 sends code only, so `bindings-file`/`state-dir` are named by the setup guidance and by OKF 2.1.2. One unqualified slot no longer masks another slot's diagnostics: every slot that has a binding interface is normalized, and the single resolver reports each invalid choice under its slot. Provider free text still never crosses the wire.
6
6
  - **`oats trust <capability> --dir <deployment>` on a lock v3 deployment** returns a typed problem with the exact working command (`oats trust <cap> --deployment <abs> --artifact-set <sha256-…>`) instead of `unsupported lockfileVersion 3`. No auto-approval.
7
7
  - **Help** documents `--artifact-set` and how `prepare`'s `selections[].artifactSet` / `approvalRequired[]` pair with it.
8
8
  - **`prepare` on an absent deployment path** returns a typed explicit-provisioning hold, not a raw `ENOENT` with a host path.
@@ -0,0 +1,10 @@
1
+ # OATS v0.24.4 — provider reasons cross the wire; one request file for both commands
2
+
3
+ Kernel/Pi/Desktop **0.24.4**. Two findings from the independent second operator's repeat on 0.24.3 (see the [program board](../design/2026-09-20-redesign-program-board.md)), plus the first half of the binding-ownership rule. Kernel-first release: the provider releases that declare the new manifest fields (OKF 2.1.2, aweb 1.11.1) require this version.
4
+
5
+ - **The kernel no longer discards the provider's reason.** A provider's `error.message` (and a non-ready `check`'s `result.problems[].message`) now crosses the binding wire when it is byte-equal to a fixed reason the provider declares — manifest `binding.reasons`, or the kernel's reviewed compatibility list for aweb 1.11.0 and OKF 2.1.2 — and is surfaced as the problem's `message` beside `slot`, `capability` and `origins`. Anything else is dropped as before and the kernel template is used. No interpolation, no operator values, no paths, ever: the second operator sees `messaging workspace must declare private: per-human` or `setting bindings-file is required (absolute host path)` in one run instead of six. Decision: `agents/oats-expert/soul/knowledge/decisions/provider-problem-reasons-cross-the-wire.md`.
6
+ - **`binding.reasons` and `binding.keys` manifest fields** (optional). `reasons`: 1–64 unique printable-ASCII strings ≤200 chars, no braces; an invalid declaration refuses, it never falls back. `keys`: a provider's owned operator-binding keys, exact names or trailing-dot namespaces (`stores.`) — **shape-validated only in 0.24.4**; forwarding, overlap refusal and stray-key attribution land in 0.25. Providers must still ignore keys they do not own (decision `operator-bindings-ownership.md`). Every earlier kernel rejects manifests carrying these fields, so declaring providers floor on `>=0.24.4`.
7
+ - **`inspect --request` accepts `prepare`'s fields.** `operator`, `launch`, `helperLaunches`, `mode`, `allowLocalPaths` are accepted, not evaluated, and listed under `ignored`; `--emit-prepare-request` carries them through unchanged. The same complete request file is now valid for both commands — the 0.24.3 fix had converged only one way.
8
+ - **CLI renders the problem `key`** (for example `/bindings/messaging/responsibleHuman`) beside its message.
9
+
10
+ Not in this release: the operator-bindings deadlock between OKF 2.1.1 and aweb 1.11.0 (OKF validated messaging's `wider` as a store locator) is fixed provider-side in OKF 2.1.2, released next with `binding.reasons`/`binding.keys` declared and floor 0.24.4; then aweb 1.11.1 (manifest-only), the `oats.framework` 1.1.2 payload, and the workspace editions re-pinned.
@@ -0,0 +1,12 @@
1
+ # OATS v0.24.5 — four small fixes from the second-operator wave
2
+
3
+ Kernel/Pi/Desktop **0.24.5**. No contract, schema or authority change. Every item was found while an independent operator (and then the lead) drove the published 0.24.4 wave to its first end-to-end resolution.
4
+
5
+ - **Retirement recovers a worktree on the branch it actually has.** `oats retire` derived the recovery clone's branch from `instance.json` (the spawn-time branch); a worktree that had legitimately switched branches during its task could never pass recovery verification and became unretirable by the normal path, even with nothing unpreserved. Recovery now derives the branch from the worktree while it exists (detached HEADs recover at their exact commit), falls back to the recorded branch only when the worktree is gone, and records the drift in `recovery.json` (`branchDrift`). Fail-closed behaviour is unchanged; the source of truth moved to the object.
6
+ - **Captured launch: `ifInstalled` runtime rows are not hard blocks, and refusals are attributed.** A provider's `requires` row marked `ifInstalled: true` (a version floor for an ambient package, if present) was treated as a hard requirement, so a Pi launch with aweb `delivery: session` could never publish a resolution — the same request without a `launch` block published fine. Such rows are now satisfied by absence in captured preparation. Remaining hard rows refuse through the ordinary preparation problem shape (`slot`, `capability`, `runtime`, `package`, the manifest's own `install` text) instead of one bare `needs-configuration`.
7
+ - **Helper-injection refusal is attributed.** A capability that ships an `inject` without a `helperInjection` policy now yields a preparation problem naming that capability (with `origins`), so the operator learns *which* sibling has not adopted the contract rather than only that one has.
8
+ - **`oats onboard --workspace` acquires the framework the workspace's catalog names.** The kernel tarball ships `package-catalog.json` as a snapshot at the kernel's tag, so it lags every `oats.framework` release cut afterwards; onboarding a current workspace edition against it refused as `integrity-drift` — correct, but a fresh machine has no reviewed list to point `OATS_PACKAGE_CATALOG` at. Onboarding now reads the workspace repository's `package-catalog.json` at the observed revision and uses its `oats.framework` entry; the bundled entry is the fallback, `OATS_PACKAGE_CATALOG` still overrides. The result reports `catalog.origin`, and a drift refusal names the lag and both integrities.
9
+
10
+ Rule going forward (recorded in the maintainer's knowledge): **`prepare` never ends in a bare `needs-configuration` after selection** — every refusal is a problem with a slot/capability, or it is a kernel defect. Remaining bare sites are pre-selection input errors, record-shape guards and the skill-name collision, none reachable from a published edition.
11
+
12
+ Not in this release: `binding.keys` enforcement and the dotted-exact-key grammar (0.25); OKF `check` per-cause reasons (OKF 2.1.3, deferred with harvest).
@@ -44,64 +44,88 @@ owner registry belongs in the public workspace. Knowledge publication and accept
44
44
  (S7) remain separate; no ready knowledge export is advertised. Preserve the parked
45
45
  roster/curation and every old home, lock, source, pending job, history and worktree.
46
46
 
47
- ## Planned onboarding: OATS Soul Setup (D3)
48
-
49
- **The D3 onboarding flow remains pending.** It will create and instantiate
50
- `oats-setup-expert`, declaring both `oats.core` and `oats.setup` from the
51
- [official marketplace](official-marketplace.md). Those capabilities are now
52
- published in `oats.framework` 1.1.0 and listed; the separate setup-expert edition
53
- and onboarding entry point do not become available merely by listing the package.
54
- This flow was not shipped in the 0.24.0 or 0.24.1 kernel releases.
55
-
56
- - The setup expert will help the operator adopt repositories, select capabilities
57
- and carry out the normal prepare/approve/scaffold/start steps. It bypasses no
58
- executable approval, provider readiness, identity or permission boundary.
59
- - Every soul created by that flow will declare `requires.capabilities.oats.core`
60
- and its source explicitly. The operator can remove or replace that dependency
61
- by editing the authored definition, not a captured record; the kernel will not
62
- silently reinsert an absent one.
63
- - The CLI/Desktop entry point still requires separate implementation and review.
64
- Do not invent a workspace init/adopt command, create a setup soul from this
65
- sketch, or treat a listed package as installed. Existing instances and retained
66
- resources are not rewritten by the plan.
47
+ ## Onboard with OATS Soul Setup (0.24.2+)
48
+
49
+ With operator approval, [OATS 0.24.2](release-notes/v0.24.2.md) and later provide:
50
+
51
+ ```sh
52
+ oats onboard --dir /absolute/context --json
53
+ ```
54
+
55
+ This is **classic local bootstrap**, not captured preparation or workspace
56
+ enrollment; the command is absent from 0.24.0/0.24.1. Classic root resolution
57
+ selects an enclosing roster, otherwise the enclosing Git root/context. Supplying
58
+ `--dir` does **not** promise that a literal nested subdirectory becomes a new
59
+ physical deployment; choose an independent context when that is intended.
60
+
61
+ Onboarding acquires the catalog's official `oats.framework` package through the
62
+ ordinary acquisition/lock engine and creates a local `oats-setup-expert`, selecting
63
+ only `oats.core` and `oats.setup` for it. Both local capability requirements name
64
+ the **actually acquired immutable commit**, not orphan `repo:` paths in the new
65
+ deployment. Knowledge, messaging and tasks default to none, with no knowledge
66
+ owner or payload. Unexpected executable surfaces refuse rather than gaining trust
67
+ from catalog membership. Review the resolved deployment and returned
68
+ `result.next.command`: it uses the same kernel for the next spawn, and onboarding
69
+ **never executes it or launches a model**.
70
+
71
+ Optional `--workspace git:host/org/repository[@revision]` selects the workspace's
72
+ pinned setup-expert import, or that explicit repository's advertised setup edition
73
+ at its observed revision. Its source-package bytes must match the official
74
+ acquisition. Failed explicit inputs never fall back to the packaged default, and
75
+ workspace policy, teams and provider adoption values are not silently adopted.
76
+
77
+ An **existing roster** requires `--force-existing` (the guard is the agent list,
78
+ not merely any existing configuration). The flag cannot replace an existing or
79
+ incomplete setup soul or disable providers/additives for other souls; exclusions
80
+ apply only to the new setup expert. Failures report partial acquisition/creation,
81
+ not atomic captured preparation. Preserve that evidence before retrying.
82
+ **`oats setup` remains record capture setup**, unchanged. Native authentication
83
+ and permission boundaries remain.
84
+
85
+ The expert can then guide deliberate configuration and the normal retained
86
+ prepare/approve/scaffold/start stages. A created soul or printed spawn command is
87
+ not a running session, provider qualification or accepted learning. Desktop
88
+ onboarding and legacy roster/knowledge cutover remain separate.
67
89
 
68
90
  ## Stage two: published experts and pinned imports
69
91
 
70
92
  - `oats-workspace.yaml` explicitly admits `oats` and the six intended capability
71
93
  repositories: `oats-dev`, `oats-okf`, `oats-aweb`, `oats-authoring`, `oats-jira`
72
94
  and `oats-linear`. It activates no additional capability; tasks default to none.
73
- - `oats.yaml` exports all five `souls/<name>` editions above and the actual package
74
- roots `oats-package` (`oats.framework`) and `capabilities/oats-authoring`, not
95
+ - `oats.yaml` exports the five knowledge-owning editions above plus the separate
96
+ `souls/oats-setup-expert` bootstrap edition, and the actual package roots
97
+ `oats-package` (`oats.framework`) and `capabilities/oats-authoring`, not
75
98
  the npm root as a fictitious OATS distribution. Its workspace backlink names
76
99
  the same framework repository.
77
100
  - The editions are parallel to, not replacements for, the live `agents/` roster.
78
101
  Each contains canonical instructions, `CLAUDE.md -> AGENTS.md`, and its reviewed
79
102
  private procedures where applicable. No durable KB is copied into them; legacy
80
- roster cutover remains deferred until S7 knowledge publication.
81
- - Each edition preserves its knowledge owner, owned node and four cross-reads.
103
+ roster/knowledge cutover remains deferred until the fresh-reader proof against
104
+ the accepted public knowledge base.
105
+ - Each of the five expertise editions preserves its owner, node and four cross-reads.
82
106
  Store `oats` requires the explicit `stores.oats` binding; no publisher writer,
83
107
  production store or grants are supplied. An acceptance fixture is parent-owned
84
108
  and cannot be counted as production knowledge adoption.
85
- - Knowledge **oats.okf@2.1.1** and messaging **oats.aweb@1.11.0** are explicit hard
86
- requirements, not optional defaults. They are published starting revisions,
87
- **not proof that their combined bindings/runtime profile is ready**. The provider
109
+ - Current authored expert editions require knowledge **oats.okf@2.1.2** and
110
+ messaging **oats.aweb@1.11.2** (both OATS >=0.24.4), not optional defaults. These published revisions
111
+ are **not proof that their combined bindings/runtime profile is ready**. The provider
88
112
  owner supplies that evidence and any subsequently reviewed compatible revision.
89
113
  Do not replace either requirement with none or erase a read edge to launch.
90
114
 
91
- At these starting pins, the provider boundary is concrete:
115
+ At those authored revisions, the provider boundary is concrete:
92
116
 
93
- - Published OKF2.1.1 supports `inherit: stores.oats`, normalized to
117
+ - Published OKF 2.1.2 supports `inherit: stores.oats`, normalized to
94
118
  `/bindings/knowledge/stores/oats`. The explicit `destination: oats` preserves
95
119
  routing; omitting it would instead require `write.default`. No new schema,
96
120
  owner or production locator is needed for this declaration.
97
121
  - aweb 1.10.3 (`24efa6f9`) has no portable binding interface; **aweb 1.11.0**
98
- (`v1.11.0`, OATS >=0.24.2) adds it and the five editions now pin it. Its `check`
122
+ (`v1.11.0`, OATS >=0.24.2) adds it; **1.11.1/1.11.2** (OATS >=0.24.4) are code-identical and declare its fixed reasons, owned operator keys and `helperInjection: omit` (a harvest helper has no messaging identity); the five editions pin 1.11.2. Its `check`
99
123
  qualifies only an input-capable Claude/Codex primary with an explicit private team
100
124
  and `delivery: session`; strict-Pi print reports `needs-configuration`. Status is on
101
125
  the [program board](design/2026-09-20-redesign-program-board.md). Qualification
102
126
  is HOME-route operational custody only: not human/native-principal delegation,
103
127
  private grants, broker delivery or model consumption.
104
- - Published OKF2.1.1 accepts retained Claude/Codex helpers with the complete approved
128
+ - Published OKF 2.1.2 accepts retained Claude/Codex helpers with the complete approved
105
129
  capability closure and native-default model intent. Strict Pi still requires an
106
130
  explicit model and the sole-OKF profile; Pi plus messaging remains unqualified.
107
131
  This provider release alone is not combined-profile acceptance. Do not silently
@@ -112,19 +136,27 @@ select reviewed compatible provider revisions and update the source pin delibera
112
136
  before claiming an operational pilot; metadata-only repository indexes change none
113
137
  of these runtime facts.
114
138
 
115
- Stage one used an empty imports list until source publication. Stage two is now
116
- committed: all five imports pin **`caa341f34009e37006567419a983d5a743037a79`**, the
117
- published edition revision containing explicit core requirements and the package.
118
- The later workspace commit `375b9f42` added those imports. Live source inspection
119
- against published main resolved all five as `ready-for-preparation`; this is
120
- metadata readiness, not provider binding, approval, enrollment or a running pilot.
139
+ Stage one used an empty imports list until source publication. All six imports
140
+ now pin **`906b1558633766cf489451f9b68016216acaa63b`** (after
141
+ [OATS v0.24.3](release-notes/v0.24.3.md)), the reviewed revision at which the
142
+ workspace declares the per-human private team policy (`teams: {private: per-human}`)
143
+ and every messaging edition carries its `teams: []` declaration — without them the
144
+ aweb provider cannot normalize in workspace context, as the second operator found.
145
+ Workspace update `f3ee31e0`. The five knowledge-owning experts therefore select
146
+ OKF 2.1.2 and aweb 1.11.2 with explicit core; setup remains provider-independent,
147
+ requiring core/setup and defaulting all three fundamental layers to none. It is
148
+ not a sixth knowledge owner. This deliberate repin, not a catalog/kernel upgrade
149
+ alone, advances the selected source requirements. Successful source inspection,
150
+ including `ready-for-preparation`, is still metadata readiness—not binding,
151
+ approval, enrollment or a running pilot.
121
152
 
122
153
  ## Preserve source-before-import publication order
123
154
 
124
- 1. Publish complete, reviewed source editions before pinning them. The current
125
- source is `caa341f34009e37006567419a983d5a743037a79`; future revisions must likewise
126
- exist before their workspace import update. Never use an invented SHA, a mutable
127
- branch or an unreviewed local candidate as the accepted source.
155
+ 1. Publish complete, reviewed source editions before pinning them. All six imports
156
+ use `906b1558633766cf489451f9b68016216acaa63b`. Future revisions must
157
+ likewise exist before their import update.
158
+ Never use an invented SHA, a mutable branch or an unreviewed local candidate
159
+ as the accepted source.
128
160
  2. In each of the six repositories, review a root `oats.yaml` against its actual
129
161
  source head and actual `oats-package/oats-package.json`. The declaration is:
130
162
 
@@ -149,13 +181,13 @@ metadata readiness, not provider binding, approval, enrollment or a running pilo
149
181
  imports:
150
182
  - source: git:github.com/awebai/oats
151
183
  soul: souls/oats-expert
152
- revision: caa341f34009e37006567419a983d5a743037a79
184
+ revision: 906b1558633766cf489451f9b68016216acaa63b
153
185
  alias: oats-expert
154
186
  ```
155
187
 
156
- The [actual workspace](../oats-workspace.yaml) contains all five imports at that
157
- same revision; this excerpt is not a replacement for the full list. The layout
158
- test now checks stage-two imports and source-document declarations. Do not
188
+ The [actual workspace](../oats-workspace.yaml) contains all six imports at that
189
+ same revision; this excerpt is not the full list.
190
+ The layout test checks all six imports and source-document declarations. Do not
159
191
  change stable export paths or owners merely because the workspace advances.
160
192
  4. Qualify reciprocal admission at the now-published observations. A missing
161
193
  backlink, a fork's copied file or a stale workspace observation is not membership.
@@ -71,7 +71,7 @@ name: domain-expert
71
71
  requires:
72
72
  knowledge:
73
73
  capability: oats.okf
74
- source: git:github.com/awebai/oats-okf@v2.1.1#oats-package
74
+ source: git:github.com/awebai/oats-okf@v2.1.2#oats-package
75
75
  ```
76
76
 
77
77
  This illustrates software selection, not complete OKF provisioning: the chosen capability also needs its own valid knowledge declaration, bindings and accepted base.
@@ -32,8 +32,28 @@ export function validateCapturedLaunchRequest(value, validateConfig) {
32
32
  export function compileCapturedLaunchRequest(request,{artifacts,manifests,settings,resources},kernel) {
33
33
  if(request===undefined)return null;
34
34
  validateCapturedLaunchRequest(request,kernel.validateLaunchConfig);
35
- const required=kernel.runtimeRequirements(request.runtime,[...manifests].filter(([id])=>Object.hasOwn(artifacts.capabilities,id)).map(([id,manifest])=>({id,manifest,settings:settings[id]})));
36
- if(required.length)throw oatsError('needs-configuration','runtime package requirements need retained runtime roots and a qualified loader; no ambient package discovery was used');
35
+ const providers=[...manifests].filter(([id])=>Object.hasOwn(artifacts.capabilities,id)).map(([id,manifest])=>({id,manifest,settings:settings[id]}));
36
+ // `ifInstalled: true` rows constrain a package that may legitimately be absent (a version floor
37
+ // for an ambient extension, if any). Captured preparation retains no ambient packages, so such a
38
+ // row is satisfied by absence — treating it as a hard requirement made a Pi launch with aweb
39
+ // `delivery: session` unpublishable (second-operator finding, 2026-09-21). Hard rows refuse
40
+ // through the preparation problem shape, naming the capability and package; the remedy text is
41
+ // the manifest's own `install` string — declared data, never free text.
42
+ // Resolve each applicable row back to its declaration, honouring the same `when` predicate the
43
+ // kernel's requirement selection uses, so two rows for one package (channel vs session) stay distinct.
44
+ const holds=(provider,row)=>!row.when || Object.entries(row.when).every(([k,v])=>String(provider.settings?.[k] ?? '')===String(v));
45
+ const declarationOf=(row)=>{const provider=providers.find(p=>p.id===row.capability);
46
+ return {provider,declared:provider?.manifest?.requires?.find(r=>r && typeof r==='object' && r.runtime===request.runtime && r.package===row.package && holds(provider,r))};};
47
+ const required=kernel.runtimeRequirements(request.runtime,providers).filter(row=>declarationOf(row).declared?.ifInstalled!==true);
48
+ if(required.length){
49
+ const error=oatsError('needs-configuration','runtime package requirements need retained runtime roots and a qualified loader; no ambient package discovery was used');
50
+ error.problems=required.map(row=>{
51
+ const {provider,declared}=declarationOf(row);
52
+ return {code:'needs-configuration',message:`${request.runtime} launch requires runtime package ${row.package}, which captured preparation has not retained`,capability:row.capability,
53
+ ...(provider?.manifest?.layer?{slot:provider.manifest.layer}:{}),runtime:request.runtime,package:row.package,...(typeof declared?.install==='string'?{install:declared.install}:{})};
54
+ });
55
+ throw error;
56
+ }
37
57
  const base={version:1,runtime:request.runtime,args:request.args,env:request.env,model:request.model,yolo:request.yolo,
38
58
  hooks:{launch:{},env:{},contributions:[],pending:true},prompt:{kind:'task-file',file:'TASK.md'}};
39
59
  if(typeof request.executable==='string')return {...base,executable:request.executable,executableResolvedFrom:'explicit-host'};
package/lib/core.mjs CHANGED
@@ -7080,6 +7080,15 @@ function fingerprintTree(root, { excludeRoot = new Set(), excludeGitMetadata = f
7080
7080
  return `sha256:${hash.digest("hex")}`;
7081
7081
  }
7082
7082
 
7083
+ /** The ref a worktree actually has checked out: `{branch, oid}` with
7084
+ * `branch === null` when HEAD is detached. Recovery derives truth from the
7085
+ * object, never from spawn-time metadata. */
7086
+ function worktreeRef(work) {
7087
+ const oid = execFileSync("git", ["-C", work, "rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER }).trim();
7088
+ let branch = null;
7089
+ try { branch = execFileSync("git", ["-C", work, "symbolic-ref", "--quiet", "--short", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER }).trim() || null; } catch { branch = null; }
7090
+ return { branch, oid };
7091
+ }
7083
7092
  function worktreeStatus(repo) {
7084
7093
  try {
7085
7094
  return execFileSync("git", ["-C", repo, "status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignored=matching", "--ignore-submodules=none"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
@@ -8304,9 +8313,21 @@ function preserveRetirementWork(observation, meta, instance) {
8304
8313
  if (fingerprintTree(observation.home, { excludeRoot: new Set(["work"]) }) !== fingerprintTree(recoveredHome)) {
8305
8314
  throw new Error("home recovery verification disagreed with the source");
8306
8315
  }
8316
+ let branchDrift;
8307
8317
  if (!homeOnly && meta.work === "worktree" && meta.repo && meta.branch && (existsSync(observation.work) || observation.branchExists)) {
8308
8318
  const recoveredRepo = join(staging, "repo");
8309
- execFileSync("git", ["clone", "--no-local", "--quiet", "--branch", meta.branch, meta.repo, recoveredRepo], { stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
8319
+ // The branch is derived from the worktree while it exists: an instance
8320
+ // that legitimately switched branches during its task must still be
8321
+ // recoverable, and the recorded spawn-time branch is only the fallback
8322
+ // when the worktree is gone. Detached HEADs recover at their exact OID.
8323
+ const ref = existsSync(observation.work) ? worktreeRef(observation.work) : { branch: meta.branch, oid: null };
8324
+ if (ref.branch !== meta.branch) branchDrift = { recordedBranch: meta.branch, worktreeBranch: ref.branch, detachedAt: ref.branch === null ? ref.oid : null };
8325
+ if (ref.branch !== null) execFileSync("git", ["clone", "--no-local", "--quiet", "--branch", ref.branch, meta.repo, recoveredRepo], { stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
8326
+ else {
8327
+ execFileSync("git", ["clone", "--no-local", "--quiet", "--no-checkout", meta.repo, recoveredRepo], { stdio: ["ignore", "pipe", "pipe"] , maxBuffer: GIT_MAX_BUFFER });
8328
+ execFileSync("git", ["-C", recoveredRepo, "fetch", "--quiet", observation.work, ref.oid], { stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER });
8329
+ execFileSync("git", ["-C", recoveredRepo, "checkout", "--quiet", "--detach", ref.oid], { stdio: ["ignore", "pipe", "pipe"], maxBuffer: GIT_MAX_BUFFER });
8330
+ }
8310
8331
  const sourceGitContext = existsSync(observation.work) ? observation.work : meta.repo;
8311
8332
  detachRecoveryClone(sourceGitContext, recoveredRepo);
8312
8333
  if (existsSync(observation.work)) {
@@ -8325,7 +8346,8 @@ function preserveRetirementWork(observation, meta, instance) {
8325
8346
  if (worktreeStatus(observation.work) !== worktreeStatus(recoveredRepo)) throw new Error("recovered Git index/status disagreed with the source");
8326
8347
  }
8327
8348
  const recoveredHead = execFileSync("git", ["-C", recoveredRepo, "rev-parse", "HEAD"], { encoding: "utf8" , maxBuffer: GIT_MAX_BUFFER }).trim();
8328
- const sourceHead = execFileSync("git", ["-C", meta.repo, "rev-parse", `refs/heads/${meta.branch}`], { encoding: "utf8" , maxBuffer: GIT_MAX_BUFFER }).trim();
8349
+ const sourceHead = ref.branch === null ? ref.oid
8350
+ : execFileSync("git", ["-C", meta.repo, "rev-parse", `refs/heads/${ref.branch}`], { encoding: "utf8" , maxBuffer: GIT_MAX_BUFFER }).trim();
8329
8351
  if (recoveredHead !== sourceHead) throw new Error("recovery clone does not retain the instance branch tip");
8330
8352
  }
8331
8353
  if (observation.directory && observation.directoryFingerprint) {
@@ -8336,7 +8358,7 @@ function preserveRetirementWork(observation, meta, instance) {
8336
8358
  }
8337
8359
  }
8338
8360
  const repoCopy = homeOnly ? { copied: false, reason: "Only instance-home bytes changed; no work state requires a repository copy", source: meta.repo, branch: meta.branch } : undefined;
8339
- writeFileSync(join(staging, "recovery.json"), JSON.stringify({ version: 1, instance, classes: observation.classes, sourceHome: observation.home, createdAt: new Date().toISOString(), ...(repoCopy ? { repoCopy } : {}) }, null, 2) + "\n", { mode: 0o600 });
8361
+ writeFileSync(join(staging, "recovery.json"), JSON.stringify({ version: 1, instance, classes: observation.classes, sourceHome: observation.home, createdAt: new Date().toISOString(), ...(repoCopy ? { repoCopy } : {}), ...(branchDrift ? { branchDrift } : {}) }, null, 2) + "\n", { mode: 0o600 });
8340
8362
  mkdirSync(dirname(recovery), { recursive: true });
8341
8363
  renameSync(staging, recovery);
8342
8364
  return { path: recovery, classes: observation.classes, ...(repoCopy ? { repoCopy } : {}) };
@@ -35,7 +35,13 @@ export function captureHelperInjectionChoices(plan, definitions) {
35
35
  const policy = helperInjectionFact(definition), id = definition.artifact.capability;
36
36
  if (policies.has(id)) throw oatsError('invalid-resolution', 'duplicate helper policy owner');
37
37
  policies.set(id, policy);
38
- if (!policy.fact && policy.manifest.inject) throw oatsError('needs-configuration', 'new helper injection requires an explicit capability policy');
38
+ if (!policy.fact && policy.manifest.inject) {
39
+ // Attributed like every other preparation problem: the operator learns WHICH
40
+ // capability lacks the declaration, not only that one does (second-operator finding).
41
+ const error = oatsError('needs-configuration', 'new helper injection requires an explicit capability policy');
42
+ error.problems = [{ code: 'needs-configuration', message: 'capability ships an inject without a helperInjection policy', capability: id, ...(policy.manifest.layer ? { slot: policy.manifest.layer } : {}) }];
43
+ throw error;
44
+ }
39
45
  if (policy.fact) requirements.push(policy.fact);
40
46
  }
41
47
  const resolved = resolveChoices({ requirements, candidates: plan.candidates });
@@ -13,6 +13,7 @@ import { validateOrigin } from "./resolution-shape.mjs";
13
13
  import { oatsError } from "./errors.mjs";
14
14
 
15
15
  export const PORTABLE_ONBOARDING_VERSION = 1;
16
+ const PREPARATION_ONLY_FIELDS = Object.freeze(["operator", "launch", "helperLaunches", "mode", "allowLocalPaths"]);
16
17
  const issued = new WeakMap();
17
18
  const preflightWitnesses = new WeakMap();
18
19
  const MANAGED_PATHS = Object.freeze([
@@ -140,14 +141,18 @@ function catalogSelection(value, count) {
140
141
  * The caller owns the repository transaction lifetime. */
141
142
  export function inspectPortableOnboarding(input, { repositories } = {}) {
142
143
  canonicalJson(input);
143
- exact(input, ["deployment", "workTarget", "source", "origin", "workspace", "member", "catalogIndexes", "standaloneContextKey"],
144
+ exact(input, ["deployment", "workTarget", "source", "origin", "workspace", "member", "catalogIndexes", "standaloneContextKey", ...PREPARATION_ONLY_FIELDS],
144
145
  ["deployment", "workTarget", "source", "origin"], "portable onboarding inspection");
145
146
  if (!repositories || typeof repositories !== "object") throw oatsError("invalid-source", "portable onboarding needs an explicit repository transaction");
146
147
  validateOrigin(input.origin);
147
148
  const standalone = explicitContext(input);
148
149
  const deployment = preflightFreshDeployment({ deployment: input.deployment });
149
150
  const target = inspectWorkTarget(input.workTarget), discovery = createWorkspaceDiscovery(repositories);
150
- const witness = { deployment: preflightWitnesses.get(deployment), work: directoryIdentity(target.path) };
151
+ const ignored = PREPARATION_ONLY_FIELDS.filter(field => Object.hasOwn(input, field));
152
+ // Retain authored data privately for explicit request export ONLY. Inspection
153
+ // neither validates provider/launch semantics nor exposes these values.
154
+ const witness = { deployment: preflightWitnesses.get(deployment), work: directoryIdentity(target.path),
155
+ preparationFields: Object.fromEntries(ignored.map(field => [field, structuredClone(input[field])])) };
151
156
  const workspace = Object.hasOwn(input, "workspace") ? discovery.readWorkspace(input.workspace) : null;
152
157
  const selected = typeof input.source === "string" ? sourceOrigin(workspace, input.source) : { reference: input.source, origin: input.origin };
153
158
  const imported = discovery.importSoul(selected.reference, { origin: selected.origin });
@@ -171,7 +176,7 @@ export function inspectPortableOnboarding(input, { repositories } = {}) {
171
176
  const declaredTeams = workspace?.parsed.declaration.teams ?? {};
172
177
  const status = deployment.status !== "ready" ? deployment.status
173
178
  : input.member && membership.status !== "eligible" ? "needs-configuration" : "ready-for-preparation";
174
- const result = freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, status, deployment, workTarget: target,
179
+ const result = freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, status, deployment, workTarget: target, ignored,
175
180
  source: { location: imported.reference.source, reference: imported.reference, identity: imported.identity,
176
181
  revision: imported.observation.source, alias: imported.reference.alias, exportPath: imported.reference.soul,
177
182
  definition: imported.definition, roots: imported.roots, exports: exports.exports, provenance: imported.provenance },
@@ -241,7 +246,9 @@ export function buildFreshPreparationRequest(inspection, options = {}) {
241
246
  origin: inspection.source.revision.provenance[0], allowLocalPaths,
242
247
  ...(inspection.workspace ? { workspace: inspection.workspace.request } : { standaloneContextKey: inspection.context.key }),
243
248
  ...(inspection.repositoryMembership.request ? { member: inspection.repositoryMembership.request } : {}),
244
- ...(operator === undefined ? {} : { operator }), ...(mode === undefined ? {} : { mode }) };
249
+ ...issued.get(inspection).preparationFields,
250
+ ...(operator === undefined ? {} : { operator }), ...(mode === undefined ? {} : { mode }),
251
+ ...(Object.hasOwn(options, "allowLocalPaths") ? { allowLocalPaths } : {}) };
245
252
  canonicalJson(input);
246
253
  return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, operation: "prepare", persisted: false,
247
254
  preparation: input, workTarget: inspection.workTarget,
@@ -31,6 +31,16 @@ function packageSubset(artifacts, root) {
31
31
 
32
32
  /** Repositories and kernel callbacks belong to this operation; the caller owns
33
33
  * their lifetime/scratch cleanup. No hook, launch, enrollment or approval here. */
34
+ /** Origins of the choices that selected a capability — the same derivation preparation
35
+ * bindings use, so completion refusals render beside binding problems. */
36
+ function originsOf(plan, capability) {
37
+ const seen = new Map();
38
+ for (const key of plan.capabilities?.[capability]?.choiceKeys ?? []) {
39
+ const origin = plan.choices?.[key]?.selectedBy;
40
+ if (origin) seen.set(canonicalJson(origin), origin);
41
+ }
42
+ return [...seen.values()];
43
+ }
34
44
  export function prepareComposition(input, { repositories, kernel, previous: suppliedPrevious }) {
35
45
  canonicalJson(input);
36
46
  objectAt(input, ["deployment", "directory", "source", "origin", "workspace", "member", "operator", "mode", "allowLocalPaths", "standaloneContextKey", "launch", "helperLaunches"], ["deployment", "directory", "source", "origin"]);
@@ -114,8 +124,17 @@ export function prepareComposition(input, { repositories, kernel, previous: supp
114
124
  if (operator) declarations.push(operatorBindingDeclaration(operator));
115
125
  const bound = prepareProviderBindings({ seed, plan, manifests, declarations }, options => kernel.binding({ ...options, deployment }));
116
126
  plan = bound.plan; seed = bound.seed;
117
- const completion = bound.problems.length ? { record: null, problems: bound.problems }
118
- : kernel.complete({ seed, plan, manifests, mode, deployment, directory, launch: input.launch, helperLaunches: input.helperLaunches });
127
+ let completion;
128
+ if (bound.problems.length) completion = { record: null, problems: bound.problems };
129
+ else {
130
+ try { completion = kernel.complete({ seed, plan, manifests, mode, deployment, directory, launch: input.launch, helperLaunches: input.helperLaunches }); }
131
+ catch (error) {
132
+ // A typed completion refusal that names its capability is a preparation problem like any
133
+ // other (slot/capability/origins), never a bare top-level error. Anything else propagates.
134
+ if (!Array.isArray(error?.problems) || !["needs-configuration", "requirement-conflict"].includes(error.code)) throw error;
135
+ completion = { record: null, problems: error.problems.map(problem => ({ ...problem, origins: problem.origins ?? originsOf(plan, problem.capability) })) };
136
+ }
137
+ }
119
138
  let resolution = null;
120
139
  if (completion.record) resolution = commitCapturedResolution(deployment, completion.record);
121
140
  // Publishing valid immutable inputs/records need not roll back on a later
@@ -6,6 +6,7 @@ import { validateOrigin } from './resolution-shape.mjs';
6
6
  import { pointerKey } from './portable-shape.mjs';
7
7
  import { bindingField, bindingOriginWitnessed } from './provider-binding-wire.mjs';
8
8
  import { oatsError } from './errors.mjs';
9
+ import { providerReasons, safeProviderReason } from './provider-reasons.mjs';
9
10
 
10
11
  /** Remap dictionary keys for an adoption subtree, but retain original document
11
12
  * pointers/spans inside each witness. No file/config is read here. */
@@ -42,7 +43,7 @@ export function prepareProviderBindings({seed,plan,manifests,declarations},invok
42
43
  }).map(origin=>[canonicalJson(origin),origin])).values()];
43
44
  const problem=(slot,id,code,message,origins=originsFor(id))=>({code,message,origins,slot,capability:id});
44
45
  const refuse=(slot,id,phase,error)=>problem(slot,id,safeCodes.has(error?.code)?error.code:'provider-not-qualified',
45
- `${id} ${slot} ${phase} binding could not be prepared`); // Never provider free text.
46
+ safeProviderReason(error?.message,providerReasons(manifests.get(id))) ?? `${id} ${slot} ${phase} binding could not be prepared`); // Recheck fixed text; never arbitrary exceptions.
46
47
  for (const [id,manifest] of manifests) {
47
48
  if (!Object.hasOwn(seed.artifacts.capabilities,id) || !manifest.layer) continue;
48
49
  if (plan.providers[manifest.layer] !== id) problems.push(problem(manifest.layer,id,'needs-configuration','fundamental capability must be selected in its own layer'));
@@ -13,6 +13,7 @@ import { readApprovalLedger, evaluateCapturedApprovals } from './artifact-approv
13
13
  import { validateBindingInterface } from './provider-binding.mjs';
14
14
  import { BINDING_LIMITS, validateBindingRequest, decodeBindingResponse } from './provider-binding-wire.mjs';
15
15
  import { oatsError } from './errors.mjs';
16
+ import { providerReasons } from './provider-reasons.mjs';
16
17
 
17
18
  // Same kernel-owned CLI locator as lifecycle hooks; never caller env or PATH.
18
19
  const CLI_BIN=fileURLToPath(new URL('../bin/oats.mjs',import.meta.url));
@@ -58,7 +59,7 @@ export function invokeProviderBinding(options,codecs) {
58
59
  timeout:timeoutMs,killSignal:'SIGKILL',maxBuffer:BINDING_LIMITS.maxBytes,stdio:['pipe','pipe','pipe'],shell:false});
59
60
  } catch { throw oatsError('provider-unavailable','provider codec could not execute'); }
60
61
  if (result.error || result.signal || result.status !== 0) throw oatsError('provider-unavailable','provider codec did not complete successfully');
61
- const response=decodeBindingResponse(result.stdout,request);
62
- if (!response.ok) throw oatsError(response.error.code,'provider binding phase refused');
62
+ const response=decodeBindingResponse(result.stdout,request,{reasons:providerReasons(manifest)});
63
+ if (!response.ok) throw oatsError(response.error.code,response.error.message ?? 'provider binding phase refused');
63
64
  return response.result;
64
65
  }
@@ -8,6 +8,7 @@ import { isMaterializedCapabilityId } from './capability-provenance.mjs';
8
8
  import { BINDING_PHASES } from './provider-binding.mjs';
9
9
  import { resolveChoices } from './portable-choices.mjs';
10
10
  import { oatsError } from './errors.mjs';
11
+ import { providerReasons, safeProviderReason, validateBindingReasons } from './provider-reasons.mjs';
11
12
 
12
13
  export const BINDING_LIMITS = Object.freeze({maxBytes:1024*1024,maxDepth:32,maxEntries:16384});
13
14
  export const BINDING_PROBLEMS = Object.freeze(['needs-configuration','requirement-conflict','invalid-binding','authorization-required','host-requirement-missing','provider-unavailable','provider-not-qualified']);
@@ -16,11 +17,12 @@ const same = (a,b) => canonicalJson(a) === canonicalJson(b);
16
17
  const roleKinds = {soul:['soul-requirement','soul-default'],workspace:['workspace-default'],adoption:['import-adoption'],operator:['operator']};
17
18
  const pointer = key => typeof key === 'string' && /^(?:\/(?:[^~]|~[01])*)+$/.test(key);
18
19
  export const bindingField = (key,slot) => pointer(key) && key.startsWith(`/bindings/${slot}/`) && key.length > `/bindings/${slot}/`.length;
19
- function problem(value) {
20
+ function problem(value,reasons) {
20
21
  objectAt(value,['code','message'],['code']);
21
22
  if (!BINDING_PROBLEMS.includes(value.code)) fail();
22
23
  if (value.message !== undefined) stringAt(value.message,'/message',{empty:true});
23
- return {code:value.code}; // Free text never leaves the provider boundary.
24
+ const message=safeProviderReason(value.message,reasons);
25
+ return {code:value.code,...(message === undefined ? {} : {message})}; // Only declared fixed text crosses.
24
26
  }
25
27
  export function validateBindingRequest(request) {
26
28
  canonicalJson(request,BINDING_LIMITS);
@@ -67,15 +69,19 @@ function witnessed(origin,declarations) {
67
69
  return same(a,b);
68
70
  }));
69
71
  }
70
- export function decodeBindingResponse(bytes,request) {
72
+ export function decodeBindingResponse(bytes,request,{reasons}={}) {
71
73
  let response;
72
74
  try {
73
75
  validateBindingRequest(request);
76
+ // Out-of-band trusted caller input, NEVER a field supplied in the response.
77
+ // Unknown providers legitimately have no compatibility reasons; an explicit
78
+ // manifest declaration was already validated with the stricter nonempty bound.
79
+ reasons=validateBindingReasons(reasons === undefined ? providerReasons({capability:request.capability}) : reasons,{allowEmpty:true});
74
80
  response=parseStrictJson(bytes,BINDING_LIMITS);
75
81
  objectAt(response,response?.ok === true ? ['schemaVersion','phase','slot','capability','ok','result'] : ['schemaVersion','phase','slot','capability','ok','error'],
76
82
  response?.ok === true ? ['schemaVersion','phase','slot','capability','ok','result'] : ['schemaVersion','phase','slot','capability','ok','error']);
77
83
  for (const key of ['schemaVersion','phase','slot','capability']) if (response[key] !== request[key]) fail();
78
- if (response.ok === false) return {ok:false,error:problem(response.error)};
84
+ if (response.ok === false) return {ok:false,error:problem(response.error,reasons)};
79
85
  if (response.ok !== true) fail();
80
86
  const result=response.result;
81
87
  if (request.phase === 'normalize') {
@@ -102,7 +108,7 @@ export function decodeBindingResponse(bytes,request) {
102
108
  }
103
109
  objectAt(result,['status','problems'],['status','problems']);
104
110
  if (!['ready','needs-configuration','authorization-required','unavailable'].includes(result.status) || !Array.isArray(result.problems)) fail();
105
- const problems=result.problems.map(problem);
111
+ const problems=result.problems.map(value=>problem(value,reasons));
106
112
  if (result.status === 'ready' && problems.length) fail();
107
113
  return {ok:true,result:{status:result.status,problems}};
108
114
  } catch { fail(); }
@@ -1,16 +1,21 @@
1
1
  /** Provider-owned binding protocol. This codec declares no provider model and
2
2
  * grants no execution authority; commands remain in the sole manifest table. */
3
- import { objectAt, stringAt, versionAt } from './portable-shape.mjs';
3
+ import { objectAt, stringAt, stringSetAt, versionAt } from './portable-shape.mjs';
4
4
  import { FUNDAMENTAL_SLOTS } from './portable-policy.mjs';
5
5
  import { oatsError } from './errors.mjs';
6
+ import { validateBindingReasons } from './provider-reasons.mjs';
6
7
 
7
8
  export const BINDING_PHASES = Object.freeze(['normalize', 'bind', 'check']);
8
9
  export function validateBindingInterface(manifest) {
9
10
  if (manifest.binding === undefined) return null;
10
11
  if (!FUNDAMENTAL_SLOTS.includes(manifest.layer)) throw oatsError('invalid-binding-interface', 'binding codecs belong to a fundamental provider slot');
11
12
  const binding = manifest.binding;
12
- objectAt(binding, ['version', ...BINDING_PHASES], ['version', ...BINDING_PHASES], '/binding');
13
+ objectAt(binding, ['version', ...BINDING_PHASES, 'reasons', 'keys'], ['version', ...BINDING_PHASES], '/binding');
13
14
  versionAt(binding.version);
15
+ if (Object.hasOwn(binding, 'reasons')) validateBindingReasons(binding.reasons);
16
+ // 0.24.4 validates declarations only; ownership/routing enforcement is 0.25.
17
+ if (Object.hasOwn(binding, 'keys')) stringSetAt(binding.keys, '/binding/keys', (key, pointer) =>
18
+ stringAt(key, pointer, { pattern: /^[A-Za-z][A-Za-z0-9_-]*\.?$(?![\s\S])/ }));
14
19
  for (const phase of BINDING_PHASES) {
15
20
  const name = binding[phase];
16
21
  stringAt(name, `/binding/${phase}`, { pattern: /^[a-z0-9][a-z0-9-]*$/ });