agent-bios 0.9.9 → 0.11.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.
Files changed (34) hide show
  1. package/DEPENDENCIES.md +3 -3
  2. package/README.md +10 -2
  3. package/claude/CLAUDE.md +4 -41
  4. package/claude/guides/cli-multi-model-workflow.md +7 -7
  5. package/claude/guides/coding-staged-workflow.md +51 -49
  6. package/claude/guides/concept-economy.md +187 -0
  7. package/claude/guides/documentation-hygiene.md +112 -0
  8. package/claude/guides/review-request.md +9 -7
  9. package/claude/guides/session-distill-workflow.md +3 -3
  10. package/claude/guides/tooling-gotchas.md +10 -0
  11. package/claude/guides/verification-discipline.md +166 -0
  12. package/claude/hooks/tooling-gotchas-hook.py +323 -13
  13. package/codex/AGENTS.md +4 -41
  14. package/codex/guides/cli-multi-model-workflow.md +7 -7
  15. package/codex/guides/coding-staged-workflow.md +51 -49
  16. package/codex/guides/concept-economy.md +187 -0
  17. package/codex/guides/documentation-hygiene.md +112 -0
  18. package/codex/guides/review-request.md +9 -7
  19. package/codex/guides/session-distill-workflow.md +3 -3
  20. package/codex/guides/tooling-gotchas.md +10 -0
  21. package/codex/guides/verification-discipline.md +166 -0
  22. package/compose/assemble.py +10 -1
  23. package/compose/check-domains.py +882 -6
  24. package/compose/domains.json +10 -44
  25. package/install.sh +176 -16
  26. package/launch/agent-launch.py +790 -173
  27. package/launch/agent-launch.toml +56 -107
  28. package/launch/i18n/en.toml +66 -0
  29. package/launch/i18n/ja.toml +63 -0
  30. package/launch/i18n/ko.toml +63 -0
  31. package/package.json +9 -4
  32. package/provenance.json +1 -0
  33. package/wrappers/codex-run.sh +1 -1
  34. package/claude/hooks/__pycache__/tooling-gotchas-hook.cpython-314.pyc +0 -0
@@ -12,53 +12,159 @@ import sys
12
12
 
13
13
  GUIDE = "guides/tooling-gotchas.md"
14
14
 
15
- # (name, compiled trigger, one-line reminder). Priority order; max 2 injected.
15
+ # (name, compiled trigger, one-line reminder, guide anchor). Priority order; max 2 injected.
16
+ #
17
+ # The anchor is the heading in tooling-gotchas.md this rule compresses. It exists because the
18
+ # hook is Claude-only and the guide is what a Codex reader gets instead: a rule admitted here
19
+ # with no counterpart there would silently give the two hosts different guidance. The mapping
20
+ # is declared rather than matched, because a rule name and a guide heading do not share a
21
+ # string. --self-test checks every anchor against the guide AND its codex mirror.
16
22
  RULES = [
17
23
  ("reserved-shell-names",
18
24
  re.compile(r"\b(UID|EUID|GID|PPID)="),
19
25
  "Assigning reserved shell names (UID/EUID/GID/PPID) can invoke the bound "
20
- f"system behavior instead of storing a value — use unreserved names ({GUIDE})."),
26
+ f"system behavior instead of storing a value — use unreserved names ({GUIDE}).",
27
+ "Reserved parameter names"),
21
28
  ("git-diff-two-dot",
22
29
  re.compile(r"git\s+diff\s+[^|;&]*(?<!\.)\.\.(?!\.)"),
23
30
  "git diff A..B is a direct snapshot comparison, not a range exclusion — "
24
- f"for PR/review diffs use three-dot origin/base...HEAD ({GUIDE})."),
31
+ f"for PR/review diffs use three-dot origin/base...HEAD ({GUIDE}).",
32
+ "Two-dot diff semantics"),
25
33
  ("git-pull-dirty",
26
34
  re.compile(r"\bgit\s+pull\b"),
27
35
  "Before pulling into a worktree with local changes: fetch first, compare "
28
- f"incoming paths against dirty paths, prefer --ff-only ({GUIDE})."),
36
+ f"incoming paths against dirty paths, prefer --ff-only ({GUIDE}).",
37
+ "Dirty-worktree pulls"),
29
38
  ("metachar-inline-arg",
30
39
  re.compile(r"(codex-run|codex-helm|codex\s+exec|claude\s+-p)\b[^|;&]*[\"'][^\"']*(\$\(|`)"),
31
40
  "Values with shell metacharacters must reach the target via stdin or a "
32
- f"file, not inline arguments — the shell expands them first ({GUIDE})."),
41
+ f"file, not inline arguments — the shell expands them first ({GUIDE}).",
42
+ "Metacharacter-bearing values"),
33
43
  ("pipe-exit-masking",
34
44
  re.compile(r"\$\?"),
35
45
  "A pipeline's $? reflects only the last stage — capture the tested "
36
- f"stage's own status (unpiped run, PIPESTATUS, per-command pipefail) ({GUIDE})."),
46
+ f"stage's own status (unpiped run, PIPESTATUS, per-command pipefail) ({GUIDE}).",
47
+ "Pipe exit masking"),
37
48
  # Path-shaped argument only: `git checkout main` is a branch switch and needs no
38
49
  # warning, while `git checkout src/x.py` silently discards every uncommitted edit in
39
50
  # that file — including ones the caller did not put there.
40
51
  ("git-checkout-path",
41
52
  re.compile(r"\bgit\s+(checkout|restore)\b[^|;&]*(--\s|[\w.-]*[./][\w./-]*)"),
42
53
  "Reverting a path discards ALL uncommitted edits in that file, not just the one "
43
- f"you planted — check `git diff <path>` first, or restore from a copy ({GUIDE})."),
54
+ f"you planted — check `git diff <path>` first, or restore from a copy ({GUIDE}).",
55
+ "Reverting a path is not undoing your edit"),
44
56
  ("grep-binary-heuristic",
45
- re.compile(r"(^|[;&(]\s*|&&\s*|\|\|\s*)grep\s"),
57
+ # Applied per extracted STAGE by matches(), not to the raw line. The stage's
58
+ # COMMAND WORD must be grep — after optional reserved words (`if ! grep -q`
59
+ # is the assert-absence idiom) and per-command assignments (`LC_ALL=C grep`
60
+ # — locale pinning is what our own guides recommend); bare, path-prefixed,
61
+ # or the DIRECT git subcommand (git grep shares the binary heuristic). A
62
+ # stage that merely passes the word along (`echo grep needle`) runs no grep
63
+ # and gets no reminder. Greps reached through any other prefix — sudo,
64
+ # xargs, git global options between git and the subcommand (`git -C repo
65
+ # grep`), env, timeout, and their successors — are accepted residual by
66
+ # decision: each is one rung of an endless prefix ladder, and a missed
67
+ # reminder is the failure mode this advisory-only hook tolerates.
68
+ re.compile(r"\s*(?:(?:!|if|elif|else|then|do|while|until|time)\s+)*"
69
+ r"(?:\w+=\S*\s+)*(?:\S+/)?(?:git\s+)?grep\s"),
46
70
  "grep can misread text with heavy non-ASCII/NUL as binary and return a "
47
- f"false no-match — prefer the Grep tool (ripgrep) or grep -a ({GUIDE})."),
71
+ f"false no-match — prefer the Grep tool (ripgrep) or grep -a ({GUIDE}).",
72
+ "grep binary heuristic"),
48
73
  ]
49
74
  MAX_INJECT = 2
50
75
 
51
76
 
52
- def matches(command: str):
77
+ def matches(command: str, limit: int | None = MAX_INJECT):
78
+ """Rules this command trips. `limit` is a DELIVERY policy — at most two reminders are
79
+ worth injecting at once — not a judgement about which rules matched, so the self-test
80
+ asks for the untruncated list rather than re-deriving the matching itself."""
53
81
  hits = []
54
- for name, rx, msg in RULES:
82
+ for name, rx, msg, _ in RULES:
55
83
  if name == "pipe-exit-masking" and "|" not in command:
56
84
  continue
57
- if name == "grep-binary-heuristic" and re.search(r"(^|\s)(-a\b|--text)", command):
85
+ if name == "grep-binary-heuristic":
86
+ # Per STAGE, not per command: -a on an upstream grep does nothing for a
87
+ # downstream one (`git grep -a foo | grep bar` leaves bar's stage on the
88
+ # binary heuristic), so the exemption holds only when EVERY grep stage
89
+ # carries its own text-mode flag.
90
+ # Every separator starts a new stage: a single & (background) and a newline
91
+ # join commands as surely as ; and | — `grep -a foo a & grep bar b` left the
92
+ # -a covering a stage it never touches.
93
+ # A command substitution executes regardless of the quotes around it:
94
+ # `echo "$(grep needle payload)"` runs that grep, and quote-blanking was
95
+ # hiding it from the stage list so an outer -a covered it. Substitution
96
+ # bodies are lifted out (innermost-first, to a fixpoint) and judged as
97
+ # stages of their own.
98
+ scan = command
99
+ seen_subs = set()
100
+ while True:
101
+ subs = [s2 for s2 in re.findall(r"\$\(([^()]*)\)", scan)
102
+ + re.findall(r"`([^`]*)`", scan)
103
+ if s2 not in seen_subs]
104
+ if not subs:
105
+ break
106
+ seen_subs.update(subs)
107
+ scan = scan + "\n" + "\n".join(subs)
108
+ # Quotes are blanked BEFORE splitting: a separator inside a quoted pattern
109
+ # (`grep 'grep -a;foo'`) manufactured a pseudo-stage carrying a text-mode
110
+ # flag that exists only as pattern content. Blanked to a placeholder TOKEN,
111
+ # not to nothing: `grep -e "needle" -a file` blanked to whitespace left -e
112
+ # to consume the -a as its argument, and the real text-mode flag vanished
113
+ # with the operand's token boundary.
114
+ blanked = re.sub(r"'[^']*'|\"(?:\\\\.|[^\"\\\\])*\"", "0", scan)
115
+ # Comments go after the quotes: with quoted text blanked, any remaining # is
116
+ # a real comment, and `grep needle payload # use -a next time` was exempting
117
+ # itself with advice bash never passes to grep.
118
+ blanked = re.sub(r"#[^\n]*", "", blanked)
119
+ # Parens split as the old line-shaped trigger's [;&(|] class did: a
120
+ # subshell or group opener starts a command (`(grep needle)` runs grep),
121
+ # and dropping ( from the boundaries regressed exactly that form.
122
+ stages = [st for st in re.split(r"\|\||&&|[|;&\n()]", blanked)
123
+ if rx.match(st)]
124
+ # The STAGES are the trigger: a grep whose only appearance is inside a
125
+ # lifted substitution (`echo "\`grep needle payload\`"`) never matched a
126
+ # line-shaped regex, and a quoted assignment value (`FILTER='two words'
127
+ # grep ...`) hid the command word from it. The rule's regex judges each
128
+ # stage in COMMAND-WORD position — `echo grep needle` passes the word as
129
+ # an argument, runs no grep, and stays quiet.
130
+ if not stages:
131
+ continue
132
+ # Options end at `--`: after it, `-a` is the PATTERN operand (grep's usage is
133
+ # [OPTION]... PATTERNS [FILE]...), so `grep -- -a payload` is still on the
134
+ # binary heuristic and must keep its reminder.
135
+ # And `-e <pattern>` consumes its argument: in `grep -e -a payload` the -a
136
+ # is the PATTERN (grep --help: -e, --regexp=PATTERNS), so it must not read
137
+ # as text mode. The pattern-taking options are blanked before the flag scan.
138
+ # -f/--file consumes its argument the same way (-f, --file=FILE): every
139
+ # pattern-taking option is blanked, or its argument reads as a flag.
140
+ # Quoted text is pattern content, never options: `grep 'foo -a bar'` has no
141
+ # text-mode flag, and the unquoted scan read the -a inside the pattern.
142
+ # Quotes are blanked first, then the -- operand split, then pattern-taking
143
+ # option arguments.
144
+ opts = [re.sub(r"(^|\s)(?:-e|--regexp|-f|--file)(?:=\S+|\s+\S+)", " ",
145
+ st.split(" -- ", 1)[0])
146
+ for st in stages]
147
+
148
+ # Bundled short options count too: `grep -qa` enables text mode (grep
149
+ # --help: -a, --text), and requiring a whitespace-delimited -a warned
150
+ # about binary data on a grep that reads it as text. A letter that takes
151
+ # an argument (-e -f -m -A -B -C -d -D) consumes the REST of the bundle,
152
+ # so in `-ea` the a is the PATTERN, not a flag — an `a` reads as text
153
+ # mode only when every letter before it is argument-free.
154
+ def text_mode(o):
155
+ if re.search(r"(^|\s)(-a\b|--text\b|--binary-files(?:=|\s+)text\b)", o):
156
+ return True
157
+ return any(len(halves) == 2 and not set(halves[0]) & set("efmABCdD")
158
+ for tok in re.findall(r"(?:^|\s)-([A-Za-z0-9]+)", o)
159
+ for halves in [tok.split("a", 1)])
160
+
161
+ if all(text_mode(o) for o in opts):
162
+ continue
163
+ hits.append((name, msg))
58
164
  continue
59
165
  if rx.search(command):
60
166
  hits.append((name, msg))
61
- return hits[:MAX_INJECT]
167
+ return hits if limit is None else hits[:limit]
62
168
 
63
169
 
64
170
  def main() -> int:
@@ -82,5 +188,209 @@ def main() -> int:
82
188
  return 0
83
189
 
84
190
 
191
+ def self_test() -> int:
192
+ """Two halves, because this hook serves two hosts differently.
193
+
194
+ Claude gets the injection, so the first half runs the real entry point over real stdin and
195
+ requires the message out. Codex gets nothing from a hook, so its equivalent is the guide —
196
+ and the second half is the only thing that keeps the two hosts saying the same thing.
197
+ """
198
+ import pathlib, subprocess
199
+
200
+ here = pathlib.Path(__file__).resolve()
201
+ repo = here.parent.parent.parent
202
+ problems = []
203
+ assert RULES, "no rules: this self-test would pass over nothing"
204
+
205
+ # -- half 1: the hook runs, matches, and emits. A file that is copied and registered but
206
+ # crashes on every invocation satisfies every other check in this repository.
207
+ fires = "UID=0 echo hi"
208
+ payload = json.dumps({"tool_name": "Bash", "tool_input": {"command": fires}})
209
+ r = subprocess.run([sys.executable, str(here)], input=payload,
210
+ capture_output=True, text=True)
211
+ if r.returncode != 0:
212
+ problems.append(f"hook exited {r.returncode} on a matching command: {r.stderr.strip()[:120]}")
213
+ elif "additionalContext" not in r.stdout:
214
+ problems.append(f"hook emitted no context for {fires!r}; got {r.stdout.strip()[:80]!r}")
215
+
216
+ # A non-matching command must stay silent, or "it fired" proves nothing.
217
+ quiet = json.dumps({"tool_name": "Bash", "tool_input": {"command": "echo hello"}})
218
+ r2 = subprocess.run([sys.executable, str(here)], input=quiet, capture_output=True, text=True)
219
+ if r2.stdout.strip():
220
+ problems.append(f"hook injected on a command that matches nothing: {r2.stdout.strip()[:80]!r}")
221
+
222
+ # Every rule needs a command that must reach it. A nonempty pattern is not evidence: a
223
+ # trigger of `(?!)` can never match, and a rule suppressed by the special-case logic in
224
+ # matches() never reaches the caller either — both leave the rule inert while this file
225
+ # goes on reporting that all of them fire.
226
+ FIXTURES = {
227
+ "reserved-shell-names": "UID=0 echo hi",
228
+ "git-diff-two-dot": "git diff main..HEAD",
229
+ "git-pull-dirty": "git pull origin main",
230
+ "metachar-inline-arg": 'codex exec "$(cat packet.md)"',
231
+ "pipe-exit-masking": "make build | tail -1; echo $?",
232
+ "git-checkout-path": "git checkout src/thing.py",
233
+ "grep-binary-heuristic": "grep needle haystack.md",
234
+ }
235
+ # A pipeline-stage grep must reach the rule too — the fixture alone exercises only
236
+ # command-start grep, and the reminder is most needed mid-pipeline.
237
+ if "grep-binary-heuristic" not in [h for h, _ in matches("cat payload | grep needle", limit=None)]:
238
+ problems.append("grep-binary-heuristic: does not fire on a pipeline stage "
239
+ "('cat payload | grep needle')")
240
+ if "grep-binary-heuristic" not in [h for h, _ in matches("git grep -a foo | grep bar", limit=None)]:
241
+ problems.append("grep-binary-heuristic: an upstream -a cancelled the reminder for "
242
+ "a downstream grep that has no text-mode flag")
243
+ if "grep-binary-heuristic" in [h for h, _ in matches("git grep -a foo | grep -a bar", limit=None)]:
244
+ problems.append("grep-binary-heuristic: fired although every grep stage carries "
245
+ "its own text-mode flag")
246
+ if "grep-binary-heuristic" not in [h for h, _ in matches("grep -- -a payload", limit=None)]:
247
+ problems.append("grep-binary-heuristic: '-a' AFTER the -- marker is the pattern "
248
+ "operand, not a flag, and the reminder was suppressed")
249
+ if "grep-binary-heuristic" in [h for h, _ in matches("grep -a -- pattern file.md", limit=None)]:
250
+ problems.append("grep-binary-heuristic: fired although -a precedes the -- marker")
251
+ if "grep-binary-heuristic" not in [h for h, _ in matches("grep -e -a payload", limit=None)]:
252
+ problems.append("grep-binary-heuristic: '-a' as the ARGUMENT of -e is the pattern, "
253
+ "not text mode, and the reminder was suppressed")
254
+ if "grep-binary-heuristic" in [h for h, _ in matches("grep -a -e pattern file.md", limit=None)]:
255
+ problems.append("grep-binary-heuristic: fired although -a stands alone before -e")
256
+ if "grep-binary-heuristic" not in [h for h, _ in matches("grep -f -a payload", limit=None)]:
257
+ problems.append("grep-binary-heuristic: '-a' as the ARGUMENT of -f is a pattern "
258
+ "file, not text mode, and the reminder was suppressed")
259
+ if "grep-binary-heuristic" not in [h for h, _ in matches("grep 'foo -a bar' payload", limit=None)]:
260
+ problems.append("grep-binary-heuristic: a text-mode spelling INSIDE a quoted "
261
+ "pattern is pattern content, and the reminder was suppressed")
262
+ if "grep-binary-heuristic" in [h for h, _ in matches("grep -a 'foo bar' payload", limit=None)]:
263
+ problems.append("grep-binary-heuristic: fired although -a stands outside the "
264
+ "quoted pattern")
265
+ if "grep-binary-heuristic" not in [h for h, _ in matches("grep 'grep -a;foo' payload", limit=None)]:
266
+ problems.append("grep-binary-heuristic: a separator inside a quoted pattern "
267
+ "manufactured a pseudo-stage whose -a suppressed the reminder")
268
+ if "grep-binary-heuristic" in [h for h, _ in matches("grep -a 'x;y' payload", limit=None)]:
269
+ problems.append("grep-binary-heuristic: fired although the real stage carries -a "
270
+ "and the separator is only pattern content")
271
+ if "grep-binary-heuristic" not in [h for h, _ in
272
+ matches('echo "`grep needle payload`" | grep -a foo', limit=None)]:
273
+ problems.append("grep-binary-heuristic: a LEGACY backtick substitution's grep lost "
274
+ "its reminder to the outer stage's -a")
275
+ if "grep-binary-heuristic" not in [h for h, _ in
276
+ matches('echo "`grep needle payload`"', limit=None)]:
277
+ problems.append("grep-binary-heuristic: a grep whose ONLY appearance is a lifted "
278
+ "substitution never triggered")
279
+ if "grep-binary-heuristic" in [h for h, _ in
280
+ matches("grep -a needle payload # note", limit=None)]:
281
+ problems.append("grep-binary-heuristic: fired although the real invocation "
282
+ "carries -a and only the comment follows")
283
+ if "grep-binary-heuristic" not in [h for h, _ in
284
+ matches("grep needle payload # use -a next time", limit=None)]:
285
+ problems.append("grep-binary-heuristic: advice in a trailing comment exempted a "
286
+ "grep bash never passes it to")
287
+ if "grep-binary-heuristic" not in [h for h, _ in
288
+ matches("FILTER='two words' grep needle payload", limit=None)]:
289
+ problems.append("grep-binary-heuristic: a quoted assignment value hid the command "
290
+ "word from the trigger")
291
+ for argcmd in ("echo grep needle", "command -v grep"):
292
+ if "grep-binary-heuristic" in [h for h, _ in matches(argcmd, limit=None)]:
293
+ problems.append(f"grep-binary-heuristic: fired although grep is an ARGUMENT, "
294
+ f"not the stage's command word ({argcmd!r})")
295
+ if "grep-binary-heuristic" not in [h for h, _ in
296
+ matches("if ! grep -q needle payload; then echo none; fi",
297
+ limit=None)]:
298
+ problems.append("grep-binary-heuristic: reserved words and negation before the "
299
+ "command word suppressed the reminder")
300
+ if "grep-binary-heuristic" not in [h for h, _ in
301
+ matches("/usr/bin/grep needle payload", limit=None)]:
302
+ problems.append("grep-binary-heuristic: a path-prefixed grep is still grep, and "
303
+ "the reminder was suppressed")
304
+ for bundled in ("grep -qa needle payload", "grep -aH needle payload",
305
+ "cat payload | grep -qa needle"):
306
+ if "grep-binary-heuristic" in [h for h, _ in matches(bundled, limit=None)]:
307
+ problems.append(f"grep-binary-heuristic: fired although a bundled short "
308
+ f"option enables text mode ({bundled!r})")
309
+ for consumed in ("grep -ea payload", "grep -fa payload"):
310
+ if "grep-binary-heuristic" not in [h for h, _ in matches(consumed, limit=None)]:
311
+ problems.append(f"grep-binary-heuristic: an 'a' consumed as an option "
312
+ f"ARGUMENT read as text mode ({consumed!r})")
313
+ for grouped in ("(grep needle payload)", "if (grep needle payload); then echo found; fi"):
314
+ if "grep-binary-heuristic" not in [h for h, _ in matches(grouped, limit=None)]:
315
+ problems.append(f"grep-binary-heuristic: a subshell-grouped grep lost its "
316
+ f"reminder to the opening paren ({grouped!r})")
317
+ for qop in ('grep -e "needle" -a file', 'grep -f "patterns.txt" -a file'):
318
+ if "grep-binary-heuristic" in [h for h, _ in matches(qop, limit=None)]:
319
+ problems.append(f"grep-binary-heuristic: fired although -a stands after a "
320
+ f"QUOTED pattern operand — blanking ate the token boundary "
321
+ f"({qop!r})")
322
+ if "grep-binary-heuristic" not in [h for h, _ in
323
+ matches('grep -e "needle" file', limit=None)]:
324
+ problems.append("grep-binary-heuristic: a quoted pattern operand with no "
325
+ "text-mode flag was read as exempt")
326
+ for bf in ("grep --binary-files=text needle payload",
327
+ "grep --binary-files text needle payload"):
328
+ if "grep-binary-heuristic" in [h for h, _ in matches(bf, limit=None)]:
329
+ problems.append(f"grep-binary-heuristic: fired although --binary-files "
330
+ f"selects text handling ({bf!r})")
331
+ if "grep-binary-heuristic" not in [h for h, _ in
332
+ matches("grep --binary-files without-match needle payload",
333
+ limit=None)]:
334
+ problems.append("grep-binary-heuristic: a non-text --binary-files TYPE was "
335
+ "read as text mode")
336
+ if "grep-binary-heuristic" in [h for h, _ in matches("(grep -a needle payload)", limit=None)]:
337
+ problems.append("grep-binary-heuristic: fired although the subshell-grouped "
338
+ "grep carries its own text-mode flag")
339
+ for envcmd in ("LC_ALL=C grep needle payload", "cat payload | LC_ALL=C grep needle"):
340
+ if "grep-binary-heuristic" not in [h for h, _ in matches(envcmd, limit=None)]:
341
+ problems.append(f"grep-binary-heuristic: an env-assignment prefix hid the "
342
+ f"command word ({envcmd!r})")
343
+ if "grep-binary-heuristic" not in [h for h, _ in
344
+ matches('echo "$(grep needle payload)" | grep -a foo', limit=None)]:
345
+ problems.append("grep-binary-heuristic: a grep inside a command substitution lost "
346
+ "its reminder to the outer stage's -a")
347
+ if "grep-binary-heuristic" in [h for h, _ in
348
+ matches('echo "$(grep -a needle payload)" | grep -a foo', limit=None)]:
349
+ problems.append("grep-binary-heuristic: fired although every invocation, nested "
350
+ "included, carries its own text-mode flag")
351
+ if "grep-binary-heuristic" not in [h for h, _ in matches("grep -a foo a & grep bar b", limit=None)]:
352
+ problems.append("grep-binary-heuristic: a background-joined second grep with no "
353
+ "text-mode flag lost its reminder to the first stage's -a")
354
+ if "grep-binary-heuristic" not in [h for h, _ in matches("grep -a foo a\ngrep bar b", limit=None)]:
355
+ problems.append("grep-binary-heuristic: a newline-joined second grep with no "
356
+ "text-mode flag lost its reminder to the first stage's -a")
357
+ if "grep-binary-heuristic" not in [h for h, _ in
358
+ matches('grep "foo \\" -a bar" payload', limit=None)]:
359
+ problems.append("grep-binary-heuristic: an escaped quote inside the pattern closed "
360
+ "the blanking early and the embedded -a read as a flag")
361
+ uncovered = [n for n, _rx, _m, _a in RULES if n not in FIXTURES]
362
+ if uncovered:
363
+ problems.append(f"rules with no fixture, so nothing proves they fire: {uncovered}")
364
+ for name, _rx, _msg, _anchor in RULES:
365
+ cmd = FIXTURES.get(name)
366
+ if cmd is None:
367
+ continue
368
+ if name not in [hit for hit, _ in matches(cmd, limit=None)]:
369
+ problems.append(f"{name}: its own fixture {cmd!r} does not reach it — the rule is inert")
370
+
371
+ # -- half 2: the Codex fallback. SURFACES.md admits a hook rule only with a guide
372
+ # counterpart, because the guide is what the other host receives.
373
+ for tree in ("claude", "codex"):
374
+ g = repo / tree / "guides" / "tooling-gotchas.md"
375
+ if not g.is_file():
376
+ problems.append(f"{tree}/guides/tooling-gotchas.md missing — no fallback to check")
377
+ continue
378
+ body = g.read_text(encoding="utf-8")
379
+ for name, _rx, _msg, anchor in RULES:
380
+ if anchor not in body:
381
+ problems.append(f"{name}: anchor {anchor!r} absent from {tree}/guides/tooling-gotchas.md")
382
+
383
+ for p in problems:
384
+ print(f"self-test [FAIL] {p}")
385
+ if problems:
386
+ print(f"HOOK SELF-TEST FAIL: {len(problems)} problem(s)")
387
+ return 1
388
+ print(f"HOOK SELF-TEST OK: {len(RULES)} rules each fire on their own fixture, stay quiet "
389
+ f"when they should, and each has its guide counterpart on both hosts")
390
+ return 0
391
+
392
+
85
393
  if __name__ == "__main__":
394
+ if "--self-test" in sys.argv[1:]:
395
+ sys.exit(self_test())
86
396
  sys.exit(main())
package/codex/AGENTS.md CHANGED
@@ -52,57 +52,25 @@
52
52
 
53
53
  ## Concept Economy
54
54
 
55
- - Keep the concept graph compact by reusing existing concepts that clearly cover the behavior.
56
- - Treat lasting or shared names as concept candidates: features, entities, variables, types, helper modules, artifacts, config keys, CLI flags, MCP/tool fields, public response fields, artifact fields, enum values, failure kinds, retry/recovery tokens, process names, and documentation terms.
57
- - Before adding or changing a concept, find the nearest existing concept and choose one path explicitly: reuse, extend, rename, or split.
58
- - Prefer broad, stable concepts with precise properties over narrow near-duplicates.
55
+ - When adding, changing, renaming, splitting, or exposing anything lasting or shared a feature, entity, type, field, config key, CLI flag, enum value, failure kind, artifact, or documentation term — read and use `${CODEX_HOME:-$HOME/.codex}/guides/concept-economy.md` as a scoped extension of this section.
59
56
  - Before fixing a review finding or test failure, classify the fix as reducing, preserving, or increasing the active concept surface.
60
- - Split or promote a concept when it changes runtime behavior, ownership, lifecycle, validation, failure mode, user-visible behavior, audit/replay requirements, authority, persistence, user control, or failure handling.
61
- - Keep derived values as properties or projections of their source concept when tools/code can derive them from the source authority.
62
- - Keep internal projections and helper outputs internal unless public exposure is required for user behavior, product contract, or artifact truth.
63
- - Distinguish authority from visibility: public responses may expose bounded views, while the source concept or artifact remains the truth location.
64
- - Reuse existing enum values, failure kinds, retry/recovery tokens, and result/failure surfaces before introducing new vocabulary.
65
- - Use fallback paths, compatibility shims, and deprecated alias normalization when explicit migration compatibility is required.
66
- - Keep comments and active docs aligned with runtime behavior, failure semantics, retry policy, ownership, and authority.
67
- - When a split is necessary, name the parent concept, explain the reason for the split, and map aliases or variants back to the canonical concept.
68
- - In ontology work, check existing entities and relations first, then keep the concept graph compact.
69
- - In code work, follow existing naming patterns and consolidate variations introduced by the current change.
70
- - Let the repository's shape mirror its concept graph: keep each shared, lasting concept's canonical name traceable across the layers it appears in — path, module, type/interface, field, and public API — so the structure is navigable by name (grep-findable, path-guessable) without a translation table. This binds shared concepts only; transient locals, generic containers, and framework- or tooling-imposed layout may diverge.
71
57
 
72
58
  ## Coding Guidelines
73
59
 
74
60
  - For `.xlsx` editing, generation, reconciliation, validation, or connected spreadsheet processing, use the installed `spreadsheet-processing` skill when present — with plain tools/code as the fallback — and validate formula-dependent Excel results with the real Microsoft Excel engine.
75
- - For meaningful development work, read and use `${CODEX_HOME:-$HOME/.codex}/guides/coding-staged-workflow.md` as a scoped extension of these Coding Guidelines.
61
+ - For development work, read and use `${CODEX_HOME:-$HOME/.codex}/guides/coding-staged-workflow.md` as a scoped extension of these Coding Guidelines — a change too narrow to need it is what its lightweight path decides, not a reason to skip the read.
76
62
  - For mock, fixture, fake, stub, simulated-provider, or test-realization design, read and use `${CODEX_HOME:-$HOME/.codex}/guides/mock-realization-boundary.md` as a scoped extension of these Coding Guidelines.
77
- - When the user asks to "설계" or design, read the coding-staged-workflow guide and focus on high-level design and implementation-process design; move to implementation only after the user asks to implement or approves the plan.
78
- - Think before coding: state key assumptions and surface ambiguity early.
79
- - Build the smallest viable functional path that satisfies the qualitative completion criteria. Minimum limits surface area, configuration, abstractions, optional scope, and implementation spread; it must not reduce required behavior, runtime authority, evidence quality, or verification depth.
80
- - Treat viability as real behavior against real inputs, real authority, and the intended runtime path. Use mocks only for tests, fixtures, or explicitly requested simulations; mock-backed paths support verification but do not count as product completion.
81
- - Make surgical changes. Touch only what the request requires, preserve existing style, and avoid casual adjacent refactors.
82
- - Clean up issues introduced by the current change. Mention unrelated dead code separately.
83
63
  - Own the full lifecycle of what you create — spawned processes and handles through teardown, artifacts out of tool-managed temp locations into a durable home — and keep differently-owned state separate: never colocate deploy-managed and user-owned data in one overwrite-managed file.
84
- - Define success criteria before multi-step coding work, then verify against them.
85
- - For bugs, prefer a reproducing test before the fix when practical.
86
- - Every changed line should trace back to the user's request.
87
- - Fix the root cause at its authority rather than the visible symptom: when downstream patches keep compensating for bad inputs, fix upstream at the source; when each fix only exposes another instance of the same defect, single-source the value and fix the whole class instead of patching instances.
88
64
  - Land risky or behavior-changing work behind a default-off path that preserves current behavior when off (proven by diff) and is enabled by an explicit opt-in, so the change stays reversible and the on/off difference is isolated. When a request would weaken a security or authority posture — removing or loosening an authentication/authorization check or access scope, or lowering a protective value such as session/token lifetime, password/crypto strength, rate limit, lockout threshold, or audit retention — treat it as a decision, not a rote edit, even when it is a one-line change and nothing in the code labels the value as security-relevant: state the consequence and at least one safer path to the real goal, and do not apply the weakening in the same turn — proceed only after the user confirms they accept the tradeoff.
89
65
 
90
66
  ## Verification Discipline
91
67
 
92
68
  - For composing a review request, packet, or reviewer role — the evidence bar, the verdict shape, and why a review returned noise, nothing, or a clean bill of health — read and use `${CODEX_HOME:-$HOME/.codex}/guides/review-request.md` as a scoped extension of this section.
93
69
  - After every meaningful code, ontology, config, data, spreadsheet, or documentation change, run a verification loop regardless of commit or handoff status.
94
- - Use static checks broadly: typecheck, lint, build, format, schema/config validation, graph validation, workbook structure checks, import boundaries, and security checks when available.
95
- - Add the narrowest reliable runtime or semantic test that proves the changed behavior, meaning, or contract.
96
- - Pick each domain's verification mix (code, ontology, config/data, spreadsheets, docs) from the Verification Menus in the coding-staged-workflow guide.
97
- - Let the LLM derive scenarios from the diff, user impact, concept impact, and failure modes; let tools/code execute and verify them. Where an artifact already defines the case space — a config, a schema, a route table — enumerate the cases from it rather than from judgment, and record real output as the expectation instead of typing one: a hand-listed set of cases silently stops covering as the artifact grows past it.
98
- - Keep E2E stable with deterministic data, resilient selectors, isolated external dependencies, and explicit waits.
70
+ - For choosing verification depth, the per-domain mix, the case space, what makes a completion criterion falsifiable, how to keep an E2E stable, or what a green result is worth, read and use `${CODEX_HOME:-$HOME/.codex}/guides/verification-discipline.md` as a scoped extension of this section — its Verification Menus carry the per-domain mixes.
99
71
  - Report the checks run, results, and any unverified risk before calling the work done.
100
72
  - Trust a green check only when it traversed the actual changed code through the real dispatch and real calls (not a mock, dry-run, or bypass), and remember that "it ran" is not "quality met" — a fallback, floor, or mock run is not done; treat a zero-findings verdict as suspect until you confirm the harness ran rather than silently crashed, and make PASS mean concrete assertions on real output from the real path.
101
- - Make completion criteria falsifiable: prefer signals that fail when the mechanism is wrong (negative or contrast controls), and if no existing gate can judge a criterion, build the executable judge or do not claim the criterion met.
102
73
  - Before comparing two of anything (cost, performance, quality, frequency), fix a common basis — units, denominators, population, measurement surface — compare on equivalent output, and exclude or flag non-representative data (promotions, outages, smoke slices).
103
- - For non-trivial designs or high-risk changes, run independent adversarial review across distinct lenses, ideally on the design before implementation, and re-verify each finding against real code before acting on it. Apply the convergence heuristic by reviewer kind (detailed in the multi-model guide): same-kind convergence is high confidence but same-kind reviewers share blind spots — their shared "clean" is not verification; different-kind divergence is the expected signal — act on the union. Never accept an orchestrated workflow's self-reported all-green as sufficient; independently re-run the diff inspection and verification suite yourself.
104
- - Proportion verification to cost, risk, and information gain: before expensive or slow live runs, diagnose in code and replay the changed deterministic logic over persisted real artifacts, probe at N=1 with inputs precondition-checked, and reserve full design-review-plus-live verification for first-of-kind or authority-changing work; proportion assurance to the deployment context — a single-user, own-data tool does not warrant production-grade assurance; prefer delivery.
105
- - Trust a green / zero-findings verdict only if the check could have failed over a real, non-empty subject: assert the entity-under-test set has cardinality > 0 before any "no bad X" or "all X satisfy P" claim (an empty subject set passes vacuously and proves nothing), and for any test touching a branch you add or delete, confirm its inputs satisfy the live branch's entry guard — a copied fixture that fails the new guard silently routes into the about-to-be-deleted dead branch and stays green even after the real behavior breaks. When a check goes green unexpectedly fast or empty, dump what it actually ran over. Checker code itself must assert the expected shape and fail loud — a permissive fallback (`a || b`) inside a gate absorbs wrong assumptions and keeps passing.
106
74
 
107
75
  ## Tooling and Operational Safety
108
76
 
@@ -113,7 +81,6 @@
113
81
  - Never accept secrets through transcript- or history-logged channels.
114
82
  - When a secret must be supplied, provide a gitignored env slot, read the value only from the environment, verify its presence and format without echoing it, and advise rotating anything already pasted; assume a resource-creating call may echo the secret back in its success output — suppress or discard the response body, and treat an echoed secret as pasted (rotate).
115
83
  - Treat a coarse runtime signal — a failure label, a `ps`/process-inspection result, idle CPU with no output — as a hypothesis, and confirm the cause against the authoritative low-level evidence the mechanism emits before attributing blame or intervening: read the raw provider/skill log payload (e.g. `input_tokens:0` proves a pre-dispatch rejection that exonerates your content and your change), and confirm a config/env toggle reached a subprocess via a cheap artifact the gated branch emits rather than an unreliable `ps` env read. A multi-minute LLM or subprocess call at ~0% CPU with an output gap is the normal signature of I/O wait, not a hang — check process state and the call trace's in-flight duration before acting, so you do not abort healthy long-running work.
116
- - Before reasoning about what a branch contains or opening a PR, run `git fetch` and compute the range as `origin/<base>..HEAD`, never `<base>..HEAD` against the local tracking ref — on a shared repo the local base drifts behind the remote until you pull, silently inflating the diff with already-merged work; if the range is surprisingly large, suspect a stale base before suspecting the branch. Platform "mergeable" flags are computed against the base only — sibling PRs can each look clean yet conflict; before picking a merge order, diff their changed-file sets and simulate the sequence.
117
84
 
118
85
  ## Multi-Model Workflow
119
86
 
@@ -131,11 +98,7 @@
131
98
  ## Documentation Hygiene
132
99
 
133
100
  - Keep runtime code, active docs, and execution-facing docs focused on current behavior, current decisions, current contracts, current authority, and current failure handling.
134
- - Use comments for non-obvious current behavior, invariants, constraints, or risks that still apply.
135
- - Put backward-compatibility notes, deprecated behavior, migration rationale, historical alternatives, change narratives, and handoff logs in isolated documentation paths such as `docs/`, `design/`, `archive/`, or `deprecated/`.
136
- - Link from active docs or code to isolated notes only when the current task needs that history or the reference helps future maintainers.
137
- - Phrase guidelines as desired behavior and preferred patterns.
138
- - Prefer established docs such as `CHANGELOG.md`, `IMPLEMENTATION_MAP.html`, or handoff notes for change history and implementation context.
101
+ - For where a comment, a compatibility note, deprecated behavior, a rejected alternative, a migration rationale, a change narrative, or a handoff log belongs — how to phrase a rule others will follow, and whether active docs should link to history — read and use `${CODEX_HOME:-$HOME/.codex}/guides/documentation-hygiene.md` as a scoped extension of this section.
139
102
 
140
103
  ## Visual Explanations
141
104
 
@@ -57,7 +57,7 @@ Delegate execution, not decisions. A unit is delegable only when it is decision-
57
57
  - Use a resident teammate only for dependent slices in one burst. Verify that the CLI preserves its model and context; resume-after-completion may silently change both. Retire after the burst or cache TTL, and persist durable knowledge in files.
58
58
  - After a discard or direction change, respawn once a routine round costs about as much as a fresh slice. Recover unique in-flight state to files first.
59
59
  - Redirects to busy workers may queue rather than preempt. Check artifacts before destructive redirects, phrase them conditionally, and stop an actively harmful worker by scoped PID/worktree authority.
60
- - Idle/progress notifications are hypotheses; verify repo artifacts before re-dispatch. Cross-reset state belongs in files, not task boards or transcripts. When polling concurrent async jobs, pin the exact id/handle received at dispatch — a "latest" convenience selector can silently point at a sibling job and return plausible-but-wrong results.
60
+ - Idle/progress notifications are hypotheses; verify repo artifacts before re-dispatch. An idle signal is liveness decoupled from the report: a subagent can go idle without ever delivering its result, so idle-without-report is not done — request the report explicitly rather than waiting. Cross-reset state belongs in files, not task boards or transcripts. When polling concurrent async jobs, pin the exact id/handle received at dispatch — a "latest" convenience selector can silently point at a sibling job and return plausible-but-wrong results.
61
61
  - Give reviewers/subagents a read-only diff, snapshot, or isolated worktree — not the live tree the main is editing — and forbid destructive git ops (checkout --, reset --hard, stash, clean) on any tree with uncommitted work; re-verify tree integrity before trusting results produced mid-edit.
62
62
  - Review cost scales with the diff, so layered review preserves delegation savings. Lower reviewer tier before dropping a review kind.
63
63
 
@@ -127,9 +127,9 @@ How much independence a review actually bought, as an ordinal grade per reviewer
127
127
 
128
128
  ## Dual-Provider Design Drafts
129
129
 
130
- - Trigger: the task is design (the staged-workflow guide's design stages) AND two or more providers are reachable at frontier tier. Reachability via an OAuth session is subscription-covered — no marginal spend, so no approval and no question: if a non-main-context OAuth frontier provider exists, proceed with the dual-provider design directly. The consent gate applies ONLY to a provider reachable solely via a metered API key: dispatching to it needs the user's explicit per-request approval of that spend (per-request, not standing — an old approval does not carry to the next design). If the only way to reach a second provider is un-approved metered API spend, stay single-provider rather than blocking the design.
130
+ - Trigger: the task is design high-level shape and implementation process, before any code — AND two or more providers are reachable at frontier tier. Reachability via an OAuth session is subscription-covered — no marginal spend, so no approval and no question: if a non-main-context OAuth frontier provider exists, proceed with the dual-provider design directly. The consent gate applies ONLY to a provider reachable solely via a metered API key: dispatching to it needs the user's explicit per-request approval of that spend (per-request, not standing — an old approval does not carry to the next design). If the only way to reach a second provider is un-approved metered API spend, stay single-provider rather than blocking the design.
131
131
  - Mechanics: compose ONE blind packet (evidence, constraints, rubric, neutral alternatives — the escalation-gate packet shape) and dispatch it unchanged to one frontier-tier model per provider; drafts stay independent — neither sees the other's output. Then adjudicate: compare the two dual-provider frontier design drafts against the rubric, take the winner as the skeleton, graft the loser's superior parts, and record what differed and why the synthesis chose as it did (FRONTIER disposition line).
132
- - Packet injection: a dispatched designer is hermetic — it reads only its packet and never loads this corpus. Inject the design principles the corpus would have supplied: concept economy (reuse/extend/rename/split, compact concept graph), the LLM/tools-code capability boundary, the staged-workflow design rules (smallest viable path, falsifiable done-when), and any domain-specific principles the design touches. A draft produced without the principles is not comparable to one produced with them.
132
+ - Packet injection: a dispatched designer is hermetic — it reads only its packet and never loads this corpus. Inject the design principles the corpus would have supplied: concept economy (reuse/extend/rename/split, compact concept graph), the LLM/tools-code capability boundary, the staged design rules (smallest viable path, falsifiable done-when), and any domain-specific principles the design touches. A draft produced without the principles is not comparable to one produced with them.
133
133
 
134
134
  ## Unattended Batch Safety
135
135
 
@@ -173,7 +173,7 @@ Write for the next agent and re-verification, not narrative. Required content:
173
173
 
174
174
  This is the human-readable projection of concrete models/tools; `launch/agent-launch.toml` is the machine launch authority and parity checks keep them aligned. Re-probe when the binding is older than ~8 weeks or a newer observable model/tool changes the surface. `agent-bios install` overwrites deployed bindings, so edit the repo copy.
175
175
 
176
- Binding (2026-07-25):
176
+ Binding (2026-08-10):
177
177
 
178
178
  | Slot | Binding | Notes |
179
179
  |---|---|---|
@@ -181,13 +181,13 @@ Binding (2026-07-25):
181
181
  | HELM | Claude Opus 5 (xhigh) · GPT-5.6 Sol (xhigh main; main Ultra requires explicit selection; bounded FRONTIER Ultra allowed) | standing main; Codex defaults bypass, explicit sandbox narrows |
182
182
  | WORKHORSE | Claude Opus 5 (medium) · GPT-5.6 Terra (high) | implementation and per-item judgment |
183
183
  | SWEEP | Claude Haiku 4.5 · GPT-5.6 Luna (low) | clear repeatable scans and mechanical work |
184
- | VERIFIER-A | onto-mcp review | logic/authority kind; tier units per funnel |
185
- | VERIFIER-B | `ultracode-for-codex` CLI v0.4.5 (Codex-backed; callable from Claude or Codex) | code/execution kind |
184
+ | VERIFIER-A | plain `codex exec` deep pass — GPT-5.6 Sol at ultra effort, packet on stdin (`-c service_tier="fast"` as explicit fast opt-in) | strongest single reader; cross-family from a Claude main |
185
+ | VERIFIER-B | Claude Code ultracode workflow (keyword-opened, many-agent) | code/execution kind; fan-out counterpart |
186
186
  | INDEPENDENT-PR-REVIEWER | Codex CLI | adversarial `gh pr diff` review |
187
187
  | Claude relocation | EnterWorktree, `/cd`, `--worktree`; resume is directory-scoped | verified 2.1.207 |
188
188
  | Codex relocation | `codex resume` (cwd-filtered; `--all` lifts), fork | verified 0.144.1 |
189
189
  | Claude teammate | named mailbox continuation; completed-agent message may cold-rerun on main model | keep resident; avoid completed resume |
190
- | Rate-limit fallback | OpenAI limited → VERIFIER-A on Anthropic; Claude limited → VERIFIER-B on Codex | record family collapse |
190
+ | Rate-limit fallback | OpenAI limited → VERIFIER-B (Anthropic workflow); Claude limited → VERIFIER-A (Codex exec) | record family collapse |
191
191
 
192
192
  Codex direct-drive (verified 0.144.1, 2026-07-12):
193
193