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.
- package/LICENSE +21 -0
- package/dist/MANIFEST.json +59 -17
- package/dist/admin-api/index.cjs +88 -1
- package/dist/admin-api/index.cjs.map +1 -1
- package/dist/admin-api/index.d.cts +105 -1
- package/dist/admin-api/index.d.ts +105 -1
- package/dist/admin-api/index.js +77 -1
- package/dist/admin-api/index.js.map +1 -1
- package/dist/bootstrap-root-FGWDICDT.js +130 -0
- package/dist/bootstrap-root-FGWDICDT.js.map +1 -0
- package/dist/chunk-OM4FAEA7.js +184 -0
- package/dist/chunk-OM4FAEA7.js.map +1 -0
- package/dist/cli.cjs +7995 -3984
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4062 -561
- package/dist/cli.js.map +1 -1
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +72 -0
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +45 -3
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +7 -3
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +72 -75
- package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +204 -0
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +10 -0
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +58 -0
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +19 -0
- package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +51 -0
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +1 -1
- package/dist/templates/cleargate-planning/.claude/settings.json +11 -0
- package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +31 -12
- package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +2 -1
- package/dist/templates/cleargate-planning/.cleargate/config.example.yml +37 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +407 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +146 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +250 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +57 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +320 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +15 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +38 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +187 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +132 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +307 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +280 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +123 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +110 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +247 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +27 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +261 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +210 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +190 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +327 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +261 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +154 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +111 -0
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +164 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +29 -3
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +9 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +42 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +175 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +29 -0
- package/dist/templates/cleargate-planning/CLAUDE.md +3 -0
- package/dist/templates/cleargate-planning/MANIFEST.json +59 -17
- package/dist/whoami-CX7CXJD5.js +76 -0
- package/dist/whoami-CX7CXJD5.js.map +1 -0
- package/package.json +6 -2
- package/templates/cleargate-planning/.claude/agents/architect.md +72 -0
- package/templates/cleargate-planning/.claude/agents/developer.md +45 -3
- package/templates/cleargate-planning/.claude/agents/qa.md +7 -3
- package/templates/cleargate-planning/.claude/agents/reporter.md +72 -75
- package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +204 -0
- package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +10 -0
- package/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +58 -0
- package/templates/cleargate-planning/.claude/hooks/pre-commit.sh +19 -0
- package/templates/cleargate-planning/.claude/hooks/session-start.sh +51 -0
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +1 -1
- package/templates/cleargate-planning/.claude/settings.json +11 -0
- package/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +31 -12
- package/templates/cleargate-planning/.cleargate/FLASHCARD.md +2 -1
- package/templates/cleargate-planning/.cleargate/config.example.yml +37 -0
- package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +407 -0
- package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +146 -0
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +250 -0
- package/templates/cleargate-planning/.cleargate/scripts/constants.mjs +57 -0
- package/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +320 -0
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +15 -0
- package/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +38 -0
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +187 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +132 -0
- package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +307 -0
- package/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +280 -0
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +123 -0
- package/templates/cleargate-planning/.cleargate/scripts/state.schema.json +110 -0
- package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +247 -0
- package/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +27 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +261 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +210 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +190 -0
- package/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +327 -0
- package/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +261 -0
- package/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +154 -0
- package/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +111 -0
- package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +164 -0
- package/templates/cleargate-planning/.cleargate/templates/Bug.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/CR.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +29 -3
- package/templates/cleargate-planning/.cleargate/templates/epic.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/proposal.md +9 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +42 -0
- package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +175 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +29 -0
- package/templates/cleargate-planning/CLAUDE.md +3 -0
- 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
|