agent-bios 0.11.0 → 0.12.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/README.md +4 -4
- package/claude/guides/cli-multi-model-workflow.md +46 -0
- package/claude/hooks/tooling-gotchas-hook.py +37 -2
- package/claude/skills/repo-charter/SKILL.md +149 -0
- package/codex/guides/cli-multi-model-workflow.md +46 -0
- package/compose/assemble.py +184 -21
- package/compose/canary.sh +13 -0
- package/compose/check-domains.py +144 -20
- package/compose/domains.json +3 -0
- package/compose/prune-backups.py +57 -7
- package/install.sh +396 -27
- package/launch/agent-launch.py +3825 -534
- package/launch/agent-launch.toml +10 -4
- package/launch/agent-launch.zsh +7 -2
- package/launch/i18n/en.toml +127 -7
- package/launch/i18n/ja.toml +126 -7
- package/launch/i18n/ko.toml +126 -7
- package/learn/check-learning.py +19 -1
- package/learn/collect-learning.py +57 -2
- package/learn/migrate-learnings.py +140 -16
- package/learn/redact.py +14 -5
- package/package.json +3 -2
- package/provenance.json +1 -1
- package/session-cost.py +402 -33
- package/wrappers/claude-run.sh +49 -4
|
@@ -61,6 +61,34 @@ HOSTS = {
|
|
|
61
61
|
CLAUDE_IMPORT_LINE = "@personal/learnings.md"
|
|
62
62
|
CLAUDE_CENTRAL_IMPORT = "@central/bundle.md"
|
|
63
63
|
|
|
64
|
+
|
|
65
|
+
def import_line_index(body, directive):
|
|
66
|
+
"""Index of the line where `directive` is an ACTIVE import, or None.
|
|
67
|
+
|
|
68
|
+
Raw containment is not this question. `@personal/learnings.md` inside a fenced
|
|
69
|
+
example, or named in a sentence, loads nothing — and reading it as an import made
|
|
70
|
+
two different files claim a file was wired when it was not: capture reported the
|
|
71
|
+
entry `present` and wrote nothing, and migrate-learnings read a fenced central
|
|
72
|
+
import as evidence the corpus was loaded, which is half of what authorizes deleting
|
|
73
|
+
a user's personal copy. An import is a line whose whole content is the directive.
|
|
74
|
+
|
|
75
|
+
Fences are tracked rather than stripped so the index is an index into `body`'s own
|
|
76
|
+
lines, which is what the insertion point needs. Both ``` and ~~~ open and close.
|
|
77
|
+
"""
|
|
78
|
+
fenced = False
|
|
79
|
+
for index, line in enumerate(body.splitlines()):
|
|
80
|
+
stripped = line.strip()
|
|
81
|
+
if stripped.startswith("```") or stripped.startswith("~~~"):
|
|
82
|
+
fenced = not fenced
|
|
83
|
+
continue
|
|
84
|
+
if not fenced and stripped == directive:
|
|
85
|
+
return index
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def has_active_import(body, directive):
|
|
90
|
+
return import_line_index(body, directive) is not None
|
|
91
|
+
|
|
64
92
|
# Codex AGENTS.md markers. The central pair is owned by compose/assemble.py
|
|
65
93
|
# (kept in sync here); the personal-learnings pair is this tool's own region,
|
|
66
94
|
# placed outside the central pair so re-assembly preserves it.
|
|
@@ -220,13 +248,16 @@ def ensure_claude_import(home, dry):
|
|
|
220
248
|
entry.write_text(f"# CLAUDE.md\n\n{CLAUDE_IMPORT_LINE}\n", encoding="utf-8")
|
|
221
249
|
return "created"
|
|
222
250
|
body = entry.read_text(encoding="utf-8")
|
|
223
|
-
if CLAUDE_IMPORT_LINE
|
|
251
|
+
if has_active_import(body, CLAUDE_IMPORT_LINE):
|
|
224
252
|
return "present"
|
|
225
253
|
if dry:
|
|
226
254
|
print(f" [dry] insert '{CLAUDE_IMPORT_LINE}' into {entry}")
|
|
227
255
|
return "inserted"
|
|
228
256
|
lines = body.splitlines(keepends=True)
|
|
229
|
-
|
|
257
|
+
# Anchored to the ACTIVE central import for the same reason: matching a fenced one
|
|
258
|
+
# would insert this import inside that fence, where it loads nothing either.
|
|
259
|
+
central = import_line_index(body, CLAUDE_CENTRAL_IMPORT)
|
|
260
|
+
idx = central if central is not None else 0
|
|
230
261
|
backup(entry)
|
|
231
262
|
lines.insert(idx + 1, CLAUDE_IMPORT_LINE + "\n")
|
|
232
263
|
entry.write_text("".join(lines), encoding="utf-8")
|
|
@@ -543,6 +574,30 @@ def _self_test():
|
|
|
543
574
|
"\n" not in nl["lesson"] and "\r" not in nl["lesson"]
|
|
544
575
|
and "line one line two line three" == nl["lesson"]))
|
|
545
576
|
|
|
577
|
+
# 10) An import is a LINE, not a substring. Reading a fenced example or a sentence as
|
|
578
|
+
# an active import made capture report the entry `present` and write nothing, so
|
|
579
|
+
# the learning it had just saved would never load. Each negative is paired with the
|
|
580
|
+
# positive that separates it: a guard that answered False to everything would
|
|
581
|
+
# satisfy the first three rows and fail the fourth.
|
|
582
|
+
real = f"# CLAUDE.md\n{CLAUDE_CENTRAL_IMPORT}\n{CLAUDE_IMPORT_LINE}\n"
|
|
583
|
+
for name, body, want in (
|
|
584
|
+
("fenced ``` example is not an import", f"# e\n```\n{CLAUDE_IMPORT_LINE}\n```\n", False),
|
|
585
|
+
("fenced ~~~ example is not an import", f"# e\n~~~md\n{CLAUDE_IMPORT_LINE}\n~~~\n", False),
|
|
586
|
+
("a sentence naming the path is not an import",
|
|
587
|
+
f"# e\nWe import {CLAUDE_IMPORT_LINE} from here.\n", False),
|
|
588
|
+
("a standalone directive line IS an import", real, True),
|
|
589
|
+
("indentation and trailing space still import",
|
|
590
|
+
f"# e\n {CLAUDE_IMPORT_LINE} \n", True),
|
|
591
|
+
):
|
|
592
|
+
checks.append((f"import detection: {name}",
|
|
593
|
+
has_active_import(body, CLAUDE_IMPORT_LINE) is want))
|
|
594
|
+
# The insertion point follows the same rule, or the new import lands inside the fence.
|
|
595
|
+
checks.append(("insertion anchors on an ACTIVE central import",
|
|
596
|
+
import_line_index(real, CLAUDE_CENTRAL_IMPORT) == 1))
|
|
597
|
+
checks.append(("a fenced central import anchors nothing",
|
|
598
|
+
import_line_index(f"# e\n```\n{CLAUDE_CENTRAL_IMPORT}\n```\n",
|
|
599
|
+
CLAUDE_CENTRAL_IMPORT) is None))
|
|
600
|
+
|
|
546
601
|
failed = [name for name, ok in checks if not ok]
|
|
547
602
|
if failed:
|
|
548
603
|
for name in failed:
|
|
@@ -42,6 +42,7 @@ import time
|
|
|
42
42
|
|
|
43
43
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "compose"))
|
|
44
44
|
import pkgid # noqa: E402 (package-identity primitive owned by compose/)
|
|
45
|
+
import assemble # noqa: E402 (owns the atomic-replace primitive both packages write with)
|
|
45
46
|
|
|
46
47
|
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
47
48
|
# Same home compose/corpus-state.py uses; the canary writes its activation proof here.
|
|
@@ -75,9 +76,21 @@ def load_collect():
|
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
def load_manifest(path=MANIFEST):
|
|
79
|
+
# "Manifest absent/empty -> no-op" is this module's stated contract, and an existing
|
|
80
|
+
# zero-byte file is the empty case it was missing: json.loads("") raised out of the
|
|
81
|
+
# update path as a traceback. Whitespace-only reads as empty; anything else that
|
|
82
|
+
# will not parse is a REAL manifest that is broken, and says so through die().
|
|
78
83
|
if not path.is_file():
|
|
79
84
|
return []
|
|
80
|
-
|
|
85
|
+
raw = path.read_text(encoding="utf-8")
|
|
86
|
+
if not raw.strip():
|
|
87
|
+
return []
|
|
88
|
+
try:
|
|
89
|
+
data = json.loads(raw)
|
|
90
|
+
except json.JSONDecodeError as exc:
|
|
91
|
+
die(f"manifest is not valid JSON: {path} ({exc})")
|
|
92
|
+
if not isinstance(data, dict):
|
|
93
|
+
die(f"manifest is not an object: {path}")
|
|
81
94
|
return [p for p in data.get("promotions", [])
|
|
82
95
|
if isinstance(p, dict) and isinstance(p.get("learning_id"), str)]
|
|
83
96
|
|
|
@@ -134,6 +147,18 @@ def corpus_text(path, collect=None):
|
|
|
134
147
|
return body
|
|
135
148
|
|
|
136
149
|
|
|
150
|
+
# A placement is one of two kinds and they are verified differently, so the kind has to
|
|
151
|
+
# be readable. It is derived from the anchor rather than carried as a new manifest field
|
|
152
|
+
# because the two shapes do not overlap: a whole-file anchor is a bare filename, and a
|
|
153
|
+
# bullet anchor is a phrase lifted from the rule, which always has spaces in it.
|
|
154
|
+
FILE_ANCHOR_SUFFIXES = (".md", ".py", ".toml", ".json", ".sh")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def anchor_names_a_file(anchor):
|
|
158
|
+
return (not any(ch.isspace() for ch in anchor)
|
|
159
|
+
and anchor.endswith(FILE_ANCHOR_SUFFIXES))
|
|
160
|
+
|
|
161
|
+
|
|
137
162
|
def placed_here(home, anchor, collect=None):
|
|
138
163
|
"""Is the promoted item REALLY in this user's deployed corpus?
|
|
139
164
|
|
|
@@ -146,9 +171,13 @@ def placed_here(home, anchor, collect=None):
|
|
|
146
171
|
if not isinstance(anchor, str) or not anchor:
|
|
147
172
|
return False
|
|
148
173
|
texts, dirs = corpus_surfaces(home)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
174
|
+
if anchor_names_a_file(anchor):
|
|
175
|
+
# A whole-file placement is proven by the deployed FILE and by nothing else. It
|
|
176
|
+
# used to fall through to the text scan below, where any corpus document that
|
|
177
|
+
# merely NAMES the guide counted as proof the guide was installed — and the
|
|
178
|
+
# corpus names its guides constantly. This is the reachable case rather than the
|
|
179
|
+
# hypothetical one: the live manifest's only anchor is `tooling-gotchas.md`.
|
|
180
|
+
return any((d / anchor).is_file() for d in dirs)
|
|
152
181
|
for f in texts:
|
|
153
182
|
if f.is_file() and anchor in corpus_text(f, collect):
|
|
154
183
|
return True
|
|
@@ -160,13 +189,17 @@ def backup(path):
|
|
|
160
189
|
|
|
161
190
|
|
|
162
191
|
def atomic_write(path, text):
|
|
163
|
-
"""Back up, then replace
|
|
164
|
-
|
|
165
|
-
|
|
192
|
+
"""Back up, then replace atomically so a crash mid-write can never truncate the
|
|
193
|
+
durable personal file.
|
|
194
|
+
|
|
195
|
+
The replace itself is compose/assemble.py's — that module needed the same discipline
|
|
196
|
+
for the user's settings.json and AGENTS.md, and a four-line primitive written twice is
|
|
197
|
+
two places for the next `os.replace` subtlety to be got right in only one of them. The
|
|
198
|
+
BACKUP stays here: its `.bak-migrate-<ts>` name is what prune-backups.py matches to
|
|
199
|
+
know the copy is ours, and assemble's writes carry a different one.
|
|
200
|
+
"""
|
|
166
201
|
backup(path)
|
|
167
|
-
|
|
168
|
-
tmp.write_text(text, encoding="utf-8")
|
|
169
|
-
os.replace(tmp, path)
|
|
202
|
+
assemble.replace_atomically(path, text)
|
|
170
203
|
|
|
171
204
|
|
|
172
205
|
def prune_jsonl(jsonl, remove_ids, dry):
|
|
@@ -215,16 +248,23 @@ def prune_claude_prose(home, remove_ids, dry):
|
|
|
215
248
|
|
|
216
249
|
|
|
217
250
|
def prune_codex_prose(home, remove_ids, collect, dry):
|
|
251
|
+
"""Bullets removed, or None when the personal region is MALFORMED.
|
|
252
|
+
|
|
253
|
+
None rather than 0, because the caller has to tell "nothing matched" from "this file
|
|
254
|
+
cannot be pruned at all". Both used to read as 0, and the jsonl prune ran anyway —
|
|
255
|
+
deleting the durable record out from under a bullet still on screen, which is the
|
|
256
|
+
opposite of the no-op the malformed case promises.
|
|
257
|
+
"""
|
|
218
258
|
agents = home / "AGENTS.md"
|
|
219
259
|
if not agents.is_file():
|
|
220
|
-
return 0
|
|
260
|
+
return 0 # no personal prose here at all — nothing to strand
|
|
221
261
|
body = agents.read_text(encoding="utf-8")
|
|
222
262
|
start, end = collect.PERSONAL_START, collect.PERSONAL_END
|
|
223
263
|
if start not in body:
|
|
224
|
-
return 0
|
|
264
|
+
return 0 # region never opened — same as above
|
|
225
265
|
pre, rest = body.split(start, 1)
|
|
226
266
|
if end not in rest: # malformed (missing, or END before START) → touch nothing
|
|
227
|
-
return
|
|
267
|
+
return None
|
|
228
268
|
region, post = rest.split(end, 1)
|
|
229
269
|
new_region, removed = prune_bullets(region, remove_ids)
|
|
230
270
|
if removed and not dry:
|
|
@@ -249,7 +289,15 @@ def claude_corpus_loaded(home, state_dir=None):
|
|
|
249
289
|
redundant, a wrong removal is data loss.
|
|
250
290
|
"""
|
|
251
291
|
entry = home / "CLAUDE.md"
|
|
252
|
-
if not
|
|
292
|
+
if not entry.is_file():
|
|
293
|
+
return False
|
|
294
|
+
# ACTIVE import, not raw containment. The paragraph above already says a fenced or
|
|
295
|
+
# prose occurrence loads nothing; the test did not implement that, so the necessary
|
|
296
|
+
# half of the authorization passed on text that imports nothing. The proof below is
|
|
297
|
+
# bound to the bundle's rev and not to this file, so an entry edited after a passing
|
|
298
|
+
# canary keeps a matching proof — which is exactly when the weak half decides.
|
|
299
|
+
if not load_collect().has_active_import(
|
|
300
|
+
entry.read_text(encoding="utf-8"), collect_central_import()):
|
|
253
301
|
return False
|
|
254
302
|
bundle = home / "central" / "bundle.md"
|
|
255
303
|
proof = (state_dir or STATE_DIR) / "activation.txt"
|
|
@@ -316,6 +364,15 @@ def migrate(home, host, promotions, in_bundle, collect, dry=False, corpus_loaded
|
|
|
316
364
|
# would strand the prose bullet forever after a jsonl-only success (Review F4).
|
|
317
365
|
if host == "codex":
|
|
318
366
|
prose_removed = prune_codex_prose(home, remove_ids, collect, dry)
|
|
367
|
+
if prose_removed is None:
|
|
368
|
+
# The malformed-marker contract is "touch nothing", and it used to hold for
|
|
369
|
+
# exactly one of the two representations. A record and the bullet keyed by it
|
|
370
|
+
# move together or not at all.
|
|
371
|
+
return {"removed": 0, "jsonl_removed": 0, "prose_removed": 0,
|
|
372
|
+
"kept_not_in_bundle": kept_not_in_bundle,
|
|
373
|
+
"kept_not_placed": kept_not_placed,
|
|
374
|
+
"kept_foreign_package": kept_foreign_pkg,
|
|
375
|
+
"skipped": "personal-region-malformed"}
|
|
319
376
|
else:
|
|
320
377
|
prose_removed = prune_claude_prose(home, remove_ids, dry)
|
|
321
378
|
jsonl_removed = prune_jsonl(jsonl, remove_ids, dry)
|
|
@@ -545,6 +602,37 @@ def _self_test():
|
|
|
545
602
|
(fenced / "CLAUDE.md").write_text("# CLAUDE.md\n```\n@central/bundle.md\n```\n", encoding="utf-8")
|
|
546
603
|
checks.append(("F2: import string with no activation proof -> KEEP",
|
|
547
604
|
claude_corpus_loaded(fenced, state_dir=fenced) is False))
|
|
605
|
+
# …and the same file WITH a matching proof, which is the case the line above cannot
|
|
606
|
+
# separate: it passes whether the import test is honest or not, because the missing
|
|
607
|
+
# proof decides it either way. The proof is bound to the bundle's rev and not to this
|
|
608
|
+
# file, so an entry edited after a passing canary keeps one — and then the import test
|
|
609
|
+
# is the only thing left standing between a fenced example and a delete.
|
|
610
|
+
(fenced / "central").mkdir(parents=True, exist_ok=True)
|
|
611
|
+
(fenced / "central" / "bundle.md").write_text(
|
|
612
|
+
"# bundle\nagent-bios-bundle-rev: deadbeef\n", encoding="utf-8")
|
|
613
|
+
(fenced / "activation.txt").write_text("agent-bios-bundle-rev: deadbeef\n", encoding="utf-8")
|
|
614
|
+
checks.append(("F2: FENCED import + matching proof -> still KEEP",
|
|
615
|
+
claude_corpus_loaded(fenced, state_dir=fenced) is False))
|
|
616
|
+
(fenced / "CLAUDE.md").write_text("# CLAUDE.md\n@central/bundle.md\n", encoding="utf-8")
|
|
617
|
+
checks.append(("F2: the same proof with a REAL import -> loaded",
|
|
618
|
+
claude_corpus_loaded(fenced, state_dir=fenced) is True))
|
|
619
|
+
|
|
620
|
+
# placed_here verifies a whole-file placement as a FILE and a bullet anchor as TEXT.
|
|
621
|
+
# They used to be OR'd, so any corpus document that merely NAMED a guide authorized
|
|
622
|
+
# deleting the personal copy of a learning placed in it — and the live manifest's one
|
|
623
|
+
# anchor is `tooling-gotchas.md`, which the entry file names in prose.
|
|
624
|
+
mention = pathlib.Path(tempfile.mkdtemp(prefix="migrate-anchor-"))
|
|
625
|
+
(mention / "guides").mkdir(parents=True)
|
|
626
|
+
(mention / "CLAUDE.md").write_text(
|
|
627
|
+
"# CLAUDE.md\nsee guides/tooling-gotchas.md for the traps\nthe bullet text itself\n",
|
|
628
|
+
encoding="utf-8")
|
|
629
|
+
checks.append(("anchor: a named-but-absent guide is NOT placed",
|
|
630
|
+
placed_here(mention, "tooling-gotchas.md") is False))
|
|
631
|
+
checks.append(("anchor: a phrase anchor still matches corpus TEXT",
|
|
632
|
+
placed_here(mention, "the bullet text itself") is True))
|
|
633
|
+
(mention / "guides" / "tooling-gotchas.md").write_text("# guide\n", encoding="utf-8")
|
|
634
|
+
checks.append(("anchor: the deployed guide file IS placed",
|
|
635
|
+
placed_here(mention, "tooling-gotchas.md") is True))
|
|
548
636
|
# (The old "full install is always loaded" case is gone with full mode: there is no shape
|
|
549
637
|
# whose corpus loads without the entry import, so nothing is exempt from the proof.)
|
|
550
638
|
|
|
@@ -557,14 +645,50 @@ def _self_test():
|
|
|
557
645
|
_, r_quoted = prune_bullets(twin, {"aaaa0000-0000-0000-0000-000000000000"})
|
|
558
646
|
checks.append(("F5: trailing id removed, quoted id ignored", r_real == 1 and r_quoted == 0))
|
|
559
647
|
|
|
560
|
-
# 7) malformed codex markers (END before START) -> no-op, no corruption.
|
|
648
|
+
# 7) malformed codex markers (END before START) -> no-op, no corruption. "No-op" is a
|
|
649
|
+
# claim about BOTH representations: reporting 0 removed said the same thing as
|
|
650
|
+
# "nothing matched", so migrate() went on to delete the durable jsonl record while
|
|
651
|
+
# the visible bullet stayed on screen. None is the signal that distinguishes them.
|
|
561
652
|
bad = pathlib.Path(tempfile.mkdtemp(prefix="migrate-badcodex-"))
|
|
653
|
+
(bad / "personal").mkdir(parents=True)
|
|
562
654
|
agents = bad / "AGENTS.md"
|
|
563
655
|
agents.write_text(f"x {collect.PERSONAL_END} y {collect.PERSONAL_START} z", encoding="utf-8")
|
|
564
656
|
before = agents.read_text(encoding="utf-8")
|
|
565
657
|
rem = prune_codex_prose(bad, {"anything"}, collect, dry=False)
|
|
566
658
|
checks.append(("malformed codex markers -> no-op",
|
|
567
|
-
rem
|
|
659
|
+
rem is None and agents.read_text(encoding="utf-8") == before))
|
|
660
|
+
bad_rec = rec(L["bb"], "builder-base")
|
|
661
|
+
(bad / "personal" / "learnings.jsonl").write_text(
|
|
662
|
+
json.dumps(bad_rec, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
663
|
+
s = migrate(bad, "codex", promos, make_in_bundle(ALL_DOMAINS), collect)
|
|
664
|
+
checks.append(("malformed markers -> the jsonl record survives too",
|
|
665
|
+
s.get("skipped") == "personal-region-malformed"
|
|
666
|
+
and s["removed"] == 0 and local_ids(bad) == {L["bb"]}))
|
|
667
|
+
# The control for that: a WELL-FORMED region under the same call really does delete,
|
|
668
|
+
# so the assertion above is not passing because migrate() stopped working.
|
|
669
|
+
ok_home = pathlib.Path(tempfile.mkdtemp(prefix="migrate-okcodex-"))
|
|
670
|
+
(ok_home / "personal").mkdir(parents=True)
|
|
671
|
+
(ok_home / "AGENTS.md").write_text(f"# AGENTS.md\n- {A['bb']}\n", encoding="utf-8")
|
|
672
|
+
with open(ok_home / "personal" / "learnings.jsonl", "w", encoding="utf-8") as f:
|
|
673
|
+
r = rec(L["bb"], "builder-base")
|
|
674
|
+
f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
|
675
|
+
collect.apply_codex(ok_home, collect.prose_bullet(r), dry=False)
|
|
676
|
+
s = migrate(ok_home, "codex", promos, make_in_bundle(ALL_DOMAINS), collect)
|
|
677
|
+
checks.append(("well-formed markers -> both representations go",
|
|
678
|
+
s.get("skipped") is None and s["removed"] == 1 and not local_ids(ok_home)))
|
|
679
|
+
|
|
680
|
+
# 8) "Manifest absent/empty -> no-op" covers an existing zero-byte file, which used to
|
|
681
|
+
# reach json.loads("") and raise out of the update path.
|
|
682
|
+
empty_dir = pathlib.Path(tempfile.mkdtemp(prefix="migrate-manifest-"))
|
|
683
|
+
empty = empty_dir / "promotions.json"
|
|
684
|
+
empty.write_text("", encoding="utf-8")
|
|
685
|
+
checks.append(("empty manifest file -> no-op", load_manifest(empty) == []))
|
|
686
|
+
(empty_dir / "whitespace.json").write_text(" \n\t\n", encoding="utf-8")
|
|
687
|
+
checks.append(("whitespace-only manifest -> no-op",
|
|
688
|
+
load_manifest(empty_dir / "whitespace.json") == []))
|
|
689
|
+
valid = empty_dir / "valid.json"
|
|
690
|
+
valid.write_text('{"version": 2, "promotions": [{"learning_id": "x"}]}', encoding="utf-8")
|
|
691
|
+
checks.append(("a real manifest still loads", len(load_manifest(valid)) == 1))
|
|
568
692
|
|
|
569
693
|
failed = [n for n, ok in checks if not ok]
|
|
570
694
|
if failed:
|
package/learn/redact.py
CHANGED
|
@@ -23,7 +23,14 @@ import sys
|
|
|
23
23
|
|
|
24
24
|
# Conservative: kill obvious secrets/identifiers, keep prose.
|
|
25
25
|
SECRET_RE = [
|
|
26
|
-
|
|
26
|
+
# The optional scheme word is what makes the HEADER form work. `\S+` alone stopped at
|
|
27
|
+
# `Bearer` and published the credential after it: `Authorization: Bearer <token>`
|
|
28
|
+
# redacted to `<REDACTED> <token>`, which is the single most common way a secret
|
|
29
|
+
# appears in a developer's terminal, and this floor is what stands between a captured
|
|
30
|
+
# learning and an upload. The `=` form was always fully covered — the two spellings
|
|
31
|
+
# of one header disagreeing is the defect, not the strictness.
|
|
32
|
+
re.compile(r'(?i)(api[_-]?key|secret|token|password|authorization|bearer)\s*[:=]\s*'
|
|
33
|
+
r'(?:(?:bearer|basic|digest|token)\s+)?\S+'),
|
|
27
34
|
re.compile(r'sk-[A-Za-z0-9_-]{16,}'),
|
|
28
35
|
re.compile(r'gh[pousr]_[A-Za-z0-9]{20,}'),
|
|
29
36
|
re.compile(r'eyJ[A-Za-z0-9_-]{20,}\.'), # JWT-ish
|
|
@@ -48,11 +55,13 @@ def _self_test():
|
|
|
48
55
|
any miss so the umbrella gate catches a regression in the floor."""
|
|
49
56
|
must_redact = [
|
|
50
57
|
("api_key value", "my api_key=sk_live_ABCDEF0123456789 leaked", "sk_live_"),
|
|
51
|
-
# key[:=]value form. NB the "Authorization: Bearer <token>" space/scheme
|
|
52
|
-
# form is only partially caught here (up to the scheme word) unless the
|
|
53
|
-
# token itself matches a shape below (JWT/sk-/ghp_/…) — a known floor
|
|
54
|
-
# boundary carried over from the heavy flow, not tightened in Phase 3.
|
|
55
58
|
("authorization value", "set authorization=topsecretvalue123 here", "topsecretvalue123"),
|
|
59
|
+
# Both spellings of the SAME header, because only one of them used to work: the
|
|
60
|
+
# `=` form was fully redacted while the wire form published its credential.
|
|
61
|
+
("authorization header form", "Authorization: Bearer opaque0123456789abcdef", "opaque0123456789abcdef"),
|
|
62
|
+
("authorization header lowercase", "authorization: bearer abcdef0123456789xyz", "abcdef0123456789xyz"),
|
|
63
|
+
("authorization header inside a command",
|
|
64
|
+
"curl -H 'Authorization: Bearer wire9876543210value'", "wire9876543210value"),
|
|
56
65
|
("openai key", "used sk-abcdefghij0123456789 here", "sk-abcdefghij"),
|
|
57
66
|
("github token", "token ghp_abcdefghij0123456789klmn committed", "ghp_"),
|
|
58
67
|
("aws key id", "key AKIA0123456789ABCD in env", "AKIA0123456789"),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bios",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"releaseDate": "2026-08-
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"releaseDate": "2026-08-18",
|
|
5
5
|
"description": "A thin, low-level instruction layer for LLM CLI agents: one set of principles and behavior whichever model you run. Deploys into $HOME by copy via an explicit `agent-bios install`.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"agent-bios": "install.sh"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"claude/guides/",
|
|
13
13
|
"claude/hooks/",
|
|
14
14
|
"claude/agents/",
|
|
15
|
+
"claude/skills/",
|
|
15
16
|
"codex/AGENTS.md",
|
|
16
17
|
"codex/guides/",
|
|
17
18
|
"codex/agents/",
|
package/provenance.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"bfcc56715fb031564e370ce236739e4208b7c513","committedAt":"2026-08-18T17:23:41+09:00","dirty":false}
|