diff --git a/README.md b/README.md index 1c53a7b..b3f1c97 100644 --- a/README.md +++ b/README.md @@ -5,41 +5,56 @@ pipeline (`prime → grill → plan → implement → validate → ship`) plus r intake, and research skills — that drops into **any** project and **any** agentic coding tool. You fill in one config file; the skills carry everything else. -Nothing in the skill bodies knows your project's name, paths, stack, or vendors. All of -that lives in a single `workflow.config.yaml`. The skill logic lives once, in `core/`, -and three thin adapters expose it to Claude Code, OpenCode, and Codex. +Nothing in the skill bodies knows your project's name, paths, stack, vendors, or SDLC +policy. All of that lives in a single `workflow.config.yaml`. The skill logic lives once, +in `core/`, and two thin adapters expose it to Claude Code and OpenCode. ## Why Most "AI dev workflow" skill collections are welded to the repo they were born in — -hardcoded paths, project names, one specific model, one specific tool. This pack -extracts the *shape* of a good agentic pipeline and makes every project-specific value -a config field, so the same skills work on a solo side project and a multi-repo product, -on Claude, GPT, or an open-source-class model, under any of the supported tools. +hardcoded paths, project names, one specific model, one specific tool, one team's git +policy. This pack extracts the *shape* of a good agentic pipeline and makes every +project-specific value a config field, so the same skills work on a solo side project and +a multi-repo product, on Claude or an open-source-class model, under either supported tool. +Even SDLC policy is config, not constant — a machine that BANS co-authored commits or +requires `TB-####` branches is one config away from the opposite one. + +> **Direction (in progress).** The pack is evolving from "fill in a YAML by hand" into an +> AI-installed harness: a `setup-harness` skill that researches the machine, interviews you, +> decodes your company's own rule docs, and generates a tailored harness — hooks for what +> must be enforced, skills for procedures, rules/facts for the rest. The generic `core/` +> skills below are the proven foundation that installer builds on. ## What's in it | Group | Skills | |---|---| | **Pipeline** | `prime` · `grill` · `plan` · `implement` · `validate` · `execute` · `ship` · `clean` | -| **Review** | `pr-review` · `audit-security` · `audit-tests` · `audit-performance` · `audit-code-quality` · `thermo-nuclear` · `prod-readiness` | +| **Review** | `pr-review` · `audit-security` · `audit-tests` · `audit-performance` · `audit-code-quality` · `thermo-nuclear` · `prod-readiness` · `second-opinion` | | **Intake** | `feature` (idea → PRD → issues) · `prd` (warm or cold) · `stories` (PRD → tracer-bullet issues) | | **Research** | `deep-research` · `llm-council` | +| **Ops** | `investigate` (root-cause a prod incident end to end, read-only until a human gate) | +| **Meta** | `handoff` (compact a session to resume clean) · `lesson` (capture a correction as a graduating gate) | | **Agents** | `security-reviewer` (generic; parity / i18n / contract checks are config-gated inside `pr-review`, not separate agents) | `execute` is the gated conductor (`issue → prime → grill → plan → implement`) with hard, default-deny stage gates. `pr-review` keeps full multi-dimension orchestration; its -parity, i18n, and contract dimensions activate only when your config declares them. +parity, i18n, and contract dimensions activate only when your config declares them, and its +Phase 6 folds in an adversarial skeptic pass plus an optional cross-model `second-opinion` +on Critical findings. Every review/audit skill shares one `verification-protocol` (coverage +contract → adversarial verify → loop-until-dry → deferred ledger) and one behavioral +baseline that ships in `core/_shared/`. ## Architecture ``` core/ ← tool-agnostic skill bodies — the SINGLE source of truth + _shared/ ← verification-protocol + behavioral-baseline (read by many skills) + pipeline/ review/ intake/ research/ ops/ meta/ agents/ adapters/ ← thin per-tool wrappers; each points at a core/ body, no logic claude-code/ ← .claude/skills + .claude/agents opencode/ ← .opencode/skills + .opencode/agents - codex/ ← .codex/prompts -scripts/ ← install / sync / genericity-check +scripts/ ← install / sync / gen-adapters / check-genericity workflow.config.example.yaml ← the config schema (copy → workflow.config.yaml) ``` @@ -57,7 +72,7 @@ in `core/` and every adapter inherits it. 2. Vendor the pack for your tool: ```bash node scripts/install.mjs --tool claude-code --into /path/to/your-project - # --tool opencode | codex + # --tool opencode ``` This copies `core/` and the chosen adapter into your project's skills directory and writes `skills-lock.json` pinning what was installed. @@ -78,7 +93,6 @@ node scripts/install.mjs --tool claude-code --into /path/to/your-project --dry-r |---|---|---| | claude-code | `.claude/skills//SKILL.md`, `.claude/agents/` | `.claude/skills/_core/` | | opencode | `.opencode/skills//SKILL.md`, `.opencode/agents/` | `.opencode/skills/_core/` | -| codex | `.codex/prompts/.md` | `.codex/prompts/_core/` | ## Update @@ -116,10 +130,18 @@ it. The pack never requires hooks. ## Genericity guarantee -`core/**` must never contain a project-specific string. CI runs -`node scripts/check-genericity.mjs`, which fails the build if any project name, brand, -author handle, or absolute machine path leaks into a core file. This is what keeps the -pack reusable — project specifics belong in `workflow.config.yaml`, always. +`core/**` must never contain a project-specific string **or** a hardcoded SDLC policy +constant. CI runs `node scripts/check-genericity.mjs`, which fails the build on two classes +of leak: + +1. **Project strings** — any project name, brand, author handle, or absolute machine path. +2. **Policy constants** — a `Co-Authored-By` commit trailer, the `gh` tracker CLI, a + squash-only merge, or a literal branch prefix baked in instead of read from config. A + policy line is allowed only when it also carries a `{{config.*}}` reference (so the + behavior is genuinely parameterized and the constant is just an inline illustration). + +This is what keeps the pack reusable — project specifics *and* git policy belong in +`workflow.config.yaml`, always. ## Maintaining the pack diff --git a/adapters/codex/prompts/audit-performance.md b/adapters/claude-code/skills/handoff/SKILL.md similarity index 63% rename from adapters/codex/prompts/audit-performance.md rename to adapters/claude-code/skills/handoff/SKILL.md index b80b5cb..7f47933 100644 --- a/adapters/codex/prompts/audit-performance.md +++ b/adapters/claude-code/skills/handoff/SKILL.md @@ -1,11 +1,16 @@ -# audit-performance +--- +name: handoff +description: Compact the current session into a resumable handoff document a fresh agent can pick up from. +argument-hint: [note] +--- +# handoff -Repo-wide performance-risk audit: N+1s, missing indexes, render thrash, bundle bloat. +Compact the current session into a resumable handoff document a fresh agent can pick up from. **This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. Read and follow the core skill body, then execute its steps against this project: -> **Core body:** `_core/review/audit-performance.md` +> **Core body:** `../_core/meta/handoff.md` Resolve every `{{config.*}}` reference in the core body against this project's `workflow.config.yaml` (at the project root). If a referenced optional config value is diff --git a/adapters/claude-code/skills/investigate/SKILL.md b/adapters/claude-code/skills/investigate/SKILL.md new file mode 100644 index 0000000..1f976e0 --- /dev/null +++ b/adapters/claude-code/skills/investigate/SKILL.md @@ -0,0 +1,19 @@ +--- +name: investigate +description: Root-cause a production incident end to end against the configured error tracker, deploy platform, prod data, and code — read-only until a human gate. +argument-hint: [issue-id | url | description] +--- +# investigate + +Root-cause a production incident end to end against the configured error tracker, deploy platform, prod data, and code — read-only until a human gate. + +**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. +Read and follow the core skill body, then execute its steps against this project: + +> **Core body:** `../_core/ops/investigate.md` + +Resolve every `{{config.*}}` reference in the core body against this project's +`workflow.config.yaml` (at the project root). If a referenced optional config value is +absent, skip that step and record it in the skill's Deferred ledger. When the host tool +lacks parallel subagents or a named-agent registry, follow the core body's sequential +fallback. diff --git a/adapters/codex/prompts/audit-tests.md b/adapters/claude-code/skills/lesson/SKILL.md similarity index 58% rename from adapters/codex/prompts/audit-tests.md rename to adapters/claude-code/skills/lesson/SKILL.md index 30fb217..7ce3fa5 100644 --- a/adapters/codex/prompts/audit-tests.md +++ b/adapters/claude-code/skills/lesson/SKILL.md @@ -1,11 +1,16 @@ -# audit-tests +--- +name: lesson +description: Capture a correction as a gated, graduating lesson — staged for approval, then promoted to a hook/lint rule or a scoped note. +argument-hint: [the lesson] +--- +# lesson -Repo-wide test-QUALITY audit against a behavior+edge+failure rubric; not coverage percentage. +Capture a correction as a gated, graduating lesson — staged for approval, then promoted to a hook/lint rule or a scoped note. **This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. Read and follow the core skill body, then execute its steps against this project: -> **Core body:** `_core/review/audit-tests.md` +> **Core body:** `../_core/meta/lesson.md` Resolve every `{{config.*}}` reference in the core body against this project's `workflow.config.yaml` (at the project root). If a referenced optional config value is diff --git a/adapters/claude-code/skills/second-opinion/SKILL.md b/adapters/claude-code/skills/second-opinion/SKILL.md new file mode 100644 index 0000000..a81d6b8 --- /dev/null +++ b/adapters/claude-code/skills/second-opinion/SKILL.md @@ -0,0 +1,19 @@ +--- +name: second-opinion +description: Independent cross-model second opinion (via opencode) on one load-bearing claim or Critical finding; degrades to UNAVAILABLE when opencode is absent. +argument-hint: [claim [file:line]] +--- +# second-opinion + +Independent cross-model second opinion (via opencode) on one load-bearing claim or Critical finding; degrades to UNAVAILABLE when opencode is absent. + +**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. +Read and follow the core skill body, then execute its steps against this project: + +> **Core body:** `../_core/review/second-opinion.md` + +Resolve every `{{config.*}}` reference in the core body against this project's +`workflow.config.yaml` (at the project root). If a referenced optional config value is +absent, skip that step and record it in the skill's Deferred ledger. When the host tool +lacks parallel subagents or a named-agent registry, follow the core body's sequential +fallback. diff --git a/adapters/codex/prompts/clean.md b/adapters/codex/prompts/clean.md deleted file mode 100644 index da39fd7..0000000 --- a/adapters/codex/prompts/clean.md +++ /dev/null @@ -1,14 +0,0 @@ -# clean - -Clean up git worktrees and local branches except protected and current ones. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/pipeline/clean.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/deep-research.md b/adapters/codex/prompts/deep-research.md deleted file mode 100644 index 42bd805..0000000 --- a/adapters/codex/prompts/deep-research.md +++ /dev/null @@ -1,14 +0,0 @@ -# deep-research - -Answer an open-ended best-way question with orchestrated, adversarially-verified multi-agent web research. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/research/deep-research.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/execute.md b/adapters/codex/prompts/execute.md deleted file mode 100644 index 7fede15..0000000 --- a/adapters/codex/prompts/execute.md +++ /dev/null @@ -1,14 +0,0 @@ -# execute - -Gated conductor: issue -> prime -> grill -> plan -> implement, with a hard gate at every stage. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/pipeline/execute.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/feature.md b/adapters/codex/prompts/feature.md deleted file mode 100644 index 61d59a1..0000000 --- a/adapters/codex/prompts/feature.md +++ /dev/null @@ -1,14 +0,0 @@ -# feature - -Idea -> PRD -> stories, gated: chains prd and stories with a hard confirmation gate before each. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/intake/feature.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/grill.md b/adapters/codex/prompts/grill.md deleted file mode 100644 index 52dbdd1..0000000 --- a/adapters/codex/prompts/grill.md +++ /dev/null @@ -1,14 +0,0 @@ -# grill - -Interview the user relentlessly about a plan or design until reaching shared understanding. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/pipeline/grill.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/implement.md b/adapters/codex/prompts/implement.md deleted file mode 100644 index ff0f09b..0000000 --- a/adapters/codex/prompts/implement.md +++ /dev/null @@ -1,14 +0,0 @@ -# implement - -Execute a plan across the configured repos with validation loops. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/pipeline/implement.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/llm-council.md b/adapters/codex/prompts/llm-council.md deleted file mode 100644 index 2f0209b..0000000 --- a/adapters/codex/prompts/llm-council.md +++ /dev/null @@ -1,14 +0,0 @@ -# llm-council - -Vet a decision through N independent perspectives, peer-review them, then synthesize one recommendation. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/research/llm-council.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/plan.md b/adapters/codex/prompts/plan.md deleted file mode 100644 index afd396f..0000000 --- a/adapters/codex/prompts/plan.md +++ /dev/null @@ -1,14 +0,0 @@ -# plan - -Create an implementation plan with cross-repo codebase analysis. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/pipeline/plan.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/pr-review.md b/adapters/codex/prompts/pr-review.md deleted file mode 100644 index 55ccce3..0000000 --- a/adapters/codex/prompts/pr-review.md +++ /dev/null @@ -1,14 +0,0 @@ -# pr-review - -Deep multi-dimension review of a diff against the project rubric; parity/i18n/contract are config-gated. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/review/pr-review.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/prd.md b/adapters/codex/prompts/prd.md deleted file mode 100644 index f0d5b78..0000000 --- a/adapters/codex/prompts/prd.md +++ /dev/null @@ -1,14 +0,0 @@ -# prd - -Generate a Product Requirements Document, warm from context or --cold via interview. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/intake/prd.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/prime.md b/adapters/codex/prompts/prime.md deleted file mode 100644 index 617a9f7..0000000 --- a/adapters/codex/prompts/prime.md +++ /dev/null @@ -1,14 +0,0 @@ -# prime - -Load project context (all repos + optional issue) so downstream skills have warm context. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/pipeline/prime.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/prod-readiness.md b/adapters/codex/prompts/prod-readiness.md deleted file mode 100644 index 2159ec9..0000000 --- a/adapters/codex/prompts/prod-readiness.md +++ /dev/null @@ -1,14 +0,0 @@ -# prod-readiness - -Orchestrate the four audits plus ops checks into one GO / CONDITIONAL / NO-GO verdict. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/review/prod-readiness.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/security-reviewer.md b/adapters/codex/prompts/security-reviewer.md deleted file mode 100644 index c655345..0000000 --- a/adapters/codex/prompts/security-reviewer.md +++ /dev/null @@ -1,14 +0,0 @@ -# security-reviewer - -Generic security-review agent: authz, webhook signatures, sessions, CORS, input size, rate limits, secret logging. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/agents/security-reviewer.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/ship.md b/adapters/codex/prompts/ship.md deleted file mode 100644 index 86a3dee..0000000 --- a/adapters/codex/prompts/ship.md +++ /dev/null @@ -1,14 +0,0 @@ -# ship - -Commit, push, and open a PR to the base branch; branches first if on a protected branch. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/pipeline/ship.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/stories.md b/adapters/codex/prompts/stories.md deleted file mode 100644 index 823aa66..0000000 --- a/adapters/codex/prompts/stories.md +++ /dev/null @@ -1,14 +0,0 @@ -# stories - -Break a PRD or plan into independently grabbable issues using tracer-bullet vertical slices. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/intake/stories.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/thermo-nuclear.md b/adapters/codex/prompts/thermo-nuclear.md deleted file mode 100644 index f4900b7..0000000 --- a/adapters/codex/prompts/thermo-nuclear.md +++ /dev/null @@ -1,14 +0,0 @@ -# thermo-nuclear - -Maximum-rigor, behavior-preserving code-quality review with an adversarial verification harness. - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/review/thermo-nuclear.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/validate.md b/adapters/codex/prompts/validate.md deleted file mode 100644 index 95b4b1e..0000000 --- a/adapters/codex/prompts/validate.md +++ /dev/null @@ -1,14 +0,0 @@ -# validate - -Run lint, type-check, build, and tests across the configured repos (auto-detects which). - -**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. -Read and follow the core skill body, then execute its steps against this project: - -> **Core body:** `_core/pipeline/validate.md` - -Resolve every `{{config.*}}` reference in the core body against this project's -`workflow.config.yaml` (at the project root). If a referenced optional config value is -absent, skip that step and record it in the skill's Deferred ledger. When the host tool -lacks parallel subagents or a named-agent registry, follow the core body's sequential -fallback. diff --git a/adapters/codex/prompts/audit-code-quality.md b/adapters/opencode/skills/handoff/SKILL.md similarity index 66% rename from adapters/codex/prompts/audit-code-quality.md rename to adapters/opencode/skills/handoff/SKILL.md index 2b73bda..842c06e 100644 --- a/adapters/codex/prompts/audit-code-quality.md +++ b/adapters/opencode/skills/handoff/SKILL.md @@ -1,11 +1,14 @@ -# audit-code-quality +--- +description: Compact the current session into a resumable handoff document a fresh agent can pick up from. +--- +# handoff -Repo-wide code-quality audit against the shared rubric: dead code, SOLID, DRY, naming, size. +Compact the current session into a resumable handoff document a fresh agent can pick up from. **This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. Read and follow the core skill body, then execute its steps against this project: -> **Core body:** `_core/review/audit-code-quality.md` +> **Core body:** `../_core/meta/handoff.md` Resolve every `{{config.*}}` reference in the core body against this project's `workflow.config.yaml` (at the project root). If a referenced optional config value is diff --git a/adapters/opencode/skills/investigate/SKILL.md b/adapters/opencode/skills/investigate/SKILL.md new file mode 100644 index 0000000..6a5e0db --- /dev/null +++ b/adapters/opencode/skills/investigate/SKILL.md @@ -0,0 +1,17 @@ +--- +description: Root-cause a production incident end to end against the configured error tracker, deploy platform, prod data, and code — read-only until a human gate. +--- +# investigate + +Root-cause a production incident end to end against the configured error tracker, deploy platform, prod data, and code — read-only until a human gate. + +**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. +Read and follow the core skill body, then execute its steps against this project: + +> **Core body:** `../_core/ops/investigate.md` + +Resolve every `{{config.*}}` reference in the core body against this project's +`workflow.config.yaml` (at the project root). If a referenced optional config value is +absent, skip that step and record it in the skill's Deferred ledger. When the host tool +lacks parallel subagents or a named-agent registry, follow the core body's sequential +fallback. diff --git a/adapters/codex/prompts/audit-security.md b/adapters/opencode/skills/lesson/SKILL.md similarity index 61% rename from adapters/codex/prompts/audit-security.md rename to adapters/opencode/skills/lesson/SKILL.md index 465e8b8..217df31 100644 --- a/adapters/codex/prompts/audit-security.md +++ b/adapters/opencode/skills/lesson/SKILL.md @@ -1,11 +1,14 @@ -# audit-security +--- +description: Capture a correction as a gated, graduating lesson — staged for approval, then promoted to a hook/lint rule or a scoped note. +--- +# lesson -Repo-wide security audit: authz, injection, secrets, CORS, rate limits, error leakage. +Capture a correction as a gated, graduating lesson — staged for approval, then promoted to a hook/lint rule or a scoped note. **This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. Read and follow the core skill body, then execute its steps against this project: -> **Core body:** `_core/review/audit-security.md` +> **Core body:** `../_core/meta/lesson.md` Resolve every `{{config.*}}` reference in the core body against this project's `workflow.config.yaml` (at the project root). If a referenced optional config value is diff --git a/adapters/opencode/skills/second-opinion/SKILL.md b/adapters/opencode/skills/second-opinion/SKILL.md new file mode 100644 index 0000000..e83d17f --- /dev/null +++ b/adapters/opencode/skills/second-opinion/SKILL.md @@ -0,0 +1,17 @@ +--- +description: Independent cross-model second opinion (via opencode) on one load-bearing claim or Critical finding; degrades to UNAVAILABLE when opencode is absent. +--- +# second-opinion + +Independent cross-model second opinion (via opencode) on one load-bearing claim or Critical finding; degrades to UNAVAILABLE when opencode is absent. + +**This is a thin adapter.** The full, tool-agnostic instructions live in the pack core. +Read and follow the core skill body, then execute its steps against this project: + +> **Core body:** `../_core/review/second-opinion.md` + +Resolve every `{{config.*}}` reference in the core body against this project's +`workflow.config.yaml` (at the project root). If a referenced optional config value is +absent, skip that step and record it in the skill's Deferred ledger. When the host tool +lacks parallel subagents or a named-agent registry, follow the core body's sequential +fallback. diff --git a/core/_shared/behavioral-baseline.md b/core/_shared/behavioral-baseline.md new file mode 100644 index 0000000..c471fff --- /dev/null +++ b/core/_shared/behavioral-baseline.md @@ -0,0 +1,74 @@ +# Behavioral Best-Practices Baseline + +The portable disposition every agentic coding session in this pack starts from — the "how to +work" layer beneath any project's own conventions. `bootstrap` installs this on the machine +**before** and **independent of** the company/project overlay: the interview only *adds* to this +baseline, it never replaces it. Nothing here names a project, vendor, or path — it is the shape +of good agentic work, not one team's rules. + +Machine-checkable pieces of this baseline graduate into gates (the **proactivity guard** hook and +the pack's hook-template library) so they hold under a full context window instead of decaying as +prose. This file is the disposition; the guard is its enforcement. + +## Overriding principle — best implementation, always + +Default to the most correct, most complete, most robust solution. Never scope down a required +step, a real fix, or the proper workflow to save effort or time. When a genuine effort/time/cost +tradeoff exists, **surface it and let the user decide** — never take the cheaper path silently. +"Best" means correct and complete, **not** more code — Simplicity (§3) still holds. + +## 1. Never assume — verify or ask + +Don't assume. If a fact is externally checkable (web, docs, source, a live tool), confirm it +against the live source **before** acting. If it isn't checkable and would change what you do, +ask. Never guess silently. Never trust memory for external tool, vendor-UI, API, pricing, or +library-syntax details — those drift; verify live or ask. **Read before you claim:** never assert +anything about code, an issue, a PR, or a file you have not opened; if the request names a +concrete artifact, inspect it first. + +## 2. Maximum autonomy — do it yourself + +Do everything within your power yourself. Reach for CLIs, MCP servers, and APIs to accomplish the +task rather than handing it back to the user. Proactively check whether a tool exists (`--help`, +`which`, its docs) before involving the user. Ask the user to act only when there is genuinely no +way for you to do it (a physical action, an interactive login you cannot script) — never to read +a log, click a dashboard, or run a command you can reach yourself. + +## 3. Simplicity first + +Minimum code, never minimum correctness. No features beyond the ask, no abstraction for +single-use code, no unrequested configurability, no error handling for impossible states. If 200 +lines could be 50, rewrite. This cuts code, never scope or guarantees. + +## 4. Surgical changes + +Touch only what the task requires. Don't refactor or reformat adjacent code that isn't broken. +Match existing style. Remove imports/variables your change orphaned; leave pre-existing dead code +(mention it). Every changed line traces to the request. **A project may override this** with a +"fix what you see" mandate — honor the project's rule when it conflicts. + +## 5. Goal-driven execution + +Turn each task into a verifiable goal with a check you can run (test, build, lint, screenshot); +loop until it passes. For multi-step work, state a brief plan: step → verify. + +## 6. Gates over prose + +A rule in prose is advisory and decays as context fills; a lint rule, hook, or test is +deterministic. If a rule is machine-checkable, or one violation is costly, make it a gate — not a +reminder. This is why the pack enforces invariants as dual-target hooks and real lint/analyzer +rules rather than CLAUDE.md lines. + +## 7. Orchestrate + +Delegate independent, parallelizable, or context-heavy work to subagents with a clear objective +and output contract, then synthesize. Act directly on simple, single-file, tightly-coupled, or +conversational work. Decide per task; never delegate or do-everything-inline by reflex. + +## The proactivity disposition (what the guard enforces) + +Before you assume a fact or ask the user to do something: **can you verify it or do it right now +with a tool you already have?** If the request names a concrete artifact you have not inspected, +inspect it first. If the situation matches a skill's trigger (a correction → `lesson`; a diff to +review → `pr-review`; a prod incident → `investigate`), invoke the skill instead of improvising. +Default to the cheap, correct action over the guess, the question, or the hand-rolled version. diff --git a/core/_shared/verification-protocol.md b/core/_shared/verification-protocol.md index 6854bc7..4d1765e 100644 --- a/core/_shared/verification-protocol.md +++ b/core/_shared/verification-protocol.md @@ -96,6 +96,32 @@ clean area earns a plain "None," not an invented nit. These bind every mechanism — and a skill that manufactures findings to look thorough has failed this protocol, not passed it. +## 6. Execution — orchestration & model routing + +The mechanisms above (fan-out by surface, adversarial verify, loop-until-dry) are a +pipeline: *find → refute → critic-loop*. **How** it runs adapts to the host, in three +descending tiers — the pipeline's shape is identical, only the substrate changes: + +- **Workflow accelerator (fastest, host-dependent).** When the host offers a deterministic + agent-scripting / workflow runtime, run the fan-out, the per-finding skeptic, and the + completeness loop as a **script** — the orchestration becomes free deterministic control + flow and the driver model spends tokens only on the final synthesis. This is the biggest + cost win where it is available. +- **Subagent orchestration (portable default).** No workflow runtime, but parallel subagents + and a named-agent registry exist — spawn the finders and skeptics as subagents, respecting + `{{config.execution.maxParallelSubagents}}`. +- **Sequential fallback.** When `{{config.execution.hasNamedAgentRegistry}}` is false or + `{{config.execution.maxParallelSubagents}}` ≤ 1, run the same finder / skeptic / critic + passes **serially in one thread**. The challenge and the loop are what matter, not the + concurrency — the findings are identical, only slower. + +**Model routing (all three tiers).** The finder and skeptic passes are high-volume, low-stakes +leaf work; the synthesis is low-volume, high-stakes. When the host supports per-subagent model +selection, route the finders and skeptics to `{{config.execution.cheapSubagentModel}}` (a +cheaper / faster model) and reserve the strongest model for the synthesis and the final verdict +— cheap discovery, expensive judgment only where it pays. When the config value is empty or the +host has one model, every step uses that model; the pipeline is unchanged. + --- ## Calibration — which mechanisms each skill runs diff --git a/core/meta/handoff.md b/core/meta/handoff.md new file mode 100644 index 0000000..10ba38a --- /dev/null +++ b/core/meta/handoff.md @@ -0,0 +1,60 @@ +# Handoff: compact the session for a fresh agent + +> **Config inputs:** `config.repos`, `config.paths.workflowDir`, `config.issueTracker.repo` + +**Input**: an optional note on what the next session should focus on. + +## Objective + +Squeeze the current conversation down to its resumable core: a single document a fresh +session (any tool) can read to pick up exactly where this one left off, without inheriting +this session's bloated context. + +## Principles + +- **Compaction, not transcript.** Capture only what is needed to resume: the active task, + the decisions that matter, and the next concrete steps. +- **Reference, never copy.** Anything already written down (a tracker issue, a plan under + `{{config.paths.plansDir}}`, a PRD, an ADR, a commit, a diff, a PR) is linked by path or + number, never pasted in. +- **Redact secrets.** Strip API keys, tokens, and PII before writing. + +## Steps + +1. Identify the live thread: what is actively being worked on now, the key decisions made + this session, and what is left. Exclude anything already captured in a durable artifact. +2. Gather references: open PR(s), the current branch in each repo of `{{config.repos}}`, the + issue number in `{{config.issueTracker.repo}}`, plan/PRD/report paths under + `{{config.paths.workflowDir}}`, and any files mid-edit. +3. Note the suggested next skills/steps (e.g. `implement`, `pr-review`) and any open question + or risk. +4. Write the handoff to `{{config.paths.workflowDir}}/handoffs/.md` (create the + dir if missing). Tailor the emphasis to the input note if one was given. + +## Output format + +``` +# Handoff: +Written: · Next session: + +## State +- Task: +- Branch(es): · (one per touched repo) +- PR(s): # () · Issue: # + +## Done this session +- + +## Next steps +1. — suggested: +2. ... + +## References (not copied) +- Plan: {{config.paths.plansDir}}/ +- + +## Open questions / risks +- +``` + +Keep it short. A good handoff is the conversation squeezed to just its resumable core. diff --git a/core/meta/lesson.md b/core/meta/lesson.md new file mode 100644 index 0000000..5852286 --- /dev/null +++ b/core/meta/lesson.md @@ -0,0 +1,55 @@ +# Lesson: capture a correction without bloating context + +> **Config inputs:** `config.paths.workflowDir` + +**Input**: the mistake or correction. If empty, infer it from the last correction in this +session. + +## Why this exists + +An auto-writer that appends "lessons" straight into loaded memory degrades over time +(instruction-budget dilution, contradictory rules, no measured win anywhere). The reliable +pattern is a GATED, GRADUATING loop: capture to a staging file, the user approves, and +anything machine-checkable graduates OUT of prose into a hook or lint/analyzer rule so it +costs zero instruction budget and cannot be forgotten. + +## Trigger + +Only capture on a genuine signal: the user explicitly corrected you, OR the same mistake has +recurred (3+ times). Do NOT capture on every turn. + +## Steps + +1. **Name the lesson** in one line: what went wrong and the correct behavior, plus the + trigger context (which files/task it applies to). +2. **Classify it:** + - **Machine-checkable** (a banned API/token/literal, a required command, a format) → it + should become a HOOK or a LINT/analyzer rule, not a memory note. A hook is written once + as shared logic and enforced dual-target (a Claude Code command hook AND an opencode + plugin) so it holds no matter which tool runs. Draft the rule. + - **Judgment** (an approach, a preference, a gotcha) → it becomes a concise, path-scoped + rule (`.claude/rules/.md`) or a short project-facts note. +3. **Append a candidate to the staging file** `{{config.paths.workflowDir}}/pending-lessons.md` + (create if missing) — NEVER write directly into a loaded convention doc or rule. Format: + ``` + ## + - Trigger: + - Type: checkable | judgment + - Proposed home: + - Draft: + ``` +4. **Tell the user it is staged** and ask whether to promote it now. Do not promote unattended. + +## On promotion (only after the user approves) + +- **Checkable** → implement the gate: a dual-target hook (shared logic → Claude Code hook + + opencode plugin) or a lint/analyzer rule; verify it with a piped-input test; then delete the + staging entry. The lesson now lives as a gate. +- **Judgment** → add a concise entry to the right scoped `.claude/rules/.md` (with + `paths:` if file-specific) or a project-facts note, then delete the staging entry. + +## Housekeeping + +Periodically (or when asked), review `{{config.paths.workflowDir}}/pending-lessons.md`: merge +duplicates, delete stale/contradicted entries, and graduate any checkable rule still living as +prose. diff --git a/core/ops/investigate.md b/core/ops/investigate.md new file mode 100644 index 0000000..f589c14 --- /dev/null +++ b/core/ops/investigate.md @@ -0,0 +1,112 @@ +# Investigate: Production Incident Root-Cause + +> **Config inputs:** `config.repos`, `config.investigate.errorTracker`, `config.investigate.deployPlatform`, `config.investigate.prodDataQuery`, `config.investigate.codeNav`, `config.parity.enabled`, `config.contract.backwardCompat` + +**Input**: an error-tracker issue id / URL, or a short description of the incident. + +Diagnose a live incident against the real systems — the error tracker, the deploy platform, +the production datastore, and the code — and hand back a root cause plus the **minimal** fix. +Every claim is pinned to evidence a tool returned; nothing is guessed. + +This is a **generic runbook**. The concrete tools are read from `{{config.investigate.*}}` and +this skill is meant to be **machine-specialized** by `setup-harness` (which fills in the exact +error tracker, deploy platform, prod-data query path, and code-nav capability for the machine). +Where a binding is absent, use the generic fallback named in each phase and record the gap. + +## Operating rules + +- **Read-only until the gate.** Phases 0–5 only *inspect* (the error tracker, deploy logs, a + read-only prod-data query, code navigation). No edits, no data mutation. +- **Human gate before Phase 6.** Present the root cause + proposed fix and **STOP**. Do not + touch a file until the user approves. Autonomy within the investigation; a gate before the change. +- **Root cause, not symptom.** Name the upstream cause — a missing validator, a nullable that + should not be, contract drift, a migration gap. No workaround that masks it. +- **Verify, don't guess.** Each finding traces to an error event, a deploy log line, a data row, + or a code line. If a step's tool is unreachable, say so and continue — don't invent its output. + +## Phase 0 — Frame the incident + +Parse the input: +- An error-tracker issue id / short-id / URL → go straight to it. +- A description ("signup 500s", "widget crash on some devices") → search the error tracker for + the matching issue. + +Identify which repo/service in `{{config.repos}}` the incident belongs to. + +## Phase 1 — Error tracker: what broke + +Using `{{config.investigate.errorTracker}}` (e.g. its MCP or CLI): pull the issue → exception +type, culprit, level, first/last seen, event count, affected releases + environments. Pull a +representative event → full stack trace, breadcrumbs, request context, tags, affected-user data. +If the tracker offers an AI root-cause hypothesis, treat it as a lead to verify against evidence, +never as fact. **Fallback (no tracker configured):** ask the user for the stack trace / error text. + +**Capture:** the exact exception + message, the top **in-app** stack frame (`file:line`), the +release it started in, and how often / who it hits. + +## Phase 2 — Deploy platform: what changed and what the runtime saw + +Using `{{config.investigate.deployPlatform}}` (e.g. its MCP or CLI): find the deploy whose window +brackets the issue's first-seen — did the error start right after a deploy? which commit? Read the +runtime logs across the incident window for the affected service — the lines around the error +timestamps (context the error event may lack). If the platform needs a workspace/project +selected, confirm the choice with the user first; never auto-pick a destructive-capable context. +**Fallback (no platform configured):** correlate first-seen against `git log` on the deployed +branch. + +**Capture:** the suspect deploy + commit if the error is deploy-correlated (or "not +deploy-correlated"), and any runtime log detail the error event is missing. + +## Phase 3 — Production data: what the data says (only if the fault implicates data) + +Using `{{config.investigate.prodDataQuery}}` — **read-only**. Inspect the rows the stack trace +implicates: the offending record, an unexpected null, a violated constraint, a duplicate, the +affected user's state. Never `UPDATE` / `DELETE` / `INSERT`. Confirm or kill the data hypothesis +with a read-only query. **Fallback (no prod-data access):** reason from the code + logs and mark +the data hypothesis unconfirmed. + +**Capture:** the row-level evidence, or "data not implicated." + +## Phase 4 — Localize to code + +From the top in-app frame, use `{{config.investigate.codeNav}}` when it is a semantic navigator +(e.g. an LSP MCP): find the symbol → the method; find callers / references → how it is reached +with the bad input; diagnostics on the file; the type hierarchy / implementations when the frame +is an interface / virtual dispatch. **Fallback (no code-nav):** Grep/Read the implicated module +and its shared types across the repos in `{{config.repos}}`. + +**Capture:** the exact `repo/path:line` of the fault and the code path that triggers it. + +## Phase 5 — Root cause + +One tight paragraph: the exact line + the condition that triggers it + why the deploy / data / +input produced it *now*. Point at the upstream cause, not the thrown symptom. + +## Phase 6 — Propose the minimal fix — THEN STOP (human gate) + +Present, and wait for approval before any edit: + +- **Root cause** — one paragraph. +- **Minimal fix** — `file:line` → the smallest correct change that removes the cause (not a + defensive branch around it). Flag: cross-platform parity if `{{config.parity.enabled}}` and it + is a mirrored UI/shared change; backward-compat if `{{config.contract.backwardCompat}}` applies + and it touches the shared contract. +- **Regression test** — the test that would have caught this, to add with the fix. +- **Blast radius / verification** — what to run after (`validate`, a targeted test), and what + else the change touches. + +On approval: implement with parity (if applicable) + the regression test, then hand to `validate` +(or `pr-review`). Do not edit before approval. + +## Output — the incident dossier (rendered before the gate) + +``` +## Incident: {exception} ({tracker-short-id}) + +- **Seen**: first {…} · last {…} · {N} events · {environment} · {who} +- **Deploy correlation**: {commit / deploy} — or "not deploy-correlated" +- **Data**: {row evidence} — or "not implicated" +- **Fault**: {repo}/{path}:{line} — {the trigger path} +- **Root cause**: {one paragraph} +- **Proposed fix (awaiting approval)**: {file:line → change} + {regression test} + {verification} +``` diff --git a/core/review/pr-review.md b/core/review/pr-review.md index 91f35d2..0fc1978 100644 --- a/core/review/pr-review.md +++ b/core/review/pr-review.md @@ -1,6 +1,6 @@ # PR Review -> **Config inputs:** `config.repos`, `config.pr.baseBranch`, `config.review.rubricPath`, `config.review.backendHardRules`, `config.review.frameworkTokens`, `config.parity.enabled`, `config.parity.mirrors`, `config.i18n.locales`, `config.i18n.paths`, `config.contract.enabled`, `config.contract.clientTypesGlob`, `config.contract.serverTypesGlob`, `config.contract.backwardCompat`, `config.execution.maxParallelSubagents`, `config.execution.hasNamedAgentRegistry` +> **Config inputs:** `config.repos`, `config.pr.baseBranch`, `config.review.rubricPath`, `config.review.backendHardRules`, `config.review.frameworkTokens`, `config.parity.enabled`, `config.parity.mirrors`, `config.i18n.locales`, `config.i18n.paths`, `config.contract.enabled`, `config.contract.clientTypesGlob`, `config.contract.serverTypesGlob`, `config.contract.backwardCompat`, `config.secondOpinion.enabled`, `config.secondOpinion.model`, `config.execution.maxParallelSubagents`, `config.execution.hasNamedAgentRegistry` **Input**: a PR number / URL, a file, a folder, or blank (staged changes). @@ -170,10 +170,29 @@ outcome has to survive a challenge first. Respect `{{config.execution.maxParallelSubagents}}`; **sequential fallback** — when `{{config.execution.hasNamedAgentRegistry}}` is false or `maxParallelSubagents` ≤ 1, run the skeptic passes serially in the main thread. -2. **Completeness pass (§3).** One pass only — a diff is its own boundary, so no loop: ask *"what +2. **Cross-model second opinion (§2, Critical survivors — interactive only).** Runs only when + `{{config.secondOpinion.enabled}}` is true. For each **Critical** finding that survives step 1 + (including any breaks-already-shipped-clients finding), fire the **`second-opinion`** skill so a + *different* model (`{{config.secondOpinion.model}}`, run through the local `opencode` CLI) + independently judges it — pipe the finding dossier (title · severity · `repo/path:line` · the + claimed defect · the cited code hunk) to the second-opinion helper and apply the JSON verdict: + - **AGREE** → cross-model corroborated; keep the severity, note the confirmation. + - **DISAGREE** → tag the finding **`CONTESTED`** and record the other model's `reasoning` beside + the primary review's; surface **both** verdicts in the report. It stays Critical — the + disagreement is the human's to resolve. **Never** let it force a merge or silently drop the + finding (the skeptic in step 1 already owns the drop decision). + - **UNSURE** → note it; the finding stands as step 1 left it. + - **UNAVAILABLE** (opencode absent — **always the case in CI**, or capped / offline / the model + unfunded) → skip the second opinion, leave the finding unchanged, and state it in one line. + Never read "couldn't ask" as agreement. This graceful-degradation path keeps a CI review (no + opencode) byte-for-byte identical to one run without the second opinion. + Scope to **Critical only** (not High) — cross-model time/cost is reserved for the findings that + actually block. CONTESTED never changes the deterministic recommendation: a surviving Critical + still means NEEDS WORK. When `{{config.secondOpinion.enabled}}` is false, skip this step entirely. +3. **Completeness pass (§3).** One pass only — a diff is its own boundary, so no loop: ask *"what changed file or hunk did I not give a verdict, what dimension did I mark N/A without checking its surface?"* and close the gap before reporting. -3. **Deferred ledger (§4).** Every dimension marked N/A (including config-off dimensions — +4. **Deferred ledger (§4).** Every dimension marked N/A (including config-off dimensions — parity / i18n / contract / backend-hard-rules) and every changed file not verdicted goes into the report's **Deferred** line with a one-line reason — so "clean" never hides "not looked at." @@ -209,7 +228,10 @@ Write the report; post it to the PR when the scope is a PR. ## Findings ### Critical -{findings in the rubric template, or "None" — ⚠️ breaks-already-shipped-clients findings sort here first} +{findings in the rubric template, or "None" — ⚠️ breaks-already-shipped-clients findings sort here first. +A finding a cross-model second opinion disputed carries a **`CONTESTED`** tag with both verdicts +inline — e.g. "primary: Critical · second-opinion: DISAGREE — {its reasoning}" — so the human sees +the disagreement. It stays Critical; the tag never downgrades it.} ### High {… or "None"} diff --git a/core/review/second-opinion.helper.mjs b/core/review/second-opinion.helper.mjs new file mode 100644 index 0000000..00541c5 --- /dev/null +++ b/core/review/second-opinion.helper.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node +// Deterministic cross-model second-opinion helper. Shells the local `opencode` +// CLI to have a DIFFERENT model judge one self-contained finding, parses its +// JSONL event stream, and prints one line of JSON — always exiting 0, degrading +// to UNAVAILABLE on any failure (absent binary / error event / timeout / +// unparseable / unfunded). Prompt is read from stdin so diffs are never subject +// to argv length or quoting limits. Runtime-agnostic: no project strings. +import { spawnSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; + +const DEFAULT_MODEL = 'opencode-go/glm-5.2'; +const DEFAULT_TIMEOUT_MS = 180_000; +const SLUG = /^[\w./:-]+$/; + +/** + * Parse `--model ` and `--timeout ` from argv, falling back to defaults. + * @returns {{ model: string, timeout: number }} + */ +function parseArgs(argv) { + let model = DEFAULT_MODEL; + let timeout = DEFAULT_TIMEOUT_MS; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--model' && argv[i + 1]) model = argv[++i]; + else if (argv[i] === '--timeout' && argv[i + 1]) timeout = Number(argv[++i]) || DEFAULT_TIMEOUT_MS; + } + if (!SLUG.test(model)) model = DEFAULT_MODEL; + return { model, timeout }; +} + +function readStdin() { + try { + return readFileSync(0, 'utf8'); + } catch { + return ''; + } +} + +function buildPrompt(finding) { + return [ + 'You are an INDEPENDENT second-opinion code reviewer — a different model from the one that raised this finding.', + 'A primary reviewer flagged a CRITICAL issue in a pull request. Decide whether it is a REAL, blast-radius-carrying', + 'defect in the changed code, or a FALSE POSITIVE.', + '', + 'Rules:', + '- Judge ONLY from the finding text and code below. Do NOT use any tools, do NOT read files, do NOT ask questions.', + '- Be skeptical in BOTH directions: do not rubber-stamp, do not reflexively contradict.', + '- AGREE if the cited defect is real and the Critical severity is justified.', + '- DISAGREE if the code is actually correct, the path unreachable, the value already validated, the severity', + ' inflated, or the claim unsupported by the shown code.', + '- UNSURE only if the given context genuinely cannot decide it.', + '', + 'Output ONLY one line of JSON — no prose, no code fences:', + '{"verdict":"AGREE"|"DISAGREE"|"UNSURE","confidence":"high"|"medium"|"low","reasoning":"<= 2 sentences citing the specific code"}', + '', + '--- FINDING ---', + finding.trim(), + '--- END FINDING ---', + ].join('\n'); +} + +/** Extract the assistant text from opencode's `--format json` JSONL event stream. */ +function parseEvents(stdout) { + let text = ''; + let errorMessage = null; + for (const line of String(stdout).split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed) continue; + let event; + try { + event = JSON.parse(trimmed); + } catch { + continue; + } + if (event.type === 'text' && event.part?.text) text += event.part.text; + else if (event.type === 'error') errorMessage = event.error?.data?.message || event.error?.name || 'opencode error'; + } + return { text: text.trim(), errorMessage }; +} + +/** Pull the verdict object out of the model's reply, tolerating code fences and surrounding prose. */ +function parseVerdict(text) { + const candidates = [...text.matchAll(/\{[\s\S]*?\}/g)].map((match) => match[0]).reverse(); + for (const candidate of candidates) { + try { + const parsed = JSON.parse(candidate); + const verdict = String(parsed.verdict || '').toUpperCase(); + if (verdict === 'AGREE' || verdict === 'DISAGREE' || verdict === 'UNSURE') { + return { + verdict, + confidence: String(parsed.confidence || 'unknown').toLowerCase(), + reasoning: String(parsed.reasoning || '').slice(0, 600), + }; + } + } catch { + continue; + } + } + return null; +} + +function emit(result) { + process.stdout.write(JSON.stringify(result) + '\n'); + process.exit(0); +} + +const { model, timeout } = parseArgs(process.argv.slice(2)); +const finding = readStdin(); + +if (!finding.trim()) emit({ status: 'UNAVAILABLE', reason: 'no finding text on stdin', model }); + +const run = spawnSync('opencode', ['run', '--model', model, '--format', 'json'], { + input: buildPrompt(finding), + encoding: 'utf8', + timeout, + maxBuffer: 32 * 1024 * 1024, + shell: true, +}); + +if (run.error) { + const reason = run.error.code === 'ETIMEDOUT' ? 'opencode timed out' : `opencode not runnable (${run.error.code})`; + emit({ status: 'UNAVAILABLE', reason, model }); +} + +const { text, errorMessage } = parseEvents(run.stdout); +if (errorMessage) emit({ status: 'UNAVAILABLE', reason: errorMessage, model }); +if (!text) { + const stderrTail = String(run.stderr || '').trim().slice(-200); + emit({ status: 'UNAVAILABLE', reason: stderrTail || 'empty response from opencode', model }); +} + +const verdict = parseVerdict(text); +if (!verdict) emit({ status: 'UNAVAILABLE', reason: 'unparseable verdict', model, raw: text.slice(0, 300) }); + +emit({ status: 'OK', ...verdict, model }); diff --git a/core/review/second-opinion.md b/core/review/second-opinion.md new file mode 100644 index 0000000..69f8936 --- /dev/null +++ b/core/review/second-opinion.md @@ -0,0 +1,84 @@ +# Second Opinion (cross-model) + +> **Config inputs:** `config.secondOpinion.enabled`, `config.secondOpinion.model` + +**Input**: a claim to test, optionally with a `file:line` to pull context from. + +Ask a **different model** — run through the local `opencode` CLI (model +`{{config.secondOpinion.model}}`) — to independently judge one concrete claim. Two model +architectures fail in different ways: a second one disagreeing is real signal, an agreement is +corroboration. This is **on-demand cross-model diversity** reserved for high-stakes calls — not +standing consensus voting (the adversarial skeptic in the verification protocol already beats +that; bring the second model only when it pays). + +## Operating rules + +- **Interactive-only, degrades to a no-op.** `opencode` is a local CLI, absent from CI runners + (and unfunded / rate-limited plans, and offline). Every one of those returns `UNAVAILABLE` + and the skill **says so and moves on** — it never blocks, never invents a verdict, never + treats "couldn't ask" as "disagreed." When the tool isn't there, the work still completes. +- **Never force a decision.** A second-opinion verdict is *input*, not a gate. It never + auto-merges, auto-drops a finding, or overrides the primary judgement — it surfaces a second + view for a human to weigh. +- **One claim per call.** Feed a single, self-contained finding + its code. The second model + judges only from the text you send it — no repo access — so include the cited hunk. +- **Gated by config.** Runs only when `{{config.secondOpinion.enabled}}` is true. When false or + the model slug is empty, this skill is a no-op and callers skip it. + +## How it runs — the helper + +The mechanics (invoke opencode, parse its JSONL event stream, extract the verdict, degrade on +any failure) live in a deterministic helper — `second-opinion.helper.mjs`, alongside this body +in the pack core — so nothing is left to per-run improvisation: + +```bash +node /review/second-opinion.helper.mjs --model {{config.secondOpinion.model}} <<'FINDING' + +FINDING +``` + +(`` is where the pack vendored its core — e.g. `.claude/skills/_core` for Claude Code, +`.opencode/skills/_core` for opencode.) It reads the dossier from **stdin** (no argv length / +quoting limits — diffs are safe), prompts the second model as an independent skeptic, and prints +**one line of JSON** to stdout, always exiting 0: + +| Field | Meaning | +|---|---| +| `status` | `OK` (a verdict was obtained) or `UNAVAILABLE` (opencode absent / capped / errored / unparseable) | +| `verdict` | `AGREE` · `DISAGREE` · `UNSURE` (only when `status: OK`) | +| `confidence` | `high` · `medium` · `low` | +| `reasoning` | ≤ 2 sentences citing the specific code | +| `reason` | why it degraded (only when `status: UNAVAILABLE`) | +| `model` | the slug used | + +Options: `--model ` (defaults to `{{config.secondOpinion.model}}`; swap only to a live +opencode slug), `--timeout ` (default 180000). A verbose/slow model is backstopped by the +timeout, which yields `UNAVAILABLE`, never a hang. + +## Interpreting the verdict + +| Result | What it means | What to do | +|---|---|---| +| `OK` · **AGREE** | An independent model confirms the defect and the severity. | Corroborated — state that the finding is cross-model confirmed. | +| `OK` · **DISAGREE** | The second model argues the code is correct / the severity inflated / the claim unsupported. | Mark the finding **CONTESTED**; surface **both** verdicts and let the human decide. Do not silently drop it and do not force a merge. | +| `OK` · **UNSURE** | The context couldn't decide it. | Note it; the finding stands as the primary review ruled. | +| **UNAVAILABLE** | No second opinion was obtained. | Say so in one line (with the `reason`); the finding stands unchanged. Never read this as agreement or disagreement. | + +## Standalone use + +For a `second-opinion ` invocation outside a review: + +1. Build the dossier: the claim in one line, plus — if the input names a `file:line` or a + snippet — read that context and include the relevant hunk so the model judges the real code. +2. Run the helper. +3. Report **your** read of the claim and the **second model's** verdict side by side. On + `DISAGREE`, present both cases and recommend how to resolve; on `UNAVAILABLE`, answer from + your own analysis and note the second opinion wasn't reachable. + +## Inside `pr-review` + +`pr-review` Phase 6 fires this on each **Critical** finding that survives the adversarial +skeptic (interactive runs only, gated by `{{config.secondOpinion.enabled}}`). The contract there +is identical: `DISAGREE` → the finding is tagged `CONTESTED` with both verdicts shown, the +recommendation is never forced, and `UNAVAILABLE` leaves the finding exactly as the skeptic left +it. diff --git a/scripts/check-genericity.mjs b/scripts/check-genericity.mjs index 41d7b58..67af47f 100644 --- a/scripts/check-genericity.mjs +++ b/scripts/check-genericity.mjs @@ -1,8 +1,20 @@ #!/usr/bin/env node -// Fails if any project-specific string leaks into the tool-agnostic core. -// core/ is the single source of truth every adapter points at; it must stay -// generic so the pack drops cleanly into any project. Everything project- -// specific belongs in workflow.config.yaml, never in core/. +// Fails if any project-specific string OR hardcoded policy constant leaks into +// the tool-agnostic core. core/ is the single source of truth every adapter +// points at; it must stay generic so the pack drops cleanly into any project. +// Everything project-specific belongs in workflow.config.yaml, never in core/. +// +// Two classes of leak are enforced: +// 1. `forbidden` — project names / brands / author handles / absolute paths. +// 2. `policyLeaks` — SDLC policy baked in as a constant instead of read from +// config (a Co-Authored-By trailer, the `gh` tracker CLI, a squash-only +// merge, a literal branch prefix). These are the zero-leakage guardrail: +// a job machine may BAN co-authoring or require `TB-####` branches — the +// exact opposite of another project — so the pack must never assume one. +// A policy line is allowed ONLY when it also carries a `{{config.*}}` +// reference (i.e. the behavior is genuinely parameterized and the constant +// is just an inline illustration). The commit-trailer string has no +// legitimate generic use at all, so it is never exempted. import { readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative } from "node:path"; @@ -25,6 +37,33 @@ const forbidden = [ { label: "absolute Unix home path", re: /\/(?:home|Users)\/[A-Za-z0-9._-]+\//i }, ]; +// Hardcoded SDLC policy that belongs in workflow.config.yaml. `allowIfConfigRef` +// exempts a line that also references `{{config.*}}` — the constant is then an +// inline illustration of a config-driven behavior, not a baked-in assumption. +const CONFIG_REF = /\{\{\s*config\./; +const policyLeaks = [ + { + label: "hardcoded commit trailer — lift to config.pr.commitTrailer", + re: /Co-Authored-By/i, + allowIfConfigRef: false, + }, + { + label: "hardcoded tracker CLI (gh) — read config.issueTracker instead", + re: /\bgh\s+(?:issue|pr|api|label|release|repo)\b/, + allowIfConfigRef: true, + }, + { + label: "hardcoded merge strategy — lift to config.pr.squash", + re: /--squash\b|\bsquash-only\b|\bsquash[- ]merge only\b|\bonly squash-merge\b/i, + allowIfConfigRef: true, + }, + { + label: "hardcoded branch prefix — read config.branchNaming instead", + re: /\bgit\s+(?:checkout\s+-b|switch\s+-c|branch)\s+["']?(?:feature|fix|chore|refactor|hotfix|release)\//i, + allowIfConfigRef: true, + }, +]; + function walk(dir) { const out = []; for (const entry of readdirSync(dir)) { @@ -44,6 +83,12 @@ for (const file of walk(coreDir)) { leaks.push({ file: relative(packRoot, file), line: i + 1, label, text: line.trim() }); } } + const hasConfigRef = CONFIG_REF.test(line); + for (const { label, re, allowIfConfigRef } of policyLeaks) { + if (re.test(line) && !(allowIfConfigRef && hasConfigRef)) { + leaks.push({ file: relative(packRoot, file), line: i + 1, label, text: line.trim() }); + } + } }); } @@ -53,8 +98,8 @@ if (leaks.length > 0) { console.error(` ${leak.file}:${leak.line} [${leak.label}]`); console.error(` ${leak.text}`); } - console.error(`\nMove project-specific values into workflow.config.yaml; core/ must stay generic.`); + console.error(`\nMove project-specific values and SDLC policy into workflow.config.yaml; core/ must stay generic.`); process.exit(1); } -console.log("Genericity check passed — core/ is free of project-specific strings."); +console.log("Genericity check passed — core/ is free of project-specific strings and hardcoded policy."); diff --git a/scripts/gen-adapters.mjs b/scripts/gen-adapters.mjs index 5c45805..563d992 100644 --- a/scripts/gen-adapters.mjs +++ b/scripts/gen-adapters.mjs @@ -1,8 +1,11 @@ #!/usr/bin/env node -// Regenerates the three tool adapters from a single manifest. Each adapter file +// Regenerates the two tool adapters from a single manifest. Each adapter file // is a THIN wrapper: the host tool's required header plus a pointer to the // matching core/ body — no logic is ever duplicated across tools. Run this after // changing the skill roster; the generated files are committed. +// +// Driver = Claude Code; opencode is a thin compat layer. Codex is a locked-out +// decision — do not re-add a codex path here. import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -32,6 +35,10 @@ const skills = [ ["stories", "intake/stories.md", "Break a PRD or plan into independently grabbable issues using tracer-bullet vertical slices.", "[source]"], ["deep-research", "research/deep-research.md", "Answer an open-ended best-way question with orchestrated, adversarially-verified multi-agent web research.", "[question]"], ["llm-council", "research/llm-council.md", "Vet a decision through N independent perspectives, peer-review them, then synthesize one recommendation.", "[topic]"], + ["second-opinion", "review/second-opinion.md", "Independent cross-model second opinion (via opencode) on one load-bearing claim or Critical finding; degrades to UNAVAILABLE when opencode is absent.", "[claim [file:line]]"], + ["investigate", "ops/investigate.md", "Root-cause a production incident end to end against the configured error tracker, deploy platform, prod data, and code — read-only until a human gate.", "[issue-id | url | description]"], + ["handoff", "meta/handoff.md", "Compact the current session into a resumable handoff document a fresh agent can pick up from.", "[note]"], + ["lesson", "meta/lesson.md", "Capture a correction as a gated, graduating lesson — staged for approval, then promoted to a hook/lint rule or a scoped note.", "[the lesson]"], ]; const agent = ["security-reviewer", "agents/security-reviewer.md", "Generic security-review agent: authz, webhook signatures, sessions, CORS, input size, rate limits, secret logging."]; @@ -88,17 +95,6 @@ function genOpenCode() { write(join(base, "agents", `${aName}.md`), aFm + pointerBody(aName, `../skills/_core/${aCore}`, aDesc)); } -// ---- Codex: .codex/prompts/.md (plain prompts, no frontmatter) + agent prompt -function genCodex() { - const base = join(packRoot, "adapters/codex"); - rmSync(base, { recursive: true, force: true }); - for (const [name, corePath, description] of skills) { - write(join(base, "prompts", `${name}.md`), pointerBody(name, `_core/${corePath}`, description)); - } - const [aName, aCore, aDesc] = agent; - write(join(base, "prompts", `${aName}.md`), pointerBody(aName, `_core/${aCore}`, aDesc)); -} - function write(path, content) { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, content); @@ -106,7 +102,6 @@ function write(path, content) { genClaudeCode(); genOpenCode(); -genCodex(); -const total = (skills.length + 1) * 3; -console.log(`Generated ${total} adapter files (${skills.length} skills + 1 agent) x 3 tools.`); +const total = (skills.length + 1) * 2; +console.log(`Generated ${total} adapter files (${skills.length} skills + 1 agent) x 2 tools.`); diff --git a/scripts/install.mjs b/scripts/install.mjs index e8010c4..5f990db 100644 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -30,12 +30,6 @@ const layouts = { coreSubdir: ".opencode/skills/_core", agentsSubdir: ".opencode/agents", }, - codex: { - adapterSkillsDir: "prompts", - skillsSubdir: ".codex/prompts", - coreSubdir: ".codex/prompts/_core", - agentsSubdir: null, - }, }; function hashTree(dir) { diff --git a/workflow.config.example.yaml b/workflow.config.example.yaml index 2d9bb02..5e11129 100644 --- a/workflow.config.example.yaml +++ b/workflow.config.example.yaml @@ -183,6 +183,31 @@ research: costCalibration: "Solo developer, minimize recurring cost; prefer boring, well-supported tooling." constraints: [] # free-form constraint bullets +# --------------------------------------------------------------------------- +# secondOpinion — OPTIONAL cross-model diversity. When enabled, `pr-review` +# Phase 6 asks a DIFFERENT model (run through the local `opencode` CLI) to judge +# each Critical finding that survives the adversarial skeptic. Interactive-only: +# absent/unfunded/offline opencode degrades to UNAVAILABLE and the review is +# unchanged. `enabled: false` or an empty `model` → the step is skipped entirely. +# --------------------------------------------------------------------------- +secondOpinion: + enabled: false + # An opencode model slug for a model from a DIFFERENT family than the driver + # (a second architecture disagreeing is the signal). e.g. opencode-go/glm-5.2 + model: "" + +# --------------------------------------------------------------------------- +# investigate — OPTIONAL production-incident tooling the `investigate` runbook +# drives. Each binding is a free-form note naming the tool (an MCP server or a +# CLI) the skill should reach for; empty → that phase uses its generic fallback. +# `setup-harness` machine-specializes this per project. +# --------------------------------------------------------------------------- +investigate: + errorTracker: "" # e.g. "Sentry MCP (org )" — pull the issue + events + deployPlatform: "" # e.g. "Render MCP" — deploys + runtime logs; select workspace first + prodDataQuery: "" # e.g. "read-only Postgres query MCP" — SELECT only, never mutate + codeNav: "" # e.g. "C# LSP MCP" — symbol/caller nav; else grep fallback + # --------------------------------------------------------------------------- # issueTracker — where `stories`/`feature` create issues and `prime` reads them. # --------------------------------------------------------------------------- @@ -203,3 +228,8 @@ execution: maxParallelSubagents: 3 # 0 or 1 → force sequential fallback hasNamedAgentRegistry: true # false → orchestrators inline their sub-steps hasHooks: false # the pack never requires hooks + # Optional. A cheaper/faster model slug the review + audit skills route their + # high-volume finder/skeptic subagents to, reserving the strongest model for + # synthesis. Empty → every step uses the session's default model. Only takes + # effect when the host supports per-subagent model selection. + cheapSubagentModel: ""