agent-bios 0.9.5 → 0.9.6

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.
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
+ "package_id": "@agent-bios/core",
3
4
  "tiers": ["core", "domain", "env-personal", "infra"],
4
5
  "domains": {
5
6
  "builder-base": "Technical-common: coding, verification menus, documentation hygiene, tooling safety, concept economy",
@@ -1,8 +1,10 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "promotions": [
4
4
  {
5
5
  "learning_id": "a6feee41-2aeb-4fb6-acc6-4584c5c1336c",
6
+ "package_id": "@agent-bios/core",
7
+ "anchor": "tooling-gotchas.md",
6
8
  "tier": "domain",
7
9
  "domains": [
8
10
  "builder-base"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bios",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
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": {
@@ -22,6 +22,7 @@
22
22
  "config/promotions.json",
23
23
  "shell/agent-launch.zsh",
24
24
  "scripts/agent-launch.py",
25
+ "scripts/pkgid.py",
25
26
  "scripts/assemble.py",
26
27
  "scripts/check-domains.py",
27
28
  "scripts/canary.sh",
@@ -29,12 +29,15 @@ import json
29
29
  import pathlib
30
30
  import sys
31
31
 
32
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
33
+ import pkgid # noqa: E402 (sibling module; scripts/ is a flat toolbox)
34
+
32
35
  REPO = pathlib.Path(__file__).resolve().parent.parent
33
36
  LEDGER = REPO / "design" / "session-distill" / "ledger.json"
34
37
  DOMAINS = REPO / "config" / "domains.json"
35
38
  MANIFEST = REPO / "config" / "promotions.json"
36
39
  FIXTURE = REPO / "design" / "collection-loop" / "fixtures" / "ledger-promote-sample.json"
37
- MANIFEST_VERSION = 1
40
+ MANIFEST_VERSION = 2
38
41
 
39
42
 
40
43
  class ManifestError(Exception):
@@ -80,7 +83,16 @@ def build_manifest(ledger, domains_manifest):
80
83
  f"placed_anchor {anchor!r} (learning {lid}) does not resolve in "
81
84
  "config/domains.json — the promotion has no verifiable placement")
82
85
  tier, domains = aud
83
- promotions.append({"learning_id": lid.lower(), "tier": tier, "domains": domains})
86
+ pid = pkgid.resolve(e, "placed_package_id")
87
+ if not pkgid.is_valid(pid):
88
+ raise ManifestError(
89
+ f"placed_package_id {pid!r} (learning {lid}) is not @scope/name")
90
+ # `anchor` ships alongside the derived audience because the consumer
91
+ # (migrate-learnings) must verify the bullet is REALLY in the user's
92
+ # deployed corpus before deleting their personal copy. Audience metadata
93
+ # alone cannot answer that — it is a build-time projection that drifts.
94
+ promotions.append({"learning_id": lid.lower(), "package_id": pid,
95
+ "anchor": anchor, "tier": tier, "domains": domains})
84
96
  promotions.sort(key=lambda p: p["learning_id"])
85
97
  return {"version": MANIFEST_VERSION, "promotions": promotions}
86
98
 
@@ -133,6 +145,21 @@ def _self_test():
133
145
  checks.append(("unresolvable placed_anchor fails",
134
146
  raises(lambda l: l["entries"][0].__setitem__("placed_anchor", "no-such-anchor"))))
135
147
 
148
+ # v2 identity + locator. Without these the manifest looks fine while the
149
+ # consumer that must verify placement has nothing to verify against.
150
+ checks.append(("manifest is v2", m["version"] == 2))
151
+ checks.append(("every promotion carries its anchor",
152
+ all(p.get("anchor") for p in m["promotions"])))
153
+ checks.append(("absent placed_package_id resolves to core",
154
+ all(p.get("package_id") == pkgid.CORE for p in m["promotions"])))
155
+ led2 = json.loads(FIXTURE.read_text(encoding="utf-8"))
156
+ led2["entries"][0]["placed_package_id"] = "@acme/security"
157
+ checks.append(("declared placed_package_id is carried through",
158
+ any(p["package_id"] == "@acme/security"
159
+ for p in build_manifest(led2, domains)["promotions"])))
160
+ checks.append(("malformed placed_package_id fails",
161
+ raises(lambda l: l["entries"][0].__setitem__("placed_package_id", "Acme/Sec"))))
162
+
136
163
  failed = [n for n, ok in checks if not ok]
137
164
  if failed:
138
165
  for n in failed:
@@ -32,6 +32,9 @@ import pathlib
32
32
  import re
33
33
  import sys
34
34
 
35
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
36
+ import pkgid # noqa: E402 (sibling module; scripts/ is a flat toolbox, not a package)
37
+
35
38
  REPO = pathlib.Path(__file__).resolve().parent.parent
36
39
  MANIFEST = REPO / "config" / "domains.json"
37
40
  MONOLITH = REPO / "claude" / "CLAUDE.md"
@@ -87,6 +90,14 @@ def run_gate(manifest, bullets, repo=REPO):
87
90
  if not tiers or not domains_reg:
88
91
  errors.append("registry: tiers/domains registry empty")
89
92
 
93
+ # -- 0. package identity ------------------------------------------------
94
+ # Absent means core (the reservation that keeps pre-v2 artifacts valid), but
95
+ # a PRESENT malformed id must fail rather than fall back — silently treating
96
+ # a typo as core would hand it the engine's unconditional-injection audience.
97
+ pid = pkgid.resolve(manifest)
98
+ if not pkgid.is_valid(pid):
99
+ errors.append(f"package_id: {pid!r} is not @scope/name (lowercase, hyphen-separated)")
100
+
90
101
  def check_registry(entry, ctx):
91
102
  if entry.get("tier") not in tiers:
92
103
  errors.append(f"{ctx}: tier {entry.get('tier')!r} not in registry")
@@ -205,6 +216,9 @@ def self_test(manifest, bullets):
205
216
  muts.append((f"unclaimed guide {first_guide}", m4, bullets))
206
217
  b5 = bullets + ["- a brand new bullet the manifest never heard of"]
207
218
  muts.append(("bullet added without manifest entry", copy.deepcopy(manifest), b5))
219
+ m6 = copy.deepcopy(manifest)
220
+ m6["package_id"] = "@Acme/Builder" # uppercase is not a legal segment
221
+ muts.append(("malformed package_id", m6, bullets))
208
222
 
209
223
  failed = []
210
224
  for name, mm, bb in muts:
@@ -13,7 +13,15 @@ bundle. Per-domain opt-in means a promotion into a package the user did NOT
13
13
  install must NOT trigger removal — that would silently lose the learning. When
14
14
  unsure, KEEP (a kept duplicate is redundant; a wrong removal is data loss).
15
15
 
16
- Bundle membership (mirrors scripts/assemble.py `kept`): full install (original
16
+ HOW THAT RULE IS ENFORCED (contract v2): audience metadata SELECTS candidates,
17
+ presence in the deployed corpus AUTHORIZES the delete. Metadata is a build-time
18
+ projection — it cannot see that this user runs a package or version whose bundle
19
+ never received the promoted bullet — so it is never the authority for an
20
+ irreversible act. Every uncertainty resolves to KEEP: a foreign package we
21
+ cannot confirm, an audience miss, an anchor absent from the corpus, a v1 record
22
+ with no anchor to verify.
23
+
24
+ Candidate selection (mirrors scripts/assemble.py `kept`): full install (original
17
25
  single-zone / no selection) -> everything installed; universal tier (core/infra)
18
26
  -> always installed; a domain key -> installed iff in the user's selection;
19
27
  anything else (env-personal / unclassified / unknown) -> KEEP.
@@ -31,6 +39,9 @@ import shutil
31
39
  import sys
32
40
  import time
33
41
 
42
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
43
+ import pkgid # noqa: E402 (sibling module; scripts/ is a flat toolbox)
44
+
34
45
  REPO = pathlib.Path(__file__).resolve().parent.parent
35
46
  MANIFEST = REPO / "config" / "promotions.json"
36
47
  UNIVERSAL_TIERS = frozenset({"core", "infra"}) # == assemble.audience UNIVERSAL
@@ -86,6 +97,59 @@ def make_in_bundle(full, selection):
86
97
  return in_bundle
87
98
 
88
99
 
100
+ def corpus_surfaces(home):
101
+ """Files and dirs that hold the DEPLOYED corpus for this host.
102
+
103
+ Deliberately excludes `personal/` — the user's own copy of a promoted
104
+ learning quotes the same lesson, so searching it would find the very thing
105
+ we are deciding whether to delete and always answer yes.
106
+ """
107
+ texts = [home / "central" / "bundle.md", # packaged install
108
+ home / "CLAUDE.md", home / "AGENTS.md"] # full install (entry is the corpus)
109
+ dirs = [home / "central" / d for d in ("guides", "hooks", "agents")]
110
+ dirs += [home / d for d in ("guides", "hooks", "agents")]
111
+ return texts, dirs
112
+
113
+
114
+ def corpus_text(path, collect=None):
115
+ """A corpus file's text with the user's personal region removed.
116
+
117
+ On codex the personal copy lives INSIDE AGENTS.md, and a promoted bullet is
118
+ written FROM the user's lesson — so its anchor can legitimately appear in
119
+ their own bullet. Searching the region would let a personal copy authorize
120
+ its own deletion. Strip it before matching.
121
+ """
122
+ body = path.read_text(encoding="utf-8", errors="replace")
123
+ if collect is None:
124
+ return body
125
+ start, end = getattr(collect, "PERSONAL_START", None), getattr(collect, "PERSONAL_END", None)
126
+ if start and end and start in body:
127
+ pre, rest = body.split(start, 1)
128
+ return pre + (rest.split(end, 1)[1] if end in rest else "")
129
+ return body
130
+
131
+
132
+ def placed_here(home, anchor, collect=None):
133
+ """Is the promoted item REALLY in this user's deployed corpus?
134
+
135
+ This is the authorization for an irreversible delete, so it asks the
136
+ filesystem rather than trusting build-time audience metadata, which is a
137
+ projection that goes stale the moment packages or versions diverge. A whole
138
+ guide/hook/agent placement is a deployed file; a bullet placement is its
139
+ anchor appearing in the deployed corpus text.
140
+ """
141
+ if not isinstance(anchor, str) or not anchor:
142
+ return False
143
+ texts, dirs = corpus_surfaces(home)
144
+ for d in dirs:
145
+ if (d / anchor).is_file():
146
+ return True
147
+ for f in texts:
148
+ if f.is_file() and anchor in corpus_text(f, collect):
149
+ return True
150
+ return False
151
+
152
+
89
153
  def backup(path):
90
154
  shutil.copy2(path, path.with_suffix(path.suffix + f".bak-migrate-{time.strftime('%Y%m%d-%H%M%S')}"))
91
155
 
@@ -202,15 +266,29 @@ def migrate(home, host, promotions, in_bundle, collect, dry=False, corpus_loaded
202
266
  if isinstance(lid, str):
203
267
  local_ids.add(lid)
204
268
 
205
- remove_ids, kept_not_in_bundle = set(), 0
269
+ # Metadata SELECTS candidates; presence in the deployed corpus AUTHORIZES the
270
+ # delete (contract v2 §4). Audience metadata is a build-time projection: it
271
+ # cannot see that this user is on a package or version whose bundle never
272
+ # received the promoted bullet, and acting on it alone loses the learning.
273
+ # Every uncertainty below resolves to KEEP.
274
+ remove_ids, kept_not_in_bundle, kept_not_placed, kept_foreign_pkg = set(), 0, 0, 0
206
275
  for p in promotions:
207
276
  lid = p["learning_id"]
208
277
  if lid not in local_ids:
209
278
  continue # not held locally (never captured here, or already migrated)
210
- if in_bundle(p.get("tier"), p.get("domains", [])):
211
- remove_ids.add(lid)
212
- else:
213
- kept_not_in_bundle += 1 # promoted but not in THIS user's bundle -> keep
279
+ pid = pkgid.resolve(p)
280
+ if pid != pkgid.CORE:
281
+ # Stage 3 resolves a package selection; until then the only package
282
+ # whose composition we can confirm is core.
283
+ kept_foreign_pkg += 1
284
+ continue
285
+ if not in_bundle(p.get("tier"), p.get("domains", [])):
286
+ kept_not_in_bundle += 1 # promoted but not in THIS user's audience
287
+ continue
288
+ if not placed_here(home, p.get("anchor"), collect):
289
+ kept_not_placed += 1 # audience says yes, the corpus does not have it
290
+ continue
291
+ remove_ids.add(lid)
214
292
 
215
293
  # Prune PROSE first, jsonl second: the gate keys off ids still in the jsonl
216
294
  # (line ~"if lid not in local_ids: continue"), so if a prose write fails, the
@@ -223,7 +301,8 @@ def migrate(home, host, promotions, in_bundle, collect, dry=False, corpus_loaded
223
301
  jsonl_removed = prune_jsonl(jsonl, remove_ids, dry)
224
302
 
225
303
  return {"removed": len(remove_ids), "jsonl_removed": jsonl_removed,
226
- "prose_removed": prose_removed, "kept_not_in_bundle": kept_not_in_bundle}
304
+ "prose_removed": prose_removed, "kept_not_in_bundle": kept_not_in_bundle,
305
+ "kept_not_placed": kept_not_placed, "kept_foreign_package": kept_foreign_pkg}
227
306
 
228
307
 
229
308
  def main():
@@ -308,13 +387,21 @@ def _self_test():
308
387
  L = {"core": "0f8c1c2a-4d1e-4abc-9def-0000000000a1",
309
388
  "bb": "0f8c1c2a-4d1e-4abc-9def-0000000000b2",
310
389
  "off": "0f8c1c2a-4d1e-4abc-9def-0000000000c3"}
311
- promos = [{"learning_id": L["core"], "tier": "core", "domains": []},
312
- {"learning_id": L["bb"], "tier": "domain", "domains": ["builder-base"]},
313
- {"learning_id": L["off"], "tier": "domain", "domains": ["office-work"]}]
314
-
315
- def seed_claude():
390
+ A = {"core": "universal corpus rule", "bb": "builder corpus rule",
391
+ "off": "office corpus rule"}
392
+ promos = [{"learning_id": L["core"], "anchor": A["core"], "tier": "core", "domains": []},
393
+ {"learning_id": L["bb"], "anchor": A["bb"], "tier": "domain", "domains": ["builder-base"]},
394
+ {"learning_id": L["off"], "anchor": A["off"], "tier": "domain", "domains": ["office-work"]}]
395
+
396
+ def seed_claude(placed=("core", "bb", "off")):
397
+ """`placed` = which promoted anchors this user's DEPLOYED corpus actually
398
+ carries. Deletion is authorized by that, not by the audience metadata, so
399
+ a fixture without a corpus would let a metadata-only bug pass."""
316
400
  home = pathlib.Path(tempfile.mkdtemp(prefix="migrate-selftest-"))
317
401
  (home / "personal").mkdir(parents=True)
402
+ (home / "central").mkdir(parents=True)
403
+ (home / "central" / "bundle.md").write_text(
404
+ "# bundle\n" + "".join(f"- {A[k]}\n" for k in placed), encoding="utf-8")
318
405
  jsonl = home / "personal" / "learnings.jsonl"
319
406
  with open(jsonl, "w", encoding="utf-8") as f:
320
407
  for k, dom in (("core", "core"), ("bb", "builder-base"), ("off", "office-work")):
@@ -350,6 +437,35 @@ def _self_test():
350
437
  s2 = migrate(home, "claude", promos, in_bundle, collect)
351
438
  checks.append(("idempotent re-run", s2["removed"] == 0))
352
439
 
440
+ # 1b) CONTRAST CONTROL for the v2 authorization. Same audience metadata as
441
+ # above — builder-base IS selected — but this user's deployed corpus does
442
+ # NOT carry the bullet (a package/version whose bundle never got it). The
443
+ # metadata-only rule deletes here and loses the learning; presence keeps.
444
+ # If placed_here() ever returns True unconditionally, this check fails.
445
+ home = seed_claude(placed=("core",))
446
+ s = migrate(home, "claude", promos, make_in_bundle(False, ["builder-base"]), collect)
447
+ checks.append(("audience says yes but corpus lacks it -> KEEP",
448
+ s["removed"] == 1 and s["kept_not_placed"] == 1
449
+ and L["bb"] in local_ids(home) and md_has(home, L["bb"])))
450
+
451
+ # 1c) A promotion from a package whose composition cannot be confirmed is
452
+ # never acted on (stage 3 resolves package selections).
453
+ home = seed_claude()
454
+ foreign = [dict(promos[1], package_id="@acme/security")]
455
+ s = migrate(home, "claude", foreign, make_in_bundle(True, ()), collect)
456
+ checks.append(("foreign package -> KEEP",
457
+ s["removed"] == 0 and s["kept_foreign_package"] == 1))
458
+
459
+ # 1d) The personal copy must not authorize its own deletion: on codex the
460
+ # copy lives inside AGENTS.md, so the anchor can appear there legitimately.
461
+ ahome = pathlib.Path(tempfile.mkdtemp(prefix="migrate-selfauth-"))
462
+ (ahome / "AGENTS.md").write_text(
463
+ f"# AGENTS.md\n{collect.PERSONAL_START}\n- {A['bb']}\n{collect.PERSONAL_END}\n",
464
+ encoding="utf-8")
465
+ checks.append(("personal region cannot authorize its own delete",
466
+ placed_here(ahome, A["bb"], collect) is False
467
+ and placed_here(ahome, A["bb"], None) is True))
468
+
353
469
  # 2) Full (non-packaged) install: everything in bundle -> all removed.
354
470
  home = seed_claude()
355
471
  s = migrate(home, "claude", promos, make_in_bundle(True, ()), collect)
@@ -365,6 +481,7 @@ def _self_test():
365
481
  # 4) codex host: seed the AGENTS.md region, migrate prunes it there.
366
482
  chome = pathlib.Path(tempfile.mkdtemp(prefix="migrate-codex-"))
367
483
  (chome / "personal").mkdir(parents=True)
484
+ (chome / "AGENTS.md").write_text(f"# AGENTS.md\n- {A['bb']}\n", encoding="utf-8")
368
485
  with open(chome / "personal" / "learnings.jsonl", "w", encoding="utf-8") as f:
369
486
  r = rec(L["bb"], "builder-base")
370
487
  f.write(json.dumps(r, ensure_ascii=False) + "\n")
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env python3
2
+ """Canonical package identity for the domain-package ecosystem (contract v2).
3
+
4
+ A package is `@scope/name` — **unversioned**, and independent of the domains it
5
+ declares. A domain's canonical identity is the pair `(package_id, domain)`, so a
6
+ bare domain string is only meaningful relative to a package.
7
+
8
+ `CORE` is the built-in engine manifest. A manifest or record that carries no
9
+ package id means `CORE`, permanently: that reservation is what keeps every
10
+ artifact written before v2 — selections, promotions, ledger placements — valid
11
+ with no migration.
12
+
13
+ Spec: design/adapter-split/ECOSYSTEM-ARCHITECTURE.md, "Foundational contract v2".
14
+ """
15
+ import re
16
+
17
+ CORE = "@agent-bios/core"
18
+
19
+ _SEG = r"[a-z0-9]+(?:-[a-z0-9]+)*"
20
+ PATTERN = re.compile(rf"^@{_SEG}/{_SEG}$")
21
+
22
+
23
+ def is_valid(pid):
24
+ """True for a well-formed package id. Callers validate before deriving paths."""
25
+ return isinstance(pid, str) and PATTERN.match(pid) is not None
26
+
27
+
28
+ def resolve(obj, key="package_id"):
29
+ """The package id an object declares, or CORE when it declares none.
30
+
31
+ Absent means CORE — never guess from context, and never treat a present but
32
+ malformed id as absent: that would silently promote a typo to core's
33
+ authority. Validate with is_valid() where the value is first accepted.
34
+ """
35
+ if not isinstance(obj, dict):
36
+ return CORE
37
+ pid = obj.get(key)
38
+ return CORE if pid is None else pid
39
+
40
+
41
+ def segments(pid):
42
+ """('scope', 'name') for deriving deploy paths. Only call on a valid id."""
43
+ if not is_valid(pid):
44
+ raise ValueError(f"not a package id: {pid!r}")
45
+ return tuple(pid[1:].split("/", 1))