@caddis/cli 0.4.55 → 0.4.56
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/bundles/antigravity-plugin/caddis_statusline.py +7 -3
- package/bundles/antigravity-plugin/guard_agy.py +378 -37
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/repo_freshness.py +182 -4
- package/bundles/antigravity-plugin/scripts/caddis_adhoc.py +20 -12
- package/bundles/antigravity-plugin/scripts/caddis_docs_pr.py +14 -5
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +6 -1
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +58 -6
- package/bundles/antigravity-plugin/scripts/caddis_relay_git.py +8 -2
- package/bundles/antigravity-plugin/scripts/caddis_review.py +231 -167
- package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +226 -26
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +2 -1
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +89 -24
- package/bundles/antigravity-plugin/scripts/caddis_statusline.py +7 -3
- package/bundles/antigravity-plugin/scripts/caddis_tidy.py +10 -6
- package/bundles/antigravity-plugin/scripts/caddis_todo.py +7 -1
- package/bundles/antigravity-plugin/scripts/caddis_worktree_close.py +12 -5
- package/bundles/antigravity-plugin/scripts/caddis_worktree_safety.py +182 -0
- package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +2 -0
- package/bundles/antigravity-plugin/scripts/claudster_doctor.py +6 -2
- package/bundles/antigravity-plugin/scripts/no_window.py +49 -0
- package/bundles/antigravity-plugin/scripts/oss_review.py +3 -0
- package/bundles/antigravity-plugin/scripts/secret_filter.py +190 -22
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +2 -1
- package/bundles/antigravity-plugin/session_end_agy.py +14 -1
- package/bundles/antigravity-plugin/skills/cloud-phases/SKILL.md +2 -1
- package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/git-commit/SKILL.md +2 -3
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +13 -6
- package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +4 -2
- package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +28 -6
- package/bundles/antigravity-plugin/skills/roadmap/assets/review-protocol.md +16 -13
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +6 -3
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +2 -2
- package/bundles/antigravity-plugin/skills/writing-plans/SKILL.md +1 -1
- package/bundles/antigravity-plugin/warm_start_agy.py +14 -1
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/deploy-local/SKILL.md +2 -1
- package/bundles/antigravity-plugin-extras/skills/update-readme/SKILL.md +1 -1
- package/bundles/codex/.codex/skills/coding/refactoring/SKILL.md +4 -2
- package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +1 -1
- package/bundles/codex/.codex/skills/devops/git-commit/SKILL.md +2 -3
- package/bundles/codex/.codex/skills/docs/writing-plans/SKILL.md +1 -1
- package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +28 -6
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/review-protocol.md +16 -13
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Start subprocesses without a visible console window on Windows. No dependencies.
|
|
2
|
+
|
|
3
|
+
Why this exists
|
|
4
|
+
---------------
|
|
5
|
+
A process that has NO console (a Claude session started by VS Code, the `claude daemon`, a
|
|
6
|
+
background session) makes Windows open a NEW, VISIBLE console for every console program it starts
|
|
7
|
+
(git.exe, cmd, python, npx, ruff). Hooks and the status line run in exactly such sessions, so each
|
|
8
|
+
`subprocess.run(["git", ...])` flashed a window (owner report, 19 Oct).
|
|
9
|
+
|
|
10
|
+
`CREATE_NO_WINDOW` fixes it. The child gets a hidden console, and output captured through pipes
|
|
11
|
+
still works. When the parent already has a console the flag is harmless.
|
|
12
|
+
|
|
13
|
+
Never add `DETACHED_PROCESS`: Windows then ignores `CREATE_NO_WINDOW`, the child has no console,
|
|
14
|
+
and every program it starts opens its own visible window. `merge()` clears that bit.
|
|
15
|
+
|
|
16
|
+
Use
|
|
17
|
+
---
|
|
18
|
+
from no_window import run # a drop-in for subprocess.run
|
|
19
|
+
subprocess.run(argv, **NO_WINDOW_KW) # or spread the keywords yourself
|
|
20
|
+
|
|
21
|
+
On POSIX `NO_WINDOW_KW` is empty and `merge()` returns the keywords unchanged.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import subprocess
|
|
27
|
+
|
|
28
|
+
CREATE_NO_WINDOW = 0x08000000
|
|
29
|
+
DETACHED_PROCESS = 0x00000008
|
|
30
|
+
|
|
31
|
+
NO_WINDOW_KW = {"creationflags": CREATE_NO_WINDOW} if os.name == "nt" else {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def merge(kwargs: dict) -> dict:
|
|
35
|
+
"""A copy of `kwargs` with CREATE_NO_WINDOW ORed into `creationflags` (Windows only)."""
|
|
36
|
+
out = dict(kwargs)
|
|
37
|
+
if os.name == "nt":
|
|
38
|
+
out["creationflags"] = (int(out.get("creationflags") or 0) & ~DETACHED_PROCESS) | CREATE_NO_WINDOW
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run(*args, **kwargs):
|
|
43
|
+
"""subprocess.run that never opens a console window on Windows."""
|
|
44
|
+
return subprocess.run(*args, **merge(kwargs))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def popen(*args, **kwargs):
|
|
48
|
+
"""subprocess.Popen that never opens a console window on Windows."""
|
|
49
|
+
return subprocess.Popen(*args, **merge(kwargs))
|
|
@@ -608,6 +608,9 @@ def main(argv: list[str] | None = None, env: dict[str, str] | None = None) -> in
|
|
|
608
608
|
secret_filter.DEFAULT_DENY_GLOBS + cfg_deny,
|
|
609
609
|
cfg_allow + list(args.allow_path),
|
|
610
610
|
)
|
|
611
|
+
for warn_path, warn_line, warn_rule in filter_result.warnings:
|
|
612
|
+
# Context lines are already in the repo: sending them adds no exposure. Name them anyway.
|
|
613
|
+
print(f"WARN {warn_path}:{warn_line} ({warn_rule}) — already in the repo, not refused")
|
|
611
614
|
if filter_result.hits:
|
|
612
615
|
print("REVIEW: REFUSED — possible secrets:")
|
|
613
616
|
for hit_path, hit_line, hit_rule in filter_result.hits:
|
|
@@ -10,9 +10,31 @@ passes through `filter_diff()` first. It does two independent things, both fail-
|
|
|
10
10
|
paths were excluded so the drop is visible, not invisible.
|
|
11
11
|
2. **Content rules** — every hunk line of a file that was NOT dropped is scanned against
|
|
12
12
|
`SECRET_RULES` (AWS keys, GitHub/Slack tokens, JWTs, PEM private keys, `key = "..."`-shaped
|
|
13
|
-
assignments). A hit refuses the ENTIRE review (not just that file) — a
|
|
14
|
-
the path denylist is exactly the case this second layer exists for, and
|
|
15
|
-
"something here looks like a credential" is "send nothing", not "guess
|
|
13
|
+
assignments). A hit on an ADDED line refuses the ENTIRE review (not just that file) — a
|
|
14
|
+
secret slipping past the path denylist is exactly the case this second layer exists for, and
|
|
15
|
+
the safe response to "something here looks like a credential" is "send nothing", not "guess
|
|
16
|
+
which part is safe".
|
|
17
|
+
|
|
18
|
+
What counts as a refusal (the rest is a WARN, or nothing):
|
|
19
|
+
- Added line: any rule refuses.
|
|
20
|
+
- Removed line: only the high-confidence rules refuse (`HIGH_CONFIDENCE_RULES`). The two
|
|
21
|
+
assignment heuristics do not: the text is leaving the code, and they guess.
|
|
22
|
+
- Context line: a high-confidence hit refuses too (the line would be SENT to the provider,
|
|
23
|
+
and a real key must not leave the machine). A hit of an assignment heuristic only goes to
|
|
24
|
+
`FilterResult.warnings`; the caller prints a WARN line, because the line is already in the
|
|
25
|
+
repo and the heuristic guesses.
|
|
26
|
+
- `env-assignment` (an unquoted `NAME_TOKEN=value`) runs only on env-like files (`.env*`,
|
|
27
|
+
`*.env`, shell, ini, yaml, tfvars, service, Dockerfile, files with no extension, ...). In
|
|
28
|
+
code, that shape is an ordinary assignment (`tokens = _ga_tokens(text)`). The quoted
|
|
29
|
+
`assignment` rule still runs on every file.
|
|
30
|
+
- An unquoted value in an env-like file is exempt only as a shell variable or substitution
|
|
31
|
+
(`$VAR` as the whole value, `${VAR}`, `$(cmd)`, a backtick command, or `$(...)` / `${...}`
|
|
32
|
+
anywhere in the value) or as a placeholder. Code shape proves nothing there: `SECRET=YWJj==.sig`,
|
|
33
|
+
`SAS=sv=2022-11-02&sig=abc123` and `PASS=ab#cdefgh12` are secrets. A quoted value is exempt
|
|
34
|
+
only as a placeholder, or as a CSS custom-property block (`--bg:#282a36;--fg:#f8f8f2`).
|
|
35
|
+
- A trailing `# caddis: not-secret` or `// caddis: not-secret` comment exempts that line from
|
|
36
|
+
the two assignment rules only. It is ignored inside a string and never exempts a
|
|
37
|
+
high-confidence rule.
|
|
16
38
|
|
|
17
39
|
Deliberately out of scope, by design (see the plan's "Design (decided)"):
|
|
18
40
|
- Removed-line hits use old-file line numbers and are marked `removed line`.
|
|
@@ -54,6 +76,7 @@ DEFAULT_DENY_GLOBS: list[str] = [
|
|
|
54
76
|
"id_rsa*", "**/id_rsa*",
|
|
55
77
|
"secrets.*", "**/secrets.*",
|
|
56
78
|
"keys.env", "**/keys.env",
|
|
79
|
+
"*.env", # prod.env, config/app.env: a leading `*` matches at any depth
|
|
57
80
|
]
|
|
58
81
|
|
|
59
82
|
# `.env.example` / `.env.sample` / `.env.template` / `.env.dist` are template files with no
|
|
@@ -70,15 +93,111 @@ SECRET_RULES: list[tuple[str, re.Pattern]] = [
|
|
|
70
93
|
("github-token", re.compile(r"\bgh[pousr]_[A-Za-z0-9]{36,}\b")),
|
|
71
94
|
("slack-token", re.compile(r"\bxox[abprs]-[A-Za-z0-9-]{10,}\b")),
|
|
72
95
|
("jwt", re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b")),
|
|
96
|
+
("github-pat", re.compile(r"\bgithub_pat_[A-Za-z0-9_]{22,}")),
|
|
97
|
+
("aws-session-key", re.compile(r"\bASIA[0-9A-Z]{16}\b")),
|
|
98
|
+
("openai-key", re.compile(r"\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}")),
|
|
99
|
+
("anthropic-key", re.compile(r"\bsk-ant-[A-Za-z0-9_-]{20,}")),
|
|
100
|
+
("google-api-key", re.compile(r"\bAIza[0-9A-Za-z_-]{35}")),
|
|
101
|
+
("npm-token", re.compile(r"\bnpm_[A-Za-z0-9]{36}\b")),
|
|
102
|
+
("slack-webhook", re.compile(r"https://hooks\.slack\.com/services/[A-Za-z0-9_/]+")),
|
|
103
|
+
# user:password@host in a URL. `value` is the password; placeholders are exempted in code.
|
|
104
|
+
("url-credentials", re.compile(
|
|
105
|
+
r"[a-z][a-z0-9+.-]*://[^/\s:@]*:(?P<value>[^/\s@]{3,})@", re.I)),
|
|
106
|
+
# `name = "v"`, `name: "v"`, and a quoted key: `"name": "v"`, `'name': 'v'`.
|
|
73
107
|
("assignment", re.compile(
|
|
74
|
-
r"(?i)[A-Za-z0-9_]*(password|passwd|secret|api[_-]?key|token)[A-Za-z0-9_]
|
|
108
|
+
r"(?i)[A-Za-z0-9_]*(password|passwd|secret|api[_-]?key|token)[A-Za-z0-9_]*['\"]?\s*[:=]\s*['\"](?P<value>[^'\"\s]{8,})['\"]"
|
|
75
109
|
)),
|
|
76
110
|
("env-assignment", re.compile(
|
|
77
111
|
r"^\s*(?:export\s+)?[A-Za-z0-9_]*(?:PASSWORD|PASSWD|SECRET|API_KEY|APIKEY|TOKEN|PRIVATE_KEY)"
|
|
78
|
-
r"[A-Za-z0-9_]*\s*=\s*(?P<value>[^\s'\"
|
|
112
|
+
r"[A-Za-z0-9_]*\s*=\s*(?P<value>[^\s'\"]{8,})", re.I
|
|
79
113
|
)),
|
|
80
114
|
]
|
|
81
115
|
|
|
116
|
+
# Rules that match a fixed credential format. These refuse on added, removed AND context lines
|
|
117
|
+
# and ignore the `caddis: not-secret` marker. The two assignment rules are heuristics and get none
|
|
118
|
+
# of that.
|
|
119
|
+
ASSIGNMENT_RULES = frozenset({"assignment", "env-assignment"})
|
|
120
|
+
HIGH_CONFIDENCE_RULES = frozenset({
|
|
121
|
+
"private-key", "aws-key", "github-token", "slack-token", "jwt",
|
|
122
|
+
"github-pat", "aws-session-key", "openai-key", "anthropic-key", "google-api-key",
|
|
123
|
+
"npm-token", "slack-webhook", "url-credentials",
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
# A trailing comment only: `# caddis: not-secret` or `// caddis: not-secret` at the end of the
|
|
127
|
+
# line, and not inside a string (see `_has_not_secret_marker`).
|
|
128
|
+
_TRAILING_MARKER = re.compile(r"(?:#|//)\s*caddis:\s*not-secret\s*$", re.I)
|
|
129
|
+
|
|
130
|
+
# `env-assignment` runs only on these files (matched on the file name, not the directory).
|
|
131
|
+
_ENV_LIKE_SUFFIXES = frozenset({
|
|
132
|
+
".sh", ".bash", ".zsh", ".ps1", ".psm1", ".bat", ".cmd", ".ini", ".cfg", ".conf", ".toml",
|
|
133
|
+
".yaml", ".yml", ".properties", ".env", ".tfvars", ".service",
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _is_env_like(path: str) -> bool:
|
|
138
|
+
name = path.replace("\\", "/").rsplit("/", 1)[-1].lower()
|
|
139
|
+
if name.startswith((".env", "dockerfile")):
|
|
140
|
+
return True
|
|
141
|
+
suffix = Path(name).suffix
|
|
142
|
+
return suffix == "" or suffix in _ENV_LIKE_SUFFIXES
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _inside_string(prefix: str) -> bool:
|
|
146
|
+
"""True when `prefix` ends inside an open '...' or "..." string."""
|
|
147
|
+
quote = None
|
|
148
|
+
i = 0
|
|
149
|
+
while i < len(prefix):
|
|
150
|
+
c = prefix[i]
|
|
151
|
+
if quote:
|
|
152
|
+
if c == "\\":
|
|
153
|
+
i += 2
|
|
154
|
+
continue
|
|
155
|
+
if c == quote:
|
|
156
|
+
quote = None
|
|
157
|
+
elif c in "'\"":
|
|
158
|
+
quote = c
|
|
159
|
+
i += 1
|
|
160
|
+
return quote is not None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _has_not_secret_marker(content: str) -> bool:
|
|
164
|
+
match = _TRAILING_MARKER.search(content)
|
|
165
|
+
return bool(match) and not _inside_string(content[:match.start()])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# `--bg:#282a36;--fg:#f8f8f2`: a CSS custom-property block held in a quoted string.
|
|
169
|
+
_CSS_DECLARATIONS = re.compile(r"(?:--[A-Za-z0-9_-]+:[^;\s]+;?)+")
|
|
170
|
+
# $(cmd) `cmd` ${VAR}. A bare `$VAR` is exempt only as the WHOLE value, through
|
|
171
|
+
# `_is_placeholder`: a value that merely starts with `$` (`$ecretP@ss`, `$argon2id$...`) is a secret.
|
|
172
|
+
_SHELL_START = re.compile(r"\$\(|`|\$\{")
|
|
173
|
+
_SHELL_SUBSTITUTION = re.compile(r"\$\([^)]*\)|\$\{[^}]*\}") # $(...) or ${...} anywhere
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _is_not_secret_value(value: str, *, quoted: bool) -> bool:
|
|
177
|
+
"""True when the value is a CSS block (quoted) or a shell variable or substitution (unquoted).
|
|
178
|
+
|
|
179
|
+
A QUOTED value is a string literal, so code shape proves nothing: `"Sec;ret(1)"` and
|
|
180
|
+
`"P@ssw0rd#2026!"` are passwords. Only a CSS custom-property block is exempt (the
|
|
181
|
+
placeholder check happens in the caller).
|
|
182
|
+
|
|
183
|
+
An UNQUOTED value reaches this rule only in an env-like file, where values are not code:
|
|
184
|
+
`SECRET=YWJj==.sig`, `SAS=sv=2022-11-02&sig=abc123`, `PASS=ab#cdefgh12` and
|
|
185
|
+
`KEY=correct_horse` are secrets. Only a shell reference is exempt: a value that starts with
|
|
186
|
+
`$(`, a backtick or `${`, or holds `$(...)` / `${...}` in any position. A bare `$name` is
|
|
187
|
+
exempt only as the whole value (a placeholder); `$argon2id$v=19$...` is a hash.
|
|
188
|
+
"""
|
|
189
|
+
if quoted:
|
|
190
|
+
return bool(_CSS_DECLARATIONS.fullmatch(value))
|
|
191
|
+
return bool(_SHELL_START.match(value) or _SHELL_SUBSTITUTION.search(value))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
_URL_PASSWORD_PLACEHOLDERS = frozenset({"password", "pass", "passwd", "pwd", "secret", "token"})
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _is_url_password_placeholder(value: str) -> bool:
|
|
198
|
+
return (_is_placeholder(value) or value.lower() in _URL_PASSWORD_PLACEHOLDERS
|
|
199
|
+
or bool(re.fullmatch(r"\{[^}]*\}|%s|%\([^)]*\)s", value)))
|
|
200
|
+
|
|
82
201
|
|
|
83
202
|
def _is_placeholder(value: str) -> bool:
|
|
84
203
|
"""Exclude obvious template values from both assignment rules."""
|
|
@@ -99,6 +218,8 @@ class FilterResult:
|
|
|
99
218
|
kept_diff: str
|
|
100
219
|
dropped_paths: list[str] = field(default_factory=list)
|
|
101
220
|
hits: list[tuple[str, int, str]] = field(default_factory=list) # (path, side's line, rule/removed marker)
|
|
221
|
+
# Context-line matches: already in the repo, so reported (WARN) but never a refusal.
|
|
222
|
+
warnings: list[tuple[str, int, str]] = field(default_factory=list)
|
|
102
223
|
|
|
103
224
|
|
|
104
225
|
# A new file block starts at a line beginning "diff --git a/..." (the common, unquoted case)
|
|
@@ -213,9 +334,38 @@ def _is_denied(path: str, deny_globs: list[str], allow_globs: list[str]) -> bool
|
|
|
213
334
|
return not any(fnmatch.fnmatchcase(path, glob) for glob in allow_globs)
|
|
214
335
|
|
|
215
336
|
|
|
216
|
-
def
|
|
217
|
-
"""
|
|
337
|
+
def _line_matches(content: str, env_like: bool) -> list[str]:
|
|
338
|
+
"""Names of the rules that match one line of content, after every exemption.
|
|
339
|
+
|
|
340
|
+
Every match of a rule is checked, so a real secret that follows a placeholder on the same
|
|
341
|
+
line (a JSON object with a placeholder key first and a real one second) is still caught.
|
|
342
|
+
"""
|
|
343
|
+
marked = _has_not_secret_marker(content)
|
|
344
|
+
names: list[str] = []
|
|
345
|
+
for rule_name, pattern in SECRET_RULES:
|
|
346
|
+
if rule_name in ASSIGNMENT_RULES and marked:
|
|
347
|
+
continue
|
|
348
|
+
if rule_name == "env-assignment" and not env_like:
|
|
349
|
+
continue
|
|
350
|
+
for match in pattern.finditer(content):
|
|
351
|
+
if rule_name in ASSIGNMENT_RULES:
|
|
352
|
+
value = match.group("value")
|
|
353
|
+
if _is_placeholder(value) or _is_not_secret_value(
|
|
354
|
+
value, quoted=rule_name == "assignment"):
|
|
355
|
+
continue
|
|
356
|
+
elif rule_name == "url-credentials" and _is_url_password_placeholder(
|
|
357
|
+
match.group("value")):
|
|
358
|
+
continue
|
|
359
|
+
names.append(rule_name)
|
|
360
|
+
break
|
|
361
|
+
return names
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _scan_block(block: str, path: str, env_like: bool) -> tuple[list[tuple[str, int, str]],
|
|
365
|
+
list[tuple[str, int, str]]]:
|
|
366
|
+
"""Scan all hunk lines; return (hits, warnings). Removed hits use old-file line numbers."""
|
|
218
367
|
hits: list[tuple[str, int, str]] = []
|
|
368
|
+
warnings: list[tuple[str, int, str]] = []
|
|
219
369
|
new_line: int | None = None
|
|
220
370
|
old_line: int | None = None
|
|
221
371
|
for line in block.splitlines():
|
|
@@ -231,16 +381,18 @@ def _scan_added_lines(block: str, path: str) -> list[tuple[str, int, str]]:
|
|
|
231
381
|
content = line[1:]
|
|
232
382
|
removed = line.startswith("-")
|
|
233
383
|
context = line.startswith(" ")
|
|
234
|
-
for rule_name
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
384
|
+
for rule_name in _line_matches(content, env_like):
|
|
385
|
+
if context:
|
|
386
|
+
# A real key on a context line is still SENT to the provider.
|
|
387
|
+
if rule_name in HIGH_CONFIDENCE_RULES:
|
|
388
|
+
hits.append((path, new_line, f"{rule_name}, context line"))
|
|
389
|
+
else:
|
|
390
|
+
warnings.append((path, new_line, f"{rule_name}, context line"))
|
|
391
|
+
elif removed:
|
|
392
|
+
if rule_name in HIGH_CONFIDENCE_RULES:
|
|
393
|
+
hits.append((path, old_line, f"{rule_name}, removed line"))
|
|
394
|
+
else:
|
|
395
|
+
hits.append((path, new_line, rule_name))
|
|
244
396
|
if removed:
|
|
245
397
|
old_line += 1
|
|
246
398
|
elif not context:
|
|
@@ -251,13 +403,25 @@ def _scan_added_lines(block: str, path: str) -> list[tuple[str, int, str]]:
|
|
|
251
403
|
else:
|
|
252
404
|
old_line += 1
|
|
253
405
|
new_line += 1 # context line exists in both files
|
|
254
|
-
return hits
|
|
406
|
+
return hits, warnings
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _scan_added_lines(block: str, path: str,
|
|
410
|
+
env_assignment_everywhere: bool = False) -> list[tuple[str, int, str]]:
|
|
411
|
+
"""The refusing hits of `_scan_block` (kept for callers that want only those)."""
|
|
412
|
+
return _scan_block(block, path, env_assignment_everywhere or _is_env_like(path))[0]
|
|
255
413
|
|
|
256
414
|
|
|
257
|
-
def filter_diff(diff_text: str, deny_globs: list[str], allow_globs: list[str]
|
|
258
|
-
|
|
415
|
+
def filter_diff(diff_text: str, deny_globs: list[str], allow_globs: list[str],
|
|
416
|
+
env_assignment_everywhere: bool = False) -> FilterResult:
|
|
417
|
+
"""Drop denylisted-path file blocks, then scan the hunk lines in kept blocks.
|
|
418
|
+
|
|
419
|
+
`env_assignment_everywhere` runs `env-assignment` on every file type. Use it for text that
|
|
420
|
+
is not a diff of real files (the relay note is prose in a synthetic `.md` file).
|
|
421
|
+
"""
|
|
259
422
|
dropped_paths: list[str] = []
|
|
260
423
|
hits: list[tuple[str, int, str]] = []
|
|
424
|
+
warnings: list[tuple[str, int, str]] = []
|
|
261
425
|
kept_parts: list[str] = []
|
|
262
426
|
for block in _split_into_file_blocks(diff_text):
|
|
263
427
|
first_line = block.splitlines()[0] if block else ""
|
|
@@ -268,9 +432,13 @@ def filter_diff(diff_text: str, deny_globs: list[str], allow_globs: list[str]) -
|
|
|
268
432
|
if denied:
|
|
269
433
|
dropped_paths.append(denied)
|
|
270
434
|
continue
|
|
271
|
-
|
|
435
|
+
block_hits, block_warnings = _scan_block(
|
|
436
|
+
block, path, env_assignment_everywhere or _is_env_like(path))
|
|
437
|
+
hits.extend(block_hits)
|
|
438
|
+
warnings.extend(block_warnings)
|
|
272
439
|
kept_parts.append(block)
|
|
273
|
-
return FilterResult(kept_diff="".join(kept_parts), dropped_paths=dropped_paths, hits=hits
|
|
440
|
+
return FilterResult(kept_diff="".join(kept_parts), dropped_paths=dropped_paths, hits=hits,
|
|
441
|
+
warnings=warnings)
|
|
274
442
|
|
|
275
443
|
|
|
276
444
|
def load_config(repo: Path) -> tuple[list[str], list[str]]:
|
|
@@ -369,7 +369,8 @@ def check_venv(target: Path, install: bool, dry: bool) -> list[str]:
|
|
|
369
369
|
notes.append("venv: .venv present — ok")
|
|
370
370
|
elif install and not dry:
|
|
371
371
|
import subprocess
|
|
372
|
-
subprocess.run([sys.executable, "-m", "venv", str(venv)], check=False
|
|
372
|
+
subprocess.run([sys.executable, "-m", "venv", str(venv)], check=False,
|
|
373
|
+
creationflags=0x08000000 if os.name == "nt" else 0)
|
|
373
374
|
notes.append("venv: created .venv (install dev deps: .venv/Scripts/pip install -e .[dev])")
|
|
374
375
|
else:
|
|
375
376
|
notes.append("venv: MISSING — create with `python -m venv .venv` then "
|
|
@@ -23,6 +23,19 @@ import subprocess
|
|
|
23
23
|
import sys
|
|
24
24
|
from datetime import datetime, timezone
|
|
25
25
|
|
|
26
|
+
# No child process of this hook opens a console window on Windows (scripts/no_window.py). agy hooks ship
|
|
27
|
+
# FLAT at the plugin root with scripts/ beside them; in the source repo scripts/ is one level up.
|
|
28
|
+
try:
|
|
29
|
+
_nw_here = os.path.dirname(os.path.abspath(__file__))
|
|
30
|
+
for _nw_cand in (os.path.join(_nw_here, "scripts"), os.path.join(os.path.dirname(_nw_here), "scripts")):
|
|
31
|
+
if os.path.isfile(os.path.join(_nw_cand, "no_window.py")):
|
|
32
|
+
if _nw_cand not in sys.path:
|
|
33
|
+
sys.path.insert(0, _nw_cand)
|
|
34
|
+
break
|
|
35
|
+
from no_window import NO_WINDOW_KW # noqa: E402
|
|
36
|
+
except Exception: # pragma: no cover - defensive
|
|
37
|
+
NO_WINDOW_KW = {"creationflags": 0x08000000} if os.name == "nt" else {}
|
|
38
|
+
|
|
26
39
|
|
|
27
40
|
def _artifact_root(root: str) -> str:
|
|
28
41
|
return os.path.join(str(root), ".caddis")
|
|
@@ -39,7 +52,7 @@ def _repo_root(path: str):
|
|
|
39
52
|
try:
|
|
40
53
|
done = subprocess.run(
|
|
41
54
|
["git", "-C", str(path), "rev-parse", "--show-toplevel"],
|
|
42
|
-
capture_output=True, text=True, timeout=5,
|
|
55
|
+
capture_output=True, text=True, timeout=5, **NO_WINDOW_KW,
|
|
43
56
|
)
|
|
44
57
|
top = done.stdout.strip()
|
|
45
58
|
return os.path.normpath(top) if done.returncode == 0 and top else None
|
|
@@ -117,7 +117,8 @@ internal host names, no machine paths and no credentials.
|
|
|
117
117
|
|
|
118
118
|
```
|
|
119
119
|
git add <each prompt file by name>
|
|
120
|
-
git
|
|
120
|
+
git diff --cached --name-only # must list only the prompt files; else stop and report
|
|
121
|
+
git commit -m "docs(prompts): cloud prompts for <feature> phases <N, M>" -- <each prompt file>
|
|
121
122
|
git push -u origin <feature branch>
|
|
122
123
|
git rev-parse HEAD
|
|
123
124
|
git ls-remote origin refs/heads/<feature branch>
|
|
@@ -168,7 +168,7 @@ gh alias set review 'pr list --search "review-requested:@me"'
|
|
|
168
168
|
```bash
|
|
169
169
|
gh issue develop 42 --branch fix/issue-42
|
|
170
170
|
git add src/auth/login.py tests/test_login.py # named files only, never `git add .`
|
|
171
|
-
git commit -m "fix: resolve login bug" && git push -u origin HEAD
|
|
171
|
+
git commit -m "fix: resolve login bug" -- src/auth/login.py tests/test_login.py && git push -u origin HEAD
|
|
172
172
|
gh pr create --title "fix: resolve login bug" --body "Closes #42"
|
|
173
173
|
gh pr checks --watch
|
|
174
174
|
gh pr merge --squash --delete-branch
|
|
@@ -87,9 +87,8 @@ Optional area of change:
|
|
|
87
87
|
# Stage specific files
|
|
88
88
|
git add path/to/file1 path/to/file2
|
|
89
89
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
git add src/components/*
|
|
90
|
+
# Never stage by pattern or folder (`*.test.*`, `src/components/`): a pattern also takes
|
|
91
|
+
# files you did not change, and the caddis guard denies it. Name each file.
|
|
93
92
|
```
|
|
94
93
|
|
|
95
94
|
**Never stage secrets** (`.env`, `credentials.json`, private keys).
|
|
@@ -46,7 +46,9 @@ depend on them:
|
|
|
46
46
|
the project's test command anywhere (config, CI, package scripts). The runner treats any such edit as
|
|
47
47
|
tampering and fails the run. If the plan asks you to touch these, skip that step and note it in the
|
|
48
48
|
review file instead.
|
|
49
|
-
- **Commit per phase, on this branch, with a normal commit.** Use `git add <paths
|
|
49
|
+
- **Commit per phase, on this branch, with a normal commit.** Use `git add <paths>`, then check `git diff --cached --name-only` lists only those paths (refuse on any
|
|
50
|
+
other staged path: a peer staged it; note it in the review file), then `git commit -m "…" -- <paths>`.
|
|
51
|
+
Never `git add -A`, `.`, `-u` or a folder, and never `git commit -a` (the guard denies them).
|
|
50
52
|
Do not use `--no-verify` (a pre-commit hook guards the branch — let it run). Do not amend or rebase
|
|
51
53
|
prior commits. One phase → one (or more) commit(s); never one giant end-of-run commit.
|
|
52
54
|
- **Never ask a question, never pause, never wait for input.** No human will answer. Never use
|
|
@@ -164,8 +166,11 @@ row, visible to any later reader without anyone having to catch it live.
|
|
|
164
166
|
so it proves timing by design.
|
|
165
167
|
3. **ACCEPT** means run `caddis_lanes.py accept --plan <plan> --phase N --attempt K --message
|
|
166
168
|
"<the phase's commit message>"`. It runs only on the programme branch, never on `main`. It
|
|
167
|
-
refuses a `BLOCKING` review
|
|
168
|
-
|
|
169
|
+
refuses a `BLOCKING` review and a `MISSING` review (no verdict: provider down, 429, filter
|
|
170
|
+
refusal). If you checked the finding and it is false, add
|
|
171
|
+
`--override-review "<why the finding is false>"`. For a `MISSING` review, re-run the review
|
|
172
|
+
first; add the override only if the owner accepts a phase with no vendor verdict. The reason
|
|
173
|
+
must not be empty. The script records it in the packet.
|
|
169
174
|
4. Allow at most **2 attempts** per phase on a non-Claude lane. An out-of-budget switch is not an
|
|
170
175
|
attempt. The owner can request TAKEOVER at any time.
|
|
171
176
|
5. A UI phase needs a `ui-design-reviewer` screenshot review before ACCEPT. This rule applies
|
|
@@ -234,7 +239,8 @@ report — do not redo completed work.
|
|
|
234
239
|
- **REFACTOR** — clean names/structure/duplication while keeping green.
|
|
235
240
|
- **VERIFY** — run the phase's exit-gate check (the literal command the plan names) and the relevant
|
|
236
241
|
suite so you didn't regress. Do not claim a state you did not run.
|
|
237
|
-
- **COMMIT** — `git add` the phase's files
|
|
242
|
+
- **COMMIT** — `git add` the phase's files by name, check `git diff --cached --name-only` shows only
|
|
243
|
+
them, and `git commit -m "…" -- <files>` with the phase's conventional-commit message
|
|
238
244
|
(from the plan, or a faithful equivalent). Stay on the current branch.
|
|
239
245
|
- **UPDATE THE TRACKER** — edit the plan's `## Tracker` row for this phase: set Status to `done`, fill
|
|
240
246
|
the short commit hash (`git rev-parse --short HEAD`), set the `Lane` column to the lane this phase
|
|
@@ -303,8 +309,9 @@ for a later session to clean up, and nothing is left uncommitted. Order matters:
|
|
|
303
309
|
no other file. **Refuse** if it would touch anything else, or prints `ERROR` or `NOT MOVED`:
|
|
304
310
|
stop, do not commit the move, and report the output in the review file.
|
|
305
311
|
3. `git add` the moved plan by name (`.caddis/plans/done/<plan>.md`, so the status and Tracker edits
|
|
306
|
-
are staged with the rename), then make the final commit
|
|
307
|
-
|
|
312
|
+
are staged with the rename), then make the final commit with
|
|
313
|
+
`git commit -m "…" -- .caddis/plans/<plan>.md .caddis/plans/done/<plan>.md` (both sides of the
|
|
314
|
+
rename). The move, the status and the last Tracker row land in one commit.
|
|
308
315
|
|
|
309
316
|
(Falls back to `scripts/caddis_tidy.py` from a source checkout; **skip this step if the script is
|
|
310
317
|
missing** — degrades open, same as the gates in Step 1.) A **collision** (`done/<name>` already
|
|
@@ -168,7 +168,7 @@ Cleanup: remote branch deleted / local branch deleted / backup ref dropped
|
|
|
168
168
|
|
|
169
169
|
- Never merge a PR whose required checks are red or absent.
|
|
170
170
|
- Never delete a feature branch before its merge is confirmed and any deploy validated.
|
|
171
|
-
- Never `git add -A`, `git add .` or `git add <dir>`, even after reading `git status`: stage named files only (the guard denies
|
|
171
|
+
- Never `git add -A`, `git add .` or `git add <dir>`, even after reading `git status`: stage named files only (the guard denies all three, and `git add -u` and `git commit -a`). Commit with `git commit -m "..." -- <files>`, after `git diff --cached --name-only` shows only those files.
|
|
172
172
|
- Never force-push without a `backup/*-preship` recovery ref; force pushes are `--force-with-lease` only.
|
|
173
173
|
- Never edit a workflow file (`.gitea/workflows/`, `.github/workflows/`) to make a gate pass — fix the source.
|
|
174
174
|
- Local-only lane: refuse + point at `/ship`. On the default branch: stop + point at `/ship`.
|
|
@@ -128,7 +128,8 @@ Activate when:
|
|
|
128
128
|
```bash
|
|
129
129
|
git status --short # stage the files you changed, by name
|
|
130
130
|
git add tests/test_<module>.py
|
|
131
|
-
git
|
|
131
|
+
git diff --cached --name-only # only the files you named
|
|
132
|
+
git commit -m "chore: add tests before refactoring" -- tests/test_<module>.py
|
|
132
133
|
```
|
|
133
134
|
|
|
134
135
|
---
|
|
@@ -387,7 +388,8 @@ DEPRECATED_VALUE = 5 # Not referenced
|
|
|
387
388
|
|
|
388
389
|
2. **If tests pass, commit**
|
|
389
390
|
```bash
|
|
390
|
-
git
|
|
391
|
+
git add <files you changed>
|
|
392
|
+
git commit -m "refactor: extract validation to separate method" -- <files you changed>
|
|
391
393
|
```
|
|
392
394
|
|
|
393
395
|
3. **If tests fail, revert**
|
|
@@ -89,8 +89,26 @@ python scripts/caddis_roadmap.py recheck --roadmap <slug>
|
|
|
89
89
|
`Depends on` milestone `done` or `not-needed`, and no open blocker other than `data-condition` (exit 1 names it),
|
|
90
90
|
then runs the Exit check (300 s): pass -> `done`; fail with a `data-condition` blocker -> `code-done/data-pending`;
|
|
91
91
|
fail otherwise -> an `external` blocker
|
|
92
|
-
`exit check failing (exit <n>)
|
|
93
|
-
|
|
92
|
+
`exit check failing (exit <n>): <tail>` (the last 3 non-empty output lines on one line, the whole reason at most
|
|
93
|
+
200 characters, masked as below). The part before `: ` is the blocker's identity: a later rollup adds no second
|
|
94
|
+
row, and `unblock --reason "exit check failing (exit <n>)"` removes every row of that prefix. A check that fails
|
|
95
|
+
on a missing environment (exit 9009/127/126, or output in its last 40 lines such as `No module named`,
|
|
96
|
+
`Cannot find module`, `ENOENT`, `run npm run build first`) is exit 3: no blocker, no PR. Either way the last 64 KB
|
|
97
|
+
of output (masked the same way) go to `.local/exit-check-<id>-<utc>.log` (the newest 10 stay), and rollup prints
|
|
98
|
+
the path. A milestone with no phase refs and no Exit check is refused (exit 4).
|
|
99
|
+
**Masked** in the reason and the log: the values of secret-named variables (`caddis_redact`); a whole
|
|
100
|
+
`-----BEGIN ... PRIVATE KEY-----` block up to its END line (or the end of the output); AWS, GitHub, Slack and JWT
|
|
101
|
+
tokens; a quoted value after a `password`/`secret`/`api_key`/`token` name, spaces included; an unquoted
|
|
102
|
+
`*PASSWORD*=`/`*TOKEN*=`-style line value; URL credentials (`scheme://***@`); and absolute paths as `<path>`: a
|
|
103
|
+
drive or UNC path up to the end of the line, a quote, `<`, `>` or `|`, and a `/home/`, `/Users/`, `/tmp/`, `/root/`,
|
|
104
|
+
`/var/`, `/etc/` or `/opt/` path up to the end of the line or a quote. Other text is published as printed.
|
|
105
|
+
- **`**Exit setup:** <command>`** (optional, one per roadmap, beside `Runner host:`; no `|`) runs in the temp worktree
|
|
106
|
+
before every Exit check (900 s). It must install fresh into that worktree, for example
|
|
107
|
+
`python -m venv .venv && .venv\Scripts\pip install -e .[dev]` or `npm ci && npm run build`. Each command runs in a
|
|
108
|
+
new shell, so the Exit check must call the venv itself: `.venv\Scripts\python -m pytest tests -q`, not
|
|
109
|
+
`python -m pytest`. Never link or junction a shared venv or `node_modules`: the worktree is removed with `--force`
|
|
110
|
+
after the check. A setup that fails or times out is exit 3 and writes nothing; the runner stops
|
|
111
|
+
`BLOCKED rollup not run` and the owner decides (the stop names a docs PR that was opened but not merged).
|
|
94
112
|
- **Owner-only changes:** `set` Status `not-needed`, any `set` Owner, `unblock` of an `owner-gate`, and `block --check`
|
|
95
113
|
(recheck runs that command unattended later). They need `--by-owner` and are refused headless: `CADDIS_HEADLESS`
|
|
96
114
|
or `CLAUDECODE` in the environment (even empty), stdin not a terminal, or on
|
|
@@ -179,8 +197,8 @@ Stops (each one notifies and releases the lock): a STOP file (`~/.caddis/STOP` o
|
|
|
179
197
|
`caddis_spend.py` still counts across runs), a lock or batch claim that cannot be written
|
|
180
198
|
(`BLOCKED claim not written`, before any session), the agy spend cap refusing a reservation, three sessions
|
|
181
199
|
with no newly merged phase, a roadmap write that fails, and the **snapshot guard**: one baseline for the whole run.
|
|
182
|
-
The Milestones and Blockers tables (every cell of every row, rows added, removed or moved)
|
|
183
|
-
are frozen. Every origin read (each `next`, after the runner's own writes, after each session) is compared with the
|
|
200
|
+
The Milestones and Blockers tables (every cell of every row, rows added, removed or moved), the `Runner host` line
|
|
201
|
+
and the `**Exit setup:**` line are frozen. Every origin read (each `next`, after the runner's own writes, after each session) is compared with the
|
|
184
202
|
previous one; the only differences allowed are the exact edits the runner itself made and recorded for that read: a
|
|
185
203
|
Status it set, a blocker row it added, and what a rollup's audit entry (`changes.jsonl`) says it actually wrote.
|
|
186
204
|
Anything else stops the run `FAILED roadmap changed`.
|
|
@@ -206,7 +224,10 @@ refuses a plan that a roadmap lock names, at start and before every session. `-D
|
|
|
206
224
|
the session command, the worktree and any pending rollup, skips the host check, writes no files and starts nothing; it
|
|
207
225
|
does run `git fetch`, which updates the remote-tracking refs. `-GhAccount <login>` (recorded in the launch options,
|
|
208
226
|
passed on by the recheck task and in the printed task) makes every gh call of the runner and of `--last-stop` use
|
|
209
|
-
GH_TOKEN from `gh auth token -u <login>`; on a machine with several gh logins, give the repo's account.
|
|
227
|
+
GH_TOKEN from `gh auth token -u <login>`; on a machine with several gh logins, give the repo's account. The sessions
|
|
228
|
+
never get that token, nor GH_TOKEN, GITHUB_TOKEN, GH_ENTERPRISE_TOKEN, NPM_TOKEN or NODE_AUTH_TOKEN from the
|
|
229
|
+
environment; with CADDIS_HEADLESS=1 the guard denies `git push`, `gh pr merge|create`, `gh release`, a write through
|
|
230
|
+
`gh api`, `gh workflow run` and package publishes. Only the runner publishes. A gh call that
|
|
210
231
|
cannot answer (gh missing, 401, 404) is an error, never "not merged": the run stops
|
|
211
232
|
`BLOCKED gh cannot see <repo> with account <login>`, notified and recorded.
|
|
212
233
|
|
|
@@ -223,7 +244,8 @@ phases) holds the whole plan for every other roadmap and for plan mode. At the t
|
|
|
223
244
|
every milestone that waits only for its rollup (Status not finished, every phase merged, and the same rule as
|
|
224
245
|
`next`: no blocker, dependencies finished, Owner the runner); `--last-stop` says `rollup-ready:` for such a milestone,
|
|
225
246
|
so recheck restarts the runner. A sweep rollup that writes nothing stops `BLOCKED rollup of <id> wrote nothing`
|
|
226
|
-
(recorded, not restartable) and is not retried or reported until the roadmap changes.
|
|
247
|
+
(recorded, not restartable) and is not retried or reported until the roadmap changes. A rollup that exits 3 (its
|
|
248
|
+
exit check or Exit setup could not run) stops `BLOCKED rollup not run: <id> ...` (not restartable; the owner decides). The dry run lists them first. If
|
|
227
249
|
the owner edits the roadmap during a merge wait, the restart stops `FAILED roadmap changed ...` and names the
|
|
228
250
|
recovery: `roadmap-run.ps1 -Roadmap <slug> -AcceptRoadmap` (owner only, refused headless) takes the runner lock (a live
|
|
229
251
|
runner refuses it), prints the diff from the published baseline, asks you to type `accept` (or `--yes`), and accepts
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
type: reference
|
|
3
3
|
title: Review protocol
|
|
4
|
-
description: The review method one model can rely on - invariants, inputs and writers, shared state,
|
|
4
|
+
description: The review method one model can rely on - invariants, inputs and writers, shared state, check what the change promises, tests, docs, then severity (only blocking stops the merge; each finding needs a reproduction), one FINDING line per finding and an END REVIEW line. caddis_review.py sends it to every review run.
|
|
5
5
|
status: current
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Review protocol (generic, any diff)
|
|
9
9
|
|
|
10
|
-
You review a diff. You
|
|
11
|
-
|
|
12
|
-
first finding. Include a short section per step, in order.
|
|
10
|
+
You review a diff. You read only: you have Read, Grep and Glob and no shell, so you run no test and no command, and
|
|
11
|
+
you do not edit, create or delete files. The diff is in the prompt. Tests are the implementer's and CI's job. Work
|
|
12
|
+
through ALL steps in order. Do not stop at the first finding. Include a short section per step, in order.
|
|
13
13
|
|
|
14
14
|
## Step 1 - Purpose and invariants
|
|
15
15
|
Read the card, the plan or design section it cites, and the diff. Write the list of INVARIANTS this change must keep:
|
|
@@ -26,12 +26,14 @@ accident or on purpose? This includes the agent or session the code is meant to
|
|
|
26
26
|
List state that two runs, two kinds of target, or two processes could share (files, locks, relays, logs, caches).
|
|
27
27
|
Can they collide? Can one read the other's output as its own?
|
|
28
28
|
|
|
29
|
-
## Step 4 -
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
and
|
|
33
|
-
with a real payload). Compare risky behaviour with the
|
|
34
|
-
something. Report any that fail or that cannot work as
|
|
29
|
+
## Step 4 - Check what the change promises
|
|
30
|
+
For every command that the plan, its exit gates, the docs or the skill text say should work for this change (the
|
|
31
|
+
documented usage lines, exactly as written), follow it through the code by reading: the argument parsing, the paths it
|
|
32
|
+
reads and writes, the repo's OWN fixtures and example files. Read unattended scripts (hooks, runners) as they are
|
|
33
|
+
really run (as a script, with a real payload). Compare risky behaviour with the code before the change (the `-` lines
|
|
34
|
+
of the diff) when the change claims to fix or tighten something. Report any that would fail or that cannot work as
|
|
35
|
+
documented. For a check you cannot settle by reading, write one line `not executed: <command> (reviewer has no
|
|
36
|
+
shell)`.
|
|
35
37
|
|
|
36
38
|
## Step 5 - Tests
|
|
37
39
|
For each new or changed test: could it pass for the wrong reason (no assertion that can fail, mocked away the thing
|
|
@@ -52,9 +54,10 @@ Severity rule. Only **blocking** stops the merge, so use it with care:
|
|
|
52
54
|
negative test, a doc that is wrong in a way that does not make a command fail, an error message that misleads.
|
|
53
55
|
- **nit** = style and wording.
|
|
54
56
|
|
|
55
|
-
Every finding needs a concrete reproduction: a command, an input or a test that
|
|
56
|
-
failure scenario. A finding without a reproduction is a **nit**,
|
|
57
|
-
write "might", "could in theory" or "a future change may": show it
|
|
57
|
+
Every finding needs a concrete reproduction: a command, an input or a test that would show the failure, written in
|
|
58
|
+
the failure scenario (you do not run it; the implementer does). A finding without a reproduction is a **nit**,
|
|
59
|
+
whatever its severity would otherwise be. Do not write "might", "could in theory" or "a future change may": show it
|
|
60
|
+
or call it a nit.
|
|
58
61
|
|
|
59
62
|
The gate exits by the findings. A **blocking** finding fails it. **should-fix** and **nit** findings do not: they are
|
|
60
63
|
listed in the report under "Follow-up (not blocking)". The tool allows 3 completed review rounds for a phase; the 4th completed round
|