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
4 changes: 3 additions & 1 deletion .agents/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ Setup looks healthy. Profile: <inferred 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`.
Expand Down Expand Up @@ -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.
<!-- generated by CodeCannon/sync.py | skill: setup | adapter: codex | hash: d5d997f8 | DO NOT EDIT — run CodeCannon/sync.py to regenerate -->
<!-- generated by CodeCannon/sync.py | skill: setup | adapter: codex | hash: c8f4433c | DO NOT EDIT — run CodeCannon/sync.py to regenerate -->
4 changes: 3 additions & 1 deletion .claude/commands/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ Setup looks healthy. Profile: <inferred 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`.
Expand Down Expand Up @@ -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.
<!-- generated by CodeCannon/sync.py | skill: setup | adapter: claude | hash: d15a6e01 | DO NOT EDIT — run CodeCannon/sync.py to regenerate -->
<!-- generated by CodeCannon/sync.py | skill: setup | adapter: claude | hash: 2173a91e | DO NOT EDIT — run CodeCannon/sync.py to regenerate -->
4 changes: 2 additions & 2 deletions .claude/review-agent-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
<!-- generated by CodeCannon/sync.py | skill: review-agent | adapter: claude | hash: 30f5425f | DO NOT EDIT — run CodeCannon/sync.py to regenerate -->
<!-- generated by CodeCannon/sync.py | skill: review-agent | adapter: claude | hash: 41482596 | DO NOT EDIT — run CodeCannon/sync.py to regenerate -->
7 changes: 7 additions & 0 deletions .codecannon.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,10 @@ config:
STALE_DAYS: "14"
PLATFORM_COMPLIANCE_NOTES: "<!-- No platform-specific compliance rules defined. Edit .codecannon.yaml to add them. -->"
CONVENTIONS_NOTES: "<!-- No project conventions defined. Edit .codecannon.yaml to add them. -->"
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)
4 changes: 3 additions & 1 deletion .cursor/rules/setup.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ Setup looks healthy. Profile: <inferred 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`.
Expand Down Expand Up @@ -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.
<!-- generated by CodeCannon/sync.py | skill: setup | adapter: cursor | hash: 0b8d48d6 | DO NOT EDIT — run CodeCannon/sync.py to regenerate -->
<!-- generated by CodeCannon/sync.py | skill: setup | adapter: cursor | hash: 579ad346 | DO NOT EDIT — run CodeCannon/sync.py to regenerate -->
4 changes: 3 additions & 1 deletion .gemini/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ Setup looks healthy. Profile: <inferred 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`.
Expand Down Expand Up @@ -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.
<!-- generated by CodeCannon/sync.py | skill: setup | adapter: gemini | hash: ff90a6d8 | DO NOT EDIT — run CodeCannon/sync.py to regenerate -->
<!-- generated by CodeCannon/sync.py | skill: setup | adapter: gemini | hash: ba7f4709 | DO NOT EDIT — run CodeCannon/sync.py to regenerate -->
19 changes: 19 additions & 0 deletions config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,22 @@ placeholders:
default: "<!-- No project conventions defined. Edit .codecannon.yaml to add them. -->"
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]
10 changes: 4 additions & 6 deletions skills/github-agile/review-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions skills/github-agile/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ Setup looks healthy. Profile: <inferred 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`.
Expand Down
57 changes: 48 additions & 9 deletions sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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

Expand Down
Loading