agent-bios 0.9.8 → 0.9.9

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 (49) hide show
  1. package/DEPENDENCIES.md +19 -19
  2. package/README.md +34 -11
  3. package/claude/CLAUDE.md +2 -1
  4. package/claude/guides/claude-prompting.md +1 -1
  5. package/claude/guides/cli-multi-model-workflow.md +19 -1
  6. package/claude/guides/coding-staged-workflow.md +32 -0
  7. package/claude/guides/gpt-prompting.md +1 -1
  8. package/claude/guides/learning-flow.md +5 -5
  9. package/claude/guides/llm-capability-boundary.md +6 -1
  10. package/claude/guides/session-distill-workflow.md +19 -9
  11. package/claude/guides/tooling-gotchas.md +16 -0
  12. package/claude/hooks/__pycache__/tooling-gotchas-hook.cpython-314.pyc +0 -0
  13. package/claude/hooks/tooling-gotchas-hook.py +7 -0
  14. package/codex/AGENTS.md +2 -1
  15. package/codex/guides/claude-prompting.md +1 -1
  16. package/codex/guides/cli-multi-model-workflow.md +19 -1
  17. package/codex/guides/coding-staged-workflow.md +32 -0
  18. package/codex/guides/gpt-prompting.md +1 -1
  19. package/codex/guides/learning-flow.md +5 -5
  20. package/codex/guides/llm-capability-boundary.md +6 -1
  21. package/codex/guides/session-distill-workflow.md +19 -9
  22. package/codex/guides/tooling-gotchas.md +16 -0
  23. package/{scripts → compose}/assemble.py +184 -16
  24. package/{scripts → compose}/canary.sh +14 -5
  25. package/{scripts → compose}/check-domains.py +9 -3
  26. package/{config → compose}/domains.json +1 -0
  27. package/{scripts → compose}/pkgid.py +8 -1
  28. package/compose/prune-backups.py +204 -0
  29. package/{scripts → compose}/register-hooks.py +3 -3
  30. package/{scripts/install.sh → install.sh} +401 -104
  31. package/launch/agent-launch.py +5294 -0
  32. package/launch/agent-launch.toml +376 -0
  33. package/{scripts → launch}/check-prompting-targets.sh +1 -1
  34. package/{scripts → launch}/provision-venv.sh +1 -1
  35. package/{scripts → learn}/check-learning.py +7 -7
  36. package/{scripts → learn}/collect-learning.py +10 -10
  37. package/{config → learn}/learning.schema.json +3 -3
  38. package/{scripts → learn}/migrate-learnings.py +95 -54
  39. package/{scripts → learn}/redact.py +4 -4
  40. package/package.json +25 -24
  41. package/wrappers/claude-run.sh +162 -0
  42. package/{scripts → wrappers}/codex-run.sh +62 -6
  43. package/config/agent-launch.toml +0 -143
  44. package/scripts/agent-launch.py +0 -2350
  45. package/scripts/check-parity.sh +0 -2003
  46. /package/{shell → launch}/agent-launch.zsh +0 -0
  47. /package/{config → learn}/promotions.json +0 -0
  48. /package/{scripts/session-cost.py → session-cost.py} +0 -0
  49. /package/{scripts → wrappers}/codex-helm.sh +0 -0
@@ -0,0 +1,376 @@
1
+ schema_version = 1
2
+
3
+ [backends.codex]
4
+ command = "codex"
5
+ passthrough_args = []
6
+
7
+ [backends.claude]
8
+ command = "claude"
9
+ passthrough_args = ["--dangerously-skip-permissions"]
10
+
11
+ # Optional dependencies. `command` gates the route: unresolvable means the route
12
+ # is unavailable and the review setup degrades without it. `install` is the single
13
+ # source for both the one-line hint shown when it is missing and what
14
+ # `agent-bios install --with <name>` runs.
15
+ [capabilities.onto]
16
+ command = "onto"
17
+ install = "npm i -g onto-mcp"
18
+ # How this tool exposes an operation. The adapter is a protocol codec owned by core,
19
+ # never a method identity, so a second method reusing the tag is data-only.
20
+ # `evidence` names what this tool reports back about the dispatch it performed, and a
21
+ # receipt missing any of it is not credited. These three are what onto's override already
22
+ # returns on its warning channel. They are here because of a measured incident, not a
23
+ # guess: an override that reached no seat resolved to a default provider with a null model
24
+ # and did NOT fail, so a reporter with nothing to check would have called it a success.
25
+ offers = [{ operation = "structured-review", adapter = "mcp-stdio-v1", hosts = ["codex", "claude"], evidence = ["reached_seat", "billing_mode", "auth_defaulted"] }]
26
+
27
+ # ── dynamic workflow, one concept and two host-bound tools ─────────────────
28
+ # `ultracode` runs ON CLAUDE and `ultracode-for-codex` runs ON CODEX — the names say which,
29
+ # and getting them the wrong way round is the mistake to avoid: the package name is
30
+ # "ultracode, ported for codex", not "the codex one is the real one".
31
+ #
32
+ # What they are FOR, which is what a reader choosing between reviewers actually needs:
33
+ # both drive a dynamic workflow, used here as a flexible many-perspective check on an
34
+ # IMPLEMENTATION — does it function, does it survive an adversary, does the logic hold,
35
+ # does it hold under a real scenario, does it hold under stress. That makes them
36
+ # COMPLEMENTARY to onto rather than competing with it: onto looks hardest at whether
37
+ # authority, gates and harness are consistent with each other. The two are independently
38
+ # selectable, and both single-sided choices are legitimate — a thing whose whole point is
39
+ # that it WORKS can be reviewed by the workflow alone, and a logic-bearing document,
40
+ # ontology, harness or gate can be reviewed by onto alone.
41
+
42
+ [capabilities.ultracode]
43
+ # Not a separate tool: the Claude Code CLI's own dynamic workflow, opened by putting the
44
+ # keyword in the prompt (read in the installed 2.1.220 bundle —
45
+ # `workflowKeywordTriggerEnabled`: "including the keyword in a prompt opts that turn into
46
+ # the Workflow tool", default true). So the command IS the claude backend, and there is
47
+ # nothing extra to install.
48
+ # `${backend}` and not the literal token: this tool IS the configured claude CLI, so a
49
+ # duplicated default broke it for anyone whose backend is a wrapper or an absolute path —
50
+ # dispatch worked, the reviewer reported NOT INSTALLED, and the review went on without it.
51
+ # No `install`: that field is EXECUTED by `agent-bios install --with`, so prose there would
52
+ # be run as a shell command, and a missing claude CLI is a missing host, not a missing
53
+ # reviewer.
54
+ command = "${backend}"
55
+ # claude only: the keyword trigger is a Claude Code feature, so a codex seat cannot run it.
56
+ offers = [{ operation = "workflow-review", adapter = "host-workflow-v1", hosts = ["claude"] }]
57
+
58
+ [capabilities.ultracode-for-codex]
59
+ command = "ultracode-for-codex"
60
+ install = "npm i -g ultracode-for-codex"
61
+ # codex only, and this is the tool's nature rather than a policy: it drives the Codex
62
+ # app-server and takes its model catalog from there, so a reviewer seated on the
63
+ # claude host is a seat it cannot fill. Declaring both hosts made a codex-main launch
64
+ # emit "run ultracode-for-codex on claude-fable-5", which cannot work.
65
+ #
66
+ # Between the two, this review is available CROSS-FAMILY from either main — which the
67
+ # codex arm previously had no way to express, and so silently went without.
68
+ offers = [{ operation = "workflow-review", adapter = "host-workflow-v1", hosts = ["codex"] }]
69
+
70
+ # ── review methods (DESIGN.md §1) ───────────────────────────────────────────
71
+ # Data only. Adding a method here — including one this repo has never seen — must
72
+ # need no code change; gates/check_parity.py proves that with a runtime-named canary.
73
+ # `instructions` may use only the core slot vocabulary: command, model, effort,
74
+ # provider, host, service_tier, perspectives, trials, severities. An unknown slot is
75
+ # rejected at load, so this list going stale is an over-restriction with a real cost — a
76
+ # reviewer that needs one of them cannot be authored from what the config says. The
77
+ # launcher's own rejection message enumerates INSTRUCTION_SLOTS, which is the list to
78
+ # trust if the two ever differ.
79
+
80
+ [review_methods.panel]
81
+ label = "Isolated panel"
82
+ description = "The always-present base: an isolated multi-perspective panel, no install required."
83
+ instructions = "dispatch {command} as a fresh read-only process for {trials} isolated passes over the perspectives {perspectives}, each pinned to {model}/{effort}, and aggregate"
84
+ output = "review-v1"
85
+ perspectives = ["correctness", "security", "reproduction"]
86
+ trials = 3
87
+ order = "randomized"
88
+ swap_augmentation = true
89
+ aggregation = "majority"
90
+ # Our own subagents report on the canonical ladder itself. Stated rather than left
91
+ # implicit, because an identity map is exactly what a wrongly copied one looks like.
92
+ severity_emits = ["blocker", "high", "medium", "low", "info"]
93
+ severity_map = { blocker = "blocker", high = "high", medium = "medium", low = "low", info = "info" }
94
+
95
+ [review_methods.onto]
96
+ label = "onto structured lens review"
97
+ description = "Audits any target — code, a design doc, a whole repo — against the intent you state, looking hardest at whether authority, gates and harness are consistent with one another; complementary to the ultracode workflows rather than an alternative, and enough on its own for a logic-bearing document, ontology, harness or gate. Six lenses, cited issues, dissent kept as an artifact. Needs no diff. Returns a handle, not a result: poll 10+ min, and read record_status before believing a clean report."
98
+ capability = "onto"
99
+ operation = "structured-review"
100
+ # `effort` is sent and `auth` is deliberately NOT. onto's per-call override REPLACES
101
+ # a seat whose route differs, so what it omits is dropped rather than inherited —
102
+ # which is why effort must be stated: this binding IS the reviewer's pinned rigour.
103
+ # auth is the opposite case. onto selects the metered route only when a seat says so in
104
+ # writing — `auth = "api_key"` or an `api_key_env` naming the variable to call — and
105
+ # defaults to the subscription worker otherwise, so an omitted auth cannot land on
106
+ # metered and stating one buys nothing. It does cost something: switching a seat to
107
+ # oauth is a route change, so a user who deliberately chose metered loses both their
108
+ # auth and the api_key_env that carried it. Omitting it lets onto's default protect the
109
+ # unconfigured seat and its overlay preserve the configured one.
110
+ instructions = 'call onto_review/onto_prepare_review with llmOverride={{"provider":"{provider}","model":"{model}","effort":"{effort}"}} so onto runs on {provider} at {effort}'
111
+ output = "review-v1"
112
+ perspectives = ["axiology", "coverage", "evolution", "logic", "semantics", "structure"]
113
+ trials = 1
114
+ order = "fixed"
115
+ swap_augmentation = false
116
+ aggregation = "union"
117
+ # onto's own REVIEW_SEVERITY_ORDER is this same ladder — read in its source, not
118
+ # assumed. Identity is correct here for a different reason than the panel below.
119
+ severity_emits = ["blocker", "high", "medium", "low", "info"]
120
+ severity_map = { blocker = "blocker", high = "high", medium = "medium", low = "low", info = "info" }
121
+
122
+ [review_methods.ultracode-for-codex]
123
+ label = "Ultracode workflow review (Codex)"
124
+ # Named for the tool, not for the legacy layer. The legacy route token `ultracode` used to
125
+ # BE this capability's key, so naming the Claude tool `ultracode` silently repointed the
126
+ # legacy route at it — the golden caught that, and the fix was to bind the legacy token to
127
+ # a capability explicitly (`LEGACY_ULTRACODE_CAPABILITY`) rather than let a setup name and
128
+ # a tool id be the same string.
129
+ description = "The same dynamic workflow, ported to Codex — same use: things that have to WORK, checked from many angles rather than one. Fans dozens of Codex agents over your UNCOMMITTED working-tree changes for file:line defects, each candidate verified by its own agent. Its built-in code-review fails before spawning anything when the tree holds no reviewable change — clean, or only types it excludes such as .java, Dockerfile or dist/. Passing that gate is not the same as your files being reviewed. Detached: launching exits 0 even when the run dies."
130
+ capability = "ultracode-for-codex"
131
+ operation = "workflow-review"
132
+ instructions = "run {command} against a self-contained review packet on {model}/{effort}"
133
+ output = "review-v1"
134
+ perspectives = ["orchestration"]
135
+ trials = 1
136
+ order = "fixed"
137
+ swap_augmentation = false
138
+ aggregation = "union"
139
+ # P0..P3 plus a null severity, taken from the tool's own result contract. The mapping
140
+ # is the owner's call: the tool documents the vocabulary but not what each level
141
+ # means, so it cannot be derived. "null" keys a finding the tool reports with none.
142
+ severity_emits = ["P0", "P1", "P2", "P3", "null"]
143
+ severity_map = { P0 = "blocker", P1 = "high", P2 = "medium", P3 = "low", null = "info" }
144
+
145
+ [review_methods.ultracode]
146
+ label = "Ultracode workflow review (Claude)"
147
+ description = "Claude Code's own dynamic workflow, on Claude. Decomposes the request across many agents and adversarially verifies each candidate finding before it survives. Use it on things that have to WORK — functional, adversarial, logic, scenario and stress passes — where onto is the one to reach for when the question is whether authority, gates and harness agree. Reviews whatever packet you hand it, so it needs no working-tree state. The keyword in the prompt is what opens the workflow: without it the turn is an ordinary session that will answer, plausibly, alone. PRECONDITION this launcher cannot establish: the keyword trigger must be enabled (`workflowKeywordTriggerEnabled`, default true, verified in claude 2.1.220). A resolvable claude CLI proves the command exists, not that the workflow opens — with that setting off the review runs as an ordinary session and would report as though it were orchestrated."
148
+ capability = "ultracode"
149
+ operation = "workflow-review"
150
+ # The keyword is the whole mechanism, so it is stated as a literal rather than left to the
151
+ # dispatcher's memory. The severity ladder is stated too, and that is what makes the
152
+ # identity map below TRUE rather than copied: this reviewer has no vocabulary of its own —
153
+ # it reports on the ladder because the request tells it to. A gate asserts the instruction
154
+ # still names every value the descriptor claims, so the two cannot drift apart.
155
+ instructions = "dispatch {command} as a fresh read-only process whose prompt carries the keyword ultracode, so the turn runs as an orchestrated workflow over a self-contained review packet on {model}/{effort} (confirm the workflow actually opened — a disabled keyword trigger yields an ordinary session), and require every finding to carry one of {severities}"
156
+ output = "review-v1"
157
+ perspectives = ["orchestration"]
158
+ trials = 1
159
+ order = "fixed"
160
+ swap_augmentation = false
161
+ aggregation = "union"
162
+ severity_emits = ["blocker", "high", "medium", "low", "info"]
163
+ severity_map = { blocker = "blocker", high = "high", medium = "medium", low = "low", info = "info" }
164
+
165
+ [hosts.codex]
166
+ # The model family this host speaks for. A review binding names a provider, and
167
+ # the reverse map must be unique so it resolves to exactly one validatable seat.
168
+ provider = "openai"
169
+ models = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]
170
+ # onto review seat used when a CROSS-family main (Claude) routes onto review to
171
+ # this (gpt/codex) family; must be an onto review-role registered (provider, model).
172
+ onto_review = { provider = "openai", model = "gpt-5.6-sol" }
173
+
174
+ [hosts.codex.tiers.frontier]
175
+ model = "gpt-5.6-sol"
176
+ effort = "max"
177
+
178
+ [hosts.codex.tiers.helm]
179
+ model = "gpt-5.6-sol"
180
+ effort = "xhigh"
181
+
182
+ [hosts.codex.tiers.workhorse]
183
+ model = "gpt-5.6-terra"
184
+ effort = "high"
185
+
186
+ [hosts.codex.tiers.sweep]
187
+ model = "gpt-5.6-luna"
188
+ effort = "low"
189
+
190
+ [hosts.codex.agent_templates]
191
+ frontier = "${CODEX_HOME}/agents/frontier.toml"
192
+ workhorse = "${CODEX_HOME}/agents/workhorse.toml"
193
+ sweep = "${CODEX_HOME}/agents/sweep.toml"
194
+
195
+ # Model id -> the name the human-readable guides write. The guides' Environment Binding tables
196
+ # restate these bindings in prose that also carries policy the profile does not hold ("main Ultra
197
+ # requires explicit selection"), so those tables are not generatable — but the MODEL IDENTITY in
198
+ # them is this file's, and until this map existed the parity gate compared each table to display
199
+ # names written inside the gate. That made the gate a third author of the binding. With the map
200
+ # here, the gate compares two real surfaces and holds no copy of its own.
201
+ [model_display]
202
+ "claude-fable-5" = "Claude Fable 5"
203
+ "claude-opus-5" = "Claude Opus 5"
204
+ "claude-haiku-4-5" = "Claude Haiku 4.5"
205
+ "gpt-5.6-sol" = "GPT-5.6 Sol"
206
+ "gpt-5.6-terra" = "GPT-5.6 Terra"
207
+ "gpt-5.6-luna" = "GPT-5.6 Luna"
208
+
209
+ [hosts.claude]
210
+ provider = "anthropic"
211
+ models = ["claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5"]
212
+ # onto review seat used when a CROSS-family main (Codex) routes onto review to
213
+ # this (anthropic/claude) family; must be an onto review-role registered (provider, model).
214
+ onto_review = { provider = "anthropic", model = "claude-fable-5" }
215
+
216
+ [hosts.claude.tiers.frontier]
217
+ model = "claude-fable-5"
218
+ effort = "max"
219
+
220
+ [hosts.claude.tiers.helm]
221
+ model = "claude-opus-5"
222
+ effort = "xhigh"
223
+
224
+ [hosts.claude.tiers.workhorse]
225
+ model = "claude-opus-5"
226
+ effort = "medium"
227
+
228
+ [hosts.claude.tiers.sweep]
229
+ model = "claude-haiku-4-5"
230
+ effort = "low"
231
+
232
+ [presets.balanced]
233
+ label = "Balanced"
234
+ description = "HELM default for everyday work with native multi-perspective review."
235
+ mode = "builder"
236
+ main_tier = "helm"
237
+ frontier_effort = "max"
238
+ delegation = true
239
+ codex_execution_policy = "bypass"
240
+ claude_permission_mode = "bypassPermissions"
241
+
242
+ [presets.deep-review]
243
+ label = "Deep review"
244
+ description = "HELM with hybrid onto, native, and Ultracode review at deep FRONTIER effort."
245
+ mode = "builder"
246
+ main_tier = "helm"
247
+ frontier_effort = { codex = "ultra", claude = "max" }
248
+ delegation = true
249
+ codex_execution_policy = "bypass"
250
+ claude_permission_mode = "bypassPermissions"
251
+
252
+ [presets.fast-batch]
253
+ label = "Fast batch"
254
+ description = "WORKHORSE default for high-volume, cost-conscious execution."
255
+ mode = "builder"
256
+ main_tier = "workhorse"
257
+ frontier_effort = "max"
258
+ delegation = true
259
+ codex_execution_policy = "bypass"
260
+ claude_permission_mode = "bypassPermissions"
261
+
262
+ [presets.solo]
263
+ label = "Solo"
264
+ description = "Single-model session: delegation off (no tier fan-out; the standing spawn policy is lifted), review via the host's own command only."
265
+ mode = "builder"
266
+ main_tier = "helm"
267
+ frontier_effort = "max"
268
+ review_setup = "none"
269
+ delegation = false
270
+ codex_execution_policy = "bypass"
271
+ claude_permission_mode = "bypassPermissions"
272
+
273
+ [presets.vanilla]
274
+ label = "Vanilla"
275
+ description = "Plain CLI session: no launch contract, no tier bindings, standard permissions — exactly what the bare backend gives you."
276
+ mode = "software-engineer"
277
+ main_tier = "helm"
278
+ frontier_effort = "max"
279
+ review_setup = "none"
280
+ delegation = false
281
+ codex_execution_policy = "standard"
282
+ claude_permission_mode = "standard"
283
+
284
+ [presets.session-distill]
285
+ # Author-only: the workflow edits the agent-bios corpus and needs a checkout, so
286
+ # its mission names repo paths a packaged user does not have. Declaring it lets
287
+ # gates/check-package.sh judge the mission text. NOTE: this declaration is read by
288
+ # the gate, not yet by the launcher — the preset is still offered on a packaged
289
+ # install. Closing that route belongs with the package-based distill work.
290
+ audience = "author"
291
+ label = "Session distill"
292
+ description = "Dedicated session-distill run: mine recent sessions, verify, place, and apply per the session-distill workflow. Requires an agent-bios checkout."
293
+ mode = "distill"
294
+ main_tier = "helm"
295
+ frontier_effort = "max"
296
+ delegation = true
297
+ codex_execution_policy = "bypass"
298
+ claude_permission_mode = "bypassPermissions"
299
+ mission = "This is a session-distill initiative session, gated on a trigger. When the user enters the trigger command '{trigger}': read claude/guides/session-distill-workflow.md in the agent-bios checkout, then the initiative state in design/session-distill/ledger.json in that same checkout, and run the next mining window per that guide. Until the trigger arrives, respond normally and do not start the workflow. Every promotion passes through explicit user approval; nothing is applied to the corpus without it."
300
+ trigger = "distill!"
301
+
302
+
303
+ # ── review, per host (DESIGN.md Corrections C7) ─────────────────────────────
304
+ # A binding names a provider, which maps to one host, so cross-family review has to
305
+ # be stated per launch host. Both answers live here rather than being inferred, and
306
+ # the reviewer's tier now tracks the preset's intent instead of exposing the whole
307
+ # opposite tier table for the reader to pick from.
308
+
309
+ [presets.balanced.review.hosts.claude.base]
310
+ provider = "openai"
311
+ tier = "helm"
312
+
313
+ [presets.balanced.review.hosts.codex.base]
314
+ provider = "anthropic"
315
+ tier = "helm"
316
+
317
+ [presets.deep-review.review.hosts.claude.base]
318
+ provider = "openai"
319
+ tier = "frontier"
320
+
321
+ [presets.deep-review.review.hosts.claude.methods."onto"]
322
+ provider = "openai"
323
+ tier = "frontier"
324
+
325
+ [presets.deep-review.review.hosts.claude.methods."ultracode-for-codex"]
326
+ provider = "openai"
327
+ tier = "frontier"
328
+
329
+ [presets.deep-review.review.hosts.codex.base]
330
+ provider = "anthropic"
331
+ tier = "frontier"
332
+
333
+ [presets.deep-review.review.hosts.codex.methods."onto"]
334
+ provider = "anthropic"
335
+ tier = "frontier"
336
+
337
+ [presets.deep-review.review.hosts.codex.methods."ultracode"]
338
+ provider = "anthropic"
339
+ tier = "frontier"
340
+
341
+ [presets.fast-batch.review.hosts.claude.base]
342
+ provider = "openai"
343
+ tier = "workhorse"
344
+
345
+ [presets.fast-batch.review.hosts.codex.base]
346
+ provider = "anthropic"
347
+ tier = "workhorse"
348
+
349
+ [presets.session-distill.review.hosts.claude.base]
350
+ provider = "openai"
351
+ tier = "frontier"
352
+
353
+ [presets.session-distill.review.hosts.claude.methods."onto"]
354
+ provider = "openai"
355
+ tier = "frontier"
356
+
357
+ [presets.session-distill.review.hosts.claude.methods."ultracode-for-codex"]
358
+ provider = "openai"
359
+ tier = "frontier"
360
+
361
+ [presets.session-distill.review.hosts.codex.base]
362
+ provider = "anthropic"
363
+ tier = "frontier"
364
+
365
+ [presets.session-distill.review.hosts.codex.methods."onto"]
366
+ provider = "anthropic"
367
+ tier = "frontier"
368
+
369
+ [presets.session-distill.review.hosts.codex.methods."ultracode"]
370
+ provider = "anthropic"
371
+ tier = "frontier"
372
+
373
+ # Nudge baseline lives in ~/.local/share/agent-bios/session-distill-state.json,
374
+ # written by session-distill/update-state.py at window close.
375
+ [session_distill]
376
+ nudge_after = 250
@@ -11,7 +11,7 @@ cd "$(dirname "$0")/.." || exit 2
11
11
  python3 - "$@" <<'PY'
12
12
  import pathlib, re, sys, tomllib
13
13
 
14
- config = pathlib.Path("config/agent-launch.toml")
14
+ config = pathlib.Path("launch/agent-launch.toml")
15
15
  hosts = tomllib.loads(config.read_text())["hosts"]
16
16
 
17
17
  # host -> the guide that owns prompting guidance for that host's model family
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # Provision the managed virtualenv that backs the agent-launch Textual preflight.
3
3
  #
4
- # The interactive launcher re-execs into this venv (scripts/agent-launch.py ->
4
+ # The interactive launcher re-execs into this venv (launch/agent-launch.py ->
5
5
  # maybe_reexec_into_venv). It is an enhancement, not a hard dependency: when the
6
6
  # venv is missing or broken, the launcher falls back to numbered prompts, and
7
7
  # every direct / non-TTY / --no-tui path keeps running under the system
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env python3
2
2
  """Learning record gate + validator (collection loop, Phase 0).
3
3
 
4
- config/learning.schema.json is the SSOT for the learning record
4
+ learn/learning.schema.json is the SSOT for the learning record
5
5
  (design/collection-loop/DESIGN.md); the dashboard mirrors only minimal
6
6
  validation. Full client-side validity = JSON Schema conformance PLUS domain
7
- membership in config/domains.json domains ∪ 'unclassified' — membership is
7
+ membership in compose/domains.json domains ∪ 'unclassified' — membership is
8
8
  checked here, not frozen in the schema, so vocabulary evolution never needs
9
9
  a schema_version bump.
10
10
 
@@ -35,8 +35,8 @@ except ImportError:
35
35
  "(pip install jsonschema; verified 4.26.0 in DEPENDENCIES.md)")
36
36
 
37
37
  REPO = pathlib.Path(__file__).resolve().parent.parent
38
- SCHEMA = REPO / "config" / "learning.schema.json"
39
- DOMAINS = REPO / "config" / "domains.json"
38
+ SCHEMA = REPO / "learn" / "learning.schema.json"
39
+ DOMAINS = REPO / "compose" / "domains.json"
40
40
  FIXTURES = REPO / "design" / "collection-loop" / "fixtures"
41
41
 
42
42
  # broken fixture -> field its single defect lives in; the reported errors
@@ -60,7 +60,7 @@ def load_json(path):
60
60
 
61
61
 
62
62
  def valid_domain_values(path=DOMAINS):
63
- """The set a record's `domain` may take, drawn from config/domains.json.
63
+ """The set a record's `domain` may take, drawn from compose/domains.json.
64
64
 
65
65
  Ledger-compatible (design/session-distill/ledger.json): the ledger's
66
66
  `domain` field uses BOTH domain keys (builder-base, …) for
@@ -73,7 +73,7 @@ def valid_domain_values(path=DOMAINS):
73
73
  manifest = load_json(path)
74
74
  values = set(manifest["domains"]) | set(manifest["tiers"]) | {"unclassified"}
75
75
  if len(values) <= 1:
76
- sys.exit("FAIL: config/domains.json registers no domains/tiers (vacuous gate)")
76
+ sys.exit("FAIL: compose/domains.json registers no domains/tiers (vacuous gate)")
77
77
  return values
78
78
 
79
79
 
@@ -88,7 +88,7 @@ def validate_record(record, validator, domain_values):
88
88
  if isinstance(dom, str) and dom not in domain_values:
89
89
  errors.append(
90
90
  f"domain: {dom!r} is not a registered domain key, tier name, "
91
- f"or 'unclassified' (config/domains.json)")
91
+ f"or 'unclassified' (compose/domains.json)")
92
92
  return errors
93
93
 
94
94
 
@@ -7,8 +7,8 @@ optional criteria / classification / proposed_domain / context) plus its session
7
7
  `--host`; THIS script owns every deterministic value and side effect:
8
8
 
9
9
  * mints learning_id (a lowercase UUID) + created (ISO-8601) + schema_version;
10
- * validates the full record against config/learning.schema.json — the single
11
- validation source, reused from scripts/check-learning.py (no second schema);
10
+ * validates the full record against learn/learning.schema.json — the single
11
+ validation source, reused from learn/check-learning.py (no second schema);
12
12
  * logs the JSON record to <home>/personal/learnings.jsonl — the durable,
13
13
  append-only upload source that the Phase 2 watermark drain re-sends from;
14
14
  * writes the lesson prose where THIS host loads it next session, and wires it:
@@ -16,7 +16,7 @@ optional criteria / classification / proposed_domain / context) plus its session
16
16
  `@personal/learnings.md` import once in the entry CLAUDE.md;
17
17
  - codex: appends into a preserved `agent-bios:personal-learnings` region
18
18
  of <home>/AGENTS.md (Codex has no @import; AGENTS.md is always loaded).
19
- The region lives OUTSIDE the central markers so scripts/assemble.py —
19
+ The region lives OUTSIDE the central markers so compose/assemble.py —
20
20
  which only rewrites the central region — preserves it across re-assembly.
21
21
 
22
22
  It REFUSES any script-owned field in the payload (deterministic values are never
@@ -61,7 +61,7 @@ HOSTS = {
61
61
  CLAUDE_IMPORT_LINE = "@personal/learnings.md"
62
62
  CLAUDE_CENTRAL_IMPORT = "@central/bundle.md"
63
63
 
64
- # Codex AGENTS.md markers. The central pair is owned by scripts/assemble.py
64
+ # Codex AGENTS.md markers. The central pair is owned by compose/assemble.py
65
65
  # (kept in sync here); the personal-learnings pair is this tool's own region,
66
66
  # placed outside the central pair so re-assembly preserves it.
67
67
  CENTRAL_START = "<!-- agent-bios:central:start -->"
@@ -72,7 +72,7 @@ PERSONAL_END = "<!-- agent-bios:personal-learnings:end -->"
72
72
  CLAUDE_LEARNINGS_HEADER = """# Personal learnings
73
73
 
74
74
  <!-- Automation-owned: written by the session learning flow (`learn!`,
75
- scripts/collect-learning.py). Do NOT hand-edit — promote→migrate clears
75
+ learn/collect-learning.py). Do NOT hand-edit — promote→migrate clears
76
76
  applied items by learning_id when the org redistributes them. Your own
77
77
  personal rules belong in the entry CLAUDE.md '## Personal' section, never
78
78
  here. This file is pulled into context by the entry file's
@@ -81,15 +81,15 @@ CLAUDE_LEARNINGS_HEADER = """# Personal learnings
81
81
 
82
82
  CODEX_REGION_HEADER = """## Personal learnings
83
83
  <!-- Automation-owned: written by the session learning flow (`learn!`,
84
- scripts/collect-learning.py). Codex loads this via AGENTS.md (no @import).
84
+ learn/collect-learning.py). Codex loads this via AGENTS.md (no @import).
85
85
  Do NOT hand-edit — promote→migrate clears applied items by learning_id.
86
86
  Kept outside the agent-bios central markers so re-assembly preserves it. -->
87
87
  """
88
88
 
89
89
 
90
90
  def load_checker():
91
- """Reuse scripts/check-learning.py as the single validation source."""
92
- path = REPO / "scripts" / "check-learning.py"
91
+ """Reuse learn/check-learning.py as the single validation source."""
92
+ path = REPO / "learn" / "check-learning.py"
93
93
  spec = importlib.util.spec_from_file_location("check_learning", path)
94
94
  module = importlib.util.module_from_spec(spec)
95
95
  spec.loader.exec_module(module)
@@ -97,9 +97,9 @@ def load_checker():
97
97
 
98
98
 
99
99
  def load_redactor():
100
- """Reuse scripts/redact.py as the single secret-redaction floor (loaded by
100
+ """Reuse learn/redact.py as the single secret-redaction floor (loaded by
101
101
  path so it works from the npm bin regardless of cwd, like load_checker)."""
102
- path = REPO / "scripts" / "redact.py"
102
+ path = REPO / "learn" / "redact.py"
103
103
  spec = importlib.util.spec_from_file_location("redact", path)
104
104
  module = importlib.util.module_from_spec(spec)
105
105
  spec.loader.exec_module(module)
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "urn:agent-bios:schema:learning:v1",
4
4
  "title": "Learning record",
5
- "description": "Per-session learning artifact produced by the session learning flow (trigger `learn!`; design/collection-loop/DESIGN.md). This file is the SSOT for the upload payload of POST /api/ingest/learnings and for the curator export; the dashboard mirrors only minimal validation from it. Field names stay ledger-compatible (design/session-distill/ledger.json). Identity is NEVER carried in the payload — the server derives it from X-Hook-Token. `domain` membership (config/domains.json domains ∪ tier names ∪ 'unclassified') is enforced by scripts/check-learning.py, not frozen here, so vocabulary evolution never requires a schema_version bump.",
5
+ "description": "Per-session learning artifact produced by the session learning flow (trigger `learn!`; design/collection-loop/DESIGN.md). This file is the SSOT for the upload payload of POST /api/ingest/learnings and for the curator export; the dashboard mirrors only minimal validation from it. Field names stay ledger-compatible (design/session-distill/ledger.json). Identity is NEVER carried in the payload — the server derives it from X-Hook-Token. `domain` membership (compose/domains.json domains ∪ tier names ∪ 'unclassified') is enforced by learn/check-learning.py, not frozen here, so vocabulary evolution never requires a schema_version bump.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": [
@@ -21,7 +21,7 @@
21
21
  "learning_id": {
22
22
  "type": "string",
23
23
  "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
24
- "description": "Client-generated UUID for this learning, lowercase canonical form (tooling must lowercase, e.g. macOS uuidgen emits uppercase). Owned by scripts/collect-learning.py, never hand-authored. Idempotency key for the pending-learnings.jsonl bounded retry in Phase 2. Distinct from the ledger's `id` (a curator-assigned short id like 'S3-07'); this is the light-flow record's own key.",
24
+ "description": "Client-generated UUID for this learning, lowercase canonical form (tooling must lowercase, e.g. macOS uuidgen emits uppercase). Owned by learn/collect-learning.py, never hand-authored. Idempotency key for the pending-learnings.jsonl bounded retry in Phase 2. Distinct from the ledger's `id` (a curator-assigned short id like 'S3-07'); this is the light-flow record's own key.",
25
25
  "$comment": "Ledger entries use `id`; the light-flow record's own key is `learning_id` (the artifact is a learning — LEXICON.md — so it is not `distill_id`, which would collide with the heavy session-distill pipeline)."
26
26
  },
27
27
  "lesson": {
@@ -33,7 +33,7 @@
33
33
  "domain": {
34
34
  "type": "string",
35
35
  "pattern": "^[a-z][a-z0-9-]*$",
36
- "description": "Curation join key, ledger-compatible: a D6 domain key from config/domains.json for a domain-specific lesson, OR a tier name (core, infra, …) for a cross-cutting lesson (4 real ledger entries carry core/infra), OR 'unclassified' (refinement B — users are never blocked at capture time; the curator assigns later; kept distinct from 'core', which asserts a genuinely cross-cutting lesson). Membership is validated by scripts/check-learning.py against config/domains.json (single source), not frozen in this pattern. When the model proposes a NOT-yet-registered domain, this stays 'unclassified' and the name goes in `proposed_domain`."
36
+ "description": "Curation join key, ledger-compatible: a D6 domain key from compose/domains.json for a domain-specific lesson, OR a tier name (core, infra, …) for a cross-cutting lesson (4 real ledger entries carry core/infra), OR 'unclassified' (refinement B — users are never blocked at capture time; the curator assigns later; kept distinct from 'core', which asserts a genuinely cross-cutting lesson). Membership is validated by learn/check-learning.py against compose/domains.json (single source), not frozen in this pattern. When the model proposes a NOT-yet-registered domain, this stays 'unclassified' and the name goes in `proposed_domain`."
37
37
  },
38
38
  "created": {
39
39
  "type": "string",