agent-bios 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPENDENCIES.md +19 -19
- package/README.md +34 -11
- package/claude/CLAUDE.md +2 -1
- package/claude/guides/claude-prompting.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +19 -1
- package/claude/guides/coding-staged-workflow.md +32 -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/session-distill-workflow.md +19 -9
- package/claude/guides/tooling-gotchas.md +16 -0
- package/claude/hooks/__pycache__/tooling-gotchas-hook.cpython-314.pyc +0 -0
- package/claude/hooks/tooling-gotchas-hook.py +7 -0
- package/codex/AGENTS.md +2 -1
- package/codex/guides/claude-prompting.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +19 -1
- package/codex/guides/coding-staged-workflow.md +32 -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/session-distill-workflow.md +19 -9
- package/codex/guides/tooling-gotchas.md +16 -0
- package/{scripts → compose}/assemble.py +209 -25
- package/{scripts → compose}/canary.sh +14 -5
- package/{scripts → compose}/check-domains.py +9 -3
- package/{config → compose}/domains.json +1 -0
- package/{scripts → compose}/pkgid.py +8 -1
- package/compose/prune-backups.py +204 -0
- package/compose/register-hooks.py +44 -0
- package/{scripts/install.sh → install.sh} +403 -94
- 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 +25 -23
- 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-parity.sh +0 -2003
- /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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
After a push lands, a learning that was promoted into the shared corpus is now
|
|
5
5
|
loaded from the corpus — its personal copy (written by `learn!`,
|
|
6
|
-
|
|
6
|
+
learn/collect-learning.py) would double-load. This user-side tool removes the
|
|
7
7
|
absorbed personal copy, and ONLY it (never the user's hand-written entry
|
|
8
8
|
CLAUDE.md `## Personal` section).
|
|
9
9
|
|
|
@@ -21,10 +21,11 @@ irreversible act. Every uncertainty resolves to KEEP: a foreign package we
|
|
|
21
21
|
cannot confirm, an audience miss, an anchor absent from the corpus, a v1 record
|
|
22
22
|
with no anchor to verify.
|
|
23
23
|
|
|
24
|
-
Candidate selection (mirrors
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
Candidate selection (mirrors compose/assemble.py `kept`): universal tier
|
|
25
|
+
(core/infra) -> always installed; a domain key -> installed iff in the user's
|
|
26
|
+
selection; anything else (env-personal / unclassified / unknown) -> KEEP. There
|
|
27
|
+
is one install shape — what used to be a "full" install is every domain
|
|
28
|
+
selected — so a selection always answers this.
|
|
28
29
|
|
|
29
30
|
Run per host (mirrors collect-learning: --host + --config-dir); install.sh calls
|
|
30
31
|
it after the corpus deploy. Manifest absent/empty -> no-op.
|
|
@@ -39,11 +40,14 @@ import shutil
|
|
|
39
40
|
import sys
|
|
40
41
|
import time
|
|
41
42
|
|
|
42
|
-
sys.path.insert(0, str(pathlib.Path(__file__).resolve().
|
|
43
|
-
import pkgid # noqa: E402 (
|
|
43
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "compose"))
|
|
44
|
+
import pkgid # noqa: E402 (package-identity primitive owned by compose/)
|
|
44
45
|
|
|
45
46
|
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
46
|
-
|
|
47
|
+
# Same home compose/corpus-state.py uses; the canary writes its activation proof here.
|
|
48
|
+
STATE_DIR = pathlib.Path(os.environ.get("AGENT_BIOS_STATE_DIR")
|
|
49
|
+
or pathlib.Path.home() / ".local/share/agent-bios")
|
|
50
|
+
MANIFEST = REPO / "learn" / "promotions.json"
|
|
47
51
|
UNIVERSAL_TIERS = frozenset({"core", "infra"}) # == assemble.audience UNIVERSAL
|
|
48
52
|
# learning_id inside a personal bullet's TRAILING comment (collect-learning
|
|
49
53
|
# prose_bullet: "... <!-- learning_id: <uuid> created: <ts> -->"). Anchored to
|
|
@@ -58,7 +62,7 @@ def die(msg, code=1):
|
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
def _load(name, filename):
|
|
61
|
-
path = REPO / "
|
|
65
|
+
path = REPO / "learn" / filename
|
|
62
66
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
63
67
|
module = importlib.util.module_from_spec(spec)
|
|
64
68
|
spec.loader.exec_module(module)
|
|
@@ -78,16 +82,17 @@ def load_manifest(path=MANIFEST):
|
|
|
78
82
|
if isinstance(p, dict) and isinstance(p.get("learning_id"), str)]
|
|
79
83
|
|
|
80
84
|
|
|
81
|
-
def make_in_bundle(
|
|
85
|
+
def make_in_bundle(selection):
|
|
82
86
|
"""A promotion's placement AUDIENCE (tier + domains, derived by
|
|
83
87
|
build-promotions from where the bullet actually landed) -> is it in THIS
|
|
84
88
|
user's bundle? Mirrors assemble.py `kept`. Biased to KEEP on anything not
|
|
85
|
-
provably installed.
|
|
89
|
+
provably installed.
|
|
90
|
+
|
|
91
|
+
There is one install shape now: what used to be a "full" install is every domain
|
|
92
|
+
selected, so a selection always answers this and there is no mode to branch on."""
|
|
86
93
|
selection = set(selection or ())
|
|
87
94
|
|
|
88
95
|
def in_bundle(tier, domains):
|
|
89
|
-
if full:
|
|
90
|
-
return True
|
|
91
96
|
if tier in UNIVERSAL_TIERS:
|
|
92
97
|
return True
|
|
93
98
|
if tier == "domain":
|
|
@@ -104,8 +109,8 @@ def corpus_surfaces(home):
|
|
|
104
109
|
learning quotes the same lesson, so searching it would find the very thing
|
|
105
110
|
we are deciding whether to delete and always answer yes.
|
|
106
111
|
"""
|
|
107
|
-
texts = [home / "central" / "bundle.md",
|
|
108
|
-
home / "CLAUDE.md", home / "AGENTS.md"] #
|
|
112
|
+
texts = [home / "central" / "bundle.md", # the assembled corpus
|
|
113
|
+
home / "CLAUDE.md", home / "AGENTS.md"] # entry files, for a pre-convergence layout
|
|
109
114
|
dirs = [home / "central" / d for d in ("guides", "hooks", "agents")]
|
|
110
115
|
dirs += [home / d for d in ("guides", "hooks", "agents")]
|
|
111
116
|
return texts, dirs
|
|
@@ -227,17 +232,32 @@ def prune_codex_prose(home, remove_ids, collect, dry):
|
|
|
227
232
|
return removed
|
|
228
233
|
|
|
229
234
|
|
|
230
|
-
def claude_corpus_loaded(home,
|
|
231
|
-
"""Is the shared corpus actually LOADED for this claude home?
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
`@central/bundle.md`
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
def claude_corpus_loaded(home, state_dir=None):
|
|
236
|
+
"""Is the shared corpus actually LOADED for this claude home?
|
|
237
|
+
|
|
238
|
+
Removing a personal copy while the corpus copy is not loaded makes the rule
|
|
239
|
+
vanish (Review F2). The entry import line is necessary and NOT sufficient. Testing
|
|
240
|
+
whether the file contains `@central/bundle.md` also passes on the string in a
|
|
241
|
+
fenced block or in prose, and no reading of the file can see an import the
|
|
242
|
+
harness was told not to load — the case PU-13 exists to name. So the
|
|
243
|
+
authorization is the activation canary's proof, written only after a live
|
|
244
|
+
session echoed the deployed bundle's rev back, and only for THAT rev: a later
|
|
245
|
+
reassembly invalidates it rather than inheriting it.
|
|
246
|
+
|
|
247
|
+
No proof means KEEP, not prune. A plain `install` runs no canary, so it now
|
|
248
|
+
prunes nothing and the next run after `onboard` does it — a kept duplicate is
|
|
249
|
+
redundant, a wrong removal is data loss.
|
|
250
|
+
"""
|
|
239
251
|
entry = home / "CLAUDE.md"
|
|
240
|
-
|
|
252
|
+
if not (entry.is_file() and collect_central_import() in entry.read_text(encoding="utf-8")):
|
|
253
|
+
return False
|
|
254
|
+
bundle = home / "central" / "bundle.md"
|
|
255
|
+
proof = (state_dir or STATE_DIR) / "activation.txt"
|
|
256
|
+
if not (bundle.is_file() and proof.is_file()):
|
|
257
|
+
return False
|
|
258
|
+
rev = next((ln for ln in bundle.read_text(encoding="utf-8").splitlines()
|
|
259
|
+
if ln.startswith("agent-bios-bundle-rev: ")), None)
|
|
260
|
+
return bool(rev) and rev.strip() == proof.read_text(encoding="utf-8").strip()
|
|
241
261
|
|
|
242
262
|
|
|
243
263
|
def collect_central_import():
|
|
@@ -311,13 +331,12 @@ def main():
|
|
|
311
331
|
ap.add_argument("--host", choices=("claude", "codex"))
|
|
312
332
|
ap.add_argument("--config-dir", default=None,
|
|
313
333
|
help="config home (default: $CLAUDE_CONFIG_DIR / $CODEX_HOME by host)")
|
|
314
|
-
|
|
315
|
-
help="non-packaged install: the full corpus is installed")
|
|
334
|
+
|
|
316
335
|
ap.add_argument("--selection-file",
|
|
317
336
|
help="packaged install: selection.json ({\"domains\":[...]})")
|
|
318
337
|
ap.add_argument("--domains", help="packaged: comma-separated selection (overrides --selection-file)")
|
|
319
338
|
ap.add_argument("--manifest", default=None,
|
|
320
|
-
help="promotion manifest (default:
|
|
339
|
+
help="promotion manifest (default: learn/promotions.json)")
|
|
321
340
|
ap.add_argument("--dry-run", action="store_true")
|
|
322
341
|
ap.add_argument("--self-test", action="store_true")
|
|
323
342
|
args = ap.parse_args()
|
|
@@ -328,10 +347,8 @@ def main():
|
|
|
328
347
|
|
|
329
348
|
if not args.host:
|
|
330
349
|
die("--host claude|codex is required")
|
|
331
|
-
if
|
|
332
|
-
die("pass --
|
|
333
|
-
if args.full and (args.selection_file is not None or args.domains is not None):
|
|
334
|
-
die("--full is mutually exclusive with --selection-file/--domains")
|
|
350
|
+
if args.selection_file is None and args.domains is None:
|
|
351
|
+
die("pass --selection-file or --domains")
|
|
335
352
|
|
|
336
353
|
promotions = load_manifest(pathlib.Path(args.manifest) if args.manifest else MANIFEST)
|
|
337
354
|
if not promotions:
|
|
@@ -344,19 +361,16 @@ def main():
|
|
|
344
361
|
# F2 gate: only prune where the shared corpus is actually loaded. Codex always
|
|
345
362
|
# loads AGENTS.md's central region; claude loads central only via the entry
|
|
346
363
|
# import (or inline in a full install).
|
|
347
|
-
corpus_loaded = True if args.host == "codex" else claude_corpus_loaded(home
|
|
364
|
+
corpus_loaded = True if args.host == "codex" else claude_corpus_loaded(home)
|
|
348
365
|
|
|
349
|
-
if args.
|
|
350
|
-
|
|
366
|
+
if args.domains is not None:
|
|
367
|
+
selection = [d for d in args.domains.split(",") if d]
|
|
351
368
|
else:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
die(f"selection file not found: {sel_path} (pass --full for a non-packaged install)")
|
|
358
|
-
selection = json.loads(sel_path.read_text(encoding="utf-8")).get("domains", [])
|
|
359
|
-
in_bundle = make_in_bundle(False, selection)
|
|
369
|
+
sel_path = pathlib.Path(args.selection_file)
|
|
370
|
+
if not sel_path.is_file():
|
|
371
|
+
die(f"selection file not found: {sel_path}")
|
|
372
|
+
selection = json.loads(sel_path.read_text(encoding="utf-8")).get("domains", [])
|
|
373
|
+
in_bundle = make_in_bundle(selection)
|
|
360
374
|
|
|
361
375
|
s = migrate(home, args.host, promotions, in_bundle, collect,
|
|
362
376
|
dry=args.dry_run, corpus_loaded=corpus_loaded)
|
|
@@ -370,6 +384,12 @@ def main():
|
|
|
370
384
|
f"kept_not_in_bundle={s['kept_not_in_bundle']}")
|
|
371
385
|
|
|
372
386
|
|
|
387
|
+
# What a "full" install now means: every domain selected. The fixtures that used to
|
|
388
|
+
# pass `full=True` say it this way, because that is the shape the code has.
|
|
389
|
+
ALL_DOMAINS = ["builder-base", "llm-pipeline-dev", "multi-agent-orchestration",
|
|
390
|
+
"office-work", "visualization-docs"]
|
|
391
|
+
|
|
392
|
+
|
|
373
393
|
def _self_test():
|
|
374
394
|
"""Temp-home verification: the safety gate (keep a promotion not in the
|
|
375
395
|
user's bundle), removal of an in-bundle promotion from BOTH prose + jsonl,
|
|
@@ -425,7 +445,7 @@ def _self_test():
|
|
|
425
445
|
# 1) Packaged, selection={builder-base}: core (universal) + builder-base
|
|
426
446
|
# removed; office-work KEPT (the silent-loss guard).
|
|
427
447
|
home = seed_claude()
|
|
428
|
-
in_bundle = make_in_bundle(
|
|
448
|
+
in_bundle = make_in_bundle(["builder-base"])
|
|
429
449
|
s = migrate(home, "claude", promos, in_bundle, collect)
|
|
430
450
|
ids = local_ids(home)
|
|
431
451
|
checks.append(("packaged: removed core+builder-base", s["removed"] == 2 and s["kept_not_in_bundle"] == 1))
|
|
@@ -443,7 +463,7 @@ def _self_test():
|
|
|
443
463
|
# metadata-only rule deletes here and loses the learning; presence keeps.
|
|
444
464
|
# If placed_here() ever returns True unconditionally, this check fails.
|
|
445
465
|
home = seed_claude(placed=("core",))
|
|
446
|
-
s = migrate(home, "claude", promos, make_in_bundle(
|
|
466
|
+
s = migrate(home, "claude", promos, make_in_bundle(["builder-base"]), collect)
|
|
447
467
|
checks.append(("audience says yes but corpus lacks it -> KEEP",
|
|
448
468
|
s["removed"] == 1 and s["kept_not_placed"] == 1
|
|
449
469
|
and L["bb"] in local_ids(home) and md_has(home, L["bb"])))
|
|
@@ -452,7 +472,7 @@ def _self_test():
|
|
|
452
472
|
# never acted on (stage 3 resolves package selections).
|
|
453
473
|
home = seed_claude()
|
|
454
474
|
foreign = [dict(promos[1], package_id="@acme/security")]
|
|
455
|
-
s = migrate(home, "claude", foreign, make_in_bundle(
|
|
475
|
+
s = migrate(home, "claude", foreign, make_in_bundle(ALL_DOMAINS), collect)
|
|
456
476
|
checks.append(("foreign package -> KEEP",
|
|
457
477
|
s["removed"] == 0 and s["kept_foreign_package"] == 1))
|
|
458
478
|
|
|
@@ -468,13 +488,13 @@ def _self_test():
|
|
|
468
488
|
|
|
469
489
|
# 2) Full (non-packaged) install: everything in bundle -> all removed.
|
|
470
490
|
home = seed_claude()
|
|
471
|
-
s = migrate(home, "claude", promos, make_in_bundle(
|
|
491
|
+
s = migrate(home, "claude", promos, make_in_bundle(ALL_DOMAINS), collect)
|
|
472
492
|
checks.append(("full install: all 3 removed", s["removed"] == 3 and not local_ids(home)))
|
|
473
493
|
|
|
474
494
|
# 3) dry-run changes nothing on disk.
|
|
475
495
|
home = seed_claude()
|
|
476
496
|
before = (home / "personal" / "learnings.jsonl").read_text(encoding="utf-8")
|
|
477
|
-
migrate(home, "claude", promos, make_in_bundle(
|
|
497
|
+
migrate(home, "claude", promos, make_in_bundle(ALL_DOMAINS), collect, dry=True)
|
|
478
498
|
checks.append(("dry-run writes nothing",
|
|
479
499
|
(home / "personal" / "learnings.jsonl").read_text(encoding="utf-8") == before))
|
|
480
500
|
|
|
@@ -486,7 +506,7 @@ def _self_test():
|
|
|
486
506
|
r = rec(L["bb"], "builder-base")
|
|
487
507
|
f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
|
488
508
|
collect.apply_codex(chome, collect.prose_bullet(r), dry=False)
|
|
489
|
-
s = migrate(chome, "codex", promos, make_in_bundle(
|
|
509
|
+
s = migrate(chome, "codex", promos, make_in_bundle(ALL_DOMAINS), collect)
|
|
490
510
|
agents_txt = (chome / "AGENTS.md").read_text(encoding="utf-8")
|
|
491
511
|
checks.append(("codex: bullet removed from AGENTS.md region",
|
|
492
512
|
s["prose_removed"] == 1 and L["bb"] not in agents_txt))
|
|
@@ -495,17 +515,38 @@ def _self_test():
|
|
|
495
515
|
# corpus is NOT loaded -> keep everything (never orphan a personal copy).
|
|
496
516
|
home = seed_claude() # apply_claude writes an entry WITHOUT @central/bundle.md
|
|
497
517
|
checks.append(("F2: unwired packaged entry -> corpus not loaded",
|
|
498
|
-
claude_corpus_loaded(home
|
|
499
|
-
s = migrate(home, "claude", promos, make_in_bundle(
|
|
518
|
+
claude_corpus_loaded(home) is False))
|
|
519
|
+
s = migrate(home, "claude", promos, make_in_bundle(["builder-base"]),
|
|
500
520
|
collect, corpus_loaded=False)
|
|
501
521
|
checks.append(("F2: corpus-not-loaded keeps ALL",
|
|
502
522
|
s.get("skipped") == "corpus-not-loaded"
|
|
503
523
|
and local_ids(home) == {L["core"], L["bb"], L["off"]}))
|
|
504
524
|
entry = home / "CLAUDE.md"
|
|
505
525
|
entry.write_text(entry.read_text(encoding="utf-8") + "\n@central/bundle.md\n", encoding="utf-8")
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
526
|
+
# The import line is necessary and NOT sufficient. Reading the file cannot see an import the
|
|
527
|
+
# harness declined, which is what the canary exists to detect, so the authorization is its
|
|
528
|
+
# recorded proof — and the proof is bound to a rev, so a reassembly does not inherit it.
|
|
529
|
+
checks.append(("F2: wired entry, no activation proof -> KEEP",
|
|
530
|
+
claude_corpus_loaded(home, state_dir=home / "state") is False))
|
|
531
|
+
bundle = home / "central" / "bundle.md"
|
|
532
|
+
bundle.write_text(bundle.read_text(encoding="utf-8") + "\nagent-bios-bundle-rev: deadbeef\n",
|
|
533
|
+
encoding="utf-8")
|
|
534
|
+
state = home / "state"
|
|
535
|
+
state.mkdir(parents=True, exist_ok=True)
|
|
536
|
+
(state / "activation.txt").write_text("agent-bios-bundle-rev: deadbeef\n", encoding="utf-8")
|
|
537
|
+
checks.append(("F2: wired entry + matching proof -> corpus loaded",
|
|
538
|
+
claude_corpus_loaded(home, state_dir=state) is True))
|
|
539
|
+
(state / "activation.txt").write_text("agent-bios-bundle-rev: 00000000\n", encoding="utf-8")
|
|
540
|
+
checks.append(("F2: proof for a different rev -> KEEP",
|
|
541
|
+
claude_corpus_loaded(home, state_dir=state) is False))
|
|
542
|
+
# CONTRAST CONTROL for the substring hole: the import string inside a fenced block is not an
|
|
543
|
+
# import, and before the proof requirement this alone authorized the delete.
|
|
544
|
+
fenced = pathlib.Path(tempfile.mkdtemp(prefix="migrate-fenced-"))
|
|
545
|
+
(fenced / "CLAUDE.md").write_text("# CLAUDE.md\n```\n@central/bundle.md\n```\n", encoding="utf-8")
|
|
546
|
+
checks.append(("F2: import string with no activation proof -> KEEP",
|
|
547
|
+
claude_corpus_loaded(fenced, state_dir=fenced) is False))
|
|
548
|
+
# (The old "full install is always loaded" case is gone with full mode: there is no shape
|
|
549
|
+
# whose corpus loads without the entry import, so nothing is exempt from the proof.)
|
|
509
550
|
|
|
510
551
|
# 6) F5: the TRAILING comment's id wins; a learning_id quoted in the lesson
|
|
511
552
|
# body must never shadow it (else a jsonl/prose desync).
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
Both flows that egress session-derived text redact through here, so the floor
|
|
5
5
|
is single-sourced (concept economy) rather than duplicated per call site:
|
|
6
|
-
* heavy flow —
|
|
7
|
-
* light flow —
|
|
6
|
+
* heavy flow — session-distill/digest.py (per-session digests);
|
|
7
|
+
* light flow — learn/collect-learning.py (`learn!` prose + upload + the
|
|
8
8
|
curator export downstream, since the server stores the payload verbatim).
|
|
9
9
|
|
|
10
10
|
`design/corpus-domain-packaging.md` declares this floor as an inherited
|
|
@@ -15,8 +15,8 @@ the `key: value` / `key=value` form and well-known token shapes, keep prose. A
|
|
|
15
15
|
touched, so lessons ABOUT tokens (e.g. "trust the X-Hook-Token header") survive.
|
|
16
16
|
|
|
17
17
|
This module has no side effects on import and is safe to load by path.
|
|
18
|
-
|
|
19
|
-
the shipped
|
|
18
|
+
learn/redact.py is shipped in the npm package (package.json `files`) because
|
|
19
|
+
the shipped learn/collect-learning.py imports it at runtime.
|
|
20
20
|
"""
|
|
21
21
|
import re
|
|
22
22
|
import sys
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bios",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"releaseDate": "2026-07-26",
|
|
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
|
-
"agent-bios": "
|
|
7
|
+
"agent-bios": "install.sh"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"claude/CLAUDE.md",
|
|
@@ -16,27 +16,29 @@
|
|
|
16
16
|
"codex/guides/",
|
|
17
17
|
"codex/agents/",
|
|
18
18
|
"codex/config-additions.toml",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
19
|
+
"launch/agent-launch.toml",
|
|
20
|
+
"compose/domains.json",
|
|
21
|
+
"learn/learning.schema.json",
|
|
22
|
+
"learn/promotions.json",
|
|
23
|
+
"launch/agent-launch.zsh",
|
|
24
|
+
"launch/agent-launch.py",
|
|
25
|
+
"compose/pkgid.py",
|
|
26
|
+
"compose/register-hooks.py",
|
|
27
|
+
"compose/assemble.py",
|
|
28
|
+
"compose/prune-backups.py",
|
|
29
|
+
"compose/check-domains.py",
|
|
30
|
+
"compose/canary.sh",
|
|
31
|
+
"launch/check-prompting-targets.sh",
|
|
32
|
+
"learn/check-learning.py",
|
|
33
|
+
"learn/collect-learning.py",
|
|
34
|
+
"learn/migrate-learnings.py",
|
|
35
|
+
"learn/redact.py",
|
|
36
|
+
"wrappers/codex-run.sh",
|
|
37
|
+
"wrappers/codex-helm.sh",
|
|
38
|
+
"wrappers/claude-run.sh",
|
|
39
|
+
"install.sh",
|
|
40
|
+
"launch/provision-venv.sh",
|
|
41
|
+
"session-cost.py",
|
|
40
42
|
"README.md",
|
|
41
43
|
"DEPENDENCIES.md"
|
|
42
44
|
],
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# claude-run.sh — thin, controllable raw adapter around `claude -p`.
|
|
3
|
+
#
|
|
4
|
+
# The claude-side twin of codex-run. Both exist for the same reason: a review
|
|
5
|
+
# dispatch has to name the seat it actually ran on, and a raw CLI call does not.
|
|
6
|
+
# Until this file existed the asymmetry was silent — the codex host dispatched
|
|
7
|
+
# reviews through our adapter while the claude host dispatched the bare binary,
|
|
8
|
+
# so half of every cross-family review had no place to report from.
|
|
9
|
+
#
|
|
10
|
+
# It is an adapter, not a policy boundary. Callers passing expert overrides after
|
|
11
|
+
# `--` are making expert decisions, exactly as with codex-run.
|
|
12
|
+
#
|
|
13
|
+
# (1) seat --model + --effort, both REQUIRED (see below)
|
|
14
|
+
# (2) mutation reach --permission-mode, defaulting to a no-edit posture
|
|
15
|
+
# (3) prompt read from stdin; the final message goes to stdout
|
|
16
|
+
#
|
|
17
|
+
# --model and --effort are required rather than optional, which is the one place
|
|
18
|
+
# this diverges from codex-run. codex-run tolerates an unpinned dispatch and warns,
|
|
19
|
+
# and that warning goes to a channel nobody reads; an unpinned review is the exact
|
|
20
|
+
# failure the receipt contract exists to catch, so here it is refused up front.
|
|
21
|
+
#
|
|
22
|
+
# NOT A SANDBOX. codex-run's `--sandbox read-only` is enforced by the OS; Claude Code
|
|
23
|
+
# has no equivalent, so the default here denies the mutating TOOLS and nothing more.
|
|
24
|
+
# A reviewer reading a self-contained packet on stdin needs no more reach than that,
|
|
25
|
+
# but do not read the two adapters' defaults as equivalent guarantees.
|
|
26
|
+
#
|
|
27
|
+
# Receipts: when REVIEW_RECEIPT_DIR is set, this adapter records what it observed —
|
|
28
|
+
# argv-independent facts only: exit status, a hash of the packet it fed the tool, a
|
|
29
|
+
# hash of the bytes the tool returned, and the seat it actually sent. Unset, it
|
|
30
|
+
# behaves exactly as it would without this block and writes nothing.
|
|
31
|
+
#
|
|
32
|
+
# Verified against claude 2.1.221.
|
|
33
|
+
set -euo pipefail
|
|
34
|
+
|
|
35
|
+
usage() {
|
|
36
|
+
cat <<'USAGE'
|
|
37
|
+
Usage: claude-run.sh --model M --effort E [--permission-mode MODE] [--cd DIR]
|
|
38
|
+
[-- ARG ...] < packet
|
|
39
|
+
--model M model to pin (required)
|
|
40
|
+
--effort E reasoning effort to pin (required)
|
|
41
|
+
--permission-mode MODE
|
|
42
|
+
claude permission mode (default: the no-edit posture below)
|
|
43
|
+
--cd DIR working root
|
|
44
|
+
-- everything after is passed to claude verbatim (expert override)
|
|
45
|
+
|
|
46
|
+
Environment (the adapter calling convention; all optional):
|
|
47
|
+
REVIEW_RECEIPT_DIR directory to write one ReviewReceipt/v1 into
|
|
48
|
+
REVIEW_METHOD_ID the review method this dispatch serves
|
|
49
|
+
REVIEW_ORDERING_SEED, REVIEW_SWAP_GROUP
|
|
50
|
+
the orchestrator's panel controls, copied into the receipt
|
|
51
|
+
USAGE
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
model=""
|
|
55
|
+
effort=""
|
|
56
|
+
permission_mode=""
|
|
57
|
+
cd_dir=""
|
|
58
|
+
passthrough=()
|
|
59
|
+
|
|
60
|
+
while [ $# -gt 0 ]; do
|
|
61
|
+
case "$1" in
|
|
62
|
+
--model) model="${2:?--model needs a value}"; shift 2 ;;
|
|
63
|
+
--effort) effort="${2:?--effort needs a value}"; shift 2 ;;
|
|
64
|
+
--permission-mode) permission_mode="${2:?--permission-mode needs a value}"; shift 2 ;;
|
|
65
|
+
--cd) cd_dir="${2:?--cd needs a value}"; shift 2 ;;
|
|
66
|
+
--) shift; while [ $# -gt 0 ]; do passthrough+=("$1"); shift; done ;;
|
|
67
|
+
-p|--print) shift ;; # already implied; a caller carrying it over is not an error
|
|
68
|
+
-) shift ;;
|
|
69
|
+
-h|--help) usage; exit 0 ;;
|
|
70
|
+
# Forwarded rather than refused. This adapter is on the dispatch path now, and a
|
|
71
|
+
# caller that reached for one of claude's own flags should get claude's behaviour
|
|
72
|
+
# and claude's error message — not exit 2 from the wrapper, which reads as "the
|
|
73
|
+
# reviewer is broken" and takes the review down with it.
|
|
74
|
+
*) passthrough+=("$1"); shift ;;
|
|
75
|
+
esac
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
if [ -z "$model" ] || [ -z "$effort" ]; then
|
|
79
|
+
# Warn and dispatch, matching codex-run. Refusing outright was right while nothing
|
|
80
|
+
# called this file; on the live path it converts "the review ran unpinned" into "the
|
|
81
|
+
# review did not run", and the honest signal already exists — an unpinned dispatch
|
|
82
|
+
# can name no seat, so no receipt is emitted and the method adjudicates to UNKNOWN.
|
|
83
|
+
echo "claude-run: WARNING: no --model/--effort pin; the seat cannot be named and this" >&2
|
|
84
|
+
echo " dispatch will produce no receipt." >&2
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Built conditionally: an empty pin must be ABSENT, not passed as `--model ""`, which
|
|
88
|
+
# claude rejects — that would turn the warning above back into the hard failure it
|
|
89
|
+
# deliberately stopped being.
|
|
90
|
+
args=(-p)
|
|
91
|
+
if [ -n "$model" ]; then args+=(--model "$model"); fi
|
|
92
|
+
if [ -n "$effort" ]; then args+=(--effort "$effort"); fi
|
|
93
|
+
if [ -n "$permission_mode" ]; then
|
|
94
|
+
args+=(--permission-mode "$permission_mode")
|
|
95
|
+
else
|
|
96
|
+
# Deny the mutating tools rather than picking a permission mode: `plan` would also
|
|
97
|
+
# reframe the task as planning, and a reviewer asked for a plan writes one.
|
|
98
|
+
args+=(--disallowed-tools Edit Write NotebookEdit)
|
|
99
|
+
fi
|
|
100
|
+
if [ -n "$cd_dir" ]; then args+=(--add-dir "$cd_dir"); fi
|
|
101
|
+
if [ "${#passthrough[@]}" -gt 0 ]; then args+=("${passthrough[@]}"); fi
|
|
102
|
+
|
|
103
|
+
cleanup_paths=()
|
|
104
|
+
cleanup_all() {
|
|
105
|
+
if [ "${#cleanup_paths[@]}" -gt 0 ]; then
|
|
106
|
+
for p in "${cleanup_paths[@]}"; do rm -rf "$p"; done
|
|
107
|
+
fi
|
|
108
|
+
}
|
|
109
|
+
trap cleanup_all EXIT
|
|
110
|
+
|
|
111
|
+
# Dispatch audit, mirroring codex-run: the line goes to the log file only, because
|
|
112
|
+
# stdout carries the review result and stderr carries claude's own progress.
|
|
113
|
+
log_home="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
114
|
+
mkdir -p "$log_home/log" 2>/dev/null || true
|
|
115
|
+
printf '%s dispatch model=%s effort=%s permission=%s\n' \
|
|
116
|
+
"$(date +%Y-%m-%dT%H:%M:%S%z)" "$model" "$effort" \
|
|
117
|
+
"${permission_mode:-no-edit-tools}" \
|
|
118
|
+
>> "$log_home/log/claude-run-dispatch.log" 2>/dev/null || true
|
|
119
|
+
|
|
120
|
+
emit_receipt() {
|
|
121
|
+
# Recording never breaks dispatch: every failure here is a warning and the tool's
|
|
122
|
+
# own exit status is what this script returns. A missing receipt adjudicates to
|
|
123
|
+
# UNKNOWN, which is the honest outcome — silence is not evidence of failure.
|
|
124
|
+
local status="$1" packet="$2" result="$3" launcher
|
|
125
|
+
launcher="${AGENT_LAUNCH_BIN:-$HOME/.local/bin/agent-launch}"
|
|
126
|
+
if [ ! -x "$launcher" ]; then
|
|
127
|
+
launcher="$(command -v agent-launch 2>/dev/null || true)"
|
|
128
|
+
fi
|
|
129
|
+
if [ -z "$launcher" ]; then
|
|
130
|
+
echo "claude-run: WARNING: agent-launch not found; no receipt emitted" >&2
|
|
131
|
+
return 0
|
|
132
|
+
fi
|
|
133
|
+
if [ -z "${REVIEW_METHOD_ID:-}" ]; then
|
|
134
|
+
echo "claude-run: WARNING: REVIEW_METHOD_ID unset; the receipt will name no method" >&2
|
|
135
|
+
fi
|
|
136
|
+
# The provider is core knowledge and not a flag: this adapter reaches exactly one
|
|
137
|
+
# family, so letting a caller name a different one would only ever be a false claim.
|
|
138
|
+
"$launcher" --emit-receipt "${REVIEW_METHOD_ID:-}" "anthropic:$model/$effort" \
|
|
139
|
+
"$status" "$packet" "$result" >/dev/null \
|
|
140
|
+
|| echo "claude-run: WARNING: receipt not emitted" >&2
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if [ -n "${REVIEW_RECEIPT_DIR:-}" ]; then
|
|
144
|
+
work="$(mktemp -d "${TMPDIR:-/tmp}/claude-run-XXXXXX")"
|
|
145
|
+
cleanup_paths+=("$work")
|
|
146
|
+
# stdin is captured rather than inherited ONLY on this branch: hashing the packet
|
|
147
|
+
# requires holding it, and teeing stdout requires a pipe. Both change the channel
|
|
148
|
+
# shape, so neither is reached unless a receipt was actually asked for.
|
|
149
|
+
cat > "$work/packet"
|
|
150
|
+
set +e
|
|
151
|
+
claude "${args[@]}" < "$work/packet" | tee "$work/result"
|
|
152
|
+
status=${PIPESTATUS[0]}
|
|
153
|
+
set -e
|
|
154
|
+
emit_receipt "$status" "$work/packet" "$work/result"
|
|
155
|
+
else
|
|
156
|
+
set +e
|
|
157
|
+
claude "${args[@]}"
|
|
158
|
+
status=$?
|
|
159
|
+
set -e
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
exit "$status"
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
# (with --schema, JSON conforming to the schema) and progress to stderr directly.
|
|
23
23
|
# Exit status mirrors `codex exec`.
|
|
24
24
|
#
|
|
25
|
+
# Receipts: when REVIEW_RECEIPT_DIR is set, this adapter records what it observed —
|
|
26
|
+
# exit status, a hash of the packet it fed codex, a hash of the bytes codex returned,
|
|
27
|
+
# and the seat it actually sent. Unset, it behaves exactly as it did before that block
|
|
28
|
+
# existed and writes nothing. The dispatch-audit line below predates receipts and stays:
|
|
29
|
+
# it is a human-readable trail, not an adjudicable record.
|
|
30
|
+
#
|
|
25
31
|
# Verified against codex-cli 0.144.1.
|
|
26
32
|
set -euo pipefail
|
|
27
33
|
|
|
@@ -53,6 +59,12 @@ Usage: codex-run.sh [--profile inherit|hermetic|custom] [--home DIR]
|
|
|
53
59
|
-c k=v expert pass-through config override (repeatable); may override
|
|
54
60
|
wrapper defaults, e.g. -c model_verbosity="low"
|
|
55
61
|
- optional explicit stdin marker
|
|
62
|
+
|
|
63
|
+
Environment (the adapter calling convention; all optional):
|
|
64
|
+
REVIEW_RECEIPT_DIR directory to write one ReviewReceipt/v1 into
|
|
65
|
+
REVIEW_METHOD_ID the review method this dispatch serves
|
|
66
|
+
REVIEW_ORDERING_SEED, REVIEW_SWAP_GROUP
|
|
67
|
+
the orchestrator's panel controls, copied into the receipt
|
|
56
68
|
USAGE
|
|
57
69
|
}
|
|
58
70
|
|
|
@@ -174,9 +186,18 @@ fi
|
|
|
174
186
|
sandbox_label="$sandbox"
|
|
175
187
|
if [ "$bypass_sandbox" -eq 1 ]; then sandbox_label="bypass"; fi
|
|
176
188
|
cmodel=""
|
|
189
|
+
ceffort=""
|
|
177
190
|
if [ "${#extra_c[@]}" -gt 0 ]; then
|
|
178
191
|
for kv in "${extra_c[@]}"; do
|
|
179
|
-
case "$kv" in
|
|
192
|
+
case "$kv" in
|
|
193
|
+
model=*) cmodel="${kv#model=}" ;;
|
|
194
|
+
# An expert -c may override the effort too, and the seat a receipt reports has to
|
|
195
|
+
# be the one actually SENT — reading only --effort would report the requested seat
|
|
196
|
+
# while the dispatch ran on another, which is the drift the receipt exists to catch.
|
|
197
|
+
model_reasoning_effort=*)
|
|
198
|
+
ceffort="${kv#model_reasoning_effort=}"
|
|
199
|
+
ceffort="${ceffort%\"}"; ceffort="${ceffort#\"}" ;;
|
|
200
|
+
esac
|
|
180
201
|
done
|
|
181
202
|
fi
|
|
182
203
|
dispatch_note="dispatch profile=$profile model=${model:-INHERITED_DEFAULT}${cmodel:+ c-model-override=$cmodel} effort=${effort:-config-default} sandbox=$sandbox_label"
|
|
@@ -186,10 +207,45 @@ fi
|
|
|
186
207
|
mkdir -p "$real_home/log" 2>/dev/null || true
|
|
187
208
|
printf '%s %s\n' "$(date +%Y-%m-%dT%H:%M:%S%z)" "$dispatch_note" >> "$real_home/log/codex-run-dispatch.log" 2>/dev/null || true
|
|
188
209
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
210
|
+
emit_receipt() {
|
|
211
|
+
# Recording never breaks dispatch: every failure here is a warning and codex's own
|
|
212
|
+
# exit status is what this script returns. A missing receipt adjudicates to UNKNOWN,
|
|
213
|
+
# which is the honest outcome — silence is not evidence of failure.
|
|
214
|
+
status="$1"; packet="$2"; result="$3"
|
|
215
|
+
launcher="${AGENT_LAUNCH_BIN:-$HOME/.local/bin/agent-launch}"
|
|
216
|
+
if [ ! -x "$launcher" ]; then
|
|
217
|
+
launcher="$(command -v agent-launch 2>/dev/null || true)"
|
|
218
|
+
fi
|
|
219
|
+
if [ -z "$launcher" ]; then
|
|
220
|
+
echo "codex-run: WARNING: agent-launch not found; no receipt emitted" >&2
|
|
221
|
+
return 0
|
|
222
|
+
fi
|
|
223
|
+
# The seat as SENT: an expert -c override beats the flag it overrode, and an
|
|
224
|
+
# unpinned dispatch names no seat at all, so the receipt is refused rather than
|
|
225
|
+
# invented — which is the same failure the warning above already reports.
|
|
226
|
+
"$launcher" --emit-receipt "${REVIEW_METHOD_ID:-}" \
|
|
227
|
+
"openai:${cmodel:-$model}/${ceffort:-$effort}" "$status" "$packet" "$result" >/dev/null \
|
|
228
|
+
|| echo "codex-run: WARNING: receipt not emitted" >&2
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if [ -n "${REVIEW_RECEIPT_DIR:-}" ]; then
|
|
232
|
+
# stdin is captured rather than inherited ONLY on this branch: hashing the packet
|
|
233
|
+
# requires holding it, and teeing stdout requires a pipe. Both change the channel
|
|
234
|
+
# shape, so neither is reached unless a receipt was actually asked for.
|
|
235
|
+
work="$(mktemp -d "${TMPDIR:-/tmp}/codex-run-receipt-XXXXXX")"
|
|
236
|
+
cleanup_paths+=("$work")
|
|
237
|
+
cat > "$work/packet"
|
|
238
|
+
set +e
|
|
239
|
+
CODEX_HOME="$run_home" codex "${args[@]}" < "$work/packet" | tee "$work/result"
|
|
240
|
+
status=${PIPESTATUS[0]}
|
|
241
|
+
set -e
|
|
242
|
+
emit_receipt "$status" "$work/packet" "$work/result"
|
|
243
|
+
else
|
|
244
|
+
# stdin, stdout, and stderr already match this adapter's channel contract.
|
|
245
|
+
set +e
|
|
246
|
+
CODEX_HOME="$run_home" codex "${args[@]}"
|
|
247
|
+
status=$?
|
|
248
|
+
set -e
|
|
249
|
+
fi
|
|
194
250
|
|
|
195
251
|
exit "$status"
|