cleargate 0.2.0 → 0.3.0

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 (113) hide show
  1. package/LICENSE +21 -0
  2. package/dist/MANIFEST.json +59 -17
  3. package/dist/admin-api/index.cjs +88 -1
  4. package/dist/admin-api/index.cjs.map +1 -1
  5. package/dist/admin-api/index.d.cts +105 -1
  6. package/dist/admin-api/index.d.ts +105 -1
  7. package/dist/admin-api/index.js +77 -1
  8. package/dist/admin-api/index.js.map +1 -1
  9. package/dist/bootstrap-root-FGWDICDT.js +130 -0
  10. package/dist/bootstrap-root-FGWDICDT.js.map +1 -0
  11. package/dist/chunk-OM4FAEA7.js +184 -0
  12. package/dist/chunk-OM4FAEA7.js.map +1 -0
  13. package/dist/cli.cjs +7995 -3984
  14. package/dist/cli.cjs.map +1 -1
  15. package/dist/cli.js +4062 -561
  16. package/dist/cli.js.map +1 -1
  17. package/dist/templates/cleargate-planning/.claude/agents/architect.md +72 -0
  18. package/dist/templates/cleargate-planning/.claude/agents/developer.md +45 -3
  19. package/dist/templates/cleargate-planning/.claude/agents/qa.md +7 -3
  20. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +72 -75
  21. package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +204 -0
  22. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +10 -0
  23. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +58 -0
  24. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +19 -0
  25. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +51 -0
  26. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +1 -1
  27. package/dist/templates/cleargate-planning/.claude/settings.json +11 -0
  28. package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +31 -12
  29. package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +2 -1
  30. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +37 -0
  31. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +407 -0
  32. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +146 -0
  33. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +250 -0
  34. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +57 -0
  35. package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +320 -0
  36. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +15 -0
  37. package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +38 -0
  38. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +187 -0
  39. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +132 -0
  40. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +307 -0
  41. package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +280 -0
  42. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +123 -0
  43. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +110 -0
  44. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +247 -0
  45. package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +27 -0
  46. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +261 -0
  47. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +210 -0
  48. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +190 -0
  49. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +327 -0
  50. package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +261 -0
  51. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +154 -0
  52. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +111 -0
  53. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +164 -0
  54. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +9 -0
  55. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +9 -0
  56. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +29 -3
  57. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +9 -0
  58. package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +9 -0
  59. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +42 -0
  60. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +175 -0
  61. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +29 -0
  62. package/dist/templates/cleargate-planning/CLAUDE.md +3 -0
  63. package/dist/templates/cleargate-planning/MANIFEST.json +59 -17
  64. package/dist/whoami-CX7CXJD5.js +76 -0
  65. package/dist/whoami-CX7CXJD5.js.map +1 -0
  66. package/package.json +6 -2
  67. package/templates/cleargate-planning/.claude/agents/architect.md +72 -0
  68. package/templates/cleargate-planning/.claude/agents/developer.md +45 -3
  69. package/templates/cleargate-planning/.claude/agents/qa.md +7 -3
  70. package/templates/cleargate-planning/.claude/agents/reporter.md +72 -75
  71. package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +204 -0
  72. package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +10 -0
  73. package/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +58 -0
  74. package/templates/cleargate-planning/.claude/hooks/pre-commit.sh +19 -0
  75. package/templates/cleargate-planning/.claude/hooks/session-start.sh +51 -0
  76. package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +1 -1
  77. package/templates/cleargate-planning/.claude/settings.json +11 -0
  78. package/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +31 -12
  79. package/templates/cleargate-planning/.cleargate/FLASHCARD.md +2 -1
  80. package/templates/cleargate-planning/.cleargate/config.example.yml +37 -0
  81. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +407 -0
  82. package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +146 -0
  83. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +250 -0
  84. package/templates/cleargate-planning/.cleargate/scripts/constants.mjs +57 -0
  85. package/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +320 -0
  86. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +15 -0
  87. package/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +38 -0
  88. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +187 -0
  89. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +132 -0
  90. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +307 -0
  91. package/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +280 -0
  92. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +123 -0
  93. package/templates/cleargate-planning/.cleargate/scripts/state.schema.json +110 -0
  94. package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +247 -0
  95. package/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +27 -0
  96. package/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +261 -0
  97. package/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +210 -0
  98. package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +190 -0
  99. package/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +327 -0
  100. package/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +261 -0
  101. package/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +154 -0
  102. package/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +111 -0
  103. package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +164 -0
  104. package/templates/cleargate-planning/.cleargate/templates/Bug.md +9 -0
  105. package/templates/cleargate-planning/.cleargate/templates/CR.md +9 -0
  106. package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +29 -3
  107. package/templates/cleargate-planning/.cleargate/templates/epic.md +9 -0
  108. package/templates/cleargate-planning/.cleargate/templates/proposal.md +9 -0
  109. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +42 -0
  110. package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +175 -0
  111. package/templates/cleargate-planning/.cleargate/templates/story.md +29 -0
  112. package/templates/cleargate-planning/CLAUDE.md +3 -0
  113. package/templates/cleargate-planning/MANIFEST.json +59 -17
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * init_sprint.mjs — Initialize a sprint state.json
4
+ *
5
+ * Usage: node init_sprint.mjs <sprint-id> --stories ID1,ID2,... [--force]
6
+ *
7
+ * Creates .cleargate/sprint-runs/<sprint-id>/state.json with initial state
8
+ * "Ready to Bounce" for each story. Refuses if state.json already exists
9
+ * unless --force is passed.
10
+ *
11
+ * Under execution_mode: v2 (read from sprint frontmatter):
12
+ * - Asserts all story files exist in pending-sync/ before writing state.json.
13
+ * - On failure: prints missing list to stderr and exits 1 WITHOUT creating state.json.
14
+ *
15
+ * Under execution_mode: v1:
16
+ * - Runs the same assertion but only warns on failure (does not block).
17
+ */
18
+
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { spawnSync } from 'node:child_process';
23
+ import { SCHEMA_VERSION, VALID_STATES, TERMINAL_STATES } from './constants.mjs';
24
+
25
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
+ // Resolve repo root: .cleargate/scripts/ -> ../../ (two levels up)
27
+ // CLEARGATE_REPO_ROOT env var overrides for testing
28
+ const REPO_ROOT = process.env.CLEARGATE_REPO_ROOT
29
+ ? path.resolve(process.env.CLEARGATE_REPO_ROOT)
30
+ : path.resolve(__dirname, '..', '..');
31
+
32
+ function usage() {
33
+ process.stderr.write(
34
+ 'Usage: node init_sprint.mjs <sprint-id> --stories ID1,ID2,... [--force]\n'
35
+ );
36
+ process.exit(2);
37
+ }
38
+
39
+ /**
40
+ * Read execution_mode from a sprint file's YAML frontmatter.
41
+ * Uses a single-field regex — does NOT hand-roll a full YAML parser.
42
+ * Returns 'v2' | 'v1' (defaults to 'v1' if field absent or unreadable).
43
+ */
44
+ function readExecutionMode(sprintFilePath) {
45
+ let content;
46
+ try {
47
+ content = fs.readFileSync(sprintFilePath, 'utf8');
48
+ } catch {
49
+ return 'v1';
50
+ }
51
+ // Match frontmatter block
52
+ const fm = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
53
+ if (!fm) return 'v1';
54
+ const modeMatch = fm[1].match(/^execution_mode:\s*["']?(v[12])/m);
55
+ if (!modeMatch) return 'v1';
56
+ return modeMatch[1];
57
+ }
58
+
59
+ /**
60
+ * Locate the sprint file in pending-sync/ or archive/ for the given sprint ID.
61
+ * Returns the absolute path if found, null otherwise.
62
+ */
63
+ function findSprintFile(repoRoot, sprintId) {
64
+ const searchDirs = [
65
+ path.join(repoRoot, '.cleargate', 'delivery', 'pending-sync'),
66
+ path.join(repoRoot, '.cleargate', 'delivery', 'archive'),
67
+ ];
68
+ for (const dir of searchDirs) {
69
+ let entries;
70
+ try {
71
+ entries = fs.readdirSync(dir);
72
+ } catch {
73
+ continue;
74
+ }
75
+ const prefix = `${sprintId}_`;
76
+ const match = entries.find(
77
+ (e) => (e === `${sprintId}.md` || e.startsWith(prefix)) && e.endsWith('.md')
78
+ );
79
+ if (match) return path.join(dir, match);
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Run assert_story_files.mjs for the given sprint file.
86
+ * Returns { exitCode, stderr }.
87
+ */
88
+ function runAssertStoryFiles(repoRoot, sprintFilePath) {
89
+ // Use __dirname to locate the sibling script (not CLEARGATE_REPO_ROOT, which is a test-isolation
90
+ // override that points to a tmpdir without scripts).
91
+ const assertScript = path.join(__dirname, 'assert_story_files.mjs');
92
+ const env = { ...process.env, CLEARGATE_REPO_ROOT: repoRoot };
93
+ const result = spawnSync(process.execPath, [assertScript, sprintFilePath], {
94
+ encoding: 'utf8',
95
+ env,
96
+ });
97
+ return {
98
+ exitCode: result.status ?? 1,
99
+ stderr: result.stderr || '',
100
+ stdout: result.stdout || '',
101
+ };
102
+ }
103
+
104
+ function main() {
105
+ const args = process.argv.slice(2);
106
+
107
+ const sprintId = args[0];
108
+ if (!sprintId || sprintId.startsWith('--')) usage();
109
+
110
+ const storiesIdx = args.indexOf('--stories');
111
+ if (storiesIdx === -1 || !args[storiesIdx + 1]) usage();
112
+
113
+ const storyIds = args[storiesIdx + 1].split(',').map((s) => s.trim()).filter(Boolean);
114
+ if (storyIds.length === 0) {
115
+ process.stderr.write('Error: --stories requires at least one story ID\n');
116
+ process.exit(2);
117
+ }
118
+
119
+ const force = args.includes('--force');
120
+
121
+ const sprintDir = path.join(REPO_ROOT, '.cleargate', 'sprint-runs', sprintId);
122
+ const stateFile = path.join(sprintDir, 'state.json');
123
+
124
+ if (fs.existsSync(stateFile) && !force) {
125
+ process.stderr.write(
126
+ `state.json already exists at ${stateFile}; pass --force to overwrite\n`
127
+ );
128
+ process.exit(1);
129
+ }
130
+
131
+ // --- Read execution_mode from sprint frontmatter ---
132
+ const sprintFilePath = findSprintFile(REPO_ROOT, sprintId);
133
+ const executionMode = sprintFilePath ? readExecutionMode(sprintFilePath) : 'v1';
134
+
135
+ // --- Gate-2 story-file assertion ---
136
+ if (sprintFilePath) {
137
+ const { exitCode, stderr, stdout } = runAssertStoryFiles(REPO_ROOT, sprintFilePath);
138
+ if (exitCode !== 0) {
139
+ if (executionMode === 'v2') {
140
+ // Hard block: do not create state.json
141
+ process.stderr.write(stderr);
142
+ process.stderr.write(
143
+ `ERROR: v2 sprint init blocked — story files missing. Fix the above, then re-run init.\n`
144
+ );
145
+ process.exit(1);
146
+ } else {
147
+ // v1: warn only
148
+ process.stderr.write(
149
+ `WARN: missing story files: ${stderr.trim().split('\n').pop() || 'see above'}\n`
150
+ );
151
+ }
152
+ }
153
+ }
154
+
155
+ const now = new Date().toISOString();
156
+ const stories = {};
157
+ for (const id of storyIds) {
158
+ stories[id] = {
159
+ state: 'Ready to Bounce',
160
+ qa_bounces: 0,
161
+ arch_bounces: 0,
162
+ worktree: null,
163
+ updated_at: now,
164
+ notes: '',
165
+ };
166
+ }
167
+
168
+ const state = {
169
+ schema_version: SCHEMA_VERSION,
170
+ sprint_id: sprintId,
171
+ execution_mode: executionMode,
172
+ sprint_status: 'Active',
173
+ stories,
174
+ last_action: `Sprint ${sprintId} initialised`,
175
+ updated_at: now,
176
+ };
177
+
178
+ fs.mkdirSync(sprintDir, { recursive: true });
179
+
180
+ const tmpFile = `${stateFile}.tmp.${process.pid}`;
181
+ fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2) + '\n', 'utf8');
182
+ fs.renameSync(tmpFile, stateFile);
183
+
184
+ process.stdout.write(`Initialized state.json for sprint ${sprintId} with ${storyIds.length} stories\n`);
185
+ }
186
+
187
+ main();
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ # pre_gate_common.sh — Shared helpers for pre_gate_runner.sh
3
+ # Sourced by pre_gate_runner.sh. Do NOT execute directly.
4
+ # Handles Node+TS stacks only (no Python/Rust/Go/Java/Swift detectors).
5
+ set -euo pipefail
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # read_config_field <field_path> <config_file>
9
+ # Uses node -p to parse JSON and extract a field.
10
+ # field_path: dot-separated path, e.g. "qa.typecheck"
11
+ # ---------------------------------------------------------------------------
12
+ read_config_field() {
13
+ local field_path="$1"
14
+ local config_file="$2"
15
+ node -p "
16
+ const c = JSON.parse(require('fs').readFileSync('${config_file}', 'utf8'));
17
+ const parts = '${field_path}'.split('.');
18
+ let v = c;
19
+ for (const p of parts) { v = v[p]; }
20
+ Array.isArray(v) ? JSON.stringify(v) : (v === undefined ? '' : String(v));
21
+ " 2>/dev/null || echo ""
22
+ }
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # get_modified_files <worktree_path>
26
+ # Lists files modified in the current git index vs HEAD.
27
+ # ---------------------------------------------------------------------------
28
+ get_modified_files() {
29
+ local worktree="$1"
30
+ git -C "$worktree" diff --name-only HEAD 2>/dev/null || true
31
+ git -C "$worktree" diff --cached --name-only 2>/dev/null || true
32
+ }
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # get_staged_diff <worktree_path>
36
+ # Returns unified diff of staged changes.
37
+ # ---------------------------------------------------------------------------
38
+ get_staged_diff() {
39
+ local worktree="$1"
40
+ git -C "$worktree" diff --cached 2>/dev/null || git -C "$worktree" diff HEAD 2>/dev/null || true
41
+ }
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # record_result <report_file> <check_name> <status> [details]
45
+ # Appends one result line to the report.
46
+ # status: PASS | FAIL | WARN | INFO
47
+ # ---------------------------------------------------------------------------
48
+ record_result() {
49
+ local report_file="$1"
50
+ local check_name="$2"
51
+ local status="$3"
52
+ local details="${4:-}"
53
+ echo "[${status}] ${check_name}: ${details}" >> "$report_file"
54
+ }
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # print_summary <report_file>
58
+ # Prints count of PASS/FAIL/WARN lines from the report.
59
+ # ---------------------------------------------------------------------------
60
+ print_summary() {
61
+ local report_file="$1"
62
+ local pass_count fail_count warn_count
63
+ pass_count=$(grep -c '^\[PASS\]' "$report_file" 2>/dev/null || echo 0)
64
+ fail_count=$(grep -c '^\[FAIL\]' "$report_file" 2>/dev/null || echo 0)
65
+ warn_count=$(grep -c '^\[WARN\]' "$report_file" 2>/dev/null || echo 0)
66
+ echo "Summary: ${pass_count} passed, ${fail_count} failed, ${warn_count} warnings"
67
+ }
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # write_report <report_file> <mode> <worktree_path> <branch>
71
+ # Writes the report header.
72
+ # ---------------------------------------------------------------------------
73
+ write_report_header() {
74
+ local report_file="$1"
75
+ local mode="$2"
76
+ local worktree="$3"
77
+ local branch="$4"
78
+ mkdir -p "$(dirname "$report_file")"
79
+ {
80
+ echo "# ClearGate Pre-Gate Scan Report"
81
+ echo "Mode: ${mode}"
82
+ echo "Worktree: ${worktree}"
83
+ echo "Branch: ${branch}"
84
+ echo "Timestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
85
+ echo "---"
86
+ } > "$report_file"
87
+ }
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # detect_stack <worktree_path>
91
+ # Returns "node-ts" if package.json + tsconfig.json found, else "unknown".
92
+ # Node+TS only — no Python/Rust/Go/Java/Swift detectors.
93
+ # ---------------------------------------------------------------------------
94
+ detect_stack() {
95
+ local worktree="$1"
96
+ if [[ -f "${worktree}/package.json" ]]; then
97
+ if [[ -f "${worktree}/tsconfig.json" ]]; then
98
+ echo "node-ts"
99
+ else
100
+ echo "node"
101
+ fi
102
+ else
103
+ echo "unknown"
104
+ fi
105
+ }
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # diff_package_json <worktree_path> <branch>
109
+ # Prints new runtime deps (non-dev) introduced vs <branch>^.
110
+ # Returns lines like: "new runtime dep: <name>"
111
+ # ---------------------------------------------------------------------------
112
+ diff_package_json() {
113
+ local worktree="$1"
114
+ local branch="$2"
115
+
116
+ # Get old package.json from branch parent (branch^ in the worktree's git)
117
+ local old_json
118
+ old_json=$(git -C "$worktree" show "${branch}^:package.json" 2>/dev/null || echo '{"dependencies":{}}')
119
+
120
+ local new_json
121
+ new_json=$(cat "${worktree}/package.json" 2>/dev/null || echo '{"dependencies":{}}')
122
+
123
+ # Extract dep keys using node
124
+ node -e "
125
+ const oldPkg = JSON.parse($(echo "$old_json" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d)))"));
126
+ const newPkg = JSON.parse($(echo "$new_json" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>process.stdout.write(JSON.stringify(d)))"));
127
+ const oldDeps = Object.keys(oldPkg.dependencies || {});
128
+ const newDeps = Object.keys(newPkg.dependencies || {});
129
+ const added = newDeps.filter(d => !oldDeps.includes(d));
130
+ added.forEach(d => console.log('new runtime dep: ' + d));
131
+ " 2>/dev/null || true
132
+ }
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env bash
2
+ # pre_gate_runner.sh — Pre-gate scanner for QA and Architect agent spawning.
3
+ # Usage: pre_gate_runner.sh qa|arch <worktree-path> <branch>
4
+ #
5
+ # Exit codes:
6
+ # 0 — all checks pass → orchestrator proceeds to spawn QA/Architect
7
+ # 1 — checks failed → orchestrator returns story to Developer
8
+ # 2 — scan couldn't run (missing config, missing worktree, bad args)
9
+ set -euo pipefail
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Source shared helpers
15
+ # ---------------------------------------------------------------------------
16
+ # shellcheck source=pre_gate_common.sh
17
+ source "${SCRIPT_DIR}/pre_gate_common.sh"
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Argument validation
21
+ # ---------------------------------------------------------------------------
22
+ if [[ $# -lt 3 ]]; then
23
+ echo "Usage: pre_gate_runner.sh qa|arch <worktree-path> <branch>" >&2
24
+ exit 2
25
+ fi
26
+
27
+ MODE="$1"
28
+ WORKTREE="$2"
29
+ BRANCH="$3"
30
+
31
+ if [[ "$MODE" != "qa" && "$MODE" != "arch" ]]; then
32
+ echo "pre_gate_runner.sh: unknown mode '${MODE}' — must be 'qa' or 'arch'" >&2
33
+ exit 2
34
+ fi
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Validate worktree path
38
+ # ---------------------------------------------------------------------------
39
+ if [[ ! -d "$WORKTREE" ]]; then
40
+ echo "pre_gate_runner.sh: worktree path does not exist: ${WORKTREE}" >&2
41
+ exit 2
42
+ fi
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Locate gate-checks.json — auto-seed if missing
46
+ # ---------------------------------------------------------------------------
47
+ CONFIG_FILE="${SCRIPT_DIR}/gate-checks.json"
48
+ if [[ ! -f "$CONFIG_FILE" ]]; then
49
+ echo "gate-checks.json not found at ${CONFIG_FILE}; running init_gate_config.sh …" >&2
50
+ bash "${SCRIPT_DIR}/init_gate_config.sh" || {
51
+ echo "pre_gate_runner.sh: init_gate_config.sh failed — cannot proceed" >&2
52
+ exit 2
53
+ }
54
+ fi
55
+
56
+ if [[ ! -f "$CONFIG_FILE" ]]; then
57
+ echo "pre_gate_runner.sh: gate-checks.json still missing after init — exit 2" >&2
58
+ exit 2
59
+ fi
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Prepare report file
63
+ # ---------------------------------------------------------------------------
64
+ REPORT_DIR="${WORKTREE}/.cleargate/reports"
65
+ REPORT_FILE="${REPORT_DIR}/pre-${MODE}-scan.txt"
66
+ write_report_header "$REPORT_FILE" "$MODE" "$WORKTREE" "$BRANCH"
67
+
68
+ OVERALL_EXIT=0
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # ── QA MODE ──────────────────────────────────────────────────────────────
72
+ # ---------------------------------------------------------------------------
73
+ run_qa() {
74
+ # 1. Typecheck
75
+ local typecheck_cmd
76
+ typecheck_cmd="$(read_config_field "qa.typecheck" "$CONFIG_FILE")"
77
+ if [[ -n "$typecheck_cmd" && -f "${WORKTREE}/package.json" ]]; then
78
+ local tc_out tc_exit
79
+ tc_exit=0
80
+ tc_out=$(cd "$WORKTREE" && eval "$typecheck_cmd" 2>&1) || tc_exit=$?
81
+ if [[ $tc_exit -eq 0 ]]; then
82
+ record_result "$REPORT_FILE" "typecheck" "PASS" "$typecheck_cmd"
83
+ else
84
+ record_result "$REPORT_FILE" "typecheck" "FAIL" "$(echo "$tc_out" | head -5)"
85
+ OVERALL_EXIT=1
86
+ fi
87
+ else
88
+ record_result "$REPORT_FILE" "typecheck" "INFO" "skipped (no package.json or cmd empty)"
89
+ fi
90
+
91
+ # 2. Debug pattern grep (staged diff)
92
+ local debug_patterns_json
93
+ debug_patterns_json="$(read_config_field "qa.debug_patterns" "$CONFIG_FILE")"
94
+ local debug_patterns=()
95
+ while IFS= read -r _line; do
96
+ [[ -z "$_line" ]] || debug_patterns+=("$_line")
97
+ done < <(node -e "
98
+ try { JSON.parse('${debug_patterns_json}').forEach(p => console.log(p)); } catch(e) {}
99
+ " 2>/dev/null)
100
+
101
+ local staged_diff
102
+ staged_diff="$(get_staged_diff "$WORKTREE")"
103
+
104
+ if [[ ${#debug_patterns[@]} -gt 0 && -n "$staged_diff" ]]; then
105
+ local pattern_found=0
106
+ local found_details=""
107
+ for pattern in "${debug_patterns[@]}"; do
108
+ local matches
109
+ # Search all tracked files (not just diff) for the pattern since in test
110
+ # scenarios the file is committed but we want to detect it
111
+ matches="$(git -C "$WORKTREE" grep -n "$pattern" -- 2>/dev/null || true)"
112
+ if [[ -n "$matches" ]]; then
113
+ found_details+="$(echo "$matches" | head -5)"$'\n'
114
+ pattern_found=1
115
+ fi
116
+ done
117
+ if [[ $pattern_found -eq 1 ]]; then
118
+ record_result "$REPORT_FILE" "debug_patterns" "FAIL" "$(echo "$found_details" | head -10 | tr '\n' '|')"
119
+ echo "$found_details" >> "$REPORT_FILE"
120
+ OVERALL_EXIT=1
121
+ else
122
+ record_result "$REPORT_FILE" "debug_patterns" "PASS" "no debug statements found"
123
+ fi
124
+ else
125
+ # Fall back to grepping all files in worktree for debug patterns
126
+ local pattern_found=0
127
+ local found_details=""
128
+ for pattern in "${debug_patterns[@]:-}"; do
129
+ [[ -z "$pattern" ]] && continue
130
+ local matches
131
+ matches="$(grep -rn "$pattern" "${WORKTREE}" \
132
+ --include="*.js" --include="*.ts" --include="*.mjs" --include="*.cjs" \
133
+ --exclude-dir=".git" --exclude-dir="node_modules" \
134
+ 2>/dev/null || true)"
135
+ if [[ -n "$matches" ]]; then
136
+ found_details+="$(echo "$matches" | head -5)"$'\n'
137
+ pattern_found=1
138
+ fi
139
+ done
140
+ if [[ $pattern_found -eq 1 ]]; then
141
+ record_result "$REPORT_FILE" "debug_patterns" "FAIL" "$(echo "$found_details" | head -10 | tr '\n' '|')"
142
+ echo "$found_details" >> "$REPORT_FILE"
143
+ OVERALL_EXIT=1
144
+ else
145
+ record_result "$REPORT_FILE" "debug_patterns" "PASS" "no debug statements found"
146
+ fi
147
+ fi
148
+
149
+ # 3. TODO/FIXME grep
150
+ local todo_patterns_json
151
+ todo_patterns_json="$(read_config_field "qa.todo_patterns" "$CONFIG_FILE")"
152
+ local todo_patterns=()
153
+ while IFS= read -r _line; do
154
+ [[ -z "$_line" ]] || todo_patterns+=("$_line")
155
+ done < <(node -e "
156
+ try { JSON.parse('${todo_patterns_json}').forEach(p => console.log(p)); } catch(e) {}
157
+ " 2>/dev/null)
158
+
159
+ local todo_found=0
160
+ local todo_details=""
161
+ for pattern in "${todo_patterns[@]:-}"; do
162
+ [[ -z "$pattern" ]] && continue
163
+ local matches
164
+ matches="$(grep -rn "$pattern" "${WORKTREE}" \
165
+ --include="*.js" --include="*.ts" --include="*.mjs" \
166
+ --exclude-dir=".git" --exclude-dir="node_modules" \
167
+ 2>/dev/null || true)"
168
+ if [[ -n "$matches" ]]; then
169
+ todo_details+="$(echo "$matches" | head -3)"$'\n'
170
+ todo_found=1
171
+ fi
172
+ done
173
+ if [[ $todo_found -eq 1 ]]; then
174
+ record_result "$REPORT_FILE" "todo_patterns" "WARN" "$(echo "$todo_details" | head -5 | tr '\n' '|')"
175
+ else
176
+ record_result "$REPORT_FILE" "todo_patterns" "PASS" "no TODO/FIXME/XXX found"
177
+ fi
178
+
179
+ # 4. npm test
180
+ local test_cmd
181
+ test_cmd="$(read_config_field "qa.test" "$CONFIG_FILE")"
182
+ if [[ -n "$test_cmd" && -f "${WORKTREE}/package.json" ]]; then
183
+ local test_exit=0
184
+ cd "$WORKTREE" && eval "$test_cmd" > /dev/null 2>&1 || test_exit=$?
185
+ if [[ $test_exit -eq 0 ]]; then
186
+ record_result "$REPORT_FILE" "test" "PASS" "$test_cmd"
187
+ else
188
+ record_result "$REPORT_FILE" "test" "FAIL" "exit code ${test_exit}"
189
+ OVERALL_EXIT=1
190
+ fi
191
+ else
192
+ record_result "$REPORT_FILE" "test" "INFO" "skipped (no package.json or cmd empty)"
193
+ fi
194
+ }
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # ── ARCH MODE ─────────────────────────────────────────────────────────────
198
+ # ---------------------------------------------------------------------------
199
+ run_arch() {
200
+ # 1. Typecheck
201
+ local typecheck_cmd
202
+ typecheck_cmd="$(read_config_field "arch.typecheck" "$CONFIG_FILE")"
203
+ if [[ -n "$typecheck_cmd" && -f "${WORKTREE}/package.json" ]]; then
204
+ local tc_exit=0
205
+ cd "$WORKTREE" && eval "$typecheck_cmd" > /dev/null 2>&1 || tc_exit=$?
206
+ if [[ $tc_exit -eq 0 ]]; then
207
+ record_result "$REPORT_FILE" "typecheck" "PASS" "$typecheck_cmd"
208
+ else
209
+ record_result "$REPORT_FILE" "typecheck" "FAIL" "exit code ${tc_exit}"
210
+ OVERALL_EXIT=1
211
+ fi
212
+ else
213
+ record_result "$REPORT_FILE" "typecheck" "INFO" "skipped (no package.json or cmd empty)"
214
+ fi
215
+
216
+ # 2. New runtime deps vs branch^
217
+ local new_deps_check
218
+ new_deps_check="$(read_config_field "arch.new_deps_check" "$CONFIG_FILE")"
219
+ if [[ "$new_deps_check" == "true" && -f "${WORKTREE}/package.json" ]]; then
220
+ # Get old package.json from branch^
221
+ local old_json
222
+ old_json="$(git -C "$WORKTREE" show "${BRANCH}^:package.json" 2>/dev/null || echo '{}')"
223
+ local new_json
224
+ new_json="$(cat "${WORKTREE}/package.json")"
225
+
226
+ local new_deps
227
+ new_deps="$(node -e "
228
+ let oldPkg, newPkg;
229
+ try { oldPkg = JSON.parse(process.argv[1]); } catch(e) { oldPkg = {}; }
230
+ try { newPkg = JSON.parse(process.argv[2]); } catch(e) { newPkg = {}; }
231
+ const oldDeps = Object.keys(oldPkg.dependencies || {});
232
+ const newDeps = Object.keys(newPkg.dependencies || {});
233
+ const added = newDeps.filter(d => !oldDeps.includes(d));
234
+ added.forEach(d => console.log('new runtime dep: ' + d));
235
+ " "$old_json" "$new_json" 2>/dev/null || true)"
236
+
237
+ if [[ -n "$new_deps" ]]; then
238
+ record_result "$REPORT_FILE" "new_deps" "FAIL" "new runtime dependencies detected"
239
+ echo "$new_deps" >> "$REPORT_FILE"
240
+ OVERALL_EXIT=1
241
+ else
242
+ record_result "$REPORT_FILE" "new_deps" "PASS" "no new runtime deps"
243
+ fi
244
+ else
245
+ record_result "$REPORT_FILE" "new_deps" "INFO" "skipped"
246
+ fi
247
+
248
+ # 3. Stray .env* files
249
+ local stray_env_json
250
+ stray_env_json="$(read_config_field "arch.stray_env_files" "$CONFIG_FILE")"
251
+ local stray_patterns=()
252
+ while IFS= read -r _line; do
253
+ [[ -z "$_line" ]] || stray_patterns+=("$_line")
254
+ done < <(node -e "
255
+ try { JSON.parse('${stray_env_json}').forEach(p => console.log(p)); } catch(e) {}
256
+ " 2>/dev/null)
257
+
258
+ local stray_found=0
259
+ local stray_details=""
260
+ for pat in "${stray_patterns[@]:-}"; do
261
+ [[ -z "$pat" ]] && continue
262
+ if [[ -f "${WORKTREE}/${pat}" ]]; then
263
+ stray_details+="${pat}"$'\n'
264
+ stray_found=1
265
+ fi
266
+ done
267
+ if [[ $stray_found -eq 1 ]]; then
268
+ record_result "$REPORT_FILE" "stray_env_files" "FAIL" "$(echo "$stray_details" | tr '\n' ' ')"
269
+ OVERALL_EXIT=1
270
+ else
271
+ record_result "$REPORT_FILE" "stray_env_files" "PASS" "no stray .env files"
272
+ fi
273
+
274
+ # 4. File count per directory
275
+ local file_count_report
276
+ file_count_report="$(read_config_field "arch.file_count_report" "$CONFIG_FILE")"
277
+ if [[ "$file_count_report" == "true" ]]; then
278
+ record_result "$REPORT_FILE" "file_count_report" "INFO" "directory file counts:"
279
+ find "$WORKTREE" -maxdepth 2 -type d \
280
+ ! -path "*/.git*" ! -path "*/node_modules*" \
281
+ 2>/dev/null | while read -r dir; do
282
+ local count
283
+ count=$(find "$dir" -maxdepth 1 -type f 2>/dev/null | wc -l | tr -d ' ')
284
+ echo " ${dir}: ${count} files" >> "$REPORT_FILE"
285
+ done
286
+ fi
287
+ }
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # Dispatch
291
+ # ---------------------------------------------------------------------------
292
+ case "$MODE" in
293
+ qa) run_qa ;;
294
+ arch) run_arch ;;
295
+ esac
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # Footer
299
+ # ---------------------------------------------------------------------------
300
+ {
301
+ echo "---"
302
+ print_summary "$REPORT_FILE"
303
+ } >> "$REPORT_FILE"
304
+
305
+ cat "$REPORT_FILE" >&2
306
+
307
+ exit $OVERALL_EXIT