claude-dev-env 8.102.0 → 8.103.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.
- package/.agents/skills/orchestrator/SKILL.md +2 -0
- package/.agents/skills/orchestrator/reference/completion-reporting.md +13 -0
- package/.agents/skills/pull-request/reference/body-sections.md +2 -1
- package/bin/install.prompt-context.test.mjs +2 -1
- package/docs/agents-guidance/asks.md +6 -0
- package/docs/rule-guides/proof-before-pull-request.md +3 -3
- package/hooks/blocking/pull_request_proof.py +64 -6
- package/hooks/blocking/test_pull_request_proof.py +87 -0
- package/hooks/blocking/test_turn_review_findings.py +25 -0
- package/hooks/blocking/test_turn_review_gate.py +462 -0
- package/hooks/blocking/test_turn_review_snapshot.py +107 -0
- package/hooks/blocking/turn_review_findings.py +102 -0
- package/hooks/blocking/turn_review_gate.py +362 -0
- package/hooks/blocking/turn_review_snapshot.py +116 -0
- package/hooks/features/conduct-gates.md +5 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/pull_request_proof_constants.py +14 -1
- package/hooks/hooks_constants/turn_review_gate_constants.py +161 -0
- package/package.json +1 -1
- package/scripts/policy_lint/adapter_configuration.py +6 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/tests/test_adapter_configuration.py +8 -0
|
@@ -110,6 +110,7 @@ Checkpoint after decisions and state changes, before waiting, and before a known
|
|
|
110
110
|
|
|
111
111
|
### Complete the requested outcome
|
|
112
112
|
|
|
113
|
+
Before a completion report or status answer, follow [completion reporting](reference/completion-reporting.md).
|
|
113
114
|
Close a task only after its evidence is checked and integrated into the task authority.
|
|
114
115
|
Close a goal only when its acceptance conditions and authorized delivery are met.
|
|
115
116
|
Keep a named pending user action open when required. Completed work needs no invented final human action.
|
|
@@ -137,6 +138,7 @@ Report the result, evidence, and any remaining limit.
|
|
|
137
138
|
| `AGENTS.md` | Skill subtree instructions. |
|
|
138
139
|
| `.claude/CLAUDE.md` | Claude instruction import. |
|
|
139
140
|
| `reference/run-state.md` | Goal records, task authority, follow list, and active-root registry. |
|
|
141
|
+
| `reference/completion-reporting.md` | Verified goal status and unfinished administrative work. |
|
|
140
142
|
| `reference/recovery.md` | Cold-start and compaction recovery. |
|
|
141
143
|
| `reference/oversee-delegated-work.md` | Spawner review of delegated work before it reaches the user. |
|
|
142
144
|
| `reference/platform-evidence.md` | Official Projects sources and coordinator report boundaries. |
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Report completion
|
|
2
|
+
|
|
3
|
+
Use this procedure before a completion report or an answer about whether work is done.
|
|
4
|
+
|
|
5
|
+
1. Recover the user's requested outcome and acceptance conditions. Read the evidence for that outcome before choosing its status.
|
|
6
|
+
2. Check what each unfinished item changes. Missing deliverables, required verification, necessary approvals, and stated completion prerequisites keep the affected goal open. An administrative item whose absence has no effect on the result, its use, or its acceptance keeps its own task open.
|
|
7
|
+
3. State the main outcome first. When that outcome is verified complete and only incidental recordkeeping remains, report the task as complete. A requested status notice alone does not make the main outcome incomplete. Mention the administrative item separately when useful or required by the active reporting rules.
|
|
8
|
+
4. Keep unfinished work tracked honestly. When one tracker item combines the completed outcome and unfinished administration, preserve the outcome's goal and evidence, mark its work complete, and keep a separate open administrative follow-up. Do reachable administrative work before ending. Preserve its next action or blocker when it cannot be finished. Never mark it done merely to make the report look complete.
|
|
9
|
+
5. Preserve other goals and required delivery. A complete subgoal does not close an unfinished goal or the parent run. Use the goal's acceptance conditions in the [run record](run-state.md) to decide what its remaining work affects.
|
|
10
|
+
|
|
11
|
+
An administrative action can be requested without being a condition of the main outcome's completion. Treat it as a completion prerequisite when the user states that dependency, when it carries an essential requested result, or when its absence affects the result's usability, safety, or completeness.
|
|
12
|
+
|
|
13
|
+
For example, a verified report already delivered to the user is complete while an internal progress notice remains unsent. A report still missing required checks, required approval, or its requested delivery remains incomplete.
|
|
@@ -7,7 +7,8 @@ A reviewer reads the body in place of the code. The body says what the change do
|
|
|
7
7
|
| `## What this does` | Two or three plain sentences: the behavior before, the behavior after, and who notices. Written for a reviewer who has not read the worker's conversation. |
|
|
8
8
|
| `## What changed` | One bullet per area of the diff, in plain words. |
|
|
9
9
|
| `## Existing work` | What already did part of the job, with a link or file and line, and what this change adds. Or the places searched. |
|
|
10
|
-
| `## Proof in practice` | Each command in backticks, the output without the change and with it
|
|
10
|
+
| `## Proof in practice` | Each command in backticks, and the output without the change and with it. The run on the target the change exists to fix is here before the pull request opens. |
|
|
11
|
+
| `## Not covered` | Optional. A condition the change does not target and the runs left out, such as an older data shape. |
|
|
11
12
|
| `## Eval` | Only for a `feat` title. The eval's cases and grader, the command, and its result. |
|
|
12
13
|
| `## Risk` | What could break, for whom, and how anyone would notice. `Low` and one reason for a doc or test change. |
|
|
13
14
|
| `## How to revert` | The exact undo step, and any data or state a plain revert leaves behind. |
|
|
@@ -35,13 +35,14 @@ test('reinstall drops the retired style reminder and keeps one skill reminder ho
|
|
|
35
35
|
mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/example/.claude', 'python');
|
|
36
36
|
|
|
37
37
|
const userPromptHooks = settings.hooks.UserPromptSubmit.flatMap(eachGroup => eachGroup.hooks);
|
|
38
|
-
assert.equal(userPromptHooks.length,
|
|
38
|
+
assert.equal(userPromptHooks.length, 7);
|
|
39
39
|
assert.equal(userPromptHooks[0].command, userCommand);
|
|
40
40
|
assert.match(userPromptHooks[1].command, /session\/skill_loaded_reminder\.py$/);
|
|
41
41
|
assert.match(userPromptHooks[2].command, /lifecycle\/nested_project_hooks\.py$/);
|
|
42
42
|
assert.match(userPromptHooks[3].command, /blocking\/chat_reply_gate\.py$/);
|
|
43
43
|
assert.match(userPromptHooks[4].command, /session\/tracking_nudge\.py$/);
|
|
44
44
|
assert.match(userPromptHooks[5].command, /session\/plugin_change_warning\.py$/);
|
|
45
|
+
assert.match(userPromptHooks[6].command, /blocking\/turn_review_gate\.py$/);
|
|
45
46
|
assert.ok(userPromptHooks.every(eachHook => !eachHook.command.includes('style_reminder_prompt')));
|
|
46
47
|
|
|
47
48
|
const firstMerge = JSON.stringify(settings);
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
- Keep each item's status current as the work moves: in progress when you start it, completed only after you saw the proof. A long turn updates the list at each step, so the list never lags the work.
|
|
12
12
|
- An item blocked on the user stays open, and the reply says what the user must give.
|
|
13
13
|
|
|
14
|
+
## Completion reports
|
|
15
|
+
|
|
16
|
+
Judge completion against the user's outcome and acceptance conditions. Report a verified main outcome as complete when only incidental recordkeeping remains, even when that administration was requested. Missing deliverables, required verification, approvals governing the outcome, stated completion prerequisites, and work needed for usability, safety, or completeness keep the affected goal open.
|
|
17
|
+
|
|
18
|
+
When one task combines a completed outcome and unfinished administration, preserve the goal and evidence, mark the main work complete, and track administration as a separate open follow-up. Do reachable administrative work now. Report the main outcome first and mention administration separately when useful.
|
|
19
|
+
|
|
14
20
|
## Asks ledger
|
|
15
21
|
|
|
16
22
|
When the host keeps an asks ledger with `ledger.py` and `asks_check.py`, record each ask in it too:
|
|
@@ -30,14 +30,14 @@ For a change to Claude Code behavior, such as a hook, skill, rule, setting, prom
|
|
|
30
30
|
- Read the broker report for the account it chose. Read the stream for the hook events and their output, `permission_denials`, the final result, and the token usage on the result event.
|
|
31
31
|
- When the broker exits 3 with action "wait", no account has room. Treat that as a blocked proof and report its reason and `resets_at`. Spawn Claude only through the broker.
|
|
32
32
|
|
|
33
|
-
Report it in the pull request body under "Proof in practice". List each command you ran, quote the output lines that show the effect, state the difference between the runs with and without the change,
|
|
33
|
+
Report it in the pull request body under "Proof in practice". List each command you ran, quote the output lines that show the effect, and state the difference between the runs with and without the change. The run on the target the change exists to fix belongs in this section before the pull request opens. List a condition the change does not target, such as an older data shape, under a "Not covered" heading at the same level.
|
|
34
34
|
|
|
35
35
|
When you cannot prove it, leave the pull request unopened. Tell the owner in three short sentences what you could not run and which tool would make it possible. A blocked proof is an acceptable result, because it shows where tooling is missing.
|
|
36
36
|
|
|
37
37
|
## Enforcement
|
|
38
38
|
|
|
39
|
-
`hooks/blocking/pull_request_proof.py` reads the body of each new pull request before it opens. `pr_lifecycle_skill_gate.py` runs it on `gh pr create`, the `pull_request.py create` script, and each `mcp__*__create_pull_request` tool. The gate denies the call when the body has no "Proof in practice" heading, or when the text under that heading names no command in backticks. The gate also denies a section whose only commands run a test runner, such as `pytest`, `python -m unittest`, `uv run pytest`, `npm test`, `npx vitest`, `jest`, `go test` or `cargo test`. Quotes around a runner word, interpreter flags before `-m`, and `coverage run -m unittest` are still test runs. A command is an inline backticked span or a `$ ` line inside a fenced block. A span that starts with a path and line, such as `gate.py:12`, is not a command. A span that holds one path or file name, such as `src/x.py`, is not a command unless that span runs a test runner, such as `py.test` or `pytest.exe`. A test run piped into `tee`, `tail`, `head`, `grep` or `cat` is still a test run. The deny quotes the line "Unit tests are welcome as extra evidence. The live run is the proof." and asks for a run of the changed thing the way its user runs it, without the change and with it. A section with one command that is not a test run passes. A `gh pr create` with no `--body` or `--body-file` is denied with a request for the body. Edits, merges, and pushes pass.
|
|
39
|
+
`hooks/blocking/pull_request_proof.py` reads the body of each new pull request before it opens. `pr_lifecycle_skill_gate.py` runs it on `gh pr create`, the `pull_request.py create` script, and each `mcp__*__create_pull_request` tool. The gate denies the call when the body has no "Proof in practice" heading, or when the text under that heading names no command in backticks. The gate also denies a section whose only commands run a test runner, such as `pytest`, `python -m unittest`, `uv run pytest`, `npm test`, `npx vitest`, `jest`, `go test` or `cargo test`. Quotes around a runner word, interpreter flags before `-m`, and `coverage run -m unittest` are still test runs. A command is an inline backticked span or a `$ ` line inside a fenced block. A span that starts with a path and line, such as `gate.py:12`, is not a command. A span that holds one path or file name, such as `src/x.py`, is not a command unless that span runs a test runner, such as `py.test` or `pytest.exe`. A test run piped into `tee`, `tail`, `head`, `grep` or `cat` is still a test run. The deny quotes the line "Unit tests are welcome as extra evidence. The live run is the proof." and asks for a run of the changed thing the way its user runs it, without the change and with it. The gate also denies a section whose prose says part of the change is not proven, not tested or not verified, with "yet", "been" or "fully" allowed after the "not", or says unproven, untested or unverified. The deny quotes that line, and asks for the run before the pull request opens, or for the line under a "Not covered" heading when the change does not target it. Words inside backticks or a fenced block are quoted output and do not count. A section with one command that is not a test run, and no such line, passes. A `gh pr create` with no `--body` or `--body-file` is denied with a request for the body. Edits, merges, and pushes pass.
|
|
40
40
|
|
|
41
41
|
When the checked-out branch changes a file people see (`.html`, `.htm`, `.css`, `.scss`, `.tsx`, `.jsx`, `.vue`, `.svelte`), the gate also denies a body whose proof section holds no picture: a markdown image, an image URL, or an Artifact page link that shows the screenshots. Render the change, read the screenshot, and put it in the section, so the reader sees what you looked at. When git in the session directory cannot list the branch's changed files, the gate asks for the picture as well.
|
|
42
42
|
|
|
43
|
-
The gate checks that the section is there
|
|
43
|
+
The gate checks that the section is there, names a command, and admits no unproven part. Whether the run proves the change stays a judgment for the author and the reviewer.
|
|
@@ -56,7 +56,12 @@ from hooks_constants.pull_request_proof_constants import (
|
|
|
56
56
|
FENCED_COMMAND_PROMPT,
|
|
57
57
|
INLINE_COMMAND_PATTERN,
|
|
58
58
|
TEST_ONLY_PROOF_REASON,
|
|
59
|
+
ALL_CODE_FENCE_MARKERS,
|
|
60
|
+
BLOCKQUOTE_MARKERS,
|
|
61
|
+
INLINE_CODE_SPAN_PATTERN,
|
|
59
62
|
UNITTEST_MODULE_NAME,
|
|
63
|
+
UNPROVEN_ADMISSION_PATTERN,
|
|
64
|
+
UNPROVEN_PROOF_REASON,
|
|
60
65
|
ALL_PASSTHROUGH_PROGRAM_NAMES,
|
|
61
66
|
PATH_WITH_LINE_PATTERN,
|
|
62
67
|
SINGLE_FILE_NAME_PATTERN,
|
|
@@ -332,14 +337,31 @@ def _line_commands(line: str, is_inside_fence: bool) -> list[str]:
|
|
|
332
337
|
return []
|
|
333
338
|
|
|
334
339
|
|
|
335
|
-
def
|
|
336
|
-
|
|
337
|
-
|
|
340
|
+
def _fence_marker(line: str) -> str | None:
|
|
341
|
+
fence_text = line.strip().lstrip(BLOCKQUOTE_MARKERS).strip()
|
|
342
|
+
for each_marker in ALL_CODE_FENCE_MARKERS:
|
|
343
|
+
if fence_text.startswith(each_marker) and each_marker not in fence_text[len(each_marker) :]:
|
|
344
|
+
return each_marker
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _lines_with_fence_state(section: str) -> Iterator[tuple[str, bool]]:
|
|
349
|
+
open_marker: str | None = None
|
|
338
350
|
for each_line in section.splitlines():
|
|
339
|
-
|
|
340
|
-
|
|
351
|
+
line_marker = _fence_marker(each_line)
|
|
352
|
+
if line_marker is not None and open_marker is None:
|
|
353
|
+
open_marker = line_marker
|
|
341
354
|
continue
|
|
342
|
-
|
|
355
|
+
if line_marker is not None and line_marker == open_marker:
|
|
356
|
+
open_marker = None
|
|
357
|
+
continue
|
|
358
|
+
yield each_line, open_marker is not None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _proof_commands(section: str) -> list[str]:
|
|
362
|
+
all_commands: list[str] = []
|
|
363
|
+
for each_line, each_is_inside_fence in _lines_with_fence_state(section):
|
|
364
|
+
all_commands.extend(_line_commands(each_line, each_is_inside_fence))
|
|
343
365
|
return [each_command for each_command in all_commands if _reads_as_command(each_command)]
|
|
344
366
|
|
|
345
367
|
|
|
@@ -401,6 +423,35 @@ def _proof_runs_only_tests(section: str) -> bool:
|
|
|
401
423
|
return bool(all_commands) and all(_command_runs_tests(each_command) for each_command in all_commands)
|
|
402
424
|
|
|
403
425
|
|
|
426
|
+
def _prose_lines(section: str) -> Iterator[tuple[str, str]]:
|
|
427
|
+
for each_line, each_is_inside_fence in _lines_with_fence_state(section):
|
|
428
|
+
if not each_is_inside_fence:
|
|
429
|
+
yield each_line, re.sub(INLINE_CODE_SPAN_PATTERN, "", each_line.replace(CODE_FENCE_MARKER, "`"))
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def unproven_admission(section: str) -> str | None:
|
|
433
|
+
"""Return the first prose line of a proof section that says something is not proven, or None.
|
|
434
|
+
|
|
435
|
+
::
|
|
436
|
+
|
|
437
|
+
"Not proven here: a fresh Convert run." -> "Not proven here: a fresh Convert run."
|
|
438
|
+
"The live run is untested." -> "The live run is untested."
|
|
439
|
+
"It has not been tested on the target." -> "It has not been tested on the target."
|
|
440
|
+
"`not proven` printed by the probe" -> None
|
|
441
|
+
"> Not proven here: the live run." -> "> Not proven here: the live run."
|
|
442
|
+
"Ran `python run.py`, quoted below." -> None
|
|
443
|
+
|
|
444
|
+
The markers are not proven, not tested and not verified, each with any
|
|
445
|
+
run of yet, been or fully after the not, plus unproven, untested and
|
|
446
|
+
unverified, matched without regard to case. Inline code spans and fenced
|
|
447
|
+
blocks are skipped, so output quoted there never counts.
|
|
448
|
+
"""
|
|
449
|
+
for each_line, each_prose in _prose_lines(section):
|
|
450
|
+
if re.search(UNPROVEN_ADMISSION_PATTERN, each_prose, re.IGNORECASE):
|
|
451
|
+
return each_line.strip()
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
|
|
404
455
|
def missing_proof_reason(all_payload_fields: Mapping[str, object]) -> str | None:
|
|
405
456
|
"""Return a deny reason when a new pull request's body has no proof, else None.
|
|
406
457
|
|
|
@@ -412,6 +463,7 @@ def missing_proof_reason(all_payload_fields: Mapping[str, object]) -> str | None
|
|
|
412
463
|
proof whose only command is `py.test` -> TEST_ONLY_PROOF_REASON
|
|
413
464
|
proof whose only command is `python -q -m unittest` -> TEST_ONLY_PROOF_REASON
|
|
414
465
|
proof with `pytest` and `python run.py` -> None
|
|
466
|
+
proof whose prose says "Not proven here: the run" -> UNPROVEN_PROOF_REASON
|
|
415
467
|
gh pr create --fill -> MISSING_BODY_REASON
|
|
416
468
|
gh pr edit, git push, any other tool -> None
|
|
417
469
|
|
|
@@ -425,6 +477,9 @@ def missing_proof_reason(all_payload_fields: Mapping[str, object]) -> str | None
|
|
|
425
477
|
the section is denied. Quotes around a runner word still count. Interpreter
|
|
426
478
|
flags before -m unittest still count. coverage run -m unittest counts.
|
|
427
479
|
A pipe into tee, tail, head, grep or cat keeps a test run a test run.
|
|
480
|
+
A section whose prose says part of the change is not proven, untested or
|
|
481
|
+
unverified is denied, with the line quoted; unproven_admission lists the
|
|
482
|
+
markers.
|
|
428
483
|
|
|
429
484
|
Args:
|
|
430
485
|
all_payload_fields: The parsed PreToolUse input.
|
|
@@ -437,6 +492,9 @@ def missing_proof_reason(all_payload_fields: Mapping[str, object]) -> str | None
|
|
|
437
492
|
return MISSING_PROOF_REASON
|
|
438
493
|
if _proof_runs_only_tests(section):
|
|
439
494
|
return TEST_ONLY_PROOF_REASON
|
|
495
|
+
admission = unproven_admission(section)
|
|
496
|
+
if admission is not None:
|
|
497
|
+
return UNPROVEN_PROOF_REASON.format(admission=admission)
|
|
440
498
|
return None
|
|
441
499
|
|
|
442
500
|
|
|
@@ -18,6 +18,7 @@ from hooks_constants.pull_request_proof_constants import (
|
|
|
18
18
|
MISSING_EXISTING_WORK_REASON,
|
|
19
19
|
MISSING_PROOF_REASON,
|
|
20
20
|
TEST_ONLY_PROOF_REASON,
|
|
21
|
+
UNPROVEN_PROOF_REASON,
|
|
21
22
|
UNREADABLE_CHANGES_REASON,
|
|
22
23
|
)
|
|
23
24
|
|
|
@@ -111,6 +112,92 @@ def test_proof_with_a_test_run_and_file_references_should_be_denied() -> None:
|
|
|
111
112
|
assert gate.missing_proof_reason(_mcp_create(body)) == TEST_ONLY_PROOF_REASON
|
|
112
113
|
|
|
113
114
|
|
|
115
|
+
def test_proof_that_says_its_target_run_is_not_proven_should_be_denied() -> None:
|
|
116
|
+
body = (
|
|
117
|
+
"## Proof in practice\nRan `python verify.py theme`.\n> PASS\n\n"
|
|
118
|
+
"Not proven here: a fresh Convert run of Bear Penguin that rewrites the record.\n"
|
|
119
|
+
)
|
|
120
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(
|
|
121
|
+
admission="Not proven here: a fresh Convert run of Bear Penguin that rewrites the record."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize(
|
|
126
|
+
"admission", ["unproven", "not yet tested", "Untested", "unverified", "not verified", "not been tested", "not fully verified"]
|
|
127
|
+
)
|
|
128
|
+
def test_each_unproven_marker_in_proof_prose_should_be_denied(admission: str) -> None:
|
|
129
|
+
admission_line = f"The live dialer path is {admission}."
|
|
130
|
+
body = f"## Proof in practice\nRan `python run.py`.\n{admission_line}\n"
|
|
131
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(admission=admission_line)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_unproven_line_moved_under_a_not_covered_heading_should_pass() -> None:
|
|
135
|
+
body = (
|
|
136
|
+
"## Proof in practice\nRan `python run.py`.\n> PASS\n\n"
|
|
137
|
+
"## Not covered\n\nNot proven here: a run on an older session.\n"
|
|
138
|
+
)
|
|
139
|
+
assert gate.missing_proof_reason(_mcp_create(body)) is None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_unproven_words_inside_quoted_output_should_pass() -> None:
|
|
143
|
+
body = (
|
|
144
|
+
"## Proof in practice\nRan `python run.py`, which printed `unverified sender denied`.\n"
|
|
145
|
+
"```\n$ python run.py\nstatus: not proven\n```\n"
|
|
146
|
+
)
|
|
147
|
+
assert gate.missing_proof_reason(_mcp_create(body)) is None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_unproven_admission_after_a_one_line_triple_backtick_span_should_be_denied() -> None:
|
|
151
|
+
body = (
|
|
152
|
+
"## Proof in practice\nRan `python run.py`.\n```python run.py``` -> PASS\n"
|
|
153
|
+
"Not proven here: the live run on the target.\n"
|
|
154
|
+
)
|
|
155
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(
|
|
156
|
+
admission="Not proven here: the live run on the target."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_test_run_after_a_one_line_triple_backtick_span_should_be_denied() -> None:
|
|
161
|
+
body = "## Proof in practice\n```pytest``` ok\nRan `pytest tests/`.\n"
|
|
162
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == TEST_ONLY_PROOF_REASON
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_unproven_words_in_a_fence_inside_a_blockquote_should_pass() -> None:
|
|
166
|
+
body = "## Proof in practice\nRan `python run.py`.\n> ```\n> status: not proven\n> ```\n"
|
|
167
|
+
assert gate.missing_proof_reason(_mcp_create(body)) is None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_unproven_words_in_a_tilde_fence_should_pass() -> None:
|
|
171
|
+
body = "## Proof in practice\nRan `python run.py`.\n~~~\nstatus: not proven\n~~~\n"
|
|
172
|
+
assert gate.missing_proof_reason(_mcp_create(body)) is None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_tilde_line_inside_a_backtick_fence_keeps_the_fence_open() -> None:
|
|
176
|
+
body = "## Proof in practice\nRan `python run.py`.\n```\n~~~\nstatus: not proven\n```\n"
|
|
177
|
+
assert gate.missing_proof_reason(_mcp_create(body)) is None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_admission_after_a_backtick_fence_holding_a_tilde_line_should_be_denied() -> None:
|
|
181
|
+
body = "## Proof in practice\nRan `python run.py`.\n```\n~~~\n```\nNot proven here: the live run.\n"
|
|
182
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(
|
|
183
|
+
admission="Not proven here: the live run."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_unproven_deny_quotes_the_line_as_written() -> None:
|
|
188
|
+
body = "## Proof in practice\nRan `python run.py`.\nNot proven here: the `convert.py` run on the target.\n"
|
|
189
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(
|
|
190
|
+
admission="Not proven here: the `convert.py` run on the target."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_unproven_admission_inside_a_blockquote_should_be_denied() -> None:
|
|
195
|
+
body = "## Proof in practice\nRan `python run.py`.\n> Not proven here: the live dialer run.\n"
|
|
196
|
+
assert gate.missing_proof_reason(_mcp_create(body)) == UNPROVEN_PROOF_REASON.format(
|
|
197
|
+
admission="> Not proven here: the live dialer run."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
114
201
|
def test_proof_with_a_test_run_piped_into_tee_should_be_denied() -> None:
|
|
115
202
|
body = "## Proof in practice\nRan `pytest x | tee log`.\n"
|
|
116
203
|
assert gate.missing_proof_reason(_mcp_create(body)) == TEST_ONLY_PROOF_REASON
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from turn_review_findings import finding_lines
|
|
2
|
+
|
|
3
|
+
CHANGED_PATH = "src/service.py"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_should_read_a_json_finding_on_a_changed_path() -> None:
|
|
7
|
+
review_text = (
|
|
8
|
+
'Review done.\n[{"file": "src/service.py", "line": 208, "summary": "drops the retry"},'
|
|
9
|
+
' {"file": "src/other.py", "line": 3, "summary": "unchanged file"}]'
|
|
10
|
+
)
|
|
11
|
+
assert finding_lines(review_text, [CHANGED_PATH]) == ["src/service.py:208 drops the retry"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_should_read_a_prose_finding_that_opens_with_a_changed_path() -> None:
|
|
15
|
+
review_text = (
|
|
16
|
+
"- `src/service.py:208` - drops the retry\nThe change at src/service.py:12 is fine."
|
|
17
|
+
)
|
|
18
|
+
assert finding_lines(review_text, [CHANGED_PATH]) == [
|
|
19
|
+
"- `src/service.py:208` - drops the retry"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_should_report_nothing_for_a_review_that_opens_with_none() -> None:
|
|
24
|
+
review_text = "(none)\n- `src/service.py:208` - looked at the retry, it stays"
|
|
25
|
+
assert finding_lines(review_text, [CHANGED_PATH]) == []
|