cleargate 0.5.0 → 0.6.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.
Files changed (54) hide show
  1. package/dist/MANIFEST.json +30 -16
  2. package/dist/cli.cjs +485 -51
  3. package/dist/cli.cjs.map +1 -1
  4. package/dist/cli.js +480 -47
  5. package/dist/cli.js.map +1 -1
  6. package/dist/templates/cleargate-planning/.claude/agents/architect.md +24 -0
  7. package/dist/templates/cleargate-planning/.claude/agents/developer.md +24 -0
  8. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +74 -0
  9. package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +162 -0
  10. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +10 -7
  11. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +9 -8
  12. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +36 -13
  13. package/dist/templates/cleargate-planning/.claude/settings.json +9 -0
  14. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +55 -0
  15. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +7 -7
  16. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +137 -40
  17. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +93 -0
  18. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +8 -4
  19. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +9 -1
  20. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +74 -0
  21. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +65 -1
  22. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +31 -8
  23. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +93 -8
  24. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +19 -4
  25. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +58 -0
  26. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +32 -2
  27. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +3 -1
  28. package/dist/templates/cleargate-planning/CLAUDE.md +1 -1
  29. package/dist/templates/cleargate-planning/MANIFEST.json +30 -16
  30. package/package.json +1 -1
  31. package/templates/cleargate-planning/.claude/agents/architect.md +24 -0
  32. package/templates/cleargate-planning/.claude/agents/developer.md +24 -0
  33. package/templates/cleargate-planning/.claude/agents/reporter.md +74 -0
  34. package/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +162 -0
  35. package/templates/cleargate-planning/.claude/hooks/session-start.sh +10 -7
  36. package/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +9 -8
  37. package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +36 -13
  38. package/templates/cleargate-planning/.claude/settings.json +9 -0
  39. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +55 -0
  40. package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +7 -7
  41. package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +137 -40
  42. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +93 -0
  43. package/templates/cleargate-planning/.cleargate/scripts/constants.mjs +8 -4
  44. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +9 -1
  45. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +74 -0
  46. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +65 -1
  47. package/templates/cleargate-planning/.cleargate/scripts/state.schema.json +31 -8
  48. package/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +93 -8
  49. package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +19 -4
  50. package/templates/cleargate-planning/.cleargate/templates/hotfix.md +58 -0
  51. package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +32 -2
  52. package/templates/cleargate-planning/.cleargate/templates/story.md +3 -1
  53. package/templates/cleargate-planning/CLAUDE.md +1 -1
  54. package/templates/cleargate-planning/MANIFEST.json +30 -16
@@ -115,6 +115,30 @@ After SPRINT-10 ships, `max = 20`. A story drafted before SPRINT-10 might cite `
115
115
 
116
116
  **Rule:** Never let a Developer emit a protocol section number that conflicts with an existing one. Audit first, emit second.
117
117
 
118
+ ## Lane Classification
119
+
120
+ Before emitting a `lane` recommendation per story during Sprint Design Review, run the seven-check rubric. A story is eligible for `lane: fast` **only if all seven checks pass**. Any single false flips it to `standard`.
121
+
122
+ 1. **Size cap.** Implementation diff projected at ≤2 files AND ≤50 LOC net (additions + deletions). Tests count toward the cap; generated files do not.
123
+ 2. **No forbidden surfaces.** Story does not modify any of the following file-path prefixes:
124
+ | Prefix | Category |
125
+ |---|---|
126
+ | `mcp/src/db/`, `**/migrations/` | Database schema / migration |
127
+ | `mcp/src/auth/`, `mcp/src/admin-api/auth-*` | Auth / identity flow |
128
+ | `cleargate.config.json`, `mcp/src/config.ts` | Runtime config schema |
129
+ | `mcp/src/adapters/` | MCP adapter API surface |
130
+ | `cleargate-planning/MANIFEST.json` | Scaffold manifest |
131
+ | token handling, invite verification, gate enforcement | Security-relevant code |
132
+ 3. **No new dependency.** Story does not add a package to any `package.json`. Removals and version pins within an existing major are allowed.
133
+ 4. **Single acceptance scenario or doc-only.** Story Gherkin has exactly one `Scenario:` block (or zero, for pure doc/comment changes). Stories with `Scenario Outline:` or multiple scenarios are not fast-eligible.
134
+ 5. **Existing tests cover the runtime change.** Either (a) story description names an existing test file the change exercises, or (b) story is doc-only / comment-only / non-runtime config. The pre-gate scanner verifies (a) by checking that at least one referenced test file exists and includes the affected module name as a string match.
135
+ 6. **`expected_bounce_exposure: low`.** A story can only be fast if its decomposition signal is already `low`. `med` or `high` is auto-`standard`.
136
+ 7. **No epic-spanning subsystem touches.** Story's affected files all live under one of the epic's declared scope directories. A story that touches files outside its parent epic's declared scope is auto-`standard`.
137
+
138
+ **Sprint Design Review tail step:** After running the rubric on each story, emit `lane: standard|fast` per story in the §1 story table. For every non-`standard` lane, emit a one-line rationale (≤80 chars). Architect MUST write a `## §2.4 Lane Audit` subsection in the Sprint Plan listing every fast-lane story with a ≤80-char rationale. Empty by default — rows added only for non-`standard` lanes.
139
+
140
+ Full rubric, demotion mechanics, and forbidden-surface table are in protocol §24 "Lane Routing". These rules apply under `execution_mode: v2`.
141
+
118
142
  ## Guardrails
119
143
  - **No production code.** You write one markdown plan file. Nothing else.
120
144
  - **No speculation.** Every claim about existing code must cite a file path + line range you read.
@@ -71,6 +71,30 @@ These rules apply under `execution_mode: v2`. Under v1 they are informational.
71
71
 
72
72
  3. **Never run `git worktree add` inside `mcp/`.** The `mcp/` directory is a nested independent git repository. Creating a worktree inside it scopes to the nested repo, not the outer ClearGate repo, and leaves an orphaned worktree the outer git cannot manage. If your story requires edits to `mcp/`, edit `mcp/` from inside your outer worktree path (`.worktrees/STORY-NNN-NN/mcp/...`). See protocol §15.3 for full rationale.
73
73
 
74
+ ## Lane-Aware Execution
75
+
76
+ These rules apply under `execution_mode: v2`. Under v1 they are informational.
77
+
78
+ **On spawn:** read `.cleargate/sprint-runs/<sprint-id>/state.json` for the current sprint (locate the active sprint via `.cleargate/sprint-runs/.active`). Look up the story's `lane` field under `state.json.stories[<story-id>].lane`. Default to `"standard"` if the field is absent, the story key is missing, or `state.json` does not exist.
79
+
80
+ **lane=fast behavior:**
81
+
82
+ - Skip writing the architect-plan-citation block. No plan exists for fast-lane stories; the orchestrator dispatched without one.
83
+ - The pre-gate scanner (`pre_gate_runner.sh`) is **never skipped** on lane=fast — that routing contract belongs to `pre_gate_runner.sh` (STORY-022-04). The Developer's commit MUST still pass typecheck + tests, and the single-commit rule is fully preserved.
84
+ - All other guardrails (no `--no-verify`, no scope bleed, no mocked DB) remain in force regardless of lane.
85
+
86
+ **lane=standard behavior (or lane absent / state.json missing):**
87
+
88
+ - Follow the existing four-agent contract verbatim: read the Architect's plan, cite it in your blueprint section, implement against it.
89
+
90
+ **Demotion handler:**
91
+
92
+ If `state.json` lane flips from `"fast"` to `"standard"` mid-sprint (`lane_demoted_at` populated by `pre_gate_runner.sh` after a fast-lane scanner failure), the orchestrator re-dispatches the story with the architect plan attached. The Developer treats the new dispatch as a fresh spawn and follows the lane=standard contract — there is no state machine or continuation logic on the Developer side.
93
+
94
+ **First-line marker contract preserved:**
95
+
96
+ The Developer's first response line still emits `STORY=NNN-NN` (or `CR=NNN`, `BUG=NNN`, `EPIC=NNN`, `PROPOSAL=NNN` / `PROP=NNN`) per BUG-010's detector contract. Lane is **NOT** part of the first-line marker.
97
+
74
98
  ## Circuit Breaker
75
99
 
76
100
  These rules apply under `execution_mode: v2`. Under v1 they are informational.
@@ -97,6 +97,80 @@ If SPRINT-09 Reporter regresses post-swap of this reporter.md, rollback path:
97
97
  `.cleargate/sprint-runs/S-09/fixtures/sprint-08-shaped/` was used to validate this
98
98
  spec before atomic swap.
99
99
 
100
+ ## Sprint Report v2.1 — Lane + Hotfix Metrics
101
+
102
+ When `state.json` has `schema_version >= 2` AND at least one story shipped with `lane: fast`,
103
+ the Reporter MUST populate the following additional rows and sections. When the activation
104
+ conditions are not met (v1 state, or all stories `lane: standard`), these rows and sections
105
+ may be omitted or left with placeholder values.
106
+
107
+ ### §3 Execution Metrics — Six New Rows
108
+
109
+ The Reporter computes and writes these six rows in §3 (after the existing rows):
110
+
111
+ | Row label | Computation | Source |
112
+ |---|---|---|
113
+ | `Fast-Track Ratio` | `count(stories where lane=fast at sprint close) / total stories × 100` | `state.json` `.stories[*].lane` |
114
+ | `Fast-Track Demotion Rate` | `count(stories with LD event) / count(stories where lane=fast was ever assigned) × 100` | `state.json` `.stories[*].lane_demoted_at` + sprint markdown §4 LD rows |
115
+ | `Hotfix Count (sprint window)` | Count of rows in `wiki/topics/hotfix-ledger.md` where `merged_at` is between sprint `started_at` and `closed_at` | `wiki/topics/hotfix-ledger.md` filtered by sprint window |
116
+ | `Hotfix-to-Story Ratio` | `Hotfix Count / total in-sprint stories` | Derived from above |
117
+ | `Hotfix Cap Breaches` | Count of rolling-7-day windows during the sprint window that had ≥ 3 hotfixes | `wiki/topics/hotfix-ledger.md` `merged_at` column |
118
+ | `LD events` | Count of LD event rows in sprint markdown §4 events list | Sprint plan file `## §4 Events Log` or equivalent |
119
+
120
+ **Sources detail:**
121
+
122
+ - `state.json` lane fields per `.cleargate/scripts/state.schema.json` StoryEntry: `lane`, `lane_assigned_by`, `lane_demoted_at`, `lane_demotion_reason`.
123
+ - Sprint markdown §4 LD events written by `pre_gate_runner.sh` `append_ld_event` (STORY-022-04). Each LD row records the story, timestamp, and demotion reason.
124
+ - `wiki/topics/hotfix-ledger.md` — filter rows by `merged_at` between sprint `started_at` and `closed_at`. If the ledger is absent, record `Hotfix Count = 0` and a note explaining the fallback.
125
+ - For historical sprints with `schema_version: 1` (no lane fields), default all lane metrics to `0` or `N/A` and note the fallback in §5 Tooling.
126
+
127
+ ### §5 Process — Lane Audit table
128
+
129
+ One row per story that was ever assigned `lane: fast` during the sprint (whether it shipped fast
130
+ or was auto-demoted). The Reporter computes the first four columns from `git log` + `state.json`;
131
+ the last two columns are left blank for human fill-in at sprint close.
132
+
133
+ Template row format (per `sprint_report.md` lines 167-172):
134
+
135
+ ```
136
+ | Story | Files touched | LOC | Demoted? | In retrospect, was fast correct? (y/n) | Notes |
137
+ ```
138
+
139
+ - **Story**: story ID (e.g. `STORY-022-08`).
140
+ - **Files touched**: count via `git diff --name-only <base>..<story-sha>`.
141
+ - **LOC**: `git diff --stat <base>..<story-sha>` insertions+deletions total.
142
+ - **Demoted?**: `y` if `lane_demoted_at` is non-null in `state.json`; `n` otherwise.
143
+ - **In retrospect, was fast correct?**: blank — human fills at close.
144
+ - **Notes**: blank — human fills at close.
145
+
146
+ ### §5 Process — Hotfix Audit table
147
+
148
+ One row per hotfix merged within the sprint window. Read from `wiki/topics/hotfix-ledger.md`
149
+ filtered by `merged_at` between sprint `started_at` and `closed_at`. Last two columns blank.
150
+
151
+ Template row format (per `sprint_report.md` lines 174-179):
152
+
153
+ ```
154
+ | Hotfix ID | Originating signal | Files touched | LOC | Resolved-by SHA | Could this have been a sprint story? (y/n) | If y — why was it missed at planning? |
155
+ ```
156
+
157
+ If zero hotfixes in window, write a single row: `| (none) | — | — | — | — | — | — |`
158
+
159
+ ### §5 Process — Hotfix Trend narrative
160
+
161
+ A one-paragraph narrative summarising the rolling 4-sprint hotfix count and a
162
+ monotonic-increase flag. The Reporter reads the last 4 sprint `REPORT.md` files
163
+ (at `.cleargate/sprint-runs/<id>/REPORT.md`) OR walks `wiki/topics/hotfix-ledger.md`
164
+ by `sprint_id` field to gather per-sprint counts.
165
+
166
+ Monotonic-increase flag: if the count increased (or stayed ≥ 1) for 3+ consecutive sprints,
167
+ flag it as `trend: INCREASING` and recommend a retrospective action in §5 Tooling.
168
+
169
+ For historical v1-schema sprints with no lane data, record `0 hotfixes (v1 — no ledger data)`.
170
+
171
+ Template location: `sprint_report.md` lines 181-188. Leave the placeholder text intact for
172
+ sprints with no hotfixes in the window.
173
+
100
174
  ## Guardrails
101
175
  - **Numbers before narrative.** Every claim in §1 must be backed by a ledger row, commit, or flashcard -- cite them.
102
176
  - **Do not fabricate cost.** If you cannot find current model rates, state the rate date and mark cost `~$X (rates as of <date>)`.
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bash
2
+ # pre-edit-gate.sh — CR-008 Phase B: planning-first PreToolUse gate
3
+ #
4
+ # Registered as a PreToolUse hook for Edit|Write tool calls.
5
+ # In warn mode (default), logs would-block decisions but always exits 0.
6
+ # In enforce mode, exits 1 to block the tool call when planning is missing.
7
+ #
8
+ # Mode controlled by CLEARGATE_PLANNING_GATE_MODE (warn|enforce|off). Default: warn.
9
+ # Bypass: CLEARGATE_PLANNING_BYPASS=1 → skip check, log bypass=true, exit 0.
10
+
11
+ set -u
12
+
13
+ REPO_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
14
+ MODE="${CLEARGATE_PLANNING_GATE_MODE:-warn}"
15
+ LOG_FILE="${REPO_ROOT}/.cleargate/hook-log/pre-edit-gate-warn.log"
16
+
17
+ # ── 0. Off mode — do nothing ──────────────────────────────────────────────────
18
+ if [ "${MODE}" = "off" ]; then
19
+ exit 0
20
+ fi
21
+
22
+ # ── 1. Read file path from stdin (PreToolUse JSON payload) ────────────────────
23
+ # Claude Code sends JSON on stdin: {"tool_name":"Edit","tool_input":{"file_path":"..."}}
24
+ INPUT=$(cat)
25
+ FILE=$(printf '%s' "${INPUT}" | node -e "
26
+ try {
27
+ var d = require('fs').readFileSync('/dev/stdin','utf8');
28
+ var o = JSON.parse(d);
29
+ var fp = (o.tool_input && o.tool_input.file_path) ? o.tool_input.file_path : '';
30
+ process.stdout.write(fp);
31
+ } catch(e) {
32
+ process.stdout.write('');
33
+ }
34
+ " 2>/dev/null || true)
35
+
36
+ TOOL_NAME=$(printf '%s' "${INPUT}" | node -e "
37
+ try {
38
+ var d = require('fs').readFileSync('/dev/stdin','utf8');
39
+ var o = JSON.parse(d);
40
+ process.stdout.write(o.tool_name || '');
41
+ } catch(e) {
42
+ process.stdout.write('');
43
+ }
44
+ " 2>/dev/null || true)
45
+
46
+ # ── Guard: if we couldn't parse the file path, fail-open ──────────────────────
47
+ if [ -z "${FILE}" ]; then
48
+ exit 0
49
+ fi
50
+
51
+ # ── 2. Bypass env var ─────────────────────────────────────────────────────────
52
+ if [ "${CLEARGATE_PLANNING_BYPASS:-0}" = "1" ]; then
53
+ ISO_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
54
+ mkdir -p "$(dirname "${LOG_FILE}")"
55
+ printf '[%s] mode=%s bypass=true file=%s\n' "${ISO_TS}" "${MODE}" "${FILE}" >> "${LOG_FILE}"
56
+ exit 0
57
+ fi
58
+
59
+ # ── 3. Whitelist: always allow these paths ────────────────────────────────────
60
+ # Normalise: strip leading REPO_ROOT prefix for matching
61
+ REL_FILE="${FILE}"
62
+ if [[ "${FILE}" == "${REPO_ROOT}/"* ]]; then
63
+ REL_FILE="${FILE#${REPO_ROOT}/}"
64
+ fi
65
+
66
+ is_whitelisted() {
67
+ local f="$1"
68
+ # Exact or prefix matches for whitelisted directories/files
69
+ case "${f}" in
70
+ .cleargate/*|.claude/*|cleargate-planning/*) return 0 ;;
71
+ CLAUDE.md|MANIFEST.json|README.md|.gitignore|.gitkeep) return 0 ;;
72
+ package.json|package-lock.json) return 0 ;;
73
+ .env|.env.*|.npmrc|.editorconfig) return 0 ;;
74
+ # Also allow absolute paths that fall under the repo's cleargate dirs
75
+ esac
76
+ # Check absolute path variants
77
+ case "${FILE}" in
78
+ "${REPO_ROOT}/.cleargate/"*|"${REPO_ROOT}/.claude/"*|"${REPO_ROOT}/cleargate-planning/"*) return 0 ;;
79
+ esac
80
+ return 1
81
+ }
82
+
83
+ if is_whitelisted "${REL_FILE}"; then
84
+ exit 0
85
+ fi
86
+
87
+ # ── 3.5 Sprint-active sentinel bypass ────────────────────────────────────────
88
+ # If a sprint is actively running, all in-sprint edits bypass the gate.
89
+ if [ -f "${REPO_ROOT}/.cleargate/sprint-runs/.active" ]; then
90
+ exit 0
91
+ fi
92
+
93
+ # ── 4. Resolve cleargate CLI (three-branch resolver — CR-009) ────────────────
94
+ if [ -f "${REPO_ROOT}/cleargate-cli/dist/cli.js" ]; then
95
+ CG=(node "${REPO_ROOT}/cleargate-cli/dist/cli.js")
96
+ elif command -v cleargate >/dev/null 2>&1; then
97
+ CG=(cleargate)
98
+ else
99
+ # Read pinned version from stamp-and-gate.sh
100
+ HOOK_PIN=""
101
+ HOOK_SH="${REPO_ROOT}/.claude/hooks/stamp-and-gate.sh"
102
+ if [ -f "${HOOK_SH}" ]; then
103
+ HOOK_PIN=$(grep -oP '(?<=# cleargate-pin: )[\S]+' "${HOOK_SH}" 2>/dev/null || \
104
+ grep -oE 'cleargate@[^"]+' "${HOOK_SH}" 2>/dev/null | head -1 | sed 's/.*@//' || true)
105
+ fi
106
+ if [ -z "${HOOK_PIN}" ]; then
107
+ HOOK_PIN="__CLEARGATE_VERSION__"
108
+ fi
109
+ CG=(npx -y "cleargate@${HOOK_PIN}")
110
+ fi
111
+
112
+ # ── 5. Read user prompt snippet for log context (optional; best-effort) ───────
113
+ PROMPT_SNIPPET=$(printf '%s' "${INPUT}" | node -e "
114
+ try {
115
+ var d = require('fs').readFileSync('/dev/stdin','utf8');
116
+ var o = JSON.parse(d);
117
+ var p = (o.user_prompt || '').slice(0, 200);
118
+ process.stdout.write(p);
119
+ } catch(e) {
120
+ process.stdout.write('');
121
+ }
122
+ " 2>/dev/null || true)
123
+
124
+ # ── 6. Ask doctor --can-edit ──────────────────────────────────────────────────
125
+ # Capture both stdout AND exit code without || true swallowing the exit code.
126
+ _DOCTOR_TMPFILE=$(mktemp)
127
+ "${CG[@]}" doctor --can-edit "${FILE}" --cwd "${REPO_ROOT}" > "${_DOCTOR_TMPFILE}" 2>/dev/null
128
+ DOCTOR_EXIT=$?
129
+ DOCTOR_OUT=$(cat "${_DOCTOR_TMPFILE}")
130
+ rm -f "${_DOCTOR_TMPFILE}"
131
+
132
+ # Parse reason from doctor output
133
+ REASON=$(printf '%s' "${DOCTOR_OUT}" | grep -oP '(?<=blocked: )\S+' 2>/dev/null || \
134
+ printf '%s' "${DOCTOR_OUT}" | sed -n 's/blocked: //p' 2>/dev/null || \
135
+ echo "unknown")
136
+
137
+ # Count approved stories in pending-sync (for log entry)
138
+ PENDING_DIR="${REPO_ROOT}/.cleargate/delivery/pending-sync"
139
+ PENDING_MATCH_COUNT=0
140
+ if [ -d "${PENDING_DIR}" ]; then
141
+ PENDING_MATCH_COUNT=$(grep -rl 'approved: true' "${PENDING_DIR}" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
142
+ fi
143
+
144
+ # ── 7. Gate decision ──────────────────────────────────────────────────────────
145
+ if [ "${DOCTOR_EXIT}" -ne 0 ]; then
146
+ # Would block
147
+ ISO_TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
148
+ mkdir -p "$(dirname "${LOG_FILE}")"
149
+ printf '[%s] mode=%s would_block file=%s reason=%s tool=%s pending_match_count=%d prompt_snippet=%s\n' \
150
+ "${ISO_TS}" "${MODE}" "${FILE}" "${REASON}" "${TOOL_NAME}" "${PENDING_MATCH_COUNT}" "${PROMPT_SNIPPET}" \
151
+ >> "${LOG_FILE}"
152
+
153
+ if [ "${MODE}" = "enforce" ]; then
154
+ printf 'ClearGate: planning-first gate — no approved story covers %s (%s). Draft a work item first.\n' \
155
+ "${FILE}" "${REASON}" >&2
156
+ exit 1
157
+ fi
158
+
159
+ # warn mode: log written above, exit 0 (do not block)
160
+ fi
161
+
162
+ exit 0
@@ -2,17 +2,20 @@
2
2
  set -u
3
3
  REPO_ROOT="${CLAUDE_PROJECT_DIR}"
4
4
 
5
- # Resolve cleargate CLI: prefer on-PATH binary, fall back to meta-repo-local
6
- # dist. If neither is present, this hook is a no-op (BUG-006).
7
- if command -v cleargate >/dev/null 2>&1; then
8
- CG=(cleargate)
9
- elif [ -f "${REPO_ROOT}/cleargate-cli/dist/cli.js" ]; then
5
+ # cleargate-pin: __CLEARGATE_VERSION__
6
+ # Resolve cleargate CLI (three-branch resolver CR-009):
7
+ # 1. meta-repo dogfood dist (fastest; only present in ClearGate's own repo)
8
+ # 2. on-PATH binary (global install or shim)
9
+ # 3. pinned npx invocation (always works wherever Node is present)
10
+ if [ -f "${REPO_ROOT}/cleargate-cli/dist/cli.js" ]; then
10
11
  CG=(node "${REPO_ROOT}/cleargate-cli/dist/cli.js")
12
+ elif command -v cleargate >/dev/null 2>&1; then
13
+ CG=(cleargate)
11
14
  else
12
- exit 0
15
+ CG=(npx -y "cleargate@__CLEARGATE_VERSION__")
13
16
  fi
14
17
 
15
- "${CG[@]}" doctor --session-start 2>/dev/null || true
18
+ "${CG[@]}" doctor --session-start || true
16
19
 
17
20
  # ── §14.9 SessionStart sync nudge (STORY-010-08) ─────────────────────────────
18
21
  # Daily-throttled: probe remote for updates at most once per 24h.
@@ -4,16 +4,17 @@ REPO_ROOT="${CLAUDE_PROJECT_DIR}"
4
4
  LOG="${REPO_ROOT}/.cleargate/hook-log/gate-check.log"
5
5
  mkdir -p "$(dirname "$LOG")"
6
6
 
7
- # Resolve cleargate CLI: prefer on-PATH binary (`npm i -g cleargate` / `npx`),
8
- # fall back to a meta-repo-local dist (dogfood case). If neither is present,
9
- # log a remediation message and exit 0 (BUG-006).
10
- if command -v cleargate >/dev/null 2>&1; then
11
- CG=(cleargate)
12
- elif [ -f "${REPO_ROOT}/cleargate-cli/dist/cli.js" ]; then
7
+ # cleargate-pin: __CLEARGATE_VERSION__
8
+ # Resolve cleargate CLI (three-branch resolver CR-009):
9
+ # 1. meta-repo dogfood dist (fastest; only present in ClearGate's own repo)
10
+ # 2. on-PATH binary (global install or shim)
11
+ # 3. pinned npx invocation (always works wherever Node is present)
12
+ if [ -f "${REPO_ROOT}/cleargate-cli/dist/cli.js" ]; then
13
13
  CG=(node "${REPO_ROOT}/cleargate-cli/dist/cli.js")
14
+ elif command -v cleargate >/dev/null 2>&1; then
15
+ CG=(cleargate)
14
16
  else
15
- echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] cleargate CLI not found — install with 'npm i -g cleargate' or run via 'npx cleargate'. Hook skipped." >>"$LOG"
16
- exit 0
17
+ CG=(npx -y "cleargate@__CLEARGATE_VERSION__")
17
18
  fi
18
19
 
19
20
  FILE=$(jq -r '.tool_input.file_path' 2>/dev/null || echo "")
@@ -15,12 +15,21 @@
15
15
  # misrouted SPRINT-04 firings to SPRINT-03 because ledger appends
16
16
  # themselves bumped SPRINT-03's mtime. See REPORT.md SPRINT-04.
17
17
  #
18
- # Work-item ID detection (GENERALIZED 2026-04-19 STORY-008-04):
18
+ # Work-item ID detection (GENERALIZED 2026-04-19 STORY-008-04, SCOPED 2026-04-26 BUG-010):
19
19
  # Primary : first user message in the transcript — that's the orchestrator's
20
20
  # dispatch prompt, which by agent convention starts with
21
21
  # `STORY=NNN-NN`, `PROPOSAL-NNN`, `EPIC-NNN`, `CR-NNN`, or `BUG-NNN`.
22
- # Pattern : (STORY|PROPOSAL|EPIC|CR|BUG)[-=]?[0-9]+(-[0-9]+)?
23
- # Fallback : grep first match anywhere in the transcript.
22
+ # Scoping : look ONLY at lines whose FIRST CHARACTER matches the dispatch-marker
23
+ # pattern (line-anchored). The SessionStart hook emits a "blocked items"
24
+ # reminder that lists work-item IDs in prose/bullets — those lines start
25
+ # with "- " or whitespace, never with the marker prefix. Scanning the full
26
+ # content caused BUG-010: all SPRINT-14 rows tagged BUG-002 (first item in
27
+ # the reminder list). Fix: join content with \n, split, filter line-starts.
28
+ # Pattern : ^(STORY|PROPOSAL|PROP|EPIC|CR|BUG)[-=][0-9]...
29
+ # PROP-NNN is normalised to PROPOSAL-NNN after match (BUG-009, 2026-04-26)
30
+ # Tie-break: when multiple dispatch-marker lines appear in the first user message,
31
+ # the FIRST line wins (deterministic; orchestrator puts the marker first).
32
+ # Fallback : grep first line-start match anywhere in the transcript.
24
33
  # story_id : populated only when the match is a STORY-* (backward compat).
25
34
  # work_item_id: always populated when detection succeeds; equals story_id for STORY items.
26
35
  # (Removed): grep-first-anywhere as PRIMARY — it picked up SPRINT-05 mentions
@@ -107,28 +116,42 @@ ACTIVE_SENTINEL="${REPO_ROOT}/.cleargate/sprint-runs/.active"
107
116
  fi
108
117
 
109
118
  # --- detect work_item_id (PRIMARY: first user message; FALLBACK: anywhere-grep) ---
110
- # Orchestrator convention (.claude/agents/developer.md): the first line of the
111
- # dispatch prompt is `STORY=NNN-NN`. The first user message in the transcript
112
- # is that prompt. Look there first to avoid picking up work-item mentions inside
113
- # the subagent's later reads of plans, sprint files, etc.
119
+ # Orchestrator convention (.claude/agents/developer.md): the FIRST LINE of the
120
+ # dispatch prompt is `STORY=NNN-NN` (or CR=NNN, BUG=NNN, EPIC=NNN, PROPOSAL=NNN,
121
+ # PROP=NNN). The SessionStart hook injects a "blocked items" reminder whose lines
122
+ # start with "- " or whitespace never with the marker prefix. We therefore
123
+ # ONLY match lines whose first character is the start of the marker (line-anchored).
124
+ #
125
+ # BUG-010 (2026-04-26): joined with " " (space) before — scan() found BUG-002 from
126
+ # the reminder text. Fix: join with "\n" so line-anchored regex works.
114
127
  #
115
128
  # Pattern covers: STORY-NNN-NN, PROPOSAL-NNN, EPIC-NNN, CR-NNN, BUG-NNN
116
129
  # (and = separator variants like STORY=008-04)
130
+ # Tie-break: first line matching the anchor wins (orchestrator puts marker first).
117
131
  WORK_ITEM_RAW="$(jq -rs '
118
132
  [.[] | select(.type == "user")] | .[0].message.content
119
- | if type == "array" then map(.text? // "") | join(" ") else (. // "") end
133
+ | if type == "array" then map(.text? // "") | join("\n") else (. // "") end
120
134
  | tostring
121
- | scan("(STORY|PROPOSAL|EPIC|CR|BUG)[-=]?([0-9]+(-[0-9]+)?)") | .[0:2] | join("-")
135
+ | split("\n")
136
+ | map(select(test("^(STORY|PROPOSAL|PROP|EPIC|CR|BUG)[-=][0-9]")))
137
+ | .[0] // ""
138
+ | capture("^(?<kind>STORY|PROPOSAL|PROP|EPIC|CR|BUG)[-=](?<id>[0-9]+(-[0-9]+)?)")
139
+ | (.kind + "-" + .id)
122
140
  ' "${TRANSCRIPT_PATH}" 2>/dev/null | head -1)"
123
141
 
124
142
  if [[ -n "${WORK_ITEM_RAW}" && "${WORK_ITEM_RAW}" != "null" && "${WORK_ITEM_RAW}" != "-" ]]; then
125
143
  # Normalize: replace any = separator with - in the result
126
- WORK_ITEM_ID="$(printf '%s' "${WORK_ITEM_RAW}" | sed 's/=/-/g')"
144
+ # Then normalize PROP-NNN → PROPOSAL-NNN (canonical form matches wiki/proposals/PROPOSAL-NNN.md filenames)
145
+ # BUG-009 (2026-04-26): PROP↔PROPOSAL normalization — PRESERVED BY BUG-010.
146
+ WORK_ITEM_ID="$(printf '%s' "${WORK_ITEM_RAW}" | sed 's/=/-/g' | sed 's/^PROP-/PROPOSAL-/')"
127
147
  else
128
- # Fallback: grep anywhere in the transcript
129
- WORK_ITEM_ID="$(grep -oE '(STORY|PROPOSAL|EPIC|CR|BUG)[-=]?[0-9]+(-[0-9]+)?' "${TRANSCRIPT_PATH}" 2>/dev/null \
148
+ # Fallback: grep for line-start dispatch markers anywhere in the transcript.
149
+ # Line-anchored (^) so that "- BUG-002:" reminder bullet lines are not matched —
150
+ # only lines whose first character starts the marker prefix.
151
+ # BUG-009 normalization preserved: sed 's/=/-/g' | sed 's/^PROP-/PROPOSAL-/'
152
+ WORK_ITEM_ID="$(grep -oE '^(STORY|PROPOSAL|PROP|EPIC|CR|BUG)[-=][0-9][0-9]*(-[0-9]+)?' "${TRANSCRIPT_PATH}" 2>/dev/null \
130
153
  | head -1 \
131
- | sed 's/=/-/g')"
154
+ | sed 's/=/-/g' | sed 's/^PROP-/PROPOSAL-/')"
132
155
  if [[ -n "${WORK_ITEM_ID}" ]]; then
133
156
  printf '[%s] work_item_id fallback grep: %s\n' "$(date -u +%FT%TZ)" "${WORK_ITEM_ID}" >> "${HOOK_LOG}"
134
157
  fi
@@ -19,6 +19,15 @@
19
19
  "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/pending-task-sentinel.sh"
20
20
  }
21
21
  ]
22
+ },
23
+ {
24
+ "matcher": "Edit|Write",
25
+ "hooks": [
26
+ {
27
+ "type": "command",
28
+ "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-edit-gate.sh"
29
+ }
30
+ ]
22
31
  }
23
32
  ],
24
33
  "PostToolUse": [
@@ -108,6 +108,8 @@ Under `execution_mode: "v1"` this rule is **advisory only** — the orchestrator
108
108
 
109
109
  **v2 story-file assertion:** Additionally for v2 sprints, `cleargate sprint init` asserts every story in §1 Consolidated Deliverables has a `pending-sync/STORY-*.md` file before writing `state.json`; missing files block init with an enumerated stderr list. Under v1 the assertion runs but only warns (does not block). The assertion is also available standalone: `node .cleargate/scripts/assert_story_files.mjs <sprint-file-path>`.
110
110
 
111
+ As of cleargate@0.6.x, sprint-init asserts all six work-item id shapes (STORY/CR/BUG/EPIC/PROPOSAL/HOTFIX). v2 mode hard-blocks on missing OR unapproved OR stub-empty items; v1 warns-only (backwards compat).
112
+
111
113
  ### Gate 3 — Push Gate
112
114
 
113
115
  - **Never call `cleargate_push_item` on a file where `approved: false`.**
@@ -913,3 +915,56 @@ wiki:
913
915
  ```
914
916
 
915
917
  Exceeding the ceiling fails `cleargate wiki lint` (enforcement mode). Under `--suggest`, the usage percentage is reported but the check does not fail. Reference: EPIC-015.
918
+
919
+ ---
920
+
921
+ ## 22. Advisory Readiness Gates on Push (v2) — CR-010
922
+
923
+ ### §22.1 Two-tier push gate semantics
924
+
925
+ Push-time gate enforcement uses two distinct tiers:
926
+
927
+ **Tier 1 — `approved: true` (hard reject, unchanged):**
928
+ `cleargate_push_item` throws `PushNotApprovedError` when `payload.approved !== true`. This is the human go/no-go gate. No advisory mode or env knob overrides it.
929
+
930
+ **Tier 2 — `cached_gate_result` (advisory by default):**
931
+ When `cached_gate_result.pass === false`, the push proceeds in default advisory mode. The pushed item's body receives a single advisory prefix line placed immediately after the H1 heading (or as the first line if no H1 exists):
932
+
933
+ ```
934
+ [advisory: gate_failed — <comma-separated criterion ids>]
935
+ ```
936
+
937
+ Body content beyond the advisory prefix is byte-identical to the input. The push result includes `gate_status: 'open'` and `failing_criteria: [...]` as response metadata (not persisted to the DB schema).
938
+
939
+ ### §22.2 Strict-mode opt-in and audit log
940
+
941
+ Set `STRICT_PUSH_GATES=true` on the MCP server to restore pre-CR-010 hard-reject behavior (`PushGateFailedError`, no DB write). Default: `false` (advisory mode).
942
+
943
+ Advisory pushes (gate_status='open') are recorded in `audit_log` with `result='ok'` — the push succeeded. The `failing_criteria` are surfaced in the push response shape, not in a new audit column. No schema migration is required.
944
+ **Rationale:** PM-tool answer-collection requires items to land before readiness answers arrive; advisory mode enables this. See CR-010 §0 for full evidence.
945
+
946
+ ---
947
+
948
+ ## 23. Doctor Exit-Code Semantics
949
+
950
+ `cleargate doctor` exits with one of three codes (all modes: default, `--session-start`, `--can-edit`, `--check-scaffold`, `--pricing`). Hooks branch on the integer, not on stdout.
951
+ - `0` — clean. No blockers, no config errors. Stdout MAY include informational lines.
952
+ - `1` — blocked items or advisory issues (gate failures, stamp errors, drifted SHAs, missing ledger rows). Stdout lists each blocker.
953
+ - `2` — ClearGate misconfigured or partially installed (missing `.cleargate/`, missing `MANIFEST.json`, missing `auth.json`, hook resolver failure). Stdout emits a remediation hint. See `cleargate doctor --help`.
954
+
955
+ ---
956
+
957
+ ## 24. Lane Routing
958
+
959
+ A story is eligible for `lane: fast` only if all seven checks pass (any false → `standard`):
960
+ 1. **Size cap.** ≤2 files AND ≤50 LOC net (tests count; generated files do not).
961
+ 2. **No forbidden surfaces.** Story does not modify: `mcp/src/db/` / `**/migrations/` (schema); `mcp/src/auth/` / `mcp/src/admin-api/auth-*` (auth); `cleargate.config.json` / `mcp/src/config.ts` (runtime config); `mcp/src/adapters/` (adapter API); `cleargate-planning/MANIFEST.json` (scaffold manifest); security-relevant code (token handling, invite verification, gate enforcement).
962
+ 3. **No new dependency.** No new package added to any `package.json`.
963
+ 4. **Single acceptance scenario or doc-only.** Exactly one `Scenario:` block (or zero for doc-only). `Scenario Outline:` or multiple scenarios → `standard`.
964
+ 5. **Existing tests cover the runtime change.** Named test file exists and includes the affected module, OR story is doc/comment/non-runtime config only.
965
+ 6. **`expected_bounce_exposure: low`.** `med` or `high` is auto-`standard`.
966
+ 7. **No epic-spanning subsystem touches.** All affected files live under the parent epic's declared scope directories.
967
+
968
+ **Demotion mechanics.** Demotion is one-way (`fast → standard`). Trigger: pre-gate scanner failure OR post-merge test failure on a fast-lane story. On demotion: set `lane = "standard"`, write `lane_demoted_at` (ISO-8601), `lane_demotion_reason`, reset `qa_bounces = 0` and `arch_bounces = 0` (see STORY-022-02 schema). Architect plan is invoked and QA spawned per standard contract.
969
+
970
+ Event-type `LD` (Lane Demotion) is recorded in sprint markdown §4 alongside existing `UR` and `CR` events; Reporter aggregates into §3 Execution Metrics > Fast-Track Demotion Rate.
@@ -52,7 +52,7 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
52
52
  - id: touched-files-populated
53
53
  check: "section(3) has ≥1 listed-item"
54
54
  - id: no-tbds
55
- check: "body does not contain 'TBD'"
55
+ check: "body does not contain marker 'TBD'"
56
56
  ```
57
57
 
58
58
  ```yaml
@@ -63,7 +63,7 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
63
63
  - id: proposal-approved
64
64
  check: "frontmatter(context_source).approved == true"
65
65
  - id: no-tbds
66
- check: "body does not contain 'TBD'"
66
+ check: "body does not contain marker 'TBD'"
67
67
  - id: scope-in-populated
68
68
  check: "section(2) has ≥1 listed-item"
69
69
  - id: affected-files-declared
@@ -84,7 +84,7 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
84
84
  - id: gherkin-error-path
85
85
  check: "body contains 'Error'"
86
86
  - id: no-tbds
87
- check: "body does not contain 'TBD'"
87
+ check: "body does not contain marker 'TBD'"
88
88
  - id: interrogation-resolved
89
89
  check: "body does not contain 'Unresolved'"
90
90
  ```
@@ -97,7 +97,7 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
97
97
  - id: parent-epic-ref-set
98
98
  check: "frontmatter(.).parent_epic_ref != null"
99
99
  - id: no-tbds
100
- check: "body does not contain 'TBD'"
100
+ check: "body does not contain marker 'TBD'"
101
101
  - id: implementation-files-declared
102
102
  check: "section(3) has ≥1 listed-item"
103
103
  - id: dod-declared
@@ -112,9 +112,9 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
112
112
  severity: enforcing
113
113
  criteria:
114
114
  - id: blast-radius-populated
115
- check: "section(1) has ≥1 listed-item"
115
+ check: "section(2) has ≥1 listed-item"
116
116
  - id: no-tbds
117
- check: "body does not contain 'TBD'"
117
+ check: "body does not contain marker 'TBD'"
118
118
  - id: sandbox-paths-declared
119
119
  check: "section(2) has ≥1 listed-item"
120
120
  ```
@@ -129,5 +129,5 @@ The asymmetry exists because Proposal documents are human-authored strategy arti
129
129
  - id: severity-set
130
130
  check: "frontmatter(.).severity != null"
131
131
  - id: no-tbds
132
- check: "body does not contain 'TBD'"
132
+ check: "body does not contain marker 'TBD'"
133
133
  ```