ticketwright 2.0.0__py3-none-any.whl
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.
- ticketwright/__init__.py +9 -0
- ticketwright/_kit/.claude/agents/qc-reviewer.md +50 -0
- ticketwright/_kit/.claude/commands/build-context-pack.md +8 -0
- ticketwright/_kit/.claude/commands/build-ticket-index.md +8 -0
- ticketwright/_kit/.claude/commands/configure-workspace.md +8 -0
- ticketwright/_kit/.claude/commands/deliver-ticket.md +8 -0
- ticketwright/_kit/.claude/commands/onboard-teammate.md +8 -0
- ticketwright/_kit/.claude/commands/prime-domain.md +8 -0
- ticketwright/_kit/.claude/commands/prime-ticket.md +8 -0
- ticketwright/_kit/.claude/commands/prime-warehouse.md +8 -0
- ticketwright/_kit/.claude/commands/productize-workflow.md +8 -0
- ticketwright/_kit/.claude/commands/qc-review.md +8 -0
- ticketwright/_kit/.claude/commands/recall.md +8 -0
- ticketwright/_kit/.claude/commands/start-ticket.md +8 -0
- ticketwright/_kit/.claude/config/stack.example.asana-bq.yaml +67 -0
- ticketwright/_kit/.claude/config/stack.example.azure.yaml +73 -0
- ticketwright/_kit/.claude/config/stack.schema.md +76 -0
- ticketwright/_kit/.claude/config/stack.yaml +76 -0
- ticketwright/_kit/.claude/hooks/db_write_guard.py +145 -0
- ticketwright/_kit/.claude/hooks/regenerate_ticket_index.py +74 -0
- ticketwright/_kit/.claude/hooks/session_context.py +78 -0
- ticketwright/_kit/.claude/hooks/ticket_index_context.py +99 -0
- ticketwright/_kit/.claude/settings.json.tmpl +59 -0
- ticketwright/_kit/.claude/skills/productize/SKILL.md +58 -0
- ticketwright/_kit/.claude/skills/productize/authoring.md +36 -0
- ticketwright/_kit/.claude/skills/refresh/SKILL.md +40 -0
- ticketwright/_kit/.claude/skills/refresh/context-pack.md +34 -0
- ticketwright/_kit/.claude/skills/refresh/index.md +45 -0
- ticketwright/_kit/.claude/skills/review/SKILL.md +76 -0
- ticketwright/_kit/.claude/skills/setup/SKILL.md +59 -0
- ticketwright/_kit/.claude/skills/setup/adopt.md +43 -0
- ticketwright/_kit/.claude/skills/setup/scaffold.md +38 -0
- ticketwright/_kit/.claude/skills/setup/teammate.md +33 -0
- ticketwright/_kit/.claude/skills/ship/SKILL.md +53 -0
- ticketwright/_kit/.claude/skills/spec-and-build/SKILL.md +54 -0
- ticketwright/_kit/.claude/skills/ticket/SKILL.md +59 -0
- ticketwright/_kit/.claude/skills/ticket/priming.md +47 -0
- ticketwright/_kit/.claude/statusline.sh +22 -0
- ticketwright/_kit/adapters/README.md +103 -0
- ticketwright/_kit/adapters/chat/slack.md +46 -0
- ticketwright/_kit/adapters/chat/teams.md +38 -0
- ticketwright/_kit/adapters/docstore/gdrive.md +39 -0
- ticketwright/_kit/adapters/docstore/sharepoint.md +41 -0
- ticketwright/_kit/adapters/tracker/asana.md +55 -0
- ticketwright/_kit/adapters/tracker/azure-devops.md +68 -0
- ticketwright/_kit/adapters/tracker/github-issues.md +59 -0
- ticketwright/_kit/adapters/tracker/jira.md +71 -0
- ticketwright/_kit/adapters/tracker/linear.md +56 -0
- ticketwright/_kit/adapters/tracker/monday.md +62 -0
- ticketwright/_kit/adapters/vcs/azure-repos.md +58 -0
- ticketwright/_kit/adapters/vcs/github.md +55 -0
- ticketwright/_kit/adapters/vcs/gitlab.md +52 -0
- ticketwright/_kit/adapters/warehouse/bigquery.md +44 -0
- ticketwright/_kit/adapters/warehouse/databricks.md +58 -0
- ticketwright/_kit/adapters/warehouse/postgres.md +44 -0
- ticketwright/_kit/adapters/warehouse/redshift.md +49 -0
- ticketwright/_kit/adapters/warehouse/snowflake.md +54 -0
- ticketwright/_kit/adapters/warehouse/synapse.md +52 -0
- ticketwright/_kit/bin/build_ticket_index.py +498 -0
- ticketwright/_kit/bin/enrich_ticket.py +138 -0
- ticketwright/_kit/bin/ingest_index_records.py +149 -0
- ticketwright/_kit/bin/recall.py +268 -0
- ticketwright/_kit/bin/render.sh +54 -0
- ticketwright/_kit/bin/render_and_validate.sh +139 -0
- ticketwright/_kit/bin/selftest.sh +408 -0
- ticketwright/_kit/bin/split_and_export.sh +138 -0
- ticketwright/_kit/bin/verify_stack.sh +74 -0
- ticketwright/_kit/templates/AGENTS.md.tmpl +74 -0
- ticketwright/_kit/templates/gitignore.tmpl +35 -0
- ticketwright/_kit/templates/plan.md.tmpl +22 -0
- ticketwright/_kit/templates/productized-skill/SKILL.md.tmpl +67 -0
- ticketwright/_kit/templates/productized-skill/bin/drift_check.sh +30 -0
- ticketwright/_kit/templates/productized-skill/golden/example.json +13 -0
- ticketwright/_kit/templates/productized-skill/sql/qc.sql.tmpl +21 -0
- ticketwright/_kit/templates/productized-skill/sql/step.sql.tmpl +20 -0
- ticketwright/_kit/templates/productized-skill/templates/README.md.tmpl +20 -0
- ticketwright/_kit/templates/productized-skill/templates/tracker_comment.txt.tmpl +14 -0
- ticketwright/_kit/templates/roles/analyst.md +6 -0
- ticketwright/_kit/templates/roles/engineer.md +6 -0
- ticketwright/_kit/templates/roles/generalist.md +3 -0
- ticketwright/_kit/templates/roles/scientist.md +5 -0
- ticketwright/_kit/templates/spec.md.tmpl +34 -0
- ticketwright/_kit/templates/ticket-README.md.tmpl +29 -0
- ticketwright/cli.py +115 -0
- ticketwright-2.0.0.dist-info/METADATA +139 -0
- ticketwright-2.0.0.dist-info/RECORD +89 -0
- ticketwright-2.0.0.dist-info/WHEEL +4 -0
- ticketwright-2.0.0.dist-info/entry_points.txt +2 -0
- ticketwright-2.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse hook — auto-regenerate tickets/INDEX.md when a ticket folder changes.
|
|
3
|
+
|
|
4
|
+
Fires after Write/Edit. If the edited file lives under tickets/ (and isn't the index
|
|
5
|
+
artifacts themselves), it re-renders tickets/INDEX.md so the catalog always reflects
|
|
6
|
+
reality: a new ticket appears immediately (marked ▱ until curated), and an edited README
|
|
7
|
+
flags its row ⚠ (summary may be stale). LLM-authored summaries are *not* regenerated here
|
|
8
|
+
(a hook can't run the model synchronously) — those refresh at ticket close via
|
|
9
|
+
bin/ingest_index_records.py.
|
|
10
|
+
|
|
11
|
+
Deterministic + fast (re-renders from disk + index_data.json). Writes only when the
|
|
12
|
+
rendered output actually changes, so irrelevant edits produce no churn. Fails open.
|
|
13
|
+
|
|
14
|
+
Wire in .claude/settings.json:
|
|
15
|
+
"hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [
|
|
16
|
+
{ "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/regenerate_ticket_index.py\"" } ] } ] }
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> int:
|
|
27
|
+
try:
|
|
28
|
+
payload = json.load(sys.stdin)
|
|
29
|
+
except (json.JSONDecodeError, ValueError):
|
|
30
|
+
return 0 # nothing to do
|
|
31
|
+
|
|
32
|
+
tool_input = payload.get("tool_input") or {}
|
|
33
|
+
fp = tool_input.get("file_path") or tool_input.get("path") or ""
|
|
34
|
+
if not fp:
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
root = Path(os.environ.get("CLAUDE_PROJECT_DIR") or payload.get("cwd") or ".").resolve()
|
|
38
|
+
tickets_dir = root / "tickets"
|
|
39
|
+
try:
|
|
40
|
+
changed = Path(fp).resolve()
|
|
41
|
+
changed.relative_to(tickets_dir) # raises if not under tickets/
|
|
42
|
+
except (ValueError, OSError):
|
|
43
|
+
return 0 # edit was not under tickets/
|
|
44
|
+
|
|
45
|
+
# Don't react to the *generated* artifacts (else the hook's own write re-triggers it).
|
|
46
|
+
# index_data.json is source, NOT generated — editing the store SHOULD re-render.
|
|
47
|
+
if changed.name in ("INDEX.md", "OBJECTS.md"):
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
sys.path.insert(0, str(root / "bin"))
|
|
51
|
+
try:
|
|
52
|
+
from build_ticket_index import build_rows, render, render_objects # type: ignore
|
|
53
|
+
rows = build_rows(root)
|
|
54
|
+
fresh = {tickets_dir / "INDEX.md": render(rows), tickets_dir / "OBJECTS.md": render_objects(rows)}
|
|
55
|
+
except SystemExit:
|
|
56
|
+
return 0 # malformed index_data.json — surfaced when the agent runs the renderer
|
|
57
|
+
except Exception:
|
|
58
|
+
return 0 # fail open
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
changed_any = False
|
|
62
|
+
for p, txt in fresh.items():
|
|
63
|
+
if (p.read_text() if p.is_file() else None) != txt:
|
|
64
|
+
p.write_bytes(txt.encode("utf-8")); changed_any = True
|
|
65
|
+
if changed_any:
|
|
66
|
+
print("Auto-regenerated tickets/INDEX.md + OBJECTS.md (ticket folder changed). "
|
|
67
|
+
"Run bin/ingest_index_records.py to refresh a ticket's curated summary.")
|
|
68
|
+
except OSError:
|
|
69
|
+
pass
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SessionStart hook — prime every session with the configured stack + AI-layer index.
|
|
3
|
+
|
|
4
|
+
Prints a compact summary (becomes session additionalContext) so the agent always knows
|
|
5
|
+
which tools are wired, which skills/commands exist, and the lifecycle — without anyone
|
|
6
|
+
having to load all of AGENTS.md. This is the always-on, *tiny* slice of context; the
|
|
7
|
+
`/prime-*` commands load the rest on demand.
|
|
8
|
+
|
|
9
|
+
Wire it in settings.json:
|
|
10
|
+
"hooks": { "SessionStart": [ { "hooks": [
|
|
11
|
+
{ "type": "command", "command": "python3 .claude/hooks/session_context.py" } ] } ] }
|
|
12
|
+
|
|
13
|
+
Stdlib only. Fails open (prints nothing) if the kit isn't configured yet.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def project_root() -> Path:
|
|
24
|
+
if os.environ.get("CLAUDE_PROJECT_DIR"):
|
|
25
|
+
return Path(os.environ["CLAUDE_PROJECT_DIR"])
|
|
26
|
+
# hook lives at <root>/.claude/hooks/
|
|
27
|
+
return Path(__file__).resolve().parent.parent.parent
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def scan_stack(stack: Path) -> dict:
|
|
31
|
+
text = stack.read_text(errors="replace")
|
|
32
|
+
out = {}
|
|
33
|
+
m = re.search(r"^\s*key_prefix:\s*([A-Za-z0-9_-]+)", text, re.MULTILINE)
|
|
34
|
+
out["key_prefix"] = m.group(1) if m else "?"
|
|
35
|
+
# tool per seam: find " <seam>:" then the nested " tool: <x>"
|
|
36
|
+
for seam in ("tracker", "warehouse", "chat", "docstore", "vcs"):
|
|
37
|
+
m = re.search(rf"^\s{{2}}{seam}:\s*\n(?:\s+.*\n)*?\s+tool:\s*([A-Za-z0-9_-]+)", text, re.MULTILINE)
|
|
38
|
+
out[seam] = m.group(1) if m else "—"
|
|
39
|
+
return out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main() -> int:
|
|
43
|
+
root = project_root()
|
|
44
|
+
stack = root / ".claude/config/stack.yaml"
|
|
45
|
+
if not stack.is_file():
|
|
46
|
+
return 0 # not configured — say nothing
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
s = scan_stack(stack)
|
|
50
|
+
except OSError:
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
skills = sorted(p.parent.name for p in (root / ".claude/skills").glob("*/SKILL.md")) \
|
|
54
|
+
if (root / ".claude/skills").is_dir() else []
|
|
55
|
+
# Hide deprecated v1 alias stubs — they route to v2 skills and shouldn't re-clutter the surface.
|
|
56
|
+
commands = sorted(
|
|
57
|
+
p.stem for p in (root / ".claude/commands").glob("*.md")
|
|
58
|
+
if "Deprecated" not in p.read_text(errors="replace")[:200]
|
|
59
|
+
) if (root / ".claude/commands").is_dir() else []
|
|
60
|
+
|
|
61
|
+
lines = [
|
|
62
|
+
"## Ticketwright — session context",
|
|
63
|
+
f"Stack ({s['key_prefix']}-tickets): tracker={s['tracker']} · warehouse={s['warehouse']} · "
|
|
64
|
+
f"chat={s['chat']} · docstore={s['docstore']} · vcs={s['vcs']}.",
|
|
65
|
+
"Lifecycle: /ticket (opens + auto-primes context) → /spec-and-build → /review → /ship.",
|
|
66
|
+
]
|
|
67
|
+
if skills:
|
|
68
|
+
lines.append("Skills: " + ", ".join(skills) + ".")
|
|
69
|
+
if commands:
|
|
70
|
+
lines.append("Commands: " + ", ".join(commands) + ".")
|
|
71
|
+
lines.append("Policies enforced: DB writes & external posts require approval (db_write_guard hook + "
|
|
72
|
+
"skill hard-halts); chat defaults to draft; outputs deterministic. See AGENTS.md.")
|
|
73
|
+
print("\n".join(lines))
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
sys.exit(main())
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SessionStart hook — surface the ticket index so Claude recalls prior work.
|
|
3
|
+
|
|
4
|
+
Prints a compact block (becomes session additionalContext): the catalog size, a
|
|
5
|
+
status breakdown, and the most recent tickets, plus a pointer to grep the full
|
|
6
|
+
`tickets/INDEX.md` before starting related work. The authoritative *count* comes from
|
|
7
|
+
discovering ticket folders on disk (so a newly-added, not-yet-enriched ticket still
|
|
8
|
+
counts); the per-ticket detail comes from the enriched `tickets/index_data.json`.
|
|
9
|
+
Stdlib only; fails open (prints nothing) on any error.
|
|
10
|
+
|
|
11
|
+
Wire in .claude/settings.json:
|
|
12
|
+
"hooks": { "SessionStart": [ { "hooks": [
|
|
13
|
+
{ "type": "command", "command": "python3 .claude/hooks/ticket_index_context.py" } ] } ] }
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
RECENT = 12
|
|
23
|
+
STATUS_ORDER = ["Deployed", "Completed", "In Review", "In Progress", "Blocked", "Unknown"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def project_root() -> Path:
|
|
27
|
+
if os.environ.get("CLAUDE_PROJECT_DIR"):
|
|
28
|
+
return Path(os.environ["CLAUDE_PROJECT_DIR"])
|
|
29
|
+
return Path(__file__).resolve().parent.parent.parent # .claude/hooks/ -> root
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def ticket_number(tid: str) -> int:
|
|
33
|
+
m = re.search(r"-(\d+)", tid)
|
|
34
|
+
return int(m.group(1)) if m else 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def discovered_total(root: Path) -> int | None:
|
|
38
|
+
"""True ticket count from the renderer's discovery (cheap globs, no README reads)."""
|
|
39
|
+
try:
|
|
40
|
+
import sys
|
|
41
|
+
sys.path.insert(0, str(root / "bin"))
|
|
42
|
+
from build_ticket_index import discover # type: ignore
|
|
43
|
+
return len(discover(root))
|
|
44
|
+
except Exception:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> int:
|
|
49
|
+
root = project_root()
|
|
50
|
+
data_file = root / "tickets" / "index_data.json"
|
|
51
|
+
index_file = root / "tickets" / "INDEX.md"
|
|
52
|
+
|
|
53
|
+
tickets = []
|
|
54
|
+
if data_file.is_file():
|
|
55
|
+
try:
|
|
56
|
+
loaded = json.loads(data_file.read_text())
|
|
57
|
+
if isinstance(loaded, dict) and isinstance(loaded.get("tickets"), list):
|
|
58
|
+
tickets = [t for t in loaded["tickets"] if isinstance(t, dict)]
|
|
59
|
+
except (OSError, json.JSONDecodeError):
|
|
60
|
+
tickets = []
|
|
61
|
+
|
|
62
|
+
if not tickets:
|
|
63
|
+
# Catalog may exist but not be enriched yet — emit a bare pointer if INDEX.md is there.
|
|
64
|
+
if index_file.is_file():
|
|
65
|
+
print("## Ticket index\nCatalog of prior ticket work: `tickets/INDEX.md` — grep it before "
|
|
66
|
+
"starting related work (same object / stakeholder / report).")
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
total = discovered_total(root) or len(tickets)
|
|
70
|
+
by_status: dict[str, int] = {}
|
|
71
|
+
for t in tickets:
|
|
72
|
+
s = t.get("status") or "Unknown"
|
|
73
|
+
by_status[s] = by_status.get(s, 0) + 1
|
|
74
|
+
extra = sorted(s for s in by_status if s not in STATUS_ORDER)
|
|
75
|
+
status_line = " · ".join(f"{s} {by_status[s]}" for s in (STATUS_ORDER + extra) if by_status.get(s))
|
|
76
|
+
|
|
77
|
+
recent = sorted(tickets, key=lambda t: (t.get("date") or "0000-00-00", ticket_number(t.get("id", ""))),
|
|
78
|
+
reverse=True)[:RECENT]
|
|
79
|
+
|
|
80
|
+
lines = [
|
|
81
|
+
"## Ticket index — recall before starting work",
|
|
82
|
+
f"{total} tickets indexed ({status_line}). Full catalog: `tickets/INDEX.md` — "
|
|
83
|
+
"**grep it for prior work on the same object / stakeholder / report before starting a ticket, and reuse it.**",
|
|
84
|
+
f"Most recent {len(recent)}:",
|
|
85
|
+
]
|
|
86
|
+
for t in recent:
|
|
87
|
+
title = (t.get("title") or "").strip()
|
|
88
|
+
if len(title) > 72:
|
|
89
|
+
title = title[:71].rstrip() + "…"
|
|
90
|
+
d = t.get("date") or "—"
|
|
91
|
+
lines.append(f"- {t.get('owner')}/{t.get('id')} ({d}) — {title}")
|
|
92
|
+
if total > len(tickets):
|
|
93
|
+
lines.append(f"({total - len(tickets)} newer ticket(s) on disk not yet enriched — run the index workflow.)")
|
|
94
|
+
print("\n".join(lines))
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_README": "Rendered to .claude/settings.json by /setup. Wires the kit's hooks (policy enforcement + session priming + ticket-index surfacing/auto-regen), a conservative read-only permission baseline (setup appends stack-specific query/CLI allows), and the stack statusline. The db_write_guard hook turns 'db_write_requires_approval' into a mechanical ask before any destructive warehouse statement.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Bash",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{ "type": "command", "command": "python3 .claude/hooks/db_write_guard.py" }
|
|
9
|
+
]
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"SessionStart": [
|
|
13
|
+
{
|
|
14
|
+
"hooks": [
|
|
15
|
+
{ "type": "command", "command": "python3 .claude/hooks/session_context.py" },
|
|
16
|
+
{ "type": "command", "command": "python3 .claude/hooks/ticket_index_context.py" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"PostToolUse": [
|
|
21
|
+
{
|
|
22
|
+
"matcher": "Write|Edit",
|
|
23
|
+
"hooks": [
|
|
24
|
+
{ "type": "command", "command": "python3 .claude/hooks/regenerate_ticket_index.py" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"permissions": {
|
|
30
|
+
"_README": "Read-only baseline that is safe in any stack. setup appends the chosen warehouse/tracker/vcs read verbs (e.g. Bash(snow sql:*)) here; the db_write_guard hook still gates destructive statements even when the CLI is allowed.",
|
|
31
|
+
"allow": [
|
|
32
|
+
"Bash(git status:*)",
|
|
33
|
+
"Bash(git diff:*)",
|
|
34
|
+
"Bash(git log:*)",
|
|
35
|
+
"Bash(git branch:*)",
|
|
36
|
+
"Bash(ls:*)",
|
|
37
|
+
"Bash(cat:*)",
|
|
38
|
+
"Bash(tree:*)",
|
|
39
|
+
"Bash(rg:*)",
|
|
40
|
+
"Bash(grep:*)",
|
|
41
|
+
"Bash(fd:*)",
|
|
42
|
+
"Bash(find:*)",
|
|
43
|
+
"Bash(jq:*)",
|
|
44
|
+
"Bash(yq:*)",
|
|
45
|
+
"Bash(python3 bin/build_ticket_index.py:*)",
|
|
46
|
+
"Bash(python3 bin/ingest_index_records.py:*)",
|
|
47
|
+
"Bash(python3 bin/recall.py:*)",
|
|
48
|
+
"Bash(python3 bin/enrich_ticket.py:*)",
|
|
49
|
+
"Bash(bash bin/verify_stack.sh:*)",
|
|
50
|
+
"Bash(bash bin/render.sh:*)",
|
|
51
|
+
"Bash(bash bin/selftest.sh:*)"
|
|
52
|
+
],
|
|
53
|
+
"deny": []
|
|
54
|
+
},
|
|
55
|
+
"statusLine": {
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": ".claude/statusline.sh"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: productize
|
|
3
|
+
description: Turn a recurring "clone-the-last-ticket" workflow into a parameterized skill of its own — phased pipeline, QC checkpoints, golden-replay test, hard-halt before external posts.
|
|
4
|
+
argument-hint: "<workflow name> (e.g. monthly vendor reconciliation)"
|
|
5
|
+
allowed-tools: [Read, Write, Edit, Bash, Glob, AskUserQuestion]
|
|
6
|
+
disable-model-invocation: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# /productize
|
|
10
|
+
|
|
11
|
+
The meta-skill (`commandify_everything`). When a task recurs (quarterly sale, monthly report,
|
|
12
|
+
periodic reconciliation), this stamps out a new **folder skill** with the productized skeleton:
|
|
13
|
+
a phased pipeline with hard halts, parameterized SQL/steps, a QC checkpoint, a golden-replay
|
|
14
|
+
regression test, and a hard halt before any external post. `/refresh index` + the `--recurring`
|
|
15
|
+
flag of `bin/build_ticket_index.py` surface candidates ("this object shows up in 7 tickets").
|
|
16
|
+
|
|
17
|
+
## Phase 1 — Interview the workflow (reduce_assumptions)
|
|
18
|
+
Ask (AskUserQuestion) for:
|
|
19
|
+
- **Name & cadence** (→ kebab skill name) and the **parameters** that change each run (e.g.
|
|
20
|
+
`--period`, `--as-of-date`, `--ticket`, an input file) — with formats + validation rules.
|
|
21
|
+
- **Steps**: the ordered phases. For each, what it reads, what it writes, and whether it has
|
|
22
|
+
external side effects or DB writes (those become hard-halt / approval gates).
|
|
23
|
+
- **QC checks**: per check — what it tests, and its tier (hard-halt / warn / info) + remediation hint.
|
|
24
|
+
- **Determinism anchor**: the known-good prior run to use as the **golden fixture** (counts/totals
|
|
25
|
+
to assert; the byte-identical output file to diff against).
|
|
26
|
+
|
|
27
|
+
## Phase 2 — Stamp from the template
|
|
28
|
+
1. Copy `templates/productized-skill/` → `.claude/skills/<name>/` (SKILL.md + `sql/ templates/
|
|
29
|
+
bin/ golden/`).
|
|
30
|
+
2. Render `SKILL.md.tmpl` with the interview answers into the canonical phase shape:
|
|
31
|
+
**Phase 0** pre-flight (validate params + `verify_stack` for touched seams + an object drift
|
|
32
|
+
check → halt-on-fail) → **render & run** parameterized steps → **QC** atomic checkpoint
|
|
33
|
+
(hard-halt tiers) → **export** (deterministic `ORDER BY`) → **render docs** (README +
|
|
34
|
+
tracker-comment draft) → **HARD HALT for human review** → **post-review** (the external posts,
|
|
35
|
+
in a separate invocation).
|
|
36
|
+
3. Write the `{{token}}` step files into `sql/` (or `bin/` for non-SQL), the output templates into
|
|
37
|
+
`templates/`, and seed `golden/<run>.json` with the determinism anchor. **Follow the two SQL
|
|
38
|
+
authoring rules in [authoring.md](authoring.md)** — a render gate enforces both.
|
|
39
|
+
4. Scaffold `bin/drift_check.sh` (confirms every object the workflow reads is still reachable —
|
|
40
|
+
the Phase-0 catch for relocations).
|
|
41
|
+
|
|
42
|
+
## Phase 3 — Wire to the stack & self-test
|
|
43
|
+
5. The stamped skill reads `.claude/config/stack.yaml` and resolves all I/O through adapters (no
|
|
44
|
+
hardcoded tools in the orchestration). Confirm the SKILL.md prose is tool-neutral:
|
|
45
|
+
`!grep -REn "acli|snow |slack|gh " .claude/skills/<name>/SKILL.md || echo OK`. (The leaf
|
|
46
|
+
`bin/drift_check.sh` is allowed to name your warehouse CLI — it's the tool-bound probe.)
|
|
47
|
+
6. Wire the **render gate** (`bin/render_and_validate.sh`) and **export helpers**
|
|
48
|
+
(`bin/split_and_export.sh`) into the stamped phases so authoring defects fail *closed*, not
|
|
49
|
+
silently — details and rationale in [authoring.md](authoring.md).
|
|
50
|
+
7. Document the **golden test** + **failure-mode tests** in the new SKILL.md (how to replay the
|
|
51
|
+
known run and assert the fixture; how each Phase-0/QC/render-gate halt is expected to trigger).
|
|
52
|
+
8. **Report** the new skill path, its parameters, and how to run its golden test.
|
|
53
|
+
|
|
54
|
+
## Output shape (what every stamped skill guarantees)
|
|
55
|
+
Parameterized · phased · QC-gated · golden-tested · hard-halts before external posts · idempotent
|
|
56
|
+
where possible (re-run converges) · records a rollback key for any DB write. Heavy pulls run in the
|
|
57
|
+
**background**, not the foreground — mark slow phases at stamp time so the operator launches them
|
|
58
|
+
detached from the start.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SQL-template authoring rules + the render gate
|
|
2
|
+
|
|
3
|
+
Two hard rules for every `sql/*.tmpl` in a productized skill. Both have burned real runs; the
|
|
4
|
+
render gate (`bin/render_and_validate.sh`) enforces both mechanically.
|
|
5
|
+
|
|
6
|
+
## Rule 1 — Never put a token in a SQL comment
|
|
7
|
+
The renderer expands tokens everywhere, **including inside a `--` comment**. A multi-line value
|
|
8
|
+
(e.g. a long `VALUES` list) then spills past the `--` and the continuation rows are parsed as bare
|
|
9
|
+
SQL → compile error. Describe parameters in *prose*, not tokens:
|
|
10
|
+
|
|
11
|
+
```sql
|
|
12
|
+
-- step SQL — params described in prose, never as tokens ✓
|
|
13
|
+
-- NOTE: substitutes {{vals}} before the run ✗ (gate rejects this)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Rule 2 — Quote any token used as a SQL string/date literal
|
|
17
|
+
`SET d = '{{asof}}'::DATE`, not `= {{asof}}`. Unquoted, `= 2026-06-30` is read as *arithmetic*
|
|
18
|
+
(= 1990), not a date → **silent wrong results**. The quotes live in the template, never injected at
|
|
19
|
+
call time. (The gate warns by default, fails under `--strict`.)
|
|
20
|
+
|
|
21
|
+
## The render gate — `bin/render_and_validate.sh`
|
|
22
|
+
Run it on every render (in the stamped skill's Phase 0/1, and once at stamp time). Stdlib-only, no
|
|
23
|
+
warehouse round-trip — cheap insurance. It asserts: zero leftover tokens · no token in a comment ·
|
|
24
|
+
balanced single-quotes / parens · flags an unquoted SQL literal. Halt on any error.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bash bin/render_and_validate.sh sql/step.sql.tmpl asof=2026-06-30 src=T [--strict]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Export helpers — `bin/split_and_export.sh`
|
|
31
|
+
Keeps multi-deliverable skills from re-improvising CSV plumbing:
|
|
32
|
+
- `--strip-only <file>` robustly drops the multi-statement CLI preamble (through the last
|
|
33
|
+
`Statement executed successfully.`, then leading blanks) so the header is row 1;
|
|
34
|
+
- split mode turns one multi-`SELECT` file (delimited by `-- Query N` markers) into N runnable
|
|
35
|
+
files — each carrying the shared preamble — and `--run` executes each via *your* warehouse verb
|
|
36
|
+
and strips its CSV.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: refresh
|
|
3
|
+
description: Maintain the repo's knowledge — `index` rebuilds the ticket catalog (INDEX.md), `context` rebuilds the domain knowledge pack (documentation/). Day-to-day, hooks keep these fresh automatically.
|
|
4
|
+
argument-hint: index [--all | TICKET-ID ...] | context [--refresh] [schema-or-scope] | all
|
|
5
|
+
allowed-tools: [Read, Write, Bash, Glob, Grep]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /refresh
|
|
9
|
+
|
|
10
|
+
One maintenance skill for the two knowledge stores the other skills read:
|
|
11
|
+
|
|
12
|
+
- **`/refresh index`** — the ticket catalog: `tickets/INDEX.md` (every ticket, status, one-line
|
|
13
|
+
summary, tags, cross-refs) + `tickets/OBJECTS.md` (object → tickets reverse map). This is what
|
|
14
|
+
`/ticket`'s recall mines. Details: [index.md](index.md).
|
|
15
|
+
- **`/refresh context`** — the domain knowledge pack in `documentation/`: data catalog, DDL, ERD,
|
|
16
|
+
dependency graph, business glossary. This is what `/ticket`'s domain + warehouse priming reads.
|
|
17
|
+
Details: [context-pack.md](context-pack.md).
|
|
18
|
+
- **`/refresh all`** — both.
|
|
19
|
+
|
|
20
|
+
## You rarely need this
|
|
21
|
+
Day-to-day the index maintains itself: the PostToolUse hook re-renders `INDEX.md` on every ticket
|
|
22
|
+
folder change, the SessionStart hook surfaces the catalog, and `/ship` curates each ticket's
|
|
23
|
+
summary at close. Invoke `/refresh` to:
|
|
24
|
+
- **bootstrap** an existing backlog (`/refresh index --all` — render every ticket, then enrich);
|
|
25
|
+
- **re-enrich** a batch after bulk edits (`/refresh index ID1 ID2 …`);
|
|
26
|
+
- **build or update the knowledge pack** (`/refresh context`, scoped to a schema if given;
|
|
27
|
+
`--refresh` updates in place — overwrite, don't sprawl).
|
|
28
|
+
|
|
29
|
+
## Mode: `index`
|
|
30
|
+
Follow [index.md](index.md): render deterministically (`bin/build_ticket_index.py` — stdlib, no
|
|
31
|
+
model), enrich the un-enriched/stale set (the model writes one curated record per ticket, ingested
|
|
32
|
+
via `bin/ingest_index_records.py`), re-render, then verify with `--check` and commit all three
|
|
33
|
+
index files together (`INDEX.md`, `OBJECTS.md`, `index_data.json`).
|
|
34
|
+
|
|
35
|
+
## Mode: `context`
|
|
36
|
+
Follow [context-pack.md](context-pack.md): verify the warehouse seam, introspect via the adapter
|
|
37
|
+
(inventory → DDL → dependencies → usage ranking), generate `data_catalog.md` / `erd.md` /
|
|
38
|
+
`glossary.md` into `documentation/` with freshness stamps, and update
|
|
39
|
+
`documentation/AI_LAYER_INDEX.md`. Don't invent business meaning — flag glossary gaps for a human
|
|
40
|
+
(`reduce_assumptions`).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# `/refresh context` — the domain knowledge pack in detail
|
|
2
|
+
|
|
3
|
+
Builds the on-demand knowledge base that `/ticket`'s domain + warehouse priming reads, so
|
|
4
|
+
day-to-day tickets prime *slices* instead of re-discovering the warehouse every time. Reads
|
|
5
|
+
`.claude/config/stack.yaml`; introspects via the warehouse adapter, so it works on Snowflake,
|
|
6
|
+
BigQuery, Databricks, etc.
|
|
7
|
+
|
|
8
|
+
## Phase 0 — Setup
|
|
9
|
+
1. Read `stack.yaml`; verify the warehouse seam (halt with auth notes if unreachable). Scope to
|
|
10
|
+
the given schema/dataset or the whole configured warehouse. `--refresh` updates an existing
|
|
11
|
+
pack in place (overwrite, don't sprawl).
|
|
12
|
+
|
|
13
|
+
## Phase 1 — Introspect (read-only, via adapter)
|
|
14
|
+
2. **Object inventory** — list tables/views/dynamic-tables in scope, using whatever
|
|
15
|
+
object-inventory idiom the adapter's `dialect_notes` specifies (e.g. an
|
|
16
|
+
`INFORMATION_SCHEMA`/`system` query).
|
|
17
|
+
3. **DDL dump** — `warehouse.describe` each object; write authoritative DDL to
|
|
18
|
+
`documentation/erd/ddl_<schema>_<object>.sql`.
|
|
19
|
+
4. **Dependency graph** — derive what-reads-what (lineage views) → an object→object edge list.
|
|
20
|
+
5. **Usage ranking** — rank objects by how often they appear across `tickets/**` (`grep -roh …`)
|
|
21
|
+
so the catalog leads with what's actually used.
|
|
22
|
+
|
|
23
|
+
## Phase 2 — Generate the pack (into documentation/)
|
|
24
|
+
6. **`data_catalog.md`** — per object: purpose, grain, key columns, join keys + cast/filter rules
|
|
25
|
+
(from `dialect_notes`), source layer, usage rank.
|
|
26
|
+
7. **`erd.md`** — the relationships + a high-level diagram; per-object DDL lives under `erd/`.
|
|
27
|
+
8. **`glossary.md`** — business terms, status taxonomies, calculation rules, known data-quality
|
|
28
|
+
caveats. Seed from existing READMEs/runbooks; **flag gaps for a human to fill** — don't invent
|
|
29
|
+
business meaning (`reduce_assumptions`).
|
|
30
|
+
9. Add a freshness stamp + scope to each file (re-check if >30 days old).
|
|
31
|
+
|
|
32
|
+
## Phase 3 — Index & report
|
|
33
|
+
10. Update `documentation/AI_LAYER_INDEX.md` with the pack's contents. Report what was generated,
|
|
34
|
+
the top-N most-used objects, and which glossary sections need human input.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# `/refresh index` — the ticket catalog in detail
|
|
2
|
+
|
|
3
|
+
Two layers, kept separate so the catalog is reproducible and CI/pre-commit-safe:
|
|
4
|
+
|
|
5
|
+
- **`bin/build_ticket_index.py`** — deterministic, LLM-free renderer. Discovers every ticket folder
|
|
6
|
+
(tracker keys from `stack.yaml` `key_prefixes`/`key_prefix`), merges curated fields from
|
|
7
|
+
`tickets/index_data.json`, and writes `INDEX.md` + `OBJECTS.md` (object → tickets reverse index;
|
|
8
|
+
objects = enrichment ∪ a deterministic grep of each ticket's SQL). `--check` (staleness gate,
|
|
9
|
+
covers both files) · `--stats` (coverage) · `--recurring` (frequently-touched objects —
|
|
10
|
+
productization candidates).
|
|
11
|
+
- **`tickets/index_data.json`** — the curated store (title/status/date/summary/tags/cross_refs/
|
|
12
|
+
objects + each README's content hash). This skill writes it.
|
|
13
|
+
|
|
14
|
+
## Phase 0 — Preflight
|
|
15
|
+
1. Confirm `stack.yaml` has `key_prefix`/`key_prefixes` and (optionally) `ticket_url_template`.
|
|
16
|
+
2. `python3 "${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR}/bin/build_ticket_index.py" --stats` — how
|
|
17
|
+
many tickets are discovered, enriched, un-enriched (`▱`), stale (`⚠`).
|
|
18
|
+
|
|
19
|
+
## Phase 1 — Render (always cheap, no model)
|
|
20
|
+
3. `python3 "${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR}/bin/build_ticket_index.py"` — every ticket
|
|
21
|
+
on disk now has a row (un-enriched ones get a deterministic title + first-paragraph summary,
|
|
22
|
+
marked `▱`).
|
|
23
|
+
|
|
24
|
+
## Phase 2 — Enrich (the model half)
|
|
25
|
+
4. Decide scope: the given ticket id(s); `--all` for the whole backlog; or default to the
|
|
26
|
+
un-enriched/stale set from `--stats`.
|
|
27
|
+
5. For each target ticket, read its `README.md` and write ONE record:
|
|
28
|
+
`{id, owner, title, status, date, summary (<=180 chars, lead with what was delivered + key
|
|
29
|
+
numbers), tags (1-4 kebab-case), cross_refs (other ticket ids), objects (qualified data objects
|
|
30
|
+
the ticket read/wrote, e.g. SCHEMA.VIEW; [] if none)}`. **id/owner come from the folder, not
|
|
31
|
+
your judgment.** Status vocab: Completed · Deployed · In Review · In Progress · Blocked · Unknown.
|
|
32
|
+
6. Collect them and upsert + re-render:
|
|
33
|
+
```bash
|
|
34
|
+
echo '{"records":[ ... ]}' | python3 "${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR}/bin/ingest_index_records.py" --from-json -
|
|
35
|
+
python3 "${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR}/bin/build_ticket_index.py"
|
|
36
|
+
```
|
|
37
|
+
(Claude-Code convenience: `python3 "${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR}/bin/enrich_ticket.py" <ID>`
|
|
38
|
+
does steps 5–6 for one ticket via `claude -p`. The inline path above is agent-agnostic.)
|
|
39
|
+
|
|
40
|
+
## Phase 3 — Verify
|
|
41
|
+
7. `python3 "${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR}/bin/build_ticket_index.py" --check` must
|
|
42
|
+
pass (INDEX.md + OBJECTS.md == fresh render).
|
|
43
|
+
8. Report: total tickets, status breakdown, any still un-enriched. Commit `tickets/INDEX.md` +
|
|
44
|
+
`tickets/OBJECTS.md` + `tickets/index_data.json` (all three — `--check` gates the two generated
|
|
45
|
+
files).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: review
|
|
3
|
+
description: Independent quality review of a ticket's deliverables — re-runs queries and walks a tiered validation pyramid to an APPROVE / REQUEST-CHANGES verdict. Run before shipping.
|
|
4
|
+
argument-hint: <ticket-id> [--deep]
|
|
5
|
+
allowed-tools: [Read, Bash, Glob, Grep, Agent]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /review
|
|
9
|
+
|
|
10
|
+
The **check** step of the lifecycle. An *independent* second pass over a ticket's deliverables —
|
|
11
|
+
re-runs queries, walks a tiered validation pyramid, sweeps for anti-patterns, and returns an
|
|
12
|
+
APPROVE / REQUEST-CHANGES verdict. Reads `.claude/config/stack.yaml`; warehouse specifics come from
|
|
13
|
+
the adapter's `dialect_notes`, so the same review runs on Snowflake, BigQuery, or Databricks.
|
|
14
|
+
Read-only: it reviews and re-runs, it does not edit code (the build owns fixes).
|
|
15
|
+
|
|
16
|
+
## Phase 0 — Setup
|
|
17
|
+
1. Read `stack.yaml`; verify the warehouse seam (halt with auth notes if unreachable). Load
|
|
18
|
+
`seams.warehouse.dialect_notes` from the adapter — it parameterizes the lint layer.
|
|
19
|
+
2. Read the ticket README, the spec (if any), and list `final_deliverables/` + `qc_queries/`.
|
|
20
|
+
|
|
21
|
+
## The validation pyramid (bottom = cheap/automated, top = human)
|
|
22
|
+
|
|
23
|
+
**① Dialect lint** (static, per `dialect_notes`)
|
|
24
|
+
- `= NULL` → must be `IS NULL`; missing div-by-zero guard; `SELECT *` in deliverables; functions on
|
|
25
|
+
filtered columns; implicit cross-source type mismatches (missing `CAST`); hardcoded values that
|
|
26
|
+
should be parameters; missing required schema/instance filters; `LEFT JOIN` predicate in `WHERE`
|
|
27
|
+
that silently becomes an inner join; `NOT IN` with nullable columns; `UNION` vs `UNION ALL`.
|
|
28
|
+
|
|
29
|
+
**② Counts & dedup** (re-run independently)
|
|
30
|
+
- Re-run the row-count; compare to the documented count. **Duplicate detection is the primary test:**
|
|
31
|
+
`COUNT(*)` vs `COUNT(DISTINCT <grain key>)` — any gap is a halt until explained.
|
|
32
|
+
- NULL-rate on critical/key columns; value-range sanity (no negative amounts where impossible).
|
|
33
|
+
|
|
34
|
+
**③ Cross-source reconciliation**
|
|
35
|
+
- Join-match rates (unmatched `LEFT JOIN` rows quantified, not hidden). Date ranges within scope.
|
|
36
|
+
- Reconcile totals against the source of truth (e.g. SUM vs the input file/feed) within tolerance.
|
|
37
|
+
|
|
38
|
+
**④ Independent re-run + anti-pattern sweep**
|
|
39
|
+
- Re-execute the main deliverable query end-to-end; diff results to the committed output (byte-level
|
|
40
|
+
for CSVs — `deterministic_outputs` requires explicit `ORDER BY`).
|
|
41
|
+
- Sweep the full anti-pattern set (correctness, performance, data-quality, dialect-specific,
|
|
42
|
+
maintainability). Classify each finding Critical / Should-fix / Review.
|
|
43
|
+
|
|
44
|
+
**⑤ Human sign-off**
|
|
45
|
+
- Output format check (CSV headers row 1, no preamble/blank rows; filenames carry record counts).
|
|
46
|
+
- README completeness (assumptions enumerated, QC results, business context). Flag for the human.
|
|
47
|
+
|
|
48
|
+
## Tiers & halting
|
|
49
|
+
- **Hard-halt** findings (Critical, count mismatch, dup gap, reconciliation break) → verdict
|
|
50
|
+
REQUEST-CHANGES with structured remediation.
|
|
51
|
+
- **Warn** findings (perf, style) → list but don't block.
|
|
52
|
+
- **Info** (distributions) → record.
|
|
53
|
+
|
|
54
|
+
## Deep mode (`--deep`) — adversarial panel
|
|
55
|
+
For high-blast-radius work (compliance/regulatory pulls, irreversible writes), replace the single
|
|
56
|
+
pass with a panel, using the host agent's own subagents (the `Agent` tool):
|
|
57
|
+
1. **Fan out** one independent `qc-reviewer` subagent per pyramid layer (① dialect-lint ·
|
|
58
|
+
② counts&dedup · ③ cross-source reconcile · ④ re-run + anti-pattern sweep) — each scoped to its
|
|
59
|
+
layer, re-running its own queries, returning findings only. Run them in parallel.
|
|
60
|
+
2. **Adversarially verify** every reported finding before it counts: a second pass re-reads the
|
|
61
|
+
cited `file:line` / re-runs the query and rules each finding confirmed / false-positive /
|
|
62
|
+
**uncertain**. A finding it cannot reproduce is **uncertain, not dismissed** — silently dropping
|
|
63
|
+
a Critical / count / reconcile finding is the expensive failure. Only a clear, demonstrated
|
|
64
|
+
non-issue is ruled false-positive.
|
|
65
|
+
3. **Synthesize** confirmed findings (deduped across layers) into one verdict — and **carry
|
|
66
|
+
uncertain findings into the verdict too**: any uncertain Critical / count / reconcile finding
|
|
67
|
+
forces REQUEST-CHANGES (or explicit human sign-off), never a silent APPROVE.
|
|
68
|
+
|
|
69
|
+
Default (no flag) is the single `qc-reviewer` pass. `/ticket` recommends `--deep` automatically for
|
|
70
|
+
high-risk tickets (external/compliance deliverables, irreversible writes) — reach for it whenever
|
|
71
|
+
the cost of a missed defect is high.
|
|
72
|
+
|
|
73
|
+
## Phase N — Verdict
|
|
74
|
+
Emit the structured report (Summary · pyramid results per layer · findings by severity ·
|
|
75
|
+
verification queries run · **APPROVE** or **REQUEST-CHANGES**). Save it into the ticket's
|
|
76
|
+
`qc_queries/` for the audit trail. APPROVE ⇒ recommend `/ship <id>`.
|