diff --git a/.agents/skills/setup/SKILL.md b/.agents/skills/setup/SKILL.md index 936f3d2..01d78c4 100644 --- a/.agents/skills/setup/SKILL.md +++ b/.agents/skills/setup/SKILL.md @@ -252,6 +252,8 @@ Setup looks healthy. Profile: QA_READY_LABEL — set / unset PLATFORM_COMPLIANCE_NOTES — set / unset CONVENTIONS_NOTES — set / unset + SENSITIVE_AREAS_GATE — "true" (default) / "false" + SENSITIVE_AREAS_CATEGORIES — set (custom list) / unset (default 5-category list) ``` A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -468,4 +470,4 @@ Add a note: `/start` can be used to create well-formed GitHub issues without wri - Never fetch more than 100 labels in a single command. `gh label list --limit 100` is the ceiling. - Do not skip any human gate in Phase 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. - + diff --git a/.claude/commands/setup.md b/.claude/commands/setup.md index 0adb239..4227f0e 100644 --- a/.claude/commands/setup.md +++ b/.claude/commands/setup.md @@ -247,6 +247,8 @@ Setup looks healthy. Profile: QA_READY_LABEL — set / unset PLATFORM_COMPLIANCE_NOTES — set / unset CONVENTIONS_NOTES — set / unset + SENSITIVE_AREAS_GATE — "true" (default) / "false" + SENSITIVE_AREAS_CATEGORIES — set (custom list) / unset (default 5-category list) ``` A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -463,4 +465,4 @@ Add a note: `/start` can be used to create well-formed GitHub issues without wri - Never fetch more than 100 labels in a single command. `gh label list --limit 100` is the ceiling. - Do not skip any human gate in Phase 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. - + diff --git a/.claude/review-agent-prompt.md b/.claude/review-agent-prompt.md index bea3999..8c3f873 100644 --- a/.claude/review-agent-prompt.md +++ b/.claude/review-agent-prompt.md @@ -85,7 +85,7 @@ If the PR touches any of the following surfaces, emit at least one `[CRITICAL]` Phrasing example: `[CRITICAL] PR modifies authentication logic — operator review required regardless of code quality.` -This gate exists because mistakes in these areas are high-blast-radius and cheap to silently miss. A clean-looking diff in auth code still warrants a human decision. +This gate exists because mistakes in these areas are high-blast-radius and cheap to silently miss. A clean-looking diff in a flagged area still warrants a human decision. ## Finding tags — semantic rubric @@ -152,4 +152,4 @@ If no findings: "No issues found. Code looks correct and follows project convent - Do not suggest improvements outside the scope of the PR. - Do not re-review files that haven't changed. - Your review is posted to GitHub for the audit trail — keep it professional. - + diff --git a/.codecannon.yaml b/.codecannon.yaml index 923536d..ac48d64 100644 --- a/.codecannon.yaml +++ b/.codecannon.yaml @@ -50,3 +50,10 @@ config: STALE_DAYS: "14" PLATFORM_COMPLIANCE_NOTES: "" CONVENTIONS_NOTES: "" + SENSITIVE_AREAS_GATE: "true" + SENSITIVE_AREAS_CATEGORIES: | + - Authentication or authorization logic + - Payments, billing, or financial transactions + - Secrets handling (API keys, tokens, credentials, encryption keys) + - Production configuration (deploy targets, prod env vars, prod-only feature flags) + - Destructive operations (e.g. `DROP TABLE`, `rm -rf`, `git push --force`, mass deletes, schema drops) diff --git a/.cursor/rules/setup.mdc b/.cursor/rules/setup.mdc index 8977e7d..c8fa0ac 100644 --- a/.cursor/rules/setup.mdc +++ b/.cursor/rules/setup.mdc @@ -253,6 +253,8 @@ Setup looks healthy. Profile: QA_READY_LABEL — set / unset PLATFORM_COMPLIANCE_NOTES — set / unset CONVENTIONS_NOTES — set / unset + SENSITIVE_AREAS_GATE — "true" (default) / "false" + SENSITIVE_AREAS_CATEGORIES — set (custom list) / unset (default 5-category list) ``` A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -469,4 +471,4 @@ Add a note: `/start` can be used to create well-formed GitHub issues without wri - Never fetch more than 100 labels in a single command. `gh label list --limit 100` is the ceiling. - Do not skip any human gate in Phase 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. - + diff --git a/.gemini/skills/setup/SKILL.md b/.gemini/skills/setup/SKILL.md index 4ab317b..0399fcc 100644 --- a/.gemini/skills/setup/SKILL.md +++ b/.gemini/skills/setup/SKILL.md @@ -252,6 +252,8 @@ Setup looks healthy. Profile: QA_READY_LABEL — set / unset PLATFORM_COMPLIANCE_NOTES — set / unset CONVENTIONS_NOTES — set / unset + SENSITIVE_AREAS_GATE — "true" (default) / "false" + SENSITIVE_AREAS_CATEGORIES — set (custom list) / unset (default 5-category list) ``` A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -468,4 +470,4 @@ Add a note: `/start` can be used to create well-formed GitHub issues without wri - Never fetch more than 100 labels in a single command. `gh label list --limit 100` is the ceiling. - Do not skip any human gate in Phase 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. - + diff --git a/config.schema.yaml b/config.schema.yaml index 34bfee2..c1cb67e 100644 --- a/config.schema.yaml +++ b/config.schema.yaml @@ -227,3 +227,22 @@ placeholders: default: "" category: review used_in: [review-agent] + + SENSITIVE_AREAS_GATE: + description: > + Controls whether the review agent forces a CRITICAL finding when the PR touches a sensitive surface (auth, payments, secrets, prod config, destructive ops by default). "true" (default) keeps the gate active; "false" removes the entire section from the rendered review agent prompt. Disable only on projects where the default categories genuinely do not apply (e.g. docs-only repos) or where you intend to customize them via SENSITIVE_AREAS_CATEGORIES. + default: "true" + category: review + used_in: [review-agent] + + SENSITIVE_AREAS_CATEGORIES: + description: > + Bulleted markdown list of surface categories the reviewer must force CRITICAL on, regardless of code quality. Replaces the default list entirely — to add to defaults, copy them and append. Only consulted when SENSITIVE_AREAS_GATE is "true". Use a YAML block scalar for multi-line content; each line should start with "- ". + default: | + - Authentication or authorization logic + - Payments, billing, or financial transactions + - Secrets handling (API keys, tokens, credentials, encryption keys) + - Production configuration (deploy targets, prod env vars, prod-only feature flags) + - Destructive operations (e.g. `DROP TABLE`, `rm -rf`, `git push --force`, mass deletes, schema drops) + category: review + used_in: [review-agent] diff --git a/skills/github-agile/review-agent.md b/skills/github-agile/review-agent.md index 9dbeb1b..550d299 100644 --- a/skills/github-agile/review-agent.md +++ b/skills/github-agile/review-agent.md @@ -85,19 +85,17 @@ Check these categories in order of priority: - Test coverage demands (flag only if a critical path has zero coverage) - Documentation completeness +{{#if SENSITIVE_AREAS_GATE}} ## Sensitive-area gate (force CRITICAL) If the PR touches any of the following surfaces, emit at least one `[CRITICAL]` finding identifying the area, **regardless of code quality** — the operator must explicitly approve before merge: -- Authentication or authorization logic -- Payments, billing, or financial transactions -- Secrets handling (API keys, tokens, credentials, encryption keys) -- Production configuration (deploy targets, prod env vars, prod-only feature flags) -- Destructive operations (e.g. `DROP TABLE`, `rm -rf`, `git push --force`, mass deletes, schema drops) +{{SENSITIVE_AREAS_CATEGORIES}} Phrasing example: `[CRITICAL] PR modifies authentication logic — operator review required regardless of code quality.` -This gate exists because mistakes in these areas are high-blast-radius and cheap to silently miss. A clean-looking diff in auth code still warrants a human decision. +This gate exists because mistakes in these areas are high-blast-radius and cheap to silently miss. A clean-looking diff in a flagged area still warrants a human decision. +{{/if}} ## Finding tags — semantic rubric diff --git a/skills/github-agile/setup.md b/skills/github-agile/setup.md index e6fded8..98f160a 100644 --- a/skills/github-agile/setup.md +++ b/skills/github-agile/setup.md @@ -250,6 +250,8 @@ Setup looks healthy. Profile: QA_READY_LABEL — set / unset PLATFORM_COMPLIANCE_NOTES — set / unset CONVENTIONS_NOTES — set / unset + SENSITIVE_AREAS_GATE — "true" (default) / "false" + SENSITIVE_AREAS_CATEGORIES — set (custom list) / unset (default 5-category list) ``` A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. diff --git a/sync.py b/sync.py index 43d993c..fd9626b 100755 --- a/sync.py +++ b/sync.py @@ -57,14 +57,23 @@ def _dequote(value): def parse_yaml_simple(text): - """Parse a simple flat YAML structure into a dict.""" + """Parse a simple flat YAML structure into a dict. + + Supports `|` block scalars for nested values (PLATFORM_COMPLIANCE_NOTES, + SENSITIVE_AREAS_CATEGORIES, etc.) — when a nested key's value is exactly `|`, + subsequent lines indented deeper than the key are captured verbatim until + indent drops to the key's level or below. + """ result = {} current_key = None + lines = text.splitlines() + i = 0 - for raw_line in text.splitlines(): - # Strip inline comments (but not inside quoted values) + while i < len(lines): + raw_line = lines[i] line = raw_line if not line.strip() or line.strip().startswith('#'): + i += 1 continue indent = len(line) - len(line.lstrip()) @@ -80,17 +89,47 @@ def parse_yaml_simple(text): result[key] = value else: result[key] = {} + i += 1 elif indent >= 2 and current_key is not None: - if stripped.startswith('- '): + if ':' in stripped and isinstance(result.get(current_key), dict): + key, _, value = stripped.partition(':') + key = key.strip() + value_raw = value.strip() + if value_raw == '|': + # Block scalar: capture indented body verbatim. + body_lines = [] + j = i + 1 + while j < len(lines): + body = lines[j] + if body.strip() == '': + body_lines.append('') + j += 1 + continue + body_indent = len(body) - len(body.lstrip()) + if body_indent <= indent: + break + # Strip exactly the block-scalar indent (parent-indent + 2). + block_indent = indent + 2 + body_lines.append(body[block_indent:] if len(body) >= block_indent else body.lstrip()) + j += 1 + # Trim trailing empty lines (default chomping). + while body_lines and body_lines[-1] == '': + body_lines.pop() + result[current_key][key] = '\n'.join(body_lines) + i = j + else: + result[current_key][key] = _dequote(value_raw) + i += 1 + elif stripped.startswith('- '): value = _dequote(stripped[2:].strip()) if not isinstance(result.get(current_key), list): result[current_key] = [] result[current_key].append(value) - elif ':' in stripped and isinstance(result.get(current_key), dict): - key, _, value = stripped.partition(':') - key = key.strip() - value = _dequote(value.strip()) - result[current_key][key] = value + i += 1 + else: + i += 1 + else: + i += 1 return result