@llblab/pi-kit 0.37.1 → 0.38.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 (124) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +30 -14
  7. package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
  8. package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
  9. package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
  59. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
  60. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
  61. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
  62. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
  63. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
  64. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
  70. package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
  71. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
  72. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
  73. package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
  74. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
  75. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
  76. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
  77. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
  78. package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
  79. package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
  80. package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
  81. package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
  82. package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
  83. package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
  84. package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
  85. package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
  86. package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
  87. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
  88. package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
  89. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
  90. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
  91. package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
  92. package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
  93. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
  94. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
  95. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
  96. package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
  97. package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
  98. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
  99. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
  100. package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
  101. package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
  102. package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
  103. package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
  104. package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
  105. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
  106. package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
  107. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
  108. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
  109. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
  110. package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
  111. package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
  112. package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
  113. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
  114. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
  115. package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
  116. package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
  117. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
  118. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
  119. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
  120. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
  121. package/node_modules/@llblab/pi-state-flow/package.json +6 -1
  122. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
  123. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
  124. package/package.json +2 -2
@@ -2,6 +2,23 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## 0.27.1: Selected Scope Read Diagnostics
8
+
9
+ - `Read availability`: Explicit reads now distinguish a selected owner with no available read view from an excluded owner. In pre-runtime Passive sessions, an unacquired Session no longer incorrectly reports itself excluded. Reads still do not create private memory, widen the selection or return partial multi-path results; normal accepted writes retain their acquisition rules.
10
+
11
+ ## 0.27.0: Semantic Memory and Scope Selection
12
+
13
+ - `Semantic API (breaking)`: `strategy`/`tactics` replace `contract`/`working` in runtime fields, public types, schemas, queries, receipts and Skills. Strategy retains binding requirements, accepted approaches and rejection reasons; tactics holds operational evidence. Proposals are not commitments. New-format retired-root writes reject; ordinary nested keys remain legal.
14
+ - `Scope selection and tools (breaking)`: All seven nonempty Global/CWD/Session choices work independently of Active/Passive/Off. Commands accept explicit owners and completion; Telegram preserves the choice, filters inspection/help, and shows Effective only for2+ owners. One WANTED owner uses direct semantic planes and relative current refs; multiple owners use explicit atomic patches. Wanted selection is distinct from available memory. New accepted answers remain Session-only.
15
+ - `Intent ownership (breaking)`: Structured refs in intents now own same-scope strategy/tactics/lazy object keys. Fixed phases apply intents, clear unprotected targets, then other fields; remaining/new related refs protect targets. Later writes recreate cleared paths without old fields, independent of JSON key order. Existing strategy refs acquire ownership for future intent deletions; accepted history replays net patches without re-running cleanup.
16
+ - `Recoverable conversion`: Legacy1/2 translate to Strategy/Tactics before filtering. Candidate metadata4 retains declared external object planes/descriptors without definitions through core writes, folding and fork; core-only streams keep3. Explicit upgrades reject raw namespace collisions and require private exact-byte backup/CAS with foreign-edit-safe rollback. Reads/mode persistence never upgrade. Quiesce old writers; universal old-writer safety is not promised. Legacy support remains throughout0.x.
17
+ - `Request and acceptance authority`: Original-ID receipts, physical runtime/CWD/storage identity, captured selection and operation signals gate tools/context/answers before semantic interpretation or source callbacks. Real scope changes revoke stale work without ABA revival; equal choices preserve eligible work. Pending/failed Active acceptance fences inference, Off stays native-only, controls remain independent, and accepted publication survives later cancellation or bookkeeping failure.
18
+ - `Current memory and private forks`: One accepted/observed runtime preserves sparse current state, independent revisions and private causal proof. Observation cannot acquire or publish. Shared drift yields a validated current-only origin without private reset. Deferred forks copy only the direct parent’s current state under exact source/target proof; acquired children never recopy. Opaque future/malformed metadata stays intact, and single-use callback publishers expire.
19
+ - `Source-bound artifacts and Skills`: Selected owner/provenance gates maintenance and compilation; excluded owners do not become flat destinations. Ordinary artifacts retain fingerprints, Skills retain read-driven hashes, and foreign native reads stay volatile rather than being relocated. Compilation/provenance publish with semantic output; caller-forged provenance rejects. Source reads stay outside canonical exclusion; exact CAS is rechecked before publication.
20
+ - `Context and modules`: Built-ins use executable immutable/context and async-producer contracts. Candidate public modules have revocable native-host lifetimes, retained metadata4 identities and matched mutable roots and captured Active/Passive user-level context; Active producers commit cohort-atomic replacements. Sparse history/IDs, special keys/provenance and one atomic engine retained. No measured quality/cost/cache gain. Calibration: `769eed097736760360cae612d2b331c7549787d15617d90c065df510a507544c`.
21
+
5
22
  ## 0.26.5: Telegram Mode Receipt Hotfix
6
23
 
7
24
  - `Telegram mode controls`: Passive now refreshes the selected button on the first accepted click after Off, including existing-memory restoration and write-fence recovery during an active run. Accepted controls retain their renewed presentation receipt instead of being mistaken for canceled work; later mode choices, branch changes and shutdown still suppress obsolete updates. Memory storage and mode semantics are unchanged; no migration is required.
@@ -100,20 +100,24 @@ Memory has three ownership scopes:
100
100
  - `cwd`: Knowledge shared by sessions in the same working directory.
101
101
  - `session`: State belonging to the current session and its selected branch.
102
102
 
103
- They compose recursively in **global → CWD → session** order:
103
+ Participation is configured with a nonempty `scopes` array in [global configuration](docs/usage.md#configuration); omission selects all three. Each native branch captures that choice once, and forks inherit it. All seven nonempty selections use the same runtime. The existing mode commands accept optional whitespace-separated owners, for example `/state-flow-active session`, `/state-flow-passive cwd global`, or `/state-flow-off cwd`. Blank/mode-only commands and Telegram controls preserve the captured choice. Duplicate/unknown tokens reject before changing policy; Off records a new choice without canonical memory I/O. No settings chooser is required. Excluded owners receive no automatic memory contribution or semantic writes; new answers are stored only when Active and Session participates. Mode-only Telegram buttons retain that choice; explicit operator inspection is separate from model participation. See the [functional boundary and legacy proof dependencies](docs/scope-selection-contract.md#m2-functional-acceptance-boundary).
104
+
105
+ Participating owners compose recursively in **global → CWD → session** order:
104
106
 
105
107
  - More-specific values override broader ones, while object fields merge.
106
108
  - Removing a local value can reveal an inherited value again.
107
109
 
108
110
  The agent receives the **effective view** of this composition, not three unrelated memory dumps. It can read that view, or inspect one scope when ownership matters. `effective` is a computed view, not a fourth storage scope. Scope precedence does not turn memory into system-level instructions.
109
111
 
112
+ With one captured owner, current same-owner structured references in Active/Passive heads and projection-bound updates are relative, lazy navigation points to `lazy`, and Active recent context records use `{id,at,patch}` without an owner wrapper. Multi-owner records stay explicit. Canonical storage, internal comparisons and causal IDs remain scoped; provenance and historical/cross-owner/resource references retain their identities. Skill acquisition targets and deletion text also follow wanted arity; new Stop/reload/bootstrap handoffs render eligible refs relatively. Source keys and canonical provenance/compiler diagnostics retain their identities, not extra flat patch wrappers. These DTOs are data, not authoring permission. See the [context presentation boundary](docs/single-scope-interface-contract.md#runtime-context-presentation).
113
+
110
114
  ### Semantic planes
111
115
 
112
116
  Runtime views provide these documented planes:
113
117
 
114
- - `intents`: The queue of chosen actions. Work from intents: a structured `{"$ref"}` inside an intent owns a same-scope `working`/`lazy` key, and deleting the intent deletes what it owns unless another intent still references it. Textual `$path` mentions only use; unowned entries remain legal.
115
- - `contract`: Requirements, decisions, rejected approaches, constraints and interface commitments.
116
- - `working`: Temporary context of those actions: observations, results and uncertainties.
118
+ - `intents`: The queue of chosen actions. Work from intents: a structured `{"$ref"}` inside an intent owns a same-scope `strategy`/`tactics`/`lazy` object key. Fixed phases ignore JSON insertion order: intents first, ownership cleanup with final remaining/new intents protecting direct and ancestor/descendant targets, then remaining authored planes and existing compilation/normalization/atomic publication. Rewrite survivors at the same/new path in that patch; cleared targets start fresh without old fields. Textual `$path` mentions only use; unowned entries remain legal.
119
+ - `strategy`: Binding requirements, accepted approaches/plans, decisions, rejected alternatives and interface commitments. A proposal is not a commitment.
120
+ - `tactics`: Temporary execution context: verified progress, consequential failures, blockers, uncertainties and next checks.
117
121
  - `artifacts`: Source-addressed descriptions and compiled knowledge.
118
122
  - `response`: The exact latest accepted answer, including an empty string. The runtime captures it only in Session; Global/CWD receive no newly accepted answers, and stored scopes may omit it entirely. Effective uses the highest-priority nonempty value. Active automatically projects it; Passive omits it from automatic context while keeping storage and explicit reads intact.
119
123
  - `lazy`: Supporting memory available through explicit reads; its body is omitted from baseline model context.
@@ -122,7 +126,7 @@ Every plane is optional on disk:
122
126
 
123
127
  - Stored checkpoints and patches may omit any documented plane, including `intents`, `lazy` or `response`.
124
128
  - Current and historical views assemble only the known fields actually present in the selected scopes; absent fields are not filled in. Empty `response` counts as absent.
125
- - Readers ignore unknown top-level fields, and writers emit only known fields. Nested data inside known planes is unrestricted.
129
+ - Readers ignore unrelated unknown top-level fields, and writers emit only known fields. Retired `contract`/`working` roots explicitly reject in the target format; nested ordinary keys remain unrestricted.
126
130
  - Reading or starting never rewrites data just to normalize it, and a missing field is not a storage-format error.
127
131
 
128
132
  These planes organize ordinary JSON; there is no project-specific schema. The model updates every plane except `response`, which is runtime-owned.
@@ -130,7 +134,7 @@ These planes organize ordinary JSON; there is no project-specific schema. The mo
130
134
  Revisions:
131
135
 
132
136
  - Global and CWD revisions are shared by their canonical stores; Session has its own revision.
133
- - Effective has no single owner: its identity is the `g#c#s#` vector.
137
+ - Effective combines participating owners: with multiple owners its identity is their revision vector (for example `g#c#s#` when all three participate), not one universal revision. Excluded owners do not become fabricated vector entries.
134
138
  - One atomic patch advances each materially changed scope once.
135
139
 
136
140
  Memory remains fallible: storing an observation does not make it current or correct.
@@ -139,7 +143,13 @@ Registered Pi Skills may be compiled into source-addressed artifacts when durabl
139
143
 
140
144
  ## Incremental updates and history
141
145
 
142
- `patch_state` updates one or more named scopes atomically:
146
+ `patch_state` uses the captured wanted interface, not the number of currently available views. With **one participating owner**, supply semantic planes directly, without an owner wrapper:
147
+
148
+ ```json
149
+ { "strategy": { "verification": { "command": "npm test" } } }
150
+ ```
151
+
152
+ With **multiple participating owners**, name the supplied owners explicitly; they commit atomically. Canonical ownership remains explicit internally in both cases:
143
153
 
144
154
  - It waits cancelably for the store lock, then applies authored Global/CWD patches to the current canonical values, so other writers' untouched fields survive.
145
155
  - Overlapping assignments follow successful acceptance order. Correct repeats succeed without new semantic revisions.
@@ -147,28 +157,32 @@ Registered Pi Skills may be compiled into source-addressed artifacts when durabl
147
157
  - Object patches merge recursively, and `null` deletes an object key instead of being stored.
148
158
  - Accepted supplied scopes recursively drop empty object fields and planes, including explicit `{}`; empty ancestors disappear. Array slots and empty arrays stay intact. Cleanup is scope-local and may reveal inherited values. Model context hides legacy empty branches, but reading never rewrites history. See [patch semantics](docs/architecture.md#model-tools).
149
159
 
160
+ Multi-owner example, requiring CWD and Session to participate:
161
+
150
162
  ```json
151
163
  {
152
164
  "cwd": {
153
- "contract": { "verification": { "command": "npm test" } }
165
+ "strategy": { "verification": { "command": "npm test" } }
154
166
  },
155
167
  "session": {
156
168
  "intents": { "verify": { "action": "Run the checks before publishing" } },
157
- "working": { "checks": "Pending" }
169
+ "tactics": { "checks": "Pending" }
158
170
  }
159
171
  }
160
172
  ```
161
173
 
162
174
  During an active episode, a material patch is an inference barrier: sibling tool calls are blocked, and the next inference sees the accepted effective state. Ordinary completion needs no finalization patch or extra State Flow reasoning loop.
163
175
 
164
- `read_state` provides targeted current and historical access:
176
+ `read_state` accepts flat ordinary paths with one owner (for example `{ "path": "tactics" }`); `effective[N]` selects that owner's retained temporal boundary and scalar `patches[N]` its retained patch. Current same-owner structured refs may use `{ "$ref": "lazy.plan" }`; persistence anchors them to the exact owner. Never reinterpret different-owner/historical/effective refs, resources or text as local permission.
177
+
178
+ With multiple owners, unscoped reads use the effective overlay. This multi-owner example requires every explicit owner to participate:
165
179
 
166
180
  ```json
167
- { "paths": ["effective.contract", "cwd.working", "session.intents"] }
181
+ { "paths": ["effective.strategy", "cwd.tactics", "session.intents"] }
168
182
  ```
169
183
 
170
- - `working`: Current effective working memory; unscoped paths are effective aliases.
171
- - `effective[1].working`: Working memory at the preceding accepted transition boundary, when retained.
184
+ - `tactics`: Current sole-owner memory, or the effective overlay when multiple owners participate.
185
+ - `effective[1].tactics`: Working memory at the preceding accepted transition boundary, when retained.
172
186
  - `cwd.patches[0]`: The latest retained CWD semantic patch.
173
187
  - `effective.lazy.memory[0..3]`: A bounded slice of a stored collection.
174
188
 
@@ -225,7 +239,7 @@ How mode changes interact with storage:
225
239
  - Explicit Off inspection may read current stored values through a disposable validated reader. It does not activate memory, install cache or change pending fork acquisition. Missing private authority stays unavailable.
226
240
  - Active waits for a coherent capture and acceptance; another mode or selection can withdraw its wait.
227
241
 
228
- State Flow accepts only the canonical store contract and provides no in-place format converter. Preserve existing data and check the [format boundary](docs/usage.md#moving-a-store-and-supported-formats) before changing versions or moving a store.
242
+ Canonical scope metadata v3 uses `strategy` and `tactics`. Supported v1/v2 checkpoint/tail cohorts are read through a declared translation and converted once on an owned semantic acceptance, after a verified exact-byte backup outside the canonical store. Upgrade/quiesce all writers first; unsupported layouts are not repaired. See the [format and backup boundary](docs/usage.md#moving-a-store-and-supported-formats) before upgrading or moving a store.
229
243
 
230
244
  ## Operational boundaries
231
245
 
@@ -244,6 +258,8 @@ Treat state, diagnostic logs and backups as private data. Revalidate consequenti
244
258
  - [Acceptance map](docs/temporal-acceptance.md) — Properties tied to concrete tests.
245
259
  - [Documentation index](docs/README.md) — All maintained guides.
246
260
 
261
+ The six built-in planes use an [internal semantic-module contract](docs/semantic-module-contract.md) for authoring policy and mode-aware context. It also supplies isolated context/producer executors. The candidate [public module entry](docs/public-module-contract.md) adds native-host-scoped registration/lifetime binding; metadata4 opaque-retention codecs have bounded local acceptance, and registered mutable external roots now use the existing read/patch tools in the local candidate; captured inputs and Active/Passive module context now execute with source and native-identity fences, and Active async producers commit complete replacements through the same atomic engine. Ordinary public consumers now have SOURCE/built-SDK acceptance evidence for current-only forks and opaque retention across folding, reload, absence and re-registration. This remains an unpublished local candidate, not a new independent activation approval. Neither contract implies a new loader or automatic background reasoning.
262
+
247
263
  For development, run `npm install` and `npm run validate`. `npm run benchmark` is opt-in and separate from the normal suite; see `benchmarks/README.md` in a source checkout.
248
264
 
249
265
  Project context: [AGENTS.md](AGENTS.md), [BACKLOG.md](BACKLOG.md), [CHANGELOG.md](CHANGELOG.md).
@@ -0,0 +1,11 @@
1
+ // Domain: public State Flow module registration membrane.
2
+ // Registry ownership and semantic publication remain package-private.
3
+ export { registerStateFlowModule } from "../lib/module-registry.ts";
4
+ export type {
5
+ ModuleContextContribution,
6
+ StateFlowModule,
7
+ StateFlowModuleFrame,
8
+ StateFlowModuleInput,
9
+ StateFlowModuleSource,
10
+ } from "../lib/semantic-module.ts";
11
+ export type { JsonObject, JsonValue } from "../lib/json.ts";
@@ -0,0 +1,3 @@
1
+ export { registerStateFlowModule } from "../lib/module-registry.ts";
2
+ export type { ModuleContextContribution, StateFlowModule, StateFlowModuleFrame, StateFlowModuleInput, StateFlowModuleSource, } from "../lib/semantic-module.ts";
3
+ export type { JsonObject, JsonValue } from "../lib/json.ts";
@@ -0,0 +1,3 @@
1
+ // Domain: public State Flow module registration membrane.
2
+ // Registry ownership and semantic publication remain package-private.
3
+ export { registerStateFlowModule } from "../lib/module-registry.js";
@@ -1,5 +1,6 @@
1
1
  import { type ArtifactInvalidationReason, type ArtifactInvalidationRequest, type ArtifactSourceIdentity, type ArtifactProvenanceRegistry } from "./artifact.ts";
2
- import type { AtomicScopePatches, ScopedSemanticStates, ScopedStates, StateScope } from "./state.ts";
2
+ import { type AtomicScopePatches, type ScopeStateValues, type StateScope } from "./state.ts";
3
+ import { type ScopeSelection } from "./selection.ts";
3
4
  /** Why the caller is considering source-body acquisition. */
4
5
  export type ArtifactAcquisitionIntent = "routine" | "new-session" | "relevant-gap" | "exact-source" | "exact-edit" | "contradiction-or-failure" | "explicit-request" | "maintenance";
5
6
  export type ArtifactAcquisitionReason = ArtifactInvalidationReason | "materialized-gap" | "exact-source" | "exact-edit" | "contradiction-or-failure" | "explicit-request" | "maintenance";
@@ -25,7 +26,7 @@ export interface SuccessfulArtifactRead extends ArtifactInvalidationRequest {
25
26
  export declare class ArtifactReadTracker {
26
27
  #private;
27
28
  readonly successful: Map<string, SuccessfulArtifactRead>;
28
- setCandidates(candidates: Iterable<ArtifactInvalidationRequest>): void;
29
+ setCandidates(candidates: Iterable<ArtifactInvalidationRequest>, selection?: ScopeSelection): void;
29
30
  clear(): void;
30
31
  recordStart(toolCallId: string, toolName: string, args: unknown): void;
31
32
  recordCall(toolCallId: string, toolName: string, input: unknown): void;
@@ -54,9 +55,11 @@ export declare class ArtifactAcquisitionState {
54
55
  /** Forget plan and hints when no memory view is selected. */
55
56
  reset(): void;
56
57
  /** Re-observe exact registered sources for the selected scope artifacts. */
57
- refresh(states: ScopedStates, provenance: (scope: StateScope) => ArtifactProvenanceRegistry): void;
58
+ refresh(states: ScopeStateValues, provenance: (scope: StateScope) => ArtifactProvenanceRegistry, selection?: ScopeSelection): void;
58
59
  /** Accepted compilations leave the plan; correlated read evidence is single-use. */
59
60
  acceptAcquired(paths?: ReadonlySet<string>): void;
60
61
  }
62
+ /** Exact-path metadata revalidation only; callers acquire source bodies outside canonical exclusion. */
63
+ export declare function validateArtifactReadEvidence(reads: Iterable<SuccessfulArtifactRead>, selection?: ScopeSelection): void;
61
64
  /** Deletion patches for registered artifacts whose exact source is observed missing, in every owning scope. */
62
- export declare function missingArtifactRemovals(states: ScopedSemanticStates): AtomicScopePatches;
65
+ export declare function missingArtifactRemovals(states: ScopeStateValues, selection?: ScopeSelection): AtomicScopePatches;
@@ -1,5 +1,7 @@
1
1
  import { classifyArtifactCompilationNeed, inspectRegisteredArtifactPaths, sameArtifactSourceFingerprint, ORDINARY_ARTIFACT_COMPILER, } from "./artifact.js";
2
- import { isObject } from "./json.js";
2
+ import { isObject, sameJson } from "./json.js";
3
+ import { assertScopeStateValues } from "./state.js";
4
+ import { captureScopeSelection, DEFAULT_SCOPE_SELECTION } from "./selection.js";
3
5
  function readPath(toolName, args) {
4
6
  return toolName === "read" && isObject(args) && typeof args.path === "string"
5
7
  ? args.path
@@ -10,10 +12,35 @@ export class ArtifactReadTracker {
10
12
  successful = new Map();
11
13
  #pending = new Map();
12
14
  #candidates = new Map();
13
- setCandidates(candidates) {
15
+ #selection = DEFAULT_SCOPE_SELECTION;
16
+ setCandidates(candidates, selection = DEFAULT_SCOPE_SELECTION) {
17
+ const wanted = captureScopeSelection(selection);
18
+ const next = new Map();
19
+ for (const candidate of candidates) {
20
+ if (!wanted.includes(candidate.scope ?? "global"))
21
+ continue;
22
+ const captured = structuredClone(candidate);
23
+ for (const key of ["scope", "hash", "sourceFingerprint"])
24
+ if (captured[key] === undefined)
25
+ delete captured[key];
26
+ next.set(candidate.path, captured);
27
+ }
28
+ if (!sameJson(wanted, this.#selection))
29
+ this.clear();
30
+ this.#selection = wanted;
31
+ for (const [path] of this.successful) {
32
+ const previous = this.#candidates.get(path), current = next.get(path);
33
+ if (previous === undefined || current === undefined || !sameJson(previous, current))
34
+ this.successful.delete(path);
35
+ }
14
36
  this.#candidates.clear();
15
- for (const candidate of candidates)
16
- this.#candidates.set(candidate.path, structuredClone(candidate));
37
+ for (const [path, candidate] of next)
38
+ this.#candidates.set(path, candidate);
39
+ for (const [id, pending] of this.#pending) {
40
+ const current = pending.path === undefined ? undefined : next.get(pending.path);
41
+ if (current === undefined || pending.candidate === undefined || !sameJson(current, pending.candidate))
42
+ this.#pending.delete(id);
43
+ }
17
44
  }
18
45
  clear() {
19
46
  this.successful.clear();
@@ -31,10 +58,10 @@ export class ArtifactReadTracker {
31
58
  if (isError || !pending || toolName !== pending.toolName)
32
59
  return;
33
60
  const path = readPath(pending.toolName, pending.args);
34
- if (path === undefined)
61
+ if (path === undefined || path !== pending.path)
35
62
  return;
36
63
  const candidate = this.#candidates.get(path);
37
- if (candidate === undefined || pending.fingerprint === undefined)
64
+ if (candidate === undefined || pending.candidate === undefined || pending.fingerprint === undefined || !sameJson(candidate, pending.candidate))
38
65
  return;
39
66
  const observation = inspectRegisteredArtifactPaths([path])[0];
40
67
  if (observation?.kind !== "present" || !sameArtifactSourceFingerprint(pending.fingerprint, observation.fingerprint))
@@ -53,7 +80,7 @@ export class ArtifactReadTracker {
53
80
  const observation = path !== undefined && this.#candidates.has(path) ? inspectRegisteredArtifactPaths([path])[0] : undefined;
54
81
  this.#pending.set(toolCallId, {
55
82
  toolName,
56
- args,
83
+ args, path, candidate: path === undefined ? undefined : this.#candidates.get(path),
57
84
  ...(observation?.kind === "present" ? { fingerprint: observation.fingerprint } : {}),
58
85
  });
59
86
  }
@@ -90,7 +117,6 @@ export function decideArtifactAcquisition(source, metadata, compiler, options) {
90
117
  }
91
118
  }
92
119
  export const SOURCE_CHANGED_HINT = "Source changed since this artifact was compiled. Read and recompile it before relying on it.";
93
- const SCOPES = ["global", "cwd", "session"];
94
120
  /**
95
121
  * One selected branch's ordinary-artifact acquisition plan: runtime-observed
96
122
  * invalidations, their model hints and the read tracker correlated to them.
@@ -100,6 +126,7 @@ export class ArtifactAcquisitionState {
100
126
  reads = new ArtifactReadTracker();
101
127
  #invalidations = [];
102
128
  #hints = {};
129
+ #selection = DEFAULT_SCOPE_SELECTION;
103
130
  get invalidations() { return this.#invalidations; }
104
131
  get hints() { return this.#hints; }
105
132
  /** Drop the invalidation plan; hints remain until the next refresh. */
@@ -110,35 +137,35 @@ export class ArtifactAcquisitionState {
110
137
  this.#setInvalidations([]);
111
138
  }
112
139
  /** Re-observe exact registered sources for the selected scope artifacts. */
113
- refresh(states, provenance) {
114
- const paths = new Set();
115
- for (const scope of SCOPES)
116
- for (const path of Object.keys(states[scope].artifacts))
117
- paths.add(path);
140
+ refresh(states, provenance, selection = DEFAULT_SCOPE_SELECTION) {
141
+ const wanted = captureScopeSelection(selection);
142
+ assertScopeStateValues(states, wanted);
143
+ const effective = new Map();
144
+ for (const scope of wanted)
145
+ for (const [path, metadata] of Object.entries(states[scope].artifacts ?? {}))
146
+ effective.set(path, { scope, metadata });
147
+ const paths = [...effective].filter(([, { metadata }]) => metadata.kind !== "skill").map(([path]) => path);
118
148
  const observations = new Map(inspectRegisteredArtifactPaths(paths).map((observation) => [observation.path, observation]));
119
149
  const hints = {};
120
- const invalidations = new Map();
121
- for (const scope of SCOPES) {
122
- const registry = provenance(scope);
123
- for (const [path, metadata] of Object.entries(states[scope].artifacts)) {
124
- // A narrower owner replaces broader evidence for the same path.
125
- delete hints[path];
126
- invalidations.delete(path);
127
- if (metadata.kind === "skill")
128
- continue;
129
- const observed = observations.get(path);
130
- if (observed?.kind !== "present")
131
- continue;
132
- const need = classifyArtifactCompilationNeed({ path, scope, sourceFingerprint: observed.fingerprint }, metadata, ORDINARY_ARTIFACT_COMPILER, false, registry[path]);
133
- if (need.kind !== "requires-compilation")
134
- continue;
135
- if (need.reason === "source-changed")
136
- hints[path] = SOURCE_CHANGED_HINT;
137
- invalidations.set(path, { path, scope, reason: need.reason });
138
- }
150
+ const invalidations = [];
151
+ const registries = new Map();
152
+ for (const path of paths) {
153
+ const { scope, metadata } = effective.get(path);
154
+ const observed = observations.get(path);
155
+ if (observed?.kind !== "present")
156
+ continue;
157
+ if (!registries.has(scope))
158
+ registries.set(scope, provenance(scope));
159
+ const need = classifyArtifactCompilationNeed({ path, scope, sourceFingerprint: observed.fingerprint }, metadata, ORDINARY_ARTIFACT_COMPILER, false, registries.get(scope)[path]);
160
+ if (need.kind !== "requires-compilation")
161
+ continue;
162
+ if (need.reason === "source-changed")
163
+ hints[path] = SOURCE_CHANGED_HINT;
164
+ invalidations.push({ path, scope, reason: need.reason });
139
165
  }
166
+ this.#selection = wanted;
140
167
  this.#hints = hints;
141
- this.#setInvalidations([...invalidations.values()].sort((left, right) => left.path.localeCompare(right.path)));
168
+ this.#setInvalidations(invalidations.sort((left, right) => left.path.localeCompare(right.path)));
142
169
  }
143
170
  /** Accepted compilations leave the plan; correlated read evidence is single-use. */
144
171
  acceptAcquired(paths = new Set(this.reads.successful.keys())) {
@@ -147,13 +174,27 @@ export class ArtifactAcquisitionState {
147
174
  }
148
175
  #setInvalidations(invalidations) {
149
176
  this.#invalidations = invalidations;
150
- this.reads.setCandidates(invalidations);
177
+ this.reads.setCandidates(invalidations, this.#selection);
178
+ }
179
+ }
180
+ /** Exact-path metadata revalidation only; callers acquire source bodies outside canonical exclusion. */
181
+ export function validateArtifactReadEvidence(reads, selection = DEFAULT_SCOPE_SELECTION) {
182
+ const wanted = captureScopeSelection(selection);
183
+ for (const read of reads) {
184
+ if (!wanted.includes(read.scope ?? "global"))
185
+ continue;
186
+ const observed = inspectRegisteredArtifactPaths([read.path])[0];
187
+ if (read.sourceFingerprint === undefined || observed?.kind !== "present" || !sameArtifactSourceFingerprint(read.sourceFingerprint, observed.fingerprint)) {
188
+ throw new Error(`Artifact source changed after acquisition: ${read.path}`);
189
+ }
151
190
  }
152
191
  }
153
192
  /** Deletion patches for registered artifacts whose exact source is observed missing, in every owning scope. */
154
- export function missingArtifactRemovals(states) {
193
+ export function missingArtifactRemovals(states, selection = DEFAULT_SCOPE_SELECTION) {
194
+ const wanted = captureScopeSelection(selection);
195
+ assertScopeStateValues(states, wanted);
155
196
  const owners = new Map();
156
- for (const scope of SCOPES)
197
+ for (const scope of wanted)
157
198
  for (const path of Object.keys(states[scope].artifacts ?? {})) {
158
199
  owners.set(path, [...owners.get(path) ?? [], scope]);
159
200
  }
@@ -107,6 +107,8 @@ export declare function isArtifactRegistry(value: unknown): value is ArtifactReg
107
107
  export declare function parseArtifactProvenanceRegistry(value: unknown, context?: string): ArtifactProvenanceRegistry;
108
108
  /** Canonical retained form; uninterpretable entries cannot round-trip and are omitted. */
109
109
  export declare function serializeArtifactProvenanceRegistry(registry: Readonly<ArtifactProvenanceRegistry>): JsonObject;
110
+ /** Eligibility to revoke known evidence, not retained-data admission or a sanitizer. */
111
+ export declare function isInterpretableArtifactProvenance(entry: unknown): entry is ArtifactProvenance;
110
112
  /** Decide whether retained compilation evidence requires source reacquisition. */
111
113
  export declare function classifyArtifactCompilationNeed(source: ArtifactSourceIdentity, metadata: unknown, compiler: string, explicitRefresh?: boolean, provenance?: unknown): ArtifactCompilationNeed;
112
114
  /** Split one compiler output into model-visible semantics and runtime-owned provenance. */
@@ -160,34 +160,25 @@ export function parseArtifactProvenanceRegistry(value, context = "Artifact prove
160
160
  return {};
161
161
  if (!isObject(value))
162
162
  throw new Error(`${context} must be a path-keyed JSON object`);
163
- const registry = {};
164
- for (const [path, entry] of Object.entries(value)) {
163
+ const known = new Set(["sourceHash", "sourceFingerprint", "compilerRevision", "compiledAt"]);
164
+ return Object.fromEntries(Object.entries(value).map(([path, entry]) => {
165
165
  if (path.trim().length === 0)
166
166
  throw new Error(`${context} path keys must be non-empty`);
167
- if (!isObject(entry) || !isJsonValue(entry)) {
168
- registry[path] = { malformed: true };
169
- continue;
170
- }
171
- const known = new Set(["sourceHash", "sourceFingerprint", "compilerRevision", "compiledAt"]);
172
- if (Object.keys(entry).some((key) => !known.has(key))) {
173
- registry[path] = { malformed: true };
174
- continue;
175
- }
176
- registry[path] = {
177
- ...(Object.hasOwn(entry, "sourceHash") ? { sourceHash: entry.sourceHash } : {}),
178
- ...(Object.hasOwn(entry, "sourceFingerprint") ? { sourceFingerprint: entry.sourceFingerprint } : {}),
179
- ...(Object.hasOwn(entry, "compilerRevision") ? { compilerRevision: entry.compilerRevision } : {}),
180
- ...(Object.hasOwn(entry, "compiledAt") ? { compiledAt: entry.compiledAt } : {}),
181
- };
182
- }
183
- return registry;
167
+ if (!isObject(entry) || !isJsonValue(entry) || Object.keys(entry).some(key => !known.has(key)))
168
+ return [path, { malformed: true }];
169
+ return [path, {
170
+ ...(Object.hasOwn(entry, "sourceHash") ? { sourceHash: entry.sourceHash } : {}),
171
+ ...(Object.hasOwn(entry, "sourceFingerprint") ? { sourceFingerprint: entry.sourceFingerprint } : {}),
172
+ ...(Object.hasOwn(entry, "compilerRevision") ? { compilerRevision: entry.compilerRevision } : {}),
173
+ ...(Object.hasOwn(entry, "compiledAt") ? { compiledAt: entry.compiledAt } : {}),
174
+ }];
175
+ }));
184
176
  }
185
177
  /** Canonical retained form; uninterpretable entries cannot round-trip and are omitted. */
186
178
  export function serializeArtifactProvenanceRegistry(registry) {
187
- const artifacts = {};
188
- for (const [path, entry] of Object.entries(registry)) {
179
+ return Object.fromEntries(Object.entries(registry).flatMap(([path, entry]) => {
189
180
  if (entry.malformed === true)
190
- continue;
181
+ return [];
191
182
  const fields = {};
192
183
  if (entry.sourceHash !== undefined)
193
184
  fields.sourceHash = entry.sourceHash;
@@ -197,11 +188,19 @@ export function serializeArtifactProvenanceRegistry(registry) {
197
188
  fields.compilerRevision = entry.compilerRevision;
198
189
  if (entry.compiledAt !== undefined)
199
190
  fields.compiledAt = entry.compiledAt;
200
- if (Object.keys(fields).length === 0)
201
- continue;
202
- artifacts[path] = fields;
203
- }
204
- return artifacts;
191
+ return Object.keys(fields).length === 0 ? [] : [[path, fields]];
192
+ }));
193
+ }
194
+ /** Eligibility to revoke known evidence, not retained-data admission or a sanitizer. */
195
+ export function isInterpretableArtifactProvenance(entry) {
196
+ if (!isObject(entry))
197
+ return false;
198
+ const keys = Object.keys(entry);
199
+ return keys.length > 0 && keys.every(key => ["sourceHash", "sourceFingerprint", "compilerRevision", "compiledAt"].includes(key))
200
+ && (!Object.hasOwn(entry, "sourceHash") || isArtifactHash(entry.sourceHash))
201
+ && (!Object.hasOwn(entry, "sourceFingerprint") || parseArtifactSourceFingerprint(entry.sourceFingerprint) !== undefined)
202
+ && (!Object.hasOwn(entry, "compilerRevision") || (typeof entry.compilerRevision === "string" && entry.compilerRevision.trim().length > 0))
203
+ && (!Object.hasOwn(entry, "compiledAt") || typeof entry.compiledAt === "string");
205
204
  }
206
205
  function invalidField(value, validate) {
207
206
  return value !== undefined && !validate(value);
@@ -1,9 +1,12 @@
1
+ import { type ScopeSelection } from "./selection.ts";
1
2
  import { type InactiveMode, type StateFlowMode } from "./snapshot.ts";
2
3
  export interface StateFlowConfig {
3
4
  /** Canonical State Flow repository. SDK callers may still override it explicitly. */
4
5
  directory: string;
5
6
  /** Default mode for genuinely new sessions only; each session owns its selected mode. */
6
7
  mode: StateFlowMode;
8
+ /** Concrete default selection for genuinely new or not-yet-captured legacy sessions. */
9
+ scopes: ScopeSelection;
7
10
  /** Non-active fallback for legacy `enabled:false` evidence and unavailable selections; never serialized. */
8
11
  inactiveMode: InactiveMode;
9
12
  /** Opt-in local capture of rejected patch attempts and unresolved terminal drafts. */
@@ -5,6 +5,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
  import { getDurableRepositoryRoot } from "./durable.js";
6
6
  import { DEFAULT_HISTORY_LIMIT, MAX_HISTORY_LIMIT } from "./history.js";
7
7
  import { isObject } from "./json.js";
8
+ import { captureScopeSelection, DEFAULT_SCOPE_SELECTION } from "./selection.js";
8
9
  import { isStateFlowMode } from "./snapshot.js";
9
10
  /** Read the repository-global config once at extension load/reload. Missing config uses defaults; invalid config never falls back. */
10
11
  export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = getDurableRepositoryRoot(agentDir)) {
@@ -13,6 +14,7 @@ export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = g
13
14
  const defaults = {
14
15
  directory,
15
16
  mode: "off",
17
+ scopes: DEFAULT_SCOPE_SELECTION,
16
18
  inactiveMode: "off",
17
19
  logging: false,
18
20
  showSuccessfulPatches: true,
@@ -28,7 +30,7 @@ export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = g
28
30
  throw new Error(`Cannot read State Flow configuration: ${path}`, { cause: error });
29
31
  }
30
32
  // autoStart/passiveBootstrap/passiveTools are read-only compatibility inputs; explicit mode is authoritative.
31
- const allowed = new Set(["mode", "autoStart", "passiveBootstrap", "passiveTools", "logging", "showSuccessfulPatches", "historyLimit"]);
33
+ const allowed = new Set(["scopes", "mode", "autoStart", "passiveBootstrap", "passiveTools", "logging", "showSuccessfulPatches", "historyLimit"]);
32
34
  if (!isObject(value) || Object.keys(value).some((key) => !allowed.has(key))) {
33
35
  throw new Error(`State Flow configuration contains unknown settings: ${path}`);
34
36
  }
@@ -46,12 +48,22 @@ export function loadStateFlowConfig(agentDir = getAgentDir(), repositoryRoot = g
46
48
  throw new Error(`State Flow showSuccessfulPatches must be a boolean: ${path}`);
47
49
  if (Object.hasOwn(value, "historyLimit") && (!Number.isSafeInteger(value.historyLimit) || value.historyLimit < 0 || value.historyLimit > MAX_HISTORY_LIMIT))
48
50
  throw new Error(`State Flow historyLimit must be an integer from 0 to ${MAX_HISTORY_LIMIT}: ${path}`);
51
+ let scopes = DEFAULT_SCOPE_SELECTION;
52
+ if (Object.hasOwn(value, "scopes")) {
53
+ try {
54
+ scopes = captureScopeSelection(value.scopes);
55
+ }
56
+ catch {
57
+ throw new Error(`Invalid State Flow scopes configuration: ${path}`);
58
+ }
59
+ }
49
60
  const legacyInactive = value.passiveBootstrap !== false || value.passiveTools !== false ? "passive" : "off";
50
61
  const hasLegacyMode = Object.hasOwn(value, "autoStart") || Object.hasOwn(value, "passiveBootstrap") || Object.hasOwn(value, "passiveTools");
51
62
  const mode = isStateFlowMode(value.mode) ? value.mode : value.autoStart === true ? "active" : hasLegacyMode ? legacyInactive : "off";
52
63
  return {
53
64
  directory,
54
65
  mode,
66
+ scopes,
55
67
  inactiveMode: isStateFlowMode(value.mode) ? inactiveModeFor(value.mode) : hasLegacyMode ? legacyInactive : "off",
56
68
  logging: value.logging === true,
57
69
  showSuccessfulPatches: value.showSuccessfulPatches !== false,