agent-bios 0.15.0 → 0.16.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 +35 -12
- package/README.md +346 -31
- package/claude/CLAUDE.md +2 -2
- package/claude/agents/frontier.md +1 -1
- package/claude/agents/sweep.md +3 -3
- package/claude/agents/workhorse.md +2 -2
- package/claude/guides/claude-prompting.md +72 -39
- package/claude/guides/cli-multi-model-workflow.md +33 -15
- package/claude/guides/gpt-prompting.md +103 -39
- package/claude/guides/review-request.md +27 -0
- package/claude/guides/session-distill-workflow.md +54 -2
- package/claude/guides/slide-writing/RUNBOOK.md +137 -0
- package/claude/guides/slide-writing/scripts/pair.py +979 -0
- package/claude/guides/slide-writing/scripts/render.mjs +82 -0
- package/claude/guides/slide-writing.md +195 -0
- package/claude/guides/svg-visualization-guide.md +9 -0
- package/claude/guides/verification-discipline.md +5 -1
- package/claude/hooks/tooling-gotchas-hook.py +7 -5
- package/codex/AGENTS.md +2 -2
- package/codex/agents/frontier.toml +2 -1
- package/codex/agents/reviewer.toml +1 -1
- package/codex/agents/sweep.toml +3 -3
- package/codex/agents/workhorse.toml +1 -1
- package/codex/config-additions.toml +1 -1
- package/codex/guides/claude-prompting.md +72 -39
- package/codex/guides/cli-multi-model-workflow.md +33 -15
- package/codex/guides/gpt-prompting.md +103 -39
- package/codex/guides/review-request.md +27 -0
- package/codex/guides/session-distill-workflow.md +54 -2
- package/codex/guides/slide-writing/RUNBOOK.md +137 -0
- package/codex/guides/slide-writing/scripts/pair.py +979 -0
- package/codex/guides/slide-writing/scripts/render.mjs +82 -0
- package/codex/guides/slide-writing.md +195 -0
- package/codex/guides/svg-visualization-guide.md +9 -0
- package/codex/guides/verification-discipline.md +5 -1
- package/compose/assemble.py +290 -14
- package/compose/bootstrap/SKILL.md +119 -0
- package/compose/check-domains.py +102 -9
- package/compose/corpus-state.py +4 -0
- package/compose/corpus.py +387 -0
- package/compose/corpus_catalog.py +882 -0
- package/compose/corpus_install.py +1617 -0
- package/compose/corpus_session.py +726 -0
- package/compose/corpus_store.py +1414 -0
- package/compose/corpus_transaction.py +236 -0
- package/compose/corpus_ui.py +644 -0
- package/compose/domains.json +101 -100
- package/install.sh +63 -18
- package/launch/agent-launch.py +1216 -179
- package/launch/agent-launch.toml +12 -16
- package/launch/i18n/en.toml +112 -7
- package/launch/i18n/ja.toml +112 -7
- package/launch/i18n/ko.toml +112 -7
- package/learn/collect-learning.py +46 -19
- package/learn/migrate-learnings.py +10 -1
- package/package.json +11 -3
- package/provenance.json +1 -1
- package/session-cost.py +22 -2
- package/wrappers/codex-helm.sh +3 -3
package/compose/assemble.py
CHANGED
|
@@ -206,6 +206,96 @@ def author_only(path):
|
|
|
206
206
|
for ln in front.splitlines() if ln.startswith("audience:"))
|
|
207
207
|
|
|
208
208
|
|
|
209
|
+
class GuideMemberError(ValueError):
|
|
210
|
+
"""A guide source tree does not have the primary-plus-companions shape."""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _guide_primaries(root):
|
|
214
|
+
"""Return the top-level primary Markdown files after validating `root`.
|
|
215
|
+
|
|
216
|
+
A guide is one manifest item even when it has resources. Its primary remains the
|
|
217
|
+
top-level ``<name>.md`` and its optional resource tree is exactly ``<name>/``. This
|
|
218
|
+
small structural contract lets delivery and every author-side consumer agree on the
|
|
219
|
+
member set without adding a second manifest entry for each resource.
|
|
220
|
+
|
|
221
|
+
The source is shipped verbatim for non-Markdown resources, so accepting a symlink
|
|
222
|
+
(or a FIFO/device) would make the shipped package's bytes depend on the checkout at
|
|
223
|
+
copy time. Refuse those shapes before a caller starts copying anything.
|
|
224
|
+
"""
|
|
225
|
+
if root.is_symlink() or not root.is_dir():
|
|
226
|
+
raise GuideMemberError(f"guide root must be a real directory: {root}")
|
|
227
|
+
primaries = []
|
|
228
|
+
entries = sorted(root.iterdir(), key=lambda p: p.name)
|
|
229
|
+
for path in entries:
|
|
230
|
+
if path.is_symlink():
|
|
231
|
+
raise GuideMemberError(f"guide source must not contain symlinks: {path}")
|
|
232
|
+
if path.is_file():
|
|
233
|
+
if path.suffix != ".md":
|
|
234
|
+
raise GuideMemberError(
|
|
235
|
+
f"unsupported top-level guide resource {path}; companion resources belong "
|
|
236
|
+
f"under a same-stem directory")
|
|
237
|
+
primaries.append(path.name)
|
|
238
|
+
continue
|
|
239
|
+
if path.is_dir():
|
|
240
|
+
primary = root / f"{path.name}.md"
|
|
241
|
+
if not primary.is_file() or primary.is_symlink():
|
|
242
|
+
raise GuideMemberError(
|
|
243
|
+
f"orphan guide companion tree {path}; expected primary {primary.name}")
|
|
244
|
+
continue
|
|
245
|
+
raise GuideMemberError(f"unsupported guide source path: {path}")
|
|
246
|
+
if not primaries:
|
|
247
|
+
raise GuideMemberError(f"guide root has no primary Markdown files: {root}")
|
|
248
|
+
return primaries
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _tree_members(root):
|
|
252
|
+
"""Return regular-file members under `root`, refusing every other entry type."""
|
|
253
|
+
out = []
|
|
254
|
+
for path in sorted(root.iterdir(), key=lambda p: p.name):
|
|
255
|
+
if path.is_symlink():
|
|
256
|
+
raise GuideMemberError(f"guide companion tree must not contain symlinks: {path}")
|
|
257
|
+
if path.is_file():
|
|
258
|
+
out.append(path)
|
|
259
|
+
elif path.is_dir():
|
|
260
|
+
children = _tree_members(path)
|
|
261
|
+
if not children:
|
|
262
|
+
raise GuideMemberError(f"empty guide companion directory: {path}")
|
|
263
|
+
out.extend(children)
|
|
264
|
+
else:
|
|
265
|
+
raise GuideMemberError(f"unsupported guide companion path: {path}")
|
|
266
|
+
return out
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def guide_members(root: pathlib.Path, name: str) -> list[str]:
|
|
270
|
+
"""Return one guide item's source-relative files in deterministic order.
|
|
271
|
+
|
|
272
|
+
``name`` is the manifest's existing top-level ``*.md`` key. The primary is first;
|
|
273
|
+
optional companions live below ``Path(name).stem/`` and retain their relative path.
|
|
274
|
+
Validating all top-level entries on every call intentionally catches an orphan companion
|
|
275
|
+
tree even when the caller happens to ask about a different guide.
|
|
276
|
+
"""
|
|
277
|
+
root = pathlib.Path(root)
|
|
278
|
+
primaries = _guide_primaries(root)
|
|
279
|
+
if name not in primaries or pathlib.PurePath(name).name != name or not name.endswith(".md"):
|
|
280
|
+
raise GuideMemberError(f"guide primary {name!r} is not a top-level Markdown file in {root}")
|
|
281
|
+
members = [name]
|
|
282
|
+
companion = root / pathlib.Path(name).stem
|
|
283
|
+
if companion.exists():
|
|
284
|
+
if companion.is_symlink() or not companion.is_dir():
|
|
285
|
+
raise GuideMemberError(f"guide companion {companion} must be a real directory")
|
|
286
|
+
companion_members = _tree_members(companion)
|
|
287
|
+
if not companion_members:
|
|
288
|
+
raise GuideMemberError(f"empty guide companion tree: {companion}")
|
|
289
|
+
members.extend(str(path.relative_to(root).as_posix()) for path in companion_members)
|
|
290
|
+
return members
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def guide_member_map(root: pathlib.Path) -> dict[str, list[str]]:
|
|
294
|
+
"""Every guide's exact source-relative member paths, keyed by its manifest key."""
|
|
295
|
+
root = pathlib.Path(root)
|
|
296
|
+
return {name: guide_members(root, name) for name in _guide_primaries(root)}
|
|
297
|
+
|
|
298
|
+
|
|
209
299
|
def copy_filtered(src_dir, names, dest, rewrite=None, dry=False, backup=None):
|
|
210
300
|
"""Write the selected files, and remove the ones we deployed and no longer select.
|
|
211
301
|
|
|
@@ -260,6 +350,160 @@ def copy_filtered(src_dir, names, dest, rewrite=None, dry=False, backup=None):
|
|
|
260
350
|
target.write_text(body, encoding="utf-8")
|
|
261
351
|
|
|
262
352
|
|
|
353
|
+
def _backup_member(path, backup, reason):
|
|
354
|
+
"""Keep a recoverable copy of one exact deployed member before changing it."""
|
|
355
|
+
if backup is None:
|
|
356
|
+
return
|
|
357
|
+
kept = backup / reason / str(path).lstrip("/")
|
|
358
|
+
kept.parent.mkdir(parents=True, exist_ok=True)
|
|
359
|
+
shutil.copy2(path, kept)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def validate_guide_destination_members(dest, members):
|
|
363
|
+
"""Refuse member paths that would traverse a symlink below the chosen guide root.
|
|
364
|
+
|
|
365
|
+
``dest`` itself is deliberately outside this check: a user may configure the whole
|
|
366
|
+
guides root as a symlink, and that pre-existing top-level behavior is not claimed by a
|
|
367
|
+
bundle. A same-stem companion directory below it is different — following it would let
|
|
368
|
+
an install replace or deselect a file outside the configured root. Validate every
|
|
369
|
+
source-owned member up front, including currently deselected ones, so no earlier member
|
|
370
|
+
can be changed before a later nested path reveals the unsafe shape.
|
|
371
|
+
"""
|
|
372
|
+
dest = pathlib.Path(dest)
|
|
373
|
+
for rel in sorted(set(members)):
|
|
374
|
+
target = dest / rel
|
|
375
|
+
if target.is_symlink():
|
|
376
|
+
die(f"refusing guide member destination symlink: {target}")
|
|
377
|
+
ancestor = target.parent
|
|
378
|
+
while ancestor != dest:
|
|
379
|
+
if ancestor.is_symlink():
|
|
380
|
+
die(f"refusing guide member ancestor symlink: {ancestor}")
|
|
381
|
+
if ancestor.exists() and not ancestor.is_dir():
|
|
382
|
+
die(f"guide member ancestor is not a directory: {ancestor}")
|
|
383
|
+
ancestor = ancestor.parent
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def prior_guide_candidates(prior_deployed, dest, current_members):
|
|
387
|
+
"""Former guide paths a prior manifest names but the current source does not.
|
|
388
|
+
|
|
389
|
+
Historic manifests over-claimed guide directories, so a prior row is not deletion
|
|
390
|
+
authority. This parser only bounds the warning to the configured guide root; current
|
|
391
|
+
source members retain the existing source-derived cleanup rule in ``copy_guide_members``.
|
|
392
|
+
"""
|
|
393
|
+
dest = pathlib.Path(dest)
|
|
394
|
+
current = set(current_members)
|
|
395
|
+
candidates = set()
|
|
396
|
+
for raw in prior_deployed:
|
|
397
|
+
if not raw or "\0" in raw:
|
|
398
|
+
continue
|
|
399
|
+
try:
|
|
400
|
+
path = pathlib.Path(raw)
|
|
401
|
+
except (TypeError, ValueError):
|
|
402
|
+
continue
|
|
403
|
+
if not path.is_absolute():
|
|
404
|
+
continue
|
|
405
|
+
try:
|
|
406
|
+
rel = path.relative_to(dest)
|
|
407
|
+
except ValueError:
|
|
408
|
+
continue
|
|
409
|
+
# `relative_to` is lexical and preserves `..`; never allow a handcrafted manifest
|
|
410
|
+
# row to escape the configured root when the target path is joined below.
|
|
411
|
+
if not rel.parts or any(part in ("", ".", "..") for part in rel.parts):
|
|
412
|
+
continue
|
|
413
|
+
rel_text = rel.as_posix()
|
|
414
|
+
if rel_text not in current:
|
|
415
|
+
candidates.add(rel_text)
|
|
416
|
+
return sorted(candidates)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def disclose_prior_guide_remnants(dest, candidates, dry=False):
|
|
420
|
+
"""Report, but never alter, prior-only guide paths that remain on disk.
|
|
421
|
+
|
|
422
|
+
The existing installer keeps the previous manifest as its recovery record. A user can
|
|
423
|
+
inspect that record and remove a remnant manually if it is known to be old product data;
|
|
424
|
+
this assembler cannot safely distinguish that case from a user-created same-named file.
|
|
425
|
+
"""
|
|
426
|
+
dest = pathlib.Path(dest)
|
|
427
|
+
for rel in sorted(set(candidates)):
|
|
428
|
+
target = dest / rel
|
|
429
|
+
# Inspect existence only. A link is also a remnant for manual review; never open
|
|
430
|
+
# or modify its target as part of this disclosure.
|
|
431
|
+
if not target.exists() and not target.is_symlink():
|
|
432
|
+
continue
|
|
433
|
+
print(f" {'[dry] ' if dry else ''}prior guide remnant left in place: {target}")
|
|
434
|
+
print(" inspect it and remove it manually if it is an obsolete agent-bios resource")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def copy_guide_members(src_dir, names, dest, rewrite=None, dry=False, backup=None):
|
|
438
|
+
"""Copy selected guide bundles and reconcile only their exact source members.
|
|
439
|
+
|
|
440
|
+
A companion directory is not an ownership boundary: it may contain a file a user added
|
|
441
|
+
after installation. Membership is therefore resolved from the source tree and every
|
|
442
|
+
replace/deselect action is addressed to one known file path. Empty directories are left
|
|
443
|
+
behind deliberately; an installer can remove them with ``rmdir`` only after all manifest
|
|
444
|
+
owned members are gone.
|
|
445
|
+
|
|
446
|
+
Markdown is rewritten only where the old file-only guide path already was. Other regular
|
|
447
|
+
files are copied byte-for-byte (and with their mode) so scripts and render assets remain
|
|
448
|
+
one source of truth rather than becoming text projections.
|
|
449
|
+
"""
|
|
450
|
+
src_dir = pathlib.Path(src_dir)
|
|
451
|
+
dest = pathlib.Path(dest)
|
|
452
|
+
all_members = guide_member_map(src_dir)
|
|
453
|
+
unknown = set(names) - set(all_members)
|
|
454
|
+
if unknown:
|
|
455
|
+
die(f"selected guide(s) not found in source tree: {sorted(unknown)}")
|
|
456
|
+
owned = {member for members in all_members.values() for member in members}
|
|
457
|
+
selected = {member for name in names for member in all_members[name]}
|
|
458
|
+
validate_guide_destination_members(dest, owned)
|
|
459
|
+
if not dry:
|
|
460
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
461
|
+
|
|
462
|
+
# Deselecting a guide means deleting files, never a same-named directory wholesale.
|
|
463
|
+
# The latter would claim a user's nested note merely because it shares our guide stem.
|
|
464
|
+
for rel in sorted(owned - selected):
|
|
465
|
+
target = dest / rel
|
|
466
|
+
if not target.is_file():
|
|
467
|
+
continue
|
|
468
|
+
print(f" {'[dry] ' if dry else ''}deselected, removed {target}")
|
|
469
|
+
if dry:
|
|
470
|
+
continue
|
|
471
|
+
_backup_member(target, backup, "deselected")
|
|
472
|
+
target.unlink()
|
|
473
|
+
|
|
474
|
+
for rel in sorted(selected):
|
|
475
|
+
source = src_dir / rel
|
|
476
|
+
target = dest / rel
|
|
477
|
+
if dry:
|
|
478
|
+
print(f" [dry] copy {rel} -> {dest}")
|
|
479
|
+
continue
|
|
480
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
481
|
+
if source.suffix == ".md":
|
|
482
|
+
body = source.read_text(encoding="utf-8")
|
|
483
|
+
if rewrite:
|
|
484
|
+
body = body.replace(*rewrite)
|
|
485
|
+
changed = True
|
|
486
|
+
if target.is_file():
|
|
487
|
+
try:
|
|
488
|
+
changed = target.read_text(encoding="utf-8") != body
|
|
489
|
+
except (OSError, UnicodeDecodeError):
|
|
490
|
+
changed = True
|
|
491
|
+
if changed and target.is_file():
|
|
492
|
+
_backup_member(target, backup, "replaced")
|
|
493
|
+
if changed:
|
|
494
|
+
target.write_text(body, encoding="utf-8")
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
# Read the bytes directly. `filecmp` caches stat signatures, which can report a
|
|
498
|
+
# same-size rewrite clean on filesystems whose mtime granularity is coarse; source
|
|
499
|
+
# code is small and byte identity is the contract here.
|
|
500
|
+
changed = not target.is_file() or source.read_bytes() != target.read_bytes()
|
|
501
|
+
if changed and target.is_file():
|
|
502
|
+
_backup_member(target, backup, "replaced")
|
|
503
|
+
if changed:
|
|
504
|
+
shutil.copy2(source, target)
|
|
505
|
+
|
|
506
|
+
|
|
263
507
|
def replace_atomically(path, text):
|
|
264
508
|
"""Write via a temp file + os.replace, so a write that fails partway cannot truncate.
|
|
265
509
|
|
|
@@ -610,15 +854,46 @@ def main():
|
|
|
610
854
|
if codex_only:
|
|
611
855
|
codex_bundle = place_codex_only(codex_bundle, codex_only)
|
|
612
856
|
|
|
857
|
+
guide_source = REPO / "claude" / "guides"
|
|
858
|
+
guide_sources = guide_member_map(guide_source)
|
|
859
|
+
codex_guide_sources = guide_member_map(REPO / "codex" / "guides")
|
|
860
|
+
if set(guide_sources) != set(codex_guide_sources):
|
|
861
|
+
die("Claude and Codex guide primary sets differ; regenerate mirrors before assembling")
|
|
862
|
+
for name, members in guide_sources.items():
|
|
863
|
+
if members != codex_guide_sources[name]:
|
|
864
|
+
die(f"Claude and Codex guide members differ for {name}; regenerate mirrors before assembling")
|
|
865
|
+
prior_deployed = set()
|
|
866
|
+
if args.prior_manifest:
|
|
867
|
+
prior = pathlib.Path(args.prior_manifest)
|
|
868
|
+
if prior.is_file():
|
|
869
|
+
prior_deployed = {str(pathlib.Path(ln.strip())) for ln in
|
|
870
|
+
prior.read_text(encoding="utf-8").splitlines() if ln.strip()}
|
|
871
|
+
current_guide_members = [member for members in guide_sources.values() for member in members]
|
|
872
|
+
current_codex_guide_members = [member for members in codex_guide_sources.values() for member in members]
|
|
873
|
+
prior_claude_candidates = prior_guide_candidates(
|
|
874
|
+
prior_deployed, claude_dir / "central" / "guides", current_guide_members)
|
|
875
|
+
prior_codex_candidates = prior_guide_candidates(
|
|
876
|
+
prior_deployed, codex_dir / "guides", current_codex_guide_members)
|
|
877
|
+
# Do this before withheld cleanup, bundle.md, or any guide copy. A companion ancestor
|
|
878
|
+
# can be a symlink even when its leaf is an ordinary file; checking leaf targets only
|
|
879
|
+
# would then write outside the selected guides root before the unsafe path was noticed.
|
|
880
|
+
validate_guide_destination_members(
|
|
881
|
+
claude_dir / "central" / "guides",
|
|
882
|
+
current_guide_members,
|
|
883
|
+
)
|
|
884
|
+
validate_guide_destination_members(
|
|
885
|
+
codex_dir / "guides",
|
|
886
|
+
current_codex_guide_members,
|
|
887
|
+
)
|
|
613
888
|
guides = filtered_files(manifest, "guides", selection)
|
|
614
|
-
withheld = [n for n in guides if author_only(
|
|
889
|
+
withheld = [n for n in guides if author_only(guide_source / n)]
|
|
615
890
|
guides = [n for n in guides if n not in withheld]
|
|
616
891
|
# Not writing it is not enough for anyone who installed before this rule: the
|
|
617
892
|
# manifest is rebuilt from the current deploy, so a file that stops being
|
|
618
893
|
# deployed stops being tracked and would sit there for good.
|
|
619
|
-
stale = [d /
|
|
894
|
+
stale = [d / member for n in withheld for member in guide_sources[n]
|
|
620
895
|
for d in (claude_dir / "central" / "guides", codex_dir / "guides")
|
|
621
|
-
if (d /
|
|
896
|
+
if (d / member).is_file()]
|
|
622
897
|
# Copy before removing, the way install.sh backs up a file it replaces. The
|
|
623
898
|
# name matching ours does not prove we wrote it — a shared or symlinked guides
|
|
624
899
|
# directory can hold somebody's own file under the same name, and a deleted
|
|
@@ -627,6 +902,13 @@ def main():
|
|
|
627
902
|
# `withheld` is an audience decision, `deselected` is a selection change.
|
|
628
903
|
run_backup = state_dir / "backups" / time.strftime("%Y%m%d-%H%M%S")
|
|
629
904
|
backup = run_backup / "withheld"
|
|
905
|
+
# A source release can remove one resource while retaining its primary guide. Prior
|
|
906
|
+
# manifests have historically included user files below these roots, so they disclose
|
|
907
|
+
# candidates only; the source-derived member list remains the sole deletion authority.
|
|
908
|
+
disclose_prior_guide_remnants(claude_dir / "central" / "guides", prior_claude_candidates,
|
|
909
|
+
dry=args.dry_run)
|
|
910
|
+
disclose_prior_guide_remnants(codex_dir / "guides", prior_codex_candidates,
|
|
911
|
+
dry=args.dry_run)
|
|
630
912
|
for path in stale:
|
|
631
913
|
print(f" {'[dry] ' if args.dry_run else ''}remove withheld {path}")
|
|
632
914
|
if not args.dry_run:
|
|
@@ -646,25 +928,19 @@ def main():
|
|
|
646
928
|
else:
|
|
647
929
|
central.mkdir(parents=True, exist_ok=True)
|
|
648
930
|
(central / "bundle.md").write_text(bundle, encoding="utf-8")
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
931
|
+
copy_guide_members(guide_source, guides, central / "guides",
|
|
932
|
+
rewrite=(f"{CLAUDE_VAR}/guides/", f"{CLAUDE_VAR}/central/guides/"),
|
|
933
|
+
dry=dry, backup=run_backup)
|
|
652
934
|
copy_filtered(REPO / "claude" / "hooks", hooks, central / "hooks", dry=dry, backup=run_backup)
|
|
653
935
|
copy_filtered(REPO / "claude" / "agents", agents, central / "agents", dry=dry,
|
|
654
936
|
backup=run_backup)
|
|
655
937
|
merge_settings(claude_dir, hooks, REPO / "claude" / "settings.template.json", dry=dry,
|
|
656
938
|
owned_names=manifest.get("hooks", {})) # deselected hooks must drop too
|
|
657
|
-
prior_deployed = set()
|
|
658
|
-
if args.prior_manifest:
|
|
659
|
-
prior = pathlib.Path(args.prior_manifest)
|
|
660
|
-
if prior.is_file():
|
|
661
|
-
prior_deployed = {str(pathlib.Path(ln.strip())) for ln in
|
|
662
|
-
prior.read_text(encoding="utf-8").splitlines() if ln.strip()}
|
|
663
939
|
entry_state = seed_entry(claude_dir, monolith, prior_deployed, dry=dry)
|
|
664
940
|
|
|
665
941
|
merge_codex(codex_dir, codex_bundle, prior_deployed, dry=dry)
|
|
666
|
-
|
|
667
|
-
|
|
942
|
+
copy_guide_members(REPO / "codex" / "guides", guides, codex_dir / "guides", dry=dry,
|
|
943
|
+
backup=run_backup)
|
|
668
944
|
|
|
669
945
|
if not dry:
|
|
670
946
|
state_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: corpus
|
|
3
|
+
description: Inspect or change the private agent-bios corpus used by activated sessions, including item creation, edits, consumption placement, removal, restore, recovery, selection, reset, rollback, history, and current-vs-pinned explanation. Use for requests about the user's agent-bios instructions or personal learnings; it does not alter the running session or native global files.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Corpus management
|
|
7
|
+
|
|
8
|
+
Use the deterministic `agent-bios corpus` client. In an activated launch, invoke
|
|
9
|
+
it as `bash "$AGENT_BIOS_PACKAGE_ROOT/install.sh" corpus` (the examples below use
|
|
10
|
+
the shorter command name). The launcher supplies that package path, so a checkout
|
|
11
|
+
installation does not accidentally call an older global npm CLI. If the variable
|
|
12
|
+
is absent, resolve the installed `agent-bios` command before using the examples.
|
|
13
|
+
It reads and writes private
|
|
14
|
+
agent-bios state; it never edits native global `AGENTS.md` / `CLAUDE.md`, host
|
|
15
|
+
discovery directories, or the current conversation.
|
|
16
|
+
|
|
17
|
+
## Establish the view
|
|
18
|
+
|
|
19
|
+
Start with:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
agent-bios corpus status --json
|
|
23
|
+
agent-bios corpus list --json
|
|
24
|
+
agent-bios corpus show '<CorpusRef>' --view effective --json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The running session is pinned to the `ContentRef` stated by its launch contract.
|
|
28
|
+
`status`, `list`, and `show` describe current authoring unless a command explicitly
|
|
29
|
+
says otherwise. A mutation changes future activated launches only; it does not
|
|
30
|
+
rewrite this session's pin. `snapshot --host codex|claude` composes current
|
|
31
|
+
authoring and returns stored snapshot evidence—it does not prove a host loaded it.
|
|
32
|
+
Use `snapshot --content-ref '<ContentRef>' --json` to inspect the exact immutable
|
|
33
|
+
items and instruction text of the running or resumed session, independently of
|
|
34
|
+
current authoring.
|
|
35
|
+
|
|
36
|
+
Use `show --view installed|change|diff|history` to compare the selected installed
|
|
37
|
+
baseline, the personal layer, and effective authoring. Use `history [CorpusRef]
|
|
38
|
+
--json` for recoverable revisions.
|
|
39
|
+
|
|
40
|
+
## Plan, preview, then apply
|
|
41
|
+
|
|
42
|
+
Send exactly one semantic operation as JSON. The runtime owns ids, paths, digests,
|
|
43
|
+
timestamps, serialization, revision calculation, and publication. Never invent or
|
|
44
|
+
edit those values. For writes, preserve the `digest` returned by `list` as
|
|
45
|
+
`item_digest`; preserve the plan's `expected_revision` when applying it.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
agent-bios corpus plan --input request.json --json
|
|
49
|
+
agent-bios corpus apply '<plan_id>' --expected-revision '<expected_revision>' --json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Supported payloads:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{"operation":"create","item":{"title":"Review releases","body":"...","surface":"requested","tier":"env-personal","domains":["personal"],"kind":"rule"}}
|
|
56
|
+
{"operation":"update","ref":"@scope/name:item-id","item_digest":"<digest>","patch":{"body":"...","surface":"relevant"}}
|
|
57
|
+
{"operation":"remove","ref":"@scope/name:item-id","item_digest":"<digest>"}
|
|
58
|
+
{"operation":"restore","ref":"@scope/name:item-id"}
|
|
59
|
+
{"operation":"recover","ref":"@local/personal:item-id"}
|
|
60
|
+
{"operation":"select","selection":["@scope/name/domain"]}
|
|
61
|
+
{"operation":"reset"}
|
|
62
|
+
{"operation":"rollback","baseline_ref":"<baseline_ref>"}
|
|
63
|
+
{"operation":"rollback","history_id":"<history_id>"}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For a body edit, submit `body`; the manager updates the item's `primary_member`
|
|
67
|
+
and derives every content view from that file. For a multi-file edit, submit
|
|
68
|
+
`members` retaining unchanged files. A conflicting simultaneous body/member edit
|
|
69
|
+
is refused. If `content_conflict` is shown, compare the preserved body and members
|
|
70
|
+
with the user; submit an explicit primary-member/content choice rather than
|
|
71
|
+
silently choosing a version. Personal IDs are issued once by the plan and remain
|
|
72
|
+
stable on retry; create payloads do not supply `item_id` or `ref`.
|
|
73
|
+
Do not turn a prose edit into an executable asset, hook,
|
|
74
|
+
permission, tool, or capability change. `always`, `relevant`, and `requested`
|
|
75
|
+
mean always in an activated selection, trigger-routed, and explicitly requested.
|
|
76
|
+
For existing Claude hook items, `hook: {"event":"PreToolUse","matcher":"Bash"}`
|
|
77
|
+
edits the binding through the same plan/apply path. Preserve the installed source
|
|
78
|
+
carrier and native agent frontmatter, including tool restrictions. Changing an
|
|
79
|
+
ordinary prose item's kind/surface does not turn it into executable hook code.
|
|
80
|
+
|
|
81
|
+
Native hook execution and corpus-agent registration are off by default. The user
|
|
82
|
+
opts in per launch with `agent-launch --corpus-native`; `snapshot --host claude
|
|
83
|
+
--native --json` previews/composes that selection without activating a host.
|
|
84
|
+
The snapshot supplies session-only local plugins; it does not install them into
|
|
85
|
+
global discovery. Check `unavailable` for unsupported carriers or hosts. Native
|
|
86
|
+
corpus agent names are qualified by their item plugin, distinct from the launcher's
|
|
87
|
+
bare tier names. Removing/restoring one item changes only future snapshots.
|
|
88
|
+
|
|
89
|
+
Show the plan and its consequences before Apply when the user has not already
|
|
90
|
+
approved that exact mutation. A stale revision or digest requires a fresh read and
|
|
91
|
+
new plan; never retry by dropping the revision check. Report after Apply that the
|
|
92
|
+
current session is unchanged and a new activated launch is needed.
|
|
93
|
+
|
|
94
|
+
## Restore, recover, and reset
|
|
95
|
+
|
|
96
|
+
- `restore` removes an installed item's personal override/tombstone and reveals
|
|
97
|
+
the version in the selected baseline for future snapshots.
|
|
98
|
+
- `recover` reactivates a personal item or host learning from private history. It is not installed
|
|
99
|
+
restore.
|
|
100
|
+
- `reset` returns future authoring and selection to the last successful installed
|
|
101
|
+
tuple while preserving sources, history, and existing session pins.
|
|
102
|
+
- For all product settings as well, `agent-bios reset` previews the effect;
|
|
103
|
+
`agent-bios reset --apply --yes --expected-revision '<preview revision>'` also
|
|
104
|
+
resets launcher overrides and disables configured ingestion. It does not reset
|
|
105
|
+
the native host's settings. Retry the accepted revision to finish an interrupted
|
|
106
|
+
reset; if state has changed, show a fresh preview and obtain approval for its
|
|
107
|
+
revision. Token bytes are never archived or restored.
|
|
108
|
+
- `rollback` selects a validated baseline or replays one history record for future
|
|
109
|
+
snapshots; it never rewrites a running or resumable session.
|
|
110
|
+
|
|
111
|
+
## Personal learnings
|
|
112
|
+
|
|
113
|
+
Claude and Codex learning sources are separate, host-qualified local authorities.
|
|
114
|
+
Captured events and their `learning_id` identify immutable captured bytes. Editing
|
|
115
|
+
their corpus presentation creates a local overlay for future snapshots; it does
|
|
116
|
+
not change an already uploaded record. Uploading revised wording is a new explicit
|
|
117
|
+
capture with a new id and provenance link. Never claim that local remove, reset,
|
|
118
|
+
promotion, or purge deleted an uploaded learning unless a real remote delete
|
|
119
|
+
protocol reports that outcome.
|
package/compose/check-domains.py
CHANGED
|
@@ -40,7 +40,12 @@ import sys
|
|
|
40
40
|
|
|
41
41
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
|
42
42
|
import pkgid # noqa: E402 (sibling module in compose/)
|
|
43
|
-
from assemble import
|
|
43
|
+
from assemble import ( # noqa: E402 the shipped structural/audience/ownership readers
|
|
44
|
+
GuideMemberError,
|
|
45
|
+
author_only,
|
|
46
|
+
guide_member_map,
|
|
47
|
+
hook_command_matches,
|
|
48
|
+
)
|
|
44
49
|
|
|
45
50
|
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
46
51
|
MANIFEST = REPO / "compose" / "domains.json"
|
|
@@ -62,6 +67,11 @@ def units_on_disk(root, glob, key):
|
|
|
62
67
|
directories that carry SKILL.md. Returns (names, malformed) — a skills subdirectory
|
|
63
68
|
without the marker is neither claimable nor ignorable."""
|
|
64
69
|
names, malformed = set(), []
|
|
70
|
+
if key == "guides":
|
|
71
|
+
try:
|
|
72
|
+
return set(guide_member_map(root)), malformed
|
|
73
|
+
except GuideMemberError as exc:
|
|
74
|
+
return names, [str(exc)]
|
|
65
75
|
for p in root.glob(glob):
|
|
66
76
|
if key == "skills":
|
|
67
77
|
if p.is_dir():
|
|
@@ -291,8 +301,11 @@ def run_gate(manifest, bullets, repo=REPO):
|
|
|
291
301
|
if not on_disk:
|
|
292
302
|
errors.append(f"non-vacuity: no files on disk under {rel}")
|
|
293
303
|
for name in malformed:
|
|
294
|
-
|
|
295
|
-
|
|
304
|
+
if key == "skills":
|
|
305
|
+
errors.append(f"{key}: directory {name} in {rel} carries no {SKILL_MARKER} — "
|
|
306
|
+
f"not a skill the hosts can load, and not a name the manifest can claim")
|
|
307
|
+
else:
|
|
308
|
+
errors.append(f"{key}: invalid bundle source shape — {name}")
|
|
296
309
|
for name, entry in section.items():
|
|
297
310
|
check_registry(entry, f"{key}/{name}")
|
|
298
311
|
if name not in on_disk:
|
|
@@ -308,6 +321,22 @@ def run_gate(manifest, bullets, repo=REPO):
|
|
|
308
321
|
# handle is only as good as its declaration: this catches phrasings someone wrote down, not
|
|
309
322
|
# every paraphrase.
|
|
310
323
|
guide_aud = {n: audience(e, errors, f"guides/{n}") for n, e in entries["guides"].items()}
|
|
324
|
+
guide_dir = repo / "claude" / "guides"
|
|
325
|
+
try:
|
|
326
|
+
guide_sources = guide_member_map(guide_dir)
|
|
327
|
+
except GuideMemberError:
|
|
328
|
+
# Rule 3 already reports the concrete shape error. Keep the router checks from
|
|
329
|
+
# raising so one malformed companion cannot hide the rest of the manifest errors.
|
|
330
|
+
guide_sources = {}
|
|
331
|
+
for name, members in guide_sources.items():
|
|
332
|
+
primary_is_author_only = author_only(guide_dir / name)
|
|
333
|
+
for member in members[1:]:
|
|
334
|
+
path = guide_dir / member
|
|
335
|
+
if path.suffix == ".md" and author_only(path) and not primary_is_author_only:
|
|
336
|
+
errors.append(
|
|
337
|
+
f"guides/{name}: companion {member} declares audience: author but its "
|
|
338
|
+
f"primary is delivered — author-only content cannot ride a public guide bundle"
|
|
339
|
+
)
|
|
311
340
|
# A declared handle is a NAME, and a name resolving to two guides routes the reader
|
|
312
341
|
# nowhere: refs_from() would credit both targets from one prose router, falsely
|
|
313
342
|
# rooting and co-packaging a guide the sentence never meant. Duplicates fail here
|
|
@@ -380,7 +409,6 @@ def run_gate(manifest, bullets, repo=REPO):
|
|
|
380
409
|
# exemption: a corpus bullet (by path or declared handle), a parent guide citing a child
|
|
381
410
|
# as a depth chain, and a launch preset's mission. An exemption list would be the fourth,
|
|
382
411
|
# and it is the one that lets a genuinely orphaned guide through.
|
|
383
|
-
guide_dir = repo / "claude" / "guides"
|
|
384
412
|
launch = repo / "launch" / "agent-launch.toml"
|
|
385
413
|
launch_text, launch_missions = "", []
|
|
386
414
|
if launch.is_file():
|
|
@@ -540,8 +568,13 @@ def run_gate(manifest, bullets, repo=REPO):
|
|
|
540
568
|
prose = re.sub(r"<!--.*?-->", "", prose, flags=re.S)
|
|
541
569
|
return re.sub(r"<!--.*", "", prose, flags=re.S)
|
|
542
570
|
|
|
543
|
-
bodies = {
|
|
544
|
-
|
|
571
|
+
bodies = {
|
|
572
|
+
name: "\n".join(
|
|
573
|
+
_defenced((guide_dir / member).read_text(encoding="utf-8"))
|
|
574
|
+
for member in members if member.endswith(".md")
|
|
575
|
+
)
|
|
576
|
+
for name, members in guide_sources.items()
|
|
577
|
+
}
|
|
545
578
|
if not bodies:
|
|
546
579
|
errors.append("non-vacuity: no guide bodies read, so consumers were not checked")
|
|
547
580
|
# Reachability is TRANSITIVE FROM ROOTS (bullets and launch missions), not "any
|
|
@@ -617,9 +650,10 @@ def run_gate(manifest, bullets, repo=REPO):
|
|
|
617
650
|
sizes[key] = sizes.get(key, 0) + len(hit) // 4
|
|
618
651
|
for name, entry in entries["guides"].items():
|
|
619
652
|
key = entry.get("tier") if entry.get("tier") != "domain" else ",".join(entry.get("domains", ["?"])[:1])
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
653
|
+
for member in guide_sources.get(name, []):
|
|
654
|
+
p = repo / FILE_SECTIONS["guides"][0] / member
|
|
655
|
+
if p.is_file():
|
|
656
|
+
sizes[key] = sizes.get(key, 0) + p.stat().st_size // 4
|
|
623
657
|
return errors, sizes
|
|
624
658
|
|
|
625
659
|
|
|
@@ -773,6 +807,57 @@ def self_test(manifest, bullets):
|
|
|
773
807
|
print(f"self-test [{'CAUGHT' if never_ok else 'MISSED'}] an env-personal (never-"
|
|
774
808
|
f"delivered) router bullet leaves {o_name} an orphan")
|
|
775
809
|
|
|
810
|
+
# Bundle shape is structural, not a second manifest class. These controls run against
|
|
811
|
+
# copies because the source is the input being judged: an orphan resource tree and a
|
|
812
|
+
# symlink must be named before router/coverage checks can pretend the guide is ordinary.
|
|
813
|
+
bundle_sources = guide_member_map(REPO / "claude" / "guides")
|
|
814
|
+
bundle_name = next((name for name, members in bundle_sources.items()
|
|
815
|
+
if len(members) > 1), None)
|
|
816
|
+
if bundle_name is None:
|
|
817
|
+
raise SystemExit("self-test: no guide bundle has a companion member, so bundle-shape "
|
|
818
|
+
"controls would pass vacuously")
|
|
819
|
+
companion_markdown = next((member for member in bundle_sources[bundle_name][1:]
|
|
820
|
+
if member.endswith(".md")), None)
|
|
821
|
+
if companion_markdown is None:
|
|
822
|
+
raise SystemExit("self-test: the companion bundle has no Markdown resource, so the "
|
|
823
|
+
"companion-router control would pass vacuously")
|
|
824
|
+
import shutil as _bundle_shutil, tempfile as _bundle_tempfile
|
|
825
|
+
with _bundle_tempfile.TemporaryDirectory() as _td:
|
|
826
|
+
_root = pathlib.Path(_td)
|
|
827
|
+
for _sub in ("claude", "ko", "launch"):
|
|
828
|
+
if (REPO / _sub).is_dir():
|
|
829
|
+
_bundle_shutil.copytree(REPO / _sub, _root / _sub)
|
|
830
|
+
_guide_root = _root / "claude" / "guides"
|
|
831
|
+
(_guide_root / "orphan-companion-control").mkdir()
|
|
832
|
+
_orphan_shape, _ = run_gate(manifest, bullets, repo=_root)
|
|
833
|
+
orphan_companion_ok = any("orphan guide companion tree" in e for e in _orphan_shape)
|
|
834
|
+
_bundle_shutil.rmtree(_guide_root / "orphan-companion-control")
|
|
835
|
+
|
|
836
|
+
_symlink = _guide_root / pathlib.Path(bundle_name).stem / "symlink-control"
|
|
837
|
+
_symlink.symlink_to(_guide_root / bundle_name)
|
|
838
|
+
_symlink_shape, _ = run_gate(manifest, bullets, repo=_root)
|
|
839
|
+
symlink_ok = any("must not contain symlinks" in e for e in _symlink_shape)
|
|
840
|
+
_symlink.unlink()
|
|
841
|
+
|
|
842
|
+
_companion = _guide_root / companion_markdown
|
|
843
|
+
_body = _companion.read_text(encoding="utf-8")
|
|
844
|
+
_companion.write_text(_body + f"\n\nSee `guides/{o_name}` for the control.\n",
|
|
845
|
+
encoding="utf-8")
|
|
846
|
+
_companion_router, _ = run_gate(m_orphan, [b for b in bullets if b != o_line], repo=_root)
|
|
847
|
+
companion_router_ok = not any(o_name in e and "orphan" in e for e in _companion_router)
|
|
848
|
+
_companion.write_text("---\naudience: author\n---\n" + _body, encoding="utf-8")
|
|
849
|
+
_companion_author, _ = run_gate(manifest, bullets, repo=_root)
|
|
850
|
+
companion_author_ok = any(bundle_name in e and companion_markdown in e
|
|
851
|
+
and "audience: author" in e for e in _companion_author)
|
|
852
|
+
print(f"self-test [{'CAUGHT' if orphan_companion_ok else 'MISSED'}] an orphan companion "
|
|
853
|
+
"tree is rejected without requiring a manifest item")
|
|
854
|
+
print(f"self-test [{'CAUGHT' if symlink_ok else 'MISSED'}] a symlink in a companion tree "
|
|
855
|
+
"is rejected")
|
|
856
|
+
print(f"self-test [{'CAUGHT' if companion_router_ok else 'MISSED'}] a Markdown companion "
|
|
857
|
+
"is scanned as its primary guide's router")
|
|
858
|
+
print(f"self-test [{'CAUGHT' if companion_author_ok else 'MISSED'}] an author-only companion "
|
|
859
|
+
"cannot ride a delivered primary")
|
|
860
|
+
|
|
776
861
|
# A registry of the wrong TYPE must be listed like any other violation. `set(None)` is
|
|
777
862
|
# not a diagnostic — it is a TypeError out of the gate before it can name anything, and
|
|
778
863
|
# `"domains": null` is exactly what a bad hand-edit of this file leaves behind. The
|
|
@@ -1222,6 +1307,14 @@ def self_test(manifest, bullets):
|
|
|
1222
1307
|
failed.append("real router in list-blockquoted prose still counts")
|
|
1223
1308
|
if not never_ok:
|
|
1224
1309
|
failed.append("never-delivered bullet does not root a guide")
|
|
1310
|
+
if not orphan_companion_ok:
|
|
1311
|
+
failed.append("orphan companion tree is rejected")
|
|
1312
|
+
if not symlink_ok:
|
|
1313
|
+
failed.append("companion-tree symlink is rejected")
|
|
1314
|
+
if not companion_router_ok:
|
|
1315
|
+
failed.append("Markdown companion prose is scanned as a router")
|
|
1316
|
+
if not companion_author_ok:
|
|
1317
|
+
failed.append("author-only companion cannot ride a delivered primary")
|
|
1225
1318
|
if not island_ok:
|
|
1226
1319
|
failed.append("reciprocal island stays orphaned")
|
|
1227
1320
|
if not inert_hits:
|
package/compose/corpus-state.py
CHANGED
|
@@ -356,6 +356,9 @@ def _assemble_at(repo: pathlib.Path, commit: str, domains: list[str]) -> tuple[b
|
|
|
356
356
|
|
|
357
357
|
|
|
358
358
|
def cmd_rollback(args: argparse.Namespace) -> int:
|
|
359
|
+
if os.environ.get("AGENT_BIOS_LEGACY_INSTALL") != "1":
|
|
360
|
+
print("corpus-state rollback: global rollback is retired; use agent-bios corpus plan with op=rollback and a private baseline_ref", file=sys.stderr)
|
|
361
|
+
return 2
|
|
359
362
|
repo = repo_root(args.repo)
|
|
360
363
|
versions = require_versions(repo, "rollback")
|
|
361
364
|
if versions is None:
|
|
@@ -1144,6 +1147,7 @@ def main() -> int:
|
|
|
1144
1147
|
# Before argparse, because --self-test takes none of the subcommands' arguments and
|
|
1145
1148
|
# every subcommand here is required.
|
|
1146
1149
|
if "--self-test" in sys.argv[1:]:
|
|
1150
|
+
os.environ["AGENT_BIOS_LEGACY_INSTALL"] = "1"
|
|
1147
1151
|
return self_test()
|
|
1148
1152
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
1149
1153
|
sub = parser.add_subparsers(dest="cmd", required=True)
|