workflow-toolkit 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/ponytail/SKILL.md +111 -0
- package/.agents/skills/ponytail-audit/SKILL.md +37 -0
- package/.agents/skills/ponytail-debt/SKILL.md +41 -0
- package/.agents/skills/ponytail-gain/SKILL.md +47 -0
- package/.agents/skills/ponytail-help/SKILL.md +70 -0
- package/.agents/skills/ponytail-review/SKILL.md +52 -0
- package/.agents/skills/prompt-review/SKILL.md +60 -0
- package/.agents/skills/wtk/SKILL.md +64 -0
- package/.agents/skills/wtk/references/artifacts.md +82 -0
- package/.agents/skills/wtk/references/evidence.md +68 -0
- package/.agents/skills/wtk/references/git.md +75 -0
- package/.agents/skills/wtk/references/test-contract.md +76 -0
- package/.agents/skills/wtk/references/validation.md +127 -0
- package/.agents/skills/wtk-config/SKILL.md +102 -0
- package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
- package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
- package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
- package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
- package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
- package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
- package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
- package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
- package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
- package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
- package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
- package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
- package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
- package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
- package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
- package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
- package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
- package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
- package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
- package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
- package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
- package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
- package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
- package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
- package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
- package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
- package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
- package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
- package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
- package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
- package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
- package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
- package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
- package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
- package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
- package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
- package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
- package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
- package/.agents/skills/wtk-discover/SKILL.md +270 -0
- package/.agents/skills/wtk-discover/references/document-format.md +149 -0
- package/.agents/skills/wtk-implement/SKILL.md +95 -0
- package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
- package/.agents/skills/wtk-implement/references/screens.md +129 -0
- package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
- package/.agents/skills/wtk-implement/references/verify.md +297 -0
- package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
- package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
- package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
- package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
- package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
- package/.agents/skills/wtk-lean/NOTICE.md +18 -0
- package/.agents/skills/wtk-lean/SKILL.md +137 -0
- package/.agents/skills/wtk-lean/references/build.md +168 -0
- package/.agents/skills/wtk-lean/references/checks.md +253 -0
- package/.agents/skills/wtk-lean/references/memory.md +156 -0
- package/.agents/skills/wtk-lean/references/plan.md +514 -0
- package/.agents/skills/wtk-lean/references/verify.md +336 -0
- package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
- package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
- package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
- package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
- package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
- package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
- package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
- package/.agents/skills/wtk-plan/SKILL.md +102 -0
- package/.agents/skills/wtk-plan/references/document-format.md +222 -0
- package/.agents/skills/wtk-qa/SKILL.md +14 -0
- package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
- package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
- package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
- package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
- package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
- package/.agents/skills/wtk-ship/SKILL.md +49 -0
- package/.agents/skills/wtk-ship/remediation.py +170 -0
- package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
- package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
- package/.wtk.toml.example +93 -0
- package/AGENTS.md +129 -0
- package/NOTICE.md +12 -0
- package/README.md +440 -0
- package/bin/wtk.js +25 -0
- package/docs/toolkit/README.md +56 -0
- package/docs/toolkit/decisions.md +61 -0
- package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
- package/docs/toolkit/guidelines/DX.md +67 -0
- package/docs/toolkit/guidelines/FRONTEND.md +73 -0
- package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
- package/docs/toolkit/guidelines/MODELING.md +134 -0
- package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
- package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
- package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
- package/docs/toolkit/guidelines/SECURITY.md +160 -0
- package/docs/toolkit/guidelines/UI-UX.md +117 -0
- package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
- package/docs/toolkit/guidelines.md +50 -0
- package/docs/toolkit/loop.md +77 -0
- package/docs/toolkit/purpose.md +51 -0
- package/docs/toolkit/repository-intelligence.md +53 -0
- package/docs/toolkit/reviews.md +77 -0
- package/knowledge/AGENTS.md +316 -0
- package/knowledge/raw/README.md +17 -0
- package/package.json +74 -0
- package/scripts/install_security_skills.py +816 -0
- package/scripts/installer/engine.js +254 -0
- package/scripts/installer/knowledge.js +28 -0
- package/scripts/installer/packets.js +77 -0
- package/scripts/installer/terminal.js +98 -0
- package/scripts/installer/transaction.js +75 -0
- package/skills-lock.json +92 -0
- package/templates/adoption/agents/core.md +23 -0
- package/templates/adoption/agents/quality.md +3 -0
- package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
- package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
- package/templates/adoption/knowledge/wiki/design/index.md +3 -0
- package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
- package/templates/adoption/knowledge/wiki/index.md +3 -0
- package/templates/adoption/knowledge/wiki/log.md +3 -0
- package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
- package/templates/adoption/knowledge/wiki/product/index.md +3 -0
- package/templates/adoption/knowledge/wiki/research/index.md +3 -0
- package/templates/adoption/product/AGENT-CONTEXT.md +25 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Deep-review plan gate (bootstrap helper; writes only under --out).
|
|
3
|
+
|
|
4
|
+
Validates plan.json cohorts against the manifest (every selected file owned
|
|
5
|
+
exactly once; hunk_scope slices line-exact; size caps), always prepares Graft context,
|
|
6
|
+
conditionally prepares one Graphify context, and renders every reviewer and sweep prompt from assets/PROMPT.md —
|
|
7
|
+
injecting only the rules whose scope globs match that cohort's files — and
|
|
8
|
+
materializes jobs.json, the work
|
|
9
|
+
contract every execution engine runs.
|
|
10
|
+
|
|
11
|
+
Prompt consistency is enforced here: the build fails when a template lost a
|
|
12
|
+
mandatory placeholder or a rendered prompt still carries an unfilled one.
|
|
13
|
+
|
|
14
|
+
Requires in <out>: manifest.json and knowledge.json (bootstrap-authored),
|
|
15
|
+
context-pack.md, rules.json, and plan.json (orchestrator-authored).
|
|
16
|
+
Exit codes: 0 ok, 1 validation failure or missing artifact.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from collections import Counter, defaultdict
|
|
26
|
+
from math import ceil
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
sys.dont_write_bytecode = True # keep the tracked skill tree free of __pycache__
|
|
30
|
+
|
|
31
|
+
from _common import (
|
|
32
|
+
ASSETS_DIR,
|
|
33
|
+
glob_to_regex,
|
|
34
|
+
hunk_text,
|
|
35
|
+
load_schema,
|
|
36
|
+
manifest_selected,
|
|
37
|
+
read_json,
|
|
38
|
+
rel,
|
|
39
|
+
repo_root,
|
|
40
|
+
skill_rel,
|
|
41
|
+
write_json,
|
|
42
|
+
)
|
|
43
|
+
from graft_context import prepare_graft_context
|
|
44
|
+
from graphify_context import prepare_graphify_context
|
|
45
|
+
|
|
46
|
+
DEFAULT_MAX_COHORT_FILES = 100
|
|
47
|
+
MAX_COHORT_CHANGED_LINES = 6000
|
|
48
|
+
TARGET_COHORT_CHANGED_LINES = 400
|
|
49
|
+
|
|
50
|
+
REVIEWER_PLACEHOLDERS = {
|
|
51
|
+
"cohort_name", "risk", "target", "file_list", "scope_instruction", "context",
|
|
52
|
+
"taxonomy", "rules_block", "diff_command", "base", "output", "schema",
|
|
53
|
+
"lane_instruction", "coverage_contract", "graft_context", "prior_findings",
|
|
54
|
+
}
|
|
55
|
+
SWEEP_PLACEHOLDERS = {
|
|
56
|
+
"sweep_key", "lens", "target", "context", "manifest", "taxonomy",
|
|
57
|
+
"diff_command", "output", "schema", "rules_block",
|
|
58
|
+
"coverage_contract", "graft_context",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
DEFAULT_LENSES = {
|
|
62
|
+
"contracts": (
|
|
63
|
+
"MISSION: prove changed contracts co-ship across implementation, clients, and generated artifacts. "
|
|
64
|
+
"FOCUS: breaking shapes, defaults, requiredness, and version drift. REPORT GATE: trace a changed "
|
|
65
|
+
"producer contract to a concrete incompatible consumer."
|
|
66
|
+
),
|
|
67
|
+
"security": (
|
|
68
|
+
"MISSION: trace attacker-controlled input to impact. FOCUS: injection, authn/authz gaps, secret "
|
|
69
|
+
"leakage, cross-tenant access, and unsafe input handling. REPORT GATE: name the controllable input, "
|
|
70
|
+
"missing boundary, and reachable impact."
|
|
71
|
+
),
|
|
72
|
+
"migrations": (
|
|
73
|
+
"MISSION: prove schema and code can roll forward safely. FOCUS: destructive operations, missing model "
|
|
74
|
+
"migrations, ordering, identity, and compatibility windows. REPORT GATE: name the database state and "
|
|
75
|
+
"operation that loses data or breaks a deployed version."
|
|
76
|
+
),
|
|
77
|
+
"consistency": (
|
|
78
|
+
"MISSION: prove a cross-file change is complete. FOCUS: incomplete renames, sibling paths that share an "
|
|
79
|
+
"invariant, and duplicated fixes. REPORT GATE: connect every missed occurrence to the same changed invariant."
|
|
80
|
+
),
|
|
81
|
+
"config": (
|
|
82
|
+
"MISSION: trace configuration from declaration through defaulting to consumption. FOCUS: unwired keys, "
|
|
83
|
+
"dead flags, undocumented public settings, and default mismatches. REPORT GATE: name the runtime path "
|
|
84
|
+
"where the configured value is ignored or misread."
|
|
85
|
+
),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
REMOVED_SWEEPS = {"tests": "test adequacy", "spec-parity": "spec parity"}
|
|
89
|
+
|
|
90
|
+
PLACEHOLDER_RE = re.compile(r"\{\{([a-z_]+)\}\}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def positive_int(value: str) -> int:
|
|
94
|
+
parsed = int(value)
|
|
95
|
+
if parsed < 1:
|
|
96
|
+
raise argparse.ArgumentTypeError("must be at least 1")
|
|
97
|
+
return parsed
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_template(name: str) -> str:
|
|
101
|
+
text = (ASSETS_DIR / "PROMPT.md").read_text(encoding="utf-8")
|
|
102
|
+
match = re.search(
|
|
103
|
+
rf"<!-- template:{name} -->\n(.*?)\n<!-- /template -->", text, re.S
|
|
104
|
+
)
|
|
105
|
+
if match is None:
|
|
106
|
+
raise RuntimeError(f"assets/PROMPT.md has no `<!-- template:{name} -->` block")
|
|
107
|
+
return match.group(1)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def render_template(name: str, template: str, required: set[str], values: dict[str, str]) -> str:
|
|
111
|
+
present = set(PLACEHOLDER_RE.findall(template))
|
|
112
|
+
missing = required - present
|
|
113
|
+
if missing:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
f"assets/PROMPT.md template `{name}` lost mandatory placeholder(s): {sorted(missing)}"
|
|
116
|
+
)
|
|
117
|
+
unknown = present - set(values)
|
|
118
|
+
if unknown:
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
f"template `{name}` uses placeholder(s) with no value: {sorted(unknown)}"
|
|
121
|
+
)
|
|
122
|
+
rendered = PLACEHOLDER_RE.sub(lambda m: values[m.group(1)], template)
|
|
123
|
+
leftover = PLACEHOLDER_RE.findall(rendered)
|
|
124
|
+
if leftover:
|
|
125
|
+
raise RuntimeError(f"template `{name}` rendered with unfilled placeholder(s): {leftover}")
|
|
126
|
+
return rendered
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def canonical_hunk(hunk: dict) -> tuple[int, int, str]:
|
|
130
|
+
return int(hunk["start"]), int(hunk["lines"]), str(hunk.get("side", "new"))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_registry(registry: dict, knowledge: dict, selected: dict[str, dict]) -> list[str]:
|
|
134
|
+
errors = []
|
|
135
|
+
rules = registry.get("rules", [])
|
|
136
|
+
if sorted(knowledge.get("selected_paths", [])) != sorted(selected):
|
|
137
|
+
errors.append("knowledge.json: selected paths are stale; rerun build_knowledge.py")
|
|
138
|
+
expected_sources = {source["path"]: source for source in knowledge.get("sources", [])}
|
|
139
|
+
rows = registry.get("sources")
|
|
140
|
+
if not isinstance(rows, list):
|
|
141
|
+
return ["rules.json: sources must account for every knowledge.json source"]
|
|
142
|
+
actual_sources = [row.get("source") for row in rows]
|
|
143
|
+
if len(actual_sources) != len(set(actual_sources)):
|
|
144
|
+
errors.append("rules.json: duplicate source accounting rows")
|
|
145
|
+
missing = set(expected_sources) - set(actual_sources)
|
|
146
|
+
extra = set(actual_sources) - set(expected_sources)
|
|
147
|
+
if missing or extra:
|
|
148
|
+
errors.append(
|
|
149
|
+
f"rules.json: source accounting mismatch missing={sorted(missing)[:8]} "
|
|
150
|
+
f"extra={sorted(extra)[:8]}"
|
|
151
|
+
)
|
|
152
|
+
applied_sources = set()
|
|
153
|
+
for row in rows:
|
|
154
|
+
source, status = row.get("source"), row.get("status")
|
|
155
|
+
if status not in {"applied", "not-applicable"}:
|
|
156
|
+
errors.append(f"rules.json: source {source!r} status must be applied|not-applicable")
|
|
157
|
+
if not str(row.get("reason", "")).strip():
|
|
158
|
+
errors.append(f"rules.json: source {source!r} needs a concrete reason")
|
|
159
|
+
if status == "applied":
|
|
160
|
+
applied_sources.add(source)
|
|
161
|
+
ids = [rule.get("id") for rule in rules]
|
|
162
|
+
if len(ids) != len(set(ids)):
|
|
163
|
+
errors.append("rules.json: duplicate rule ids")
|
|
164
|
+
for rule in rules:
|
|
165
|
+
if not rule.get("id") or not str(rule.get("guideline", "")).strip():
|
|
166
|
+
errors.append(f"rules.json: rule {rule.get('id')!r} lacks id or guideline")
|
|
167
|
+
scope = rule.get("scope")
|
|
168
|
+
if not isinstance(scope, list) or not scope:
|
|
169
|
+
errors.append(f"rules.json: rule {rule.get('id')!r} needs a scope glob list")
|
|
170
|
+
continue
|
|
171
|
+
if rule.get("source") not in applied_sources:
|
|
172
|
+
errors.append(
|
|
173
|
+
f"rules.json: rule {rule.get('id')!r} cites source {rule.get('source')!r} "
|
|
174
|
+
"that is not marked applied"
|
|
175
|
+
)
|
|
176
|
+
regexes = [glob_to_regex(str(glob)) for glob in scope]
|
|
177
|
+
if not any(rx.match(path) for rx in regexes for path in selected):
|
|
178
|
+
errors.append(
|
|
179
|
+
f"rules.json: rule {rule.get('id')!r} scope matches no selected path"
|
|
180
|
+
)
|
|
181
|
+
return errors
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cohort_target(selected: dict[str, dict], concurrency: int) -> tuple[int, int]:
|
|
185
|
+
"""(max cohorts, changed lines): enough ~400-line cohorts to fill the reviewer slots, never more."""
|
|
186
|
+
total_lines = sum(int(f.get("adds") or 0) + int(f.get("dels") or 0) for f in selected.values())
|
|
187
|
+
return min(concurrency, max(1, ceil(total_lines / TARGET_COHORT_CHANGED_LINES))), total_lines
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def validate_cohorts(
|
|
191
|
+
cohorts: list[dict], selected: dict[str, dict], max_cohort_files: int, concurrency: int
|
|
192
|
+
) -> list[str]:
|
|
193
|
+
errors: list[str] = []
|
|
194
|
+
expected, total_lines = cohort_target(selected, concurrency)
|
|
195
|
+
if len(cohorts) > expected:
|
|
196
|
+
errors.append(
|
|
197
|
+
f"plan has {len(cohorts)} cohorts; {total_lines} changed lines at concurrency {concurrency} "
|
|
198
|
+
f"should use at most {expected} cohorts (~{TARGET_COHORT_CHANGED_LINES} lines each); merge plan.json cohorts"
|
|
199
|
+
)
|
|
200
|
+
seen_ids: set[str] = set()
|
|
201
|
+
full_owners: dict[str, list[str]] = defaultdict(list)
|
|
202
|
+
scoped_owners: dict[str, list[tuple[str, tuple[int, int, str]]]] = defaultdict(list)
|
|
203
|
+
for cohort in cohorts:
|
|
204
|
+
cohort_id = cohort.get("id")
|
|
205
|
+
if not cohort_id or cohort_id in seen_ids:
|
|
206
|
+
errors.append(f"duplicate or missing cohort id {cohort_id!r}")
|
|
207
|
+
seen_ids.add(cohort_id)
|
|
208
|
+
if cohort.get("risk") not in {"high", "normal", "low"}:
|
|
209
|
+
errors.append(f"{cohort_id}: risk must be high|normal|low")
|
|
210
|
+
files = cohort.get("files", [])
|
|
211
|
+
if not files or len(files) > max_cohort_files:
|
|
212
|
+
errors.append(
|
|
213
|
+
f"{cohort_id}: invalid file count {len(files)} (1..{max_cohort_files})"
|
|
214
|
+
)
|
|
215
|
+
scope = cohort.get("hunk_scope") or {}
|
|
216
|
+
extra_scope = set(scope) - set(files)
|
|
217
|
+
if extra_scope:
|
|
218
|
+
errors.append(f"{cohort_id}: hunk_scope paths absent from files: {sorted(extra_scope)}")
|
|
219
|
+
for path in files:
|
|
220
|
+
if path not in selected:
|
|
221
|
+
errors.append(f"{cohort_id}: non-selected or unknown path {path}")
|
|
222
|
+
continue
|
|
223
|
+
if path in scope:
|
|
224
|
+
for hunk in scope[path]:
|
|
225
|
+
scoped_owners[path].append((cohort_id, canonical_hunk(hunk)))
|
|
226
|
+
else:
|
|
227
|
+
full_owners[path].append(cohort_id)
|
|
228
|
+
if scope:
|
|
229
|
+
scoped_lines = sum(int(h["lines"]) for hunks in scope.values() for h in hunks)
|
|
230
|
+
if scoped_lines > MAX_COHORT_CHANGED_LINES:
|
|
231
|
+
errors.append(f"{cohort_id}: scoped changed lines {scoped_lines} > {MAX_COHORT_CHANGED_LINES}")
|
|
232
|
+
else:
|
|
233
|
+
changed = sum(
|
|
234
|
+
int(selected[path].get("adds") or 0) + int(selected[path].get("dels") or 0)
|
|
235
|
+
for path in files
|
|
236
|
+
if path in selected
|
|
237
|
+
)
|
|
238
|
+
if changed > MAX_COHORT_CHANGED_LINES:
|
|
239
|
+
errors.append(
|
|
240
|
+
f"{cohort_id}: changed lines {changed} > {MAX_COHORT_CHANGED_LINES} without hunk_scope"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
for path, item in selected.items():
|
|
244
|
+
full, scoped = full_owners.get(path, []), scoped_owners.get(path, [])
|
|
245
|
+
if full and scoped:
|
|
246
|
+
errors.append(f"{path}: mixed full and scoped ownership")
|
|
247
|
+
elif full:
|
|
248
|
+
if len(full) != 1:
|
|
249
|
+
errors.append(f"{path}: owned by {len(full)} cohorts ({full})")
|
|
250
|
+
elif scoped:
|
|
251
|
+
want = Counter(
|
|
252
|
+
(side, line)
|
|
253
|
+
for start, lines, side in (canonical_hunk(h) for h in item["hunks"])
|
|
254
|
+
for line in range(start, start + lines)
|
|
255
|
+
)
|
|
256
|
+
got = Counter(
|
|
257
|
+
(side, line)
|
|
258
|
+
for _, (start, lines, side) in scoped
|
|
259
|
+
for line in range(start, start + lines)
|
|
260
|
+
)
|
|
261
|
+
if got != want:
|
|
262
|
+
errors.append(
|
|
263
|
+
f"{path}: hunk slice mismatch missing_lines={sum((want - got).values())} "
|
|
264
|
+
f"duplicated_or_extra_lines={sum((got - want).values())}"
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
errors.append(f"{path}: missing cohort ownership")
|
|
268
|
+
return errors
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def normalize_sweeps(plan: dict) -> list[dict]:
|
|
272
|
+
sweeps, errors = [], []
|
|
273
|
+
for entry in plan.get("sweeps", []):
|
|
274
|
+
key, lens = (entry, DEFAULT_LENSES.get(entry)) if isinstance(entry, str) else (entry.get("key"), entry.get("lens"))
|
|
275
|
+
if key in REMOVED_SWEEPS:
|
|
276
|
+
errors.append(f"sweep {key!r} was removed: the Technical Verifier owns {REMOVED_SWEEPS[key]}")
|
|
277
|
+
continue
|
|
278
|
+
if isinstance(entry, str) and lens is None:
|
|
279
|
+
errors.append(f"sweep {entry!r} has no built-in lens; use {{key, lens}}")
|
|
280
|
+
continue
|
|
281
|
+
if not key or not lens:
|
|
282
|
+
errors.append(f"sweep entry {entry!r} needs key and lens")
|
|
283
|
+
continue
|
|
284
|
+
sweeps.append({"key": key, "lens": lens})
|
|
285
|
+
keys = [sweep["key"] for sweep in sweeps]
|
|
286
|
+
if len(keys) != len(set(keys)):
|
|
287
|
+
errors.append("duplicate sweep keys")
|
|
288
|
+
cohorts = len(plan.get("cohorts", []))
|
|
289
|
+
if sweeps and cohorts <= 2:
|
|
290
|
+
errors.append(
|
|
291
|
+
f"sweeps need at least 3 cohorts ({cohorts} planned); cohort lanes own every finding "
|
|
292
|
+
"in a small diff — remove sweeps from plan.json"
|
|
293
|
+
)
|
|
294
|
+
if errors:
|
|
295
|
+
raise RuntimeError("sweep validation failed:\n- " + "\n- ".join(errors))
|
|
296
|
+
return sweeps
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def cohort_rules(rules: list[dict], files: list[str]) -> list[dict]:
|
|
300
|
+
compiled = [(rule, [glob_to_regex(str(glob)) for glob in rule["scope"]]) for rule in rules]
|
|
301
|
+
return [
|
|
302
|
+
rule
|
|
303
|
+
for rule, regexes in compiled
|
|
304
|
+
if any(rx.match(path) for rx in regexes for path in files)
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def rules_block(rules: list[dict], files: list[str]) -> tuple[str, int]:
|
|
309
|
+
bound = cohort_rules(rules, files)
|
|
310
|
+
if not bound:
|
|
311
|
+
return (
|
|
312
|
+
"- No repo rules map to these files — judge on the taxonomy and general correctness alone.",
|
|
313
|
+
0,
|
|
314
|
+
)
|
|
315
|
+
lines = [
|
|
316
|
+
f"- [{rule['id']}] (`{rule['source']}`): \"{' '.join(str(rule['guideline']).split())}\""
|
|
317
|
+
for rule in bound
|
|
318
|
+
]
|
|
319
|
+
return "\n".join(lines), len(bound)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def file_list_block(cohort: dict, selected: dict[str, dict]) -> str:
|
|
323
|
+
return "\n".join(
|
|
324
|
+
f"- `{path}` status={selected[path]['status']} hunks="
|
|
325
|
+
+ ", ".join(
|
|
326
|
+
hunk_text(h)
|
|
327
|
+
for h in (cohort.get("hunk_scope", {}).get(path) or selected[path]["hunks"])
|
|
328
|
+
)
|
|
329
|
+
for path in cohort["files"]
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def scope_instruction(cohort: dict) -> str:
|
|
334
|
+
scope = cohort.get("hunk_scope")
|
|
335
|
+
if scope:
|
|
336
|
+
return (
|
|
337
|
+
"Judge ONLY these hunks — sibling reviewers own the rest of the file; read beyond them "
|
|
338
|
+
f"freely, report inside them: `{json.dumps(scope, separators=(',', ':'))}`."
|
|
339
|
+
)
|
|
340
|
+
return "Judge every manifest hunk of every listed file."
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def owned_hunks(cohort: dict, selected: dict[str, dict]) -> list[dict]:
|
|
344
|
+
scope = cohort.get("hunk_scope") or {}
|
|
345
|
+
return [
|
|
346
|
+
{"file": path, "hunk": hunk_text(hunk)}
|
|
347
|
+
for path in cohort["files"]
|
|
348
|
+
for hunk in (scope.get(path) or selected[path]["hunks"])
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def prior_findings_block(ledger: dict[str, dict]) -> str:
|
|
353
|
+
"""Remediation-check contract: one disposition row per open prior finding."""
|
|
354
|
+
rows = sorted((fp, entry) for fp, entry in ledger.items() if entry.get("status") == "open")
|
|
355
|
+
if not rows:
|
|
356
|
+
return "PRIOR FINDINGS: No prior findings to disposition — leave `prior_findings` empty."
|
|
357
|
+
lines = [
|
|
358
|
+
"PRIOR FINDINGS — return one `prior_findings` row per fingerprint with `status` `resolved` "
|
|
359
|
+
"or `open` and a one-line `evidence`; re-run the certificate Path before marking resolved. "
|
|
360
|
+
"A prior finding appears ONLY as a `prior_findings` row. Never list it, or a rewording of it, "
|
|
361
|
+
"in `defects`; `defects` holds only failures introduced or newly discovered in `reviewed_head..HEAD`:"
|
|
362
|
+
]
|
|
363
|
+
for fp, entry in rows:
|
|
364
|
+
also = ", ".join(entry.get("also_applies") or []) or "none"
|
|
365
|
+
lines.append(
|
|
366
|
+
f"- `{fp}` {entry['severity']} at `{entry['file']}:{entry.get('line', '?')}` — "
|
|
367
|
+
f"{entry.get('certificate') or entry['title']}; also applies: {also}"
|
|
368
|
+
)
|
|
369
|
+
return "\n".join(lines)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def coverage_contract(required_hunks: list[dict], rule_ids: list[str] | None = None) -> str:
|
|
373
|
+
contract = f"HUNK COVERAGE (one exact row per assignment): `{json.dumps(required_hunks, separators=(',', ':'))}`"
|
|
374
|
+
if rule_ids is not None:
|
|
375
|
+
contract += (
|
|
376
|
+
"\nASSIGNED RULE ACCOUNTING (one exact row per id, even when compliant or not applicable): "
|
|
377
|
+
f"`{json.dumps(rule_ids, separators=(',', ':'))}`"
|
|
378
|
+
)
|
|
379
|
+
return contract
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def main() -> int:
|
|
383
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
384
|
+
parser.add_argument("--out", required=True)
|
|
385
|
+
parser.add_argument(
|
|
386
|
+
"--max-cohort-files",
|
|
387
|
+
type=positive_int,
|
|
388
|
+
default=DEFAULT_MAX_COHORT_FILES,
|
|
389
|
+
help=f"maximum files per cohort (default: {DEFAULT_MAX_COHORT_FILES})",
|
|
390
|
+
)
|
|
391
|
+
parser.add_argument(
|
|
392
|
+
"--graphify-question",
|
|
393
|
+
help="one explicit architectural question for a bounded Graphify context",
|
|
394
|
+
)
|
|
395
|
+
args = parser.parse_args()
|
|
396
|
+
|
|
397
|
+
repo = repo_root()
|
|
398
|
+
out = Path(args.out).resolve()
|
|
399
|
+
try:
|
|
400
|
+
manifest = read_json(out / "manifest.json")
|
|
401
|
+
plan = read_json(out / "plan.json")
|
|
402
|
+
registry = read_json(out / "rules.json")
|
|
403
|
+
knowledge = read_json(out / "knowledge.json")
|
|
404
|
+
rules = registry.get("rules")
|
|
405
|
+
if not isinstance(rules, list):
|
|
406
|
+
raise RuntimeError("rules.json: rules must be an array")
|
|
407
|
+
(out / "context-pack.md").read_text(encoding="utf-8") # required by every prompt
|
|
408
|
+
if "diff_command" not in manifest:
|
|
409
|
+
raise RuntimeError("manifest.json lacks diff_command — rebuild it with the current build_manifest.py")
|
|
410
|
+
|
|
411
|
+
selected = manifest_selected(manifest)
|
|
412
|
+
errors = validate_registry(registry, knowledge, selected)
|
|
413
|
+
incremental = manifest.get("mode") == "incremental"
|
|
414
|
+
if incremental:
|
|
415
|
+
# Remediation check: one job over every selected path; plan cohorts and sweeps are ignored.
|
|
416
|
+
cohorts = [{"id": "rc", "name": "remediation check", "risk": "high", "files": sorted(selected)}]
|
|
417
|
+
if plan.get("sweeps"):
|
|
418
|
+
print("sweeps skipped in incremental mode")
|
|
419
|
+
ledger = read_json(out / "state.json").get("ledger", {})
|
|
420
|
+
prior_anchors = [
|
|
421
|
+
{"fingerprint": fp, "file": entry["file"], "line": entry.get("line")}
|
|
422
|
+
for fp, entry in sorted(ledger.items()) if entry.get("status") == "open"
|
|
423
|
+
]
|
|
424
|
+
else:
|
|
425
|
+
cohorts = plan["cohorts"]
|
|
426
|
+
errors += validate_cohorts(cohorts, selected, args.max_cohort_files, manifest["concurrency"])
|
|
427
|
+
ledger, prior_anchors = {}, []
|
|
428
|
+
prior_fps = [anchor["fingerprint"] for anchor in prior_anchors]
|
|
429
|
+
if errors:
|
|
430
|
+
raise RuntimeError("plan validation failed:\n- " + "\n- ".join(errors))
|
|
431
|
+
sweeps = [] if incremental else normalize_sweeps(plan)
|
|
432
|
+
graft = prepare_graft_context(repo, out, sorted(selected))
|
|
433
|
+
graphify = (
|
|
434
|
+
prepare_graphify_context(repo, out, args.graphify_question)
|
|
435
|
+
if args.graphify_question is not None
|
|
436
|
+
else None
|
|
437
|
+
)
|
|
438
|
+
if graphify is not None and graft.get("question_hash") == graphify.get("question_hash"):
|
|
439
|
+
raise RuntimeError("Graft and Graphify questions must be distinct")
|
|
440
|
+
|
|
441
|
+
reviewer_template = load_template("reviewer")
|
|
442
|
+
sweep_template = load_template("sweep")
|
|
443
|
+
schema = json.dumps(load_schema("findings"), separators=(",", ":"))
|
|
444
|
+
shared = {
|
|
445
|
+
"target": manifest["target"],
|
|
446
|
+
"context": rel(out / "context-pack.md", repo),
|
|
447
|
+
"taxonomy": f"{skill_rel(repo)}/references/taxonomy.md",
|
|
448
|
+
"diff_command": manifest["diff_command"],
|
|
449
|
+
"schema": schema,
|
|
450
|
+
"graft_context": rel(Path(graft["path"]), repo),
|
|
451
|
+
"prior_findings": prior_findings_block(ledger) if incremental else "",
|
|
452
|
+
}
|
|
453
|
+
graphify_note = ""
|
|
454
|
+
if graphify is not None:
|
|
455
|
+
graphify_note = (
|
|
456
|
+
f"\n\nGRAPHIFY CONTEXT: read `{rel(Path(graphify['path']), repo)}` for the one bounded "
|
|
457
|
+
"architectural question; verify every claim against the frozen checkout."
|
|
458
|
+
)
|
|
459
|
+
prompts_dir = out / "prompts"
|
|
460
|
+
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
461
|
+
(out / "agents").mkdir(exist_ok=True)
|
|
462
|
+
(out / "runs").mkdir(exist_ok=True)
|
|
463
|
+
|
|
464
|
+
jobs, bound_counts, cohort_hunks = [], [], []
|
|
465
|
+
for cohort in cohorts:
|
|
466
|
+
label = f"cohort-{cohort['id'].lower()}"
|
|
467
|
+
output = out / "agents" / f"{label}.json"
|
|
468
|
+
bound_rules = cohort_rules(rules, cohort["files"])
|
|
469
|
+
block, bound = rules_block(rules, cohort["files"])
|
|
470
|
+
rule_ids = [rule["id"] for rule in bound_rules]
|
|
471
|
+
required_hunks = owned_hunks(cohort, selected)
|
|
472
|
+
cohort_hunks += required_hunks
|
|
473
|
+
bound_counts.append(bound)
|
|
474
|
+
prompt = render_template("reviewer", reviewer_template, REVIEWER_PLACEHOLDERS, {
|
|
475
|
+
**shared,
|
|
476
|
+
"cohort_name": cohort["name"],
|
|
477
|
+
"risk": cohort["risk"],
|
|
478
|
+
"file_list": file_list_block(cohort, selected),
|
|
479
|
+
"scope_instruction": scope_instruction(cohort),
|
|
480
|
+
"rules_block": block,
|
|
481
|
+
"base": manifest["base"],
|
|
482
|
+
"output": rel(output, repo),
|
|
483
|
+
"lane_instruction": (
|
|
484
|
+
"DEFECT LANE: report only concrete correctness, security, data, contract, "
|
|
485
|
+
"reliability, or failing-capable test defects in `defects`; report concrete, "
|
|
486
|
+
"actionable maintainability or project-rule improvements in `advisories`."
|
|
487
|
+
),
|
|
488
|
+
"coverage_contract": coverage_contract(required_hunks, [rule["id"] for rule in bound_rules]),
|
|
489
|
+
}) + graphify_note
|
|
490
|
+
(prompts_dir / f"{label}.md").write_text(prompt, encoding="utf-8")
|
|
491
|
+
jobs.append({
|
|
492
|
+
"label": label, "kind": "cohort", "lane": "defect", "required_hunks": required_hunks,
|
|
493
|
+
"rule_ids": rule_ids, "prior_fingerprints": prior_fps, "prior_anchors": prior_anchors,
|
|
494
|
+
"prompt": rel(prompts_dir / f"{label}.md", repo),
|
|
495
|
+
"output": rel(output, repo),
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
for sweep in sweeps:
|
|
499
|
+
label = f"sweep-{sweep['key']}"
|
|
500
|
+
output = out / "agents" / f"{label}.json"
|
|
501
|
+
bound_rules = cohort_rules(rules, list(selected))
|
|
502
|
+
block, _ = rules_block(rules, list(selected))
|
|
503
|
+
rule_ids = [rule["id"] for rule in bound_rules]
|
|
504
|
+
prompt = render_template("sweep", sweep_template, SWEEP_PLACEHOLDERS, {
|
|
505
|
+
**shared,
|
|
506
|
+
"sweep_key": sweep["key"],
|
|
507
|
+
"lens": sweep["lens"],
|
|
508
|
+
"manifest": rel(out / "manifest.json", repo),
|
|
509
|
+
"output": rel(output, repo),
|
|
510
|
+
"rules_block": block,
|
|
511
|
+
"coverage_contract": coverage_contract([], [rule["id"] for rule in bound_rules]),
|
|
512
|
+
}) + graphify_note
|
|
513
|
+
(prompts_dir / f"{label}.md").write_text(prompt, encoding="utf-8")
|
|
514
|
+
jobs.append({
|
|
515
|
+
"label": label, "kind": "sweep", "lane": "sweep", "required_hunks": [],
|
|
516
|
+
"cohort_hunks": cohort_hunks, "rule_ids": rule_ids,
|
|
517
|
+
"prompt": rel(prompts_dir / f"{label}.md", repo),
|
|
518
|
+
"output": rel(output, repo),
|
|
519
|
+
})
|
|
520
|
+
write_json(out / "jobs.json", {
|
|
521
|
+
"jobs": jobs,
|
|
522
|
+
"repository_intelligence": {
|
|
523
|
+
"graft": {
|
|
524
|
+
"status": graft["status"],
|
|
525
|
+
"path": rel(Path(graft["path"]), repo),
|
|
526
|
+
"question_hash": graft["question_hash"],
|
|
527
|
+
**({"reason": graft["reason"]} if graft.get("reason") else {}),
|
|
528
|
+
},
|
|
529
|
+
"graphify": (
|
|
530
|
+
{
|
|
531
|
+
"status": graphify["status"],
|
|
532
|
+
"path": rel(Path(graphify["path"]), repo),
|
|
533
|
+
"question_hash": graphify["question_hash"],
|
|
534
|
+
**({"reason": graphify["reason"]} if graphify.get("reason") else {}),
|
|
535
|
+
}
|
|
536
|
+
if graphify is not None else None
|
|
537
|
+
),
|
|
538
|
+
"dual_use_reason": (
|
|
539
|
+
"Graphify provides architecture relationships; Graft provides code callers and symbols."
|
|
540
|
+
if graphify is not None else None
|
|
541
|
+
),
|
|
542
|
+
},
|
|
543
|
+
})
|
|
544
|
+
except RuntimeError as error:
|
|
545
|
+
sys.stderr.write(f"{error}\n")
|
|
546
|
+
return 1
|
|
547
|
+
|
|
548
|
+
with_rules = sum(1 for count in bound_counts if count)
|
|
549
|
+
expected, total_lines = cohort_target(selected, manifest["concurrency"])
|
|
550
|
+
print(
|
|
551
|
+
f"jobs: {len(cohorts)} defect cohorts + {len(sweeps)} sweeps -> {out / 'jobs.json'}\n"
|
|
552
|
+
f"cohort target: {expected} for {total_lines} changed lines at concurrency {manifest['concurrency']}\n"
|
|
553
|
+
f"cohort limit: {args.max_cohort_files} files / {MAX_COHORT_CHANGED_LINES} changed lines\n"
|
|
554
|
+
f"rules: {len(rules)} registered; {with_rules}/{len(bound_counts)} cohorts carry bound rules\n"
|
|
555
|
+
f"every selected hunk has one defect owner; prompts under {out / 'prompts'}"
|
|
556
|
+
)
|
|
557
|
+
return 0
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
if __name__ == "__main__":
|
|
561
|
+
sys.exit(main())
|