@caddis/cli 0.4.52 → 0.4.54
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-GLOSSARY.md +1 -0
- package/bundles/antigravity-plugin/guard_agy.py +184 -0
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/repo_freshness.py +132 -5
- package/bundles/antigravity-plugin/scripts/caddis_budget.py +283 -0
- package/bundles/antigravity-plugin/scripts/caddis_checkout_guard.py +227 -0
- package/bundles/antigravity-plugin/scripts/caddis_docs_pr.py +284 -0
- package/bundles/antigravity-plugin/scripts/caddis_exit.py +2 -0
- package/bundles/antigravity-plugin/scripts/caddis_fleet.py +439 -0
- package/bundles/antigravity-plugin/scripts/caddis_frontmatter.py +1 -0
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +255 -195
- package/bundles/antigravity-plugin/scripts/caddis_glossary.py +3 -3
- package/bundles/antigravity-plugin/scripts/caddis_governor.py +48 -4
- package/bundles/antigravity-plugin/scripts/caddis_inventory.py +38 -0
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +411 -127
- package/bundles/antigravity-plugin/scripts/caddis_lineage_publish.py +458 -0
- package/bundles/antigravity-plugin/scripts/caddis_merge_chain.py +237 -0
- package/bundles/antigravity-plugin/scripts/caddis_notify.py +125 -0
- package/bundles/antigravity-plugin/scripts/caddis_prod_check.py +380 -0
- package/bundles/antigravity-plugin/scripts/caddis_redact.py +99 -0
- package/bundles/antigravity-plugin/scripts/caddis_relay_git.py +2 -2
- package/bundles/antigravity-plugin/scripts/caddis_review.py +1309 -0
- package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +2109 -0
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +676 -0
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +2605 -0
- package/bundles/antigravity-plugin/scripts/caddis_routing.py +7 -6
- package/bundles/antigravity-plugin/scripts/caddis_rows.py +476 -0
- package/bundles/antigravity-plugin/scripts/caddis_spawn.py +8 -9
- package/bundles/antigravity-plugin/scripts/caddis_spend.py +415 -0
- package/bundles/antigravity-plugin/scripts/caddis_tidy.py +357 -20
- package/bundles/antigravity-plugin/scripts/caddis_todo.py +55 -0
- package/bundles/antigravity-plugin/scripts/caddis_workstreams.py +178 -19
- package/bundles/antigravity-plugin/scripts/caddis_worktree_close.py +349 -0
- package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +0 -56
- package/bundles/antigravity-plugin/scripts/claudster_doctor.py +1 -1
- package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +0 -1
- package/bundles/antigravity-plugin/scripts/oss_review.py +2 -2
- package/bundles/antigravity-plugin/scripts/roadmap-recheck.ps1 +89 -0
- package/bundles/antigravity-plugin/scripts/roadmap-run.ps1 +88 -0
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +43 -1
- package/bundles/antigravity-plugin/session_end_agy.py +3 -3
- package/bundles/antigravity-plugin/skills/_registry.md +4 -6
- package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +1 -1
- package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +1 -1
- package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +1 -1
- package/bundles/antigravity-plugin/skills/catchup/SKILL.md +11 -0
- package/bundles/antigravity-plugin/skills/cloud-phases/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +5 -2
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +35 -83
- package/bundles/antigravity-plugin/skills/fleet-close/SKILL.md +167 -0
- package/bundles/antigravity-plugin/skills/fleet-open/SKILL.md +110 -0
- package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +2 -1
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +177 -48
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +87 -147
- package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +4 -3
- package/bundles/antigravity-plugin/skills/mmr/SKILL.md +5 -2
- package/bundles/antigravity-plugin/skills/office-documents-official/SKILL.md +24 -0
- package/bundles/antigravity-plugin/skills/park/SKILL.md +0 -1
- package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +28 -20
- package/bundles/antigravity-plugin/skills/prd/SKILL.md +2 -10
- package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +2 -1
- package/bundles/antigravity-plugin/skills/review-pr/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +260 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/plan-resume-prompt.md +77 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/render-check.template.mjs +68 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/review-protocol.md +78 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/roadmap.template.md +32 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/value-check.template.py +161 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +16 -16
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +15 -5
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +36 -16
- package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +4 -8
- package/bundles/antigravity-plugin/warm_start_agy.py +1 -3
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +1 -2
- package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +5 -5
- package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +5 -2
- package/bundles/antigravity-plugin-extras/skills/runner-move/SKILL.md +214 -0
- package/bundles/codex/.codex/skills/coding/refactoring/SKILL.md +2 -1
- package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +2 -1
- package/bundles/codex/.codex/skills/docs/office-documents-official/SKILL.md +24 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/best-practices-referencer.md +1 -1
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/codebase-context-builder.md +1 -1
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/task-intent-analyzer.md +1 -1
- package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +260 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/plan-resume-prompt.md +77 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/render-check.template.mjs +68 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/review-protocol.md +78 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/roadmap.template.md +32 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/value-check.template.py +161 -0
- package/bundles/codex/AGENTS.md +5 -4
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
- package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +0 -233
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/context-template.md +0 -214
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/hub-handbook.md +0 -233
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/report-contract.md +0 -94
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/succession.md +0 -118
- package/bundles/antigravity-plugin/skills/resume/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/review/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/spawn-hub/SKILL.md +0 -341
- package/bundles/antigravity-plugin/skills/statusline/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/validate-phase/SKILL.md +0 -143
- package/bundles/antigravity-plugin-extras/skills/data-lineage/SKILL.md +0 -158
- package/bundles/antigravity-plugin-extras/skills/data-lineage/scripts/convert_doc_to_confluence.py +0 -1430
- package/bundles/antigravity-plugin-extras/skills/golden-plan/SKILL.md +0 -586
- package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +0 -233
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/context-template.md +0 -214
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/hub-handbook.md +0 -233
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/report-contract.md +0 -94
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/succession.md +0 -118
|
@@ -5,11 +5,9 @@ WHY
|
|
|
5
5
|
---
|
|
6
6
|
Most of caddis's guarantees are sentences in markdown that a model may or may not honour
|
|
7
7
|
(measured: 7.0% machine-enforced at the time this landed — `scripts/enforcement_inventory.py`).
|
|
8
|
-
The
|
|
8
|
+
The two gates below are the ones that have actually bitten, so they become exit codes:
|
|
9
9
|
|
|
10
10
|
lane-check the assigned execution lane was never read, so a "cheap" lane ran nothing
|
|
11
|
-
verdict-gate an Advisory-Hub verdict from a DIFFERENT plan could unlock the next phase,
|
|
12
|
-
and a batched verdict (`phase-10-11`) falsely blocked the phase after it
|
|
13
11
|
tracker-vs-git a stray staged edit reverted four completed Tracker rows to "not started"
|
|
14
12
|
and it was caught two milestones later
|
|
15
13
|
|
|
@@ -62,9 +60,7 @@ def _degrade(msg: str) -> int:
|
|
|
62
60
|
|
|
63
61
|
def is_headless(env=None) -> bool:
|
|
64
62
|
e = env if env is not None else os.environ
|
|
65
|
-
|
|
66
|
-
return True
|
|
67
|
-
return bool(e.get("DOCKET_PLAN") or e.get("DOCKET_BRANCH"))
|
|
63
|
+
return str(e.get("CADDIS_HEADLESS", "")).strip().lower() in _TRUTHY
|
|
68
64
|
|
|
69
65
|
|
|
70
66
|
# ── plan parsing ────────────────────────────────────────────────────────────
|
|
@@ -93,10 +89,11 @@ def phase_lane(block: str) -> str:
|
|
|
93
89
|
def launch_command(block: str) -> str:
|
|
94
90
|
"""The literal command for a non-claude lane, if the phase names one.
|
|
95
91
|
|
|
96
|
-
Also matches a caddis_lanes.py run command
|
|
92
|
+
Also matches a caddis_lanes.py run command, a direct codex/agy command, and an agy command
|
|
93
|
+
wrapped in the spend cap (`python .../caddis_spend.py run --caller X -- agy ...`).
|
|
97
94
|
"""
|
|
98
95
|
m = re.search(
|
|
99
|
-
r'`((?:claude-(?:glm|oss|deepseek)\b|' + _LANES_RUN
|
|
96
|
+
r'`((?:claude-(?:glm|oss|deepseek)\b|' + _LANES_RUN + r'\b|' + _SPEND_RUN
|
|
100
97
|
+ r'\b|codex\s+exec\b|agy\b)[^`]*)`',
|
|
101
98
|
block,
|
|
102
99
|
)
|
|
@@ -104,6 +101,22 @@ def launch_command(block: str) -> str:
|
|
|
104
101
|
|
|
105
102
|
|
|
106
103
|
_LANES_RUN = r'python\s+"?\$\{CLAUDE_PLUGIN_ROOT\}/scripts/caddis_lanes\.py"?\s+run'
|
|
104
|
+
# Every agy lane goes through the global spend cap (caddis-next Phase 4). Any path that ends in
|
|
105
|
+
# scripts/caddis_spend.py: `${CLAUDE_PLUGIN_ROOT}/...`, an expanded plugin-cache path (the codex bundle
|
|
106
|
+
# rewrites the variable), or the bare repo-relative `scripts/caddis_spend.py` - which gate_lane accepts
|
|
107
|
+
# only in the caddis source repo, because in an app repo that path is a file the agent can write.
|
|
108
|
+
_SPEND_RUN = r'python3?\s+"?[^"\s`]*scripts/caddis_spend\.py"?\s+run'
|
|
109
|
+
_SPEND_PATH = re.compile(r'python3?\s+"?([^"\s`]*scripts/caddis_spend\.py)"?\s+run')
|
|
110
|
+
# Shell control characters outside the quoted prompt would chain a second, uncapped command.
|
|
111
|
+
_SHELL_CHAIN = re.compile(r"[;&|`\n]|\$\(")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _caddis_source_root(plan: Path) -> bool:
|
|
115
|
+
"""True when the plan lives in the caddis source repo (the only place a repo-relative wrapper is trusted)."""
|
|
116
|
+
for parent in plan.resolve().parents:
|
|
117
|
+
if (parent / ".git").exists():
|
|
118
|
+
return (parent / "claude-harness" / "scripts" / "caddis_governor.py").is_file()
|
|
119
|
+
return False
|
|
107
120
|
|
|
108
121
|
|
|
109
122
|
# ── gate: lane-check ────────────────────────────────────────────────────────
|
|
@@ -140,6 +153,46 @@ def gate_lane(plan: Path, phase: int) -> int:
|
|
|
140
153
|
if not cmd:
|
|
141
154
|
sys.stderr.write(f"[caddis-gate] phase {phase} is lane '{lane}' but names no launch command\n")
|
|
142
155
|
return EXIT_MALFORMED
|
|
156
|
+
if re.match(r"agy\b", cmd):
|
|
157
|
+
sys.stderr.write(
|
|
158
|
+
f"[caddis-gate] phase {phase}'s agy command is not wrapped in the spend cap: {cmd}\n"
|
|
159
|
+
" agy lanes must go through caddis_spend.py run --caller <name> -- agy ... Refusing to run it.\n")
|
|
160
|
+
return EXIT_MALFORMED
|
|
161
|
+
inner = cmd
|
|
162
|
+
if re.match(_SPEND_RUN, cmd):
|
|
163
|
+
wrapped = re.match(_SPEND_RUN + r'\s+--caller\s+\S+\s+--\s+(agy\b.*)$', cmd, re.S)
|
|
164
|
+
if not wrapped:
|
|
165
|
+
sys.stderr.write(
|
|
166
|
+
f"[caddis-gate] phase {phase}'s spend-cap command is malformed: {cmd}\n"
|
|
167
|
+
" Expected: caddis_spend.py run --caller <name> -- agy ... Refusing to run it.\n")
|
|
168
|
+
return EXIT_MALFORMED
|
|
169
|
+
inner = wrapped.group(1)
|
|
170
|
+
# Command substitution runs even inside double quotes (bash and PowerShell), so `$(` and
|
|
171
|
+
# backticks are refused anywhere; `${` is refused after the `--` (the wrapper path may use it).
|
|
172
|
+
if (_SHELL_CHAIN.search(_QUOTED.sub('""', cmd)) or "$(" in cmd or "`" in cmd
|
|
173
|
+
or "${" in inner):
|
|
174
|
+
sys.stderr.write(
|
|
175
|
+
f"[caddis-gate] phase {phase}'s agy command chains another command: {cmd}\n"
|
|
176
|
+
" A second command after the wrapped agy would run outside the spend cap. Refusing to run it.\n")
|
|
177
|
+
return EXIT_MALFORMED
|
|
178
|
+
formats = re.findall(r"--output-format(?:\s+|=)(\S+)", _QUOTED.sub('""', inner))
|
|
179
|
+
if formats != ["json"]:
|
|
180
|
+
sys.stderr.write(
|
|
181
|
+
f"[caddis-gate] phase {phase}'s agy command must have exactly one --output-format json: {cmd}\n"
|
|
182
|
+
" Without JSON output the spend cap cannot read the token counts. Refusing to run it.\n")
|
|
183
|
+
return EXIT_MALFORMED
|
|
184
|
+
path = _SPEND_PATH.match(cmd).group(1)
|
|
185
|
+
norm = path.replace("\\", "/")
|
|
186
|
+
trusted = (norm == "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_spend.py"
|
|
187
|
+
or ("/plugins/cache/" in norm and (norm.startswith("/") or re.match(r"[A-Za-z]:/", norm)
|
|
188
|
+
or norm.startswith("${HOME}/")))
|
|
189
|
+
or (norm == "scripts/caddis_spend.py" and _caddis_source_root(plan)))
|
|
190
|
+
if not trusted:
|
|
191
|
+
sys.stderr.write(
|
|
192
|
+
f"[caddis-gate] phase {phase}'s agy command uses an untrusted caddis_spend.py path: {cmd}\n"
|
|
193
|
+
" Use \"${CLAUDE_PLUGIN_ROOT}/scripts/caddis_spend.py\" (or the installed plugin-cache path;\n"
|
|
194
|
+
" the bare scripts/caddis_spend.py only in the caddis source repo). Refusing to run it.\n")
|
|
195
|
+
return EXIT_MALFORMED
|
|
143
196
|
if re.match(_LANES_RUN, cmd):
|
|
144
197
|
named = re.search(r"--phase[\s=]+(\d+)", cmd)
|
|
145
198
|
if not named:
|
|
@@ -152,13 +205,13 @@ def gate_lane(plan: Path, phase: int) -> int:
|
|
|
152
205
|
f"[caddis-gate] phase {phase}'s caddis_lanes command names --phase {named.group(1)}: {cmd}\n"
|
|
153
206
|
" The lane would run another phase's work. Refusing to run it.\n")
|
|
154
207
|
return EXIT_MALFORMED
|
|
155
|
-
elif re.match(r"(?:codex\s+exec|agy)\b",
|
|
156
|
-
if "HEADLESS RUN RULES" not in
|
|
208
|
+
elif re.match(r"(?:codex\s+exec|agy)\b", inner):
|
|
209
|
+
if "HEADLESS RUN RULES" not in inner:
|
|
157
210
|
sys.stderr.write(
|
|
158
211
|
f"[caddis-gate] phase {phase}'s direct command has no HEADLESS RUN RULES: {cmd}\n"
|
|
159
212
|
" The child has no headless run rules. Refusing to run it.\n")
|
|
160
213
|
return EXIT_MALFORMED
|
|
161
|
-
named = re.search(r"Implement Phase (\d+) only",
|
|
214
|
+
named = re.search(r"Implement Phase (\d+) only", inner)
|
|
162
215
|
if not named:
|
|
163
216
|
sys.stderr.write(
|
|
164
217
|
f"[caddis-gate] phase {phase}'s direct command has no phase restriction: {cmd}\n"
|
|
@@ -169,7 +222,7 @@ def gate_lane(plan: Path, phase: int) -> int:
|
|
|
169
222
|
f"[caddis-gate] phase {phase}'s direct command names Phase {named.group(1)}: {cmd}\n"
|
|
170
223
|
" The lane would run another phase's work. Refusing to run it.\n")
|
|
171
224
|
return EXIT_MALFORMED
|
|
172
|
-
if
|
|
225
|
+
if inner.startswith("agy") and not _has_print_flag(inner):
|
|
173
226
|
sys.stderr.write(
|
|
174
227
|
f"[caddis-gate] phase {phase}'s direct agy command has no -p/--print: {cmd}\n"
|
|
175
228
|
" The child would not run headless. Refusing to run it.\n")
|
|
@@ -185,83 +238,6 @@ def gate_lane(plan: Path, phase: int) -> int:
|
|
|
185
238
|
return EXIT_WRONG_LANE
|
|
186
239
|
|
|
187
240
|
|
|
188
|
-
# ── gate: verdict-gate ──────────────────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
def _frontmatter(p: Path) -> dict:
|
|
191
|
-
return caddis_frontmatter.parse(p.read_text(encoding="utf-8", errors="ignore"))
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def _covers(phase_field: str, n: int) -> bool:
|
|
195
|
-
s = str(phase_field).strip()
|
|
196
|
-
m = re.fullmatch(r"(\d+)\s*-\s*(\d+)", s)
|
|
197
|
-
if m:
|
|
198
|
-
return int(m.group(1)) <= n <= int(m.group(2))
|
|
199
|
-
return s.isdigit() and int(s) == n
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def _hub_context_for(plan: Path) -> Path | None:
|
|
203
|
-
"""The advisory-context doc guarding this plan, or None if Hub mode is off.
|
|
204
|
-
|
|
205
|
-
Primary: the documented `<plan-stem>-advisory-context.md`.
|
|
206
|
-
|
|
207
|
-
Fallback by IDENTITY, and it is load-bearing: a real plan was renamed
|
|
208
|
-
`genesys-rebuild.md` -> `genesys-rebuild-ui-revamp.md` and its context doc kept the old stem.
|
|
209
|
-
The filename convention therefore stopped matching, Advisory-Hub mode silently switched OFF
|
|
210
|
-
for every subsequent run, and nobody noticed because the Hub kept being driven by hand — 31
|
|
211
|
-
artifacts were written while the gate meant to enforce them was dark. Both files still carry
|
|
212
|
-
`feature: genesys-rebuild-ui-revamp`, so identity survives a rename where a filename does not.
|
|
213
|
-
|
|
214
|
-
Requires an explicit matching `feature:` on BOTH sides, so it cannot switch Hub mode on for a
|
|
215
|
-
plan that never opted in — "no file -> zero behaviour change" still holds.
|
|
216
|
-
"""
|
|
217
|
-
exact = plan.with_name(plan.stem + "-advisory-context.md")
|
|
218
|
-
if exact.is_file():
|
|
219
|
-
return exact
|
|
220
|
-
mine = _frontmatter(plan).get("feature", "").strip()
|
|
221
|
-
if not mine:
|
|
222
|
-
return None
|
|
223
|
-
for cand in sorted(plan.parent.glob("*-advisory-context.md")):
|
|
224
|
-
if _frontmatter(cand).get("feature", "").strip() == mine:
|
|
225
|
-
sys.stderr.write(
|
|
226
|
-
"[caddis-gate] Hub context matched by feature slug, not filename: "
|
|
227
|
-
f"{cand.name} (expected {exact.name} - a rename left the convention broken)\n")
|
|
228
|
-
return cand
|
|
229
|
-
return None
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def gate_verdict(plan: Path, phase: int, reports: Path | None = None) -> int:
|
|
233
|
-
if phase <= 1:
|
|
234
|
-
return EXIT_OK # no predecessor
|
|
235
|
-
if not _hub_context_for(plan):
|
|
236
|
-
return EXIT_OK # Advisory-Hub mode is OFF — the normal case, zero behaviour change
|
|
237
|
-
d = reports or (plan.parent.parent / "advisory-hub-reports")
|
|
238
|
-
if not d.is_dir():
|
|
239
|
-
sys.stderr.write(f"[caddis-gate] hub mode ON but no {d} — phase {phase-1} unvalidated\n")
|
|
240
|
-
return EXIT_BLOCKED
|
|
241
|
-
|
|
242
|
-
want = phase - 1
|
|
243
|
-
slug, path_s = plan.stem, str(plan).replace("\\", "/")
|
|
244
|
-
for f in sorted(d.glob("*.verdict.md")):
|
|
245
|
-
fm = _frontmatter(f)
|
|
246
|
-
if not _covers(fm.get("phase", ""), want):
|
|
247
|
-
continue
|
|
248
|
-
ident = fm.get("plan") or fm.get("feature") or ""
|
|
249
|
-
# Accept either spelling: both are in live fleet use and neither is wrong.
|
|
250
|
-
if ident and not (slug in ident or ident.replace("\\", "/") in path_s or ident == slug):
|
|
251
|
-
continue
|
|
252
|
-
v = fm.get("verdict", "")
|
|
253
|
-
if v in ("accept", "accept-with-correction"):
|
|
254
|
-
return EXIT_OK
|
|
255
|
-
if v == "accept-degraded":
|
|
256
|
-
sys.stderr.write(f"[caddis-gate] {f.name} is accept-degraded — validated WITHOUT an "
|
|
257
|
-
"advisory context; record that you proceeded on it\n")
|
|
258
|
-
return EXIT_NOTE
|
|
259
|
-
sys.stderr.write(f"[caddis-gate] {f.name} is '{v}' — phase {want} must be redone\n")
|
|
260
|
-
return EXIT_BLOCKED
|
|
261
|
-
sys.stderr.write(f"[caddis-gate] no verdict covering phase {want} for plan '{slug}'\n")
|
|
262
|
-
return EXIT_BLOCKED
|
|
263
|
-
|
|
264
|
-
|
|
265
241
|
# ── gate: tracker-vs-git ────────────────────────────────────────────────────
|
|
266
242
|
|
|
267
243
|
_ROW = re.compile(r"^\|\s*(\d+(?:-\d+)?)\s*\|(.*)$", re.M)
|
|
@@ -294,102 +270,6 @@ def gate_tracker(plan: Path, cwd: Path | None = None) -> int:
|
|
|
294
270
|
return EXIT_NOTE
|
|
295
271
|
|
|
296
272
|
|
|
297
|
-
|
|
298
|
-
# ── gate: hub-artifacts ─────────────────────────────────────────────────────
|
|
299
|
-
|
|
300
|
-
def gate_hub_artifacts(reports: Path, extra_refs: list[Path] | None = None) -> int:
|
|
301
|
-
"""An artifact that exists on disk but is referenced nowhere the next Hub will look.
|
|
302
|
-
|
|
303
|
-
`/spawn-hub` already conserves LEDGER IDS mechanically and fails the spawn when one vanishes.
|
|
304
|
-
There was no equivalent for the artifacts the Hub itself writes, so the failure mode was exactly
|
|
305
|
-
inverted: the ledger check catches an id that DISAPPEARED; nothing caught a file that EXISTS and
|
|
306
|
-
is invisible.
|
|
307
|
-
|
|
308
|
-
Observed 2026-08-05: a Hub wrote `phase-15-16.prompt.md` and `phase-17.prompt.md`, and its spawn
|
|
309
|
-
prompt named only the first. The spawn passed cleanly - ids conserved, every context question
|
|
310
|
-
answered, plan drift caught. Nothing failed because nothing was looking. The successor would have
|
|
311
|
-
found no prompt for Phase 17, written a fresh one, and silently lost the single load-bearing
|
|
312
|
-
paragraph explaining why that phase must run from a cold process - the whole reason it was split
|
|
313
|
-
out of its batch. A regenerated prompt would have looked complete.
|
|
314
|
-
|
|
315
|
-
Degrades open like every gate here: no directory, no artifacts, nothing readable -> exit 0.
|
|
316
|
-
"""
|
|
317
|
-
if not reports.is_dir():
|
|
318
|
-
return _degrade(f"no reports dir at {reports}")
|
|
319
|
-
# PROMPTS ONLY, and deliberately so. A successor Hub regenerates a phase PROMPT it does not
|
|
320
|
-
# know about - that is the documented near-miss. It does not regenerate reports (the
|
|
321
|
-
# implementer writes those) or verdicts (it writes those after the fact, per phase). Checking
|
|
322
|
-
# all three flagged 11 of 37 artifacts in one live repo and 25 of 37 in another, which is a
|
|
323
|
-
# gate nobody keeps switched on. Narrow to the file class that can actually be clobbered.
|
|
324
|
-
artifacts = sorted(reports.glob("phase-*.prompt.md"))
|
|
325
|
-
if not artifacts:
|
|
326
|
-
return _degrade(f"no phase artifacts in {reports}")
|
|
327
|
-
|
|
328
|
-
# Everywhere a successor Hub is told to look: the dir's own index/rules, the most recent
|
|
329
|
-
# spawn docs, and any relay handed over. Read them all as one corpus.
|
|
330
|
-
# This gate is about HANDOVER conservation. With no spawn doc, no handover has happened,
|
|
331
|
-
# so there is no successor and nothing can be orphaned from one. Flagging every prompt in a
|
|
332
|
-
# single-Hub repo would be pure noise: one live repo with 8 prompts and zero handovers was
|
|
333
|
-
# reporting all 8 until this check was added.
|
|
334
|
-
if not any(reports.glob("hub-*.spawn.md")):
|
|
335
|
-
return _degrade(f"no handover has occurred in {reports.name} (no spawn doc)")
|
|
336
|
-
|
|
337
|
-
ref_files: list[Path] = []
|
|
338
|
-
for pat in ("README.md", "AGENTS.md", "hub-*.spawn.md"):
|
|
339
|
-
ref_files += sorted(reports.glob(pat))
|
|
340
|
-
for extra in (extra_refs or []):
|
|
341
|
-
if extra.is_file():
|
|
342
|
-
ref_files.append(extra)
|
|
343
|
-
corpus = ""
|
|
344
|
-
for f in ref_files:
|
|
345
|
-
try:
|
|
346
|
-
corpus += f.read_text(encoding="utf-8", errors="ignore")
|
|
347
|
-
except Exception:
|
|
348
|
-
continue
|
|
349
|
-
if not corpus:
|
|
350
|
-
return _degrade(f"nothing references artifacts in {reports} (no index, no spawn doc)")
|
|
351
|
-
|
|
352
|
-
# A spawn prompt that never names the resume doc produces a Hub that keeps no tracker.
|
|
353
|
-
#
|
|
354
|
-
# Measured: a spawned Hub read relay.md, passed a thirteen-question context check, found four
|
|
355
|
-
# real defects in its handover — and created no tracker, because nothing asked it to. The work
|
|
356
|
-
# survived only because the OUTGOING Hub had written one into relay.md an hour earlier, unasked.
|
|
357
|
-
#
|
|
358
|
-
# The parking-lot note suggested checking that relay.md was "modified in the same commit range".
|
|
359
|
-
# That cannot work: relay.md is GITIGNORED by design, so git never sees it. What is checkable is
|
|
360
|
-
# the instruction — does the spawn prompt tell its successor to keep the tracker at all?
|
|
361
|
-
# ONLY the newest spawn doc, by hub number. Older ones are immutable history: a spawn prompt
|
|
362
|
-
# that was already used cannot be fixed, only falsified, and a gate that stays red on a record
|
|
363
|
-
# nobody may edit is the "always red, therefore ignored" failure this file keeps warning about.
|
|
364
|
-
spawns = sorted(reports.glob("hub-*.spawn.md"),
|
|
365
|
-
key=lambda p: (int(m.group(1)) if (m := re.search(r"hub-(\d+)", p.name)) else 0,
|
|
366
|
-
p.name))
|
|
367
|
-
newest = spawns[-1] if spawns else None
|
|
368
|
-
if newest and "relay" not in newest.read_text(encoding="utf-8", errors="ignore").lower():
|
|
369
|
-
sys.stderr.write(
|
|
370
|
-
f"[caddis-gate] the newest spawn prompt never mentions the resume doc:\n"
|
|
371
|
-
f" {newest.name}\n")
|
|
372
|
-
sys.stderr.write(
|
|
373
|
-
" The succession table records WHO HELD THE ROLE; .caddis/relay.md records WHERE THE\n"
|
|
374
|
-
" WORK IS. The table is only written when a Hub ends, so nothing covers the hours a\n"
|
|
375
|
-
" Hub spends working — the exact period this mechanism exists to protect. Tell the\n"
|
|
376
|
-
" incoming Hub to mirror its task list into relay.md as it goes.\n")
|
|
377
|
-
return EXIT_BLOCKED
|
|
378
|
-
|
|
379
|
-
orphans = [f.name for f in artifacts if f.name not in corpus and f.stem not in corpus]
|
|
380
|
-
if not orphans:
|
|
381
|
-
return EXIT_OK
|
|
382
|
-
sys.stderr.write(
|
|
383
|
-
f"[caddis-gate] {len(orphans)} Hub artifact(s) exist on disk but are referenced nowhere "
|
|
384
|
-
"the next Hub will look:\n")
|
|
385
|
-
for o in orphans:
|
|
386
|
-
sys.stderr.write(f" {o}\n")
|
|
387
|
-
sys.stderr.write(
|
|
388
|
-
" A successor will not know these exist, will regenerate them, and will lose whatever\n"
|
|
389
|
-
" reasoning they carried. Reference them in the spawn doc or the index before handing over.\n")
|
|
390
|
-
return EXIT_BLOCKED
|
|
391
|
-
|
|
392
|
-
|
|
393
273
|
def gate_parking_lot(repo_root: Path) -> int:
|
|
394
274
|
"""Every future-work item conforms to the one-register contract, or the push stops.
|
|
395
275
|
|
|
@@ -491,8 +371,8 @@ def gate_handover_check(doc: Path, repo_root: Path, max_chars: int | None = None
|
|
|
491
371
|
|
|
492
372
|
Degrades open on a missing/unreadable doc. Blocks on a path the doc claims and the repo does
|
|
493
373
|
not have, and -- only when the caller passes ``max_chars`` -- on a doc longer than that. The
|
|
494
|
-
cap is opt-in: /handoff caps a workstream relay at 4,000 characters, but a /spawn-
|
|
495
|
-
|
|
374
|
+
cap is opt-in: /handoff caps a workstream relay at 4,000 characters, but a /spawn-session
|
|
375
|
+
handover may embed a full inventory verbatim and would not fit.
|
|
496
376
|
"""
|
|
497
377
|
if not doc.is_file():
|
|
498
378
|
return _degrade(f"no handover doc at {doc}")
|
|
@@ -507,6 +387,35 @@ def gate_handover_check(doc: Path, repo_root: Path, max_chars: int | None = None
|
|
|
507
387
|
)
|
|
508
388
|
return EXIT_BLOCKED
|
|
509
389
|
|
|
390
|
+
# `/handoff` Step 3c saves the open tasks beside the relay. A tasks file the resume prompt
|
|
391
|
+
# never names is a task list the next session never reloads.
|
|
392
|
+
tasks_file = doc.with_name(doc.stem + ".tasks.md")
|
|
393
|
+
if tasks_file.is_file() and tasks_file.name not in text:
|
|
394
|
+
sys.stderr.write(f"handover-check: the resume prompt does not load {tasks_file.name}\n")
|
|
395
|
+
return EXIT_BLOCKED
|
|
396
|
+
|
|
397
|
+
done_bullets = 0
|
|
398
|
+
in_done = in_fence = False
|
|
399
|
+
for line in text.splitlines():
|
|
400
|
+
stripped = line.strip()
|
|
401
|
+
if stripped.startswith("```"): # a heading or bullet inside a code block is text, not structure
|
|
402
|
+
in_fence = not in_fence
|
|
403
|
+
continue
|
|
404
|
+
if in_fence:
|
|
405
|
+
continue
|
|
406
|
+
if stripped.startswith("## Done"):
|
|
407
|
+
in_done = True
|
|
408
|
+
continue
|
|
409
|
+
if in_done:
|
|
410
|
+
if re.match(r"#{1,2} ", stripped): # the next ## (or #) section ends Done; a ### stays inside it
|
|
411
|
+
in_done = False
|
|
412
|
+
continue
|
|
413
|
+
if line.lstrip().startswith(("- ", "* ")):
|
|
414
|
+
done_bullets += 1
|
|
415
|
+
if done_bullets > 8:
|
|
416
|
+
sys.stderr.write(f"handover-check: Done has {done_bullets} bullets; the cap is 8\n")
|
|
417
|
+
return EXIT_BLOCKED
|
|
418
|
+
|
|
510
419
|
missing: list[str] = []
|
|
511
420
|
try:
|
|
512
421
|
# One rule for both gates: `caddis_spawn.check_document` already treats a bare filename
|
|
@@ -667,6 +576,151 @@ def gate_review_trigger(repo_root: Path, rev_range: str | None = None) -> int:
|
|
|
667
576
|
return EXIT_NOTE
|
|
668
577
|
|
|
669
578
|
|
|
579
|
+
TIER_REVIEWERS = {
|
|
580
|
+
"0": 0,
|
|
581
|
+
"1-small": 0,
|
|
582
|
+
"1": 1,
|
|
583
|
+
"2": 2,
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def is_test_file(path: str) -> bool:
|
|
588
|
+
norm = path.replace("\\", "/").lower()
|
|
589
|
+
if norm.startswith("./"):
|
|
590
|
+
norm = norm[2:]
|
|
591
|
+
segs = norm.split("/")
|
|
592
|
+
filename = segs[-1]
|
|
593
|
+
if any(s in ("tests", "test") for s in segs[:-1]):
|
|
594
|
+
return True
|
|
595
|
+
if filename.startswith("test_") and filename.endswith(".py"):
|
|
596
|
+
return True
|
|
597
|
+
if filename.endswith("_test.py"):
|
|
598
|
+
return True
|
|
599
|
+
for ext in (".test.ts", ".test.tsx", ".test.js", ".test.jsx"):
|
|
600
|
+
if filename.endswith(ext):
|
|
601
|
+
return True
|
|
602
|
+
return False
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def is_tier0_file(path: str) -> bool:
|
|
606
|
+
norm = path.replace("\\", "/").lower()
|
|
607
|
+
if norm.startswith("./"):
|
|
608
|
+
norm = norm[2:]
|
|
609
|
+
if norm.startswith("docs/") or norm.startswith(".caddis/"):
|
|
610
|
+
return True
|
|
611
|
+
if "/" not in norm:
|
|
612
|
+
stem = norm.rsplit(".", 1)[0]
|
|
613
|
+
ext = norm.rsplit(".", 1)[1] if "." in norm else ""
|
|
614
|
+
if ext == "md" and (stem.startswith("readme") or stem.startswith("changelog")):
|
|
615
|
+
return True
|
|
616
|
+
if is_test_file(norm):
|
|
617
|
+
return True
|
|
618
|
+
return False
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
BINARY_LINES = 41 # a binary change has no line count: count it above the 40-line small limit, never "1-small"
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _rename_paths(path: str) -> list[str]:
|
|
625
|
+
"""Both sides of a `--numstat` rename path (`old => new` or `pre/{old => new}/post`); [path] when not a rename."""
|
|
626
|
+
if " => " not in path:
|
|
627
|
+
return [path]
|
|
628
|
+
if "{" in path and "}" in path:
|
|
629
|
+
prefix, rest = path.split("{", 1)
|
|
630
|
+
inner, suffix = rest.split("}", 1)
|
|
631
|
+
old, new = inner.split(" => ", 1)
|
|
632
|
+
return [f"{prefix}{side}{suffix}".replace("//", "/").lstrip("/") for side in (old, new)]
|
|
633
|
+
old, new = path.split(" => ", 1)
|
|
634
|
+
return [old, new]
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _numstat(rng: str | None = None, repo_root: Path = Path(".")) -> list[tuple[str, int]]:
|
|
638
|
+
"""(path, changed-lines) per file for an explicit range or the working tree.
|
|
639
|
+
|
|
640
|
+
A rename lists BOTH the old and the new path, so moving code into `docs/` or `tests/` is not tier 0. A binary
|
|
641
|
+
change counts `BINARY_LINES`. A failed `git diff` raises RuntimeError: the caller must not read it as an empty
|
|
642
|
+
diff (tier 0)."""
|
|
643
|
+
args = ["git", "-C", str(repo_root), "diff", "--numstat"]
|
|
644
|
+
if rng:
|
|
645
|
+
args.append(rng)
|
|
646
|
+
res = subprocess.run(args, capture_output=True, text=True, timeout=30)
|
|
647
|
+
if res.returncode != 0:
|
|
648
|
+
reason = (res.stderr or "").strip().splitlines()
|
|
649
|
+
raise RuntimeError(f"git diff --numstat exited {res.returncode}: {reason[0] if reason else 'no message'}")
|
|
650
|
+
files: list[tuple[str, int]] = []
|
|
651
|
+
for row in res.stdout.splitlines():
|
|
652
|
+
parts = row.split("\t")
|
|
653
|
+
if len(parts) != 3:
|
|
654
|
+
continue
|
|
655
|
+
add, dele, path = parts
|
|
656
|
+
if add == "-" or dele == "-":
|
|
657
|
+
line_count = BINARY_LINES
|
|
658
|
+
else:
|
|
659
|
+
line_count = (int(add) if add.isdigit() else 0) + (int(dele) if dele.isdigit() else 0)
|
|
660
|
+
paths = _rename_paths(path.strip())
|
|
661
|
+
if len(paths) == 2:
|
|
662
|
+
# A rename moves code out of its old path; a pure rename has 0 changed lines, so without this a move
|
|
663
|
+
# into tests/ would read as a small, covered change (tier 1-small, no review). The old path counts as large.
|
|
664
|
+
files.append((paths[0], max(line_count, BINARY_LINES)))
|
|
665
|
+
files.append((paths[1], max(line_count, BINARY_LINES))) # tests/x.py -> x.py moves test code into production
|
|
666
|
+
else:
|
|
667
|
+
for p in paths:
|
|
668
|
+
files.append((p, line_count))
|
|
669
|
+
return files
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def review_tier(files: list[tuple[str, int]], plan_risk: str = "normal") -> str:
|
|
673
|
+
"""Return '0', '1-small', '1' or '2' by the model routing review budget."""
|
|
674
|
+
non_tier0 = [(p, lines) for p, lines in files if not is_tier0_file(p)]
|
|
675
|
+
if not non_tier0:
|
|
676
|
+
return "0"
|
|
677
|
+
if str(plan_risk).strip().lower() == "high":
|
|
678
|
+
return "2"
|
|
679
|
+
non_tier0_paths = [p for p, _ in non_tier0]
|
|
680
|
+
non_tier0_lines = sum(lines for _, lines in non_tier0)
|
|
681
|
+
if review_triggers(non_tier0_paths, non_tier0_lines):
|
|
682
|
+
return "2"
|
|
683
|
+
has_test = any(is_test_file(p) for p, _ in files)
|
|
684
|
+
if non_tier0_lines <= 40 and has_test:
|
|
685
|
+
return "1-small"
|
|
686
|
+
return "1"
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def gate_review_tier(
|
|
690
|
+
repo_root: Path,
|
|
691
|
+
rev_range: str | None = None,
|
|
692
|
+
plan: Path | str | None = None,
|
|
693
|
+
phase: int | str | None = None,
|
|
694
|
+
) -> int:
|
|
695
|
+
"""Print the tier and reviewer count for the diff and plan phase."""
|
|
696
|
+
risk = "normal"
|
|
697
|
+
if plan and phase is None:
|
|
698
|
+
risk = "high" # fail closed: a plan without a phase cannot give a risk
|
|
699
|
+
elif plan and phase is not None:
|
|
700
|
+
try:
|
|
701
|
+
try:
|
|
702
|
+
import caddis_review
|
|
703
|
+
except ModuleNotFoundError:
|
|
704
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
705
|
+
import caddis_review
|
|
706
|
+
risk = caddis_review.phase_risk(Path(plan), phase)
|
|
707
|
+
except Exception:
|
|
708
|
+
# Fail closed: a plan risk we cannot read counts as high (tier 2).
|
|
709
|
+
risk = "high"
|
|
710
|
+
try:
|
|
711
|
+
files = _numstat(rev_range, repo_root)
|
|
712
|
+
except Exception as exc: # noqa: BLE001 - a diff we cannot read is reviewed in full, never skipped
|
|
713
|
+
sys.stderr.write(f"caddis-gate: review-tier could not read the diff ({exc}); counting it as tier 2\n")
|
|
714
|
+
print("TIER: 2")
|
|
715
|
+
print("REVIEWERS: 2")
|
|
716
|
+
return EXIT_OK
|
|
717
|
+
tier = review_tier(files, plan_risk=risk)
|
|
718
|
+
reviewers = TIER_REVIEWERS.get(tier, 2)
|
|
719
|
+
print(f"TIER: {tier}")
|
|
720
|
+
print(f"REVIEWERS: {reviewers}")
|
|
721
|
+
return EXIT_OK
|
|
722
|
+
|
|
723
|
+
|
|
670
724
|
def gate_docs_check(root: Path) -> int:
|
|
671
725
|
"""Report every typed .caddis Markdown document with an invalid header."""
|
|
672
726
|
failures = 0
|
|
@@ -686,21 +740,24 @@ def gate_docs_check(root: Path) -> int:
|
|
|
686
740
|
def main(argv: list[str] | None = None) -> int:
|
|
687
741
|
ap = argparse.ArgumentParser(description="caddis machine gates")
|
|
688
742
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
689
|
-
for name in ("lane-check",
|
|
743
|
+
for name in ("lane-check",):
|
|
690
744
|
s = sub.add_parser(name)
|
|
691
745
|
s.add_argument("--plan", required=True)
|
|
692
746
|
s.add_argument("--phase", type=int, required=True)
|
|
693
747
|
t = sub.add_parser("tracker-vs-git")
|
|
694
748
|
t.add_argument("--plan", required=True)
|
|
695
|
-
h = sub.add_parser("hub-artifacts")
|
|
696
|
-
h.add_argument("--reports", required=True)
|
|
697
|
-
h.add_argument("--refs", nargs="*", default=[])
|
|
698
749
|
pl = sub.add_parser("parking-lot")
|
|
699
750
|
pl.add_argument("--repo-root", default=".")
|
|
700
751
|
rt = sub.add_parser("review-trigger")
|
|
701
752
|
rt.add_argument("--repo-root", default=".")
|
|
702
753
|
rt.add_argument("--range", dest="rev_range", default=None,
|
|
703
754
|
help="revision range, e.g. origin/main...HEAD (default: the working tree)")
|
|
755
|
+
rw = sub.add_parser("review-tier")
|
|
756
|
+
rw.add_argument("--repo-root", default=".")
|
|
757
|
+
rw.add_argument("--range", dest="rev_range", default=None,
|
|
758
|
+
help="revision range, e.g. origin/main...HEAD (default: the working tree)")
|
|
759
|
+
rw.add_argument("--plan", default=None, help="path to plan markdown")
|
|
760
|
+
rw.add_argument("--phase", default=None, help="phase number or heading")
|
|
704
761
|
vd = sub.add_parser("vendor-drift")
|
|
705
762
|
vd.add_argument("--repo-root", default=".")
|
|
706
763
|
hc = sub.add_parser("handover-check")
|
|
@@ -722,14 +779,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
722
779
|
try:
|
|
723
780
|
if a.cmd == "lane-check":
|
|
724
781
|
return gate_lane(plan, a.phase)
|
|
725
|
-
if a.cmd == "verdict-gate":
|
|
726
|
-
return gate_verdict(plan, a.phase)
|
|
727
|
-
if a.cmd == "hub-artifacts":
|
|
728
|
-
return gate_hub_artifacts(Path(a.reports), [Path(r) for r in a.refs])
|
|
729
782
|
if a.cmd == "parking-lot":
|
|
730
783
|
return gate_parking_lot(Path(a.repo_root).resolve())
|
|
731
784
|
if a.cmd == "review-trigger":
|
|
732
785
|
return gate_review_trigger(Path(a.repo_root).resolve(), a.rev_range)
|
|
786
|
+
if a.cmd == "review-tier":
|
|
787
|
+
return gate_review_tier(
|
|
788
|
+
Path(a.repo_root).resolve(),
|
|
789
|
+
a.rev_range,
|
|
790
|
+
getattr(a, "plan", None),
|
|
791
|
+
getattr(a, "phase", None),
|
|
792
|
+
)
|
|
733
793
|
if a.cmd == "vendor-drift":
|
|
734
794
|
return gate_vendor_drift(Path(a.repo_root).resolve())
|
|
735
795
|
if a.cmd == "handover-check":
|
|
@@ -20,7 +20,7 @@ stop-list below is the load-bearing part, and it is deliberately generous: a fal
|
|
|
20
20
|
costs a human three seconds to ignore, while a false negative leaves a coined word undefined
|
|
21
21
|
forever, which is the failure being fixed.
|
|
22
22
|
|
|
23
|
-
ACCURACY, MEASURED. On a normal application repo it is good: run against
|
|
23
|
+
ACCURACY, MEASURED. On a normal application repo it is good: run against a task-board app it proposed
|
|
24
24
|
`board`, `lane`, `card`, `gate` — that project's actual coined vocabulary, top of the list. On a
|
|
25
25
|
HARNESS repo full of documentation ABOUT agents it is noisier, because words like `protocol` and
|
|
26
26
|
`intent` are both ordinary English and genuinely overloaded here. It is a suggester, not an
|
|
@@ -107,8 +107,8 @@ MAX_SUGGESTIONS = 25
|
|
|
107
107
|
# A coined term is specific, so in a LARGE corpus it shows up in a minority of files. Measured:
|
|
108
108
|
# `description` appeared in 2112 of ~2500 caddis docs and outranked every real term.
|
|
109
109
|
#
|
|
110
|
-
# But this rule INVERTS on a small repo. Applied unconditionally it deleted
|
|
111
|
-
# candidates — `board`, `lane`, `card
|
|
110
|
+
# But this rule INVERTS on a small repo. Applied unconditionally it deleted a task-board app's four best
|
|
111
|
+
# candidates — `board`, `lane`, `card` and the app's own name — because in a 50-file repo the core vocabulary
|
|
112
112
|
# genuinely is in most files. So it only engages once there are enough files for "share" to
|
|
113
113
|
# mean anything.
|
|
114
114
|
MAX_FILE_SHARE = 0.25
|