agent-bios 0.19.0 → 0.19.2
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 +27 -27
- package/INSTALL.md +4 -4
- package/README.md +58 -28
- package/claude/CLAUDE.md +1 -1
- package/claude/guides/claude-prompting.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +3 -3
- package/claude/guides/documentation-hygiene.md +3 -0
- package/claude/guides/gpt-prompting.md +1 -1
- package/claude/guides/korean-writing.md +153 -0
- package/claude/guides/learning-flow.md +4 -4
- package/claude/guides/session-distill-workflow.md +8 -8
- package/claude/guides/slide-writing/RUNBOOK.md +5 -5
- package/claude/skills/repo-charter/SKILL.md +3 -3
- package/claude/skills/understand/SKILL.md +58 -28
- package/codex/AGENTS.md +1 -1
- package/codex/guides/claude-prompting.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +3 -3
- package/codex/guides/documentation-hygiene.md +3 -0
- package/codex/guides/gpt-prompting.md +1 -1
- package/codex/guides/korean-writing.md +153 -0
- package/codex/guides/learning-flow.md +4 -4
- package/codex/guides/session-distill-workflow.md +8 -8
- package/codex/guides/slide-writing/RUNBOOK.md +5 -5
- package/compose/app_bridge/SKILL.md +12 -12
- package/compose/app_bridge/scripts/bridge.py +15 -7
- package/compose/assemble.py +5 -5
- package/compose/bootstrap/SKILL.md +18 -18
- package/compose/canary.sh +4 -4
- package/compose/check-domains.py +6 -6
- package/compose/corpus-state.py +16 -1168
- package/compose/corpus.py +13 -402
- package/compose/corpus_app.py +14 -450
- package/compose/corpus_catalog.py +15 -926
- package/compose/corpus_import.py +14 -523
- package/compose/corpus_install.py +14 -1841
- package/compose/corpus_session.py +16 -848
- package/compose/corpus_setup.py +16 -670
- package/compose/corpus_setup_cli.py +15 -577
- package/compose/corpus_setup_i18n.py +20 -318
- package/compose/corpus_setup_ui.py +18 -631
- package/compose/corpus_store.py +16 -1621
- package/compose/corpus_transaction.py +15 -284
- package/compose/corpus_ui.py +17 -972
- package/compose/corpus_ui_runtime.py +16 -274
- package/compose/corpus_understand.py +13 -520
- package/compose/domains.json +2 -1
- package/compose/instructions-state.py +1175 -0
- package/compose/instructions.py +409 -0
- package/compose/instructions_app.py +464 -0
- package/compose/instructions_catalog.py +931 -0
- package/compose/instructions_import.py +529 -0
- package/compose/instructions_install.py +1866 -0
- package/compose/instructions_session.py +852 -0
- package/compose/instructions_setup.py +676 -0
- package/compose/instructions_setup_cli.py +586 -0
- package/compose/instructions_setup_i18n.py +324 -0
- package/compose/instructions_setup_ui.py +647 -0
- package/compose/instructions_store.py +1668 -0
- package/compose/instructions_transaction.py +306 -0
- package/compose/instructions_ui.py +975 -0
- package/compose/instructions_ui_runtime.py +278 -0
- package/compose/instructions_understand.py +678 -0
- package/compose/register-hooks.py +1 -1
- package/compose/setup/START.md +24 -13
- package/docs/advanced-launch.md +11 -11
- package/docs/instructions-compatibility.md +86 -0
- package/docs/{corpus.md → instructions.md} +35 -8
- package/docs/recovery.md +8 -8
- package/docs/releases/0.19.2.md +38 -0
- package/docs/session-model.md +23 -20
- package/docs/setup.md +42 -25
- package/docs/understand.md +57 -9
- package/install.sh +70 -69
- package/launch/agent-launch.py +306 -295
- package/launch/agent-launch.toml +2 -2
- package/launch/i18n/en.toml +55 -55
- package/launch/i18n/ja.toml +56 -56
- package/launch/i18n/ko.toml +56 -56
- package/launch/shell_integration.py +4 -4
- package/learn/collect-learning.py +10 -10
- package/learn/learning.schema.json +1 -1
- package/learn/migrate-learnings.py +51 -51
- package/package.json +27 -11
- package/provenance.json +1 -1
- /package/docs/assets/{corpus-studio.svg → instructions-studio.svg} +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Instructions Studio and machine CLI over the private, revision-checked instructions store.
|
|
3
|
+
|
|
4
|
+
The command is deliberately a client. It renders records and translates user
|
|
5
|
+
input into semantic plan payloads; ``InstructionsStore`` remains the only writer and
|
|
6
|
+
owns ids, revisions, paths, serialization, validation, and publication.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import difflib
|
|
12
|
+
import importlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import shlex
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
from typing import Any, Sequence
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from instructions_store import InstructionsStore, InstructionsStoreError
|
|
24
|
+
except ImportError: # package-style import in tests
|
|
25
|
+
from .instructions_store import InstructionsStore, InstructionsStoreError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
SURFACES = ("always", "relevant", "requested", "event", "delegated")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def default_repo() -> Path:
|
|
32
|
+
return Path(os.environ.get("AGENT_BIOS_REPO", Path(__file__).resolve().parents[1]))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def venv_python() -> Path | None:
|
|
36
|
+
roots: list[Path] = []
|
|
37
|
+
if override := os.environ.get("AGENT_LAUNCH_VENV"):
|
|
38
|
+
roots.append(Path(override).expanduser())
|
|
39
|
+
roots.append(Path.home() / ".local/share/agent-launch/venv")
|
|
40
|
+
for root in roots:
|
|
41
|
+
interpreter = root / "bin" / "python"
|
|
42
|
+
if interpreter.is_file() and os.access(interpreter, os.X_OK):
|
|
43
|
+
return interpreter
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
48
|
+
parser = argparse.ArgumentParser(
|
|
49
|
+
prog="agent-bios instructions",
|
|
50
|
+
description="Inspect and change the private instructions used by future activated sessions.",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument("--repo", type=Path, default=default_repo())
|
|
53
|
+
parser.add_argument("--state-dir", type=Path)
|
|
54
|
+
parser.add_argument("--user-dir", type=Path)
|
|
55
|
+
parser.add_argument("--json", action="store_true", dest="as_json")
|
|
56
|
+
sub = parser.add_subparsers(dest="command")
|
|
57
|
+
|
|
58
|
+
list_cmd = sub.add_parser("list", help="list instructions items")
|
|
59
|
+
list_cmd.add_argument("--active-only", action="store_true")
|
|
60
|
+
|
|
61
|
+
search = sub.add_parser("search", help="search item text and metadata")
|
|
62
|
+
search.add_argument("query")
|
|
63
|
+
search.add_argument("--active-only", action="store_true")
|
|
64
|
+
|
|
65
|
+
show = sub.add_parser("show", help="show one instructions item")
|
|
66
|
+
show.add_argument("ref")
|
|
67
|
+
show.add_argument(
|
|
68
|
+
"--view", default="effective",
|
|
69
|
+
choices=("effective", "installed", "change", "diff", "history"),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
plan = sub.add_parser("plan", help="validate a semantic change without publishing it")
|
|
73
|
+
plan.add_argument("--input", default="-", metavar="FILE")
|
|
74
|
+
|
|
75
|
+
apply_cmd = sub.add_parser("apply", help="publish a previously validated plan")
|
|
76
|
+
apply_cmd.add_argument("plan")
|
|
77
|
+
apply_cmd.add_argument("--expected-revision")
|
|
78
|
+
|
|
79
|
+
history = sub.add_parser("history", help="list recoverable authoring revisions")
|
|
80
|
+
history.add_argument("ref", nargs="?")
|
|
81
|
+
|
|
82
|
+
sub.add_parser("status", help="show private store state")
|
|
83
|
+
|
|
84
|
+
snapshot = sub.add_parser("snapshot", help="compose current authoring or inspect an immutable snapshot")
|
|
85
|
+
snapshot_view = snapshot.add_mutually_exclusive_group(required=True)
|
|
86
|
+
snapshot_view.add_argument("--host", choices=("claude", "codex"))
|
|
87
|
+
snapshot_view.add_argument("--content-ref", help="read this stored snapshot without resolving current authoring")
|
|
88
|
+
snapshot.add_argument("--native", action="store_true", help="opt into selected native hooks on either host and Claude agents for this snapshot")
|
|
89
|
+
snapshot.add_argument("--selection-mode", choices=("default", "selected", "none"))
|
|
90
|
+
snapshot.add_argument("--select", action="append", help="one-session package/domain/item selection")
|
|
91
|
+
snapshot.add_argument("--cwd", type=Path, help="project scope for this snapshot")
|
|
92
|
+
|
|
93
|
+
install = sub.add_parser("install", help="record the current package as a private baseline")
|
|
94
|
+
install.add_argument("--domains", help="comma-separated qualified selection values")
|
|
95
|
+
return parser
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _load_json(source: str) -> dict[str, Any]:
|
|
99
|
+
try:
|
|
100
|
+
text = sys.stdin.read() if source == "-" else Path(source).read_text(encoding="utf-8")
|
|
101
|
+
payload = json.loads(text)
|
|
102
|
+
except (OSError, ValueError) as exc:
|
|
103
|
+
raise InstructionsStoreError(f"cannot read plan input {source!r}: {exc}") from exc
|
|
104
|
+
if not isinstance(payload, dict):
|
|
105
|
+
raise InstructionsStoreError("plan input must be one JSON object")
|
|
106
|
+
return payload
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _matches(item: dict[str, Any], query: str) -> bool:
|
|
110
|
+
needle = query.casefold()
|
|
111
|
+
values: list[str] = []
|
|
112
|
+
for key in ("ref", "title", "body", "surface", "tier", "kind", "state", "package_id"):
|
|
113
|
+
value = item.get(key)
|
|
114
|
+
if isinstance(value, str):
|
|
115
|
+
values.append(value)
|
|
116
|
+
values.extend(str(value) for value in item.get("domains", []) if isinstance(value, str))
|
|
117
|
+
return needle in "\n".join(values).casefold()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def search_items(store: InstructionsStore, query: str, *, include_removed: bool = True) -> list[dict[str, Any]]:
|
|
121
|
+
return [item for item in store.list_items(include_removed=include_removed) if _matches(item, query)]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _human_item(item: dict[str, Any]) -> str:
|
|
125
|
+
state = item.get("state", "active")
|
|
126
|
+
return f"{item.get('ref', '?')}\t{state}\t{item.get('surface', '?')}\t{item.get('title', '')}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _human_show(result: dict[str, Any]) -> str:
|
|
130
|
+
item = result.get("item")
|
|
131
|
+
if isinstance(item, dict):
|
|
132
|
+
metadata = (
|
|
133
|
+
f"`{item.get('ref', result.get('ref', ''))}` · {result.get('state', 'active')} · "
|
|
134
|
+
f"{item.get('surface', '?')} · {item.get('kind', '?')}"
|
|
135
|
+
)
|
|
136
|
+
return f"# {item.get('title', item.get('ref', 'Instructions item'))}\n\n{metadata}\n\n{item.get('body', '')}"
|
|
137
|
+
return json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def emit(value: Any, *, as_json: bool, command: str) -> None:
|
|
141
|
+
if as_json:
|
|
142
|
+
print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
|
|
143
|
+
return
|
|
144
|
+
if command in {"list", "search"} and isinstance(value, list):
|
|
145
|
+
for item in value:
|
|
146
|
+
print(_human_item(item))
|
|
147
|
+
elif command == "show" and isinstance(value, dict):
|
|
148
|
+
print(_human_show(value))
|
|
149
|
+
elif command == "history" and isinstance(value, list):
|
|
150
|
+
for row in value:
|
|
151
|
+
print(f"{row.get('history_id', '?')}\t{row.get('revision', '?')}\t{row.get('path', '')}")
|
|
152
|
+
elif command == "status" and isinstance(value, dict):
|
|
153
|
+
for key, current in value.items():
|
|
154
|
+
print(f"{key}: {json.dumps(current, ensure_ascii=False)}")
|
|
155
|
+
elif command == "snapshot" and isinstance(value, dict):
|
|
156
|
+
print(f"content_ref: {value.get('content_ref')}\npath: {value.get('path')}\nrevision: {value.get('revision')}")
|
|
157
|
+
if value.get("unavailable"):
|
|
158
|
+
print("activation unverified or unavailable:")
|
|
159
|
+
for row in value["unavailable"]:
|
|
160
|
+
print(f"- {row.get('ref')}: {row.get('reason')}")
|
|
161
|
+
else:
|
|
162
|
+
print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def run_command(args: argparse.Namespace, store: InstructionsStore) -> Any:
|
|
166
|
+
command = args.command
|
|
167
|
+
if command == "list":
|
|
168
|
+
return store.list_items(include_removed=not args.active_only)
|
|
169
|
+
if command == "search":
|
|
170
|
+
return search_items(store, args.query, include_removed=not args.active_only)
|
|
171
|
+
if command == "show":
|
|
172
|
+
return store.show(args.ref, view=args.view)
|
|
173
|
+
if command == "plan":
|
|
174
|
+
return store.plan(_load_json(args.input))
|
|
175
|
+
if command == "apply":
|
|
176
|
+
return store.apply(args.plan, expected_revision=args.expected_revision)
|
|
177
|
+
if command == "history":
|
|
178
|
+
return store.history(args.ref)
|
|
179
|
+
if command == "status":
|
|
180
|
+
return store.status()
|
|
181
|
+
if command == "snapshot":
|
|
182
|
+
if args.content_ref:
|
|
183
|
+
if args.native or args.selection_mode or args.select or args.cwd:
|
|
184
|
+
raise InstructionsStoreError("a stored snapshot is immutable; selection and scope options require --host")
|
|
185
|
+
return store.snapshot_inventory(args.content_ref)
|
|
186
|
+
return store.snapshot(args.host, selection=args.select, native=args.native,
|
|
187
|
+
selection_mode=args.selection_mode or ("selected" if args.select is not None else None),
|
|
188
|
+
cwd=args.cwd)
|
|
189
|
+
if command == "install":
|
|
190
|
+
domains = None
|
|
191
|
+
if args.domains is not None:
|
|
192
|
+
domains = [part.strip() for part in args.domains.split(",") if part.strip()]
|
|
193
|
+
if domains == ["none"]:
|
|
194
|
+
domains = []
|
|
195
|
+
return store.install(domains=domains)
|
|
196
|
+
raise InstructionsStoreError(f"unknown instructions command: {command}")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _draft_text(initial: str = "") -> str:
|
|
200
|
+
editor = os.environ.get("VISUAL") or os.environ.get("EDITOR")
|
|
201
|
+
if editor:
|
|
202
|
+
with tempfile.TemporaryDirectory(prefix="agent-bios-instructions-draft-") as directory:
|
|
203
|
+
path = Path(directory) / "draft.md"
|
|
204
|
+
path.write_text(initial, encoding="utf-8")
|
|
205
|
+
try:
|
|
206
|
+
completed = subprocess.run([*shlex.split(editor), str(path)], check=False)
|
|
207
|
+
except OSError as exc:
|
|
208
|
+
raise InstructionsStoreError(f"cannot run editor: {exc}") from exc
|
|
209
|
+
if completed.returncode != 0:
|
|
210
|
+
raise InstructionsStoreError(f"editor exited with status {completed.returncode}")
|
|
211
|
+
return path.read_text(encoding="utf-8")
|
|
212
|
+
print("Enter Markdown. Finish with a line containing only '.'")
|
|
213
|
+
if initial:
|
|
214
|
+
print("Current body follows; enter a complete replacement.")
|
|
215
|
+
print(initial)
|
|
216
|
+
lines: list[str] = []
|
|
217
|
+
while True:
|
|
218
|
+
line = input()
|
|
219
|
+
if line == ".":
|
|
220
|
+
return "\n".join(lines) + ("\n" if lines else "")
|
|
221
|
+
lines.append(line)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _choose_surface(current: str = "requested") -> str:
|
|
225
|
+
print("Consumption surface:")
|
|
226
|
+
for index, surface in enumerate(SURFACES, 1):
|
|
227
|
+
marker = " *" if surface == current else ""
|
|
228
|
+
print(f" {index}. {surface}{marker}")
|
|
229
|
+
raw = input(f"Select [default {SURFACES.index(current) + 1}]: ").strip()
|
|
230
|
+
if not raw:
|
|
231
|
+
return current
|
|
232
|
+
try:
|
|
233
|
+
return SURFACES[int(raw) - 1]
|
|
234
|
+
except (ValueError, IndexError) as exc:
|
|
235
|
+
raise InstructionsStoreError("invalid surface selection") from exc
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _preview_and_apply(store: InstructionsStore, payload: dict[str, Any], preview: str) -> None:
|
|
239
|
+
plan = store.plan(payload)
|
|
240
|
+
print(preview)
|
|
241
|
+
print(json.dumps(plan, ensure_ascii=False, indent=2, sort_keys=True))
|
|
242
|
+
if input("Apply this plan? [y/N]: ").strip().casefold() not in {"y", "yes"}:
|
|
243
|
+
print("Plan was not applied.")
|
|
244
|
+
return
|
|
245
|
+
applied = store.apply(plan["plan_id"], expected_revision=plan["expected_revision"])
|
|
246
|
+
print(json.dumps(applied, ensure_ascii=False, indent=2, sort_keys=True))
|
|
247
|
+
print("Future activated sessions use the new authoring state; this running session is unchanged.")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def run_numbered(store: InstructionsStore) -> int:
|
|
251
|
+
"""Useful fallback when Textual is unavailable; canonical writes still use the store."""
|
|
252
|
+
while True:
|
|
253
|
+
items = store.list_items(include_removed=True)
|
|
254
|
+
print("\nInstructions Studio (numbered fallback)")
|
|
255
|
+
for index, item in enumerate(items, 1):
|
|
256
|
+
print(f" {index}. {_human_item(item)}")
|
|
257
|
+
print(" c. create e. edit r. remove s. restore v. recover x. reset /. search q. quit")
|
|
258
|
+
choice = input("Choice: ").strip()
|
|
259
|
+
if choice.casefold() == "q":
|
|
260
|
+
return 0
|
|
261
|
+
if choice == "/":
|
|
262
|
+
query = input("Search: ")
|
|
263
|
+
for item in search_items(store, query):
|
|
264
|
+
print(_human_item(item))
|
|
265
|
+
continue
|
|
266
|
+
if choice.casefold() == "c":
|
|
267
|
+
title = input("Title: ").strip()
|
|
268
|
+
body = _draft_text()
|
|
269
|
+
surface = _choose_surface()
|
|
270
|
+
item = {"title": title, "body": body, "surface": surface,
|
|
271
|
+
"tier": "env-personal", "domains": ["personal"], "kind": "rule",
|
|
272
|
+
"members": {"content.md": body}}
|
|
273
|
+
_preview_and_apply(store, {"operation": "create", "item": item}, f"Create {title!r} on {surface}.")
|
|
274
|
+
continue
|
|
275
|
+
if choice.casefold() == "x":
|
|
276
|
+
_preview_and_apply(store, {"operation": "reset"}, "Reset future instructions authoring to the last successful install tuple.")
|
|
277
|
+
continue
|
|
278
|
+
if choice.casefold() in {"e", "r", "s", "v"}:
|
|
279
|
+
ref = input("InstructionsRef: ").strip()
|
|
280
|
+
row = next((item for item in items if item.get("ref") == ref), None)
|
|
281
|
+
if row is None:
|
|
282
|
+
print("Unknown InstructionsRef.", file=sys.stderr)
|
|
283
|
+
continue
|
|
284
|
+
if choice.casefold() == "e":
|
|
285
|
+
body = _draft_text(str(row.get("body", "")))
|
|
286
|
+
surface = _choose_surface(str(row.get("surface", "requested")))
|
|
287
|
+
patch = {"body": body, "surface": surface}
|
|
288
|
+
if row.get("kind") == "hook":
|
|
289
|
+
try:
|
|
290
|
+
from instructions_catalog import HOOK_EVENTS
|
|
291
|
+
except ImportError: # package-style import from repository root
|
|
292
|
+
from .instructions_catalog import HOOK_EVENTS
|
|
293
|
+
binding = row.get("hook") if isinstance(row.get("hook"), dict) else {}
|
|
294
|
+
default_event = str(binding.get("event", sorted(HOOK_EVENTS)[0]))
|
|
295
|
+
while True:
|
|
296
|
+
event = input(f"Hook event [{default_event}]: ").strip() or default_event
|
|
297
|
+
if event in HOOK_EVENTS:
|
|
298
|
+
break
|
|
299
|
+
print("Unsupported hook event.", file=sys.stderr)
|
|
300
|
+
default_matcher = str(binding.get("matcher", ""))
|
|
301
|
+
matcher = input(f"Hook matcher [{default_matcher}]: ")
|
|
302
|
+
matcher = matcher if matcher else default_matcher
|
|
303
|
+
if not matcher or "\n" in matcher or "\r" in matcher:
|
|
304
|
+
print("Hook matcher must be one non-empty line.", file=sys.stderr)
|
|
305
|
+
continue
|
|
306
|
+
patch["hook"] = {"event": event, "matcher": matcher}
|
|
307
|
+
diff = "".join(difflib.unified_diff(
|
|
308
|
+
str(row.get("body", "")).splitlines(True), body.splitlines(True),
|
|
309
|
+
fromfile="current", tofile="planned",
|
|
310
|
+
)) or "(metadata-only change)"
|
|
311
|
+
hook_preview = (f"\nHook binding: {patch['hook']['event']} / {patch['hook']['matcher']}"
|
|
312
|
+
if "hook" in patch else "")
|
|
313
|
+
_preview_and_apply(store, {"operation": "update", "ref": ref,
|
|
314
|
+
"item_digest": row["digest"], "patch": patch}, diff + hook_preview)
|
|
315
|
+
elif choice.casefold() == "r":
|
|
316
|
+
_preview_and_apply(store, {"operation": "remove", "ref": ref,
|
|
317
|
+
"item_digest": row.get("digest")}, f"Remove {ref} from future snapshots.")
|
|
318
|
+
elif choice.casefold() == "s":
|
|
319
|
+
_preview_and_apply(store, {"operation": "restore", "ref": ref},
|
|
320
|
+
f"Restore {ref} from the selected installed baseline.")
|
|
321
|
+
else:
|
|
322
|
+
_preview_and_apply(store, {"operation": "recover", "ref": ref},
|
|
323
|
+
f"Recover personal item {ref} for future snapshots.")
|
|
324
|
+
continue
|
|
325
|
+
try:
|
|
326
|
+
index = int(choice) - 1
|
|
327
|
+
print(_human_show(store.show(items[index]["ref"])))
|
|
328
|
+
except (ValueError, IndexError):
|
|
329
|
+
print("Invalid choice.", file=sys.stderr)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _run_tui_or_fallback(args: argparse.Namespace, store: InstructionsStore) -> int:
|
|
333
|
+
module_name = f"{__package__}.instructions_ui" if __package__ else "instructions_ui"
|
|
334
|
+
try:
|
|
335
|
+
module = importlib.import_module(module_name)
|
|
336
|
+
except ImportError as exc:
|
|
337
|
+
missing_ui_dependency = any(exc.name == name or (exc.name or "").startswith(name + ".")
|
|
338
|
+
for name in ("textual", "rich"))
|
|
339
|
+
if not missing_ui_dependency and exc.name not in {"instructions_ui", module_name}:
|
|
340
|
+
raise
|
|
341
|
+
interpreter = venv_python()
|
|
342
|
+
if (
|
|
343
|
+
missing_ui_dependency
|
|
344
|
+
and interpreter is not None
|
|
345
|
+
and Path(sys.executable).absolute() != interpreter.absolute()
|
|
346
|
+
and os.environ.get("AGENT_BIOS_INSTRUCTIONS_TUI_REEXEC") != "1"
|
|
347
|
+
):
|
|
348
|
+
env = os.environ.copy()
|
|
349
|
+
env["AGENT_BIOS_INSTRUCTIONS_TUI_REEXEC"] = "1"
|
|
350
|
+
try:
|
|
351
|
+
os.execve(
|
|
352
|
+
str(interpreter),
|
|
353
|
+
[str(interpreter), str(Path(__file__).resolve()), *sys.argv[1:]],
|
|
354
|
+
env,
|
|
355
|
+
)
|
|
356
|
+
except OSError as reexec_error:
|
|
357
|
+
print(
|
|
358
|
+
f"agent-bios instructions: managed Textual runtime is unusable ({reexec_error}); "
|
|
359
|
+
"using numbered fallback.",
|
|
360
|
+
file=sys.stderr,
|
|
361
|
+
)
|
|
362
|
+
return run_numbered(store)
|
|
363
|
+
app = module.InstructionsStudio(store)
|
|
364
|
+
app.run()
|
|
365
|
+
return 0
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _run_bundled_tui(store: InstructionsStore) -> int:
|
|
369
|
+
try:
|
|
370
|
+
from instructions_ui_runtime import activate_ui_runtime
|
|
371
|
+
activate_ui_runtime(Path(__file__).resolve().parents[1])
|
|
372
|
+
module_name = f"{__package__}.instructions_ui" if __package__ else "instructions_ui"
|
|
373
|
+
module = importlib.import_module(module_name)
|
|
374
|
+
except (ImportError, OSError, RuntimeError) as exc:
|
|
375
|
+
raise InstructionsStoreError(f"bundled terminal UI could not start: {exc}") from exc
|
|
376
|
+
app = module.InstructionsStudio(store)
|
|
377
|
+
app.run()
|
|
378
|
+
return 0
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def main(argv: Sequence[str] | None = None, *, bundled_ui: bool = False) -> int:
|
|
382
|
+
parser = build_parser()
|
|
383
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
384
|
+
# Machine callers naturally put --json after the verb. argparse only accepts
|
|
385
|
+
# a parent option before a subparser, so normalize this one order-independent
|
|
386
|
+
# presentation flag without changing any semantic argument.
|
|
387
|
+
json_requested = "--json" in raw
|
|
388
|
+
raw = [value for value in raw if value != "--json"]
|
|
389
|
+
args = parser.parse_args(raw)
|
|
390
|
+
args.as_json = args.as_json or json_requested
|
|
391
|
+
store = InstructionsStore(args.repo, state_root=args.state_dir, user_root=args.user_dir)
|
|
392
|
+
try:
|
|
393
|
+
if args.command is None:
|
|
394
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
395
|
+
if bundled_ui:
|
|
396
|
+
return _run_bundled_tui(store)
|
|
397
|
+
return _run_tui_or_fallback(args, store)
|
|
398
|
+
args.command = "list"
|
|
399
|
+
args.active_only = False
|
|
400
|
+
result = run_command(args, store)
|
|
401
|
+
emit(result, as_json=args.as_json, command=args.command)
|
|
402
|
+
return 0
|
|
403
|
+
except InstructionsStoreError as exc:
|
|
404
|
+
print(f"agent-bios instructions: {exc}", file=sys.stderr)
|
|
405
|
+
return 2
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
raise SystemExit(main(bundled_ui=True))
|