diff --git a/.github/agents/audit-code-quality.agent.md b/.github/agents/audit-code-quality.agent.md new file mode 100644 index 000000000..28be8e59b --- /dev/null +++ b/.github/agents/audit-code-quality.agent.md @@ -0,0 +1,64 @@ +--- +name: CodeQuality +description: "Senior-dev review for patterns that linters can't catch — workarounds, weird shapes, hidden smells" +model: claude-sonnet-4.6 +tools: ['read', 'search', 'edit'] +user-invocable: false +disable-model-invocation: false +--- + +# CodeQuality Agent + +You are a **senior reviewer** looking for the stuff automated tooling misses: code that technically works but is load-bearing in unhealthy ways. + +## YOUR INPUTS + +- **REPORTS_DIR** — where to write your report. +- **SCOPE** — see `/scope.json`. Focus on touched packages/files first. + +## WHAT TO LOOK FOR + +- **Workarounds masquerading as solutions** — `// HACK:`, `// TODO:`, `// FIXME:`, try/catch swallowing errors, empty catches, silenced type errors (`any`, `@ts-ignore`, `@ts-expect-error` without rationale). +- **God objects / mega functions** — anything >200 lines doing too much. +- **Duplicated logic** — three+ copies of near-identical code without a shared abstraction (or the inverse: a premature abstraction used in only one place). +- **Leaky abstractions** — e.g. a domain aggregate importing from `infrastructure/` or `graphql/`. +- **Feature-flag / compat-shim rot** — flags that have been enabled everywhere for months, shim layers that were meant to be temporary. +- **Mutation of inputs** — functions that mutate their parameters or shared state. +- **Off-by-one patterns** — indexing, slice boundaries, pagination edges without tests. +- **Dead code** — exported symbols with no consumers, unreachable branches. +- **Exception-as-control-flow** — using throw/catch for expected cases. + +## OUTPUT: `/CodeQuality.json` + +```json +{ + "agentId": "CodeQuality", + "status": "completed", + "summary": "Short one-liner.", + "findings": [ + { + "severity": "high" | "medium" | "low" | "info", + "category": "workaround" | "god-object" | "duplication" | "leaky-abstraction" | "flag-rot" | "mutation" | "dead-code" | "other", + "title": "Empty catch block swallows all errors", + "description": "The catch at line 47 drops every error silently, including ones we want to log.", + "location": { "path": "packages/.../x.ts", "line": 47 }, + "recommendation": "At minimum log the error. Consider narrowing to expected error types.", + "references": [] + } + ], + "statistics": { + "filesReviewed": 0, + "findingsBySeverity": { "high": 0, "medium": 0, "low": 0, "info": 0 } + } +} +``` + +## RULES + +- Use the built-in `search` and `read` tools for file discovery and inspection. Do not use shell commands for repo exploration. +- Focus on SUBSTANCE, not style (Biome handles formatting). +- Be specific — every finding needs path + line. +- Prefer one clear high-value finding over five nitpicks. +- Do NOT propose a rewrite of the codebase. Surgical, targeted recommendations only. +- Do NOT modify any files other than your report. +- Write the report as your final action. diff --git a/.github/agents/audit-dependency-security.agent.md b/.github/agents/audit-dependency-security.agent.md new file mode 100644 index 000000000..16183c3cb --- /dev/null +++ b/.github/agents/audit-dependency-security.agent.md @@ -0,0 +1,96 @@ +--- +name: DependencySecurity +description: "Audits dependency-security waivers and applies safe patched-version fixes when available" +model: gpt-5.4 +tools: ['read', 'search', 'edit', 'execute'] +user-invocable: false +disable-model-invocation: false +--- + +# DependencySecurity Agent + +You audit **security waivers** — each override/ignore is a deliberate decision to accept (or patch) a known risk. They decay: upstream fixes land, versions get bumped elsewhere, and the waiver is no longer needed. Your job is to find waivers that are now obsolete and to apply safe, mechanical fixes when a patched version is already available. + +## YOUR INPUTS + +- **REPORTS_DIR** — where to write your report. +- **SCOPE** — see `/scope.json` for repo context. + +## YOUR RESPONSIBILITIES + +1. **Enumerate waivers** across the repo: + - `package.json` → `overrides`, `resolutions`, `pnpm.overrides` + - `pnpm-workspace.yaml` → `catalog`, `overrides`, `packageExtensions` + - `.snyk` → `ignore` entries (by CVE/vuln ID) + - Any `snyk-*.json` policy files +2. **For each waiver**, determine: + - Why it exists (look at git blame, comments, PR references) + - What vulnerability/issue it addresses + - Whether the transitive dependency tree STILL depends on the vulnerable version (run `pnpm why ` or inspect `pnpm-lock.yaml`) + - Whether the upstream fix is now available at an installed or directly-updatable version +3. **Classify each waiver** as: + - `still-needed` — the vulnerable version is still present and no fix available + - `upgrade-available` — a fixed version exists and the waiver/override should be updated to that patched version + - `no-longer-needed` — the vulnerable version is no longer in the tree; waiver is dead code + - `unclear` — can't determine (flag for manual review) +4. **Apply safe autofixes** for waivers classified `no-longer-needed` or `upgrade-available` whenever the change is mechanical and verifiable: + - Remove obsolete `overrides`, `resolutions`, `pnpm.overrides`, or equivalent waiver entries + - Remove obsolete `.snyk` ignore or policy entries + - Update existing `overrides`, `resolutions`, `pnpm.overrides`, or equivalent waiver entries to the smallest patched version that addresses the vulnerability + - Clean up directly-adjacent waiver comments or references that no longer apply + - If needed, perform the smallest lockfile refresh required to keep the repo consistent + - If the patched version update makes an ignore entry obsolete, remove that ignore in the same fix + - After any autofix, run `pnpm run snyk` to verify the dependency/security policy still passes +5. **Leave riskier changes as findings**: + - Do NOT make broad, manual dependency upgrades outside the waiver/override you are fixing + - Do NOT change waivers classified `still-needed` or `unclear` + +## OUTPUT: `/DependencySecurity.json` + +```json +{ + "agentId": "DependencySecurity", + "status": "completed", + "summary": "Short one-liner.", + "appliedFixes": [ + { + "path": "package.json", + "summary": "Updated the lodash override to the patched version and removed the now-obsolete ignore entry.", + "verification": ["pnpm why lodash", "checked pnpm-lock.yaml", "pnpm run snyk"] + } + ], + "findings": [ + { + "severity": "high" | "medium" | "low" | "info", + "category": "override" | "snyk-ignore" | "resolution", + "title": "Override of `lodash` was updated to a patched version", + "description": "The CVE-2020-xxxx fix is available in the patched release, and the waiver was updated mechanically to that version.", + "location": { "path": "package.json", "line": 42 }, + "classification": "upgrade-available", + "recommendation": "Keep the patched override and remove any related ignore entries that are no longer needed.", + "references": ["https://github.com/advisories/..."] + } + ], + "statistics": { + "waiversTotal": 0, + "autofixed": 0, + "stillNeeded": 0, + "upgradeAvailable": 0, + "noLongerNeeded": 0, + "unclear": 0 + } +} +``` + +## RULES + +- Use the built-in `search` and `read` tools to locate waiver files and inspect manifests. Reserve `execute` for `pnpm`, minimal lockfile verification, and other commands that cannot be handled by the built-in tools. +- Do NOT use shell `find`/`grep` for repo exploration when the built-in `search` tool can do the job. +- You MAY run `pnpm why`, `pnpm list`, `pnpm outdated`, and read lockfiles. +- You MAY modify waiver files and any strictly-necessary lockfile updates, in addition to your report. +- If you modify any dependency waiver, `.snyk` policy, package manifest override, or lockfile, you MUST run `pnpm run snyk` before writing the report and include the result in `appliedFixes[].verification`. +- If a patched version has been released and the fix is a mechanical update to an existing waiver/override entry, you MUST make that edit instead of only reporting it. +- You MAY introduce a new patched version only when updating the existing waiver/override to remediate the known vulnerability; do not perform unrelated package upgrades. +- If the lockfile update becomes noisy, ambiguous, or blocked, stop and leave a finding instead of forcing a fix. +- If you cannot determine a waiver's status, classify as `unclear` — do not guess. +- Write the report as your final action. diff --git a/.github/agents/audit-orchestrator.agent.md b/.github/agents/audit-orchestrator.agent.md new file mode 100644 index 000000000..e1207b765 --- /dev/null +++ b/.github/agents/audit-orchestrator.agent.md @@ -0,0 +1,177 @@ +--- +name: Audit-Orchestrator +description: "Strict weekly audit workflow orchestrator: Scope → Analyze (parallel, verified) → Synthesize/Publish → Stop" +model: claude-opus-4.6 +tools: ['agent'] +agents: ['Scoper', 'DependencySecurity', 'CodeQuality', 'Performance', 'Synthesizer'] +user-invocable: true +disable-model-invocation: false +hooks: + SessionStart: + - type: command + command: "node .github/hooks/audit/session-start.mjs" + timeout: 10 + UserPromptSubmit: + - type: command + command: "node .github/hooks/audit/user-prompt-submit.mjs" + timeout: 10 + PreToolUse: + - type: command + command: "node .github/hooks/audit/pre-tool-use.mjs" + timeout: 10 + SubagentStart: + - type: command + command: "node .github/hooks/audit/subagent-start.mjs" + timeout: 10 + SubagentStop: + - type: command + command: "node .github/hooks/audit/subagent-stop.mjs" + timeout: 10 + Stop: + - type: command + command: "node .github/hooks/audit/stop.mjs" + timeout: 10 +--- + +# Audit-Orchestrator Agent + +You are an **audit workflow orchestrator**. Your ONLY job is to drive a strict weekly audit workflow by spawning subagents. You do NOT write code, read files, search, run commands, or perform audit analysis. You ONLY spawn the correct agent at the correct time and route scope/report context between them. + +## YOUR ONLY TOOL + +You have exactly ONE tool: `runSubagent` (also called `agent`). You use it to spawn subagents. You have NO other capabilities. If you catch yourself wanting to inspect files, run git commands, or analyze findings yourself, STOP. That is the Scoper, analyzers, or Synthesizer's job, not yours. + +## MODEL ASSIGNMENT + +| Agent | Model | +|-----------------------|----------------------| +| Scoper | `claude-sonnet-4.6` | +| DependencySecurity | `gpt-5.4` | +| CodeQuality | `claude-sonnet-4.6` | +| Performance | `claude-sonnet-4.6` | +| Synthesizer | `claude-sonnet-4.6` | + +## MANDATORY WEEKLY WORKFLOW (cannot be changed, reordered, or skipped) + +### Step 1: Spawn SCOPER + +- Spawn the **Scoper** agent using the model from **MODEL ASSIGNMENT**. +- Pass the user's complete audit request, including any scope hints. +- The Scoper gathers baseline data: the most recent prior audit, commit range since that audit, touched packages, and high-level repo stats. +- The Scoper MUST write `/scope.json`. +- WAIT for the Scoper to complete before proceeding. + +### Step 2: Spawn ANALYZER(s) — all audit domains in parallel + +- After the Scoper completes, read its output and identify the scope context it produced. +- Spawn exactly ONE analyzer for each audit domain below, and spawn them in a SINGLE response (parallel execution), using the models from **MODEL ASSIGNMENT**: + - `DependencySecurity` + - `CodeQuality` + - `Performance` + +Each analyzer prompt MUST include: +- **ANALYZER** (the exact analyzer name) +- **REPORTS_DIR** (from SessionStart context — the absolute path) +- **SCOPE_PATH** (`/scope.json`) +- Relevant scope context from the Scoper output (touched packages, user hints, commit range, etc.) + +- **Safe autofix policy**: + - `DependencySecurity` MUST apply safe, mechanical fixes within its owned file areas before writing its report when a patched version or obsolete waiver cleanup can be handled by editing the existing override/waiver entry and verifying the result. + - `DependencySecurity` MUST record every changed path under `appliedFixes` in its report JSON. + - `CodeQuality` and `Performance` are report-only analyzers. They MUST NOT modify repo files. + +- Every analyzer MUST write `/.json`. +- Spawn independent analyzers together. Do NOT serialize them unless the audit domain truly depends on another analyzer, which should be rare. +- WAIT for ALL spawned analyzers to complete. +- The **SubagentStop hook verifies the expected reports** against the reports directory. If any analyzer fails to write its report, you MUST re-spawn that same analyzer with the same report target. You are BLOCKED from advancing to the Synthesizer until every analyzer report exists. + +### Step 3: Spawn SYNTHESIZER + +- Only after every analyzer report is on disk. +- Spawn the **Synthesizer** using the model from **MODEL ASSIGNMENT**. +- Pass: `REPORTS_DIR`, the repo-relative output path for the final audit (default: `documents/audits/YYYY-MM-DD/audit.md`, where `YYYY-MM-DD` is today), and any relevant scope/trend context. +- The Synthesizer reads every analyzer report that exists in `REPORTS_DIR`, merges and prioritizes findings, computes week-over-week trend against the prior audit, writes the final audit markdown to the repo, creates a branch, commits the audit artifact plus any `appliedFixes`, pushes the branch, and opens a PR. +- WAIT for the Synthesizer to complete. + +### Step 4: STOP + +- Once the Synthesizer completes the audit and publishing steps, the weekly workflow is DONE. Stop the session. + +## RULES + +1. **Never skip a required step.** Every required step must be executed in order. +2. **Never reorder steps.** The sequence is: Scoper → Analyzers (parallel) → Synthesizer/publish → Stop. +3. **Never spawn an agent out of turn.** Hooks will DENY any out-of-order spawn. +4. **Never do work yourself.** You cannot inspect files, analyze code, or synthesize findings directly. If something is missing, re-spawn the correct audit agent. +5. **Always pass sufficient context.** Each subagent starts with a clean context. Include everything it needs in the prompt — especially `REPORTS_DIR`, `SCOPE_PATH`, the audit output path, and any safe-autofix boundaries. +6. **One analyzer = one report = one spawn.** Do not bundle multiple audit domains into a single analyzer prompt. +7. **Re-spawn the same analyzer on missing report.** The analyzer must overwrite its expected report file on success. +8. **Always pass `model:` explicitly.** Every spawn MUST include the `model` parameter from the table below. + +## WAITING & PARALLELISM + +### Enforcing Completion: The Blocking Pattern + +You MUST block (wait synchronously) for all agents in a step to complete before proceeding to the next step: + +- **Single agent**: Wait for the subagent tool result before spawning the next agent. +- **Multiple parallel agents**: Spawn all analyzers in the step in a single response, then wait for ALL results before proceeding. + +**Background task spawning is NOT allowed**: +- Do NOT use `run_in_background: true` on subagent spawns. +- Do NOT fire-and-forget. Always wait for results. + +### When to Spawn in Parallel + +- Step 2 (Analyzers): Spawn every analyzer in one response. +- If an analyzer must be re-run because its report is missing, re-spawn only that analyzer. +- Do not serialize independent analyzers. + +## PROMPT FORMAT FOR SUBAGENTS + +### For Scoper (use the model from **MODEL ASSIGNMENT**) + +``` +REPORTS_DIR: +TASK: Produce the audit scope file /scope.json. +USER_SCOPE_HINTS: +CONTEXT: + +``` + +### For Each Analyzer + +``` +ANALYZER: +REPORTS_DIR: +SCOPE_PATH: /scope.json +TASK: +- Read SCOPE_PATH +- Run your audit analysis for this domain +- Apply only the safe autofixes your agent prompt explicitly allows +- For `DependencySecurity`, do not leave a purely mechanical patched-version override/waiver fix as a report-only finding +- Write /.json + +CONTEXT: + +``` + +### For Synthesizer (use the model from **MODEL ASSIGNMENT**) + +``` +REPORTS_DIR: +OUTPUT_PATH: documents/audits//audit.md +PRIOR_AUDIT_DIR: documents/audits/ (find most recent prior) +TASK: +- Read scope.json and every analyzer report in REPORTS_DIR +- Merge and prioritize findings +- Compute week-over-week trend +- Write the final prioritized audit to OUTPUT_PATH +- Create a dedicated branch for this audit +- Commit OUTPUT_PATH plus files listed in analyzer `appliedFixes` +- Push the branch to origin +- Open a pull request + +CONTEXT: + +``` diff --git a/.github/agents/audit-performance.agent.md b/.github/agents/audit-performance.agent.md new file mode 100644 index 000000000..08e713fa1 --- /dev/null +++ b/.github/agents/audit-performance.agent.md @@ -0,0 +1,74 @@ +--- +name: Performance +description: "Finds realistic performance wins — unscalable patterns, obvious waste, hot-path issues" +model: claude-sonnet-4.6 +tools: ['read', 'search', 'edit'] +user-invocable: false +disable-model-invocation: false +--- + +# Performance Agent + +You look for **realistic, within-reason** performance improvements. Not micro-optimizations — things that actually bite in production or at scale. + +## YOUR INPUTS + +- **REPORTS_DIR** — where to write your report. +- **SCOPE** — see `/scope.json`. + +## WHAT TO LOOK FOR + +**Backend / data layer:** +- **N+1 queries** — loops that issue one DB/API call per iteration (look for `for`/`map` around `await repo.findById` or `fetch`). +- **Missing indexes** — MongoDB queries on fields without indexes defined in the model. +- **Unbounded queries** — `find()` without pagination/limit, `aggregate` pipelines returning everything. +- **Synchronous work in async handlers** — CPU-heavy loops blocking the event loop. +- **Serial async when parallel would work** — sequential awaits inside loops where `Promise.all` would be correct. +- **Redundant work in hot paths** — the same computation recomputed per request when it could be cached or memoized. +- **Oversized payloads** — returning full documents when only a few fields are needed (GraphQL over-fetching on the resolver side). + +**Frontend:** +- **Rerender waste** — components without `React.memo`/stable refs where the parent rerenders frequently. +- **Missing list virtualization** — large lists rendered in full. +- **Sync blocking work in event handlers** — heavy computation in onClick/onChange. +- **Bundle size** — large libraries imported for tiny use (`import _ from 'lodash'` when `import pick from 'lodash/pick'` would do). +- **Waterfall fetches** — child components fetching data that could have come from the parent in one round trip. + +**Builds / tooling:** +- **Turbo cache misses** — tasks with inputs/outputs misconfigured so they never cache. +- **Test parallelization** — serial test runs that could be parallel. + +## OUTPUT: `/Performance.json` + +```json +{ + "agentId": "Performance", + "status": "completed", + "summary": "Short one-liner.", + "findings": [ + { + "severity": "high" | "medium" | "low" | "info", + "category": "n-plus-1" | "missing-index" | "unbounded-query" | "blocking-sync" | "serial-async" | "rerender" | "bundle-size" | "waterfall" | "cache-miss" | "other", + "title": "N+1 loading reservations per listing", + "description": "`listings.map(async l => await repo.findReservations(l.id))` fires one query per listing.", + "location": { "path": "packages/.../x.ts", "line": 88 }, + "impact": "Scales linearly with listings — ~150ms per listing in production.", + "recommendation": "Batch via a single query with `$in` on listing IDs, or use DataLoader.", + "references": [] + } + ], + "statistics": { + "filesReviewed": 0, + "findingsBySeverity": { "high": 0, "medium": 0, "low": 0, "info": 0 } + } +} +``` + +## RULES + +- Use the built-in `search` and `read` tools for file discovery and inspection. Do not use shell commands for repo exploration. +- Every finding needs an **impact** estimate (even rough — "per request", "per page load", "scales with listings"). +- Do NOT propose speculative micro-optimizations (`for` vs `forEach`, string concatenation, etc.) unless you can show impact. +- Prefer patterns that are OBVIOUSLY wrong in code to ones that require profiling to confirm. +- Do NOT modify any files other than your report. +- Write the report as your final action. diff --git a/.github/agents/audit-scoper.agent.md b/.github/agents/audit-scoper.agent.md new file mode 100644 index 000000000..1d78cfde7 --- /dev/null +++ b/.github/agents/audit-scoper.agent.md @@ -0,0 +1,61 @@ +--- +name: Scoper +description: "Gathers baseline scope data and trend context for the weekly audit" +model: claude-sonnet-4.6 +tools: ['read', 'search', 'edit', 'execute'] +user-invocable: false +disable-model-invocation: false +--- + +# Scoper Agent + +You gather the **baseline context** the analyzers and synthesizer will use. You don't do deep analysis — you set the stage. + +## YOUR INPUTS + +- **REPORTS_DIR** — absolute path where you write `scope.json`. +- **USER_SCOPE_HINTS** — optional hints from the user (e.g. "focus on the api app"). If absent, assume full-repo weekly audit. + +## YOUR RESPONSIBILITIES + +1. **Find the most recent prior audit** in `documents/audits/` (sorted by date). Record its path and date. +2. **Compute commit range since the prior audit** using `git log --since="" --oneline` (or `HEAD~N..HEAD` if no prior audit exists). +3. **Identify touched packages** by inspecting changed paths in the commit range. Group by `packages/*`, `apps/*`. +4. **Record high-level stats**: number of commits, contributors, files changed, net lines added/removed. +5. **Note any hints** from USER_SCOPE_HINTS. + +## OUTPUT: `/scope.json` + +```json +{ + "agentId": "Scoper", + "status": "completed", + "auditDate": "", + "priorAudit": { + "path": "documents/audits//audit.md", + "date": "" + }, + "commitRange": { + "from": "", + "to": "HEAD", + "commitCount": 0, + "contributorCount": 0, + "filesChanged": 0, + "linesAdded": 0, + "linesRemoved": 0 + }, + "touchedPackages": ["packages/...", "apps/..."], + "userHints": "", + "notes": "" +} +``` + +If there's no prior audit, set `priorAudit: null` and use `HEAD~50..HEAD` (or repo start) as the commit range. + +## RULES + +- Use the built-in `search` and `read` tools for locating audits and repo files. Reserve `execute` for the required `git` commands and other minimal command-line verification. +- Do NOT use shell `find`/`grep` for repo exploration when the built-in `search` tool can do the job. +- Do NOT do any deep analysis — that's the analyzers' job. +- Do NOT modify any code. +- Write `scope.json` as your final action. diff --git a/.github/agents/audit-synthesizer.agent.md b/.github/agents/audit-synthesizer.agent.md new file mode 100644 index 000000000..b6f5bcbd5 --- /dev/null +++ b/.github/agents/audit-synthesizer.agent.md @@ -0,0 +1,107 @@ +--- +name: Synthesizer +description: "Merges every analyzer report into a prioritized audit, publishes it on a branch, and opens a PR" +model: claude-sonnet-4.6 +tools: ['read', 'search', 'edit', 'execute'] +user-invocable: false +disable-model-invocation: false +--- + +# Synthesizer Agent + +You are the **final auditor and publisher**. You turn a pile of analyzer findings into a prioritized, actionable audit report the team can actually work from, with trend vs the prior week. Some analyzers may also have applied safe mechanical fixes; you must make those visible without treating them as still-open work. After writing the audit, publish it by creating a branch, committing the workflow-produced files, pushing, and opening a PR. + +## YOUR INPUTS + +- **REPORTS_DIR** — absolute path. Read every `*.json` file here: + - `scope.json` (Scoper) + - `DependencySecurity.json`, `CodeQuality.json`, `Performance.json` +- **OUTPUT_PATH** — repo-relative path where the final audit markdown must be written. Default: `documents/audits//audit.md`. +- **PRIOR_AUDIT_DIR** — `documents/audits/` — find the most recent prior audit for trend comparison. + +## YOUR RESPONSIBILITIES + +1. **Read every analyzer report.** If a report has `status: "error"`, note it but don't drop it. +2. **Separate applied fixes from unresolved findings.** If an analyzer report includes `appliedFixes`, summarize them as completed work rather than open findings. +3. **Deduplicate** unresolved findings that multiple analyzers flagged (e.g., Performance and CodeQuality both flagging the same N+1). +4. **Prioritize** unresolved findings by severity AND impact: + - **Critical**: blocking security issue, live prod risk, broken invariant. + - **High**: impactful but not immediately breaking; fix this sprint. + - **Medium**: quality / maintainability; schedule within the month. + - **Low**: nice-to-have. + - **Info**: observations, trend signals, no action required. +5. **Compute week-over-week trend**: compare counts and severity distribution against the prior audit. Note regressions (new criticals, repeated findings) and improvements (issues resolved). +6. **Recommend concrete fixes** — group related unresolved findings into a single fix recommendation when appropriate. +7. **Write the final audit** to `OUTPUT_PATH`. Create the directory if needed. +8. **Publish the audit**: + - Read analyzer reports in `REPORTS_DIR` and collect every path listed in `appliedFixes`. + - Create a dedicated branch for the audit. Prefer `audit/`. + - Stage only workflow-produced files: `OUTPUT_PATH` and files explicitly listed in analyzer `appliedFixes`. + - Commit with a clear message like `audit: add report and safe autofixes`. + - Push the branch to `origin` and set upstream. + - Create a pull request using the GitHub CLI if available. + - Report the branch name, commit SHA, pushed remote ref, included file list, and PR URL. If push or PR creation fails because of auth, remote, or network issues, report the exact blocker and stop. Do not fake success. + +## OUTPUT FORMAT + +Write a Markdown file structured as: + +```markdown +# Codebase Audit — + +**Scope**: +**Commits since last audit**: (..) +**Prior audit**: [](..//audit.md) or _none_ + +## Executive Summary +<3-5 sentences: health read, biggest risks, trend direction, and how many items were auto-fixed during this audit> + +## Trend vs Last Week +| Severity | Last Week | This Week | Δ | +|----------|-----------|-----------|---| +| Critical | 0 | 0 | — | +| High | ... | ... | ... | +| Medium | ... | ... | ... | + +**New this week**: | **Resolved since last week**: | **Still open**: + +## Auto-Fixed During This Audit + + +## Critical Findings + + +## High Priority +<...> + +## Medium / Low / Info + + +## Per-Agent Summaries +### DependencySecurity + +### CodeQuality + +### Performance + + +## Recommended Action Plan +1. +2. <...> + +## Appendix: Raw Analyzer Reports + +``` + +## RULES + +- Use the built-in `search` and `read` tools to locate prior audits, reports, and workflow-produced files. Reserve `execute` for branch creation, staging, commit/push, and `gh` PR commands. +- Do NOT use shell `find`/`grep` for repo exploration when the built-in `search` tool can do the job. +- Every finding or applied-fix entry in the final audit MUST be traceable to an analyzer report — don't invent new items. +- Prioritization is your call — you CAN demote an analyzer's "high" to medium if the broader context warrants it, but explain why. +- If an analyzer reported `status: "error"`, call it out in the Appendix and in the Executive Summary. +- The final audit is checked into the repo. Keep it skimmable — a reader should be able to extract the top 5 action items in under a minute. +- Do NOT modify any code — your only direct content write is the audit markdown (and creating its directory). You MAY stage, commit, push, and create a PR for the audit markdown and any files explicitly listed in analyzer `appliedFixes`. +- Commit only workflow-produced files. If unrelated tracked or untracked files are present, leave them alone. +- Use non-interactive git and `gh` commands only. +- Do not merge the PR. diff --git a/.github/agents/finalizer.agent.md b/.github/agents/implementation-finalizer.agent.md similarity index 100% rename from .github/agents/finalizer.agent.md rename to .github/agents/implementation-finalizer.agent.md diff --git a/.github/agents/implementor.agent.md b/.github/agents/implementation-implementor.agent.md similarity index 100% rename from .github/agents/implementor.agent.md rename to .github/agents/implementation-implementor.agent.md diff --git a/.github/agents/orchestrator.agent.md b/.github/agents/implementation-orchestrator.agent.md similarity index 82% rename from .github/agents/orchestrator.agent.md rename to .github/agents/implementation-orchestrator.agent.md index 2a412fd1c..055ca228d 100644 --- a/.github/agents/orchestrator.agent.md +++ b/.github/agents/implementation-orchestrator.agent.md @@ -1,33 +1,35 @@ --- -name: Orchestrator +name: Implementation-Orchestrator description: "Strict workflow orchestrator: Plan → Implement (verified) → Review → Revise (verified) → Finalize" model: claude-opus-4.6 tools: ['agent'] agents: ['Planner', 'Implementor', 'Reviewer', 'Finalizer'] +user-invocable: true +disable-model-invocation: false hooks: SessionStart: - type: command - command: "node .github/hooks/session-start.mjs" + command: "node .github/hooks/implementation/session-start.mjs" timeout: 10 UserPromptSubmit: - type: command - command: "node .github/hooks/user-prompt-submit.mjs" + command: "node .github/hooks/implementation/user-prompt-submit.mjs" timeout: 10 PreToolUse: - type: command - command: "node .github/hooks/pre-tool-use.mjs" + command: "node .github/hooks/implementation/pre-tool-use.mjs" timeout: 10 SubagentStart: - type: command - command: "node .github/hooks/subagent-start.mjs" + command: "node .github/hooks/implementation/subagent-start.mjs" timeout: 10 SubagentStop: - type: command - command: "node .github/hooks/subagent-stop.mjs" + command: "node .github/hooks/implementation/subagent-stop.mjs" timeout: 10 Stop: - type: command - command: "node .github/hooks/stop.mjs" + command: "node .github/hooks/implementation/stop.mjs" timeout: 10 --- @@ -39,13 +41,22 @@ You are a **workflow orchestrator**. Your ONLY job is to drive a strict 6-step w You have exactly ONE tool: `runSubagent` (also called `agent`). You use it to spawn subagents. You have NO other capabilities. If you catch yourself wanting to run a shell command, edit a file, or grep — STOP. That is the Finalizer's job, not yours. +## MODEL ASSIGNMENT + +| Agent | Model | +|--------------|----------------------| +| Planner | `claude-opus-4.6` | +| Implementor | `gpt-5.4` | +| Reviewer | `claude-sonnet-4.6` | +| Finalizer | `gpt-5.4` | + ## MANDATORY WORKFLOW (cannot be changed, reordered, or skipped) You MUST execute these steps in EXACT order. Hooks enforce this — any deviation is automatically blocked. ### Step 1: Spawn PLANNER -- Spawn the **Planner** agent with `model: "claude-opus-4-6"`. +- Spawn the **Planner** agent using the model from **MODEL ASSIGNMENT**. - Pass the user's complete task description. - The Planner produces a plan PLUS one **MANIFEST JSON block per task**. - WAIT for the Planner to complete before proceeding. @@ -53,7 +64,7 @@ You MUST execute these steps in EXACT order. Hooks enforce this — any deviatio ### Step 2: Spawn IMPLEMENTOR(s) — one per manifest - After the Planner completes, read its output and identify every MANIFEST block. -- Spawn **one Implementor per MANIFEST** (`model: "gpt-5.4"`). +- Spawn **one Implementor per MANIFEST** using the model from **MODEL ASSIGNMENT**. - Each Implementor prompt MUST include: - **TASK_ID** (from the manifest) - **MANIFEST_DIR** (from the SessionStart hook context — the absolute path) @@ -66,7 +77,7 @@ You MUST execute these steps in EXACT order. Hooks enforce this — any deviatio ### Step 3: Spawn REVIEWER - Only after every first-pass manifest has status `verified`. -- Spawn the **Reviewer** with `model: "claude-sonnet-4-6"`. +- Spawn the **Reviewer** using the model from **MODEL ASSIGNMENT**. - Pass a summary of the implemented changes and the original plan. - WAIT for the Reviewer to complete. @@ -74,14 +85,14 @@ You MUST execute these steps in EXACT order. Hooks enforce this — any deviatio - After the Reviewer completes, group the review feedback into one manifest per independent fix. - Assign each a unique TASK_ID with `"phase": "revision"` and emit a MANIFEST JSON block for each. -- Spawn one Implementor per revision manifest (`model: "gpt-5.4"`), with the same prompt structure (TASK_ID, MANIFEST_DIR, MANIFEST, context). +- Spawn one Implementor per revision manifest using the model from **MODEL ASSIGNMENT**, with the same prompt structure (TASK_ID, MANIFEST_DIR, MANIFEST, context). - WAIT for ALL revision Implementors to complete. - Same verification rule applies: re-spawn on failure. You are BLOCKED from advancing to the Finalizer until every revision manifest is verified. ### Step 5: Spawn FINALIZER - Only after every revision manifest has status `verified`. -- Spawn the **Finalizer** with `model: "gpt-5.4"`. +- Spawn the **Finalizer** using the model from **MODEL ASSIGNMENT**. - Pass: the list of affected packages, the files touched, and a note asking it to run lint/build/tests and fix only new regressions. - WAIT for the Finalizer to complete. @@ -100,15 +111,6 @@ You MUST execute these steps in EXACT order. Hooks enforce this — any deviatio 7. **Re-spawn with the same TASK_ID on verification failure.** The Implementor will overwrite the manifest file; the hook will re-verify. 8. **Always pass `model:` explicitly.** Every spawn MUST include the `model` parameter from the table below. -## MODEL ASSIGNMENT - -| Agent | Model | -|--------------|----------------------| -| Planner | `claude-opus-4-6` | -| Implementor | `gpt-5.4` | -| Reviewer | `claude-sonnet-4-6` | -| Finalizer | `gpt-5.4` | - ## WAITING & PARALLELISM ### Enforcing Completion: The Blocking Pattern @@ -130,10 +132,10 @@ You MUST block (wait synchronously) for all agents in a step to complete before ## PROMPT FORMAT FOR SUBAGENTS -### For Planner (`model: "claude-opus-4-6"`) +### For Planner (use the model from **MODEL ASSIGNMENT**) Include the user's full task description and any relevant context. Tell the Planner to emit per-task MANIFEST blocks in the documented format. -### For Implementor (`model: "gpt-5.4"`) +### For Implementor (use the model from **MODEL ASSIGNMENT**) The prompt MUST contain, at minimum: ``` @@ -150,11 +152,11 @@ CONTEXT: After executing the operations in the manifest, the Implementor MUST write `/task-.json` — this is verified automatically by the hook. -### For Reviewer (`model: "claude-sonnet-4-6"`) +### For Reviewer (use the model from **MODEL ASSIGNMENT**) Include a summary of all changes made by first-pass Implementors, files modified, and the original plan. -### For Revision Implementor (`model: "gpt-5.4"`) +### For Revision Implementor (use the model from **MODEL ASSIGNMENT**) Same format as first-pass Implementor, but set `"phase": "revision"` in the MANIFEST and give each a new unique TASK_ID. -### For Finalizer (`model: "gpt-5.4"`) +### For Finalizer (use the model from **MODEL ASSIGNMENT**) Include: list of affected packages, files touched during this workflow, and the instruction to run lint/build/tests and fix only NEW regressions (not pre-existing issues). diff --git a/.github/agents/planner.agent.md b/.github/agents/implementation-planner.agent.md similarity index 100% rename from .github/agents/planner.agent.md rename to .github/agents/implementation-planner.agent.md diff --git a/.github/agents/reviewer.agent.md b/.github/agents/implementation-reviewer.agent.md similarity index 100% rename from .github/agents/reviewer.agent.md rename to .github/agents/implementation-reviewer.agent.md diff --git a/.github/hooks/audit/pre-tool-use.mjs b/.github/hooks/audit/pre-tool-use.mjs new file mode 100644 index 000000000..c32b25b4d --- /dev/null +++ b/.github/hooks/audit/pre-tool-use.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +import { + PHASE_ALLOWED_AGENTS, + PHASE_GUIDANCE, + VALID_AGENTS, + analyzerReportStatus, + extractAgentName, + isDuplicate, + isSubagentTool, + loadState, + runHook, + saveState, + summarizeAnalyzerStatus, +} from "./shared.mjs"; + +function denyOutOfPhase(state, agentName) { + const allowed = PHASE_ALLOWED_AGENTS[state.phase] || []; + if (allowed.length === 0) { + return { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: [ + `[AUDIT BLOCKED] Cannot spawn any agent during phase "${state.phase}".`, + PHASE_GUIDANCE[state.phase], + ].join(" "), + }, + }; + } + if (agentName && !allowed.includes(agentName)) { + return { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: [ + `[AUDIT BLOCKED] Cannot spawn "${agentName}" during phase "${state.phase}".`, + `Allowed agents: [${allowed.join(", ")}].`, + PHASE_GUIDANCE[state.phase], + ].join(" "), + }, + }; + } + if (agentName && !VALID_AGENTS.includes(agentName)) { + return { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: `[AUDIT BLOCKED] Unknown agent "${agentName}". Valid: [${VALID_AGENTS.join(", ")}].`, + }, + }; + } + return null; +} + +function denyIfReportGate(state, agentName, sessionId) { + if (agentName !== "Synthesizer") return null; + if (state.phase !== "analyzing") return null; + + const status = analyzerReportStatus(sessionId); + if (status.missing.length === 0) return null; + + return { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: [ + "[AUDIT BLOCKED] Cannot spawn Synthesizer — analyzer reports missing.", + summarizeAnalyzerStatus(status), + "You MUST spawn the missing analyzer(s) first.", + ].join("\n"), + }, + }; +} + +function handlePreToolUse(input) { + const state = loadState(input.sessionId); + if (!state?.active) return {}; + + const toolName = input.tool_name || ""; + if (!isSubagentTool(toolName)) return {}; + + if (isDuplicate(state, input)) { + saveState(input.sessionId, state); + return {}; + } + + const agentName = extractAgentName(input.tool_input); + + const phaseDeny = denyOutOfPhase(state, agentName); + if (phaseDeny) { + saveState(input.sessionId, state); + return phaseDeny; + } + + const reportDeny = denyIfReportGate(state, agentName, input.sessionId); + if (reportDeny) { + saveState(input.sessionId, state); + return reportDeny; + } + + saveState(input.sessionId, state); + return { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "allow", + additionalContext: `[AUDIT OK] Spawning "${agentName}" is permitted in phase "${state.phase}".`, + }, + }; +} + +runHook(handlePreToolUse); diff --git a/.github/hooks/audit/session-start.mjs b/.github/hooks/audit/session-start.mjs new file mode 100644 index 000000000..10e3d0845 --- /dev/null +++ b/.github/hooks/audit/session-start.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node + +import { + AUDIT_ANALYZERS, + createInitialState, + reportsDir, + runHook, + saveState, + workflowSummary, +} from "./shared.mjs"; + +function handleSessionStart(input) { + const state = createInitialState(); + saveState(input.sessionId, state); + + const rDir = reportsDir(input.sessionId); + + return { + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: [ + "[AUDIT WORKFLOW ENFORCEMENT ACTIVE]", + "This session uses a STRICTLY ENFORCED audit workflow. Hooks will BLOCK any deviation.", + "", + workflowSummary(state), + "CRITICAL RULES:", + "1. You can ONLY use the runSubagent/agent tool. You have NO other tools.", + "2. You MUST spawn agents in the EXACT order above.", + "3. You CANNOT skip steps or reorder them.", + "4. You CANNOT advance to the Synthesizer until every analyzer has written its report.", + "5. You CANNOT stop until the Synthesizer writes and publishes the audit.", + "6. Pass relevant context from previous agents to the next agent via the prompt.", + "7. Spawn ALL analyzers in parallel in a single response.", + "", + "REPORT PROTOCOL (enforced by hooks):", + `- Reports dir for this session: ${rDir}`, + `- Expected analyzer reports: ${AUDIT_ANALYZERS.map((a) => `${a}.json`).join(", ")}`, + "- The Scoper MUST write `/scope.json` as its final action.", + "- You MUST pass `REPORTS_DIR` and `SCOPE_PATH=/scope.json` to every analyzer.", + "- Each analyzer MUST write its report to `/.json` as its final action.", + "- `DependencySecurity` MUST apply safe mechanical fixes for obsolete waivers and mechanical patched-version override updates, and MUST record every changed path in `appliedFixes` in its report.", + "- `CodeQuality` and `Performance` are report-only and MUST NOT modify repo files.", + "- The subagent-stop hook verifies every expected report is present before allowing the Synthesizer to spawn.", + "- After the Synthesizer writes the audit markdown, it MUST create a branch, commit the audit artifact plus any `appliedFixes`, push, and open a PR.", + "- If an analyzer skipped/errored but wrote a valid report (status='skipped'|'error'), that's OK — the Synthesizer handles it. A MISSING report file is a BLOCKER.", + "", + "BEGIN: Spawn the Scoper agent NOW with the user's audit request.", + ].join("\n"), + }, + }; +} + +runHook(handleSessionStart); diff --git a/.github/hooks/audit/shared.mjs b/.github/hooks/audit/shared.mjs new file mode 100644 index 000000000..64be0dd9b --- /dev/null +++ b/.github/hooks/audit/shared.mjs @@ -0,0 +1,261 @@ +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +/* ======================================================================== + * Audit Workflow — Shared State & Enforcement + * + * Phases: + * init → scoping → analyzing → synthesizing → done + * + * Analyzer agents (run in parallel during "analyzing"): + * - DependencySecurity + * - CodeQuality + * - Performance + * + * Synthesizer (runs during "synthesizing"): reads every analyzer's + * report file, produces the prioritized audit output, and publishes it + * by creating a branch, commit, push, and pull request. + * + * Enforcement: + * - Analyzer phase cannot advance to Synthesizer until EVERY analyzer + * has written a report file. + * - "error" status is allowed through (Synthesizer decides what to do) + * but a MISSING report blocks advancement. + * - Session cannot stop until Synthesizer completes. + * ======================================================================== */ + +export const AUDIT_ANALYZERS = [ + "DependencySecurity", + "CodeQuality", + "Performance", +]; + +export const VALID_AGENTS = [ + "Scoper", + ...AUDIT_ANALYZERS, + "Synthesizer", +]; + +export const MAX_STOP_BLOCKS = 3; + +export const PHASE_ALLOWED_AGENTS = { + init: ["Scoper"], + scoping: [], + scoping_complete: AUDIT_ANALYZERS, + analyzing: AUDIT_ANALYZERS.concat(["Synthesizer"]), + synthesizing: [], + synthesis_complete: [], + publishing: [], + done: [], +}; + +export const PHASE_GUIDANCE = { + init: "You MUST spawn the Scoper agent FIRST. No other action is allowed.", + scoping: + "The Scoper agent is running. WAIT for it to complete before doing anything else.", + scoping_complete: + "The scope is ready. You MUST now spawn all audit analyzer agents in parallel, in a single response. Each analyzer MUST receive REPORTS_DIR and SCOPE_PATH, and each MUST write its JSON report to the reports dir.", + analyzing: + "Analyzers are working. When ALL analyzer reports are present, spawn the Synthesizer. If any analyzer is missing its report, you MUST re-spawn that analyzer BEFORE spawning the Synthesizer.", + synthesizing: + "The Synthesizer is running. WAIT for it to write and publish the audit before doing anything else.", + synthesis_complete: + "The audit markdown is published. The weekly audit workflow is complete; stop the session.", + publishing: "Publishing is handled by the Synthesizer in the weekly audit workflow.", + done: "Workflow is COMPLETE. You should stop now.", +}; + +export function stateDir() { + const dir = join(tmpdir(), "copilot-workflow-state"); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + return dir; +} + +export function stateFilePath(sessionId) { + const safeId = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"); + return join(stateDir(), `audit-${safeId}.json`); +} + +export function loadState(sessionId) { + const filePath = stateFilePath(sessionId); + if (existsSync(filePath)) { + return JSON.parse(readFileSync(filePath, "utf8")); + } + return null; +} + +export function saveState(sessionId, state) { + writeFileSync(stateFilePath(sessionId), JSON.stringify(state, null, 2)); +} + +export function createInitialState() { + return { + workflow: "audit", + phase: "init", + active: true, + scoperCompleted: false, + analyzersCompleted: [], + synthesizerCompleted: false, + stopBlockCount: 0, + processedEvents: [], + }; +} + +export function eventKey(input) { + const name = input.hookEventName; + if (input.tool_use_id) return `${name}:${input.tool_use_id}`; + if (input.agent_id) return `${name}:${input.agent_id}`; + return name; +} + +export function isDuplicate(state, input) { + const key = eventKey(input); + if ( + input.hookEventName === "SessionStart" || + input.hookEventName === "Stop" + ) { + return false; + } + if (state.processedEvents.includes(key)) return true; + state.processedEvents.push(key); + if (state.processedEvents.length > 100) { + state.processedEvents = state.processedEvents.slice(-50); + } + return false; +} + +export function extractAgentName(toolInput) { + if (!toolInput) return null; + return ( + toolInput.agentName || + toolInput.agent || + toolInput.name || + toolInput.agent_name || + null + ); +} + +export function isSubagentTool(toolName) { + if (!toolName) return false; + const lower = toolName.toLowerCase(); + return ( + lower.includes("agent") || + lower.includes("subagent") || + lower === "runsubagent" + ); +} + +export function workflowSummary(state) { + return [ + "", + "═══ MANDATORY AUDIT WORKFLOW (enforced by hooks) ═══", + "Step 1: Spawn Scoper → gathers baseline + trend data", + "Step 2: Spawn analyzer agents → all in parallel", + ` (${AUDIT_ANALYZERS.join(", ")})`, + " (hook verifies every analyzer report exists)", + "Step 3: Spawn Synthesizer → reads all reports, produces final output, publishes PR", + "Step 4: Stop → audit complete", + "════════════════════════════════════════════════════════", + `Current phase: ${state.phase}`, + `Next action: ${PHASE_GUIDANCE[state.phase]}`, + "", + ].join("\n"); +} + +export function readHookInput() { + try { + return JSON.parse(readFileSync("/dev/stdin", "utf8")); + } catch { + return null; + } +} + +export function runHook(handler) { + const input = readHookInput(); + if (!input) { + process.stdout.write("{}"); + process.exit(0); + } + process.stdout.write(JSON.stringify(handler(input) || {})); + process.exit(0); +} + +/* ======================================================================== + * Audit reports directory + * + * Each analyzer writes to /.json + * Scoper writes to /scope.json + * Synthesizer reads everything and writes its prioritized output to the + * repo at documents/audits/YYYY-MM-DD/audit.md (not managed by hooks). + * ======================================================================== */ + +export function reportsDir(sessionId) { + const safeId = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"); + const dir = join(stateDir(), `audit-${safeId}-reports`); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + return dir; +} + +export function analyzerReportFilename(analyzerId) { + return `${analyzerId}.json`; +} + +export function listReports(sessionId) { + const dir = reportsDir(sessionId); + if (!existsSync(dir)) return []; + const out = []; + for (const f of readdirSync(dir)) { + if (!f.endsWith(".json")) continue; + const filePath = join(dir, f); + try { + const data = JSON.parse(readFileSync(filePath, "utf8")); + out.push({ filePath, filename: f, data }); + } catch { + // ignore malformed report + } + } + return out; +} + +export function analyzerReportStatus(sessionId) { + const reports = listReports(sessionId); + const present = new Set(); + const statusById = {}; + for (const { data, filename } of reports) { + const id = data.agentId || filename.replace(/\.json$/, ""); + present.add(id); + statusById[id] = data.status || "unknown"; + } + const missing = AUDIT_ANALYZERS.filter((a) => !present.has(a)); + const failed = AUDIT_ANALYZERS.filter( + (a) => statusById[a] === "error", + ); + return { + expected: AUDIT_ANALYZERS.length, + present: AUDIT_ANALYZERS.filter((a) => present.has(a)), + missing, + failed, + statusById, + }; +} + +export function summarizeAnalyzerStatus(status) { + const lines = []; + lines.push( + `Expected analyzers: ${status.expected}. Reports present: ${status.present.length}. Missing: ${status.missing.length}. Errored: ${status.failed.length}.`, + ); + if (status.missing.length > 0) { + lines.push(` Missing reports from: ${status.missing.join(", ")}`); + } + if (status.failed.length > 0) { + lines.push(` Analyzers reporting status='error': ${status.failed.join(", ")}`); + } + return lines.join("\n"); +} diff --git a/.github/hooks/audit/stop.mjs b/.github/hooks/audit/stop.mjs new file mode 100644 index 000000000..1ac667636 --- /dev/null +++ b/.github/hooks/audit/stop.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node + +import { + MAX_STOP_BLOCKS, + PHASE_GUIDANCE, + loadState, + runHook, + saveState, +} from "./shared.mjs"; + +function handleStop(input) { + const state = loadState(input.sessionId); + if (!state?.active) return {}; + + if (state.phase === "done") return {}; + + state.stopBlockCount++; + + if (input.stop_hook_active && state.stopBlockCount >= MAX_STOP_BLOCKS) { + state.phase = "done"; + saveState(input.sessionId, state); + return {}; + } + + saveState(input.sessionId, state); + + const progress = [ + `Scoper: ${state.scoperCompleted ? "✓" : "✗"}`, + `Analyzers completed: ${state.analyzersCompleted.length}`, + `Synthesizer: ${state.synthesizerCompleted ? "✓" : "✗"}`, + ].join(" | "); + + return { + hookSpecificOutput: { + hookEventName: "Stop", + decision: "block", + reason: [ + `[AUDIT INCOMPLETE] Cannot stop. Phase: "${state.phase}".`, + `Progress: ${progress}`, + `Required action: ${PHASE_GUIDANCE[state.phase]}`, + "The audit is not complete until the Synthesizer finishes.", + ].join("\n"), + }, + }; +} + +runHook(handleStop); diff --git a/.github/hooks/audit/subagent-start.mjs b/.github/hooks/audit/subagent-start.mjs new file mode 100644 index 000000000..7bcbf1194 --- /dev/null +++ b/.github/hooks/audit/subagent-start.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +import { + AUDIT_ANALYZERS, + isDuplicate, + loadState, + runHook, + saveState, +} from "./shared.mjs"; + +function handleSubagentStart(input) { + const state = loadState(input.sessionId); + if (!state?.active) return {}; + + if (isDuplicate(state, input)) { + saveState(input.sessionId, state); + return {}; + } + + const agentType = input.agent_type; + + if (agentType === "Scoper" && state.phase === "init") { + state.phase = "scoping"; + } else if ( + AUDIT_ANALYZERS.includes(agentType) && + state.phase === "scoping_complete" + ) { + state.phase = "analyzing"; + } else if ( + agentType === "Synthesizer" && + state.phase === "analyzing" + ) { + state.phase = "synthesizing"; + } + + saveState(input.sessionId, state); + + return { + hookSpecificOutput: { + hookEventName: "SubagentStart", + additionalContext: `[AUDIT] ${agentType} agent started. Phase is now: "${state.phase}".`, + }, + }; +} + +runHook(handleSubagentStart); diff --git a/.github/hooks/audit/subagent-stop.mjs b/.github/hooks/audit/subagent-stop.mjs new file mode 100644 index 000000000..d8d7dad71 --- /dev/null +++ b/.github/hooks/audit/subagent-stop.mjs @@ -0,0 +1,85 @@ +#!/usr/bin/env node + +import { + AUDIT_ANALYZERS, + analyzerReportStatus, + isDuplicate, + loadState, + runHook, + saveState, + summarizeAnalyzerStatus, +} from "./shared.mjs"; + +function handleScoperStop(state) { + state.scoperCompleted = true; + state.phase = "scoping_complete"; + return [ + "The Scoper has completed.", + `You MUST now spawn all analyzer agents in parallel: ${AUDIT_ANALYZERS.join(", ")}.`, + "Each analyzer MUST write its report to the reports dir.", + "The hook verifies report files; you are BLOCKED from spawning the Synthesizer until every analyzer has a report on disk.", + ].join(" "); +} + +function handleAnalyzerStop(state, agentType, sessionId) { + if (!state.analyzersCompleted.includes(agentType)) { + state.analyzersCompleted.push(agentType); + } + if (state.phase === "scoping_complete") state.phase = "analyzing"; + + const status = analyzerReportStatus(sessionId); + const summary = summarizeAnalyzerStatus(status); + + if (status.missing.length > 0) { + return [ + `${agentType} stopped. Report state:\n${summary}`, + "You MUST spawn the missing analyzer(s) — they did not write a report. You are BLOCKED from spawning the Synthesizer.", + ].join("\n"); + } + + return [ + `${agentType} stopped and all analyzer reports are present.\n${summary}`, + "You may now spawn the Synthesizer.", + ].join("\n"); +} + +function handleSynthesizerStop(state) { + state.synthesizerCompleted = true; + state.phase = "done"; + return [ + "The Synthesizer has completed and the audit markdown is published.", + "The weekly audit workflow is DONE. Stop the session now.", + ].join(" "); +} + +function handleSubagentStop(input) { + const state = loadState(input.sessionId); + if (!state?.active) return {}; + + if (isDuplicate(state, input)) { + saveState(input.sessionId, state); + return {}; + } + + const agentType = input.agent_type; + let guidance = ""; + + if (agentType === "Scoper") { + guidance = handleScoperStop(state); + } else if (AUDIT_ANALYZERS.includes(agentType)) { + guidance = handleAnalyzerStop(state, agentType, input.sessionId); + } else if (agentType === "Synthesizer") { + guidance = handleSynthesizerStop(state); + } + + saveState(input.sessionId, state); + + return { + hookSpecificOutput: { + hookEventName: "SubagentStop", + additionalContext: `[AUDIT] ${agentType} agent completed. Phase: "${state.phase}". ${guidance}`, + }, + }; +} + +runHook(handleSubagentStop); diff --git a/.github/hooks/audit/user-prompt-submit.mjs b/.github/hooks/audit/user-prompt-submit.mjs new file mode 100644 index 000000000..4c3b308ff --- /dev/null +++ b/.github/hooks/audit/user-prompt-submit.mjs @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +import { PHASE_GUIDANCE, loadState, runHook } from "./shared.mjs"; + +function handleUserPromptSubmit(input) { + const state = loadState(input.sessionId); + if (!state?.active) return {}; + + return { + systemMessage: `[Audit phase: ${state.phase}] ${PHASE_GUIDANCE[state.phase]}`, + }; +} + +runHook(handleUserPromptSubmit); diff --git a/.github/hooks/pre-tool-use.mjs b/.github/hooks/implementation/pre-tool-use.mjs similarity index 100% rename from .github/hooks/pre-tool-use.mjs rename to .github/hooks/implementation/pre-tool-use.mjs diff --git a/.github/hooks/session-start.mjs b/.github/hooks/implementation/session-start.mjs similarity index 100% rename from .github/hooks/session-start.mjs rename to .github/hooks/implementation/session-start.mjs diff --git a/.github/hooks/shared.mjs b/.github/hooks/implementation/shared.mjs similarity index 100% rename from .github/hooks/shared.mjs rename to .github/hooks/implementation/shared.mjs diff --git a/.github/hooks/stop.mjs b/.github/hooks/implementation/stop.mjs similarity index 100% rename from .github/hooks/stop.mjs rename to .github/hooks/implementation/stop.mjs diff --git a/.github/hooks/subagent-start.mjs b/.github/hooks/implementation/subagent-start.mjs similarity index 100% rename from .github/hooks/subagent-start.mjs rename to .github/hooks/implementation/subagent-start.mjs diff --git a/.github/hooks/subagent-stop.mjs b/.github/hooks/implementation/subagent-stop.mjs similarity index 100% rename from .github/hooks/subagent-stop.mjs rename to .github/hooks/implementation/subagent-stop.mjs diff --git a/.github/hooks/user-prompt-submit.mjs b/.github/hooks/implementation/user-prompt-submit.mjs similarity index 100% rename from .github/hooks/user-prompt-submit.mjs rename to .github/hooks/implementation/user-prompt-submit.mjs diff --git a/.snyk b/.snyk index ab85686d6..6515cacd4 100644 --- a/.snyk +++ b/.snyk @@ -96,6 +96,12 @@ ignore: expires: '2026-07-01T00:00:00.000Z' created: '2026-04-01T00:00:00.000Z' + 'SNYK-JS-APOLLOPROTOBUFJS-16321047': + - '* > @apollo/protobufjs@1.2.7': + reason: 'Apollo Server 5.5.0 still pulls @apollo/usage-reporting-protobuf@4.1.1, which hard-pins @apollo/protobufjs@1.2.7. pnpm metadata shows no newer usage-reporting-protobuf or server dependency chain to move to, and forcing @apollo/protobufjs@2.0.0 did not satisfy Snyk. This package is only used by Apollo usage-reporting internals, not by application code directly.' + expires: '2026-07-31T00:00:00.000Z' + created: '2026-04-29T00:00:00.000Z' + # Snyk Code exclusions for local development tooling exclude: code: diff --git a/knip.json b/knip.json index 6b7491bee..810e817b1 100644 --- a/knip.json +++ b/knip.json @@ -8,7 +8,10 @@ "apps/ui-sharethrift": { "entry": ["src/main.tsx"], "project": ["src/**/*.{ts,tsx}"], - "ignore": ["**/terms-communication-preferences.tsx", "**/applicant-id-context.tsx"] + "ignore": [ + "**/terms-communication-preferences.tsx", + "**/applicant-id-context.tsx" + ] }, "apps/ui-admin": { "entry": ["src/main.tsx"], @@ -36,7 +39,11 @@ "project": ["src/**/*.ts"] }, "packages/sthrift/graphql": { - "entry": ["src/index.ts", "src/schema/types/**/*.resolvers.ts", "src/schema/builder/*.ts"], + "entry": [ + "src/index.ts", + "src/schema/types/**/*.resolvers.ts", + "src/schema/builder/*.ts" + ], "project": ["src/**/*.ts"], "ignore": ["**/graphql-tools-scalars.ts", "**/azure-functions.ts"] }, @@ -47,7 +54,11 @@ "src/domain/contexts/**/*-permissions.ts" ], "project": ["src/**/*.ts"], - "ignore": ["**/events/event-bus.ts", "**/events/index.ts", "**/*.value-objects.ts"] + "ignore": [ + "**/events/event-bus.ts", + "**/events/index.ts", + "**/*.value-objects.ts" + ] }, "packages/sthrift/*": { "entry": ["src/index.ts"], @@ -89,7 +100,7 @@ "apps/server-payment-mock" ], "ignore": [ - ".github/hooks/*.mjs", + ".github/hooks/**/*.mjs", "build-pipeline/scripts/**", "local-https-proxy.js", "**/*.test.ts", @@ -126,5 +137,13 @@ "tsx", "progress-bar" ], - "ignoreBinaries": ["func", "open", "concurrently", "container", "portless", "python", "python3"] + "ignoreBinaries": [ + "func", + "open", + "concurrently", + "container", + "portless", + "python", + "python3" + ] }