Skip to content

Commit 177e62b

Browse files
0xaslanclaude
andauthored
Bound predict-audit ownership-walk + rule-sweep harnesses; consolidate unverified findings (#1088)
* Document Propbook Block Scholes feed split * Bound predict-audit ownership-walk + rule-sweep by construction; consolidate unverified findings Sibling half of the predict-audit harness rework whose orchestrator + SKILL.md half already landed in main via the reference-tick admission PR. Read-only on packages/* — audit tooling only. ownership-walk.workflow.js, rule-sweep.workflow.js: - Bound by construction: MAX_ROUNDS default 12->3 (ownership-walk) and 10->3 (rule-sweep), add verifyCap (60), severity-gated verify (cleanup-tier reported raw, no subagent). Agents <= maxRounds*units + verifyCap, so neither can run away to the agent cap. - Each now emits an `unverified` array (Info/Low/cleanup triaged out of verify). consolidate.py: - Collect the new `unverified` key from every harness: record it under its own key in findings.json (so track.py never floods the tracker with low-priority noise), keep it in the DROPPED-0 accounting (honest, no silent drop), and surface it in an Unverified report section. Re-syncs the consolidator with the already-merged orchestrator, which emits `unverified`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Untrack docs/superpowers spec (tracked despite /docs/ gitignore) Committed in 219a910 and left tracked, so .gitignore (/docs/, /docs/superpowers/) never applied — tracked files bypass ignore rules, which is why these specs kept reappearing in PRs. git rm --cached untracks it (local copy kept); the existing rule now keeps it out. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5744ec3 commit 177e62b

3 files changed

Lines changed: 62 additions & 15 deletions

File tree

.claude/skills/predict-audit/consolidate.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def collect(res, harness):
8585
promoted = res.get('promoted', []) or []
8686
for f in promoted:
8787
r = norm(f, harness, 'open'); r['verdict'] = 'promoted-unverified'; out.append(r) # not panel-verified
88+
# Info/Low/cleanup findings the orchestrator triaged out of the verify panel: recorded (so they are NOT
89+
# silently dropped + the accounting stays honest), but kept OUT of the open tracker — raw, low-priority.
90+
for f in res.get('unverified', []) or []:
91+
out.append(norm(f, harness, 'unverified'))
8892
return out, len(promoted)
8993

9094
def main():
@@ -136,12 +140,13 @@ def main():
136140
f['also'] = []; seen[k] = f; dedup_open.append(f)
137141
settled = [x for x in all_findings if x['status'] == 'settled']
138142
refuted = [x for x in all_findings if x['status'] == 'refuted']
143+
unverified = [x for x in all_findings if x['status'] == 'unverified']
139144

140-
# ACCOUNTING: every input finding is either a unique open, a merge, a settled, or a refuted.
141-
accounted = len(dedup_open) + merges + len(settled) + len(refuted)
145+
# ACCOUNTING: every input finding is either a unique open, a merge, a settled, a refuted, or an unverified.
146+
accounted = len(dedup_open) + merges + len(settled) + len(refuted) + len(unverified)
142147
dropped = total_in - accounted
143148
acct = (f"ACCOUNTING — parsed {total_in} | open {len(dedup_open)} (+{merges} dup-merges) | "
144-
f"settled {len(settled)} | refuted {len(refuted)} | DROPPED {dropped} "
149+
f"settled {len(settled)} | refuted {len(refuted)} | unverified {len(unverified)} | DROPPED {dropped} "
145150
+ ("✅ 0 dropped" if dropped == 0 else "⚠️ MISMATCH — investigate"))
146151
if parse_failures:
147152
acct += f"\n{len(parse_failures)} INPUT FILE(S) FAILED TO PARSE (findings NOT in this report): " \
@@ -176,18 +181,25 @@ def main():
176181
L.append("\n## Refuted (second-guess these — a wrong refutation hides a real bug)\n")
177182
for f in refuted:
178183
L.append(f"- [{f['harness']}/{f['source']}] {f['location']}{f['title']}{f['why']}")
184+
L.append("\n## Unverified (raw finder output — Info/Low/cleanup, NOT panel-verified; triage manually)\n")
185+
if not unverified: L.append("_(none)_")
186+
for f in sorted(unverified, key=lambda f: -SEV.get(f['severity'].lower(), 1)):
187+
sref = f" → {f['settled_ref']}" if f.get('settled_ref') else ""
188+
L.append(f"- [{f['severity'] or '?'}] {f['title']}{f['location']} ({f['harness']}/{f['source']}){sref}")
179189
L.append("\n## Coverage — what was examined and (esp.) NOT examined\n")
180190
for h, lane, cov, top3 in coverage_blocks:
181191
L.append(f"- **{h}/{lane}**: {cov}")
182192
if top3: L.append(" - top3: " + " | ".join(top3))
183193

184194
rpt = os.path.join(out_dir, 'consolidated-report.md')
185195
open(rpt, 'w', encoding='utf-8').write("\n".join(L) + "\n")
186-
# findings.json (open findings with stable ids) feeds track.py, the live OPEN-ITEMS.md tracker.
187-
json.dump({'open': dedup_open, 'settled': settled, 'refuted': refuted},
196+
# findings.json (open findings with stable ids) feeds track.py, the live OPEN-ITEMS.md tracker. `unverified`
197+
# is recorded here too but under its OWN key, so track.py (which keys on `open`) never floods the tracker
198+
# with Info/Low/cleanup noise.
199+
json.dump({'open': dedup_open, 'settled': settled, 'refuted': refuted, 'unverified': unverified},
188200
open(os.path.join(out_dir, 'findings.json'), 'w', encoding='utf-8'), indent=1)
189201
print(acct)
190-
print(f"wrote {rpt} ({len(dedup_open)} open, {len(settled)} settled, {len(refuted)} refuted)")
202+
print(f"wrote {rpt} ({len(dedup_open)} open, {len(settled)} settled, {len(refuted)} refuted, {len(unverified)} unverified)")
191203
sys.exit(0 if (dropped == 0 and not parse_failures and not errored_harnesses) else 1)
192204

193205
if __name__ == '__main__':

.claude/skills/predict-audit/ownership-walk.workflow.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@
1010
// units?: string[], // subset of MAP_UNIT keys to walk (default: all) — use to scope cost
1111
// groundTruth?: string,
1212
// maxViolations?: number, // cap per check unit (default 10)
13+
// dryRounds?: number, // stop after this many no-new-violation rounds (default 2)
14+
// maxRounds?: number, // hard round cap (default 3)
15+
// verifyCap?: number, // max violations sent to the verifier (default 60)
1316
// }
17+
// COST IS BOUNDED BY CONSTRUCTION: agents <= units (map) + maxRounds*checkUnits (check) + verifyCap (verify),
18+
// so a run cannot hit the 1000-agent cap. The `budget` global (a "+NNNm" turn directive) is only an optional
19+
// early-stop and often does NOT propagate into a background workflow — never rely on it. Verify is
20+
// SEVERITY-GATED: 'cleanup'-tier violations are reported RAW (unverified, no subagent); 'high'/'correctness'
21+
// get one verifier each.
1422
// Subagents are READ-ONLY on source; no sui build/test or localnet (watchdog) — reason from source + grep + git.
1523

1624
export const meta = {
@@ -19,7 +27,7 @@ export const meta = {
1927
phases: [
2028
{ title: 'Map', detail: 'build the responsibility map per subsystem (BARRIER — checks need the whole map)' },
2129
{ title: 'Check', detail: 'per-module fan-out: walk every function vs R1-R7 + the map' },
22-
{ title: 'Verify', detail: 'adversarially refute each violation vs the map + DECISION_JOURNAL' },
30+
{ title: 'Verify', detail: 'severity-gated: refute each high/correctness violation vs the map + DECISION_JOURNAL; cleanup-tier reported raw' },
2331
],
2432
}
2533

@@ -151,7 +159,8 @@ function composedContext(entry) {
151159
// told what's already found FOR THAT MODULE so it hunts new ground), union new violations, until K dry
152160
// rounds or the budget floor. Auto-retries flaky units (a failed unit is an empty round that re-runs).
153161
const DRY_TARGET = A.dryRounds || 2
154-
const MAX_ROUNDS = A.maxRounds || 12
162+
const MAX_ROUNDS = A.maxRounds || 3
163+
const VERIFY_CAP = A.verifyCap || 60
155164
const RESERVE = (budget && budget.total) ? Math.max(4_000_000, Math.floor(budget.total * 0.3)) : 4_000_000
156165
function budgetLeft() { return budget && typeof budget.remaining === 'function' ? budget.remaining() : Infinity }
157166
// strip line numbers from node so a same violation at a shifted line still dedups across rounds (else the
@@ -193,8 +202,16 @@ while (dry < DRY_TARGET && round < MAX_ROUNDS && budgetLeft() > RESERVE) {
193202
}
194203
log(`Check converged after ${round} rounds (${dry} dry): ${candidates.length} unique candidate violations`)
195204

205+
// SEVERITY-GATED + CAPPED: only 'high'/'correctness' violations get a verifier; 'cleanup'-tier violations are
206+
// reported RAW (unverified). Capped at VERIFY_CAP by severity so the agent count stays bounded.
196207
phase('Verify')
197-
const all = (await parallel(candidates.map((v, vi) => () => agent(verifyPromptV(v),
208+
const sevRankV = { high: 3, correctness: 2, cleanup: 1 }
209+
const verifyAll = candidates.filter(v => v.severity !== 'cleanup').sort((a, b) => (sevRankV[b.severity] || 0) - (sevRankV[a.severity] || 0))
210+
const toVerify = verifyAll.slice(0, VERIFY_CAP)
211+
const verifyOverflow = verifyAll.slice(VERIFY_CAP) // beyond the cap -> reported unverified (logged, never silently dropped)
212+
const unverifiedV = candidates.filter(v => v.severity === 'cleanup').concat(verifyOverflow)
213+
if (verifyOverflow.length) log(`Verify cap ${VERIFY_CAP} hit: ${verifyOverflow.length} violation(s) reported UNVERIFIED — raise verifyCap to verify them all`)
214+
const all = (await parallel(toVerify.map((v, vi) => () => agent(verifyPromptV(v),
198215
{ schema: VERDICT_SCHEMA, effort: 'high', phase: 'Verify', label: `verify:${v.module}:${vi}` })
199216
.then(verdict => ({ ...v, verdict }))))).filter(Boolean)
200217
const confirmed = all.filter(x => x.verdict && (x.verdict.verdict === 'confirmed' || x.verdict.verdict === 'uncertain'))
@@ -210,9 +227,10 @@ all.filter(x => x.verdict && (x.verdict.classification === 'update-rule' || x.ve
210227
log(`Modules: ${allModules.length} | check units: ${CHECK_UNITS.length} | rounds: ${round} | violations: ${all.length} | confirmed: ${confirmed.length} | settled: ${settledOut.length} | refuted: ${refutedOut.length}`)
211228

212229
return {
213-
summary: { modules: allModules.length, checkUnits: CHECK_UNITS.length, unmapped_units: unmappedUnits, rounds: round, violations: all.length, confirmed: confirmed.length, settled: settledOut.length, refuted: refutedOut.length },
230+
summary: { modules: allModules.length, checkUnits: CHECK_UNITS.length, unmapped_units: unmappedUnits, rounds: round, violations: all.length, confirmed: confirmed.length, settled: settledOut.length, refuted: refutedOut.length, unverified: unverifiedV.length },
214231
responsibility_map: allModules.map(m => ({ module: m.module, role: m.role, owns: m.owns, composes: m.composes })),
215232
coverage: Object.keys(coverageByModule).map(m => ({ lane: m, coverage: coverageByModule[m] })),
233+
unverified: unverifiedV.map(x => ({ rule_family: x.rule_family, node: x.node, claim: x.claim, severity: x.severity, settled_ref: x.settled_ref, recommendation: x.recommendation, status: 'unverified' })),
216234
confirmed: confirmed.map(x => ({ rule_family: x.rule_family, node: x.node, claim: x.claim, expected_owner: x.expected_owner, actual_owner: x.actual_owner, severity: (x.verdict && x.verdict.adjusted_severity) || x.severity, status: (x.verdict && x.verdict.verdict) || 'confirmed', classification: x.verdict && x.verdict.classification, settled_ref: x.settled_ref, recommendation: x.recommendation, data_flow: x.data_flow, proof: x.verdict && x.verdict.evidence })),
217235
settled: settledOut.map(x => ({ rule_family: x.rule_family, node: x.node, claim: x.claim, settled_ref: (x.verdict && x.verdict.evidence) || x.settled_ref })),
218236
refuted: refutedOut.map(x => ({ rule_family: x.rule_family, node: x.node, claim: x.claim, why: x.verdict && x.verdict.reasoning })),

.claude/skills/predict-audit/rule-sweep.workflow.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
// Before Mutate", and the leaf-self-consistency half of 9) are NOT here — they need deep per-module context
99
// and live in ownership-walk.workflow.js (R1-R7). Do not duplicate them.
1010
//
11-
// args = { rules?: string[] (subset of family keys), maxFindings?: number, groundTruth?: string }
11+
// args = { rules?: string[] (subset of family keys), maxFindings?: number, groundTruth?: string,
12+
// dryRounds?: number (default 2), maxRounds?: number (default 3), verifyCap?: number (default 60) }
13+
// COST IS BOUNDED BY CONSTRUCTION: agents <= maxRounds*families (sweep) + verifyCap (verify), so a run cannot
14+
// hit the 1000-agent cap. The `budget` global (a "+NNNm" turn directive) is only an optional early-stop and
15+
// often does NOT propagate into a background workflow — never rely on it. Verify is SEVERITY-GATED: low/cleanup
16+
// hygiene findings are reported RAW (unverified, no subagent); high/medium (or fix-code) get one verifier each.
1217
// Subagents READ-ONLY; no sui build/test or localnet (watchdog) — the main loop runs the compiler in the
1318
// parent-reconciliation pass (rule-auditor's build/test step).
1419

@@ -17,7 +22,7 @@ export const meta = {
1722
description: 'Per-rule sweep of the mechanical/local repo rules across the Predict packages (refreshed rule-auditor): sweep -> verify/classify',
1823
phases: [
1924
{ title: 'Sweep', detail: 'one agent per rule family sweeps every relevant module for that rule' },
20-
{ title: 'Verify', detail: 'refute / classify each violation (fix-code / update-rule / design-decision / false-positive)' },
25+
{ title: 'Verify', detail: 'severity-gated: refute/classify each high/medium finding; low/cleanup hygiene reported raw' },
2126
],
2227
}
2328

@@ -114,7 +119,8 @@ const VERDICT_SCHEMA = {
114119
// MAXIMAL MODE: loop-until-dry SWEEP. Re-sweep each rule family across rounds (each told what's already
115120
// found for that family so it hunts new sites), union new findings, until K dry rounds or the budget floor.
116121
const DRY_TARGET = A.dryRounds || 2
117-
const MAX_ROUNDS = A.maxRounds || 10
122+
const MAX_ROUNDS = A.maxRounds || 3
123+
const VERIFY_CAP = A.verifyCap || 60
118124
const RESERVE = (budget && budget.total) ? Math.max(3_000_000, Math.floor(budget.total * 0.3)) : 3_000_000
119125
function budgetLeft() { return budget && typeof budget.remaining === 'function' ? budget.remaining() : Infinity }
120126
// strip line numbers (so a shifted-line same violation dedups) but KEEP a claim digest, so two DISTINCT
@@ -151,8 +157,18 @@ while (dry < DRY_TARGET && round < MAX_ROUNDS && budgetLeft() > RESERVE) {
151157
}
152158
log(`Sweep converged after ${round} rounds (${dry} dry): ${candidates.length} unique candidate findings`)
153159

160+
// SEVERITY-GATED + CAPPED: only high/medium (or fix-code) findings get a verifier; low/cleanup hygiene is
161+
// reported RAW (unverified). Capped at VERIFY_CAP by severity so the agent count stays bounded.
154162
phase('Verify')
155-
const all = (await parallel(candidates.map((f, fi) => () => agent(
163+
const sevRankF = { high: 4, medium: 3, low: 2, cleanup: 1 }
164+
const effSev = f => f.severity || (f.classification === 'fix-code' ? 'medium' : 'low')
165+
const ruleShouldVerify = f => { const s = effSev(f); return s === 'high' || s === 'medium' }
166+
const verifyAll = candidates.filter(ruleShouldVerify).sort((a, b) => (sevRankF[effSev(b)] || 0) - (sevRankF[effSev(a)] || 0))
167+
const toVerify = verifyAll.slice(0, VERIFY_CAP)
168+
const verifyOverflow = verifyAll.slice(VERIFY_CAP) // beyond the cap -> reported unverified (logged, never silently dropped)
169+
const unverifiedF = candidates.filter(f => !ruleShouldVerify(f)).concat(verifyOverflow)
170+
if (verifyOverflow.length) log(`Verify cap ${VERIFY_CAP} hit: ${verifyOverflow.length} finding(s) reported UNVERIFIED — raise verifyCap to verify them all`)
171+
const all = (await parallel(toVerify.map((f, fi) => () => agent(
156172
`${PRELUDE}\n\nADVERSARIALLY VERIFY this single rule-sweep finding (rule family ${f.rule_family}). Read the cited code; decide: confirmed (real violation) / refuted (not a violation, or the claim is wrong) / settled (matches a DECISION_JOURNAL/AGENTS decision — cite it). Then classify: fix-code / update-rule (defensible → narrowest exception) / design-decision / false-positive. Be skeptical; mechanical rules have many intentional exceptions (getters retained by D003, public APIs needed for PTBs, disabled test suites).\n\nFINDING:\n${JSON.stringify(f, null, 2)}`,
157173
{ schema: VERDICT_SCHEMA, effort: 'high', phase: 'Verify', label: `verify:${f.rule_family}:${fi}` })
158174
.then(verdict => ({ ...f, verdict }))))).filter(Boolean)
@@ -165,9 +181,10 @@ function shape(x) {
165181
return { rule_family: x.rule_family, severity: x.severity || (x.classification === 'fix-code' ? 'medium' : 'low'), location: x.location, claim: x.claim, classification: (x.verdict && x.verdict.classification) || x.classification, recommendation: x.recommendation, evidence: x.verdict && x.verdict.evidence }
166182
}
167183
return {
168-
summary: { rule_families: FAMILIES.length, rounds: round, findings: all.length, confirmed: confirmed.length, settled: settledOut.length, refuted: refutedOut.length },
184+
summary: { rule_families: FAMILIES.length, rounds: round, findings: all.length, confirmed: confirmed.length, settled: settledOut.length, refuted: refutedOut.length, unverified: unverifiedF.length },
169185
coverage: Object.keys(coverageByFamily).map(rf => ({ lane: rf, coverage: coverageByFamily[rf] })),
170186
confirmed: confirmed.map(shape),
171187
settled: settledOut.map(shape),
172188
refuted: refutedOut.map(x => ({ rule_family: x.rule_family, location: x.location, claim: x.claim, why: x.verdict && x.verdict.reasoning })),
189+
unverified: unverifiedF.map(x => ({ rule_family: x.rule_family, severity: effSev(x), location: x.location, claim: x.claim, classification: x.classification, recommendation: x.recommendation, status: 'unverified' })),
173190
}

0 commit comments

Comments
 (0)