agent-bios 0.9.8 → 0.10.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.
- package/DEPENDENCIES.md +19 -19
- package/README.md +43 -12
- package/claude/CLAUDE.md +5 -41
- package/claude/guides/claude-prompting.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +22 -4
- package/claude/guides/coding-staged-workflow.md +49 -15
- package/claude/guides/concept-economy.md +187 -0
- package/claude/guides/documentation-hygiene.md +112 -0
- package/claude/guides/gpt-prompting.md +1 -1
- package/claude/guides/learning-flow.md +5 -5
- package/claude/guides/llm-capability-boundary.md +6 -1
- package/claude/guides/review-request.md +9 -7
- package/claude/guides/session-distill-workflow.md +19 -9
- package/claude/guides/tooling-gotchas.md +26 -0
- package/claude/guides/verification-discipline.md +166 -0
- package/claude/hooks/tooling-gotchas-hook.py +329 -12
- package/codex/AGENTS.md +5 -41
- package/codex/guides/claude-prompting.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +22 -4
- package/codex/guides/coding-staged-workflow.md +49 -15
- package/codex/guides/concept-economy.md +187 -0
- package/codex/guides/documentation-hygiene.md +112 -0
- package/codex/guides/gpt-prompting.md +1 -1
- package/codex/guides/learning-flow.md +5 -5
- package/codex/guides/llm-capability-boundary.md +6 -1
- package/codex/guides/review-request.md +9 -7
- package/codex/guides/session-distill-workflow.md +19 -9
- package/codex/guides/tooling-gotchas.md +26 -0
- package/codex/guides/verification-discipline.md +166 -0
- package/{scripts → compose}/assemble.py +194 -17
- package/{scripts → compose}/canary.sh +14 -5
- package/compose/check-domains.py +1178 -0
- package/{config → compose}/domains.json +11 -44
- package/{scripts → compose}/pkgid.py +8 -1
- package/compose/prune-backups.py +204 -0
- package/{scripts → compose}/register-hooks.py +3 -3
- package/install.sh +1233 -0
- package/launch/agent-launch.py +5294 -0
- package/launch/agent-launch.toml +376 -0
- package/{scripts → launch}/check-prompting-targets.sh +1 -1
- package/{scripts → launch}/provision-venv.sh +1 -1
- package/{scripts → learn}/check-learning.py +7 -7
- package/{scripts → learn}/collect-learning.py +10 -10
- package/{config → learn}/learning.schema.json +3 -3
- package/{scripts → learn}/migrate-learnings.py +95 -54
- package/{scripts → learn}/redact.py +4 -4
- package/package.json +32 -27
- package/provenance.json +1 -0
- package/wrappers/claude-run.sh +162 -0
- package/{scripts → wrappers}/codex-run.sh +62 -6
- package/config/agent-launch.toml +0 -143
- package/scripts/agent-launch.py +0 -2350
- package/scripts/check-domains.py +0 -296
- package/scripts/check-parity.sh +0 -2003
- package/scripts/install.sh +0 -819
- /package/{shell → launch}/agent-launch.zsh +0 -0
- /package/{config → learn}/promotions.json +0 -0
- /package/{scripts/session-cost.py → session-cost.py} +0 -0
- /package/{scripts → wrappers}/codex-helm.sh +0 -0
|
@@ -204,7 +204,12 @@ Use it per field or operation, not as a blanket replacement for LLM judgment.
|
|
|
204
204
|
Use this procedure when designing a new LLM-assisted artifact or revising an
|
|
205
205
|
existing one.
|
|
206
206
|
|
|
207
|
-
1. Identify the canonical artifact and downstream consumers.
|
|
207
|
+
1. Identify the canonical artifact and downstream consumers. When the consumer already
|
|
208
|
+
exists, read its **acceptance predicate**, not only its schema: the schema says which
|
|
209
|
+
fields may appear, and the predicate says which combinations are credited. A producer
|
|
210
|
+
designed against the schema alone can emit records that are valid and never
|
|
211
|
+
accepted — one record per event where the consumer judges one record per subject is
|
|
212
|
+
the common shape of this, and it survives every field-level check.
|
|
208
213
|
2. Split fields into semantic fields, deterministic fields, provenance fields,
|
|
209
214
|
and side-effect operations.
|
|
210
215
|
3. Assign each field or operation one primary authority.
|
|
@@ -26,10 +26,11 @@ verification_focus:
|
|
|
26
26
|
This guide is a scoped extension of the global Coding Guidelines. Use it when
|
|
27
27
|
composing what you ask a reviewer for — the request, the evidence bar, the
|
|
28
28
|
verdict shape. It does not cover when to review, how deep, or what counts as
|
|
29
|
-
material (the
|
|
30
|
-
which reviewer kind to route to
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
material (the severity ladder and review loop in `${CODEX_HOME:-$HOME/.codex}/guides/coding-staged-workflow.md` own that),
|
|
30
|
+
which reviewer kind to route to — the convergence heuristic in
|
|
31
|
+
`${CODEX_HOME:-$HOME/.codex}/guides/verification-discipline.md` owns that. Phrasing a prompt for a specific model
|
|
32
|
+
family is out of scope here; where that guidance ships, the rule that needs it
|
|
33
|
+
points at it.
|
|
33
34
|
|
|
34
35
|
The rules below are derived from ~330 real multi-lens review sessions run in
|
|
35
36
|
this environment. That corpus is one model family in practice, so nothing here
|
|
@@ -80,8 +81,9 @@ So state the floor in the request: **do not report a finding you would rate low.
|
|
|
80
81
|
It will be discarded; spend the effort on a medium-or-above finding instead.**
|
|
81
82
|
This is not a quality bar on the reviewer, it is a cost decision — the corpus
|
|
82
83
|
shows the discard happens regardless, so the only question is whether you pay to
|
|
83
|
-
generate it first. (What counts as each severity is the
|
|
84
|
-
|
|
84
|
+
generate it first. (What counts as each severity is the ladder in
|
|
85
|
+
`${CODEX_HOME:-$HOME/.codex}/guides/coding-staged-workflow.md`, not
|
|
86
|
+
this guide's.)
|
|
85
87
|
|
|
86
88
|
## Forbid carry-forward findings
|
|
87
89
|
|
|
@@ -128,7 +130,7 @@ nicely: the submit schema refuses output without them. The result is a corpus
|
|
|
128
130
|
where reviewers are right about existence (0.3% of issues end unresolved after
|
|
129
131
|
argument) and where "found nothing" is a verified statement rather than silence.
|
|
130
132
|
|
|
131
|
-
This is the general
|
|
133
|
+
This is the general capability-boundary rule applied to review:
|
|
132
134
|
when output must have a property, make it unavailable without it. If your review
|
|
133
135
|
route has a schema, put the anchor there. If it does not, the demand belongs in
|
|
134
136
|
the request — but expect the weaker result that a request-only rule gives you.
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
guide_id: session-distill-workflow
|
|
3
3
|
language: en
|
|
4
4
|
status: active
|
|
5
|
+
audience: author
|
|
5
6
|
use_when:
|
|
6
7
|
- a session was launched with the Session distill preset (mission-injected)
|
|
7
8
|
- the launcher nudge says enough sessions accumulated for a mining window
|
|
8
9
|
- mining local Claude/Codex sessions for learnings absent from the corpus
|
|
9
10
|
- promoting, incubating, or retiring items in the session-distill ledger
|
|
10
11
|
core_rules:
|
|
11
|
-
- the ledger
|
|
12
|
+
- the ledger is the SSOT for state; read it before touching the pipeline
|
|
12
13
|
- placement follows PLACEMENT-FRAMEWORK.md, never ad-hoc judgment
|
|
13
14
|
- every promotion passes an explicit user-approval gate
|
|
14
15
|
- global growth per round is hard-capped (~500 tokens) by a measured gate
|
|
@@ -17,19 +18,28 @@ core_rules:
|
|
|
17
18
|
|
|
18
19
|
# Session-Distill Workflow
|
|
19
20
|
|
|
21
|
+
**Requires an agent-bios checkout.** This runbook edits the corpus itself, so it
|
|
22
|
+
names repo paths and runs repo scripts. On a packaged install those do not exist:
|
|
23
|
+
say so and stop rather than following steps you cannot execute.
|
|
24
|
+
|
|
20
25
|
Runbook for a session-distill run: mine recent main-context sessions,
|
|
21
26
|
verify candidates, place them through the framework, and apply with the user.
|
|
22
27
|
Everything durable lives in the agent-bios repo.
|
|
23
28
|
|
|
24
29
|
## Read first (SSOT)
|
|
25
30
|
|
|
26
|
-
1. `design/session-distill/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
1. `design/session-distill/ledger.json` — the initiative's state. Every item
|
|
32
|
+
carries its status (placed / incubating / incubating-G / absorbed /
|
|
33
|
+
adopted-no-text), strength, and provenance, so what is open, what was
|
|
34
|
+
promoted, and what is still incubating are all queries against this file.
|
|
35
|
+
Read state here and nowhere else: a count or a status written into prose is
|
|
36
|
+
correct on the day it is written and silently wrong afterwards.
|
|
37
|
+
2. `design/session-distill/versions.json` — which closed mining window maps to
|
|
38
|
+
which commit, and therefore what a rollback restores.
|
|
39
|
+
3. `design/session-distill/PLACEMENT-FRAMEWORK.md` — the placement authority
|
|
40
|
+
(typology A–G, layers, admission bars, lifecycle).
|
|
31
41
|
|
|
32
|
-
## Stage 1 — Mine (pipeline in `
|
|
42
|
+
## Stage 1 — Mine (pipeline in `session-distill/`)
|
|
33
43
|
|
|
34
44
|
Run in order; each stage reads the previous stage's `out/`:
|
|
35
45
|
|
|
@@ -73,7 +83,7 @@ Run in order; each stage reads the previous stage's `out/`:
|
|
|
73
83
|
stdout/stderr channel contracts) → codex/ + ko/ mirrors.
|
|
74
84
|
- Verify per layer, not just by diff: enforcement/gate fixture tests
|
|
75
85
|
(non-vacuous — known-bad must fire), hook trigger positive/negative sets,
|
|
76
|
-
`
|
|
86
|
+
`gates/check-parity.sh` exit 0 unpiped, prompting-target gate, then
|
|
77
87
|
`agent-bios install` to activate and re-verify.
|
|
78
88
|
|
|
79
89
|
## Stage 4 — G-pass (principles, not directives)
|
|
@@ -95,6 +105,6 @@ Run in order; each stage reads the previous stage's `out/`:
|
|
|
95
105
|
3. Register the corpus version: append {version = window end, commit = the
|
|
96
106
|
corpus-close commit} to `design/session-distill/versions.json` — this is
|
|
97
107
|
what the launcher's Versions & rollback screen offers — then run
|
|
98
|
-
`python3
|
|
108
|
+
`python3 session-distill/update-state.py --window-end <date>`
|
|
99
109
|
(nudge baseline) and `corpus-state.py project` (launcher status panel).
|
|
100
110
|
4. Merge the branch, push, and confirm deployed state (`agent-bios verify`).
|
|
@@ -62,6 +62,15 @@ depends on it, pin it explicitly instead of trusting the environment.
|
|
|
62
62
|
early-exit consumers (`cmd | head -1` → SIGPIPE 141), so it is a per-command
|
|
63
63
|
choice, not a global default. Does not apply when the final stage IS the
|
|
64
64
|
assertion (`cmd | grep -q pattern`).
|
|
65
|
+
- **Passthrough arguments in a CLI you author**: an option meant to carry
|
|
66
|
+
another command's own flags cannot use a greedy-but-dash-stopping arity —
|
|
67
|
+
Python's `nargs="+"` ends at the first token starting with `-`, so the
|
|
68
|
+
wrapped command's `--model x` lands on the next positional and the error
|
|
69
|
+
names a parameter the caller never mentioned. Use the parser's
|
|
70
|
+
everything-after form (`argparse.REMAINDER`). A bare `--` separator is a
|
|
71
|
+
second, separate trap: argparse consumes it as its own positional marker
|
|
72
|
+
before the remainder sees it, so the form every caller reaches for first is
|
|
73
|
+
the one that breaks — normalize it out of `argv` before parsing.
|
|
65
74
|
- **Reserved parameter names**: assigning to reserved shell names (`UID`,
|
|
66
75
|
`EUID`, `GID`, `PPID`) can invoke the bound system behavior instead of
|
|
67
76
|
storing a value — silently changing process credentials mid-script. Use
|
|
@@ -99,12 +108,29 @@ depends on it, pin it explicitly instead of trusting the environment.
|
|
|
99
108
|
|
|
100
109
|
## Git operations
|
|
101
110
|
|
|
111
|
+
- **A stale local base inflates the range**: before reasoning about what a branch
|
|
112
|
+
contains or opening a PR, `git fetch`, then ask against the remote rather than the
|
|
113
|
+
local tracking ref — `git log origin/<base>..HEAD` for which commits are yours, and
|
|
114
|
+
the merge-base form below for the diff. On a shared repo the local base lags until
|
|
115
|
+
you pull, so `<base>..HEAD` quietly folds in work that already merged. When a range
|
|
116
|
+
looks surprisingly large, suspect the base before the branch.
|
|
117
|
+
- **"Mergeable" is measured against the base, not against siblings**: the platform
|
|
118
|
+
flag says each PR merges into the base, and two PRs can both be clean while
|
|
119
|
+
conflicting with each other. Before choosing a merge order, diff their changed-file
|
|
120
|
+
sets and simulate the sequence.
|
|
102
121
|
- **Two-dot diff semantics**: `git diff A..B` is a direct snapshot
|
|
103
122
|
comparison — unlike `git log A..B` it excludes nothing, so a lagging
|
|
104
123
|
merge-base injects unrelated upstream changes into the diff. For PR/review
|
|
105
124
|
diffs use `git diff origin/base...HEAD` (merge-base form); suspect this
|
|
106
125
|
mechanism first when a diff looks too large or shows deletions in untouched
|
|
107
126
|
files.
|
|
127
|
+
- **Reverting a path is not undoing your edit**: `git checkout <path>` and
|
|
128
|
+
`git restore <path>` discard *every* uncommitted change in that file. Used
|
|
129
|
+
to remove a planted probe it also removes whatever else was in flight there,
|
|
130
|
+
and the loss is silent. Check `git diff <path>` first, or plant in a copy and
|
|
131
|
+
restore from that. The same asymmetry makes the restore step fragile: if the
|
|
132
|
+
probe can time out or abort, the restore must not be the next command in the
|
|
133
|
+
same invocation — put it where a failure cannot skip it.
|
|
108
134
|
- **Dirty-worktree pulls**: before pulling into a worktree with
|
|
109
135
|
staged/unstaged/untracked changes, fetch first and compare incoming paths
|
|
110
136
|
against every dirty path; on overlap or a non-fast-forward, stop and clear
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
guide_id: verification-discipline
|
|
3
|
+
language: en
|
|
4
|
+
status: active
|
|
5
|
+
use_when:
|
|
6
|
+
- deciding how much verification a change deserves, before spending on a slow or expensive run
|
|
7
|
+
- choosing what to run for a domain — code, ontology, config/data, spreadsheets, docs, a release
|
|
8
|
+
- building the case space for a check, or deciding what its expected answer should be
|
|
9
|
+
- a check came back green, empty, or fast, and you are about to believe it
|
|
10
|
+
- running independent or adversarial review, and judging what its agreement is worth
|
|
11
|
+
core_rules:
|
|
12
|
+
- a check is only evidence if it could have failed — assert a non-empty subject before any "no bad X" claim
|
|
13
|
+
- enumerate the case space from the artifact that defines it, and record real output instead of typing an expectation
|
|
14
|
+
- proportion depth to cost, risk, and information gain; a single-user tool does not warrant production assurance
|
|
15
|
+
- same-kind reviewers share blind spots, so their shared "clean" is an absence of objection, not verification
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Verification Discipline
|
|
19
|
+
|
|
20
|
+
A scoped extension of the global Verification Discipline section. Its subject is not "did you
|
|
21
|
+
test it" but the harder question underneath: **could this check have failed?** Everything below
|
|
22
|
+
is a way of answering that before the result is believed rather than after it is quoted.
|
|
23
|
+
|
|
24
|
+
The global rules that stay always-loaded are the ones whose moment does not announce itself — you
|
|
25
|
+
believe you are finished, and that belief is the failure. This guide is what you open once you
|
|
26
|
+
know you are verifying.
|
|
27
|
+
|
|
28
|
+
## Proportion the depth before you spend
|
|
29
|
+
|
|
30
|
+
Verification has a cost and an information yield, and they are not correlated by default. Decide
|
|
31
|
+
the depth first:
|
|
32
|
+
|
|
33
|
+
- Diagnose in code before running anything expensive, and replay the changed deterministic logic
|
|
34
|
+
over persisted real artifacts rather than re-running the whole pipeline to observe it.
|
|
35
|
+
- Probe at N=1 with the inputs precondition-checked. A single well-chosen case that reaches the
|
|
36
|
+
real path outranks a hundred that stop short of it.
|
|
37
|
+
- Reserve the full design-review-plus-live-verification treatment for first-of-kind work and for
|
|
38
|
+
changes that move authority — who may decide, who may write, what is irreversible.
|
|
39
|
+
- Proportion assurance to the deployment context. A single-user tool operating on its owner's own
|
|
40
|
+
data does not warrant production-grade assurance, and treating it as if it did buys nothing
|
|
41
|
+
while delaying delivery. Prefer shipping.
|
|
42
|
+
|
|
43
|
+
The failure this prevents is not under-testing. It is spending the verification budget on the
|
|
44
|
+
cheap half of the risk and having nothing left for the part that could actually hurt.
|
|
45
|
+
|
|
46
|
+
## The static floor
|
|
47
|
+
|
|
48
|
+
Run the broad, cheap checks first and let them fail before anything slower starts: typecheck,
|
|
49
|
+
lint, build, format, schema and config validation, graph validation, workbook structure checks,
|
|
50
|
+
import boundaries, and security checks where they exist. These are a floor, not a verdict — they
|
|
51
|
+
prove the artifact is well-formed, never that it behaves.
|
|
52
|
+
|
|
53
|
+
## Verification Menus
|
|
54
|
+
|
|
55
|
+
Pick the narrowest reliable mix that proves the changed behavior, meaning, or contract. Inside the
|
|
56
|
+
mix, the unit to add is the narrowest reliable runtime or semantic test that proves it — narrowest
|
|
57
|
+
meaning the smallest test that would fail if the change were wrong, which is not the same as the
|
|
58
|
+
cheapest one to write.
|
|
59
|
+
|
|
60
|
+
- Code: a layered mix of unit tests, integration tests for E2E segments, targeted E2E for changed flows, and full E2E for release or high-risk changes.
|
|
61
|
+
- Ontology: static graph checks, concept economy gates, changed-path integration checks, and competency-question E2E checks.
|
|
62
|
+
- Config or data: real parsers, schema checks, fixture validation, and sample transformations.
|
|
63
|
+
- Spreadsheets: static workbook checks, fixture-based output checks, cross-sheet flow checks, visual/layout checks, and real Microsoft Excel engine recalculation for formula-dependent results.
|
|
64
|
+
- Docs: links, terminology, current behavior alignment, and references to isolated historical notes.
|
|
65
|
+
- Release or distribution: after publishing to multiple independently writable channels (signed manifest, object storage, release host, embedded updater), digest-verify every referenced object against the staging original per channel — publish success and upload order are not evidence — and run the real installer/updater through its default path.
|
|
66
|
+
- A/B or on/off measurements: before accepting a null result, verify the arms actually received different treatment in the mechanism under test — a shared default or unconditional upstream step can silently apply the treatment to both arms.
|
|
67
|
+
- Model-behavior guardrails: verify by changed behavior, not recitation — a staged battery from named-trigger cases through disguised, deconfounded, category-wide, and single-variable framings; a clean pass means "no known defect", so re-run the battery when the model changes.
|
|
68
|
+
- Branch/version test builds against real data: explicitly separate every state sink the app touches (files, DB, OS-level stores that ignore env overrides), confirm the launch path propagates the isolation to child processes, and back up live data before the first run — a mismatched schema that drops unknown fields on write is data loss, not a no-op.
|
|
69
|
+
- Irreversible capture switches: when activation itself has unreproducible cost (a capture window that cannot be replayed), prove the downstream consumption path against existing samples before enabling — reversibility of the code path alone is not enough.
|
|
70
|
+
|
|
71
|
+
## Deriving the case space
|
|
72
|
+
|
|
73
|
+
Which scenarios exist is semantic work: derive them from the diff, the user impact, the
|
|
74
|
+
concept impact, and the failure modes. Running them is not — tools and code execute the
|
|
75
|
+
cases and report the evidence. Keeping that split is what stops a suite from being a
|
|
76
|
+
record of what someone imagined.
|
|
77
|
+
|
|
78
|
+
A check has two authored halves, and they rot differently. The **verdict** — what the
|
|
79
|
+
answer should be — rots by encoding a belief that was wrong from the start. The
|
|
80
|
+
**space** — which cases exist — rots by staying still while the thing it covers grows.
|
|
81
|
+
Recording the verdict is common practice; deriving the space is the half usually left
|
|
82
|
+
hand-written, and a suite can have every expectation derived and still cover a set
|
|
83
|
+
someone typed once.
|
|
84
|
+
|
|
85
|
+
- Make the criterion falsifiable before you make it green. Prefer a signal that fails when
|
|
86
|
+
the mechanism is wrong — a negative or contrast control. Where no existing gate can judge
|
|
87
|
+
a criterion, build the executable judge or do not claim the criterion met: a criterion
|
|
88
|
+
nothing can fail is a description of the work, not a check on it.
|
|
89
|
+
- Record the verdict, do not type it. Run the real path and store what came back;
|
|
90
|
+
drift then shows as a diff instead of as a belief someone has to re-justify.
|
|
91
|
+
- Enumerate the space from the artifact that defines it — the config's entries, the
|
|
92
|
+
schema's fields, the router's routes, the installer's call sites. Adding one there
|
|
93
|
+
should widen coverage with no edit here.
|
|
94
|
+
- Derive the exemption rule too. If some cases legitimately have no answer, decide that
|
|
95
|
+
from a property the artifact carries, never from a list of names: the list is the
|
|
96
|
+
authored space coming back through a side door, and it absorbs the regression where
|
|
97
|
+
a case that should have an answer stops having one.
|
|
98
|
+
- Dedupe on the tuple that actually determines the outcome, and report how many
|
|
99
|
+
collapsed. A coverage count that hides its own truncation reads as more than it is.
|
|
100
|
+
- Split by cost, not by space. When the real path needs money, credentials, or a
|
|
101
|
+
network, run a cheap stand-in on every commit and the real one on demand — both from
|
|
102
|
+
the **same enumeration**, so the two can never disagree about which cases exist.
|
|
103
|
+
- Derivation moves authorship rather than removing it: the extractor and the invariants
|
|
104
|
+
are still written by hand. Give them a negative control, or the derived suite is just
|
|
105
|
+
a larger unfalsifiable one.
|
|
106
|
+
- Planting a violation to prove a control fires is a write into the working tree, and
|
|
107
|
+
the restore is not atomic with it: if the probe can time out, abort, or be
|
|
108
|
+
interrupted, a restore sitting after it never runs and the plant survives into a
|
|
109
|
+
commit. Plant in a copy where the shape allows it, and when it must be in place, snapshot
|
|
110
|
+
first and restore from the snapshot as its own step rather than trusting the probe to finish.
|
|
111
|
+
|
|
112
|
+
## When a green means nothing
|
|
113
|
+
|
|
114
|
+
A passing check and a check that never ran look identical from outside. These are the shapes that
|
|
115
|
+
produce a green with no evidence behind it:
|
|
116
|
+
|
|
117
|
+
- **The empty subject.** Any "no bad X" or "all X satisfy P" claim over an empty set is
|
|
118
|
+
vacuously true. Assert the entity-under-test set has cardinality greater than zero **before**
|
|
119
|
+
the claim, and make the gate itself refuse to report clean when it judged nothing.
|
|
120
|
+
- **The fixture that misses the guard.** For a test touching a branch you are adding or deleting,
|
|
121
|
+
confirm its inputs satisfy the live branch's entry guard. A copied fixture that fails the new
|
|
122
|
+
guard routes silently into the about-to-be-deleted dead branch and stays green after the real
|
|
123
|
+
behavior breaks.
|
|
124
|
+
- **The permissive fallback in the checker.** A `a || b` inside a gate absorbs a wrong assumption
|
|
125
|
+
and keeps passing. Checker code must assert the shape it expects and fail loud.
|
|
126
|
+
- **The suspiciously fast or empty run.** When a check goes green unexpectedly quickly, or reports
|
|
127
|
+
nothing at all, dump what it actually ran over before believing it. A harness that crashed early
|
|
128
|
+
and one that found nothing produce the same exit code.
|
|
129
|
+
- **The control that went quiet.** A negative control indexing a live list stops testing when that
|
|
130
|
+
list empties, and says nothing about it. New controls build their own subject; resolving an item
|
|
131
|
+
means re-reading the controls for ones that have gone silent.
|
|
132
|
+
|
|
133
|
+
The discipline that covers all five: after adding a check, revert the fix it guards and watch the
|
|
134
|
+
check fail. A control that survives a faithful revert was never testing the thing it names.
|
|
135
|
+
|
|
136
|
+
## Keeping E2E honest
|
|
137
|
+
|
|
138
|
+
E2E is where flakiness is mistaken for environment noise and then ignored. Keep it deterministic
|
|
139
|
+
with fixed data, resilient selectors, isolated external dependencies, and explicit waits rather
|
|
140
|
+
than sleeps. A flaky E2E is not a weaker test; it is a test whose result carries no information,
|
|
141
|
+
and a suite that people re-run until it passes has been switched off without anyone deciding to.
|
|
142
|
+
|
|
143
|
+
## Independent review, and what agreement is worth
|
|
144
|
+
|
|
145
|
+
For non-trivial designs and high-risk changes, run independent adversarial review across distinct
|
|
146
|
+
lenses — ideally on the design, before implementation, when a finding is still cheap to act on.
|
|
147
|
+
Then re-verify each finding against real code before acting on it: a reviewer reasons from what it
|
|
148
|
+
was shown, and what it was shown may be wrong.
|
|
149
|
+
|
|
150
|
+
Apply the **convergence heuristic by reviewer kind** — judge the result by reviewer kind, not by count:
|
|
151
|
+
|
|
152
|
+
- Same-kind convergence is high confidence but blind-spot-sharing. Two reviewers of the same kind
|
|
153
|
+
agreeing that something is clean is an absence of objection, not verification.
|
|
154
|
+
- Different-kind divergence is the expected signal, not a problem to resolve. Act on the union of
|
|
155
|
+
what they found rather than the intersection.
|
|
156
|
+
- An orchestrated workflow's self-reported all-green is never sufficient on its own. Re-run the
|
|
157
|
+
diff inspection and the verification suite yourself.
|
|
158
|
+
|
|
159
|
+
The cheapest way to buy real independence is a different provider; after that a different model;
|
|
160
|
+
after that strictly higher effort. A reviewer run at lower effort than the work it checks buys
|
|
161
|
+
nothing — cheaper is not another perspective.
|
|
162
|
+
|
|
163
|
+
## Reporting
|
|
164
|
+
|
|
165
|
+
Before calling the work done, state the checks that ran, their results, and any risk left
|
|
166
|
+
unverified. "Unverified" is a legitimate outcome and a useful one; silence about it is not.
|
|
@@ -51,12 +51,12 @@ ENTRY_SEED = f"""# CLAUDE.md
|
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
53
|
# Automation-owned personal learnings file, pulled in by PERSONAL_IMPORT_LINE.
|
|
54
|
-
# Kept in sync with
|
|
54
|
+
# Kept in sync with learn/collect-learning.py (the light-flow submit tool),
|
|
55
55
|
# which appends learnings here; seeding it keeps the import from dangling.
|
|
56
56
|
PERSONAL_LEARNINGS_HEADER = """# Personal learnings
|
|
57
57
|
|
|
58
58
|
<!-- Automation-owned: written by the session learning flow (`learn!`,
|
|
59
|
-
|
|
59
|
+
learn/collect-learning.py). Do NOT hand-edit — promote→migrate clears
|
|
60
60
|
applied items by learning_id when the org redistributes them. Your own
|
|
61
61
|
personal rules belong in the entry CLAUDE.md '## Personal' section, never
|
|
62
62
|
here. This file is pulled into context by the entry file's
|
|
@@ -139,9 +139,68 @@ def filtered_files(manifest, key, selection):
|
|
|
139
139
|
return sorted(n for n, e in manifest.get(key, {}).items() if kept(e, selection))
|
|
140
140
|
|
|
141
141
|
|
|
142
|
-
def
|
|
142
|
+
def author_only(path):
|
|
143
|
+
"""Does this file's own frontmatter say it is for the corpus author?
|
|
144
|
+
|
|
145
|
+
A guide declaring `audience: author` documents a step only the author can
|
|
146
|
+
perform, and names repository paths that exist in a checkout and nowhere
|
|
147
|
+
else. The tier says who NEEDS the subject; this says who can ACT on it, and
|
|
148
|
+
the two are independent — the distill workflow is `infra`, so tier alone
|
|
149
|
+
delivered it to every selection.
|
|
150
|
+
|
|
151
|
+
The declaration in the file is the authority, deliberately not restated in
|
|
152
|
+
`domains.json`: a second copy is one more thing that can disagree, and until
|
|
153
|
+
this read existed the label was consumed only by the gate that the label
|
|
154
|
+
exempts. A clone is a checkout, so `install.sh`'s non-packaged path still
|
|
155
|
+
deploys these; this filter is the packaged one.
|
|
156
|
+
|
|
157
|
+
THE single reader of that declaration. `gates/check-package.sh` imports this
|
|
158
|
+
rather than parsing the frontmatter again: the gate tolerates references that
|
|
159
|
+
only this withholding makes safe, so a second parser drifting from this one
|
|
160
|
+
would exempt a file the assembler still installs — precisely the defect the
|
|
161
|
+
pair exists to close. The direction is fixed: author-side may import shipped
|
|
162
|
+
code, never the reverse, because the payload cannot depend on `gates/`.
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
text = path.read_text(encoding="utf-8")
|
|
166
|
+
except OSError:
|
|
167
|
+
return False
|
|
168
|
+
if not text.startswith("---\n"):
|
|
169
|
+
return False
|
|
170
|
+
front = text.split("---\n", 2)[1]
|
|
171
|
+
return any(ln.split(":", 1)[1].strip() == "author"
|
|
172
|
+
for ln in front.splitlines() if ln.startswith("audience:"))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def copy_filtered(src_dir, names, dest, rewrite=None, dry=False, backup=None):
|
|
176
|
+
"""Write the selected files, and remove the ones we deployed and no longer select.
|
|
177
|
+
|
|
178
|
+
Writing alone leaves the destination describing a selection nobody chose: a machine that
|
|
179
|
+
took every domain and later narrowed to one kept the whole set on disk, so selection.json
|
|
180
|
+
stopped describing what was deployed. merge_settings already drops a deselected hook's
|
|
181
|
+
REGISTRATION through `owned_names`; this is the same reconciliation for the files.
|
|
182
|
+
|
|
183
|
+
Ownership is "the name exists in our source tree", which is what keeps a file the user put
|
|
184
|
+
in the same directory safe — it is not in `src_dir`, so it is never a candidate.
|
|
185
|
+
"""
|
|
143
186
|
if not dry:
|
|
144
187
|
dest.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
keep_names = set(names)
|
|
189
|
+
if dest.is_dir() and src_dir.is_dir():
|
|
190
|
+
ours = {p.name for p in src_dir.iterdir() if p.is_file()}
|
|
191
|
+
for path in sorted(dest.iterdir()):
|
|
192
|
+
if not path.is_file() or path.name in keep_names or path.name not in ours:
|
|
193
|
+
continue
|
|
194
|
+
print(f" {'[dry] ' if dry else ''}deselected, removed {path}")
|
|
195
|
+
if dry:
|
|
196
|
+
continue
|
|
197
|
+
# Backed up first, the way every other removal here is: these are copies of repo
|
|
198
|
+
# content, but a machine offline from the repo has no other way back.
|
|
199
|
+
if backup is not None:
|
|
200
|
+
kept = backup / "deselected" / str(path).lstrip("/")
|
|
201
|
+
kept.parent.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
shutil.copy2(path, kept)
|
|
203
|
+
path.unlink()
|
|
145
204
|
for n in names:
|
|
146
205
|
if dry:
|
|
147
206
|
print(f" [dry] copy {n} -> {dest}")
|
|
@@ -149,7 +208,30 @@ def copy_filtered(src_dir, names, dest, rewrite=None, dry=False):
|
|
|
149
208
|
body = (src_dir / n).read_text(encoding="utf-8")
|
|
150
209
|
if rewrite:
|
|
151
210
|
body = body.replace(*rewrite)
|
|
152
|
-
|
|
211
|
+
target = dest / n
|
|
212
|
+
# Replacing is as destructive as removing, and README promises a copy of the exact
|
|
213
|
+
# prior bytes under the state backup dir. install.sh's deploy_file kept that promise;
|
|
214
|
+
# the assembler is the default path now, so writing straight over a guide the user had
|
|
215
|
+
# edited broke it for every file it deploys. Identical content is not a replacement,
|
|
216
|
+
# and copying it would fill the backup dir on every no-op reinstall.
|
|
217
|
+
if backup is not None and target.is_file():
|
|
218
|
+
try:
|
|
219
|
+
changed = target.read_text(encoding="utf-8") != body
|
|
220
|
+
except (OSError, UnicodeDecodeError):
|
|
221
|
+
changed = True # unreadable is not "unchanged"; keep the bytes
|
|
222
|
+
if changed:
|
|
223
|
+
kept = backup / "replaced" / str(target).lstrip("/")
|
|
224
|
+
kept.parent.mkdir(parents=True, exist_ok=True)
|
|
225
|
+
shutil.copy2(target, kept)
|
|
226
|
+
target.write_text(body, encoding="utf-8")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def hook_command_matches(command, name):
|
|
230
|
+
"""A complete `/hooks/<name>` token: the command ends with it, or it is followed by an
|
|
231
|
+
argument. One matcher for the merge below AND the domain gate's settings leg — the gate
|
|
232
|
+
used a substring test, so `central/hooks/<name>.disabled` passed the gate while this
|
|
233
|
+
merge skipped it, and the install carried no hook under a green gate."""
|
|
234
|
+
return command.endswith("/hooks/" + name) or f"/hooks/{name} " in command + " "
|
|
153
235
|
|
|
154
236
|
|
|
155
237
|
def merge_settings(claude_dir, hook_names, template_path, dry=False, owned_names=None):
|
|
@@ -180,7 +262,8 @@ def merge_settings(claude_dir, hook_names, template_path, dry=False, owned_names
|
|
|
180
262
|
for event, entries in template.get("hooks", {}).items(): # re-add per selection
|
|
181
263
|
for en in entries:
|
|
182
264
|
cmds = [h.get("command", "") for h in en.get("hooks", [])]
|
|
183
|
-
owner = next((n for n in hook_names
|
|
265
|
+
owner = next((n for n in hook_names
|
|
266
|
+
if any(hook_command_matches(c, n) for c in cmds)), None)
|
|
184
267
|
if owner is None:
|
|
185
268
|
continue
|
|
186
269
|
clone = json.loads(json.dumps(en))
|
|
@@ -200,7 +283,7 @@ def merge_settings(claude_dir, hook_names, template_path, dry=False, owned_names
|
|
|
200
283
|
def seed_personal_learnings(claude_dir, dry=False):
|
|
201
284
|
"""Create the automation-owned personal learnings file when seeding the entry,
|
|
202
285
|
so the entry's @personal/learnings.md import always resolves before the first
|
|
203
|
-
learn!.
|
|
286
|
+
learn!. learn/collect-learning.py appends to it thereafter."""
|
|
204
287
|
md = claude_dir / "personal" / "learnings.md"
|
|
205
288
|
if md.exists():
|
|
206
289
|
return
|
|
@@ -211,7 +294,18 @@ def seed_personal_learnings(claude_dir, dry=False):
|
|
|
211
294
|
md.write_text(PERSONAL_LEARNINGS_HEADER, encoding="utf-8")
|
|
212
295
|
|
|
213
296
|
|
|
214
|
-
def seed_entry(claude_dir, legacy_monolith, dry=False):
|
|
297
|
+
def seed_entry(claude_dir, legacy_monolith, prior_deployed=(), dry=False):
|
|
298
|
+
"""Seed the entry, and never rewrite a file the user wrote.
|
|
299
|
+
|
|
300
|
+
Telling those apart used to be a byte-comparison against THIS commit's monolith, which
|
|
301
|
+
recognizes only a re-install of the same release. Anyone upgrading from an earlier one had
|
|
302
|
+
that release's monolith on disk — our file, not theirs — and it was reported as user-owned,
|
|
303
|
+
so the import line was never added and the corpus did not load until they edited it by hand.
|
|
304
|
+
|
|
305
|
+
`prior_deployed` is the previous install's manifest, which is the repo's existing answer to
|
|
306
|
+
"did we write this": deploy_file records every destination it writes, and the pre-unification
|
|
307
|
+
full install deployed the entry through it. Ownership is read the same way it was written.
|
|
308
|
+
"""
|
|
215
309
|
entry = claude_dir / "CLAUDE.md"
|
|
216
310
|
if not entry.exists():
|
|
217
311
|
if dry:
|
|
@@ -225,7 +319,11 @@ def seed_entry(claude_dir, legacy_monolith, dry=False):
|
|
|
225
319
|
body = entry.read_text(encoding="utf-8")
|
|
226
320
|
if IMPORT_LINE in body:
|
|
227
321
|
return "ok"
|
|
228
|
-
|
|
322
|
+
# Ours by content (a re-install of this release) OR by record (any earlier one).
|
|
323
|
+
# Both sides go through pathlib first: the manifest is written by shell, so a config dir
|
|
324
|
+
# with a trailing or doubled slash lands in it verbatim, and raw string equality then
|
|
325
|
+
# misses a path that is the same file.
|
|
326
|
+
if body == legacy_monolith or str(pathlib.Path(entry)) in prior_deployed:
|
|
229
327
|
if dry:
|
|
230
328
|
print(" [dry] replace legacy deployed CLAUDE.md with seed (backup)")
|
|
231
329
|
seed_personal_learnings(claude_dir, dry)
|
|
@@ -259,12 +357,50 @@ def merge_codex(codex_dir, central_text, dry=False):
|
|
|
259
357
|
agents.write_text(new, encoding="utf-8")
|
|
260
358
|
|
|
261
359
|
|
|
360
|
+
def remove_owned(claude_dir, codex_dir, manifest, dry=False):
|
|
361
|
+
"""Undo the two spans this file writes into files it does not own.
|
|
362
|
+
|
|
363
|
+
Every merge here needs a matching removal, and for a long time these two did not have one:
|
|
364
|
+
uninstall deleted the deployed hook FILES while leaving their registrations in the user's
|
|
365
|
+
settings.json, and deleted the guides while leaving the AGENTS.md central region that
|
|
366
|
+
references them. The user was left with hooks invoking missing paths and instructions
|
|
367
|
+
pointing at deleted files, after a command that reported success.
|
|
368
|
+
|
|
369
|
+
Ownership is read the same way it is written — `merge_settings` owning by manifest NAME, and
|
|
370
|
+
the marker pair for the Codex region — so removal can never reach further than the merge did.
|
|
371
|
+
Text outside the markers, and settings entries this repo did not register, are untouched.
|
|
372
|
+
"""
|
|
373
|
+
owned = sorted({h for h in manifest.get("hooks", {})})
|
|
374
|
+
merge_settings(claude_dir, [], REPO / "claude" / "settings.template.json",
|
|
375
|
+
dry=dry, owned_names=owned)
|
|
376
|
+
|
|
377
|
+
agents = codex_dir / "AGENTS.md"
|
|
378
|
+
if not agents.is_file():
|
|
379
|
+
return
|
|
380
|
+
body = agents.read_text(encoding="utf-8")
|
|
381
|
+
if MARK_START not in body or MARK_END not in body:
|
|
382
|
+
return # nothing of ours in there; a whole-file legacy deploy is not ours to judge
|
|
383
|
+
pre, rest = body.split(MARK_START, 1)
|
|
384
|
+
_, post = rest.split(MARK_END, 1)
|
|
385
|
+
if dry:
|
|
386
|
+
print(" [dry] strip AGENTS.md central region, keep everything outside the markers")
|
|
387
|
+
return
|
|
388
|
+
agents.write_text((pre + post).lstrip("\n"), encoding="utf-8")
|
|
389
|
+
|
|
390
|
+
|
|
262
391
|
def main():
|
|
263
392
|
ap = argparse.ArgumentParser()
|
|
393
|
+
ap.add_argument("--remove-owned", action="store_true",
|
|
394
|
+
help="undo the settings registrations and the AGENTS.md central region "
|
|
395
|
+
"(uninstall's half of the merge); writes nothing else")
|
|
264
396
|
ap.add_argument("--domains", help="comma-separated selection; overrides selection.json")
|
|
265
397
|
ap.add_argument("--claude-dir", default=None)
|
|
266
398
|
ap.add_argument("--codex-dir", default=None)
|
|
267
399
|
ap.add_argument("--state-dir", default=None)
|
|
400
|
+
ap.add_argument("--prior-manifest", default=None,
|
|
401
|
+
help="the previous install's manifest. Ownership of the entry file is read "
|
|
402
|
+
"from it, so an earlier release's deployed CLAUDE.md is recognized as "
|
|
403
|
+
"ours instead of being reported as the user's.")
|
|
268
404
|
ap.add_argument("--dry-run", action="store_true")
|
|
269
405
|
args = ap.parse_args()
|
|
270
406
|
|
|
@@ -273,12 +409,17 @@ def main():
|
|
|
273
409
|
codex_dir = pathlib.Path(args.codex_dir or os.environ.get("CODEX_HOME") or pathlib.Path.home() / ".codex")
|
|
274
410
|
state_dir = pathlib.Path(args.state_dir or pathlib.Path.home() / ".local/share/agent-bios")
|
|
275
411
|
|
|
276
|
-
|
|
412
|
+
manifest = json.loads((REPO / "compose" / "domains.json").read_text(encoding="utf-8"))
|
|
413
|
+
if args.remove_owned:
|
|
414
|
+
# No domains gate: removal does not depend on the manifest being well-formed, and an
|
|
415
|
+
# uninstall that refuses to run because the corpus is mid-edit would strand the user.
|
|
416
|
+
remove_owned(claude_dir, codex_dir, manifest, dry=args.dry_run)
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
gate = subprocess.run([sys.executable, str(REPO / "compose" / "check-domains.py")],
|
|
277
420
|
capture_output=True, text=True)
|
|
278
421
|
if gate.returncode != 0:
|
|
279
422
|
die("domains gate FAILED — fix manifest/corpus first:\n" + gate.stdout + gate.stderr)
|
|
280
|
-
|
|
281
|
-
manifest = json.loads((REPO / "config" / "domains.json").read_text(encoding="utf-8"))
|
|
282
423
|
if args.domains is not None:
|
|
283
424
|
selection = frozenset(d for d in args.domains.split(",") if d)
|
|
284
425
|
else:
|
|
@@ -300,6 +441,29 @@ def main():
|
|
|
300
441
|
codex_bundle += codex_only + "\n"
|
|
301
442
|
|
|
302
443
|
guides = filtered_files(manifest, "guides", selection)
|
|
444
|
+
withheld = [n for n in guides if author_only(REPO / "claude" / "guides" / n)]
|
|
445
|
+
guides = [n for n in guides if n not in withheld]
|
|
446
|
+
# Not writing it is not enough for anyone who installed before this rule: the
|
|
447
|
+
# manifest is rebuilt from the current deploy, so a file that stops being
|
|
448
|
+
# deployed stops being tracked and would sit there for good.
|
|
449
|
+
stale = [d / n for n in withheld
|
|
450
|
+
for d in (claude_dir / "central" / "guides", codex_dir / "guides")
|
|
451
|
+
if (d / n).is_file()]
|
|
452
|
+
# Copy before removing, the way install.sh backs up a file it replaces. The
|
|
453
|
+
# name matching ours does not prove we wrote it — a shared or symlinked guides
|
|
454
|
+
# directory can hold somebody's own file under the same name, and a deleted
|
|
455
|
+
# one is not recoverable from anywhere else.
|
|
456
|
+
# One timestamped directory per run, with a subdirectory per reason for the removal:
|
|
457
|
+
# `withheld` is an audience decision, `deselected` is a selection change.
|
|
458
|
+
run_backup = state_dir / "backups" / time.strftime("%Y%m%d-%H%M%S")
|
|
459
|
+
backup = run_backup / "withheld"
|
|
460
|
+
for path in stale:
|
|
461
|
+
print(f" {'[dry] ' if args.dry_run else ''}remove withheld {path}")
|
|
462
|
+
if not args.dry_run:
|
|
463
|
+
keep = backup / str(path).lstrip("/")
|
|
464
|
+
keep.parent.mkdir(parents=True, exist_ok=True)
|
|
465
|
+
shutil.copy2(path, keep)
|
|
466
|
+
path.unlink()
|
|
303
467
|
hooks = filtered_files(manifest, "hooks", selection)
|
|
304
468
|
agents = filtered_files(manifest, "agents", selection)
|
|
305
469
|
dry = args.dry_run
|
|
@@ -307,19 +471,30 @@ def main():
|
|
|
307
471
|
central = claude_dir / "central"
|
|
308
472
|
if dry:
|
|
309
473
|
print(f"[dry] bundle.md: {n_bullets} bullets; guides={guides} hooks={hooks} agents={agents}")
|
|
474
|
+
if withheld:
|
|
475
|
+
print(f" [dry] withheld (audience: author): {withheld}")
|
|
310
476
|
else:
|
|
311
477
|
central.mkdir(parents=True, exist_ok=True)
|
|
312
478
|
(central / "bundle.md").write_text(bundle, encoding="utf-8")
|
|
313
479
|
copy_filtered(REPO / "claude" / "guides", guides, central / "guides",
|
|
314
|
-
rewrite=(f"{CLAUDE_VAR}/guides/", f"{CLAUDE_VAR}/central/guides/"), dry=dry
|
|
315
|
-
|
|
316
|
-
copy_filtered(REPO / "claude" / "
|
|
480
|
+
rewrite=(f"{CLAUDE_VAR}/guides/", f"{CLAUDE_VAR}/central/guides/"), dry=dry,
|
|
481
|
+
backup=run_backup)
|
|
482
|
+
copy_filtered(REPO / "claude" / "hooks", hooks, central / "hooks", dry=dry, backup=run_backup)
|
|
483
|
+
copy_filtered(REPO / "claude" / "agents", agents, central / "agents", dry=dry,
|
|
484
|
+
backup=run_backup)
|
|
317
485
|
merge_settings(claude_dir, hooks, REPO / "claude" / "settings.template.json", dry=dry,
|
|
318
486
|
owned_names=manifest.get("hooks", {})) # deselected hooks must drop too
|
|
319
|
-
|
|
487
|
+
prior_deployed = set()
|
|
488
|
+
if args.prior_manifest:
|
|
489
|
+
prior = pathlib.Path(args.prior_manifest)
|
|
490
|
+
if prior.is_file():
|
|
491
|
+
prior_deployed = {str(pathlib.Path(ln.strip())) for ln in
|
|
492
|
+
prior.read_text(encoding="utf-8").splitlines() if ln.strip()}
|
|
493
|
+
entry_state = seed_entry(claude_dir, monolith, prior_deployed, dry=dry)
|
|
320
494
|
|
|
321
495
|
merge_codex(codex_dir, codex_bundle, dry=dry)
|
|
322
|
-
copy_filtered(REPO / "codex" / "guides", guides, codex_dir / "guides", dry=dry
|
|
496
|
+
copy_filtered(REPO / "codex" / "guides", guides, codex_dir / "guides", dry=dry,
|
|
497
|
+
backup=run_backup)
|
|
323
498
|
|
|
324
499
|
if not dry:
|
|
325
500
|
state_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -327,7 +502,9 @@ def main():
|
|
|
327
502
|
json.dumps({"version": 1, "domains": sorted(selection)}, indent=2) + "\n", encoding="utf-8")
|
|
328
503
|
|
|
329
504
|
print(f"ASSEMBLED: {n_bullets} bullets, {len(guides)} guides, {len(hooks)} hooks, "
|
|
330
|
-
f"{len(agents)} agents for selection {sorted(selection)}; entry={entry_state}"
|
|
505
|
+
f"{len(agents)} agents for selection {sorted(selection)}; entry={entry_state}"
|
|
506
|
+
+ (f"; withheld {len(withheld)} author-only guide(s): {', '.join(withheld)}"
|
|
507
|
+
if withheld else ""))
|
|
331
508
|
if entry_state == "needs-action":
|
|
332
509
|
print(f"ACTION NEEDED: {claude_dir / 'CLAUDE.md'} is user-owned and lacks '{IMPORT_LINE}' — "
|
|
333
510
|
"add the import line manually; the installer will not rewrite your file.")
|