cleargate 0.2.1 → 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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/dist/MANIFEST.json +58 -16
  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 +7980 -3956
  14. package/dist/cli.cjs.map +1 -1
  15. package/dist/cli.js +3980 -466
  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 +71 -89
  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/.cleargate/config.example.yml +37 -0
  29. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +407 -0
  30. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +146 -0
  31. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +250 -0
  32. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +57 -0
  33. package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +320 -0
  34. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +15 -0
  35. package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +38 -0
  36. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +187 -0
  37. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +132 -0
  38. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +307 -0
  39. package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +280 -0
  40. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +123 -0
  41. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +110 -0
  42. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +247 -0
  43. package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +27 -0
  44. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +261 -0
  45. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +210 -0
  46. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +190 -0
  47. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +327 -0
  48. package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +261 -0
  49. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +154 -0
  50. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +111 -0
  51. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +164 -0
  52. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +9 -0
  53. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +9 -0
  54. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +29 -3
  55. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +9 -0
  56. package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +9 -0
  57. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +42 -0
  58. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +175 -0
  59. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +19 -0
  60. package/dist/templates/cleargate-planning/CLAUDE.md +2 -0
  61. package/dist/templates/cleargate-planning/MANIFEST.json +58 -16
  62. package/dist/whoami-CX7CXJD5.js +76 -0
  63. package/dist/whoami-CX7CXJD5.js.map +1 -0
  64. package/package.json +6 -2
  65. package/templates/cleargate-planning/.claude/agents/architect.md +72 -0
  66. package/templates/cleargate-planning/.claude/agents/developer.md +45 -3
  67. package/templates/cleargate-planning/.claude/agents/qa.md +7 -3
  68. package/templates/cleargate-planning/.claude/agents/reporter.md +71 -89
  69. package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +204 -0
  70. package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +10 -0
  71. package/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +58 -0
  72. package/templates/cleargate-planning/.claude/hooks/pre-commit.sh +19 -0
  73. package/templates/cleargate-planning/.claude/hooks/session-start.sh +51 -0
  74. package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +1 -1
  75. package/templates/cleargate-planning/.claude/settings.json +11 -0
  76. package/templates/cleargate-planning/.cleargate/config.example.yml +37 -0
  77. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +407 -0
  78. package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +146 -0
  79. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +250 -0
  80. package/templates/cleargate-planning/.cleargate/scripts/constants.mjs +57 -0
  81. package/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +320 -0
  82. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +15 -0
  83. package/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +38 -0
  84. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +187 -0
  85. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +132 -0
  86. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +307 -0
  87. package/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +280 -0
  88. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +123 -0
  89. package/templates/cleargate-planning/.cleargate/scripts/state.schema.json +110 -0
  90. package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +247 -0
  91. package/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +27 -0
  92. package/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +261 -0
  93. package/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +210 -0
  94. package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +190 -0
  95. package/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +327 -0
  96. package/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +261 -0
  97. package/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +154 -0
  98. package/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +111 -0
  99. package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +164 -0
  100. package/templates/cleargate-planning/.cleargate/templates/Bug.md +9 -0
  101. package/templates/cleargate-planning/.cleargate/templates/CR.md +9 -0
  102. package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +29 -3
  103. package/templates/cleargate-planning/.cleargate/templates/epic.md +9 -0
  104. package/templates/cleargate-planning/.cleargate/templates/proposal.md +9 -0
  105. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +42 -0
  106. package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +175 -0
  107. package/templates/cleargate-planning/.cleargate/templates/story.md +19 -0
  108. package/templates/cleargate-planning/CLAUDE.md +2 -0
  109. 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();