Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/communication-structure/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Generated by `scripts/skill_index.py` (do not hand-edit; rerun it after changing

| File | Read this when... | Tok |
|---|---|---|
| `references/artifact-templates.md` | you draft a status update, PR body, release note, decision memo or executive summary, incident summ… | 2292 |
| `references/artifact-templates.md` | you draft a status update, PR body, release note, decision memo or executive summary, incident summ… | 2308 |
| `references/leadership-status-update.md` | you write any status update a non-technical leader will read: a tracker project update, an issue st… | 500 |

## Scripts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ verified and command. Fenced code does not count toward the budget.
`audit-pr` is the body of an audit, security or hardening PR. `sections` fails, at any length, when a required heading
(every heading of the template below except `Dependency upgrades`) is missing, `What changes for reviewers and users` is
empty (items, or `None`), or `Not fixed` carries a file:line, route, URL or file path (counts only). A closing footer
(`---` or `🤖 Generated`, then only blank, `Generated with` and `Co-Authored-By:` lines) is not checked; other text after
a `---` is. With `--range BASE..HEAD` it also fails when a changed file is not named under `Changes by area`, by full path
(`---` or `🤖 Generated`, then only blank, `Generated with` and `Co-Authored-By:` lines, any of them in `<sub>`, `<sup>`,
`<small>`, `*` or `_` markup) is not checked; other text after a `---` is. With `--range BASE..HEAD` it also fails when a changed file is not named under `Changes by area`, by full path
or by a containing directory with a trailing slash. The guard hook picks this kind when the `gh pr create|edit` title says audit, security
or harden, and for `create` passes the diff against `--base` (default `origin/HEAD`).

Expand Down
13 changes: 8 additions & 5 deletions .claude/skills/communication-structure/scripts/comms_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
AUDIT_SECTIONS heading is present, "What changes for reviewers and users" is not empty (items, or "None"), "Not fixed"
holds counts only (no file:line, route or URL), and, with --range BASE..HEAD, every file the range changes is named
under "Changes by area" by its full path or by a containing directory with a trailing slash. A trailing attribution footer
(a `---` rule or `🤖 Generated` line, then only blank, `---`, `Generated with/by` and `Co-Authored-By:` lines) is not part
of the last section; any other text after a `---` is still checked.
(a `---` rule or `🤖 Generated` line, then only blank, `---`, `Generated with/by` and `Co-Authored-By:` lines; those lines
may sit in `<sub>`, `<sup>`, `<small>`, `*` or `_` markup) is not part of the last section; any other text after a `---` is
still checked.

Prints `FAIL <rule> L<n>: <problem>. Fix: <exact fix>` per failure and a score line; exit 1 when any non-advisory
rule fails (below the bar), 0 otherwise, 2 on usage, unreadable input or a --range git cannot resolve (fail closed).
Expand Down Expand Up @@ -239,8 +240,10 @@ def _meeting(res, prose):
DETAIL = re.compile(r"\.[A-Za-z]\w{0,7}:\d+|#L\d+|\blines?\s+\d+|\b(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+/|"
r"(?<![\w/:.-])/[\w.-]+|\bhttps?://|\b[\w.-]{1,100}/[\w./-]{0,200}\.[A-Za-z]\w{0,7}\b")
# one attribution-footer line, matched whole: a `---` rule or `🤖 Generated ...` (either starts a footer), `Generated with/by ...`,
# `Co-Authored-By: ...` or nothing. ponytail: a line is judged by its prefix, so detail on a `Generated ...` or `Co-Authored-By:` line goes unchecked; tighten the shapes if that is abused
FOOTER = re.compile(r"(?:(?P<mark>-{3,}|🤖\s*Generated\b.*)|Generated (?:with|by)\b.*|Co-Authored-By:.*)?", re.I)
# `Co-Authored-By: ...` or nothing. The `🤖 Generated`, `Generated with/by` and `Co-Authored-By:` shapes may open in <sub>, <sup>, <small>, `*` or `_` markup (the closer is part of the tail).
# ponytail: a line is judged by its prefix, so detail on a `Generated ...` or `Co-Authored-By:` line goes unchecked; tighten the shapes if that is abused
_MARKUP = r"(?:</?(?:sub|sup|small)>|[*_]){0,6}" # bounded: an unbounded repeat would be quadratic on a long run of `*`
FOOTER = re.compile(r"(?:(?P<mark>-{3,}|" + _MARKUP + r"🤖[\s*_]*Generated\b.*)|" + _MARKUP + r"Generated (?:with|by)\b.*|" + _MARKUP + r"Co-Authored-By:.*)?", re.I)


def _name(text):
Expand All @@ -265,7 +268,7 @@ def _sections(text):
runs to the next heading of the same or a higher level (sub-headings stay inside it); the first of a repeated heading
wins; HTML comments are dropped, so a template hint is not content. A trailing attribution footer is not content either:
the last section ends at the first `---` rule or `🤖 Generated` line that is followed only by FOOTER-shaped lines (blank,
`---`, `Generated with/by ...`, `Co-Authored-By: ...`) to the end; any other text after a rule keeps the rule as content."""
`---`, `Generated with/by ...`, `Co-Authored-By: ...`, in markup or not) to the end; any other text after a rule keeps the rule as content."""
lines = _blank_comments(text).splitlines()
heads, fence, foot = [], False, len(lines)
for i, ln in enumerate(lines):
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/idea-critic/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Generated by `scripts/skill_index.py` (do not hand-edit; rerun it after changing
| File | Read this when... | Tok |
|---|---|---|
| `references/anti-rationalization.md` | you are about to emit PASS_TO_USER, skip a hat, or accept a shortcut (see SKILL.md, Escalate). | 332 |
| `references/council.md` | a decision is high-stakes AND hard to reverse, and one inline pass is too weak: you want independen… | 2267 |
| `references/council.md` | a decision is high-stakes AND hard to reverse, and one inline pass is too weak: you want independen… | 2379 |
| `references/decision-hygiene.md` | the owner faces a hard, high-stakes call of their *own* — a pivot, quitting or killing a line of wo… | 1137 |
| `references/full-map.md` | the quick attack in SKILL.md is not enough: an owner-decision or high-blast claim, an agent-origina… | 3395 |

Expand Down
10 changes: 5 additions & 5 deletions .claude/skills/idea-critic/references/council.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ neither and it convenes (legacy behavior), so declare them.
| The evil twin argues the strongest dissent | `--twin`: after the decision, one call (`lenses/evil-twin.txt`) writes a pre-mortem and the strongest case against it; kept in `minority_report` with the dissenting seats' names (their answers stay verbatim in `dissent`) |
| Pre-mortem | every seat answers `premortem`, `kill_criterion`, `leading_metric` (recorded in `premortems`), then the twin's |
| Deterministic aggregate; the chair breaks ties | decision = highest weighted mean of seat scores (0-5 per option and criterion); a tie goes to `--chair`'s own top option, then to option order (`do-nothing` first). The verdict tally ties go to the chair, else `HOLD` > `REVISE` > `PASS_TO_USER` |
| Output: decision, kill criteria, metric, review date | record keys `decision`, `kill_criteria`, `leading_metric`, `review_date` (`--review-date`, else today + `--review-days`, default 30). A closer no seat supplied is `UNVERIFIED`, never invented |
| Output: decision, kill criteria, metric, review date | record keys `decision`, `kill_criteria`, `kill_criteria_by_lens` (`{lens: text}`), `leading_metric`, `review_date` (`--review-date`, else today + `--review-days`, default 30). A closer no seat supplied is `UNVERIFIED`, never invented |
| Convene only when it matters | the gate above |

## Run
Expand All @@ -36,7 +36,7 @@ python3 scripts/veche.py --cmd "claude -p --strict-mcp-config" \
--stakes high --reversibility hard
```

- `--cmd` reads the prompt on stdin and prints the answer. Without `--lenses` it is solo-3: one cmd, the three hats.
- `--cmd` reads the prompt on stdin and prints the answer. A custom `--cmd` reply needs a JSON object with a `verdict` (`HOLD|REVISE|PASS_TO_USER`) and string-only `claims` items; prose and stray braces around it are skipped, the first object with a valid `verdict` wins, and with `--option` the `scores` map may sit under `scores` in that object, or in any other object (bare or under `scores`). Without `--lenses` it is solo-3: one cmd, the three hats.
- `--option` names must be short tokens (letters, digits, `-`, `_`, starting with a letter or digit; at most 32): seats echo them as JSON keys. Others fail before any call.
- The packet is shown with numbered lines and its path under `--root`, so a model with no file tools
can still cite `path:line`; keep the packet under `--root`.
Expand All @@ -56,7 +56,7 @@ python3 scripts/veche.py --cmd "claude -p --strict-mcp-config" \
checked before each call. Any cap hit, non-zero exit, timeout or unparseable answer yields
`COULD_NOT_DECIDE` (exit 3), never a guessed verdict, with `dropped` reasons and `cost`. A timed-out command's grandchildren may outlive
it, so use a cmd that exits cleanly. `--dry-run` prints planned calls.
- `--json-out PATH` also writes the record there (a bad path fails before any call). A gate skip prints `NOT_CONVENED` on stderr, noting an ignored `--twin`.
- `--json-out PATH` also writes the record there (a bad path fails before any call; a write that fails at the end exits 2 only when the verdict would exit 0, so 3 and 4 stay). A gate skip prints `NOT_CONVENED` on stderr, noting an ignored `--twin`.
- Validate: `scripts/validate_verdict.py --file record.json` accepts the record (it has `tally`),
`COULD_NOT_DECIDE` and `NOT_CONVENED` with a `reason`, and a record with a `decision` only when it also
carries `kill_criteria`, `leading_metric` and an ISO `review_date`.
Expand All @@ -80,8 +80,8 @@ which yields the `default_action` above. A malformed `--criteria` error states t
When the critique round runs, each seat also ranks the other (anonymized, lens names masked) answers best
first; valid rankings are Borda-aggregated into `peer_ranking` (lens to mean points, informational: the
verdict still comes from the tally and evidenced attacks). There is no separate adapter: any other council
can be a seat or the whole panel through the existing `--cmd`, a script that reads the prompt on stdin and
prints the JSON contract. A seat that fails (cap, timeout, cmd exit) is dropped and lowers confidence; a
can be a seat or the whole panel through the existing `--cmd` (a script; its reply contract is under Run).
A seat that fails (cap, timeout, cmd exit) is dropped and lowers confidence; a
failed critique call or twin keeps the round-1 verdict. Versus the fetched sources (`docs/standards-index.md`):

| | llm-council | Du et al. debate | Mixture-of-Agents | Veche |
Expand Down
55 changes: 39 additions & 16 deletions .claude/skills/idea-critic/scripts/veche.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,19 @@
dropped with their reasons and the call cost, and a stderr COUNCIL_BLOCKING_WORK alarm (fired while the council
runs, once --alarm-s has passed), so a stuck council never stalls unrelated work. --cmd can wrap any
external council (a script reading the prompt on stdin, printing the JSON contract).
Side effects: spawns the given commands, reads files under --root, writes --json-out (an unwritable path exits 2, also after the
record was printed). Exit 0 decided.
Side effects: spawns the given commands, reads files under --root, writes --json-out (a temp file beside it, then os.replace; an unwritable path exits 2,
also after the record was printed, unless the verdict already exits 3 or 4). Exit 0 decided.
"""
from __future__ import annotations

import argparse, concurrent.futures, datetime, threading, json, os, random, re, secrets, shlex, subprocess, sys, time
import argparse, concurrent.futures, contextlib, datetime, threading, json, os, random, re, secrets, shlex, subprocess, sys, time
from pathlib import Path

VERDICTS = ("HOLD", "REVISE", "PASS_TO_USER") # most cautious first: ties go left
HATS = "skeptic,better-way,kill-criteria"
NAME = re.compile(r"^[a-z][a-z-]{0,30}$")
TOKEN = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]{0,31}") # an --option name: every seat must echo it exactly as a JSON key
TOKEN_RULE = f"letters, digits, - or _, starting with a letter or digit, at most 32 chars: {TOKEN.pattern}"
CAP = "cap reached before call"
CITE = re.compile(r"^(?P<p>[^\s:][^:]*):(?P<a>\d+)(?:-(?P<b>\d+))?$")
DO_NOTHING = "do-nothing"
Expand Down Expand Up @@ -72,10 +73,11 @@ def norm(t: str) -> str:

def uniq(items: list) -> list:
"""Drop near-duplicate strings, keeping the first wording and the order: case, spacing and punctuation are
ignored; digits and comparison operators are not (5% and 6% stay two items, and so do `< 200 ms` and `> 200 ms`)."""
ignored; digits and comparison operators are not (5% and 6% stay two items, and so do `< 200 ms` and `> 200 ms`); `≤` and `≥`
are the same operators as `<=` and `>=`, so the Unicode and ASCII spellings fold together."""
seen: dict = {}
for t in items:
seen.setdefault(" ".join(re.findall(r"\w+|[<>≤≥]=?", t.lower())), t)
seen.setdefault(" ".join(re.findall(r"\w+|[<>]=?", t.lower().replace("≤", "<=").replace("≥", ">="))), t)
return list(seen.values())


Expand Down Expand Up @@ -200,7 +202,7 @@ def run(a, st=None) -> dict:
names = [o.strip() for o in (a.option or []) if o.strip() and o.strip() != DO_NOTHING]
for o in names:
if not TOKEN.fullmatch(o):
raise Stop(f"bad --option {o!r}: use a short token (letters, digits, - or _, at most 32 chars), e.g. ship; "
raise Stop(f"bad --option {o!r}: use a short token ({TOKEN_RULE}), e.g. ship; "
"every seat must repeat it exactly as a JSON key")
options = [DO_NOTHING] + names
if len(options) > 8 or len(set(options)) != len(options):
Expand Down Expand Up @@ -388,6 +390,7 @@ def others_of(i):
["UNVERIFIED: no seat wrote a pre-mortem"]
kills = uniq([x["kill"] for x in answers if x["kill"]] + ([twin["kill_criterion"]] if isinstance(twin.get("kill_criterion"), str) and twin["kill_criterion"].strip() else []))
rec["kill_criteria"] = kills or ["UNVERIFIED: no seat stated a measurable tripwire"]
rec["kill_criteria_by_lens"] = {x["lens"]: x["kill"] for x in answers if x["kill"]} # each seat's own wording, before the dedupe
metrics = {x["lens"]: x["metric"] for x in answers if x["metric"]}
rec["leading_metric"] = metrics.get(a.chair) or next(iter(metrics.values()), "UNVERIFIED: no seat named a metric")
rec["minority_report"] = {"twin": twin or None, "dissenting_seats": [x["lens"] for x in dissent]} # their answers are in `dissent`
Expand Down Expand Up @@ -424,7 +427,7 @@ def stop():

def main(argv=None) -> int:
"""CLI entry. Prints the JSON record (and writes it to --json-out). Exit 0 decided, 2 --json-out unwritable (the record is still
printed), 3 COULD_NOT_DECIDE (fail closed), 4 NOT_CONVENED."""
printed; a write that fails at the end changes only exit 0, never 3 or 4), 3 COULD_NOT_DECIDE (fail closed), 4 NOT_CONVENED."""
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--cmd", action="append", required=True)
ap.add_argument("--claim", required=True)
Expand All @@ -433,12 +436,15 @@ def main(argv=None) -> int:
ap.add_argument("--origin", default="agent-originated", choices=("owner-request", "agent-originated"))
ap.add_argument("--root", default=".")
ap.add_argument("--lenses", default=HATS, help="comma list of lens files; seats: cpo,cto,cfo,ciso,user-advocate")
ap.add_argument("--option", action="append", help="an option to score: a short token such as ship (repeat; do-nothing is always added; include the claim)")
ap.add_argument("--option", action="append", help=f"an option to score, a short token ({TOKEN_RULE}) since every seat repeats it as a JSON key, e.g. ship (repeat the flag; do-nothing is always added; include the claim)")
ap.add_argument("--criteria", help="name:weight,... fixed before scoring; required with --option")
ap.add_argument("--chair", help="a lens name: breaks ties in the verdict tally and the option scores")
ap.add_argument("--twin", action="store_true", help="add the evil twin: pre-mortem and strongest dissent")
ap.add_argument("--stakes", choices=("low", "medium", "high"))
ap.add_argument("--reversibility", choices=("easy", "hard"))
ap.add_argument("--stakes", choices=("low", "medium", "high"),
help="low or medium skips the council (NOT_CONVENED, exit 4, no seat called) only if --reversibility easy is also given; high, or no --reversibility, convenes")
ap.add_argument("--reversibility", choices=("easy", "hard"),
help="easy skips the council (NOT_CONVENED, exit 4, no seat called) only if --stakes low or medium is also given; hard, or no --stakes, convenes. "
"easy also makes the default_action of a COULD_NOT_DECIDE record proceed, else hold")
ap.add_argument("--review-date", help="ISO date to re-check the decision (default: today + --review-days)")
ap.add_argument("--review-days", type=int, default=30)
ap.add_argument("--max-calls", type=int, default=12)
Expand All @@ -447,15 +453,28 @@ def main(argv=None) -> int:
ap.add_argument("--timeout", type=int, default=120)
ap.add_argument("--alarm-s", type=int, default=300, help="print COUNCIL_BLOCKING_WORK to stderr once the council has run this long, while it runs")
ap.add_argument("--progress-s", type=int, default=30, help="stderr heartbeat every N seconds while the council runs (0 = off)")
ap.add_argument("--json-out", help="also write the JSON record to this file (opened before any call, so a bad path fails early)")
ap.add_argument("--json-out", help="also write the JSON record to this file on every outcome, via a temp file in the same directory and os.replace; "
"an empty file is written first, before any call, so a bad path fails early. The directory must be writable: a read-only file in it is replaced, "
"while a read-only directory or a bind-mounted file (os.replace cannot replace a mount point) exits 2")
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--dry-run", action="store_true")
a = ap.parse_args(argv)

def write_out(text): # an unwritable --json-out is a usage error (exit 2, no traceback), at the start and at the end
def write_out(text): # atomic: a reader sees the old file or the whole new one. ponytail: no fsync, so not durable across power loss
real = os.path.realpath(a.json_out) # a symlinked path keeps pointing at its target
tmp = f"{real}.{secrets.token_hex(4)}.tmp"
try:
Path(a.json_out).write_text(text)
except OSError as e:
if os.path.exists(a.json_out) and not os.path.isfile(a.json_out): # a device, FIFO or directory: renaming over it would replace the node itself
Path(a.json_out).write_text(text) # (a directory fails here, as before)
else:
with open(tmp, "x", encoding="utf-8") as f:
f.write(text)
with contextlib.suppress(OSError): # an existing record keeps the mode its owner gave it
os.chmod(tmp, os.stat(real).st_mode & 0o7777)
os.replace(tmp, real)
except OSError as e: # an unwritable --json-out is a usage error (exit 2, no traceback), at the start and at the end
with contextlib.suppress(OSError):
os.unlink(tmp)
ap.error(f"--json-out: cannot write {a.json_out}: {e.strerror}")

if a.json_out:
Expand Down Expand Up @@ -487,9 +506,13 @@ def write_out(text): # an unwritable --json-out is a usage error (exit 2, no tr
f"default_action={rec.get('default_action', 'n/a')}", file=sys.stderr)
out = json.dumps(rec, indent=2)
print(out)
code = {"COULD_NOT_DECIDE": 3, "NOT_CONVENED": 4}.get(rec["verdict"], 0)
if a.json_out:
write_out(out + "\n") # the record is already on stdout, so an unwritable path loses nothing
return {"COULD_NOT_DECIDE": 3, "NOT_CONVENED": 4}.get(rec["verdict"], 0)
try:
write_out(out + "\n") # the record is already on stdout, so an unwritable path loses nothing
except SystemExit as e: # the message is printed; a blocking verdict exit (3, 4) must not be turned into 2
code = code or e.code
return code


if __name__ == "__main__":
Expand Down
Loading
Loading