cleargate 0.2.1 → 0.4.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 +58 -16
- 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 +8530 -3957
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +5282 -1220
- 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 +71 -89
- 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/.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 +19 -0
- package/dist/templates/cleargate-planning/CLAUDE.md +2 -0
- package/dist/templates/cleargate-planning/MANIFEST.json +58 -16
- 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 +71 -89
- 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/.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 +19 -0
- package/templates/cleargate-planning/CLAUDE.md +2 -0
- package/templates/cleargate-planning/MANIFEST.json +58 -16
|
@@ -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
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* prefill_report.mjs — Backfill missing YAML frontmatter fields in agent reports
|
|
4
|
+
*
|
|
5
|
+
* Usage: node prefill_report.mjs <sprint-id>
|
|
6
|
+
* or: CLEARGATE_STATE_FILE=... node prefill_report.mjs <sprint-id>
|
|
7
|
+
*
|
|
8
|
+
* Reads:
|
|
9
|
+
* - state.json (v1 schema) for story metadata (bounce counts, states)
|
|
10
|
+
* - token-ledger.jsonl for commit_sha attribution
|
|
11
|
+
* - All STORY-<id>-dev.md and STORY-<id>-qa.md in sprint-runs/<id>/reports/
|
|
12
|
+
*
|
|
13
|
+
* Backfills missing deterministic YAML frontmatter fields:
|
|
14
|
+
* - story_id, sprint_id, commit_sha, qa_bounces, arch_bounces
|
|
15
|
+
*
|
|
16
|
+
* Atomic write (tmp+rename per M1 pattern). Idempotent: re-run on a
|
|
17
|
+
* fully-prefilled report is a no-op.
|
|
18
|
+
*
|
|
19
|
+
* SubagentStop hook attribution note (FLASHCARD 2026-04-19 #reporting #hooks #ledger):
|
|
20
|
+
* Ledger rows may lack story_id; those are attributed to "unassigned" bucket
|
|
21
|
+
* and do not prevent backfill of reports that can be matched by filename.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { validateState } from './validate_state.mjs';
|
|
28
|
+
|
|
29
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse a minimal YAML frontmatter block from markdown content.
|
|
34
|
+
* Returns { frontmatter: string, body: string, fields: object }
|
|
35
|
+
* Only parses simple key: value pairs (no nested objects).
|
|
36
|
+
* @param {string} content
|
|
37
|
+
* @returns {{ hasFrontmatter: boolean, frontmatter: string, body: string, fields: object }}
|
|
38
|
+
*/
|
|
39
|
+
function parseFrontmatter(content) {
|
|
40
|
+
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
41
|
+
if (!fmMatch) {
|
|
42
|
+
return { hasFrontmatter: false, frontmatter: '', body: content, fields: {} };
|
|
43
|
+
}
|
|
44
|
+
const rawFm = fmMatch[1];
|
|
45
|
+
const body = fmMatch[2];
|
|
46
|
+
const fields = {};
|
|
47
|
+
for (const line of rawFm.split('\n')) {
|
|
48
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)$/);
|
|
49
|
+
if (m) {
|
|
50
|
+
const key = m[1];
|
|
51
|
+
let val = m[2].trim();
|
|
52
|
+
// Strip surrounding quotes
|
|
53
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
54
|
+
val = val.slice(1, -1);
|
|
55
|
+
}
|
|
56
|
+
// Parse nulls
|
|
57
|
+
if (val === 'null' || val === '') val = null;
|
|
58
|
+
fields[key] = val;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { hasFrontmatter: true, frontmatter: rawFm, body, fields };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Serialize fields back to YAML frontmatter lines (simple key: value).
|
|
66
|
+
* @param {object} fields
|
|
67
|
+
* @param {string} originalFrontmatter - preserve original order and formatting
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function serializeFrontmatter(fields, originalFrontmatter) {
|
|
71
|
+
const lines = originalFrontmatter.split('\n');
|
|
72
|
+
const updatedLines = [];
|
|
73
|
+
const processedKeys = new Set();
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*):/);
|
|
77
|
+
if (m) {
|
|
78
|
+
const key = m[1];
|
|
79
|
+
processedKeys.add(key);
|
|
80
|
+
if (key in fields && fields[key] !== null && fields[key] !== undefined) {
|
|
81
|
+
const val = fields[key];
|
|
82
|
+
updatedLines.push(`${key}: "${val}"`);
|
|
83
|
+
} else {
|
|
84
|
+
updatedLines.push(line);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
updatedLines.push(line);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Append any new fields not in the original
|
|
92
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
93
|
+
if (!processedKeys.has(key) && val !== null && val !== undefined) {
|
|
94
|
+
updatedLines.push(`${key}: "${val}"`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return updatedLines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Atomic write to a file using tmp+rename pattern.
|
|
103
|
+
* @param {string} filePath
|
|
104
|
+
* @param {string} content
|
|
105
|
+
*/
|
|
106
|
+
function atomicWrite(filePath, content) {
|
|
107
|
+
const tmpFile = `${filePath}.tmp.${process.pid}`;
|
|
108
|
+
fs.writeFileSync(tmpFile, content, 'utf8');
|
|
109
|
+
fs.renameSync(tmpFile, filePath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parse JSONL file, returning an array of parsed objects.
|
|
114
|
+
* Tolerates malformed lines (skips them with a warning).
|
|
115
|
+
* @param {string} filePath
|
|
116
|
+
* @returns {object[]}
|
|
117
|
+
*/
|
|
118
|
+
function parseJsonl(filePath) {
|
|
119
|
+
if (!fs.existsSync(filePath)) return [];
|
|
120
|
+
const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(l => l.trim());
|
|
121
|
+
const results = [];
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
try {
|
|
124
|
+
results.push(JSON.parse(line));
|
|
125
|
+
} catch {
|
|
126
|
+
process.stderr.write(`Warning: skipping malformed JSONL line: ${line.slice(0, 80)}\n`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function main() {
|
|
133
|
+
const args = process.argv.slice(2);
|
|
134
|
+
if (args.length < 1) {
|
|
135
|
+
process.stderr.write('Usage: node prefill_report.mjs <sprint-id>\n');
|
|
136
|
+
process.exit(2);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sprintId = args[0];
|
|
140
|
+
const sprintRunsDir = path.join(REPO_ROOT, '.cleargate', 'sprint-runs');
|
|
141
|
+
const sprintDir = process.env.CLEARGATE_SPRINT_DIR
|
|
142
|
+
? path.resolve(process.env.CLEARGATE_SPRINT_DIR)
|
|
143
|
+
: path.join(sprintRunsDir, sprintId);
|
|
144
|
+
|
|
145
|
+
if (!fs.existsSync(sprintDir)) {
|
|
146
|
+
process.stderr.write(`Error: sprint directory not found: ${sprintDir}\n`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Load state.json
|
|
151
|
+
const stateFile = process.env.CLEARGATE_STATE_FILE
|
|
152
|
+
? path.resolve(process.env.CLEARGATE_STATE_FILE)
|
|
153
|
+
: path.join(sprintDir, 'state.json');
|
|
154
|
+
|
|
155
|
+
if (!fs.existsSync(stateFile)) {
|
|
156
|
+
process.stderr.write(`Error: state.json not found at ${stateFile}\n`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let state;
|
|
161
|
+
try {
|
|
162
|
+
state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
163
|
+
} catch (err) {
|
|
164
|
+
process.stderr.write(`Error: failed to parse state.json: ${err.message}\n`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { valid, errors } = validateState(state);
|
|
169
|
+
if (!valid) {
|
|
170
|
+
process.stderr.write('Error: state.json is invalid:\n');
|
|
171
|
+
for (const e of errors) process.stderr.write(` - ${e}\n`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Load token-ledger.jsonl — build a map of story_id -> commit_sha
|
|
176
|
+
// Rows lacking story_id are attributed to 'unassigned' (per FLASHCARD 2026-04-19 #reporting #hooks #ledger)
|
|
177
|
+
const ledgerFile = path.join(sprintDir, 'token-ledger.jsonl');
|
|
178
|
+
const ledgerRows = parseJsonl(ledgerFile);
|
|
179
|
+
const storyCommits = {};
|
|
180
|
+
for (const row of ledgerRows) {
|
|
181
|
+
const sid = row.story_id || 'unassigned';
|
|
182
|
+
if (sid !== 'unassigned' && row.commit_sha) {
|
|
183
|
+
storyCommits[sid] = row.commit_sha;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Find all agent reports in the sprint dir (reports/ subdirectory + top level)
|
|
188
|
+
const reportsDir = path.join(sprintDir, 'reports');
|
|
189
|
+
const reportFiles = [];
|
|
190
|
+
|
|
191
|
+
// Check reports subdirectory
|
|
192
|
+
if (fs.existsSync(reportsDir)) {
|
|
193
|
+
for (const f of fs.readdirSync(reportsDir)) {
|
|
194
|
+
if (/^STORY-.*-(dev|qa)\.md$/.test(f)) {
|
|
195
|
+
reportFiles.push(path.join(reportsDir, f));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check sprint dir directly
|
|
201
|
+
for (const f of fs.readdirSync(sprintDir)) {
|
|
202
|
+
if (/^STORY-.*-(dev|qa)\.md$/.test(f)) {
|
|
203
|
+
reportFiles.push(path.join(sprintDir, f));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (reportFiles.length === 0) {
|
|
208
|
+
process.stdout.write(`No agent reports found in ${sprintDir}; nothing to prefill.\n`);
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let prefilled = 0;
|
|
213
|
+
let noops = 0;
|
|
214
|
+
|
|
215
|
+
for (const reportPath of reportFiles) {
|
|
216
|
+
const filename = path.basename(reportPath);
|
|
217
|
+
// Extract story_id from filename: STORY-NNN-NN-dev.md or STORY-NNN-NN-qa.md
|
|
218
|
+
const storyMatch = filename.match(/^(STORY-\d+-\d+)-(dev|qa)\.md$/);
|
|
219
|
+
if (!storyMatch) continue;
|
|
220
|
+
|
|
221
|
+
const storyId = storyMatch[1];
|
|
222
|
+
const content = fs.readFileSync(reportPath, 'utf8');
|
|
223
|
+
const { hasFrontmatter, frontmatter, body, fields } = parseFrontmatter(content);
|
|
224
|
+
|
|
225
|
+
if (!hasFrontmatter) {
|
|
226
|
+
process.stdout.write(`Skipping ${filename}: no frontmatter found.\n`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Determine what needs backfill
|
|
231
|
+
const updates = {};
|
|
232
|
+
let needsUpdate = false;
|
|
233
|
+
|
|
234
|
+
if (!fields.story_id) {
|
|
235
|
+
updates.story_id = storyId;
|
|
236
|
+
needsUpdate = true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!fields.sprint_id) {
|
|
240
|
+
updates.sprint_id = state.sprint_id || sprintId;
|
|
241
|
+
needsUpdate = true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!fields.commit_sha && storyCommits[storyId]) {
|
|
245
|
+
updates.commit_sha = storyCommits[storyId];
|
|
246
|
+
needsUpdate = true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const storyEntry = state.stories && state.stories[storyId];
|
|
250
|
+
if (storyEntry) {
|
|
251
|
+
if (fields.qa_bounces === null || fields.qa_bounces === undefined) {
|
|
252
|
+
updates.qa_bounces = String(storyEntry.qa_bounces || 0);
|
|
253
|
+
needsUpdate = true;
|
|
254
|
+
}
|
|
255
|
+
if (fields.arch_bounces === null || fields.arch_bounces === undefined) {
|
|
256
|
+
updates.arch_bounces = String(storyEntry.arch_bounces || 0);
|
|
257
|
+
needsUpdate = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!needsUpdate) {
|
|
262
|
+
noops++;
|
|
263
|
+
process.stdout.write(`No-op: ${filename} already fully prefilled.\n`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Merge updates into existing fields
|
|
268
|
+
const mergedFields = Object.assign({}, fields, updates);
|
|
269
|
+
const newFrontmatter = serializeFrontmatter(mergedFields, frontmatter);
|
|
270
|
+
const newContent = `---\n${newFrontmatter}\n---\n${body}`;
|
|
271
|
+
|
|
272
|
+
atomicWrite(reportPath, newContent);
|
|
273
|
+
prefilled++;
|
|
274
|
+
process.stdout.write(`Prefilled ${filename}: ${Object.keys(updates).join(', ')}\n`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
process.stdout.write(`\nDone. prefilled=${prefilled} noops=${noops}\n`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
main();
|