5-phase-workflow 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,6 +92,7 @@ Verification runs at the end of `/5:implement` and records a concise result in `
92
92
  | `/5:split` / `$5-split` | Split an existing plan into smaller linked plans for separate implementation |
93
93
  | `/5:implement` / `$5-implement` | Derive the execution graph, execute steps with agents, and verify inline |
94
94
  | `/5:review` / `$5-review` | Review code changes and save findings |
95
+ | `/5:lean-check` / `$5-lean-check` | Check any diff for scope drift and over-engineering — works without a plan |
95
96
  | `/5:commit` / `$5-commit` | Create a git commit using the configured commit message template |
96
97
  | `/5:address-review-findings` / `$5-address-review-findings` | Decide on review findings interactively, then apply approved fixes and PR comments |
97
98
  | `/5:reconfigure` / `$5-reconfigure` | Refresh docs, index, skills, and rules |
@@ -123,6 +124,8 @@ Verification uses a dedicated agent. `/5:implement` runs `verification-agent` at
123
124
 
124
125
  Review delegates to Claude Code's built-in `code-review` skill (default effort `high`, override with `/5:review {low|medium|high|max}`) and maps its findings into the workflow's findings file. Where that skill is unavailable — Codex, for example — a built-in review agent takes over and triages changed files by risk instead. `/5:address-review-findings` presents each finding one by one with a recommendation, records `fix`/`wont_fix`/`wait` decisions, then coordinates narrower helpers for approved local fixes, PR comment triage, and PR replies so the common path stays compact.
125
126
 
127
+ Scope drift is checked, not just discouraged, and not only inside the workflow. The generated `AGENTS.md` carries a simplicity ladder and a "check your own diff before finishing" rule for every session. `/5:lean-check` checks any diff against the request for drift, new dependencies, and over-engineering. On Claude Code, the `dependency-guard` Stop hook makes Claude justify or remove any dependency the uncommitted diff adds, once per session; Codex gets the same rule through its generated instructions. Inside `/5:implement`, executors report what they deliberately skipped and verification reports `SCOPE: passed | drift`.
128
+
126
129
  Reconfiguration uses a compact `.5/reconfigure-manifest.json` to pass refresh decisions to documentation and skill generation helpers without duplicating long detection summaries in prompts.
127
130
 
128
131
  For Codex installs, the workflow is token-budgeted: exploration, orchestration, and simple executors default to `gpt-5.4-mini` with low reasoning. Complex logic, security-sensitive work, data migrations, public API changes, final verification that needs deeper review, and failed retries escalate to `gpt-5.4` with medium reasoning.
package/bin/install.js CHANGED
@@ -413,7 +413,8 @@ function getWorkflowManagedFiles() {
413
413
  'statusline.js',
414
414
  'check-updates.js',
415
415
  'check-reconfig.js',
416
- 'config-guard.js'
416
+ 'config-guard.js',
417
+ 'dependency-guard.js'
417
418
  ],
418
419
 
419
420
  // References: lookup tables and schemas read on-demand by commands
@@ -890,7 +891,7 @@ function cleanupOrphanedFiles(targetPath, dataDir) {
890
891
  // Ensure .5/.gitignore exists and contains transient runtime files
891
892
  function ensureDotFiveGitignore(dataDir) {
892
893
  const gitignorePath = path.join(dataDir, '.gitignore');
893
- const entries = ['.update-cache.json', '.migration-v*', '.reconfig-reminder'];
894
+ const entries = ['.update-cache.json', '.migration-v*', '.reconfig-reminder', '.dependency-guard.json'];
894
895
  if (fs.existsSync(gitignorePath)) {
895
896
  let content = fs.readFileSync(gitignorePath, 'utf8');
896
897
  for (const entry of entries) {
@@ -1009,7 +1010,7 @@ function mergeHookArrays(targetArr, sourceArr) {
1009
1010
  // Hook event keys that contain arrays of hook entries
1010
1011
  const HOOK_ARRAY_KEYS = new Set([
1011
1012
  'PreToolUse', 'PostToolUse', 'SessionStart', 'SessionEnd',
1012
- 'PreCompact', 'PostCompact'
1013
+ 'PreCompact', 'PostCompact', 'Stop', 'SubagentStop', 'UserPromptSubmit'
1013
1014
  ]);
1014
1015
 
1015
1016
  // Deep merge for settings.json
@@ -1077,6 +1078,7 @@ function showCommandsHelp(isGlobal) {
1077
1078
  log.info(' $5-split - Split plan into smaller plans');
1078
1079
  log.info(' $5-implement - Execute implementation + verification');
1079
1080
  log.info(' $5-review - Code review');
1081
+ log.info(' $5-lean-check - Check a diff for scope drift/over-engineering');
1080
1082
  log.info(' $5-commit - Create a templated git commit');
1081
1083
  log.info(' $5-address-review-findings - Decide review findings & PR comments');
1082
1084
  log.info(' $5-configure - Interactive project setup');
@@ -1089,6 +1091,7 @@ function showCommandsHelp(isGlobal) {
1089
1091
  log.info(' /5:split - Split plan into smaller plans');
1090
1092
  log.info(' /5:implement - Execute implementation + verification');
1091
1093
  log.info(' /5:review - Code review');
1094
+ log.info(' /5:lean-check - Check a diff for scope drift/over-engineering');
1092
1095
  log.info(' /5:commit - Create a templated git commit');
1093
1096
  log.info(' /5:address-review-findings - Decide review findings & PR comments');
1094
1097
  log.info(' /5:configure - Interactive project setup');
@@ -1306,6 +1309,10 @@ During the planning phase ($5-plan), until the user approves the plan:
1306
1309
  - Do NOT write files outside \`.5/\`
1307
1310
  - Do NOT write source code — only the unified plan and scan cache
1308
1311
 
1312
+ ## Scope Discipline (replaces the dependency-guard Stop hook)
1313
+
1314
+ Before you report any task as done — inside or outside the workflow skills — check the uncommitted diff of dependency manifests (\`package.json\`, \`requirements*.txt\`, \`pyproject.toml\`, \`go.mod\`, \`Cargo.toml\`, \`Gemfile\`, \`composer.json\`, \`pom.xml\`, \`build.gradle\`, \`*.csproj\`). For every added dependency: keep it and name it in your reply if the user asked for it; otherwise remove it with the package manager and use existing code, the standard library, a native platform feature, or an installed dependency instead. For a deeper check, run \`$5-lean-check\`.
1315
+
1309
1316
  ## Configuration
1310
1317
 
1311
1318
  Run \`$5-configure\` after installation to set up your project.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "5-phase-workflow",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "A dev-workflow for Claude Code and Codex",
5
5
  "bin": {
6
6
  "foifi": "bin/install.js"
7
7
  },
8
8
  "scripts": {
9
- "test": "bash test/verify-install-js.sh && bash test/test-version-compare.sh && bash test/test-check-updates-hook.sh && bash test/test-update-system.sh",
9
+ "test": "bash test/verify-install-js.sh && bash test/test-version-compare.sh && bash test/test-check-updates-hook.sh && bash test/test-dependency-guard.sh && bash test/test-update-system.sh",
10
10
  "test:install": "bash test/verify-install-js.sh",
11
11
  "test:hook": "bash test/test-check-updates-hook.sh",
12
12
  "test:update": "bash test/test-update-system.sh",
@@ -21,7 +21,7 @@ You follow the plan, state entry, and existing codebase patterns. You do not wid
21
21
 
22
22
  ## Simplicity
23
23
 
24
- Write the minimum that satisfies the component (the "smallest coherent change"): prefer the standard library, a native framework feature, or an already-installed dependency over anything new. Add no new dependency, no abstraction for single-use code, no unrequested flexibility, and no error handling for impossible cases. Follow `Simplicity First` in the project `AGENTS.md`.
24
+ Write the minimum that satisfies the component (the "smallest coherent change"): reuse what already exists in this codebase first, then prefer the standard library, a native framework feature, or an already-installed dependency over anything new. Add no new dependency, no abstraction for single-use code, no unrequested flexibility, and no error handling for impossible cases. Touch only the target file and what it strictly needs (its test, an import site). Follow `Simplicity First` in the project `AGENTS.md`.
25
25
 
26
26
  End with exactly:
27
27
 
@@ -32,6 +32,7 @@ FILES_CREATED: [comma-separated paths]
32
32
  FILES_MODIFIED: [comma-separated paths]
33
33
  VERIFY: passed | failed | skipped
34
34
  DEVIATIONS: none | {brief list}
35
+ SKIPPED: none | {what you deliberately did not build — add when ...}
35
36
  ERROR: none | {error description}
36
37
  ---END---
37
38
  ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: verification-agent
3
- description: Verifies a workflow implementation across completeness, correctness, infrastructure, acceptance criteria, and quality. Used by /5:implement.
3
+ description: Verifies a workflow implementation across completeness, correctness, infrastructure, acceptance criteria, quality, and scope. Used by /5:implement.
4
4
  tools: Read, Glob, Grep, Bash
5
5
  ---
6
6
 
@@ -22,6 +22,7 @@ Read `.5/features/{feature-name}/plan.md` and `.5/config.json` if present. Read
22
22
  4. Tests: run the configured test command unless it is `none` or the baseline and component results already prove its status.
23
23
  5. Correctness: inspect changed files and executor results to confirm the implementation matches the plan and does not only satisfy file existence. Prefer changed files and targeted imports over broad codebase scanning.
24
24
  6. Quality: logic-bearing created or modified components have tests when the project has a test framework.
25
+ 7. Scope: run `git status --short` and `git diff HEAD --stat`, and read untracked (`??`) files directly — `git diff HEAD` omits them. Every changed file traces to a planned component (its target, its test, or an import site it needs). Flag files outside the plan, work the plan's Scope marks Out or `[DEFERRED]`, new dependencies no component requires, and abstractions or configurability no acceptance criterion asks for. Report drift only in `SCOPE`; it does not change `STATUS` — the user decides what to revert.
25
26
 
26
27
  Rerun only the commands whose inputs changed. Do not rerun an identical passing command just to see it pass again.
27
28
 
@@ -36,8 +37,9 @@ COMPLETENESS: passed | partial | failed
36
37
  INFRASTRUCTURE: passed | failed
37
38
  ACCEPTANCE_CRITERIA: satisfied/total
38
39
  QUALITY: passed | partial | failed
40
+ SCOPE: passed | drift
39
41
  ERRORS: none | {summary}
40
42
  ---END_VERIFICATION---
41
43
  ```
42
44
 
43
- Keep `ERRORS` to a compact summary. Do not paste command logs or diffs.
45
+ Keep `ERRORS` to a compact summary; list each drift finding there as `drift: {path} — {why}`. Do not paste command logs or diffs.
@@ -69,8 +69,9 @@ Run build/test commands from `.5/config.json`. Skip commands set to `none`. Keep
69
69
  - The workflow reads the config **file** at `paths.config` itself; do not pass build/test/commit settings inline — baseline (Step 2) and auto-commit (Step 5) are run by this command, not the workflow.
70
70
 
71
71
  2. Call `Workflow({name: "5-implement", args})`.
72
- 3. When it returns, **merge** (never replace) its `completedComponents` into the existing array and write `state.json` (Step 4). A resumed run reports only the components it ran this invocation. Mark the matching tasks completed with `TaskUpdate`. The workflow does not touch the filesystem itself.
73
- 4. Auto-commit per step (Step 5), then report (Step 6).
72
+ 3. If the returned `verification.inline` is `true` (the workflow took the mechanical fast path and no verification agent ran), run the **fast-path scope check** from 3c yourself and set `verification.scope` from it.
73
+ 4. When it returns, **merge** (never replace) its `completedComponents` into the existing array and write `state.json` (Step 4). A resumed run reports only the components it ran this invocation. Mark the matching tasks completed with `TaskUpdate`. The workflow does not touch the filesystem itself.
74
+ 5. Auto-commit per step (Step 5), then report (Step 6).
74
75
 
75
76
  > The Workflow path persists only after the workflow returns — if a run is interrupted mid-way, this session's progress is not saved and those components run again on the next `/5:implement`. The executor's smallest-coherent-change contract makes a re-touch safe but not free.
76
77
 
@@ -95,7 +96,7 @@ For each step, skipping components already in `completedComponents`:
95
96
  - Give each executor only its component block, required `patternRefs`, verify commands, and the inline contract below — do not make it read `step-executor-agent.md`.
96
97
 
97
98
  ```text
98
- Implement exactly the assigned component. Read only listed patternRefs ranges/symbols and the target file. Make the smallest coherent change, run assigned verify commands, and stop (STATUS: failed) for missing dependencies, unplanned auth/schema/API changes, or unclear product decisions. If verify fails only from pre-existing unrelated issues, report it under DEVIATIONS with the exact evidence and keep STATUS: success — your change is complete. Do not make more than three attempts on the same failing issue.
99
+ Implement exactly the assigned component. Read only listed patternRefs ranges/symbols and the target file. Make the smallest coherent change — reuse existing codebase code first, then stdlib, native platform features, and installed dependencies; touch only the target file and what it strictly needs (its test, an import site) — run assigned verify commands, and stop (STATUS: failed) for missing dependencies, unplanned auth/schema/API changes, or unclear product decisions. If verify fails only from pre-existing unrelated issues, report it under DEVIATIONS with the exact evidence and keep STATUS: success — your change is complete. Do not make more than three attempts on the same failing issue.
99
100
 
100
101
  End with:
101
102
  ---RESULT---
@@ -104,6 +105,7 @@ FILES_CREATED: [comma-separated paths]
104
105
  FILES_MODIFIED: [comma-separated paths]
105
106
  VERIFY: passed | failed | skipped
106
107
  DEVIATIONS: none | {brief list}
108
+ SKIPPED: none | {what you deliberately did not build — add when ...}
107
109
  ERROR: none | {error description}
108
110
  ---END---
109
111
  ```
@@ -125,10 +127,19 @@ COMPLETENESS: passed | partial | failed
125
127
  INFRASTRUCTURE: passed | failed
126
128
  ACCEPTANCE_CRITERIA: satisfied/total
127
129
  QUALITY: passed | partial | failed
130
+ SCOPE: passed | drift
128
131
  ERRORS: none | {summary}
129
132
  ---END_VERIFICATION---
130
133
  ```
131
134
 
135
+ **Fast-path scope check** — the fast path skips `verification-agent`, so run this before Step 4 (on both the Workflow and the prose path):
136
+
137
+ 1. `git status --short` and `git diff HEAD --stat`: every changed file must be a planned target, its test, or an import site it needs. Anything else is drift.
138
+ 2. Changed dependency manifests or lockfiles: a dependency no component requires is drift.
139
+ 3. `git diff HEAD` on the changed files plus the full contents of untracked (`??`) files — `git diff HEAD` omits files not yet committed — read against the plan's Scope **Out** and `[DEFERRED]` decisions: work the plan excludes, or abstraction/configurability no acceptance criterion asks for, is drift.
140
+
141
+ Set `SCOPE: passed | drift`. Drift never changes the verification status or `state.json`; it only goes into the report.
142
+
132
143
  ## Step 4: Persist Result
133
144
 
134
145
  Write `state.json` with the merged `completedComponents`, a one-line `verification` summary condensed from the block above, and `lastUpdated`. Set `status` to `completed` when verification passed; otherwise `failed`, and tell the user to fix the reported issues and rerun `/5:implement {feature-name}` to resume.
@@ -145,4 +156,9 @@ No changed files → skip. Commit error → report it and continue; do not retry
145
156
 
146
157
  ## Step 6: Report
147
158
 
148
- Report: completed/failed component counts, verification status, path to `state.json`, auto-commit count and any failures, and any failed commands, missing tests, or unmet acceptance criteria. Then stop.
159
+ Report: completed/failed component counts, verification status, path to `state.json`, auto-commit count and any failures, and any failed commands, missing tests, or unmet acceptance criteria. Also list, one line each:
160
+
161
+ - **Scope:** drift findings from verification, plus files an executor touched outside its planned target (the workflow returns these as `extraFiles`). Suggest `/5:lean-check {feature-name}` when there are any.
162
+ - **Skipped:** every non-`none` `SKIPPED` entry from the executors, so deliberate omissions stay visible.
163
+
164
+ Then stop.
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: 5:lean-check
3
+ description: Checks any diff for scope drift and over-engineering — changes that do not trace to the request, new dependencies, reinvented code. Works with or without a /5:plan feature.
4
+ allowed-tools: Bash, Read, Glob, Grep, Edit, Write, AskUserQuestion
5
+ argument-hint: [base-branch|feature-name] [what was requested]
6
+ user-invocable: true
7
+ model: sonnet
8
+ ---
9
+
10
+ <role>
11
+ You are a Lean Checker. You find what a change added beyond what was asked and what could be smaller.
12
+ You only hunt scope drift and complexity. Correctness, security, and performance belong to `/5:review`.
13
+ </role>
14
+
15
+ # Lean Check
16
+
17
+ ## Step 1: Collect the Diff
18
+
19
+ Resolve a base, then diff the working tree against it — `git diff {base} --stat` and `git diff {base}` cover committed and uncommitted changes together; add untracked files from `git status --short`.
20
+
21
+ - A base branch argument (`git rev-parse --verify {argument}` succeeds and `.5/features/{argument}/` does not exist): `base = git merge-base {argument} HEAD`.
22
+ - A feature name (`.5/features/{argument}/` exists) or no argument: `base = git merge-base {default-branch} HEAD`, where `{default-branch}` comes from `git symbolic-ref --short refs/remotes/origin/HEAD` (fall back to `main`, then `master`). This includes commits already made by `/5:implement` with `git.autoCommit`. When `HEAD` is the default branch itself, `base = HEAD` (uncommitted changes only).
23
+ - Any other argument is request text for Step 2.
24
+
25
+ No changes → say `Nothing to check.` and stop.
26
+
27
+ ## Step 2: Establish the Request
28
+
29
+ Every finding is judged against what was asked. Take the first that exists:
30
+
31
+ 1. Request text passed as an argument.
32
+ 2. `.5/features/{feature-name}/plan.md` when a feature name was passed or the current branch matches a feature — use its Scope (In/Out), Acceptance Criteria, `[DEFERRED]` decisions, and Component Checklist target paths.
33
+ 3. The request from this conversation.
34
+
35
+ None of these → ask the user for one sentence describing what the change should do.
36
+
37
+ ## Step 3: Check
38
+
39
+ Read the diff first; read full files only where a finding needs the surrounding code. Search the codebase before claiming something is reinvented.
40
+
41
+ Tags, one per finding:
42
+
43
+ - `drift:` a file or change that does not trace to the request (unrelated refactor, reformatting, a feature nobody asked for, work the plan marks Out or `[DEFERRED]`). Replacement: revert it.
44
+ - `dep:` a new dependency in a manifest or lockfile. Name what already covers it (stdlib, platform, installed dependency), or `needed` if nothing does.
45
+ - `reuse:` re-implements something that already lives in this codebase. Name the existing symbol and path.
46
+ - `stdlib:` / `native:` hand-rolled code the standard library or platform ships. Name the function or feature.
47
+ - `yagni:` abstraction with one implementation, config nobody sets, a layer with one caller, flexibility nobody asked for.
48
+ - `delete:` dead code, unreachable branches, error handling for impossible cases.
49
+ - `shrink:` same logic, fewer lines. Show the shorter form.
50
+
51
+ Never flag: input validation at trust boundaries, error handling that prevents data loss, security measures, accessibility, one small test for non-trivial logic, or anything the request explicitly asked for.
52
+
53
+ ## Step 4: Report
54
+
55
+ One line per finding, biggest cut first:
56
+
57
+ ```text
58
+ {file}:L{line}: {tag} {what}. {replacement}.
59
+ ```
60
+
61
+ End with `net: -{N} lines, -{M} dependencies possible.` With no findings, output `Lean already. Ship.` and stop.
62
+
63
+ ## Step 5: Offer to Apply
64
+
65
+ Ask the user:
66
+
67
+ - "Apply these cuts?"
68
+ - Options:
69
+ 1. "Apply all" — apply every finding
70
+ 2. "Only drift and dependencies" — revert unrelated changes and new dependencies, leave the rest
71
+ 3. "No, report only"
72
+
73
+ When applying: make only the listed changes, revert `drift:` hunks without touching the rest of the file, remove dependencies with the project's package manager (never by editing a lockfile by hand), then run the configured build/test commands from `.5/config.json` (skip any set to `none`) and report the result. Never commit.
@@ -114,6 +114,7 @@ Approving a plan means the user wants it built. Branch on the plan format you ch
114
114
 
115
115
  1. Say what you are about to do, then implement the Component Checklist directly. Make the smallest coherent change per component and follow the patterns named in the plan.
116
116
  2. Run the verify commands from `.5/config.json`. Skip any set to `none`.
117
+ Then check scope: `git status --short` must list only the Component Checklist target paths and what they strictly need (their tests, import sites). Revert anything else before continuing, and tell the user in one line what you deliberately left out (`skipped: X — add when Y`).
117
118
  3. Write `.5/features/{name}/state.json`:
118
119
 
119
120
  ```json
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ // Stop hook: when the uncommitted diff adds a dependency, make Claude justify it once
3
+ // before finishing. New dependencies are the most reliable, heuristic-free drift signal.
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { execFileSync } = require('child_process');
7
+
8
+ // Each extractor returns the dependency identities a manifest declares. The hook compares
9
+ // those sets with HEAD, so config edits and version bumps of existing packages never count.
10
+ const pep508Name = spec => (spec.trim().match(/^([A-Za-z0-9][A-Za-z0-9._-]*)/) || [])[1];
11
+ const normalizePy = name => name && name.toLowerCase().replace(/[_.]+/g, '-');
12
+ const jsonKeys = sections => text => { const json = JSON.parse(text); return sections.flatMap(s => Object.keys(json[s] || {})); };
13
+ const matchAll = (text, re, pick) => [...text.matchAll(re)].map(pick);
14
+
15
+ function requirementsDeps(text) {
16
+ return text.split('\n')
17
+ .map(l => l.replace(/#.*/, '').trim())
18
+ .filter(l => l && !l.startsWith('-'))
19
+ .map(l => normalizePy(pep508Name(l)))
20
+ .filter(Boolean);
21
+ }
22
+
23
+ // Minimal TOML walk for Cargo.toml, Pipfile, and pyproject.toml (PEP 621, PEP 735, Poetry).
24
+ function tomlDeps(text) {
25
+ const deps = [];
26
+ let section = '';
27
+ let inArray = false;
28
+ // Collects quoted specs; returns true once the array closes (a `]` outside quotes, not in "pkg[extra]").
29
+ const arrayLine = line => {
30
+ matchAll(line, /"([^"]+)"|'([^']+)'/g, m => normalizePy(pep508Name(m[1] || m[2]))).forEach(d => d && deps.push(d));
31
+ return line.replace(/"[^"]*"|'[^']*'/g, '').includes(']');
32
+ };
33
+
34
+ for (const raw of text.split('\n')) {
35
+ const line = raw.replace(/\s#.*$/, '').trim();
36
+ if (inArray) { inArray = !arrayLine(line); continue; }
37
+ const header = line.match(/^\[\[?([^\]]+)\]\]?$/);
38
+ if (header) {
39
+ section = header[1].trim();
40
+ const dotted = section.match(/(?:^|\.)(?:dependencies|dev-dependencies|build-dependencies)\.([^.]+)$/);
41
+ if (dotted) deps.push(dotted[1]); // Cargo: [dependencies.serde]
42
+ continue;
43
+ }
44
+ const key = (line.match(/^"?([A-Za-z0-9_.-]+)"?\s*=/) || [])[1];
45
+ if (!key) continue;
46
+ const arrayOfSpecs = (section === 'project' && key === 'dependencies') ||
47
+ section === 'project.optional-dependencies' || section === 'dependency-groups';
48
+ if (arrayOfSpecs && line.includes('[')) {
49
+ inArray = !arrayLine(line.slice(line.indexOf('[') + 1));
50
+ } else if (/(?:^|\.)(?:dependencies|dev-dependencies|build-dependencies|packages|dev-packages)$/.test(section) && key !== 'python') {
51
+ deps.push(key);
52
+ }
53
+ }
54
+ return deps;
55
+ }
56
+
57
+ function goModDeps(text) {
58
+ const deps = [];
59
+ let inBlock = false;
60
+ for (const raw of text.split('\n')) {
61
+ const line = raw.trim();
62
+ if (/^require\s*\($/.test(line)) { inBlock = true; continue; }
63
+ if (inBlock && line === ')') { inBlock = false; continue; }
64
+ if (line.includes('// indirect')) continue; // pulled in by tooling, not chosen
65
+ const spec = inBlock ? line : (line.match(/^require\s+(.+)$/) || [])[1];
66
+ const mod = spec && spec.split(/\s+/)[0];
67
+ if (mod && !mod.startsWith('//')) deps.push(mod);
68
+ }
69
+ return deps;
70
+ }
71
+
72
+ const EXTRACTORS = [
73
+ [/^package\.json$/, jsonKeys(['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'])],
74
+ [/^composer\.json$/, jsonKeys(['require', 'require-dev'])],
75
+ [/^requirements.*\.txt$/, requirementsDeps],
76
+ [/^(pyproject\.toml|Pipfile|Cargo\.toml)$/, tomlDeps],
77
+ [/^go\.mod$/, goModDeps],
78
+ [/^Gemfile$/, text => matchAll(text, /^\s*gem\s+['"]([^'"]+)['"]/gm, m => m[1])],
79
+ [/^pom\.xml$/, text => matchAll(text, /<dependency>([\s\S]*?)<\/dependency>/g, m =>
80
+ [/<groupId>([^<]+)</, /<artifactId>([^<]+)</].map(re => (m[1].match(re) || [])[1] || '?').join(':'))],
81
+ [/^build\.gradle(\.kts)?$/, text => matchAll(text,
82
+ /^\s*(?:implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly|annotationProcessor|kapt|ksp|classpath)\s*\(?\s*(?:['"]([^'":]+:[^'":]+)|(libs\.[\w.]+))/gm,
83
+ m => m[1] || m[2])],
84
+ [/\.csproj$/, text => matchAll(text, /<PackageReference\s+Include="([^"]+)"/gi, m => m[1])]
85
+ ];
86
+
87
+ function git(cwd, args) {
88
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
89
+ }
90
+
91
+ function headVersion(root, file) {
92
+ try { return git(root, ['show', `HEAD:${file}`]); } catch (e) { return null; }
93
+ }
94
+
95
+ function addedDependencies(root) {
96
+ const changed = git(root, ['diff', 'HEAD', '--name-only']).split('\n').filter(Boolean);
97
+ const untracked = git(root, ['ls-files', '--others', '--exclude-standard']).split('\n').filter(Boolean);
98
+ const found = [];
99
+
100
+ for (const file of new Set([...changed, ...untracked])) {
101
+ const extract = (EXTRACTORS.find(([re]) => re.test(path.basename(file))) || [])[1];
102
+ const abs = path.join(root, file);
103
+ if (!extract || !fs.existsSync(abs)) continue;
104
+ try {
105
+ const before = headVersion(root, file);
106
+ const old = new Set(before ? extract(before) : []);
107
+ extract(fs.readFileSync(abs, 'utf8'))
108
+ .filter(d => !old.has(d))
109
+ .forEach(d => found.push(`${d} (${file})`));
110
+ } catch (e) {
111
+ // Unparseable manifest (e.g. mid-edit JSON): skip this file, keep checking the others.
112
+ }
113
+ }
114
+ return [...new Set(found)];
115
+ }
116
+
117
+ let input = '';
118
+ process.stdin.setEncoding('utf8');
119
+ process.stdin.on('data', chunk => input += chunk);
120
+ process.stdin.on('end', () => {
121
+ try {
122
+ const data = JSON.parse(input);
123
+ if (data.stop_hook_active) process.exit(0); // Claude is already continuing because of a Stop hook
124
+
125
+ const root = git(data.cwd || process.cwd(), ['rev-parse', '--show-toplevel']).trim(); // git paths are root-relative
126
+ const deps = addedDependencies(root);
127
+ if (!deps.length) process.exit(0);
128
+
129
+ // Ask once per session and dependency set, so an accepted dependency doesn't nag on every stop.
130
+ const stateFile = path.join(root, '.5', '.dependency-guard.json');
131
+ const signature = `${data.session_id || ''}:${deps.slice().sort().join('|')}`;
132
+ let seen = [];
133
+ try { seen = JSON.parse(fs.readFileSync(stateFile, 'utf8')).signatures || []; } catch (e) { /* first run */ }
134
+ if (seen.includes(signature)) process.exit(0);
135
+ if (fs.existsSync(path.dirname(stateFile))) {
136
+ fs.writeFileSync(stateFile, JSON.stringify({ signatures: [...seen, signature].slice(-20) })); // concurrent sessions
137
+ }
138
+
139
+ process.stdout.write(JSON.stringify({
140
+ decision: 'block',
141
+ reason: `Uncommitted changes add dependencies:\n${deps.map(d => `- ${d}`).join('\n')}\n\n` +
142
+ 'For each one: if the user asked for it or added it themselves, keep it and name it in your reply. ' +
143
+ 'Otherwise remove it with the package manager (never edit a lockfile by hand) and use existing code, ' +
144
+ 'the standard library, a native platform feature, or an installed dependency instead.'
145
+ }));
146
+ process.exit(0);
147
+ } catch (e) {
148
+ process.exit(0); // Never block on errors (not a git repo, no HEAD, unparseable manifest)
149
+ }
150
+ });
package/src/settings.json CHANGED
@@ -37,6 +37,17 @@
37
37
  }
38
38
  ]
39
39
  }
40
+ ],
41
+ "Stop": [
42
+ {
43
+ "hooks": [
44
+ {
45
+ "type": "command",
46
+ "command": "node .claude/hooks/dependency-guard.js",
47
+ "timeout": 10
48
+ }
49
+ ]
50
+ }
40
51
  ]
41
52
  }
42
53
  }
@@ -38,11 +38,14 @@ Each phase produces a specific artifact - do not create artifacts belonging to o
38
38
  Before writing code, walk this decision hierarchy and stop at the first step that solves the problem:
39
39
 
40
40
  1. **Does this need to exist at all?** If the requirement is speculative, don't build it (YAGNI).
41
- 2. Does the **language or standard library** already solve it?
42
- 3. Is there a **native platform or framework** feature for it?
43
- 4. Does an **already-installed dependency** cover it? Don't add a new dependency for this.
44
- 5. Can it be a **single, clear expression** instead of a new abstraction?
45
- 6. Only then: write the **minimum viable implementation**.
41
+ 2. Does it **already exist in this codebase**? Search before you write — reuse the helper, util, type, or pattern that is already here instead of re-implementing it.
42
+ 3. Does the **language or standard library** already solve it?
43
+ 4. Is there a **native platform or framework** feature for it?
44
+ 5. Does an **already-installed dependency** cover it? Don't add a new dependency for this.
45
+ 6. Can it be a **single, clear expression** instead of a new abstraction?
46
+ 7. Only then: write the **minimum viable implementation**.
47
+
48
+ The hierarchy shortens the solution, never the reading: understand the code the change touches before picking a step.
46
49
 
47
50
  Non-negotiable regardless of the above: security, data integrity, correctness, and accessibility.
48
51
 
@@ -53,6 +56,9 @@ Then apply these rules:
53
56
  - No flexibility or configurability that was not requested.
54
57
  - No error handling for impossible scenarios.
55
58
  - If you write 200 lines and it could be 50, rewrite it.
59
+ - Bug fix = root cause, not symptom: find every caller of the function you touch and fix the shared code path once.
60
+ - Large or ambiguous request: build the smallest version that satisfies it and ask about the rest in the same response instead of building it speculatively.
61
+ - When you finish, name what you deliberately left out in one line each (`skipped: X — add when Y`). No essays defending the simplification.
56
62
 
57
63
  Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
58
64
 
@@ -80,6 +86,8 @@ When your changes create orphans:
80
86
 
81
87
  The test: Every changed line should trace directly to the user's request.
82
88
 
89
+ Before you report a task as done, check your own diff (`git status --short` and `git diff --stat`): revert files and changes that do not trace to the request, and remove new dependencies nobody asked for. For a deeper pass, run `/5:lean-check` (Codex: `$5-lean-check`).
90
+
83
91
  ## Goal-Driven Execution
84
92
 
85
93
  Define success criteria. Loop until verified.
@@ -26,13 +26,14 @@ export const meta = {
26
26
  const RESULT_SCHEMA = {
27
27
  type: 'object',
28
28
  additionalProperties: false,
29
- required: ['status', 'verify'],
29
+ required: ['status', 'verify', 'skipped'],
30
30
  properties: {
31
31
  status: { enum: ['success', 'failed'] },
32
32
  filesCreated: { type: 'array', items: { type: 'string' } },
33
33
  filesModified: { type: 'array', items: { type: 'string' } },
34
34
  verify: { enum: ['passed', 'failed', 'skipped'] },
35
35
  deviations: { type: 'string', description: 'none or a brief list' },
36
+ skipped: { type: 'string', description: 'none, or what was deliberately not built and when to add it' },
36
37
  error: { type: 'string', description: 'none or an error description' }
37
38
  }
38
39
  }
@@ -40,13 +41,14 @@ const RESULT_SCHEMA = {
40
41
  const VERIFICATION_SCHEMA = {
41
42
  type: 'object',
42
43
  additionalProperties: false,
43
- required: ['status', 'completeness', 'infrastructure', 'quality'],
44
+ required: ['status', 'completeness', 'infrastructure', 'quality', 'scope'],
44
45
  properties: {
45
46
  status: { enum: ['passed', 'partial', 'failed'] },
46
47
  completeness: { enum: ['passed', 'partial', 'failed'] },
47
48
  infrastructure: { enum: ['passed', 'failed'] },
48
49
  acceptanceCriteria: { type: 'string', description: 'satisfied/total, e.g. "3/3"' },
49
50
  quality: { enum: ['passed', 'partial', 'failed'] },
51
+ scope: { enum: ['passed', 'drift'] },
50
52
  commands: {
51
53
  type: 'array',
52
54
  items: {
@@ -159,11 +161,11 @@ Intent: ${c.description || ''}
159
161
  Read only these pattern references (ranges/symbols), plus the target file for modify/rename:
160
162
  ${refs}
161
163
 
162
- Rules: smallest coherent change; mirror existing naming/exports/layout/tests; follow any local skill or rule; add no new dependency; no abstraction or error handling beyond what the intent requires. Fix mechanical issues you cause (imports, types, lint). STOP and report failed for missing dependencies, unplanned auth/schema/API/contract changes, or unclear product decisions. If verify fails only from pre-existing unrelated issues, report that as a deviation with the exact evidence — do not mark the component failed. Do not make more than three attempts on the same failing issue.
164
+ Rules: smallest coherent change; reuse what already exists in this codebase before writing new code, then prefer stdlib, native platform features, and installed dependencies; touch only the target file and what it strictly needs (its test, an import site); mirror existing naming/exports/layout/tests; follow any local skill or rule; add no new dependency; no abstraction or error handling beyond what the intent requires. Fix mechanical issues you cause (imports, types, lint). STOP and report failed for missing dependencies, unplanned auth/schema/API/contract changes, or unclear product decisions. If verify fails only from pre-existing unrelated issues, report that as a deviation with the exact evidence — do not mark the component failed. Do not make more than three attempts on the same failing issue.
163
165
 
164
166
  Then run: ${verify}
165
167
 
166
- Return structured output: status, filesCreated, filesModified, verify, deviations (none or brief), error (none or description). Keep it concise — no logs or diffs unless failed.`
168
+ Return structured output: status, filesCreated, filesModified, verify, deviations (none or brief), skipped (none, or what you deliberately did not build — add when ...), error (none or description). Keep it concise — no logs or diffs unless failed.`
167
169
  }
168
170
 
169
171
  function verifyPrompt(a, results, resumeDone) {
@@ -188,9 +190,9 @@ ${baseline}
188
190
  Component results:
189
191
  ${summary}
190
192
 
191
- Checks: completeness (every planned component done, acceptance criteria addressed); files exist for create/modify, rename moved correctly, delete removed; build + test pass (reuse the baseline and component results above when they already prove status — rerun only the commands whose inputs changed, not identical passing ones); correctness (inspect changed files, not just existence); quality (logic-bearing changes have tests when a test framework exists). Prefer changed files over broad scanning.
193
+ Checks: completeness (every planned component done, acceptance criteria addressed); files exist for create/modify, rename moved correctly, delete removed; build + test pass (reuse the baseline and component results above when they already prove status — rerun only the commands whose inputs changed, not identical passing ones); correctness (inspect changed files, not just existence); quality (logic-bearing changes have tests when a test framework exists); scope (run \`git status --short\` and \`git diff HEAD --stat\`, and read untracked files directly since \`git diff HEAD\` omits them: every changed file traces to a planned component — its target, its test, or an import site; flag files outside the plan, work the plan's Scope marks Out or [DEFERRED], new dependencies no component requires, and unrequested abstractions — report drift only in scope; it does not change status). Prefer changed files over broad scanning.
192
194
 
193
- Return structured output: status, completeness, infrastructure, acceptanceCriteria ("satisfied/total"), quality, commands[], failures[].`
195
+ Return structured output: status, completeness, infrastructure, acceptanceCriteria ("satisfied/total"), quality, scope, commands[], failures[] (one "drift: {path} — {why}" entry per drift finding).`
194
196
  }
195
197
 
196
198
  // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -294,9 +296,19 @@ const allPassed = allItems.length > 0 && allItems.every(i => i.result && i.resul
294
296
  // Non-mechanical when a component was planned as sonnet OR needed a sonnet retry to pass —
295
297
  // either signals reasoning-level work that warrants the full verification agent.
296
298
  const usedSonnet = components.some(c => (c.model || 'haiku') === 'sonnet') || allItems.some(i => i.escalated)
299
+ // Deterministic scope signal: files an executor touched beyond its planned target(s).
300
+ // Tests and import sites are legitimate, so this is reported for the user, never a failure.
301
+ const extraFiles = allItems.flatMap(i => {
302
+ const stripDot = f => f.replace(/^\.\//, '')
303
+ const planned = new Set([i.component.file, i.component.sourceFile].filter(Boolean).map(stripDot))
304
+ const touched = i.result ? [...(i.result.filesCreated || []), ...(i.result.filesModified || [])].map(stripDot) : []
305
+ return touched.filter(f => !planned.has(f)).map(f => ({ component: i.component.name, file: f }))
306
+ })
307
+ if (extraFiles.length) log(`scope: ${extraFiles.length} file(s) touched outside planned targets`)
297
308
  let verification
298
309
  if (allPassed && !usedSonnet) {
299
310
  log('all components passed + mechanical (no sonnet, no escalation) — inline verify, no verification agent')
311
+ // No git access here: /5:implement runs the fast-path scope check itself when it sees inline: true.
300
312
  verification = { status: 'passed', completeness: 'passed', infrastructure: 'passed', acceptanceCriteria: 'n/a', quality: 'passed', commands: [], failures: [], inline: true }
301
313
  } else {
302
314
  if (!allItems.length) log('no components ran this invocation (resume) — verifying the already-completed feature against the plan')
@@ -324,5 +336,6 @@ return {
324
336
  items: s.items.map(i => ({ component: i.component.name, file: i.component.file, sourceFile: i.component.sourceFile, escalated: !!i.escalated, result: i.result }))
325
337
  })),
326
338
  verification,
339
+ extraFiles,
327
340
  status: verification.status === 'passed' ? 'completed' : 'failed'
328
341
  }