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
66 changes: 66 additions & 0 deletions corpus/skills/principle-prove-it/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
name: principle-prove-it
description: "Apply before any claim about code, systems, or history. No claim without same-turn evidence; a hedge is a trigger to verify, never a place to stop. Routes the four investigation products that build the evidence. Unlike the other principles this one auto-fires: it must intercept a claim as it forms, not be named after the fact."
---

# Prove It

A claim is settled only when the evidence for it is in the same message.
Everything else is a hypothesis and must say so.

**Why:** the cheap signal always stands in for the expensive one. A build
passes and gets reported as "it works." A file exists and gets reported as
"it's wired up." A name matches and gets reported as "that's the bug." Each
substitution is individually reasonable and collectively how wrong answers
reach the user with confidence attached.

## The gate

Before writing any claim of the form "this is fixed," "this works," "the
cause is X," "it's merged," "N are running" — you must already have, in the
same message, one of:

1. A command run this turn, with its real pasted output. Not summarized.
2. A `file:line` read this turn, named with the ref it was read at.
3. A test name plus its real pass/fail line from the runner.

Otherwise write `UNVERIFIED:` immediately before the claim. There is no
softer wording. The full evidence rules live in `engine/CLAUDE.core.md` and
are always loaded; this file is the judgment half plus the routing below.

**A hedge is a trigger.** "I think," "probably," "should work" mean run the
check now, not lower the confidence and continue.

**Absence of output is not proof of success.** A command that printed
nothing needs its exit code shown.

## What builds the evidence

The gate says what counts. It does not gather anything. Four product skills
do that, each fanning out to parallel subagents and returning the shape in
`corpus/skills/principle-prove-it/references/finding-shape.md`:

| Product | Question | Returns |
| --- | --- | --- |
| `how` | How does this work now? | The mechanism you must exercise |
| `why` | Why is it shaped this way? | The constraint you must not break |
| `alternatives-considered` | What else could this be? | Options, each labeled considered or invented |
| `spike-and-validate` | Does the invented one hold? | A throwaway that ran, and its real output |

They compose in that order and each is optional. A one-line fix needs none
of them. A claim about a subsystem nobody on the team has read needs `how`
before anything else.

The pairing that matters: `alternatives-considered` **generates**, so it can
always produce three plausible options and none of them are evidence.
`spike-and-validate` is its gate. An invented option that reaches a decision
without a spike is exactly the manufactured evidence this principle exists
to stop.

## Related

- `prove-it-ship-gate` — the done/shipped auto-route for live side effects.
- `principle-fix-root-causes` — reproduce before explaining.
- `principle-sequence-verifiable-units` — end each unit in a check.
- Mechanical enforcement: `engine/hooks/prove-it-ship-gate`,
`engine/hooks/hedge-runs-prove-it`, `engine/hooks/diu-stop`.
45 changes: 45 additions & 0 deletions corpus/skills/principle-prove-it/references/finding-shape.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Shared finding shape

Every investigation product (`how`, `why`, `alternatives-considered`,
`spike-and-validate`) fans out to parallel subagents and merges their
returns. They share this shape so `principle-prove-it` can consume any of
them without a per-skill adapter.

This is **not** the judge board in `product/skills/independent-judge-swarm`.
That board grades a finished artifact (`verdict`, `score`,
`blocking_issues`). These skills return findings about work that has not
happened yet, so there is nothing to grade.

## Fields

| Field | Shape |
| --- | --- |
| `lens` | string — which angle this subagent owned |
| `claim` | string — one sentence, the thing found |
| `grounding` | `read-confirmed` \| `name-matched` \| `inferred` \| `invented` |
| `evidence` | `file:line` + ref, a command + its real output, or a URL |
| `ref` | which ref `evidence` was read at: working tree, `HEAD`, `origin/<base>`, installed bundle |
| `null_result` | string — what was searched and came back empty |

## Rules

- A subagent that found nothing still returns a row with `null_result` set.
Silence is not the same as absence, and a skipped source is not a null —
see the Evidence rules in `engine/CLAUDE.core.md`.
- `grounding: read-confirmed` means the subagent opened the actual
reference and traced it. `name-matched` means the path or symbol name
looked right. A working-tree read under a dirty path is `name-matched`.
- `grounding: inferred` and `grounding: invented` rows carry no authority
on their own. They are hypotheses. Route an `invented` row that matters
to `spike-and-validate` before it reaches a decision.
- The merge step reports disagreement rather than resolving it silently.

## Merge

1. Group rows by `claim`.
2. Two subagents reaching the same `read-confirmed` claim independently is
the strongest signal available here. Check they were not handed the same
anchor and prompt first — template-identical attempts converge on the
same wrong answer exactly like independent ones agree.
3. Any `read-confirmed` row beats any number of `inferred` rows.
4. Report every `null_result` in the output. Do not drop it for brevity.
15 changes: 15 additions & 0 deletions corpus/skills/principle-prove-it/tests/fires_example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
An agent has just edited a retry handler and is about to write "this fixes
the duplicate-write bug" into a PR body. Nothing ran this turn: no repro, no
test, no command output. The agent never types a slash command.

This skill fires on the claim shape itself. Unlike the other `principle-*`
skills, it does not carry `disable-model-invocation: true`, so its
`description:` is loaded and the model can match it — which is the whole
point: a gate that only works when someone remembers to name it is not a
gate. A fix claim with no evidence in the same message is exactly what the
description targets, and once loaded the skill supplies both the three
accepted evidence forms and the routing table that sends the agent to `how`
to find which artifact to exercise.

The same auto-fire path covers the hedge case: "this should work" is a
trigger to run the check, not a softer way to state the claim.
12 changes: 12 additions & 0 deletions corpus/skills/principle-prove-it/tests/stays_silent_example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
A user asks which name reads better for a local variable in a helper they are
writing, `retryCount` or `attempts`, and the agent gives an opinion.

This skill stays silent. Auto-firing is driven by the presence of a claim to
hold evidence against, and there is none here: a naming preference asserts
nothing about behavior, system state, history, or a cause. There is no
"this is fixed", no "the cause is X", no hedge about code that could be
checked by running something.

Firing here would be the skill misbehaving rather than working — demanding a
command run and pasted output before answering a style question is the
false-positive this fixture exists to pin down.
72 changes: 72 additions & 0 deletions docs/skill-triggers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Skill triggers: what the model may fire on its own

Every skill in this repo is in one of two states, set by one line of
frontmatter.

**Auto-fire** (no flag). Claude Code loads the skill's `description:` and the
model may invoke the skill when a turn matches it. Use this for a gate or a
procedure that is worthless if it only runs when someone remembers to name
it — `principle-prove-it` fires on a claim taking shape, `narrow-the-scope`
on a loop that is not converging.

**Explicit only** (`disable-model-invocation: true`). The `description:` is
never loaded into context, so no description match can reach the skill. The
only way in is a typed `/<name>`. Use this for steering vocabulary you reach
for deliberately (the `principle-*` set), personal convention files
(`*-mode`), and anything whose blast radius demands a human in the loop.

## How to choose

Ask what happens when the model guesses wrong in each direction.

| | A false fire costs | A missed fire costs |
| --- | --- | --- |
| Gate / evidence rule | A little context | A wrong claim reaching the user |
| Steering vocabulary | Context on 25 principles every task | Nothing — you name it when you want it |
| Irreversible action | A force-merge nobody asked for | A human types six more words |

Auto-fire when a missed fire is the expensive direction. Flag it when a
false fire is.

## Prose is not a trigger mechanism

A skill cannot opt out of auto-invocation by asking. `admin-bypass-sweep`
force-merges PRs past required checks, and its description said "MANUAL,
HUMAN-ONLY ... Do not auto-invoke this skill from a natural-language
request, a description match, or another agent's delegation" — while
remaining fully auto-invocable, because the flag was missing. The model reads
that description to decide whether to fire; the sentence asking it not to is
inside the thing it is matching on.

`scripts/check_skill_trigger_policy.py` fails closed on that contradiction:
a skill that declares itself manual in its own frontmatter must carry the
flag. It also regenerates the inventory below, because a hand-maintained
list of 35+ skills is stale by the next PR.

```sh
python3 scripts/check_skill_trigger_policy.py # verify
python3 scripts/check_skill_trigger_policy.py --write # regenerate
```

Fixture contracts follow from the same line — see
`scripts/check_skill_trigger_mechanism.py`. A flagged skill's
`fires_*.md` must contain its literal `/<name>`; an auto-fire skill's must
instead share vocabulary with its `description:`.

## Inventory

<!-- BEGIN generated: skill-triggers (scripts/check_skill_trigger_policy.py) -->

### Auto-fire (17)

The model may invoke these from a description match. Everything here is a gate or a procedure that is useless if it only runs when named.

`create-skill`, `draft-pr`, `make-pr`, `thrash-reflect-automate`, `principle-prove-it`, `prove-it-ship-gate`, `alternatives-considered`, `diu`, `how`, `land-stack`, `loop-generator`, `narrow-the-scope`, `show-me-your-work`, `spike-and-validate`, `split-scope`, `visual-proof`, `why`

### Explicit invocation only (30)

These carry `disable-model-invocation: true`. Claude Code does not load their `description:` at all, so the only way in is a typed `/<name>`.

`automate-me`, `reflect`, `cat-mode`, `principle-assert-invariants-not-last-bug`, `principle-bind-to-named-inventory`, `principle-build-the-lever`, `principle-encode-lessons-in-structure`, `principle-experience-first`, `principle-explicit-errors`, `principle-fix-root-causes`, `principle-flag-your-own-corrections`, `principle-foundational-thinking`, `principle-generalize-from-rejection`, `principle-guard-the-context-window`, `principle-laziness-protocol`, `principle-manage-idle-resumption`, `principle-minimize-reader-load`, `principle-name-the-scorer`, `principle-never-block-on-the-human`, `principle-outcome-oriented-execution`, `principle-push-not-poll`, `principle-scope-the-session`, `principle-separate-before-serializing-shared-state`, `principle-sequence-verifiable-units`, `principle-subtract-before-you-add`, `principle-trace-token-burn-loop`, `principle-type-system-discipline`, `admin-bypass-sweep`, `i-have-adhd`, `independent-judge-swarm`

<!-- END generated: skill-triggers -->
34 changes: 31 additions & 3 deletions engine/hooks/no-comments/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
comments: shebangs, encoding lines, noqa, type:, pragma, pylint, mypy,
eslint, prettier, ts-ignore, ts-expect-error, istanbul, nosec, ruff,
fmt, and SPDX or license headers. Markdown, JSON, YAML, TOML and other
non-code files are out of scope. Docstrings are out of scope.
non-code files are out of scope. Python triple-quoted strings are out
of scope: a docstring's usage examples and a markdown fixture's '#'
headings are string content, not comments.
"""
from __future__ import annotations

Expand Down Expand Up @@ -34,6 +36,7 @@
HASH_LINE_RE = re.compile(r"^\s*#")
SLASH_LINE_RE = re.compile(r"^\s*(?://|/\*|\*(?!/)|<!--)")
STRING_RE = re.compile(r"""("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)""")
TRIPLE_QUOTE_RE = re.compile(r'"""|\'\'\'')
TRAILING_HASH_RE = re.compile(r"\s#(?!\{)\s*\S")
TRAILING_SLASH_RE = re.compile(r"\s//\s*\S")
TRAILING_DIRECTIVE_RE = re.compile(
Expand All @@ -51,16 +54,41 @@ def _strip_strings(line: str) -> str:
return STRING_RE.sub('""', line)


def _starts_outside_triple_quotes(ext: str, lines: list[str]) -> list[bool]:
"""Per line, whether it begins outside every Python triple-quoted string.

Only Python has triple-quoted strings among the hash languages, so every
other extension is entirely outside by definition. Tracks parity across
the text it is handed, which for the CI twin is the run of added diff
lines rather than the whole file.
"""
if ext != ".py":
return [True] * len(lines)
outside: list[bool] = []
open_delim: str | None = None
for line in lines:
outside.append(open_delim is None)
for match in TRIPLE_QUOTE_RE.finditer(line):
token = match.group(0)
if open_delim is None:
open_delim = token
elif open_delim == token:
open_delim = None
return outside


def comment_lines(path: str, text: str) -> list[str]:
if not is_code_file(path) or not text:
return []
ext = os.path.splitext(path.lower())[1]
hash_lang = ext in HASH_LANGS
slash_lang = ext in SLASH_LANGS or ext in (".html", ".vue", ".svelte")
hits: list[str] = []
for raw in text.splitlines():
raw_lines = text.splitlines()
outside_triple_quotes = _starts_outside_triple_quotes(ext, raw_lines)
for raw, outside in zip(raw_lines, outside_triple_quotes):
line = raw.rstrip()
if not line.strip() or DIRECTIVE_RE.match(line):
if not outside or not line.strip() or DIRECTIVE_RE.match(line):
continue
if hash_lang and HASH_LINE_RE.match(line):
hits.append(line.strip())
Expand Down
29 changes: 29 additions & 0 deletions engine/hooks/no-comments/tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ def edit(path: str, new: str) -> dict:
)
REAL_JS_COMMENT = "const x = 1; // fallback when config is missing\n"

MARKDOWN_FIXTURE_IN_TRIPLE_QUOTES = (
'MANUAL_NO_FLAG = """---\n'
"name: admin-bypass-sweep\n"
"---\n"
"\n"
"# admin-bypass-sweep\n"
'"""\n'
)
DOCSTRING_WITH_TRAILING_HASH_USAGE = (
'"""Which skills may the model fire on its own?\n'
"\n"
" python3 scripts/check_skill_trigger_policy.py # verify\n"
" python3 scripts/check_skill_trigger_policy.py --write # regenerate doc\n"
'"""\n'
)
COMMENT_AFTER_A_CLOSED_TRIPLE_QUOTED_STRING = (
MARKDOWN_FIXTURE_IN_TRIPLE_QUOTES + "# real shape: the flag was missing here\n"
)


class TestBlocks(unittest.TestCase):
def test_blocks_python_line_comment_from_real_session(self):
Expand All @@ -56,6 +75,10 @@ def test_blocks_block_comment_and_html_comment(self):
self.assertIsNotNone(detect.decide(edit("/repo/a.js", "/* why */\nlet a = 1;\n")))
self.assertIsNotNone(detect.decide(edit("/repo/a.html", "<!-- nav -->\n<div></div>\n")))

def test_blocks_a_real_comment_after_a_closed_triple_quoted_string(self):
hits = detect.comment_lines("/r/t.py", COMMENT_AFTER_A_CLOSED_TRIPLE_QUOTED_STRING)
self.assertEqual(hits, ["# real shape: the flag was missing here"])

def test_blocks_write_and_multiedit_shapes(self):
w = {"tool_name": "Write", "tool_input": {"file_path": "/r/x.sh", "content": "#!/bin/bash\n# step one\nls\n"}}
m = {"tool_name": "MultiEdit", "tool_input": {"file_path": "/r/x.py", "edits": [{"new_string": "a = 1\n"}, {"new_string": "# TODO\n"}]}}
Expand All @@ -77,6 +100,12 @@ def test_allows_hash_and_slashes_inside_strings_and_urls(self):
def test_allows_docstrings_and_plain_code(self):
self.assertEqual(detect.comment_lines("/r/a.py", '"""Module doc.\n\nMore prose.\n"""\ndef f():\n return 1\n'), [])

def test_allows_markdown_headings_inside_a_python_triple_quoted_fixture(self):
self.assertEqual(detect.comment_lines("/r/t.py", MARKDOWN_FIXTURE_IN_TRIPLE_QUOTES), [])

def test_allows_trailing_hash_usage_lines_inside_a_module_docstring(self):
self.assertEqual(detect.comment_lines("/r/a.py", DOCSTRING_WITH_TRAILING_HASH_USAGE), [])

def test_allows_non_code_files(self):
self.assertIsNone(detect.decide(edit("/r/README.md", "# Heading\n")))
self.assertIsNone(detect.decide(edit("/r/ci.yml", "# comment in yaml\n")))
Expand Down
1 change: 1 addition & 0 deletions engine/skills/make-pr/scripts/preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def gates_for(paths: list[str], base: str | None = None) -> list[list[str]]:
["python3", "scripts/check_skill_file_refs.py"],
["python3", "scripts/check_skill_test_coverage.py"],
["python3", "scripts/check_skill_trigger_mechanism.py"],
["python3", "scripts/check_skill_trigger_policy.py"],
]
return cmds

Expand Down
10 changes: 10 additions & 0 deletions engine/skills/make-pr/tests/test_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ def test_gates_for_rule_prose_include_codify_check(self):
def test_gates_for_neutral_only_are_empty(self):
self.assertEqual(pf.gates_for(["docs/ecosystem.md", "README.md"]), [])

def test_gates_for_skill_slice_include_trigger_policy(self):
"""A skill slice must run the trigger-policy gate.

It catches a skill declaring itself human-only with no
disable-model-invocation behind it -- the admin-bypass-sweep shape,
where a force-merge skill was reachable by description match.
"""
cmds = pf.gates_for(["product/skills/how/SKILL.md"])
self.assertIn(["python3", "scripts/check_skill_trigger_policy.py"], cmds)

def test_gates_for_rule_prose_with_base_includes_dated_provenance_check(self):
self.assertIn(
["python3", "scripts/check_no_dated_provenance.py", "--base", "origin/main"],
Expand Down
1 change: 1 addition & 0 deletions product/skills/admin-bypass-sweep/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ description: >
has typed its literal slash command themselves in the current turn (the
exact form depends on how it was installed — see the "STOP" section).
See that section before doing anything.
disable-model-invocation: true
---

# admin-bypass-sweep
Expand Down
Loading
Loading