Refuses file writes that break a written Cure house rule (no cron in workflows, Vertex-only Gemini in Level5, no new AI vendors in minors' repos, no hard-coded…

The complete skill library that Cure Consulting Group uses to build apps, platforms, and products. These skills encode our standards, frameworks, and processes — so every project ships with the same level of rigor.
Now available as a Claude Code Plugin — install once, get auto-updates across all projects.
ProductEngineeringSkills/
├── .claude-plugin/ # Plugin manifest
│ └── plugin.json
├── skills/{domain}/ # 80 skills, organized by domain (engineering/platform/product/business/marketing/security/legal)
│ ├── sdlc/
│ ├── android-feature-scaffold/
│ ├── incident-response/ # NEW
│ ├── accessibility-audit/ # NEW
│ ├── performance-review/ # NEW
│ ├── database-architect/ # NEW
│ ├── infrastructure-scaffold/ # NEW
│ ├── project-bootstrap/
│ ├── e2e-testing/
│ ├── test-accounts/
│ ├── uat/
│ ├── compliance-architect/
│ ├── data-migration/
│ ├── feature-flags/
│ ├── release-management/
│ ├── observability/
│ ├── client-handoff/
│ ├── llmops/
│ ├── disaster-recovery/
│ ├── dora-metrics/
│ ├── design-system/
│ ├── client-communication/
│ ├── i18n/
│ ├── notification-architect/
│ ├── offline-first/
│ ├── chaos-engineering/
│ ├── edge-computing/
│ ├── finops/
│ ├── micro-frontends/
│ ├── growth-engineering/
│ ├── green-software/
│ ├── proposal-generator/
│ ├── api-gateway/
│ ├── ... (75 total — see docs/OVERVIEW.md for full inventory)
│ └── legal-doc-scaffold/
├── agents/ # 35 custom subagent definitions
├── personas/ # 4 cross-domain engagement archetypes
│ ├── code-reviewer.md # Security + quality review agent
│ ├── project-bootstrapper.md # New project setup agent
│ ├── test-runner.md # Execute test suites, report coverage
│ ├── pr-reviewer.md # Automated PR diff review
│ ├── refactor-assistant.md # Safe refactoring with test validation
│ ├── ci-debugger.md # Diagnose failed CI/CD runs
│ ├── release-coordinator.md # Version bump, changelog, deploy validation
│ ├── doc-generator.md # API docs, ADRs, changelogs from code
│ ├── codebase-explainer.md # Onboarding — explain architecture, trace flows
│ ├── migration-validator.md # Database migration safety checks
│ ├── deployment-validator.md # Pre-deployment checklist validation
│ ├── dependency-auditor.md # Vulnerability and outdated package audit
│ ├── api-validator.md # OpenAPI spec and contract validation
│ ├── product-analyst.md # Feature adoption, analytics instrumentation
│ ├── ux-researcher.md # Usability analysis, friction mapping
│ ├── roadmap-strategist.md # RICE scoring, dependency mapping, roadmaps
│ ├── competitive-intel.md # Feature matrices, positioning, moat analysis
│ ├── content-strategist.md # Editorial calendars, SEO, content briefs
│ ├── campaign-analyst.md # Attribution, funnel analysis, channel ROI
│ ├── brand-guardian.md # Voice/tone, visual identity, microcopy audit
│ ├── growth-analyst.md # Activation, retention, viral mechanics
│ ├── financial-analyst.md # Revenue forecasts, unit economics, scenarios
│ ├── market-intelligence.md # TAM/SAM/SOM, trends, market timing
│ ├── investor-relations.md # Board updates, KPIs, fundraising narratives
│ ├── contract-reviewer.md # SOW/contract risk, terms, IP review
│ ├── data-analyst.md # Schema exploration, queries, data quality
│ ├── metrics-dashboard.md # KPI definitions, SLOs, dashboard wireframes
│ ├── ab-test-analyst.md # Experiment design, statistical analysis
│ ├── qa-engineer.md # Test planning, edge cases, regression, quality gates
│ ├── accessibility-checker.md # WCAG 2.2 automated compliance
│ └── firebase-security-auditor.md # Firestore rules and Functions audit
├── hooks/ # Multi-layer automated enforcement
│ └── hooks.json # Command + Prompt hooks (9 event types)
├── rules/ # 11 path-specific coding standards
│ ├── android.md # Loads for *.kt files
│ ├── ios.md # Loads for *.swift files
│ ├── web.md # Loads for *.ts/*.tsx files
│ ├── firebase.md # Loads for functions/**
│ ├── python.md # Loads for *.py files
│ ├── go.md # Loads for *.go files
│ ├── rust.md # Loads for *.rs files
│ ├── sql.md # Loads for *.sql, migrations/**
│ ├── docker.md # Loads for Dockerfile, *.dockerfile
│ ├── terraform.md # Loads for *.tf, *.tfvars
│ └── cicd.md # Loads for .github/workflows/**
├── output-styles/ # 9 custom output formatting styles
│ ├── prd/ # Product docs (PRDs, GTM, research)
│ ├── code-generation/ # Code scaffolds and implementations
│ ├── financial-analysis/ # Cost models, SaaS metrics
│ ├── audit-report/ # Audits, reviews, compliance
│ ├── api-specification/ # OpenAPI specs, endpoint docs
│ ├── architecture-decision/ # ADRs, RFCs, trade-off matrices
│ ├── runbook/ # Incident runbooks, DR procedures
│ ├── test-plan/ # Test plans, coverage reports
│ └── monitoring-alert/ # Alert definitions, thresholds
├── .mcp.json # MCP server configs (GitHub, Sentry, Firestore, PostgreSQL)
├── .lsp.json # LSP server configs (TypeScript, Python/Pyright)
├── marketplace.json # Plugin marketplace manifest
├── settings.json # Default permission rules
├── claude-commands/ # Legacy format (backwards compat, 64 files)
├── gemini skills/ # Google Gemini skills (.skill ZIP)
├── CLAUDE.md # Project instructions (Claude)
├── GEMINI.md # Project instructions (Gemini CLI)
├── AGENT-GUIDE.md # How to structure prompts for agents & skills
├── setup.sh # Setup script for Antigravity & other projects
└── README.md
Install the plugin as an npm package from GitHub Packages. This is the easiest way to keep all your projects up to date.
1. Authenticate with GitHub Packages (one-time setup):
# Create a Personal Access Token (PAT) with read:packages scope at
# https://github.com/settings/tokens, then:
npm login --scope=@cure-consulting-group --registry=https://npm.pkg.github.com
Or add to your project's .npmrc:
@cure-consulting-group:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
2. Install in your project:
npm install @cure-consulting-group/product-engineering-skills
The postinstall script automatically:
~/.claude/plugins/ProductEngineeringSkills~/.claude/settings.jsonAll 80 skills, 39 agents, 4 personas, hooks, rules, and output styles are immediately available.
3. Enable auto-updates with Dependabot (recommended):
Add .github/dependabot.yml to your project (or run setup.sh which does this automatically):
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
allow:
- dependency-name: "@cure-consulting-group/product-engineering-skills"
labels:
- "dependencies"
- "skills-update"
commit-message:
prefix: "chore"
include: "scope"
Dependabot will open a PR in your project whenever a new version is published. Merge it and every agent on that project gets the updated skills.
4. Manual update:
npm update @cure-consulting-group/product-engineering-skills
# Load the plugin during a session
claude --plugin-dir /path/to/ProductEngineeringSkills
# Or for development/testing
claude --plugin-dir ./ProductEngineeringSkills
Once loaded, all skills are available as namespaced commands:
/cure-product-engineering:sdlc
/cure-product-engineering:feature-audit
/cure-product-engineering:android-feature-scaffold
/cure-product-engineering:incident-response
/cure-product-engineering:accessibility-audit
Hooks, agents, rules, output styles, and MCP servers are all included automatically.
The fastest way to onboard any project:
# From the target project directory
/path/to/ProductEngineeringSkills/setup.sh
# Or specify the project path
/path/to/ProductEngineeringSkills/setup.sh /path/to/antigravity-app
# Install globally for ALL projects
/path/to/ProductEngineeringSkills/setup.sh --global
# Legacy mode (just copy skills, no hooks/agents)
/path/to/ProductEngineeringSkills/setup.sh --legacy
The setup script will:
~/.claude/plugins/.claude/settings.local.json to .gitignore# Add the Cure Consulting marketplace
claude marketplace add https://github.com/Cure-Consulting-Group/ProductEngineeringSkills/marketplace.json
# Install the plugin
claude plugin install cure-product-engineering
Copy the claude-commands/ files into your project's .claude/commands/ directory:
cp claude-commands/*.md /path/to/your/project/.claude/commands/
Then use them as slash commands:
/sdlc — Generate SDLC artifacts
/android-feature-scaffold — Scaffold an Android feature module
/feature-audit — Audit a completed feature
Import the .skill files from gemini skills/ into your Gemini workspace. Each .skill file is a ZIP archive containing:
SKILL.md — The main skill definitionreferences/ — Supporting documents and templates| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| product-manager | OKRs, roadmaps, RICE prioritization, feature briefs | Yes |
| product-design | Apple HIG, Material Design 3, design tokens, accessibility-first | Yes |
| market-research | TAM/SAM/SOM, competitive analysis, ICP definition (read-only) | Yes |
| go-to-market | GTM plans, launch strategy, channel selection, growth playbooks | Yes |
| product-marketing | Brand strategy, messaging frameworks, campaigns | Yes |
| customer-onboarding | Activation flows, empty states, email sequences, retention | Yes |
| seo-content-engine | Technical SEO, structured data, content strategy | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| sdlc | PRDs, ADRs, RFCs, Epics, Stories, Task specs — full SDLC | Yes |
| android-feature-scaffold | Clean Architecture Android scaffolding (MVI, Compose, Hilt) | Yes |
| ios-architect | Swift/SwiftUI Clean Architecture, MVVM, structured concurrency | Yes |
| nextjs-feature-scaffold | App Router, Server/Client components, Tailwind patterns | Yes |
| firebase-architect | Firestore schema, security rules, Cloud Functions | Yes |
| api-architect | REST/GraphQL design, versioning, auth, rate limiting | Yes |
| api-gateway | API gateway and BFF layers, rate limiting, GraphQL federation | Yes |
| stripe-integration | Stripe payments + subscriptions via Firebase Functions | Yes |
| ai-feature-builder | LLM integration, RAG pipelines, prompt engineering | Yes |
| llmops | LLM operationalization — prompt versioning, eval pipelines, cost optimization, guardrails | Yes |
| database-architect | Schema design, migrations, indexing for Firestore/PostgreSQL/SQLite | Yes |
| data-migration | ETL pipelines, zero-downtime cutover, validation, rollback strategies | Yes |
| infrastructure-scaffold | Cloud infra configs for Firebase, GCP, Vercel, Docker | Yes |
| edge-computing | Edge functions, CDN strategies, cache invalidation, edge middleware | Yes |
| micro-frontends | Module federation, monorepo management, independent deployments | Yes |
| offline-first | Offline-first architecture, sync strategies, conflict resolution, optimistic UI | Yes |
| i18n | Internationalization — string extraction, RTL, locale-aware formatting, translation workflows | Yes |
| notification-architect | Push (FCM/APNs), in-app messaging, email, preference management | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| feature-audit | 5-phase post-completion audit with scored gap report | Yes (read-only, forked) |
| testing-strategy | Testing pyramid, platform standards, coverage rules | Yes |
| e2e-testing | E2E test suites with page objects, visual regression, CI integration | Yes |
| test-accounts | Test user personas, seed data scripts, environment credentials | Yes |
| uat | UAT plans, acceptance criteria checklists, go/no-go release gates | Yes |
| security-review | OWASP checklist, auth/data/API/mobile/web security | Yes (read-only, forked) |
| compliance-architect | HIPAA, COPPA, GDPR, PCI compliance frameworks, consent flows, audit trails | Yes |
| accessibility-audit | WCAG 2.2 compliance, screen readers, inclusive design | Yes (read-only, forked) |
| performance-review | Performance budgets, load testing, optimization strategies | Yes |
| chaos-engineering | Resilience testing, failure injection, graceful degradation, game days | Yes |
| green-software | Sustainable software practices, carbon-aware computing, energy efficiency | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| project-bootstrap | Bootstrap repo with CLAUDE.md + STATE.md via codebase inspection and developer interview | Yes |
| project-manager | Sprint planning, RACI, risk registers, retrospectives | Yes |
| ci-cd-pipeline | GitHub Actions, build/test/deploy, environments, secrets | Yes |
| release-management | App store submissions, staged rollouts, versioning, ASO, changelogs | Yes |
| feature-flags | Progressive rollouts, A/B testing, kill switches, experimentation frameworks | Yes |
| observability | Structured logging, distributed tracing, alerting, SLO/SLI, dashboards | Yes |
| dora-metrics | DORA and SPACE metrics — deployment frequency, lead time, MTTR, developer experience | Yes |
| analytics-implementation | Event taxonomy, tracking plans, funnels, dashboards | Yes |
| incident-response | Runbooks, severity classification, post-mortems, escalation | Yes |
| disaster-recovery | DR and business continuity — RTO/RPO, backup strategies, failover, DR testing | Yes |
| growth-engineering | Activation funnels, referral programs, lifecycle automation, PLG patterns | Yes |
| design-system | Design tokens, component libraries, Storybook/Catalog, cross-platform consistency | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| engineering-cost-model | Project estimates, infrastructure costs, build vs buy | Yes (read-only) |
| saas-financial-model | Unit economics, MRR/ARR, pricing tiers, break-even | Yes (read-only) |
| finops | Cloud cost optimization, budget alerts, resource right-sizing, FinOps practices | Yes |
| investor-reporting | Investor updates, board decks, portfolio financials, cap table, runway modeling | Yes |
| fundraising-materials | Pitch decks, data rooms, investor updates, cap table scenarios, fundraising pipeline | Yes |
| burn-rate-tracker | Burn rates, runway scenarios, break-even analysis, cash flow projections | Yes |
| legal-doc-scaffold | ToS, Privacy Policy, SOW, NDA scaffolds | No (manual only) |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| portfolio-registry | Product portfolio registry — single source of truth for all products, stacks, teams, stages | Yes |
| technology-radar | ThoughtWorks-style technology radar — Adopt/Trial/Assess/Hold across the portfolio | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| client-handoff | Handoff packages, runbooks, credential transfers, maintenance SLAs, knowledge transfer | Yes |
| client-communication | Sprint demo scripts, stakeholder updates, risk escalation, executive summaries | Yes |
| proposal-generator | Consulting proposals, SOWs, milestone pricing, engagement structure | No (manual only) |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| android-design-expert | Material Design 3 — dynamic color, component tokens, adaptive layouts, motion, Compose patterns | Yes |
| ios-design-expert | Apple HIG — SF Symbols, Dynamic Type, navigation patterns, SwiftUI components | Yes |
| web-design-expert | Responsive design, CSS architecture, design tokens, container queries, accessibility-first, Tailwind | Yes |
| stitch-design | AI-native UI design via Stitch MCP — vibe design, mockups, screen generation, design tokens, component export | Yes |
The library ships a standard maintenance loop (self-provisioned to .claude/loop.md on first session start — run bare /loop to use it), Recurring Mode sections in the goal-shaped skills (finops, burn-rate-tracker, investor-reporting, security-review, and others), and copy-paste cloud-routine recipes in docs/AUTOMATION.md. Library upkeep cadence: docs/MAINTENANCE.md. Mechanism selection and unattended-run guardrails: /cure-product-engineering:engagement-automation.
The plugin ships command and prompt hooks across 9 event types: SessionStart, PreCompact, PostCompact, ConfigChange, PostToolUseFailure, UserPromptSubmit, PreToolUse, Stop, SubagentStop. Highlights: a Stop-hook quality gate (blocks "done" without verification), a PreToolUse static security guard on skill/agent/persona files, and a ConfigChange audit trigger when skill files change mid-session.
New in v4.0: Hooks now suggest and auto-trigger agents based on context. Every code edit, test run, deployment, and PR action recommends the most relevant agent(s).
| Hook | Event | What It Does | Agent Integration |
|---|---|---|---|
| Welcome | SessionStart | Confirms plugin loaded with counts; full inventory stays in docs/OVERVIEW.md | Points to inventory |
| Git status | SessionStart | Reports current branch, uncommitted changes, last commit | — |
| Dependency check | SessionStart | Detects outdated packages | Suggests dependency-auditor |
| Code edit advisor | PostToolUse (Edit/Write) | Context-aware suggestions based on file type (.kt, .swift, .ts, .sql, .tf, etc.) | Suggests code-reviewer, test-runner, brand-guardian, migration-validator |
| Command advisor | PostToolUse (Bash) | Post-action guidance for tests, installs, deploys, PRs, releases | Suggests ci-debugger, dependency-auditor, pr-reviewer, release-coordinator |
| Failure recovery | PostToolUseFailure | Diagnoses failure type and suggests fix approach | Auto-suggests ci-debugger, deployment-validator, dependency-auditor |
| Destructive prompt guard | UserPromptSubmit | Detects destructive operations in prompts | Blocks and confirms |
| Protected files | PreToolUse (Edit/Write) | Blocks edits to .env, lock files, credentials, tfstate | — |
| Dangerous commands | PreToolUse (Bash) | Blocks force push, destructive rm, DROP TABLE, prod deploys | — |
| Context re-injection | PreCompact | Re-injects all 80 skills, 39 agents, 4 personas, and Cure standards | Full inventory preserved |
| Post-compact restore | PostCompact | Confirms context restored with agent availability | — |
| Subagent start banner | SubagentStart | Announces agent with role, standards, and companion agents | Lists companion agents |
| Subagent completion | SubagentStop | Suggests follow-up agents (test-runner, code-reviewer, pr-reviewer) | Agent chaining |
| Task quality check | TaskCompleted | Validates tests, security, docs, brand consistency | Suggests test-runner, code-reviewer, doc-generator, brand-guardian |
| Hook | Event | What It Does | Agent Integration |
|---|---|---|---|
| Code quality gate | PreToolUse (Edit/Write) | Haiku validates: no secrets, no debug logs, no disabled tests, no any types | — |
| Deployment safety | PreToolUse (Bash) | Haiku validates: blocks production deployments outside CI/CD | — |
| Intent classifier | UserPromptSubmit | Haiku classifies prompt intent and suggests the most relevant agent(s) from all 30 | Maps prompts → agents with confidence scores |
| Hook | Event | What It Does | Agent Integration |
|---|---|---|---|
| Completion validator | Stop | Validates: tests for new code, security review for sensitive changes, rollback for migrations, docs for features, brand consistency for UI, analytics for events, API contracts | Suggests specific agents for each gap found |
Pre-configured MCP servers in .mcp.json:
| Server | Type | What It Does |
|---|---|---|
| GitHub | HTTP | PR management, issue tracking, code search |
| Sentry | HTTP | Error monitoring, issue tracking, release health |
| Firestore | stdio | Direct database queries, schema inspection |
| PostgreSQL | stdio | Database queries, schema inspection, migrations |
Pre-configured LSP servers in .lsp.json:
| Server | Language | What It Provides |
|---|---|---|
| TypeScript | .ts, .tsx, .js | Type checking, auto-imports, refactoring, go-to-definition |
| Python (Pyright) | *.py | Static type analysis, import resolution, error diagnostics |
Custom output formatting for different artifact types:
| Style | Used By | Key Rules |
|---|---|---|
| prd | Product skills (PRDs, GTM, research) | Numbered sections, decision matrices, executive summaries |
| code-generation | Engineering skills (scaffolds) | File tree first, dependency order, complete runnable code |
| financial-analysis | Business skills (costs, models) | ASCII tables, explicit assumptions, sensitivity analysis |
| audit-report | Quality skills (audits, reviews) | Severity scoring, checklists, remediation with effort estimates |
| api-specification | API design skills | OpenAPI 3.0 blocks, endpoint tables, request/response examples |
| architecture-decision | ADR and RFC skills | Context/decision/consequences format, trade-off matrices |
| runbook | Incident response, disaster recovery | Numbered steps, command blocks, decision trees, escalation paths |
| test-plan | Testing strategy, QA skills | Coverage tables, test case templates, pass/fail criteria |
| monitoring-alert | Observability, incident response | Alert definition tables, threshold rationale, runbook links |
| Agent | Purpose | Tools | Auto-Triggered By |
|---|---|---|---|
| code-reviewer | Security + quality review against Cure standards | Read-only | Stop hook, SubagentStop |
| project-bootstrapper | Set up new projects with correct architecture |
hooks/register.ts 143 lines1import type { On } from 'claude-code'
2
3import { addedText, check, dependencyNames, parentDirs, targetPath, type Violation } from './rules'
4
5/**
6 * cure-policy-guard: refuses a file write that breaks a written Cure house
7 * rule, unless the person at the keyboard overrides it, and records every
8 * override.
9 *
10 * Scope: Claude's own Write / Edit / MultiEdit / NotebookEdit calls. A Bash
11 * command that writes a file (`cat > x`, `sed -i`) is not read here; the
12 * repos' CI and scanners remain the backstop for those.
13 *
14 * Fails closed on the question, open on the plumbing: if the person cannot be
15 * asked (headless, dismissed), the write is refused; if the repo or
16 * package.json cannot be read, the rules that need them see '' / [] and the
17 * path- and text-only rules still apply.
18 */
19
20const FILE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit'])
21const OVERRIDES_KEY = 'overrides'
22const MAX_OVERRIDES = 200
23
24type Override = { at: string; rule: string; path: string; repo: string }
25
26const REFUSE = 'Refuse the write'
27const ALLOW = 'Allow this once (logged)'
28
29export function register(on: On) {
30 on('session.start', async ($, e, next) => {
31 await $.command.register({
32 name: 'policy-guard',
33 description: 'Show the house rules the guard enforces and the recent overrides',
34 })
35 return next(e)
36 })
37
38 on('command.run', { command: 'policy-guard' }, async $ => {
39 const log = ((await $.store.get(OVERRIDES_KEY)) as Override[] | undefined) ?? []
40 const recent = log.slice(-10).reverse()
41 const lines = [
42 'cure-policy-guard rules: no-cron · level5-vertex-only · minors-new-ai-vendor · hardcoded-secret',
43 recent.length === 0
44 ? 'No overrides recorded.'
45 : 'Recent overrides:\n' +
46 recent.map(o => ` ${o.at} ${o.rule} ${o.repo} ${o.path}`).join('\n'),
47 ]
48 return { text: lines.join('\n') }
49 })
50
51 on('tool.call', async ($, e, next) => {
52 if (!FILE_TOOLS.has(String(e.tool))) return next(e)
53 const path = targetPath(e)
54 const added = addedText(e)
55 if (!path || !added) return next(e)
56
57 const { repo, root } = await locateRepo($, path)
58 const deps = await nearestDependencies($, path, root)
59 const violations = check({ path, repo, added, deps })
60 if (violations.length === 0) return next(e)
61
62 const summary = violations.map(v => `[${v.rule}] ${v.reason}`).join('\n')
63 let answer: string
64 try {
65 answer = await $.ui.ask(
66 `cure-policy-guard: this ${String(e.tool)} to ${shortPath(path)} breaks a house rule:\n${summary}\nAllow it anyway?`,
67 { options: [REFUSE, ALLOW], header: 'House rule' },
68 )
69 } catch {
70 return { deny: denial(violations, 'nobody could be asked to override it') }
71 }
72 if (answer !== ALLOW) return { deny: denial(violations, 'the user refused it') }
73
74 await recordOverride($, violations, path, repo)
75 $.ui.log(`cure-policy-guard: override recorded for ${violations.map(v => v.rule).join(', ')} on ${shortPath(path)}`)
76 return next(e)
77 })
78}
79
80function denial(violations: Violation[], why: string): string {
81 return (
82 `Blocked by cure-policy-guard (${why}). ` +
83 violations.map(v => `${v.rule}: ${v.reason} Source: ${v.source}.`).join(' ') +
84 ' Do not retry the same write; change the approach or ask the user.'
85 )
86}
87
88export const shortPath = (p: string, maxLen = 38): string => {
89 const parts = p.split('/').filter(Boolean)
90 const tail = parts.slice(-3).join('/')
91 if (tail.length <= maxLen) return tail
92 const filename = parts[parts.length - 1] ?? ''
93 if (parts.length <= 2) return tail
94 const dir = parts[parts.length - 2] ?? ''
95 const mid = `.../${dir}/${filename}`
96 return mid.length <= maxLen ? mid : `.../${filename}`
97}
98
99/** The repository's name (main checkout's directory, also from a worktree) and root. */
100async function locateRepo($: any, path: string): Promise<{ repo: string; root: string }> {
101 try {
102 const dir = await firstExistingDir($, path)
103 if (!dir) return { repo: '', root: '' }
104 const common = await $.process.run(['git', '-C', dir, 'rev-parse', '--path-format=absolute', '--git-common-dir'])
105 const top = await $.process.run(['git', '-C', dir, 'rev-parse', '--show-toplevel'])
106 if (common.exitCode !== 0 || top.exitCode !== 0) return { repo: '', root: '' }
107 const commonDir = common.stdout.trim().replace(/\/$/, '')
108 const repoDir = commonDir.endsWith('/.git') ? commonDir.slice(0, -5) : commonDir
109 return { repo: repoDir.split('/').pop() ?? '', root: top.stdout.trim() }
110 } catch {
111 return { repo: '', root: '' }
112 }
113}
114
115async function firstExistingDir($: any, path: string): Promise<string | undefined> {
116 for (const dir of parentDirs(path)) {
117 if (await $.fs.exists(dir)) return dir
118 }
119 return undefined
120}
121
122/** Dependencies of the nearest package.json between the file and the repo root. */
123async function nearestDependencies($: any, path: string, root: string): Promise<string[]> {
124 if (!root) return []
125 try {
126 for (const dir of parentDirs(path)) {
127 if (!dir.startsWith(root)) break
128 const pkg = `${dir}/package.json`
129 if (await $.fs.exists(pkg)) return dependencyNames(await $.fs.read(pkg))
130 }
131 } catch {
132 // unreadable: treat as no dependencies, which makes the vendor rule stricter
133 }
134 return []
135}
136
137async function recordOverride($: any, violations: Violation[], path: string, repo: string) {
138 const log = ((await $.store.get(OVERRIDES_KEY)) as Override[] | undefined) ?? []
139 const at = new Date(await $.clock.now()).toISOString()
140 for (const v of violations) log.push({ at, rule: v.rule, path, repo })
141 await $.store.set(OVERRIDES_KEY, log.slice(-MAX_OVERRIDES))
142}
143hooks/rules.ts 189 lines1/**
2 * The house rules the guard enforces, as pure functions of one file write.
3 *
4 * Each rule comes from a decision already written down in a Cure repo; the
5 * `source` says where, so a refusal can be traced to the rule and argued with.
6 * Rules look only at text a write ADDS, so an edit that leaves an existing
7 * violation alone is not blocked here (the repo's own CI owns old debt).
8 */
9
10export type Write = {
11 /** Absolute path of the file being written. */
12 path: string
13 /** Basename of the git repository root holding the file, or '' outside one. */
14 repo: string
15 /** The text this write introduces (whole file for Write, new_string for Edit). */
16 added: string
17 /** Dependency names in the nearest package.json, if one was found. */
18 deps: readonly string[]
19}
20
21export type Violation = {
22 rule: string
23 reason: string
24 source: string
25}
26
27const isEnvFile = (path: string) => /(^|\/)\.env(\.[^/]*)?$/.test(path)
28
29/** Org policy since 2026-08-08: no `schedule:` trigger in any workflow. */
30export function noCron(w: Write): Violation | undefined {
31 if (!/(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(w.path)) return undefined
32 if (!/^\s*schedule\s*:/m.test(w.added)) return undefined
33 return {
34 rule: 'no-cron',
35 reason:
36 'adds a `schedule:` trigger to a workflow. Org policy since 2026-08-08 bans cron in GitHub Actions; use push, PR, dispatch or an age gate on a push trigger instead.',
37 source: 'DistrictZero/CLAUDE.md "No cron. Org policy since 2026-08-08"; github_policy scanner',
38 }
39}
40
41const CONSUMER_GEMINI = /@google\/generative-ai\b|generativelanguage\.googleapis\.com|\bGEMINI_API_KEY\b/
42
43/** Level5 handles PHI: Gemini only through Vertex AI under the GCP BAA. */
44export function level5VertexOnly(w: Write): Violation | undefined {
45 if (w.repo !== 'Level5') return undefined
46 const hit = CONSUMER_GEMINI.exec(w.added)
47 if (!hit) return undefined
48 return {
49 rule: 'level5-vertex-only',
50 reason: `introduces \`${hit[0]}\`, the consumer Gemini API path. Level5 processes PHI, and only Vertex AI is covered by the Google Cloud BAA.`,
51 source: 'Level5/docs/compliance/BAA_INVENTORY.md:33-34, :48',
52 }
53}
54
55/** Repos whose data is about minors or students (COPPA/FERPA-adjacent). */
56export const MINORS_REPOS: ReadonlySet<string> = new Set([
57 'initiated-recruiting',
58 'initiated-recruiting-nil',
59 'SPEDTECH',
60 'LearnLift',
61 'iep-and-thrive',
62])
63
64/** Model-vendor SDKs, as an import names them. */
65export const AI_VENDOR_PACKAGES: readonly string[] = [
66 'openai',
67 '@anthropic-ai/sdk',
68 '@google/genai',
69 '@google/generative-ai',
70 '@google-cloud/vertexai',
71 'cohere-ai',
72 '@mistralai/mistralai',
73 'groq-sdk',
74 'together-ai',
75 'replicate',
76 '@typesafe-ai/sdk',
77 'typesafe',
78]
79
80const importedPackages = (text: string): string[] => {
81 const found = new Set<string>()
82 const re = /(?:from\s+|require\(\s*|import\(\s*|import\s+)['"]([^'"]+)['"]/g
83 for (const m of text.matchAll(re)) {
84 const spec = m[1]
85 const name = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0]
86 found.add(name)
87 }
88 return [...found]
89}
90
91/** A new AI vendor in a minors' repo needs a decision, not a default. */
92export function minorsNewAiVendor(w: Write): Violation | undefined {
93 if (!MINORS_REPOS.has(w.repo)) return undefined
94 const added = importedPackages(w.added).filter(
95 p => AI_VENDOR_PACKAGES.includes(p) && !w.deps.includes(p),
96 )
97 if (added.length === 0) return undefined
98 return {
99 rule: 'minors-new-ai-vendor',
100 reason: `imports ${added.map(p => `\`${p}\``).join(', ')}, an AI vendor this repo does not already depend on. ${w.repo} handles data about minors or students; a new processor needs a data-processing decision first (several vendors, TypeSafe among them, exclude under-18 data by policy).`,
101 source: 'Jev / AI-vendor review 2026-10-04; SPEDTECH privacy page "de-identified prompts only"',
102 }
103}
104
105const SECRET_PATTERNS: readonly [string, RegExp][] = [
106 ['Google API key', /AIza[0-9A-Za-z_-]{35}/],
107 ['Anthropic API key', /sk-ant-[0-9A-Za-z_-]{20,}/],
108 ['OpenAI API key', /\bsk-(?:proj-)?[0-9A-Za-z_-]{32,}/],
109 ['GitHub token', /\b(?:ghp|gho|ghs|ghu)_[0-9A-Za-z]{36}\b|\bgithub_pat_[0-9A-Za-z_]{40,}/],
110 ['Stripe secret key', /\b(?:sk|rk)_live_[0-9A-Za-z]{20,}/],
111 ['private key', /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/],
112 ['service-account key', /"private_key"\s*:\s*"-----BEGIN/],
113]
114
115/** A credential literal in a tracked file. `.env*` files are where keys belong. */
116export function hardcodedSecret(w: Write): Violation | undefined {
117 if (isEnvFile(w.path)) return undefined
118 for (const [kind, re] of SECRET_PATTERNS) {
119 if (re.test(w.added)) {
120 return {
121 rule: 'hardcoded-secret',
122 reason: `writes what looks like a ${kind} into ${w.path.split('/').pop()}. Keep credentials in Secret Manager, a GitHub secret or a gitignored .env, and read them at run time.`,
123 source: 'TIR scripts/data-fixes/*.js shipped a hard-coded Gemini key (found 2026-10-04)',
124 }
125 }
126 }
127 return undefined
128}
129
130export const RULES = [noCron, level5VertexOnly, minorsNewAiVendor, hardcodedSecret] as const
131
132/** Every rule the write breaks, in RULES order. */
133export function check(w: Write): Violation[] {
134 return RULES.map(rule => rule(w)).filter((v): v is Violation => v !== undefined)
135}
136
137/** The text a built-in file tool call adds; '' for a call this guard does not read. */
138export function addedText(e: Record<string, unknown>): string {
139 const str = (v: unknown) => (typeof v === 'string' ? v : '')
140 switch (e.tool) {
141 case 'Write':
142 return str(e.content)
143 case 'Edit':
144 return str(e.new_string)
145 case 'MultiEdit':
146 return Array.isArray(e.edits)
147 ? e.edits.map(x => str((x as Record<string, unknown>)?.new_string)).join('\n')
148 : ''
149 case 'NotebookEdit':
150 return str(e.new_source)
151 default:
152 return ''
153 }
154}
155
156/** The path a built-in file tool call writes; '' when it names none. */
157export function targetPath(e: Record<string, unknown>): string {
158 const p = e.file_path ?? e.notebook_path
159 return typeof p === 'string' ? p : ''
160}
161
162/** Dependency names from a package.json's text; [] when it does not parse. */
163export function dependencyNames(packageJson: string | undefined): string[] {
164 if (!packageJson) return []
165 try {
166 const pkg = JSON.parse(packageJson) as Record<string, Record<string, string> | undefined>
167 return [
168 ...Object.keys(pkg.dependencies ?? {}),
169 ...Object.keys(pkg.devDependencies ?? {}),
170 ...Object.keys(pkg.optionalDependencies ?? {}),
171 ...Object.keys(pkg.peerDependencies ?? {}),
172 ]
173 } catch {
174 return []
175 }
176}
177
178/** The directories above a path, nearest first, ending at '/'. */
179export function parentDirs(path: string): string[] {
180 const out: string[] = []
181 let dir = path.replace(/\/+$/, '')
182 while (dir.includes('/')) {
183 dir = dir.slice(0, dir.lastIndexOf('/'))
184 out.push(dir === '' ? '/' : dir)
185 if (dir === '') break
186 }
187 return out
188}
189