agent-bios 0.19.1 → 0.19.3

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.
Files changed (106) hide show
  1. package/DEPENDENCIES.md +58 -30
  2. package/INSTALL.md +4 -4
  3. package/README.md +105 -28
  4. package/claude/CLAUDE.md +1 -1
  5. package/claude/guides/claude-prompting.md +1 -1
  6. package/claude/guides/cli-multi-model-workflow.md +4 -4
  7. package/claude/guides/coding-staged-workflow.md +17 -0
  8. package/claude/guides/documentation-hygiene.md +3 -0
  9. package/claude/guides/gpt-prompting.md +1 -1
  10. package/claude/guides/korean-writing.md +153 -0
  11. package/claude/guides/learning-flow.md +4 -4
  12. package/claude/guides/llm-capability-boundary.md +7 -1
  13. package/claude/guides/session-distill-workflow.md +8 -8
  14. package/claude/guides/slide-writing/RUNBOOK.md +5 -5
  15. package/claude/guides/tooling-gotchas.md +20 -1
  16. package/claude/guides/ui-design/visual-direction.md +88 -0
  17. package/claude/guides/ui-design.md +90 -0
  18. package/claude/guides/verification-discipline.md +10 -1
  19. package/claude/hooks/tooling-gotchas-hook.py +41 -0
  20. package/claude/skills/repo-charter/SKILL.md +3 -3
  21. package/claude/skills/understand/SKILL.md +5 -5
  22. package/codex/AGENTS.md +1 -1
  23. package/codex/guides/claude-prompting.md +1 -1
  24. package/codex/guides/cli-multi-model-workflow.md +4 -4
  25. package/codex/guides/coding-staged-workflow.md +17 -0
  26. package/codex/guides/documentation-hygiene.md +3 -0
  27. package/codex/guides/gpt-prompting.md +1 -1
  28. package/codex/guides/korean-writing.md +153 -0
  29. package/codex/guides/learning-flow.md +4 -4
  30. package/codex/guides/llm-capability-boundary.md +7 -1
  31. package/codex/guides/session-distill-workflow.md +8 -8
  32. package/codex/guides/slide-writing/RUNBOOK.md +5 -5
  33. package/codex/guides/tooling-gotchas.md +20 -1
  34. package/codex/guides/ui-design/visual-direction.md +88 -0
  35. package/codex/guides/ui-design.md +90 -0
  36. package/codex/guides/verification-discipline.md +10 -1
  37. package/compose/app_bridge/SKILL.md +12 -12
  38. package/compose/app_bridge/scripts/bridge.py +35 -10
  39. package/compose/app_desktop/server.py +250 -0
  40. package/compose/assemble.py +5 -5
  41. package/compose/bootstrap/SKILL.md +18 -18
  42. package/compose/canary.sh +4 -4
  43. package/compose/check-domains.py +6 -6
  44. package/compose/corpus-state.py +16 -1168
  45. package/compose/corpus.py +13 -402
  46. package/compose/corpus_app.py +14 -450
  47. package/compose/corpus_catalog.py +15 -926
  48. package/compose/corpus_import.py +14 -523
  49. package/compose/corpus_install.py +14 -1847
  50. package/compose/corpus_session.py +16 -848
  51. package/compose/corpus_setup.py +16 -672
  52. package/compose/corpus_setup_cli.py +15 -580
  53. package/compose/corpus_setup_i18n.py +20 -324
  54. package/compose/corpus_setup_ui.py +18 -645
  55. package/compose/corpus_store.py +16 -1664
  56. package/compose/corpus_transaction.py +15 -284
  57. package/compose/corpus_ui.py +17 -972
  58. package/compose/corpus_ui_runtime.py +16 -274
  59. package/compose/corpus_understand.py +13 -671
  60. package/compose/domains.json +3 -1
  61. package/compose/host_platform.py +121 -0
  62. package/compose/instructions-state.py +1178 -0
  63. package/compose/instructions.py +409 -0
  64. package/compose/instructions_app.py +697 -0
  65. package/compose/instructions_catalog.py +931 -0
  66. package/compose/instructions_import.py +537 -0
  67. package/compose/instructions_install.py +1932 -0
  68. package/compose/instructions_session.py +852 -0
  69. package/compose/instructions_setup.py +713 -0
  70. package/compose/instructions_setup_cli.py +607 -0
  71. package/compose/instructions_setup_i18n.py +327 -0
  72. package/compose/instructions_setup_ui.py +647 -0
  73. package/compose/instructions_store.py +1668 -0
  74. package/compose/instructions_transaction.py +308 -0
  75. package/compose/instructions_ui.py +975 -0
  76. package/compose/instructions_ui_runtime.py +279 -0
  77. package/compose/instructions_understand.py +678 -0
  78. package/compose/native_cli.py +52 -0
  79. package/compose/register-hooks.py +1 -1
  80. package/compose/runtime_entry.py +58 -0
  81. package/compose/setup/START.md +11 -11
  82. package/compose/windows_deploy.py +719 -0
  83. package/docs/advanced-launch.md +11 -11
  84. package/docs/instructions-compatibility.md +86 -0
  85. package/docs/{corpus.md → instructions.md} +36 -8
  86. package/docs/recovery.md +10 -10
  87. package/docs/releases/0.19.2.md +38 -0
  88. package/docs/releases/0.19.3.md +107 -0
  89. package/docs/session-model.md +31 -20
  90. package/docs/setup.md +63 -26
  91. package/docs/understand.md +6 -6
  92. package/docs/windows.md +99 -0
  93. package/install.sh +71 -69
  94. package/launch/agent-launch.py +309 -293
  95. package/launch/agent-launch.toml +2 -2
  96. package/launch/agent-launch.zsh +11 -1
  97. package/launch/i18n/en.toml +55 -55
  98. package/launch/i18n/ja.toml +56 -56
  99. package/launch/i18n/ko.toml +56 -56
  100. package/launch/shell_integration.py +4 -4
  101. package/learn/collect-learning.py +10 -10
  102. package/learn/learning.schema.json +1 -1
  103. package/learn/migrate-learnings.py +51 -51
  104. package/package.json +33 -12
  105. package/provenance.json +1 -1
  106. /package/docs/assets/{corpus-studio.svg → instructions-studio.svg} +0 -0
@@ -0,0 +1,308 @@
1
+ """Shared lock and durable pending-operation visibility for instructions state.
2
+
3
+ The instructions store owns source bytes and the installer owns projections. Neither
4
+ may publish independently while an install, reset or migration is incomplete: a reader
5
+ must see the completed generation or a clear recovery requirement, never a
6
+ mixture. Journals deliberately contain no token bytes.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import contextlib
11
+ import base64
12
+ try:
13
+ from host_platform import file_locks as fcntl, redirected
14
+ except ImportError:
15
+ from .host_platform import file_locks as fcntl, redirected
16
+ import hashlib
17
+ import json
18
+ import os
19
+ from pathlib import Path
20
+ import stat
21
+ import sys
22
+ import threading
23
+ from typing import Any, Iterator
24
+
25
+
26
+ PENDING_STATES = frozenset({"PREPARED", "APPLYING", "NEEDS_RECOVERY"})
27
+ _local = threading.local()
28
+
29
+ # Both public import spellings must share the same reentrant lock and operation
30
+ # scope. Separate module objects can deadlock when a new installer calls an old
31
+ # release's store while already holding this process's lock.
32
+ if __name__ in {"instructions_transaction", "compose.instructions_transaction"}:
33
+ for _alias in ("instructions_transaction", "compose.instructions_transaction",
34
+ "corpus_transaction", "compose.corpus_transaction"):
35
+ sys.modules[_alias] = sys.modules[__name__]
36
+
37
+
38
+ class TransactionError(RuntimeError):
39
+ pass
40
+
41
+
42
+ class TransactionPendingError(TransactionError):
43
+ pass
44
+
45
+
46
+ def environment_value(env, canonical: str, legacy: str, default=None):
47
+ """Resolve one renamed environment setting without silently splitting state."""
48
+ current, previous = env.get(canonical), env.get(legacy)
49
+ if current is not None and previous is not None and current != previous:
50
+ raise TransactionError(f"conflicting {canonical} and {legacy}; set one value")
51
+ return current if current is not None else previous if previous is not None else default
52
+
53
+
54
+ def reject_symlink_ancestors(path: Path) -> None:
55
+ """Refuse redirected user paths, allowing only macOS's fixed system aliases.
56
+
57
+ /var, /tmp and /etc are root-owned aliases to /private on macOS. They precede
58
+ ordinary temporary HOME/state roots and are not user-controlled redirections.
59
+ The exception names their exact destinations and never applies to the leaf,
60
+ a same-named link elsewhere, or a user-owned link.
61
+ """
62
+ for ancestor in (path, *path.parents):
63
+ if not redirected(ancestor):
64
+ continue
65
+ system_alias = (sys.platform == "darwin" and ancestor != path
66
+ and ancestor in {Path("/var"), Path("/tmp"), Path("/etc")}
67
+ and ancestor.lstat().st_uid == 0
68
+ and ancestor.parent / os.readlink(ancestor) == Path("/private") / ancestor.name)
69
+ if not system_alias:
70
+ raise TransactionError(f"refusing symlink transaction target: {ancestor}")
71
+
72
+
73
+ def _key(state_root: Path) -> str:
74
+ return str(Path(state_root).expanduser().resolve())
75
+
76
+
77
+ def _depths(name: str) -> dict[str, int]:
78
+ value = getattr(_local, name, None)
79
+ if value is None:
80
+ value = {}
81
+ setattr(_local, name, value)
82
+ return value
83
+
84
+
85
+ @contextlib.contextmanager
86
+ def _transaction_lock(state_root: Path, *, readonly: bool) -> Iterator[bool]:
87
+ root = Path(state_root).expanduser()
88
+ if redirected(root):
89
+ raise TransactionError(f"unsafe instructions state root: {root}")
90
+ reject_symlink_ancestors(root)
91
+ key = _key(root)
92
+ depths = _depths("lock_depths")
93
+ if depths.get(key, 0):
94
+ depths[key] += 1
95
+ try:
96
+ yield True
97
+ finally:
98
+ depths[key] -= 1
99
+ return
100
+ if not readonly:
101
+ root.mkdir(parents=True, exist_ok=True, mode=0o700)
102
+ # Schema-v1 lock identity is retained so old and new writers serialize together.
103
+ lock = root / ".corpus-store.lock"
104
+ if lock.is_symlink():
105
+ raise TransactionError(f"unsafe instructions transaction lock: {lock}")
106
+ flags = ((os.O_RDWR if os.name == "nt" else os.O_RDONLY) | getattr(os, "O_NONBLOCK", 0) if readonly else os.O_CREAT | os.O_RDWR) | getattr(os, "O_NOFOLLOW", 0)
107
+ try:
108
+ descriptor = os.open(lock, flags, 0o600)
109
+ except FileNotFoundError:
110
+ if not readonly:
111
+ raise
112
+ yield False
113
+ return
114
+ try:
115
+ if readonly and not stat.S_ISREG(os.fstat(descriptor).st_mode):
116
+ raise TransactionError(f"unsafe instructions transaction lock: {lock}")
117
+ try:
118
+ fcntl.flock(descriptor, fcntl.LOCK_EX | (fcntl.LOCK_NB if readonly else 0))
119
+ except BlockingIOError:
120
+ if not readonly:
121
+ raise
122
+ yield False
123
+ return
124
+ depths[key] = 1
125
+ try:
126
+ yield True
127
+ finally:
128
+ depths.pop(key, None)
129
+ fcntl.flock(descriptor, fcntl.LOCK_UN)
130
+ finally:
131
+ os.close(descriptor)
132
+
133
+
134
+ @contextlib.contextmanager
135
+ def transaction_lock(state_root: Path) -> Iterator[None]:
136
+ """The single re-entrant cross-process lock for store and installer work."""
137
+ with _transaction_lock(state_root, readonly=False):
138
+ yield
139
+
140
+
141
+ @contextlib.contextmanager
142
+ def try_transaction_lock(state_root: Path) -> Iterator[bool]:
143
+ """Observe under the existing lock, or defer without blocking or creating state."""
144
+ with _transaction_lock(state_root, readonly=True) as acquired:
145
+ yield acquired
146
+
147
+
148
+ @contextlib.contextmanager
149
+ def operation_scope(state_root: Path) -> Iterator[None]:
150
+ """Permit the coordinating writer to inspect its own pending journal."""
151
+ key = _key(Path(state_root))
152
+ depths = _depths("operation_depths")
153
+ depths[key] = depths.get(key, 0) + 1
154
+ try:
155
+ yield
156
+ finally:
157
+ remaining = depths[key] - 1
158
+ if remaining:
159
+ depths[key] = remaining
160
+ else:
161
+ depths.pop(key, None)
162
+
163
+
164
+ def operation_scope_active(state_root: Path) -> bool:
165
+ return bool(_depths("operation_depths").get(_key(Path(state_root)), 0))
166
+
167
+
168
+ def _journal_records(state_root: Path, *, coordinator_only: bool = False) -> list[dict[str, Any]]:
169
+ root = Path(state_root).expanduser() / "runtime"
170
+ records: list[dict[str, Any]] = []
171
+ # ``resets`` predates the unified transaction directory and remains visible
172
+ # so an old interrupted reset is never silently treated as complete.
173
+ for directory in (root / "transactions", root / "installer-transactions", root / "resets", root / "migrations"):
174
+ if not directory.exists():
175
+ continue
176
+ if directory.is_symlink() or not directory.is_dir():
177
+ raise TransactionError(f"unsafe transaction journal root: {directory}")
178
+ for journal in sorted(directory.glob("*/journal.json")):
179
+ if journal.parent.is_symlink() or journal.is_symlink() or not journal.is_file():
180
+ raise TransactionError(f"unsafe transaction journal: {journal}")
181
+ try:
182
+ value = json.loads(journal.read_text(encoding="utf-8"))
183
+ except (OSError, ValueError) as exc:
184
+ raise TransactionError(f"unreadable transaction journal: {journal}") from exc
185
+ if not isinstance(value, dict):
186
+ raise TransactionError(f"invalid transaction journal: {journal}")
187
+ # Store owns its source-only install journal. It can safely finish
188
+ # its own PREPARED source pair under the same lock; launcher/config
189
+ # readers must only block on the wider installer/reset coordinator.
190
+ coordinator = directory.name in {"installer-transactions", "resets", "migrations"} or value.get("owner") == "installer"
191
+ if value.get("state") in PENDING_STATES and (not coordinator_only or coordinator):
192
+ fallback = "migrate" if directory.name == "migrations" else "reset" if coordinator else "source"
193
+ records.append({"path": str(journal), "kind": value.get("kind", fallback),
194
+ "owner": "installer" if coordinator else "store",
195
+ "state": value.get("state"), "phase": value.get("phase")})
196
+ return records
197
+
198
+
199
+ def pending_status(state_root: Path) -> dict[str, Any]:
200
+ records = _journal_records(state_root)
201
+ return {"pending": bool(records), "transactions": records}
202
+
203
+
204
+ def pending_operations(state_root: Path) -> list[dict[str, Any]]:
205
+ """Return pending journals for source readers and management views."""
206
+ return pending_status(state_root)["transactions"]
207
+
208
+
209
+ def guard_pending(state_root: Path) -> None:
210
+ """Fail before a config/source reader consumes an incomplete publication."""
211
+ key = _key(Path(state_root))
212
+ if _depths("operation_depths").get(key, 0):
213
+ return
214
+ pending = _journal_records(state_root, coordinator_only=True)
215
+ if pending:
216
+ first = pending[0]
217
+ raise TransactionPendingError(
218
+ "instructions install/reset/migration needs recovery before reading current configuration: "
219
+ f"{first['path']} ({first['state']})"
220
+ )
221
+
222
+
223
+ def _valid_release(state_root: Path, record: dict[str, Any]) -> Path:
224
+ raw = record.get("package_root")
225
+ digest = record.get("release_digest")
226
+ if not isinstance(raw, str) or not isinstance(digest, str):
227
+ raise TransactionPendingError("private install record has no confirmed immutable release")
228
+ root = (Path(state_root).expanduser() / "runtime" / "releases").absolute()
229
+ release = Path(raw)
230
+ try:
231
+ release.absolute().relative_to(root)
232
+ except ValueError as exc:
233
+ raise TransactionPendingError("private install record points outside immutable releases") from exc
234
+ if release.name != digest or release.is_symlink() or not release.is_dir():
235
+ raise TransactionPendingError("private install record has an invalid immutable release")
236
+ entries = record.get("installed_files")
237
+ if not isinstance(entries, list) or not entries:
238
+ raise TransactionPendingError("private install record has no release inventory")
239
+ # The release digest is the digest of the ordered path+bytes stream. The
240
+ # installer computes it before any pointer publication, so this is a cheap
241
+ # structural confirmation suitable for a config reader; full verification
242
+ # remains InstructionsInstaller.verify's responsibility.
243
+ actual = hashlib.sha256()
244
+ for entry in entries:
245
+ if not isinstance(entry, dict) or not isinstance(entry.get("path"), str):
246
+ raise TransactionPendingError("private install record has invalid release inventory")
247
+ relative = Path(entry["path"])
248
+ if relative.is_absolute() or ".." in relative.parts:
249
+ raise TransactionPendingError("private install record has unsafe release inventory")
250
+ member = release / relative
251
+ if member.is_symlink() or not member.is_file():
252
+ raise TransactionPendingError("confirmed release member is unavailable")
253
+ data = member.read_bytes()
254
+ actual.update(entry["path"].encode("utf-8") + b"\0" + data + b"\0")
255
+ if actual.hexdigest() != digest:
256
+ raise TransactionPendingError("confirmed release digest does not match inventory")
257
+ return release
258
+
259
+
260
+ def confirmed_release(state_root: Path) -> Path:
261
+ """Return the last fully confirmed release while a coordinator is pending.
262
+
263
+ A pending update's ``after`` record is deliberately not trusted. The
264
+ journal's exact ``before`` private-install bytes identify the previous
265
+ generation; a first installation has none and therefore fails closed.
266
+ """
267
+ root = Path(state_root).expanduser()
268
+ pending = _journal_records(root, coordinator_only=True)
269
+ migration = next((item for item in pending if item["kind"] == "migrate"), None)
270
+ if migration is not None:
271
+ try:
272
+ value = json.loads(Path(migration["path"]).read_text(encoding="utf-8"))
273
+ before = value["prior_install"]
274
+ encoded = before.get("bytes_b64") if isinstance(before, dict) else None
275
+ if not isinstance(encoded, str):
276
+ raise ValueError("no prior private install")
277
+ record = json.loads(base64.b64decode(encoded.encode("ascii"), validate=True))
278
+ except (KeyError, OSError, ValueError, TypeError) as exc:
279
+ raise TransactionPendingError("pending migration has no confirmed prior private install") from exc
280
+ if not isinstance(record, dict):
281
+ raise TransactionPendingError("prior migration install record is invalid")
282
+ return _valid_release(root, record)
283
+ install = next((item for item in pending if item["kind"] == "install"), None)
284
+ if install is not None:
285
+ journal = Path(install["path"])
286
+ try:
287
+ value = json.loads(journal.read_text(encoding="utf-8"))
288
+ record_entry = next(entry for entry in value.get("paths", [])
289
+ if isinstance(entry, dict) and str(entry.get("path", "")).endswith(
290
+ "/runtime/private-install.json"))
291
+ before = record_entry.get("before", {})
292
+ encoded = before.get("bytes_b64") if isinstance(before, dict) else None
293
+ if not isinstance(encoded, str):
294
+ raise ValueError("no prior private-install record")
295
+ record = json.loads(base64.b64decode(encoded.encode("ascii"), validate=True))
296
+ except (StopIteration, ValueError, TypeError, json.JSONDecodeError) as exc:
297
+ raise TransactionPendingError("first or incomplete install has no confirmed prior release") from exc
298
+ if not isinstance(record, dict):
299
+ raise TransactionPendingError("prior private-install record is invalid")
300
+ return _valid_release(root, record)
301
+ path = root / "runtime" / "private-install.json"
302
+ try:
303
+ record = json.loads(path.read_text(encoding="utf-8"))
304
+ except (OSError, ValueError) as exc:
305
+ raise TransactionPendingError("pending reset has no confirmed private install") from exc
306
+ if not isinstance(record, dict):
307
+ raise TransactionPendingError("confirmed private install is invalid")
308
+ return _valid_release(root, record)