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
|
@@ -48,51 +48,19 @@
|
|
|
48
48
|
{"anchor": "Treat a produced field, flag, signal", "tier": "domain", "domains": ["llm-pipeline-dev"]},
|
|
49
49
|
{"anchor": "Hard-block only deterministically decidable structural", "tier": "domain", "domains": ["llm-pipeline-dev"]},
|
|
50
50
|
{"anchor": "Runtime/code may enforce the contract", "tier": "domain", "domains": ["llm-pipeline-dev"]},
|
|
51
|
-
{"anchor": "
|
|
52
|
-
{"anchor": "Treat lasting or shared names as concept", "tier": "domain", "domains": ["builder-base"]},
|
|
53
|
-
{"anchor": "Before adding or changing a concept", "tier": "domain", "domains": ["builder-base"]},
|
|
54
|
-
{"anchor": "Prefer broad, stable concepts with precise", "tier": "domain", "domains": ["builder-base"]},
|
|
51
|
+
{"anchor": "When adding, changing, renaming, splitting, or exposing anything lasting or shared", "tier": "domain", "domains": ["builder-base"]},
|
|
55
52
|
{"anchor": "Before fixing a review finding or test failure", "tier": "domain", "domains": ["builder-base"]},
|
|
56
|
-
{"anchor": "Split or promote a concept when it changes", "tier": "domain", "domains": ["builder-base"]},
|
|
57
|
-
{"anchor": "Keep derived values as properties or projections", "tier": "domain", "domains": ["builder-base"]},
|
|
58
|
-
{"anchor": "Keep internal projections and helper outputs internal", "tier": "domain", "domains": ["builder-base"]},
|
|
59
|
-
{"anchor": "Distinguish authority from visibility", "tier": "domain", "domains": ["builder-base"]},
|
|
60
|
-
{"anchor": "Reuse existing enum values, failure kinds", "tier": "domain", "domains": ["builder-base"]},
|
|
61
|
-
{"anchor": "Use fallback paths, compatibility shims", "tier": "domain", "domains": ["builder-base"]},
|
|
62
|
-
{"anchor": "Keep comments and active docs aligned", "tier": "domain", "domains": ["builder-base"]},
|
|
63
|
-
{"anchor": "When a split is necessary, name the parent", "tier": "domain", "domains": ["builder-base"]},
|
|
64
|
-
{"anchor": "In ontology work, check existing entities", "tier": "domain", "domains": ["builder-base"]},
|
|
65
|
-
{"anchor": "In code work, follow existing naming patterns", "tier": "domain", "domains": ["builder-base"]},
|
|
66
|
-
{"anchor": "Let the repository's shape mirror its concept graph", "tier": "domain", "domains": ["builder-base"]},
|
|
67
53
|
{"anchor": "For `.xlsx` editing, generation, reconciliation", "tier": "domain", "domains": ["office-work"]},
|
|
68
|
-
{"anchor": "For
|
|
54
|
+
{"anchor": "For development work, read and use", "tier": "domain", "domains": ["builder-base"]},
|
|
69
55
|
{"anchor": "For mock, fixture, fake, stub, simulated-provider", "tier": "domain", "domains": ["builder-base"]},
|
|
70
|
-
{"anchor": "When the user asks to", "tier": "domain", "domains": ["builder-base"]},
|
|
71
|
-
{"anchor": "Think before coding: state key assumptions", "tier": "domain", "domains": ["builder-base"]},
|
|
72
|
-
{"anchor": "Build the smallest viable functional path", "tier": "domain", "domains": ["builder-base"]},
|
|
73
|
-
{"anchor": "Treat viability as real behavior against", "tier": "domain", "domains": ["builder-base"]},
|
|
74
|
-
{"anchor": "Make surgical changes. Touch only what", "tier": "domain", "domains": ["builder-base"]},
|
|
75
|
-
{"anchor": "Clean up issues introduced by the current", "tier": "domain", "domains": ["builder-base"]},
|
|
76
56
|
{"anchor": "Own the full lifecycle of what you create", "tier": "domain", "domains": ["builder-base"]},
|
|
77
|
-
{"anchor": "Define success criteria before multi-step coding work", "tier": "domain", "domains": ["builder-base"]},
|
|
78
|
-
{"anchor": "For bugs, prefer a reproducing test", "tier": "domain", "domains": ["builder-base"]},
|
|
79
|
-
{"anchor": "Every changed line should trace back", "tier": "domain", "domains": ["builder-base"]},
|
|
80
|
-
{"anchor": "Fix the root cause at its authority", "tier": "domain", "domains": ["builder-base"]},
|
|
81
57
|
{"anchor": "Land risky or behavior-changing work behind", "tier": "domain", "domains": ["builder-base"]},
|
|
82
58
|
{"anchor": "For composing a review request, packet", "tier": "domain", "domains": ["builder-base"]},
|
|
83
59
|
{"anchor": "After every meaningful code, ontology, config", "tier": "domain", "domains": ["builder-base"]},
|
|
84
|
-
{"anchor": "
|
|
85
|
-
{"anchor": "Add the narrowest reliable runtime or semantic test", "tier": "domain", "domains": ["builder-base"]},
|
|
86
|
-
{"anchor": "Pick each domain's verification mix", "tier": "domain", "domains": ["builder-base"]},
|
|
87
|
-
{"anchor": "Let the LLM derive scenarios from the diff", "tier": "domain", "domains": ["builder-base"]},
|
|
88
|
-
{"anchor": "Keep E2E stable with deterministic data", "tier": "domain", "domains": ["builder-base"]},
|
|
60
|
+
{"anchor": "For choosing verification depth, the per-domain mix, the case space, what makes a completion criterion falsifiable", "tier": "domain", "domains": ["builder-base"]},
|
|
89
61
|
{"anchor": "Report the checks run, results", "tier": "core", "domains": []},
|
|
90
62
|
{"anchor": "Trust a green check only when it traversed", "tier": "domain", "domains": ["builder-base"]},
|
|
91
|
-
{"anchor": "Make completion criteria falsifiable", "tier": "domain", "domains": ["builder-base"]},
|
|
92
63
|
{"anchor": "Before comparing two of anything", "tier": "domain", "domains": ["builder-base"]},
|
|
93
|
-
{"anchor": "For non-trivial designs or high-risk changes", "tier": "domain", "domains": ["builder-base"]},
|
|
94
|
-
{"anchor": "Proportion verification to cost, risk, and information gain", "tier": "domain", "domains": ["builder-base"]},
|
|
95
|
-
{"anchor": "Trust a green / zero-findings verdict only", "tier": "domain", "domains": ["builder-base"]},
|
|
96
64
|
{"anchor": "For concrete shell/CLI traps", "tier": "domain", "domains": ["builder-base"]},
|
|
97
65
|
{"anchor": "Ambient state — the active shell", "tier": "domain", "domains": ["builder-base"]},
|
|
98
66
|
{"anchor": "Before relying on any model id", "tier": "domain", "domains": ["builder-base"]},
|
|
@@ -100,21 +68,17 @@
|
|
|
100
68
|
{"anchor": "Never accept secrets through transcript- or history-logged channels", "tier": "core", "domains": []},
|
|
101
69
|
{"anchor": "When a secret must be supplied", "tier": "domain", "domains": ["builder-base"]},
|
|
102
70
|
{"anchor": "Treat a coarse runtime signal", "tier": "domain", "domains": ["builder-base"]},
|
|
103
|
-
{"anchor": "Before reasoning about what a branch contains", "tier": "domain", "domains": ["builder-base"]},
|
|
104
71
|
{"anchor": "Standing spawn policy: check the spawn gates", "tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
105
72
|
{"anchor": "Down-spawns carry a machine-checkable done-when", "tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
106
73
|
{"anchor": "For work spanning multiple models or CLI agents", "tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
107
74
|
{"anchor": "For composing a prompt, packet, or tool description", "tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
108
75
|
{"anchor": "Allocate models by difficulty", "tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
76
|
+
{"anchor": "Judge a review by how much independence", "tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
109
77
|
{"anchor": "dual-provider frontier design drafts", "tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
110
78
|
{"anchor": "Never retry-storm a live rate limit", "tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
111
79
|
{"anchor": "On any resumed, cleared, or relocated session", "tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
112
80
|
{"anchor": "Keep runtime code, active docs, and execution-facing", "tier": "domain", "domains": ["builder-base"]},
|
|
113
|
-
{"anchor": "
|
|
114
|
-
{"anchor": "Put backward-compatibility notes, deprecated behavior", "tier": "domain", "domains": ["builder-base"]},
|
|
115
|
-
{"anchor": "Link from active docs or code to isolated notes", "tier": "domain", "domains": ["builder-base"]},
|
|
116
|
-
{"anchor": "Phrase guidelines as desired behavior", "tier": "domain", "domains": ["builder-base"]},
|
|
117
|
-
{"anchor": "Prefer established docs such as", "tier": "domain", "domains": ["builder-base"]},
|
|
81
|
+
{"anchor": "For where a comment, a compatibility note, deprecated behavior, a rejected alternative", "tier": "domain", "domains": ["builder-base"]},
|
|
118
82
|
{"anchor": "For SVG diagrams, service blueprints", "tier": "domain", "domains": ["visualization-docs"]},
|
|
119
83
|
{"anchor": "When a concept is easier to understand visually", "tier": "domain", "domains": ["visualization-docs"]},
|
|
120
84
|
{"anchor": "Use HTML for comparisons, flows, state changes", "tier": "domain", "domains": ["visualization-docs"]},
|
|
@@ -125,10 +89,12 @@
|
|
|
125
89
|
{"anchor": "`learn!` — session learning", "tier": "core", "domains": []}
|
|
126
90
|
],
|
|
127
91
|
"guides": {
|
|
128
|
-
"claude-prompting.md": {"tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
129
|
-
"cli-multi-model-workflow.md": {"tier": "domain", "domains": ["multi-agent-orchestration"]},
|
|
92
|
+
"claude-prompting.md": {"tier": "domain", "domains": ["multi-agent-orchestration"], "handles": ["per-family prompting guide", "계열별 prompting guide"]},
|
|
93
|
+
"cli-multi-model-workflow.md": {"tier": "domain", "domains": ["multi-agent-orchestration"], "handles": ["multi-model guide", "multi-model 가이드"]},
|
|
130
94
|
"coding-staged-workflow.md": {"tier": "domain", "domains": ["builder-base"]},
|
|
131
|
-
"
|
|
95
|
+
"concept-economy.md": {"tier": "domain", "domains": ["builder-base"]},
|
|
96
|
+
"documentation-hygiene.md": {"tier": "domain", "domains": ["builder-base"]},
|
|
97
|
+
"gpt-prompting.md": {"tier": "domain", "domains": ["multi-agent-orchestration"], "handles": ["prompting guides for gpt-family"]},
|
|
132
98
|
"implementation-map.md": {"tier": "domain", "domains": ["visualization-docs"]},
|
|
133
99
|
"llm-capability-boundary-examples.md": {"tier": "domain", "domains": ["llm-pipeline-dev"]},
|
|
134
100
|
"llm-capability-boundary-patterns.md": {"tier": "domain", "domains": ["llm-pipeline-dev"]},
|
|
@@ -138,6 +104,7 @@
|
|
|
138
104
|
"session-distill-workflow.md": {"tier": "infra", "domains": []},
|
|
139
105
|
"learning-flow.md": {"tier": "infra", "domains": []},
|
|
140
106
|
"svg-visualization-guide.md": {"tier": "domain", "domains": ["visualization-docs"]},
|
|
107
|
+
"verification-discipline.md": {"tier": "domain", "domains": ["builder-base"]},
|
|
141
108
|
"tooling-gotchas.md": {"tier": "domain", "domains": ["builder-base"]}
|
|
142
109
|
},
|
|
143
110
|
"hooks": {
|
|
@@ -39,7 +39,14 @@ def resolve(obj, key="package_id"):
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def segments(pid):
|
|
42
|
-
"""('scope', 'name') for deriving deploy paths. Only call on a valid id.
|
|
42
|
+
"""('scope', 'name') for deriving deploy paths. Only call on a valid id.
|
|
43
|
+
|
|
44
|
+
No caller yet, and that is recorded rather than left to be rediscovered: deploy-path
|
|
45
|
+
derivation belongs to the multi-package composer, which
|
|
46
|
+
design/adapter-split/ECOSYSTEM-ARCHITECTURE.md defers "to the first real second package".
|
|
47
|
+
Kept because the id grammar it splits is settled and rewriting it later would re-derive the
|
|
48
|
+
same two lines; delete it if that deferral is ever abandoned rather than landed.
|
|
49
|
+
"""
|
|
43
50
|
if not is_valid(pid):
|
|
44
51
|
raise ValueError(f"not a package id: {pid!r}")
|
|
45
52
|
return tuple(pid[1:].split("/", 1))
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Retention for the copies this repo makes before it overwrites or removes something.
|
|
3
|
+
|
|
4
|
+
A backup exists so a mistake is recoverable, so pruning is the one operation here that can
|
|
5
|
+
destroy the thing the feature is for. That decides the rule: a copy is deleted only when it is
|
|
6
|
+
BOTH older than the age window AND outside the most recent N. Either condition alone would be
|
|
7
|
+
enough to delete something someone still wants — a busy week would age out yesterday's work, and
|
|
8
|
+
a quiet quarter would trim a set small enough to keep whole.
|
|
9
|
+
|
|
10
|
+
Worked through, that single condition gives the intended behaviour:
|
|
11
|
+
|
|
12
|
+
20 copies made inside one month -> none are old enough; keep all 20
|
|
13
|
+
8 copies spread over two months -> none are outside the newest 10; keep all 8
|
|
14
|
+
11 copies, 7 of them this month -> the oldest is both aged and outside the 10; keep 10
|
|
15
|
+
|
|
16
|
+
Two groups are pruned, because this repo writes backups in two shapes: timestamped directories
|
|
17
|
+
under the state dir, and sibling backup files next to the file they copy (assemble, learn and
|
|
18
|
+
migrate write those). They are pruned independently — ten of each — since losing one group's
|
|
19
|
+
history to the other group's churn is not what either was kept for.
|
|
20
|
+
|
|
21
|
+
This module also answers WHICH loose files are ours (`OWNED_SIBLING`, `owned_siblings`), for
|
|
22
|
+
itself and for uninstall. Both delete, so the rule lives in one place.
|
|
23
|
+
|
|
24
|
+
--claude-dir/--codex-dir/--state-dir where to look
|
|
25
|
+
--dry-run print what would go, delete nothing
|
|
26
|
+
--list-owned print the owned loose backups, NUL-separated (uninstall reads this)
|
|
27
|
+
--self-test the retention rule and the ownership rule, both directions
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import os
|
|
33
|
+
import pathlib
|
|
34
|
+
import re
|
|
35
|
+
import shutil
|
|
36
|
+
import sys
|
|
37
|
+
import time
|
|
38
|
+
|
|
39
|
+
KEEP_RECENT = 10
|
|
40
|
+
MAX_AGE_DAYS = 30
|
|
41
|
+
|
|
42
|
+
# What counts as OUR loose backup — and the only place that question is answered.
|
|
43
|
+
# `install.sh` asks this module rather than restating the pattern, because both sides
|
|
44
|
+
# delete, and a rule stated twice is a rule that drifts on one side.
|
|
45
|
+
#
|
|
46
|
+
# The shapes below are exactly what this repo writes: compose/assemble.py (a plain
|
|
47
|
+
# timestamp for settings.json, `legacy` for a migrated entry file), learn/collect-learning.py,
|
|
48
|
+
# learn/migrate-learnings.py, and install.sh's codex-config removal. A bare `.bak-`
|
|
49
|
+
# substring — what this used to match — also claims `notes.bak-old`, which is the user's.
|
|
50
|
+
# These live in directories we SHARE with the user and with other tools, so ownership has
|
|
51
|
+
# to be carried by the name; the directory cannot confer it.
|
|
52
|
+
# The bare `.bak-<timestamp>` form carries no marker of its own, so it is pinned to the ONE
|
|
53
|
+
# basename that receives it — `settings.json`, from merge_settings. Left generic it also claimed
|
|
54
|
+
# a `notes.bak-20260101-000000` the user wrote, which is a correctly shaped timestamp on a file
|
|
55
|
+
# that is not ours; matching the timestamp SHAPE is not the same as matching our ownership.
|
|
56
|
+
_TS = r"\d{8}-\d{6}"
|
|
57
|
+
OWNED_SIBLING = re.compile(
|
|
58
|
+
rf"^(?:settings\.json\.bak-{_TS}"
|
|
59
|
+
rf"|.+\.bak-(?:legacy|learn|migrate)-{_TS}"
|
|
60
|
+
rf"|.+\.bak-agent-bios-uninstall)$")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def owned_siblings(root):
|
|
64
|
+
"""Every loose backup under `root` that this repo wrote. Nothing else is ours."""
|
|
65
|
+
if not root.is_dir():
|
|
66
|
+
return []
|
|
67
|
+
return [f for f in root.rglob("*") if f.is_file() and OWNED_SIBLING.search(f.name)]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def to_delete(entries, now, keep_recent=KEEP_RECENT, max_age_days=MAX_AGE_DAYS):
|
|
71
|
+
"""(path, mtime) pairs -> the ones both aged out AND outside the newest `keep_recent`.
|
|
72
|
+
|
|
73
|
+
Both conditions, never either: this is the whole retention policy, and it is a pure function
|
|
74
|
+
so the policy can be tested without creating and destroying real backups.
|
|
75
|
+
"""
|
|
76
|
+
newest_first = sorted(entries, key=lambda e: e[1], reverse=True)
|
|
77
|
+
cutoff = now - max_age_days * 86400
|
|
78
|
+
return [path for i, (path, mtime) in enumerate(newest_first)
|
|
79
|
+
if i >= keep_recent and mtime < cutoff]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _entries(paths):
|
|
83
|
+
out = []
|
|
84
|
+
for p in paths:
|
|
85
|
+
try:
|
|
86
|
+
out.append((p, p.stat().st_mtime))
|
|
87
|
+
except OSError:
|
|
88
|
+
continue # vanished under us; nothing to prune
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def prune(state_dir, homes, now=None, dry=False):
|
|
93
|
+
"""Prune both groups. Returns the paths removed (or that would be)."""
|
|
94
|
+
now = time.time() if now is None else now
|
|
95
|
+
removed = []
|
|
96
|
+
|
|
97
|
+
backups = state_dir / "backups"
|
|
98
|
+
if backups.is_dir():
|
|
99
|
+
for path in to_delete(_entries([d for d in backups.iterdir() if d.is_dir()]), now):
|
|
100
|
+
removed.append(path)
|
|
101
|
+
if not dry:
|
|
102
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
103
|
+
|
|
104
|
+
for home in homes:
|
|
105
|
+
for path in to_delete(_entries(owned_siblings(home)), now):
|
|
106
|
+
removed.append(path)
|
|
107
|
+
if not dry:
|
|
108
|
+
path.unlink(missing_ok=True)
|
|
109
|
+
return removed
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def self_test():
|
|
113
|
+
day = 86400
|
|
114
|
+
now = 1_000_000_000.0
|
|
115
|
+
cases = [
|
|
116
|
+
("20 within a month -> keep all",
|
|
117
|
+
[(f"b{i}", now - i * day) for i in range(20)], 0),
|
|
118
|
+
("8 over two months -> keep all",
|
|
119
|
+
[(f"b{i}", now - i * 8 * day) for i in range(8)], 0),
|
|
120
|
+
("11 with 7 recent -> drop the oldest one",
|
|
121
|
+
[(f"b{i}", now - i * day) for i in range(7)]
|
|
122
|
+
+ [(f"o{i}", now - (40 + i * 5) * day) for i in range(4)], 1),
|
|
123
|
+
("11 all recent -> drop none",
|
|
124
|
+
[(f"b{i}", now - i * day) for i in range(11)], 0),
|
|
125
|
+
("30 all aged -> keep the newest ten",
|
|
126
|
+
[(f"b{i}", now - (40 + i) * day) for i in range(30)], 20),
|
|
127
|
+
]
|
|
128
|
+
bad = 0
|
|
129
|
+
for label, entries, want in cases:
|
|
130
|
+
got = len(to_delete(entries, now))
|
|
131
|
+
ok = got == want
|
|
132
|
+
bad += not ok
|
|
133
|
+
print(f" {'ok ' if ok else 'FAIL'} {label} (deleted {got}, want {want})")
|
|
134
|
+
# A rule that deletes nothing whatever the input would pass every case above by accident.
|
|
135
|
+
proof = to_delete([(f"x{i}", now - (100 + i) * day) for i in range(50)], now)
|
|
136
|
+
if len(proof) != 40:
|
|
137
|
+
print(" FAIL contrast control: the rule never deletes anything")
|
|
138
|
+
bad += 1
|
|
139
|
+
else:
|
|
140
|
+
print(" ok contrast control: it does delete when both conditions hold")
|
|
141
|
+
|
|
142
|
+
# Ownership, in both directions. The first list is every shape this repo writes; the
|
|
143
|
+
# second is what a person or another tool leaves in the same directories. Matching the
|
|
144
|
+
# first proves the rule still finds our copies after a rename; refusing the second is
|
|
145
|
+
# the half that matters, because uninstall DELETES whatever this claims.
|
|
146
|
+
ours = ["settings.json.bak-20260101-000000", "CLAUDE.md.bak-legacy-20260101-000000",
|
|
147
|
+
"learnings.md.bak-learn-20260101-000000", "learnings.md.bak-migrate-20260101-000000",
|
|
148
|
+
"config.toml.bak-agent-bios-uninstall"]
|
|
149
|
+
theirs = ["notes.bak-old", "db.bak-2026", "x.bak-", "report.bak-final.txt",
|
|
150
|
+
"settings.json.bak-2026010-000000", "a.bak-legacy-nope",
|
|
151
|
+
# A correctly shaped timestamp on a basename that is not ours. The first pass
|
|
152
|
+
# only rejected MALFORMED timestamps, which tested the shape and not the
|
|
153
|
+
# ownership — so the rule went on claiming these and the control stayed green.
|
|
154
|
+
"notes.bak-20260101-000000", "db.bak-20240301-120000",
|
|
155
|
+
"settings.json.txt.bak-20260101-000000"]
|
|
156
|
+
missed = [n for n in ours if not OWNED_SIBLING.search(n)]
|
|
157
|
+
grabbed = [n for n in theirs if OWNED_SIBLING.search(n)]
|
|
158
|
+
for name in missed:
|
|
159
|
+
print(f" FAIL ownership: {name} is ours but went unclaimed")
|
|
160
|
+
for name in grabbed:
|
|
161
|
+
print(f" FAIL ownership: {name} is NOT ours but was claimed for deletion")
|
|
162
|
+
bad += len(missed) + len(grabbed)
|
|
163
|
+
if not missed and not grabbed:
|
|
164
|
+
print(f" ok ownership: claims {len(ours)} shapes we write, "
|
|
165
|
+
f"refuses {len(theirs)} we do not")
|
|
166
|
+
print(f"prune-backups self-test: {'OK' if not bad else 'FAIL'} ({len(cases) + 2} checks)")
|
|
167
|
+
return 1 if bad else 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main():
|
|
171
|
+
ap = argparse.ArgumentParser()
|
|
172
|
+
ap.add_argument("--state-dir")
|
|
173
|
+
ap.add_argument("--claude-dir")
|
|
174
|
+
ap.add_argument("--codex-dir")
|
|
175
|
+
ap.add_argument("--dry-run", action="store_true")
|
|
176
|
+
ap.add_argument("--self-test", action="store_true")
|
|
177
|
+
ap.add_argument("--list-owned", action="store_true",
|
|
178
|
+
help="print every owned loose backup under the config dirs, NUL-separated, "
|
|
179
|
+
"then exit. uninstall consumes this instead of restating the rule.")
|
|
180
|
+
args = ap.parse_args()
|
|
181
|
+
if args.self_test:
|
|
182
|
+
sys.exit(self_test())
|
|
183
|
+
|
|
184
|
+
state = pathlib.Path(args.state_dir or pathlib.Path.home() / ".local/share/agent-bios")
|
|
185
|
+
homes = [pathlib.Path(d) for d in (
|
|
186
|
+
args.claude_dir or os.environ.get("CLAUDE_CONFIG_DIR") or pathlib.Path.home() / ".claude",
|
|
187
|
+
args.codex_dir or os.environ.get("CODEX_HOME") or pathlib.Path.home() / ".codex")]
|
|
188
|
+
if args.list_owned:
|
|
189
|
+
# NUL-separated: these are real paths from a user's home, and a newline in one would
|
|
190
|
+
# otherwise split a single file into two entries — one of which the caller then deletes.
|
|
191
|
+
for home in homes:
|
|
192
|
+
for path in owned_siblings(home):
|
|
193
|
+
sys.stdout.write(f"{path}\0")
|
|
194
|
+
return
|
|
195
|
+
removed = prune(state, homes, dry=args.dry_run)
|
|
196
|
+
for path in removed:
|
|
197
|
+
print(f" {'[dry] ' if args.dry_run else ''}pruned backup {path}")
|
|
198
|
+
if removed and not args.dry_run:
|
|
199
|
+
print(f" pruned {len(removed)} backup(s) past retention "
|
|
200
|
+
f"(kept: newest {KEEP_RECENT}, plus anything under {MAX_AGE_DAYS} days)")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
main()
|
|
@@ -18,8 +18,8 @@ import sys
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def load_assemble(repo):
|
|
21
|
-
"""Import assemble.py by path —
|
|
22
|
-
spec = importlib.util.spec_from_file_location("assemble", repo / "
|
|
21
|
+
"""Import assemble.py by path — compose/ is a flat toolbox, not a package."""
|
|
22
|
+
spec = importlib.util.spec_from_file_location("assemble", repo / "compose" / "assemble.py")
|
|
23
23
|
mod = importlib.util.module_from_spec(spec)
|
|
24
24
|
spec.loader.exec_module(mod)
|
|
25
25
|
return mod
|
|
@@ -29,7 +29,7 @@ def main():
|
|
|
29
29
|
if len(sys.argv) != 3:
|
|
30
30
|
sys.exit("usage: register-hooks.py <repo> <claude-dir>")
|
|
31
31
|
repo, claude_dir = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
|
|
32
|
-
manifest = json.loads((repo / "
|
|
32
|
+
manifest = json.loads((repo / "compose" / "domains.json").read_text(encoding="utf-8"))
|
|
33
33
|
names = sorted(manifest.get("hooks", {}))
|
|
34
34
|
if not names:
|
|
35
35
|
sys.exit("register-hooks: manifest declares no hooks — refusing to rewrite settings")
|