Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ TestResults/
.complexity-log.md
.codex/
docs/superpowers/plans/
.mcp.json
.grok/
.pi/analyzer-health/
.claude/settings.local.json
.superpowers/
42 changes: 0 additions & 42 deletions .grok/config.toml

This file was deleted.

15 changes: 0 additions & 15 deletions .mcp.json

This file was deleted.

39 changes: 0 additions & 39 deletions .pi/analyzer-health/audit-2026-06-30T14-26-59-992Z.md

This file was deleted.

39 changes: 0 additions & 39 deletions .pi/analyzer-health/audit-2026-06-30T14-27-29-048Z.md

This file was deleted.

45 changes: 0 additions & 45 deletions .pi/analyzer-health/audit-2026-06-30T14-40-39-846Z.md

This file was deleted.

8 changes: 4 additions & 4 deletions .pi/extensions/analyzer-health/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ Project-local pi extension for making `analyzer-health.md` refreshes repeatable
- Loads the report into the editor so it can be used as the evidence pack for updating `analyzer-health.md`.
- `/analyzer-health-validate`
- Checks weighted score math and whether `Base audited commit` is an ancestor of current `HEAD`.
- `/analyzer-health-iterate [--dry-run] [--base=main] [--verification=full|analyzer|codefix|core|none] [--no-pr] [--no-release] [--auto-merge]`
- Selects the highest-priority work from `Current Shortlist` (falling back to the lowest-score highest-priority Health Baseline row).
- `/analyzer-health-iterate [--dry-run] [--base=main] [--verification=full|analyzer|codefix|core|none] [--no-pr] [--release] [--auto-merge]`
- Selects the highest-priority work from a numbered `Current Shortlist` item. If the shortlist has no numbered item, it refuses to start (monitor-only) instead of fabricating a target from the lowest-scored Health Baseline row — future work must be driven by a reproducible runtime or documentation mismatch.
- Requires a clean git tree and authenticated `gh`, updates the base branch, creates an `analyzer-health/...` branch, then sends pi a guarded implementation workflow.
- The workflow requires evidence collection, targeted/full verification, analyzer-health updates, release metadata preparation when releases are enabled, commit, PR, self-review, merge, and release tagging.
- When release tagging is enabled, the generated prompt tells the agent to infer the next semantic version before PR creation, update package versions, `CHANGELOG.md`, package release notes, and analyzer-health package/version notes where present, then rerun full verification.
- The workflow requires evidence collection, targeted/full verification, analyzer-health updates, commit, PR, self-review, and merge. Release tagging and release-metadata preparation are opt-in via `--release`.
- When `--release` is supplied, the generated prompt tells the agent to infer the next semantic version before PR creation, update package versions, `CHANGELOG.md`, package release notes, and analyzer-health package/version notes where present, then rerun full verification. Without `--release`, no versions, changelog, or release tags are touched.
- The generated prompt tells the agent to use body files or single-quoted heredocs for GitHub CLI markdown bodies so backticks are not executed by the shell.
- By default it pauses for explicit user confirmation before merge/tag; use `--auto-merge` only when you want unattended merge/release after checks pass.

Expand Down
55 changes: 29 additions & 26 deletions .pi/extensions/analyzer-health/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ interface AuditReport {
interface HealthWorkItem {
rank: number;
title: string;
source: "current-shortlist" | "health-baseline";
source: "current-shortlist";
rules: string[];
}

Expand Down Expand Up @@ -176,6 +176,14 @@ export default function analyzerHealthExtension(pi: ExtensionAPI) {

const options = parseIterationArgs(args);
const workItem = await selectHighestPriorityWork(ctx.cwd);
if (!workItem) {
ctx.ui.notify(
"Monitor-only: the Current Shortlist has no numbered work item, so there is nothing to iterate. Add a reproducible runtime or documentation mismatch to the shortlist first.",
"warning",
);
return;
}

const branchName = createBranchName(workItem);
const prompt = buildIterationPrompt(workItem, branchName, options);

Expand Down Expand Up @@ -205,7 +213,7 @@ export default function analyzerHealthExtension(pi: ExtensionAPI) {
});
}

async function selectHighestPriorityWork(cwd: string): Promise<HealthWorkItem> {
async function selectHighestPriorityWork(cwd: string): Promise<HealthWorkItem | undefined> {
const text = await readFile(join(cwd, "analyzer-health.md"), "utf8");
const shortlist = extractSection(text, "Current Shortlist");
const first = shortlist.split(/\r?\n/).map((line) => /^\s*(\d+)\.\s+(.+)$/.exec(line)).find(Boolean);
Expand All @@ -218,16 +226,10 @@ async function selectHighestPriorityWork(cwd: string): Promise<HealthWorkItem> {
};
}

const health = await parseHealth(cwd, "analyzer-health.md");
const priorityOrder: Record<string, number> = { P1: 1, P2: 2, P3: 3 };
const sorted = [...health.rules].sort((a, b) => {
const priority = (priorityOrder[a.priority] ?? 99) - (priorityOrder[b.priority] ?? 99);
if (priority !== 0) return priority;
return a.score - b.score;
});
const top = sorted[0];
if (!top) throw new Error("No Current Shortlist item or Health Baseline rows found in analyzer-health.md.");
return { rank: 1, title: `${top.rule} (${top.priority}, score ${top.score.toFixed(2)})`, source: "health-baseline", rules: [top.rule.split(" ")[0]] };
// Monitor-only: when the Current Shortlist has no numbered work item there is no actionable
// hardening target. Do not fabricate one from the lowest-scored Health Baseline rule — future
// work must be driven by a reproducible runtime or documentation mismatch.
return undefined;
}

async function prepareIterationBranch(pi: ExtensionAPI, cwd: string, baseBranch: string, branchName: string): Promise<string | undefined> {
Expand All @@ -253,18 +255,19 @@ async function prepareIterationBranch(pi: ExtensionAPI, cwd: string, baseBranch:
}

function parseIterationArgs(args: string): IterationArgs {
const options: IterationArgs = {
dryRun: false,
baseBranch: "main",
verification: "full",
autoMerge: false,
tagRelease: true,
pushPr: true,
};
for (const part of args.trim().split(/\s+/).filter(Boolean)) {
if (part === "--dry-run") options.dryRun = true;
else if (part === "--auto-merge") options.autoMerge = true;
else if (part === "--no-release") options.tagRelease = false;
const options: IterationArgs = {
dryRun: false,
baseBranch: "main",
verification: "full",
autoMerge: false,
tagRelease: false,
pushPr: true,
};
for (const part of args.trim().split(/\s+/).filter(Boolean)) {
if (part === "--dry-run") options.dryRun = true;
else if (part === "--auto-merge") options.autoMerge = true;
else if (part === "--release") options.tagRelease = true;
else if (part === "--no-release") options.tagRelease = false;
else if (part === "--no-pr") options.pushPr = false;
else if (part.startsWith("--base=")) options.baseBranch = part.slice("--base=".length) || options.baseBranch;
else if (part.startsWith("--verification=")) {
Expand All @@ -278,10 +281,10 @@ function parseIterationArgs(args: string): IterationArgs {
function buildIterationPrompt(workItem: HealthWorkItem, branchName: string, options: IterationArgs): string {
const releaseStep = options.tagRelease
? "After merge, tag the GitHub release version prepared in the PR. Do not tag if verification, release metadata, or PR checks fail."
: "Do not create a release tag for this run because --no-release was supplied.";
: "Do not create a release tag for this run because release tagging was not requested (pass --release to opt in).";
const releasePrepPolicy = options.tagRelease
? "Before opening the PR, prepare release metadata for the next semantic version inferred from project metadata/CHANGELOG: update package versions, CHANGELOG.md, package release notes, and analyzer-health.md package/version notes when those files exist. Then rerun full verification."
: "Do not change package versions, changelog, package release notes, or release-specific analyzer-health metadata because --no-release was supplied.";
: "Do not change package versions, changelog, package release notes, or release-specific analyzer-health metadata because release tagging was not requested (pass --release to opt in).";
const mergePolicy = options.autoMerge
? "If review and checks pass, merge the PR using gh without asking another confirmation."
: "Before merging the PR or creating any release tag, pause and ask the user for explicit confirmation with the PR URL, checks status, release version, and verification evidence.";
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to ConfigContraband will be documented in this file.

## 0.7.22 - 2026-07-21

- Fixed an infinite-loop analyzer hang in the `CFG002` constructor-overwrite proof. A public
parameterless constructor whose initializer is a zero-argument `: this()` (a self-cycle that is a
`CS0516` compile error, but well-formed broken-compilation input the analyzer must survive) made
the runtime-constructor walk re-select the same constructor forever, hanging the analyzer for any
`OptionsBuilder<T>` registration of such a type. The self-targeting zero-argument `: this()` is now
treated as an unprovable constructor chain — the satisfying default is conservatively rejected and
the missing required key reported — instead of spinning. No valid compiling program is affected
because self-delegating constructors are illegal. No diagnostic IDs, severities, messages, or code
fixes changed.

## 0.7.21 - 2026-07-21

- Fixed a potential `AD0001` analyzer crash: `BindConfiguration(...)` registration
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ConfigContraband (aka ConfiContraband / LinqContraband)

Roslyn analyzer for .NET configuration/Options validation. Rule IDs are `CFG0xx` (CFG001–CFG007) —
Roslyn analyzer for .NET configuration/Options validation. Rule IDs are `CFG0xx` (CFG001–CFG009) —
not `DI0xx`; that prefix belongs to a different project/skill template.

- Source of truth for current rule health, precision/recall scores, and known gaps:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Use it when your app relies on strongly typed options and you want configuration
## Install

```xml
<PackageReference Include="ConfigContraband" Version="0.7.21" PrivateAssets="all" />
<PackageReference Include="ConfigContraband" Version="0.7.22" PrivateAssets="all" />
```

The package includes `buildTransitive` props that pass visible `appsettings.json` and `appsettings.*.json` files to the analyzer automatically. Add the package, build, and let your editor or CI tell you when your options contract and configuration drift apart.
Expand Down
Loading
Loading