agent-bios 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/compose/bootstrap/SKILL.md +11 -1
- package/compose/corpus_store.py +75 -11
- package/compose/corpus_ui.py +279 -8
- package/compose/corpus_understand.py +6 -1
- package/package.json +1 -1
- package/provenance.json +1 -1
package/README.md
CHANGED
|
@@ -291,6 +291,44 @@ are display-only references, not new dependencies or proof of loading: rule bodi
|
|
|
291
291
|
IDs, ordering, selection, and delivery remain unchanged. Personal body edits are
|
|
292
292
|
reflected when the library refreshes; ordinary rules are not classified by meaning.
|
|
293
293
|
|
|
294
|
+
Moving the library cursor to an item immediately displays its document; Enter is
|
|
295
|
+
not required. Group rows show group guidance and disable item-specific actions.
|
|
296
|
+
Cursor events cannot retarget an open editor or a prepared change preview.
|
|
297
|
+
|
|
298
|
+
Arrow keys also navigate the reading controls: **Search ↓ buttons ↓ library →
|
|
299
|
+
document**, with **←/→** between neighboring buttons and **↑** back toward search.
|
|
300
|
+
At the end of search text, **→** focuses the view selector; within text, the caret
|
|
301
|
+
moves normally. **←** from a closed view selector returns to search. Open menus
|
|
302
|
+
keep their native arrow selection. The library keeps normal **↑/↓** item movement;
|
|
303
|
+
at its top, **↑** returns to controls. In the document, arrows scroll until an
|
|
304
|
+
edge: **←** returns to the library, **↑** reaches the controls/member selector,
|
|
305
|
+
and **↓** at the bottom reaches pending on/off buttons when present. Disabled or
|
|
306
|
+
hidden buttons are skipped. TextArea editing, modal boundaries, and Tab remain intact.
|
|
307
|
+
|
|
308
|
+
In the library, **Space** toggles an item's use in future activated sessions:
|
|
309
|
+
`[x]` is on, `[ ]` is off, `[-]` is removed, and `*` marks an unapplied change.
|
|
310
|
+
The Available group means retained authoring items, not that every item is on.
|
|
311
|
+
Stage several choices, then use **Preview on/off → Apply**, or **Discard on/off**.
|
|
312
|
+
Text inputs keep normal spaces; Space on a group still expands or collapses it.
|
|
313
|
+
Unapplied choices require discard confirmation on exit and cannot be mixed with
|
|
314
|
+
content edits. A stale authoring revision refuses the preview or apply.
|
|
315
|
+
|
|
316
|
+
Per-item choices override domain and explicit launch selections, including core
|
|
317
|
+
and infrastructure defaults. Turning an item off does not delete its body, edits,
|
|
318
|
+
or identity, and old snapshots and session pins remain intact. Choices survive
|
|
319
|
+
updates and content restoration; full reset returns to installed defaults.
|
|
320
|
+
Enabling a removed item requires Restore/Recover first. A guide switched off is
|
|
321
|
+
marked in its referring rule, without automatically changing that rule. Native
|
|
322
|
+
hook/agent opt-in, trust, host support, and promotion rules still apply: a checked
|
|
323
|
+
item is a projection choice, not proof of execution. This does not block host
|
|
324
|
+
global/project instructions or a tool from opening a file independently.
|
|
325
|
+
|
|
326
|
+
The same revision-checked manager accepts `{"operation":"enable","items":{"@agent-bios/core:rule-003":false}}`
|
|
327
|
+
through `corpus plan`; `true` forces inclusion, `false` excludes, and `null`
|
|
328
|
+
removes that override so the normal selection applies. `list` reports `enabled`,
|
|
329
|
+
`enabled_override`, and the captured authoring `revision`; pass that revision as
|
|
330
|
+
`expected_revision` when planning a batch from the displayed inventory.
|
|
331
|
+
|
|
294
332
|
New personal identities are allocated once in the creation plan and remain stable
|
|
295
333
|
on retry; reset and history rollback do not make retired identities reusable.
|
|
296
334
|
Member files are the content authority: `primary_member` identifies the main file,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
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.
|
|
3
|
+
description: Inspect or change the private agent-bios corpus used by activated sessions, including item creation, edits, consumption placement, enablement, 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
4
|
---
|
|
5
5
|
|
|
6
6
|
# Corpus management
|
|
@@ -64,11 +64,21 @@ Supported payloads:
|
|
|
64
64
|
{"operation":"restore","ref":"@scope/name:item-id"}
|
|
65
65
|
{"operation":"recover","ref":"@local/personal:item-id"}
|
|
66
66
|
{"operation":"select","selection":["@scope/name/domain"]}
|
|
67
|
+
{"operation":"enable","items":{"@scope/name:item-id":false},"expected_revision":"<revision from list>"}
|
|
67
68
|
{"operation":"reset"}
|
|
68
69
|
{"operation":"rollback","baseline_ref":"<baseline_ref>"}
|
|
69
70
|
{"operation":"rollback","history_id":"<history_id>"}
|
|
70
71
|
```
|
|
71
72
|
|
|
73
|
+
For turning items on or off without losing edits, use `enable`, not remove/restore.
|
|
74
|
+
Its `items` mapping supports batches: `true` forces inclusion, `false` excludes,
|
|
75
|
+
and `null` returns to normal selection. Read `enabled`, `enabled_override`, and
|
|
76
|
+
`revision` from `list`; the captured revision binds the whole batch. Overrides
|
|
77
|
+
take precedence over domain/core/infra and per-launch selections, survive updates
|
|
78
|
+
and content restoration, and clear on full reset. They do not bypass native
|
|
79
|
+
opt-in, trust, host support, or promotion rules, or prevent independent file reads.
|
|
80
|
+
Removed items require Restore/Recover first. Existing session pins remain unchanged.
|
|
81
|
+
|
|
72
82
|
For a body edit, submit `body`; the manager updates the item's `primary_member`
|
|
73
83
|
and derives every content view from that file. For a multi-file edit, submit
|
|
74
84
|
`members` retaining unchanged files. A conflicting simultaneous body/member edit
|
package/compose/corpus_store.py
CHANGED
|
@@ -37,6 +37,7 @@ ITEM_FIELDS = {
|
|
|
37
37
|
RUNTIME_FIELDS = {
|
|
38
38
|
"digest", "revision", "content_ref", "path", "state", "created_at",
|
|
39
39
|
"updated_at", "baseline_ref", "plan_id", "history_id",
|
|
40
|
+
"enabled", "enabled_override",
|
|
40
41
|
}
|
|
41
42
|
LEARNING_FIELDS = {"schema_version", "learning_id", "lesson", "domain", "created", "supporting_sessions",
|
|
42
43
|
"criteria", "classification", "proposed_domain", "context"}
|
|
@@ -307,8 +308,20 @@ class CorpusStore:
|
|
|
307
308
|
for field, typ in (("items", dict), ("overrides", dict), ("tombstones", dict), ("learning_suppressions", dict)):
|
|
308
309
|
if not isinstance(state.get(field), typ):
|
|
309
310
|
raise CorpusStoreError(f"personal state has invalid {field}")
|
|
311
|
+
# Optional, so reading an older state does not change its revision or the
|
|
312
|
+
# exact before/after documents used by prepared-transaction recovery.
|
|
313
|
+
self._enabled_overrides(state)
|
|
310
314
|
return state
|
|
311
315
|
|
|
316
|
+
@staticmethod
|
|
317
|
+
def _enabled_overrides(user: dict[str, Any]) -> dict[str, bool]:
|
|
318
|
+
values = user.get("enabled_overrides", {})
|
|
319
|
+
if (not isinstance(values, dict) or any(
|
|
320
|
+
not isinstance(ref, str) or ":" not in ref or not isinstance(value, bool)
|
|
321
|
+
for ref, value in values.items())):
|
|
322
|
+
raise CorpusStoreError("personal state has invalid enabled_overrides")
|
|
323
|
+
return values
|
|
324
|
+
|
|
312
325
|
def _write_transaction(self, tx_id: str, record: dict[str, Any]) -> None:
|
|
313
326
|
_atomic_write(self.runtime / "transactions" / tx_id / "journal.json", record)
|
|
314
327
|
|
|
@@ -351,7 +364,7 @@ class CorpusStore:
|
|
|
351
364
|
journal["history_id"] = history_id
|
|
352
365
|
_atomic_write(journal_path, journal)
|
|
353
366
|
history = {"runtime": prior["runtime"], "user": prior["user"],
|
|
354
|
-
"revision": plan.get("expected_revision")}
|
|
367
|
+
"revision": plan.get("expected_revision"), "details": plan.get("details", {})}
|
|
355
368
|
history_path = self.user_root / "history" / history_id / "state.json"
|
|
356
369
|
if not history_path.exists():
|
|
357
370
|
_atomic_write(history_path, history)
|
|
@@ -701,7 +714,7 @@ class CorpusStore:
|
|
|
701
714
|
active = [item for item in items if item.get("active", True) is not False]
|
|
702
715
|
selection = self._effective_selection(user, defaults, None)
|
|
703
716
|
self._validate_snapshot_selection(selection, _inventory, active)
|
|
704
|
-
selected = self._selected_items(active, selection)
|
|
717
|
+
selected = self._selected_items(active, selection, self._enabled_overrides(user))
|
|
705
718
|
self._require_resolved(selected)
|
|
706
719
|
catalog.compile_items(_copy_json(selected), Path(temp) / host, host)
|
|
707
720
|
except Exception as exc:
|
|
@@ -896,18 +909,24 @@ class CorpusStore:
|
|
|
896
909
|
"revision": revision, "baseline_count": len(refs),
|
|
897
910
|
"personal_items": len(user["items"]), "overrides": len(user["overrides"]),
|
|
898
911
|
"tombstones": len(user["tombstones"]), "selection": self._effective_selection(user, defaults, None),
|
|
912
|
+
"enabled_overrides": _copy_json(self._enabled_overrides(user)),
|
|
899
913
|
}
|
|
900
914
|
|
|
901
915
|
def list_items(self, include_removed: bool = True) -> list[dict[str, Any]]:
|
|
902
916
|
with self._lock():
|
|
903
917
|
self._recover_locked()
|
|
904
918
|
runtime, user = self._runtime_state(), self._user_state()
|
|
905
|
-
selected_ref, inventory,
|
|
919
|
+
selected_ref, inventory, defaults = self._selected_baseline(runtime)
|
|
906
920
|
all_items, _, _, _ = self._effective_items(runtime, user)
|
|
907
921
|
effective = {item["ref"]: item for item in all_items}
|
|
908
922
|
for host in ("claude", "codex"):
|
|
909
923
|
effective.update({item["ref"]: item for item in self._effective_items(runtime, user, host=host, include_suppressed=True)[0]})
|
|
910
924
|
source = {item["ref"]: self._validate_item(item, allow_origin=True) for item in inventory["items"]}
|
|
925
|
+
overrides = self._enabled_overrides(user)
|
|
926
|
+
selection = self._effective_selection(user, defaults, None)
|
|
927
|
+
enabled = {item["ref"] for item in self._selected_items(
|
|
928
|
+
[item for item in effective.values() if item.get("active", True) is not False], selection, overrides)}
|
|
929
|
+
revision = self._authoring_revision(runtime, user)
|
|
911
930
|
rows: list[dict[str, Any]] = []
|
|
912
931
|
for ref in sorted(set(source) | set(user["items"]) | set(effective)):
|
|
913
932
|
base = source.get(ref)
|
|
@@ -919,6 +938,9 @@ class CorpusStore:
|
|
|
919
938
|
row["state"] = "removed" if removed else ("conflict" if item and (item.get("conflict") or item.get("content_conflict")) else "active")
|
|
920
939
|
row["digest"] = _digest(item or base or user["items"][ref])
|
|
921
940
|
row["baseline_ref"] = selected_ref if base else None
|
|
941
|
+
row["enabled"] = not removed and ref in enabled
|
|
942
|
+
row["enabled_override"] = overrides.get(ref)
|
|
943
|
+
row["revision"] = revision
|
|
922
944
|
rows.append(row)
|
|
923
945
|
return rows
|
|
924
946
|
|
|
@@ -944,7 +966,11 @@ class CorpusStore:
|
|
|
944
966
|
return {"ref": ref, "history": self.history(ref)}
|
|
945
967
|
removed = ref in user["tombstones"] or (effective is not None and effective.get("active", True) is False)
|
|
946
968
|
state = "removed" if removed else ("conflict" if effective and (effective.get("conflict") or effective.get("content_conflict")) else "active")
|
|
947
|
-
|
|
969
|
+
overrides = self._enabled_overrides(user)
|
|
970
|
+
enabled = bool(effective and not removed and self._selected_items(
|
|
971
|
+
[effective], self._effective_selection(user, _defaults, None), overrides))
|
|
972
|
+
return {"ref": ref, "item": effective, "digest": _digest(effective) if effective else None, "state": state,
|
|
973
|
+
"enabled": enabled, "enabled_override": overrides.get(ref)}
|
|
948
974
|
|
|
949
975
|
# ---- plans --------------------------------------------------------------
|
|
950
976
|
|
|
@@ -971,7 +997,7 @@ class CorpusStore:
|
|
|
971
997
|
if not isinstance(payload, dict):
|
|
972
998
|
raise ValidationError("plan payload must be an object")
|
|
973
999
|
op = payload.get("operation", payload.get("op"))
|
|
974
|
-
if op not in {"create", "update", "remove", "restore", "recover", "reset", "rollback", "select"}:
|
|
1000
|
+
if op not in {"create", "update", "remove", "restore", "recover", "reset", "rollback", "select", "enable"}:
|
|
975
1001
|
raise ValidationError("unknown corpus operation")
|
|
976
1002
|
allowed = {
|
|
977
1003
|
"create": {"operation", "op", "item", "package_id", "expected_revision"},
|
|
@@ -982,6 +1008,7 @@ class CorpusStore:
|
|
|
982
1008
|
"reset": {"operation", "op", "expected_revision"},
|
|
983
1009
|
"rollback": {"operation", "op", "baseline_ref", "history_id", "expected_revision"},
|
|
984
1010
|
"select": {"operation", "op", "selection", "expected_revision"},
|
|
1011
|
+
"enable": {"operation", "op", "items", "expected_revision"},
|
|
985
1012
|
}[op]
|
|
986
1013
|
unknown = set(payload) - allowed
|
|
987
1014
|
if unknown:
|
|
@@ -1114,6 +1141,31 @@ class CorpusStore:
|
|
|
1114
1141
|
else:
|
|
1115
1142
|
next_user["items"][ref].pop("active", None)
|
|
1116
1143
|
details["ref"] = ref
|
|
1144
|
+
elif op == "enable":
|
|
1145
|
+
choices = payload.get("items")
|
|
1146
|
+
if not isinstance(choices, dict) or not choices:
|
|
1147
|
+
raise ValidationError("enable needs a non-empty mapping of refs to booleans or null")
|
|
1148
|
+
available = dict(effective)
|
|
1149
|
+
for host in ("claude", "codex"):
|
|
1150
|
+
available.update({item["ref"]: item for item in self._effective_items(runtime, user, host=host)[0]})
|
|
1151
|
+
overrides = dict(self._enabled_overrides(user))
|
|
1152
|
+
known = set(available) | set(overrides) | set(user["items"]) | {item["ref"] for item in inventory["items"]}
|
|
1153
|
+
for ref, value in choices.items():
|
|
1154
|
+
if not isinstance(ref, str) or ref not in known:
|
|
1155
|
+
raise ValidationError(f"unknown corpus enablement ref: {ref}")
|
|
1156
|
+
if value is None:
|
|
1157
|
+
overrides.pop(ref, None)
|
|
1158
|
+
elif not isinstance(value, bool):
|
|
1159
|
+
raise ValidationError("enable values must be booleans or null")
|
|
1160
|
+
elif ref not in available or available[ref].get("active", True) is False:
|
|
1161
|
+
raise ValidationError("recover or restore a removed item before enabling it")
|
|
1162
|
+
else:
|
|
1163
|
+
overrides[ref] = value
|
|
1164
|
+
if overrides:
|
|
1165
|
+
next_user["enabled_overrides"] = overrides
|
|
1166
|
+
else:
|
|
1167
|
+
next_user.pop("enabled_overrides", None)
|
|
1168
|
+
details["items"] = _copy_json(choices)
|
|
1117
1169
|
elif op == "select":
|
|
1118
1170
|
selection = self._validate_selection(payload.get("selection"), inventory)
|
|
1119
1171
|
next_user["selection"] = selection
|
|
@@ -1208,7 +1260,8 @@ class CorpusStore:
|
|
|
1208
1260
|
history_id = f"{prepared['prepared_at'].replace(':', '').replace('+00:00', 'Z')}-{plan_id[:12]}"
|
|
1209
1261
|
prepared["history_id"] = history_id
|
|
1210
1262
|
self._write_transaction(plan_id, prepared)
|
|
1211
|
-
_atomic_write(self.user_root / "history" / history_id / "state.json",
|
|
1263
|
+
_atomic_write(self.user_root / "history" / history_id / "state.json",
|
|
1264
|
+
{"runtime": runtime, "user": user, "revision": current, "details": details})
|
|
1212
1265
|
if details.get("operation") == "reset":
|
|
1213
1266
|
_atomic_write(self.user_root / "trash" / history_id / "state.json", {"runtime": runtime, "user": user, "revision": current})
|
|
1214
1267
|
_atomic_write(self._user_state_path, next_user)
|
|
@@ -1225,11 +1278,18 @@ class CorpusStore:
|
|
|
1225
1278
|
|
|
1226
1279
|
# ---- immutable snapshots and history -----------------------------------
|
|
1227
1280
|
|
|
1228
|
-
def _selected_items(self, items: list[dict[str, Any]], selection: list[str] | None
|
|
1229
|
-
|
|
1230
|
-
|
|
1281
|
+
def _selected_items(self, items: list[dict[str, Any]], selection: list[str] | None,
|
|
1282
|
+
overrides: dict[str, bool] | None = None) -> list[dict[str, Any]]:
|
|
1283
|
+
overrides = overrides or {}
|
|
1231
1284
|
selected: list[dict[str, Any]] = []
|
|
1232
1285
|
for item in items:
|
|
1286
|
+
if item["ref"] in overrides:
|
|
1287
|
+
if overrides[item["ref"]]:
|
|
1288
|
+
selected.append(item)
|
|
1289
|
+
continue
|
|
1290
|
+
if selection and "all" in selection:
|
|
1291
|
+
selected.append(item)
|
|
1292
|
+
continue
|
|
1233
1293
|
if item.get("tier") in {"core", "infra"}:
|
|
1234
1294
|
selected.append(item)
|
|
1235
1295
|
continue
|
|
@@ -1268,7 +1328,7 @@ class CorpusStore:
|
|
|
1268
1328
|
active = [item for item in items if item.get("active", True) is not False]
|
|
1269
1329
|
effective_selection = self._effective_selection(user, defaults, selection)
|
|
1270
1330
|
self._validate_snapshot_selection(effective_selection, inventory, active)
|
|
1271
|
-
selected = self._selected_items(active, effective_selection)
|
|
1331
|
+
selected = self._selected_items(active, effective_selection, self._enabled_overrides(user))
|
|
1272
1332
|
self._require_resolved(selected)
|
|
1273
1333
|
selected, promotion_warnings = self._resolve_promotions(selected, user, host, baseline_ref)
|
|
1274
1334
|
bootstrap_path = self.repo / "compose" / "bootstrap" / "SKILL.md"
|
|
@@ -1372,7 +1432,11 @@ class CorpusStore:
|
|
|
1372
1432
|
continue
|
|
1373
1433
|
if ref is not None:
|
|
1374
1434
|
user = record.get("user", {})
|
|
1375
|
-
|
|
1435
|
+
details = record.get("details", {})
|
|
1436
|
+
changed = details.get("items", {}) if details.get("operation") == "enable" else {}
|
|
1437
|
+
if (ref not in user.get("items", {}) and ref not in user.get("overrides", {})
|
|
1438
|
+
and ref not in user.get("tombstones", {}) and ref not in user.get("enabled_overrides", {})
|
|
1439
|
+
and ref not in changed):
|
|
1376
1440
|
continue
|
|
1377
1441
|
rows.append({"history_id": path.parent.name, "revision": record.get("revision"), "path": str(path.parent)})
|
|
1378
1442
|
return rows
|
package/compose/corpus_ui.py
CHANGED
|
@@ -18,6 +18,7 @@ from rich.text import Text
|
|
|
18
18
|
from textual.app import App, ComposeResult
|
|
19
19
|
from textual.binding import Binding
|
|
20
20
|
from textual.containers import Horizontal, Vertical
|
|
21
|
+
from textual.events import Key
|
|
21
22
|
from textual.screen import ModalScreen
|
|
22
23
|
from textual.widgets import (
|
|
23
24
|
Button,
|
|
@@ -43,6 +44,10 @@ SURFACES = ("always", "relevant", "requested", "event", "delegated")
|
|
|
43
44
|
VIEWS = ("effective", "installed", "change", "diff", "history")
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
def availability_label(state: str) -> str:
|
|
48
|
+
return "available" if state == "active" else state
|
|
49
|
+
|
|
50
|
+
|
|
46
51
|
def guide_pointers(item: dict[str, Any], inventory: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
47
52
|
"""Display literal guide references, not inferred dependencies or new routing."""
|
|
48
53
|
if item.get("kind") != "rule":
|
|
@@ -63,6 +68,8 @@ def guide_pointers(item: dict[str, Any], inventory: list[dict[str, Any]]) -> lis
|
|
|
63
68
|
def library_label(item: dict[str, Any], inventory: list[dict[str, Any]]) -> Text:
|
|
64
69
|
pointers = guide_pointers(item, inventory)
|
|
65
70
|
label = Text()
|
|
71
|
+
mark = "-" if item.get("state") == "removed" else "x" if item.get("enabled", True) else " "
|
|
72
|
+
label.append(f"[{mark}]" + ("* " if item.get("enabled_pending") else " "))
|
|
66
73
|
if pointers:
|
|
67
74
|
label.append("→ GUIDE ", style="bold cyan")
|
|
68
75
|
label.append(", ".join(PurePosixPath(pointer["path"]).stem for pointer in pointers))
|
|
@@ -112,6 +119,31 @@ class Confirmation(ModalScreen[bool]):
|
|
|
112
119
|
class CorpusMarkdownViewer(MarkdownViewer):
|
|
113
120
|
"""Keep all links inside Studio; never hand a URL to the operating system."""
|
|
114
121
|
|
|
122
|
+
BINDINGS = [
|
|
123
|
+
Binding("left", "left_or_library", "Library", show=False),
|
|
124
|
+
Binding("up", "up_or_controls", "Controls", show=False),
|
|
125
|
+
Binding("down", "down_or_pending", "Pending choices", show=False),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
def action_left_or_library(self) -> None:
|
|
129
|
+
if self.scroll_x <= 0:
|
|
130
|
+
self.app.action_focus_library()
|
|
131
|
+
else:
|
|
132
|
+
self.action_scroll_left()
|
|
133
|
+
|
|
134
|
+
def action_up_or_controls(self) -> None:
|
|
135
|
+
if self.scroll_y <= 0:
|
|
136
|
+
if not self.app._focus_id("member-select"):
|
|
137
|
+
self.app._focus_controls()
|
|
138
|
+
else:
|
|
139
|
+
self.action_scroll_up()
|
|
140
|
+
|
|
141
|
+
def action_down_or_pending(self) -> None:
|
|
142
|
+
if self.scroll_y >= self.max_scroll_y:
|
|
143
|
+
self.app._focus_id("enablement-preview")
|
|
144
|
+
else:
|
|
145
|
+
self.action_scroll_down()
|
|
146
|
+
|
|
115
147
|
async def _on_markdown_link_clicked(self, message: Markdown.LinkClicked) -> None:
|
|
116
148
|
# stop() prevents bubbling, not the inherited MarkdownViewer handler.
|
|
117
149
|
# Its default go() would try to load corpus:// as a filesystem path.
|
|
@@ -124,6 +156,57 @@ class CorpusMarkdownViewer(MarkdownViewer):
|
|
|
124
156
|
self.app.notify("External links are disabled in Corpus Studio.", severity="warning")
|
|
125
157
|
|
|
126
158
|
|
|
159
|
+
class CorpusSearch(Input):
|
|
160
|
+
"""Leave search with arrows without taking editing keys away from text."""
|
|
161
|
+
|
|
162
|
+
BINDINGS = [
|
|
163
|
+
Binding("down", "focus_controls", "Controls", show=False),
|
|
164
|
+
Binding("right", "right_or_view", "View", show=False),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
def action_focus_controls(self) -> None:
|
|
168
|
+
self.app._focus_controls()
|
|
169
|
+
|
|
170
|
+
def action_right_or_view(self) -> None:
|
|
171
|
+
if self.selection.start == self.selection.end == len(self.value):
|
|
172
|
+
self.app._focus_id("view-select")
|
|
173
|
+
else:
|
|
174
|
+
self.action_cursor_right()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class CorpusTree(Tree):
|
|
178
|
+
"""Space is local to the library, never intercepted in text inputs."""
|
|
179
|
+
|
|
180
|
+
BINDINGS = [
|
|
181
|
+
Binding("space", "toggle_item", "On/off"),
|
|
182
|
+
Binding("up", "up_or_controls", "Controls", show=False),
|
|
183
|
+
Binding("down", "down_or_pending", "Pending choices", show=False),
|
|
184
|
+
Binding("right", "focus_document", "Document", show=False),
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
def action_up_or_controls(self) -> None:
|
|
188
|
+
if self.cursor_line <= 0:
|
|
189
|
+
self.app._focus_controls()
|
|
190
|
+
else:
|
|
191
|
+
self.action_cursor_up()
|
|
192
|
+
|
|
193
|
+
def action_down_or_pending(self) -> None:
|
|
194
|
+
before = self.cursor_node
|
|
195
|
+
self.action_cursor_down()
|
|
196
|
+
if self.cursor_node is before:
|
|
197
|
+
self.app._focus_id("enablement-preview")
|
|
198
|
+
|
|
199
|
+
def action_focus_document(self) -> None:
|
|
200
|
+
self.app._focus_document()
|
|
201
|
+
|
|
202
|
+
async def action_toggle_item(self) -> None:
|
|
203
|
+
node = self.cursor_node
|
|
204
|
+
if node is not None and isinstance(node.data, str):
|
|
205
|
+
await self.app.toggle_enabled(node.data)
|
|
206
|
+
else:
|
|
207
|
+
self.action_toggle_node()
|
|
208
|
+
|
|
209
|
+
|
|
127
210
|
class CorpusStudio(App[None]):
|
|
128
211
|
"""Searchable corpus library, editor, and revision-bound Preview → Apply flow."""
|
|
129
212
|
|
|
@@ -169,6 +252,9 @@ class CorpusStudio(App[None]):
|
|
|
169
252
|
.actions { height: 3; align-horizontal: right; }
|
|
170
253
|
.actions Button { margin-left: 1; }
|
|
171
254
|
#status-line { height: 1; padding: 0 1; color: $text-muted; }
|
|
255
|
+
#enablement-bar { height: 3; display: none; }
|
|
256
|
+
#enablement-summary { width: 1fr; padding: 1; }
|
|
257
|
+
#enablement-bar Button { margin-right: 1; }
|
|
172
258
|
"""
|
|
173
259
|
|
|
174
260
|
def __init__(self, store: Any) -> None:
|
|
@@ -183,13 +269,16 @@ class CorpusStudio(App[None]):
|
|
|
183
269
|
self.view_item: dict[str, Any] | None = None
|
|
184
270
|
self.member_drafts: dict[str, str] = {}
|
|
185
271
|
self.pending_plan: dict[str, Any] | None = None
|
|
272
|
+
self.toggle_drafts: dict[str, bool] = {}
|
|
273
|
+
self.toggle_original: dict[str, bool] = {}
|
|
274
|
+
self.toggle_revision: str | None = None
|
|
186
275
|
self.mode = "view"
|
|
187
276
|
|
|
188
277
|
def compose(self) -> ComposeResult:
|
|
189
278
|
yield Header()
|
|
190
279
|
with Vertical(id="toolbar"):
|
|
191
280
|
with Horizontal(id="search-row"):
|
|
192
|
-
yield
|
|
281
|
+
yield CorpusSearch(placeholder="Search title, content, ref, package, domain, or state (↓ controls)", id="search")
|
|
193
282
|
yield Select([(view.title(), view) for view in VIEWS], value="effective",
|
|
194
283
|
allow_blank=False, id="view-select")
|
|
195
284
|
with Horizontal(id="action-row"):
|
|
@@ -200,7 +289,7 @@ class CorpusStudio(App[None]):
|
|
|
200
289
|
yield Button("Recover", id="recover", compact=True)
|
|
201
290
|
yield Button("Reset", id="reset", variant="error", compact=True)
|
|
202
291
|
with Horizontal(id="workspace"):
|
|
203
|
-
yield
|
|
292
|
+
yield CorpusTree("Corpus", id="library")
|
|
204
293
|
with Vertical(id="detail"):
|
|
205
294
|
yield Select([], prompt="Bundle document", allow_blank=True, id="member-select")
|
|
206
295
|
yield CorpusMarkdownViewer(
|
|
@@ -240,6 +329,10 @@ class CorpusStudio(App[None]):
|
|
|
240
329
|
with Horizontal(classes="actions"):
|
|
241
330
|
yield Button("Cancel plan", id="plan-cancel")
|
|
242
331
|
yield Button("Apply", id="apply", variant="success")
|
|
332
|
+
with Horizontal(id="enablement-bar"):
|
|
333
|
+
yield Static("", id="enablement-summary", markup=False)
|
|
334
|
+
yield Button("Preview on/off", id="enablement-preview", variant="primary")
|
|
335
|
+
yield Button("Discard on/off", id="enablement-discard")
|
|
243
336
|
yield Static("Stored privately; no host session is activated by this screen.", id="status-line")
|
|
244
337
|
yield Footer()
|
|
245
338
|
|
|
@@ -247,6 +340,63 @@ class CorpusStudio(App[None]):
|
|
|
247
340
|
await self.refresh_library()
|
|
248
341
|
self.query_one("#search", Input).focus()
|
|
249
342
|
|
|
343
|
+
@staticmethod
|
|
344
|
+
def _focusable(widget: Any) -> bool:
|
|
345
|
+
return widget.focusable and all(getattr(node, "display", True) for node in [widget, *widget.ancestors])
|
|
346
|
+
|
|
347
|
+
def _focus_id(self, *ids: str) -> bool:
|
|
348
|
+
# Query the current screen only: arrows must never escape a modal.
|
|
349
|
+
for ident in ids:
|
|
350
|
+
for widget in self.screen.query(f"#{ident}"):
|
|
351
|
+
if self._focusable(widget):
|
|
352
|
+
widget.focus()
|
|
353
|
+
return True
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
def _focus_controls(self) -> None:
|
|
357
|
+
for button in self.screen.query("#action-row Button"):
|
|
358
|
+
if self._focusable(button):
|
|
359
|
+
button.focus()
|
|
360
|
+
return
|
|
361
|
+
self._focus_id("library" if getattr(self.focused, "id", None) == "search" else "search")
|
|
362
|
+
|
|
363
|
+
def _focus_document(self) -> None:
|
|
364
|
+
if self.mode != "view" or self._focus_id("member-select"):
|
|
365
|
+
return
|
|
366
|
+
for viewer in self.screen.query("#wiki"):
|
|
367
|
+
if self._focusable(viewer.document):
|
|
368
|
+
viewer.document.focus()
|
|
369
|
+
|
|
370
|
+
def on_key(self, event: Key) -> None:
|
|
371
|
+
focused = self.focused
|
|
372
|
+
moved = False
|
|
373
|
+
if isinstance(focused, Button) and focused.parent is not None:
|
|
374
|
+
siblings = [button for button in focused.parent.query(Button)
|
|
375
|
+
if button.parent is focused.parent and self._focusable(button)]
|
|
376
|
+
if event.key in {"left", "right"} and focused in siblings:
|
|
377
|
+
index = siblings.index(focused) + (-1 if event.key == "left" else 1)
|
|
378
|
+
if 0 <= index < len(siblings):
|
|
379
|
+
siblings[index].focus()
|
|
380
|
+
moved = True
|
|
381
|
+
elif event.key in {"up", "down"} and len(self.screen_stack) == 1:
|
|
382
|
+
if focused.parent.id == "action-row":
|
|
383
|
+
moved = self._focus_id("search" if event.key == "up" else "library")
|
|
384
|
+
elif focused.parent.id == "enablement-bar":
|
|
385
|
+
moved = self._focus_id("library")
|
|
386
|
+
elif self.mode == "editor" and event.key == "up":
|
|
387
|
+
moved = self._focus_id("editor-body")
|
|
388
|
+
elif isinstance(focused, Select) and not focused.expanded and self.mode == "view":
|
|
389
|
+
if event.key == "left" and focused.id in {"view-select", "member-select"}:
|
|
390
|
+
moved = self._focus_id("search" if focused.id == "view-select" else "library")
|
|
391
|
+
elif event.key == "right" and focused.id == "member-select":
|
|
392
|
+
for viewer in self.screen.query("#wiki"):
|
|
393
|
+
if self._focusable(viewer.document):
|
|
394
|
+
viewer.document.focus()
|
|
395
|
+
moved = True
|
|
396
|
+
if moved:
|
|
397
|
+
event.prevent_default()
|
|
398
|
+
event.stop()
|
|
399
|
+
|
|
250
400
|
def _set_mode(self, mode: str) -> None:
|
|
251
401
|
self.mode = mode
|
|
252
402
|
self.query_one("#member-select", Select).disabled = mode == "preview"
|
|
@@ -257,6 +407,83 @@ class CorpusStudio(App[None]):
|
|
|
257
407
|
self.query_one("#wiki").display = mode == "view"
|
|
258
408
|
self.query_one("#editor-panel").display = mode == "editor"
|
|
259
409
|
self.query_one("#preview-panel").display = mode == "preview"
|
|
410
|
+
self._toggle_controls()
|
|
411
|
+
# Hiding a panel does not reliably release its focused child. Choose a
|
|
412
|
+
# visible, non-destructive entry point for the next screen state.
|
|
413
|
+
if mode == "preview":
|
|
414
|
+
self._focus_id("plan-cancel")
|
|
415
|
+
elif mode == "view":
|
|
416
|
+
self._focus_id("enablement-preview", "library")
|
|
417
|
+
|
|
418
|
+
def _display_items(self) -> list[dict[str, Any]]:
|
|
419
|
+
return [dict(row, enabled=self.toggle_drafts[row["ref"]], enabled_pending=True)
|
|
420
|
+
if row.get("ref") in self.toggle_drafts else row for row in self.items]
|
|
421
|
+
|
|
422
|
+
def _toggle_controls(self) -> None:
|
|
423
|
+
pending = bool(self.toggle_drafts)
|
|
424
|
+
self.query_one("#enablement-bar").display = pending and self.mode == "view"
|
|
425
|
+
self.query_one("#enablement-summary", Static).update(
|
|
426
|
+
f"{len(self.toggle_drafts)} unapplied on/off change(s) · * = pending")
|
|
427
|
+
if self.mode == "view":
|
|
428
|
+
for selector in ("#create", "#reset"):
|
|
429
|
+
self.query_one(selector).disabled = pending
|
|
430
|
+
if pending:
|
|
431
|
+
for selector in ("#edit", "#remove", "#restore", "#recover"):
|
|
432
|
+
self.query_one(selector).disabled = True
|
|
433
|
+
|
|
434
|
+
async def _repaint_enablement(self) -> None:
|
|
435
|
+
displayed = self._display_items()
|
|
436
|
+
by_ref = {row["ref"]: row for row in displayed}
|
|
437
|
+
nodes = [self.query_one("#library", Tree).root]
|
|
438
|
+
while nodes:
|
|
439
|
+
node = nodes.pop()
|
|
440
|
+
if isinstance(node.data, str) and node.data in by_ref:
|
|
441
|
+
node.set_label(library_label(by_ref[node.data], displayed))
|
|
442
|
+
nodes.extend(node.children)
|
|
443
|
+
self._toggle_controls()
|
|
444
|
+
if self.current_ref:
|
|
445
|
+
await self.select_ref(self.current_ref)
|
|
446
|
+
|
|
447
|
+
async def toggle_enabled(self, ref: str) -> None:
|
|
448
|
+
if self.mode != "view":
|
|
449
|
+
return
|
|
450
|
+
row = self._row(ref)
|
|
451
|
+
if row is None or row.get("state") == "removed":
|
|
452
|
+
self.notify("Recover or restore this item before changing its use.", severity="warning")
|
|
453
|
+
return
|
|
454
|
+
if not self.toggle_drafts:
|
|
455
|
+
self.toggle_revision = row["revision"]
|
|
456
|
+
original = self.toggle_original.setdefault(ref, row["enabled"])
|
|
457
|
+
value = not self.toggle_drafts.get(ref, row["enabled"])
|
|
458
|
+
if value == original:
|
|
459
|
+
self.toggle_drafts.pop(ref, None)
|
|
460
|
+
self.toggle_original.pop(ref, None)
|
|
461
|
+
else:
|
|
462
|
+
self.toggle_drafts[ref] = value
|
|
463
|
+
if not self.toggle_drafts:
|
|
464
|
+
self.toggle_revision = None
|
|
465
|
+
await self._repaint_enablement()
|
|
466
|
+
|
|
467
|
+
def _clear_toggle_drafts(self) -> None:
|
|
468
|
+
self.toggle_drafts.clear()
|
|
469
|
+
self.toggle_original.clear()
|
|
470
|
+
self.toggle_revision = None
|
|
471
|
+
|
|
472
|
+
async def _discard_toggles(self) -> None:
|
|
473
|
+
self._clear_toggle_drafts()
|
|
474
|
+
self._toggle_controls()
|
|
475
|
+
await self.refresh_library(self.query_one("#search", Input).value)
|
|
476
|
+
if self.current_ref:
|
|
477
|
+
await self.select_ref(self.current_ref)
|
|
478
|
+
self._focus_id("library")
|
|
479
|
+
|
|
480
|
+
def _preview_toggles(self) -> None:
|
|
481
|
+
if not self.toggle_drafts or self.mode != "view":
|
|
482
|
+
return
|
|
483
|
+
summary = "Use in future sessions (no content is deleted):\n" + "\n".join(
|
|
484
|
+
f"{'ON' if enabled else 'OFF'} {ref}" for ref, enabled in self.toggle_drafts.items())
|
|
485
|
+
self._stage_plan({"operation": "enable", "items": dict(self.toggle_drafts),
|
|
486
|
+
"expected_revision": self.toggle_revision}, summary)
|
|
260
487
|
|
|
261
488
|
async def refresh_library(self, query: str = "") -> None:
|
|
262
489
|
if self.mode != "view":
|
|
@@ -272,20 +499,22 @@ class CorpusStudio(App[None]):
|
|
|
272
499
|
self.query_one("#status-line", Static).update(str(exc))
|
|
273
500
|
return
|
|
274
501
|
visible = [item for item in self.items if self._matches(item, query)]
|
|
502
|
+
displayed = self._display_items()
|
|
503
|
+
display_by_ref = {item["ref"]: item for item in displayed}
|
|
275
504
|
groups: dict[tuple[str, str], Any] = {}
|
|
276
505
|
state_nodes: dict[str, Any] = {}
|
|
277
506
|
for item in visible:
|
|
278
507
|
state = str(item.get("state", "active"))
|
|
279
508
|
package = str(item.get("package_id", "unknown"))
|
|
280
509
|
if state not in state_nodes:
|
|
281
|
-
state_nodes[state] = tree.root.add(state.title())
|
|
510
|
+
state_nodes[state] = tree.root.add(availability_label(state).title())
|
|
282
511
|
state_nodes[state].expand()
|
|
283
512
|
key = (state, package)
|
|
284
513
|
if key not in groups:
|
|
285
514
|
groups[key] = state_nodes[state].add(package)
|
|
286
515
|
groups[key].expand()
|
|
287
516
|
groups[key].add_leaf(
|
|
288
|
-
library_label(item,
|
|
517
|
+
library_label(display_by_ref[item["ref"]], displayed),
|
|
289
518
|
data=item.get("ref"),
|
|
290
519
|
)
|
|
291
520
|
tree.root.expand()
|
|
@@ -296,6 +525,7 @@ class CorpusStudio(App[None]):
|
|
|
296
525
|
)
|
|
297
526
|
if visible and (self.current_ref is None or not any(i.get("ref") == self.current_ref for i in visible)):
|
|
298
527
|
await self.select_ref(str(visible[0]["ref"]))
|
|
528
|
+
self._toggle_controls()
|
|
299
529
|
|
|
300
530
|
@staticmethod
|
|
301
531
|
def _matches(item: dict[str, Any], query: str) -> bool:
|
|
@@ -342,6 +572,7 @@ class CorpusStudio(App[None]):
|
|
|
342
572
|
self.query_one("#remove", Button).disabled = row.get("state") == "removed"
|
|
343
573
|
self.query_one("#restore", Button).disabled = not bool(row.get("baseline_ref"))
|
|
344
574
|
self.query_one("#recover", Button).disabled = not self._recoverable(row)
|
|
575
|
+
self._toggle_controls()
|
|
345
576
|
|
|
346
577
|
def _select_members(self, item: dict[str, Any] | None, preferred: str | None = None) -> None:
|
|
347
578
|
selector = self.query_one("#member-select", Select)
|
|
@@ -376,7 +607,9 @@ class CorpusStudio(App[None]):
|
|
|
376
607
|
+ (f" Available members: {members}." if members else "") + "\n"
|
|
377
608
|
)
|
|
378
609
|
body = render_member_body(item.get("body", ""), item.get("primary_member"), item.get("kind"))
|
|
379
|
-
|
|
610
|
+
displayed = self._display_items()
|
|
611
|
+
display_row = next((entry for entry in displayed if entry.get("ref") == item.get("ref")), row)
|
|
612
|
+
pointers = guide_pointers(item, displayed)
|
|
380
613
|
guide_links = ""
|
|
381
614
|
if pointers:
|
|
382
615
|
lines = []
|
|
@@ -386,8 +619,9 @@ class CorpusStudio(App[None]):
|
|
|
386
619
|
lines.append(f"- `{pointer['path']}` — {pointer['problem']}")
|
|
387
620
|
else:
|
|
388
621
|
destination = quote(target["ref"], safe="@/:")
|
|
622
|
+
use = "ON" if target.get("enabled", True) else "OFF — linked guide disabled"
|
|
389
623
|
lines.append(f"- [{pointer['path']}](corpus://{destination}) — "
|
|
390
|
-
f"**{target['surface']}** · {target.get('state', 'active')}")
|
|
624
|
+
f"**{target['surface']}** · {availability_label(target.get('state', 'active'))} · {use}")
|
|
391
625
|
guide_links = (
|
|
392
626
|
"## Guide pointer\n\nThis rule explicitly references the following guide(s). "
|
|
393
627
|
"The rule keeps its own consumption surface; these links do not change "
|
|
@@ -395,8 +629,11 @@ class CorpusStudio(App[None]):
|
|
|
395
629
|
)
|
|
396
630
|
return (
|
|
397
631
|
f"# {item.get('title', item.get('ref'))}\n\n"
|
|
398
|
-
f"`{item.get('ref')}` · **{row.get('state', 'active')}** · "
|
|
632
|
+
f"`{item.get('ref')}` · **{availability_label(row.get('state', 'active'))}** · "
|
|
399
633
|
f"{item.get('surface')} · {item.get('kind')}\n\n"
|
|
634
|
+
f"Use in future sessions: **{'ON' if display_row.get('enabled', True) else 'OFF'}**"
|
|
635
|
+
+ (" (pending; not applied)" if display_row.get("enabled_pending") else " (saved selection)")
|
|
636
|
+
+ ". Inclusion is not proof of loading or permission to execute.\n\n"
|
|
400
637
|
f"{guide_links}{reconciliation}\n{body}\n\n## Dependencies\n\n{links}\n"
|
|
401
638
|
+ ("\n## Native consumption\n\nRequires explicit `agent-launch --corpus-native`.\n"
|
|
402
639
|
if item.get("kind") == "hook" else "")
|
|
@@ -408,6 +645,26 @@ class CorpusStudio(App[None]):
|
|
|
408
645
|
if isinstance(event.node.data, str):
|
|
409
646
|
await self.select_ref(event.node.data)
|
|
410
647
|
|
|
648
|
+
async def on_tree_node_highlighted(self, event: Tree.NodeHighlighted) -> None:
|
|
649
|
+
tree = self.query_one("#library", Tree)
|
|
650
|
+
# Ignore queued highlights superseded by another cursor move or tree rebuild.
|
|
651
|
+
if self.mode != "view" or event.node is not tree.cursor_node:
|
|
652
|
+
return
|
|
653
|
+
if isinstance(event.node.data, str):
|
|
654
|
+
await self.select_ref(event.node.data)
|
|
655
|
+
return
|
|
656
|
+
# A group is not the previously displayed item: never leave its edit/delete
|
|
657
|
+
# actions live while the cursor points at a different kind of node.
|
|
658
|
+
self.current_ref = None
|
|
659
|
+
self.view_item = None
|
|
660
|
+
self._select_members(None)
|
|
661
|
+
for selector in ("#edit", "#remove", "#restore", "#recover"):
|
|
662
|
+
self.query_one(selector, Button).disabled = True
|
|
663
|
+
label = str(event.node.label.plain)
|
|
664
|
+
await self.query_one("#wiki", CorpusMarkdownViewer).document.update(
|
|
665
|
+
f"# Corpus group\n\n{label}\n\nMove the cursor to a corpus item to read it."
|
|
666
|
+
)
|
|
667
|
+
|
|
411
668
|
async def on_markdown_link_clicked(self, message: Markdown.LinkClicked) -> None:
|
|
412
669
|
# CorpusMarkdownViewer normally handles this before it bubbles. Keeping
|
|
413
670
|
# the app handler makes direct Markdown messages safe in tests and future layouts.
|
|
@@ -459,6 +716,9 @@ class CorpusStudio(App[None]):
|
|
|
459
716
|
def _open_editor(self, item: dict[str, Any] | None) -> None:
|
|
460
717
|
if self.mode != "view":
|
|
461
718
|
return
|
|
719
|
+
if self.toggle_drafts:
|
|
720
|
+
self.notify("Apply or discard on/off changes first.", severity="warning")
|
|
721
|
+
return
|
|
462
722
|
preferred = self.current_member if item and item.get("ref") == self.current_ref else None
|
|
463
723
|
self.editor_item = item
|
|
464
724
|
self.editor_ref = str(item["ref"]) if item else None
|
|
@@ -577,6 +837,8 @@ class CorpusStudio(App[None]):
|
|
|
577
837
|
async def action_request_quit(self) -> None:
|
|
578
838
|
if self.editor_dirty():
|
|
579
839
|
self.push_screen(Confirmation("Discard the unsaved draft and exit Studio?"), self._discard_and_exit)
|
|
840
|
+
elif self.toggle_drafts:
|
|
841
|
+
self.push_screen(Confirmation("Discard unapplied on/off changes and exit Studio?"), self._discard_and_exit)
|
|
580
842
|
else:
|
|
581
843
|
self.exit()
|
|
582
844
|
|
|
@@ -637,6 +899,9 @@ class CorpusStudio(App[None]):
|
|
|
637
899
|
}, diff + hook_summary
|
|
638
900
|
|
|
639
901
|
def _stage_plan(self, payload: dict[str, Any], summary: str) -> None:
|
|
902
|
+
if self.toggle_drafts and payload.get("operation") != "enable":
|
|
903
|
+
self.notify("Apply or discard on/off changes first.", severity="warning")
|
|
904
|
+
return
|
|
640
905
|
try:
|
|
641
906
|
plan = self.store.plan(payload)
|
|
642
907
|
except Exception as exc:
|
|
@@ -665,6 +930,8 @@ class CorpusStudio(App[None]):
|
|
|
665
930
|
self.notify(str(exc), severity="error")
|
|
666
931
|
return
|
|
667
932
|
ref = result.get("details", {}).get("ref")
|
|
933
|
+
if result.get("details", {}).get("operation") == "enable":
|
|
934
|
+
self._clear_toggle_drafts()
|
|
668
935
|
self.pending_plan = None
|
|
669
936
|
self._set_mode("view")
|
|
670
937
|
await self.refresh_library(self.query_one("#search", Input).value)
|
|
@@ -674,7 +941,11 @@ class CorpusStudio(App[None]):
|
|
|
674
941
|
|
|
675
942
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
676
943
|
button = event.button.id
|
|
677
|
-
if button == "
|
|
944
|
+
if button == "enablement-preview":
|
|
945
|
+
self._preview_toggles()
|
|
946
|
+
elif button == "enablement-discard":
|
|
947
|
+
await self._discard_toggles()
|
|
948
|
+
elif button == "create":
|
|
678
949
|
self.action_create()
|
|
679
950
|
elif button == "edit":
|
|
680
951
|
self.action_edit()
|
|
@@ -144,7 +144,12 @@ class CorpusUnderstand:
|
|
|
144
144
|
return value
|
|
145
145
|
|
|
146
146
|
def _bundles(self) -> list[dict]:
|
|
147
|
-
|
|
147
|
+
# Projection preferences and the inventory read revision are not learning
|
|
148
|
+
# content. Disabled items remain readable and learnable without repinning
|
|
149
|
+
# every bundle when an unrelated activation preference changes.
|
|
150
|
+
rows = [{key: value for key, value in x.items()
|
|
151
|
+
if key not in {"enabled", "enabled_override", "revision"}}
|
|
152
|
+
for x in self.store.list_items(include_removed=False) if x.get("state") == "active"]
|
|
148
153
|
bundles, assigned = [], set()
|
|
149
154
|
for ident, title, purpose, numbers in CORE_BUNDLES:
|
|
150
155
|
wanted = {f"rule-{n:03}" for n in numbers}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bios",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"releaseDate": "2026-09-11",
|
|
5
5
|
"description": "A thin, low-level instruction layer for LLM CLI agents: one set of principles and behavior whichever model you run. Stores a private, editable corpus for explicitly activated sessions.",
|
|
6
6
|
"bin": {
|
package/provenance.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"588260e5b60f52f0f58b47e8d2ef7ae567b382a6","committedAt":"2026-09-11T21:09:27+09:00","dirty":false}
|