cleargate 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/MANIFEST.json +72 -16
  3. package/dist/admin-api/index.cjs +0 -1
  4. package/dist/admin-api/index.js +1 -2
  5. package/dist/auth/factory.cjs +0 -1
  6. package/dist/auth/factory.js +2 -3
  7. package/dist/auth/require-token.cjs +0 -1
  8. package/dist/auth/require-token.js +1 -2
  9. package/dist/auth/token-store.cjs +0 -1
  10. package/dist/auth/token-store.js +1 -2
  11. package/dist/{bootstrap-root-QKSA5V75.js → bootstrap-root-2H5HVTCC.js} +1 -2
  12. package/dist/{chunk-PDE37WFQ.js → chunk-A7MSQUU7.js} +2 -3
  13. package/dist/{chunk-BTSZOEWC.js → chunk-P6KEDAK2.js} +0 -1
  14. package/dist/{chunk-E3X7IE5E.js → chunk-PY6FHGV5.js} +1 -2
  15. package/dist/{chunk-5DI2Z3C2.js → chunk-Y53ZZYYU.js} +1 -2
  16. package/dist/cli.cjs +1564 -1414
  17. package/dist/cli.js +1514 -1364
  18. package/dist/lib/ledger.cjs +0 -1
  19. package/dist/lib/ledger.js +1 -2
  20. package/dist/lib/lifecycle-reconcile.cjs +0 -1
  21. package/dist/lib/lifecycle-reconcile.js +2 -3
  22. package/dist/{whoami-EANGN46Z.js → whoami-JKQQPABQ.js} +3 -4
  23. package/package.json +4 -3
  24. package/templates/cleargate-planning/.claude/agents/architect-synth.md +2 -0
  25. package/templates/cleargate-planning/.claude/agents/architect.md +4 -2
  26. package/templates/cleargate-planning/.claude/agents/developer.md +4 -11
  27. package/templates/cleargate-planning/.claude/agents/qa.md +14 -6
  28. package/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +2 -2
  29. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +19 -1
  30. package/templates/cleargate-planning/.cleargate/config.example.yml +16 -0
  31. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.deferred-verify.red.node.test.ts +245 -0
  32. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +227 -0
  33. package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +5 -4
  34. package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +75 -2
  35. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +48 -0
  36. package/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +57 -1
  37. package/templates/cleargate-planning/.cleargate/scripts/provision_worktree_config.sh +155 -0
  38. package/templates/cleargate-planning/.cleargate/scripts/qa_red_lint.mjs +380 -0
  39. package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +34 -1
  40. package/templates/cleargate-planning/.cleargate/scripts/test/cr077_eviction.red.sh +113 -0
  41. package/templates/cleargate-planning/.cleargate/scripts/test/cr078_init.test.sh +309 -0
  42. package/templates/cleargate-planning/.cleargate/scripts/test/cr079_provision.red.sh +262 -0
  43. package/templates/cleargate-planning/.cleargate/scripts/test/cr080_wrapper.test.sh +177 -0
  44. package/templates/cleargate-planning/.cleargate/scripts/test/cr081_qa_red_lint.red.sh +348 -0
  45. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/.session-totals.json +1 -0
  46. package/templates/cleargate-planning/.cleargate/sprint-runs/_off-sprint/token-ledger.jsonl +222 -0
  47. package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +17 -0
  48. package/templates/cleargate-planning/.cleargate/templates/story.md +1 -0
  49. package/templates/cleargate-planning/MANIFEST.json +72 -16
  50. package/dist/admin-api/index.cjs.map +0 -1
  51. package/dist/admin-api/index.js.map +0 -1
  52. package/dist/auth/factory.cjs.map +0 -1
  53. package/dist/auth/factory.js.map +0 -1
  54. package/dist/auth/require-token.cjs.map +0 -1
  55. package/dist/auth/require-token.js.map +0 -1
  56. package/dist/auth/token-store.cjs.map +0 -1
  57. package/dist/auth/token-store.js.map +0 -1
  58. package/dist/bootstrap-root-QKSA5V75.js.map +0 -1
  59. package/dist/chunk-5DI2Z3C2.js.map +0 -1
  60. package/dist/chunk-BTSZOEWC.js.map +0 -1
  61. package/dist/chunk-E3X7IE5E.js.map +0 -1
  62. package/dist/chunk-PDE37WFQ.js.map +0 -1
  63. package/dist/cli.cjs.map +0 -1
  64. package/dist/cli.js.map +0 -1
  65. package/dist/lib/ledger.cjs.map +0 -1
  66. package/dist/lib/ledger.js.map +0 -1
  67. package/dist/lib/lifecycle-reconcile.cjs.map +0 -1
  68. package/dist/lib/lifecycle-reconcile.js.map +0 -1
  69. package/dist/templates/cleargate-planning/.claude/agents/architect-reader.md +0 -61
  70. package/dist/templates/cleargate-planning/.claude/agents/architect-synth.md +0 -124
  71. package/dist/templates/cleargate-planning/.claude/agents/architect.md +0 -230
  72. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +0 -108
  73. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +0 -194
  74. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +0 -261
  75. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-query.md +0 -143
  76. package/dist/templates/cleargate-planning/.claude/agents/developer.md +0 -185
  77. package/dist/templates/cleargate-planning/.claude/agents/devops.md +0 -257
  78. package/dist/templates/cleargate-planning/.claude/agents/qa.md +0 -171
  79. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +0 -274
  80. package/dist/templates/cleargate-planning/.claude/hooks/pending-task-sentinel.sh +0 -209
  81. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +0 -33
  82. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-test-ratchet.sh +0 -58
  83. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit.sh +0 -19
  84. package/dist/templates/cleargate-planning/.claude/hooks/pre-edit-gate.sh +0 -162
  85. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-autonomy.sh +0 -58
  86. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +0 -148
  87. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +0 -75
  88. package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +0 -43
  89. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +0 -590
  90. package/dist/templates/cleargate-planning/.claude/settings.json +0 -68
  91. package/dist/templates/cleargate-planning/.claude/skills/flashcard/SKILL.md +0 -102
  92. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +0 -742
  93. package/dist/templates/cleargate-planning/.cleargate/FLASHCARD.md +0 -7
  94. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +0 -67
  95. package/dist/templates/cleargate-planning/.cleargate/config.yml +0 -18
  96. package/dist/templates/cleargate-planning/.cleargate/delivery/archive/.gitkeep +0 -0
  97. package/dist/templates/cleargate-planning/.cleargate/delivery/pending-sync/.gitkeep +0 -0
  98. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +0 -551
  99. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +0 -878
  100. package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +0 -160
  101. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +0 -213
  102. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +0 -71
  103. package/dist/templates/cleargate-planning/.cleargate/scripts/_migrate-schema-v3.mjs +0 -120
  104. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +0 -265
  105. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +0 -1012
  106. package/dist/templates/cleargate-planning/.cleargate/scripts/collision_surface.sh +0 -114
  107. package/dist/templates/cleargate-planning/.cleargate/scripts/constants.mjs +0 -62
  108. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +0 -219
  109. package/dist/templates/cleargate-planning/.cleargate/scripts/file_surface_diff.sh +0 -320
  110. package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +0 -15
  111. package/dist/templates/cleargate-planning/.cleargate/scripts/init_gate_config.sh +0 -38
  112. package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +0 -240
  113. package/dist/templates/cleargate-planning/.cleargate/scripts/launch_wave.mjs +0 -341
  114. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +0 -54
  115. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_common.sh +0 -206
  116. package/dist/templates/cleargate-planning/.cleargate/scripts/pre_gate_runner.sh +0 -371
  117. package/dist/templates/cleargate-planning/.cleargate/scripts/prefill_report.mjs +0 -280
  118. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +0 -378
  119. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +0 -888
  120. package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +0 -209
  121. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +0 -71
  122. package/dist/templates/cleargate-planning/.cleargate/scripts/state.schema.json +0 -127
  123. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +0 -717
  124. package/dist/templates/cleargate-planning/.cleargate/scripts/surface-whitelist.txt +0 -27
  125. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_assert_story_files.sh +0 -261
  126. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_file_surface.sh +0 -210
  127. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +0 -190
  128. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +0 -482
  129. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_test_ratchet.sh +0 -327
  130. package/dist/templates/cleargate-planning/.cleargate/scripts/test_ratchet.mjs +0 -261
  131. package/dist/templates/cleargate-planning/.cleargate/scripts/update_state.mjs +0 -246
  132. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_bounce_readiness.mjs +0 -111
  133. package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +0 -184
  134. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +0 -172
  135. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +0 -126
  136. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +0 -130
  137. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +0 -137
  138. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +0 -166
  139. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +0 -111
  140. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +0 -122
  141. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +0 -50
  142. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +0 -224
  143. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +0 -213
  144. package/dist/templates/cleargate-planning/CLAUDE.md +0 -66
  145. package/dist/templates/cleargate-planning/MANIFEST.json +0 -503
  146. package/dist/templates/synthesis/active-sprint.md +0 -30
  147. package/dist/templates/synthesis/open-gates.md +0 -38
  148. package/dist/templates/synthesis/product-state.md +0 -31
  149. package/dist/templates/synthesis/roadmap.md +0 -63
  150. package/dist/whoami-EANGN46Z.js.map +0 -1
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * qa_red_lint.mjs — Semantic QA-Red fixture lint (CR-081).
4
+ *
5
+ * Usage: node .cleargate/scripts/qa_red_lint.mjs <dir>
6
+ *
7
+ * Globs QA-Red test files under <dir> by their red-test naming convention:
8
+ * *.red.node.test.ts (meta-repo default)
9
+ * *.red.test.ts
10
+ * *.red.test.tsx
11
+ * test_*_red.py
12
+ *
13
+ * Scope: ONLY actual test files — NOT *.red.sh bash harnesses (prevents
14
+ * self-flagging the CR-081 harness which contains HEREDOC fixture strings).
15
+ *
16
+ * Exit 0 = clean (no flags).
17
+ * Exit 1 = ≥1 rule flagged (messages written to stderr: file:line rule-id fix).
18
+ *
19
+ * Rule registry (structured for growth — R-enum + R-query only in first ship):
20
+ *
21
+ * R-enum: Detects when a fixture constructs a typed model with an enum/Literal
22
+ * field whose argument is a string literal NOT in the statically-declared set.
23
+ * Conservative: only flags when BOTH the declared set AND the out-of-set
24
+ * literal are statically resolvable in the same file. Never flags when the set
25
+ * cannot be resolved.
26
+ *
27
+ * R-query: Flags queryByText(<s>) / getByText(<s>) whose target string <s> is
28
+ * duplicated across ≥2 rows in the same render-input literal within the test
29
+ * body. Recommends queryAllByText(<s>)[0] / getByTestId(...).
30
+ *
31
+ * No-false-flag guarantee: a file with no out-of-set Literal AND no
32
+ * duplicate-string getByText/queryByText → zero flags → exit 0.
33
+ * This specifically ensures CR-082's plain node:test file exits 0.
34
+ */
35
+
36
+ import fs from 'node:fs';
37
+ import path from 'node:path';
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Rule registry — {id, description, scan(fileText) -> [{line, message}]}
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * R-enum: Detect out-of-set literal in a Pydantic-style or TS-union typed constructor.
45
+ *
46
+ * Pattern:
47
+ * 1. Find Literal[...] declarations — e.g. Literal["a", "b", "c"]
48
+ * 2. Find TS string-union type aliases — e.g. type Theme = "a" | "b" | "c"
49
+ * 3. Find TS enums — e.g. enum Theme { A = "a", B = "b" }
50
+ * 4. Find constructor/prop calls that pass a bare string literal for a field
51
+ * whose allowed set was found in step 1-3.
52
+ * 5. Flag if the string literal is NOT in the allowed set.
53
+ *
54
+ * Conservative: only flags when set resolution succeeded AND the literal is
55
+ * statically visible (a bare string, not a variable/expression).
56
+ */
57
+ const rEnum = {
58
+ id: 'R-enum',
59
+ description: 'Constructed-fixture enum/Literal validity — literal argument not in declared set',
60
+ scan(fileText) {
61
+ const flags = [];
62
+ const lines = fileText.split('\n');
63
+
64
+ // ---- Step 1: Extract Pydantic Literal[...] sets ----
65
+ // Pattern: Literal["a", "b", "c"] or Literal['a', 'b', 'c']
66
+ // We collect all such sets from the file and build a map of set_values → declared.
67
+ // We then check constructor kwarg calls where theme=<literal>.
68
+ //
69
+ // Strategy: scan the file for lines containing Literal[...] and extract members.
70
+ // Then scan for constructor kwargs of the form field="value" or field='value'
71
+ // where the field name matches a Literal-typed field seen earlier.
72
+
73
+ /** @type {Map<string, {values: Set<string>, fieldName: string}>} */
74
+ const literalSets = new Map(); // fieldName -> {values}
75
+
76
+ // Regex: <fieldName>: Literal["v1", "v2", ...]
77
+ // Handles multi-line with a single-line scan (most fixture files are compact)
78
+ const literalTypeRegex = /(\w+)\s*:\s*Literal\[([^\]]+)\]/g;
79
+ for (const match of fileText.matchAll(literalTypeRegex)) {
80
+ const fieldName = match[1];
81
+ const rawValues = match[2];
82
+ const values = new Set();
83
+ // Extract quoted strings from the Literal[...] content
84
+ for (const vm of rawValues.matchAll(/["']([^"']+)["']/g)) {
85
+ values.add(vm[1]);
86
+ }
87
+ if (values.size > 0) {
88
+ literalSets.set(fieldName, values);
89
+ }
90
+ }
91
+
92
+ // ---- Step 2: Extract TS string-union type aliases ----
93
+ // Pattern: type <Name> = "a" | "b" | "c"
94
+ // We map the type NAME to its value set.
95
+ /** @type {Map<string, Set<string>>} */
96
+ const tsUnionTypes = new Map(); // typeName -> values
97
+
98
+ const tsUnionRegex = /type\s+(\w+)\s*=\s*((?:"[^"]*"|'[^']*')\s*(?:\|\s*(?:"[^"]*"|'[^']*')\s*)*)/g;
99
+ for (const match of fileText.matchAll(tsUnionRegex)) {
100
+ const typeName = match[1];
101
+ const rawUnion = match[2];
102
+ const values = new Set();
103
+ for (const vm of rawUnion.matchAll(/["']([^"']+)["']/g)) {
104
+ values.add(vm[1]);
105
+ }
106
+ if (values.size > 0) {
107
+ tsUnionTypes.set(typeName, values);
108
+ }
109
+ }
110
+
111
+ // ---- Step 3: Extract TS enums ----
112
+ // Pattern: enum <Name> { A = "a", B = "b" }
113
+ /** @type {Map<string, Set<string>>} */
114
+ const tsEnums = new Map(); // enumName -> values
115
+
116
+ const tsEnumRegex = /enum\s+(\w+)\s*\{([^}]+)\}/g;
117
+ for (const match of fileText.matchAll(tsEnumRegex)) {
118
+ const enumName = match[1];
119
+ const body = match[2];
120
+ const values = new Set();
121
+ for (const vm of body.matchAll(/=\s*["']([^"']+)["']/g)) {
122
+ values.add(vm[1]);
123
+ }
124
+ if (values.size > 0) {
125
+ tsEnums.set(enumName, values);
126
+ }
127
+ }
128
+
129
+ // Merge all TS typed sets into a field-name map.
130
+ // For TS: we also look for prop declarations like: fieldName: TypeName
131
+ // and then check constructor calls with fieldName: "value"
132
+ /** @type {Map<string, Set<string>>} */
133
+ const allFieldSets = new Map(literalSets); // start with Python Literal sets
134
+
135
+ // For TS unions and enums, find property declarations that use them:
136
+ // pattern: <fieldName>: <TypeName> (in interfaces or class definitions)
137
+ const tsPropDeclRegex = /(\w+)\s*:\s*(\w+)/g;
138
+ for (const match of fileText.matchAll(tsPropDeclRegex)) {
139
+ const propName = match[1];
140
+ const typeName = match[2];
141
+ const values = tsUnionTypes.get(typeName) ?? tsEnums.get(typeName);
142
+ if (values && !allFieldSets.has(propName)) {
143
+ allFieldSets.set(propName, values);
144
+ }
145
+ }
146
+
147
+ if (allFieldSets.size === 0) {
148
+ // No declared sets found — nothing to check
149
+ return flags;
150
+ }
151
+
152
+ // ---- Step 4: Check constructor/object calls for out-of-set literals ----
153
+ // Python: FieldName(field="value") or FieldName(field='value')
154
+ // TS: { field: "value" } or field="value" in JSX/React props
155
+
156
+ for (const [fieldName, allowedValues] of allFieldSets) {
157
+ // Scan every line with the given regex (capture group 1 = the literal),
158
+ // flagging any literal not in allowedValues. Shared by the Python-kwarg
159
+ // and TS-object-prop passes below (identical flag logic, different regex).
160
+ const scanForOutOfSet = (regex) => {
161
+ let lineIndex = 0;
162
+ for (const line of lines) {
163
+ lineIndex++;
164
+ for (const match of line.matchAll(regex)) {
165
+ const literal = match[1];
166
+ if (!allowedValues.has(literal)) {
167
+ flags.push({
168
+ line: lineIndex,
169
+ message: `R-enum: field "${fieldName}" value "${literal}" is not in the declared set [${[...allowedValues].map(v => `"${v}"`).join(', ')}]. Fix: use one of the declared values.`,
170
+ });
171
+ }
172
+ }
173
+ }
174
+ };
175
+
176
+ // Python kwarg: fieldName="value" or fieldName='value'
177
+ const pythonKwargRegex = new RegExp(
178
+ `\\b${fieldName}\\s*=\\s*["']([^"']+)["']`,
179
+ 'g'
180
+ );
181
+ scanForOutOfSet(pythonKwargRegex);
182
+
183
+ // TS object prop: fieldName: "value" — only when it looks like a value assignment (in { } context)
184
+ // We look for patterns like: fieldName: "value" but NOT fieldName: TypeName (declaration)
185
+ // Heuristic: in the same line, if it's preceded by whitespace or comma+whitespace and followed
186
+ // by a string literal (not a type identifier like a capital-letter word), flag it.
187
+ const tsObjPropRegex = new RegExp(
188
+ `(?:,|\\{|^)\\s*${fieldName}\\s*:\\s*["']([^"']+)["']`,
189
+ 'g'
190
+ );
191
+ scanForOutOfSet(tsObjPropRegex);
192
+ }
193
+
194
+ return flags;
195
+ },
196
+ };
197
+
198
+ /**
199
+ * R-query: Detect queryByText(<s>) / getByText(<s>) where <s> appears on
200
+ * ≥2 rows in the same render-input object literal within the test body.
201
+ *
202
+ * Pattern:
203
+ * 1. Find array literals used as render input (rows/data/items arrays)
204
+ * that contain objects with string values.
205
+ * 2. Collect all string values that appear ≥2 times across the array items.
206
+ * 3. For each queryByText/getByText call whose argument matches a duplicated string,
207
+ * flag it with a recommendation.
208
+ *
209
+ * Conservative: only flags when:
210
+ * - The duplicated string is a plain string literal (not variable).
211
+ * - The queryByText/getByText call uses the exact same plain string literal.
212
+ */
213
+ const rQuery = {
214
+ id: 'R-query',
215
+ description: 'query-by-text single-match hazard — target text is duplicated in render input',
216
+ scan(fileText) {
217
+ const flags = [];
218
+ const lines = fileText.split('\n');
219
+
220
+ // ---- Step 1: Collect all string literals that appear ≥2 times in object
221
+ // property values within array literals (render-input rows). ----
222
+ //
223
+ // Strategy: scan the file for string literal values in object contexts.
224
+ // Count occurrences of each string value in what looks like array-of-objects.
225
+ //
226
+ // We use a conservative heuristic: look for patterns like:
227
+ // detail: 'Connected' or detail: "Connected"
228
+ // status: 'ok'
229
+ // within array contexts (we check the whole file for simplicity,
230
+ // counting how many times each value appears in property contexts).
231
+ //
232
+ // Strip line comments (//) before scanning to avoid counting comment text.
233
+
234
+ /** @type {Map<string, number>} */
235
+ const propValueCounts = new Map();
236
+
237
+ // Strip single-line (//) comments before scanning for property values.
238
+ // This prevents comment text from inflating property value counts.
239
+ const strippedLines = lines.map(line => {
240
+ // Remove // comment suffix — naive but sufficient for fixture files.
241
+ // Does not handle strings containing // (e.g. URLs in strings) but
242
+ // fixture files in QA-Red tests rarely contain such patterns.
243
+ const commentIdx = line.indexOf('//');
244
+ return commentIdx >= 0 ? line.slice(0, commentIdx) : line;
245
+ });
246
+ const strippedText = strippedLines.join('\n');
247
+
248
+ // Match object property assignments: key: "value" or key: 'value'
249
+ // We want the VALUE part (the string literal)
250
+ // Only scan non-comment portions of the file.
251
+ const objPropValueRegex = /:\s*["']([^"']+)["']/g;
252
+ for (const match of strippedText.matchAll(objPropValueRegex)) {
253
+ const val = match[1];
254
+ propValueCounts.set(val, (propValueCounts.get(val) ?? 0) + 1);
255
+ }
256
+
257
+ // Find strings that appear ≥2 times as property values
258
+ const duplicatedValues = new Set(
259
+ [...propValueCounts.entries()]
260
+ .filter(([, count]) => count >= 2)
261
+ .map(([val]) => val)
262
+ );
263
+
264
+ if (duplicatedValues.size === 0) {
265
+ return flags;
266
+ }
267
+
268
+ // ---- Step 2: Find queryByText / getByText calls that use duplicated strings ----
269
+ const queryRegex = /(?:queryByText|getByText)\(\s*["']([^"']+)["']\s*\)/g;
270
+ let lineIndex = 0;
271
+ for (const line of lines) {
272
+ lineIndex++;
273
+ for (const match of line.matchAll(queryRegex)) {
274
+ const queryStr = match[1];
275
+ if (duplicatedValues.has(queryStr)) {
276
+ const fnName = match[0].startsWith('queryByText') ? 'queryByText' : 'getByText';
277
+ flags.push({
278
+ line: lineIndex,
279
+ message: `R-query: ${fnName}('${queryStr}') targets a string that appears on ≥2 rows in the render input — this will throw "Found multiple elements". Fix: use queryAllByText('${queryStr}')[0] or getByTestId(...) instead.`,
280
+ });
281
+ }
282
+ }
283
+ }
284
+
285
+ return flags;
286
+ },
287
+ };
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Rule registry (ordered; add new rules by appending)
291
+ // ---------------------------------------------------------------------------
292
+ const RULES = [rEnum, rQuery];
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // File glob — scan only red test files, NOT bash harnesses
296
+ // ---------------------------------------------------------------------------
297
+ const RED_TEST_EXTENSIONS = [
298
+ /\.red\.node\.test\.ts$/,
299
+ /\.red\.test\.ts$/,
300
+ /\.red\.test\.tsx$/,
301
+ /^test_.*_red\.py$/,
302
+ ];
303
+
304
+ /**
305
+ * Recursively collect all red-test files under a directory.
306
+ * Excludes node_modules, .git, and *.red.sh files (bash harnesses).
307
+ * @param {string} dir
308
+ * @returns {string[]} absolute paths
309
+ */
310
+ function collectRedTestFiles(dir) {
311
+ const results = [];
312
+ let entries;
313
+ try {
314
+ entries = fs.readdirSync(dir, { withFileTypes: true });
315
+ } catch {
316
+ return results;
317
+ }
318
+ for (const entry of entries) {
319
+ const fullPath = path.join(dir, entry.name);
320
+ if (entry.isDirectory()) {
321
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
322
+ results.push(...collectRedTestFiles(fullPath));
323
+ } else if (entry.isFile()) {
324
+ const name = entry.name;
325
+ // Must match a red-test pattern AND must NOT be a .sh file
326
+ if (name.endsWith('.sh')) continue;
327
+ if (RED_TEST_EXTENSIONS.some(re => re.test(name))) {
328
+ results.push(fullPath);
329
+ }
330
+ }
331
+ }
332
+ return results;
333
+ }
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // Main
337
+ // ---------------------------------------------------------------------------
338
+ const [, , dirArg] = process.argv;
339
+
340
+ if (!dirArg) {
341
+ process.stderr.write('Usage: node qa_red_lint.mjs <dir>\n');
342
+ process.exit(2);
343
+ }
344
+
345
+ const targetDir = path.resolve(dirArg);
346
+
347
+ if (!fs.existsSync(targetDir)) {
348
+ process.stderr.write(`qa_red_lint: directory does not exist: ${targetDir}\n`);
349
+ process.exit(2);
350
+ }
351
+
352
+ const testFiles = collectRedTestFiles(targetDir);
353
+
354
+ let totalFlags = 0;
355
+
356
+ for (const filePath of testFiles) {
357
+ let fileText;
358
+ try {
359
+ fileText = fs.readFileSync(filePath, 'utf8');
360
+ } catch (err) {
361
+ process.stderr.write(`qa_red_lint: cannot read ${filePath}: ${err.message}\n`);
362
+ continue;
363
+ }
364
+
365
+ for (const rule of RULES) {
366
+ let ruleFlags;
367
+ try {
368
+ ruleFlags = rule.scan(fileText);
369
+ } catch {
370
+ // Never crash on malformed input — rule scan failure is non-fatal
371
+ continue;
372
+ }
373
+ for (const flag of ruleFlags) {
374
+ process.stderr.write(`${filePath}:${flag.line}: [${rule.id}] ${flag.message}\n`);
375
+ totalFlags++;
376
+ }
377
+ }
378
+ }
379
+
380
+ process.exit(totalFlags > 0 ? 1 : 0);
@@ -25,6 +25,13 @@
25
25
  # AGENT_TYPE — populates incident JSON agent_type field (null if empty)
26
26
  # WORK_ITEM_ID — populates incident JSON work_item_id field (null if empty)
27
27
  # RUN_SCRIPT_ACTIVE — self-exemption guard (set to 1 by this wrapper)
28
+ # CLEARGATE_STATE_FILE — explicitly forwarded/exported to child (F8, CR-080)
29
+ # CLAUDE_PROJECT_DIR — explicitly forwarded/exported to child (F8, CR-080)
30
+ # RUN_SCRIPT_ENV_ALLOWLIST — optional: comma/space-separated list of env var names;
31
+ # when set, only those vars (plus the always-forwarded set
32
+ # above) are guaranteed exported to the child. Default:
33
+ # full inherited-environment pass-through (allowlist is
34
+ # opt-in only — do NOT set this unless you need isolation).
28
35
 
29
36
  set -euo pipefail
30
37
 
@@ -32,7 +39,13 @@ set -euo pipefail
32
39
  # Self-exemption guard — do not wrap recursively
33
40
  # ---------------------------------------------------------------------------
34
41
  if [[ "${RUN_SCRIPT_ACTIVE:-}" == "1" ]]; then
35
- # Already inside a wrapper invocation; pass through directly, no JSON capture
42
+ # Already inside a wrapper invocation; pass through directly, no JSON capture.
43
+ # F8 (CR-080): guarantee ClearGate config vars are exported even on this fast path.
44
+ [[ -n "${CLEARGATE_STATE_FILE:-}" ]] && export CLEARGATE_STATE_FILE
45
+ [[ -n "${ORCHESTRATOR_PROJECT_DIR:-}" ]] && export ORCHESTRATOR_PROJECT_DIR
46
+ [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && export CLAUDE_PROJECT_DIR
47
+ [[ -n "${AGENT_TYPE:-}" ]] && export AGENT_TYPE
48
+ [[ -n "${WORK_ITEM_ID:-}" ]] && export WORK_ITEM_ID
36
49
  exec "$@"
37
50
  fi
38
51
 
@@ -88,6 +101,26 @@ trap 'rm -f "$STDOUT_TMP" "$STDERR_TMP"' EXIT
88
101
  # Mark self as active before running the wrapped command
89
102
  export RUN_SCRIPT_ACTIVE=1
90
103
 
104
+ # F8 (CR-080): explicitly export the documented ClearGate config vars so they
105
+ # reach the child regardless of whether the caller used `export` or plain assignment.
106
+ # Default = full inherited-environment pass-through; allowlist is opt-in only.
107
+ [[ -n "${CLEARGATE_STATE_FILE:-}" ]] && export CLEARGATE_STATE_FILE
108
+ [[ -n "${ORCHESTRATOR_PROJECT_DIR:-}" ]] && export ORCHESTRATOR_PROJECT_DIR
109
+ [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && export CLAUDE_PROJECT_DIR
110
+ [[ -n "${AGENT_TYPE:-}" ]] && export AGENT_TYPE
111
+ [[ -n "${WORK_ITEM_ID:-}" ]] && export WORK_ITEM_ID
112
+
113
+ # Optional allowlist: when RUN_SCRIPT_ENV_ALLOWLIST is set (comma/space-separated
114
+ # var names), restrict env to only those vars + the always-forwarded set above.
115
+ # This is opt-in isolation — never the default.
116
+ if [[ -n "${RUN_SCRIPT_ENV_ALLOWLIST:-}" ]]; then
117
+ _allowed_env=""
118
+ for _var in ${RUN_SCRIPT_ENV_ALLOWLIST//,/ }; do
119
+ _val="${!_var:-}"
120
+ [[ -n "$_val" ]] && _allowed_env="${_allowed_env}${_var}=${_val} "
121
+ done
122
+ fi
123
+
91
124
  EXIT_CODE=0
92
125
  "$@" >"$STDOUT_TMP" 2>"$STDERR_TMP" || EXIT_CODE=$?
93
126
 
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ # cr077_eviction.red.sh — CR-077 QA-Red eviction grep test.
3
+ #
4
+ # RED: fails against clean baseline — the EPIC-028 policy tokens (node:test,
5
+ # vitest, check:no-vitest, tsx --test, node.test.ts) are still present in the
6
+ # three scoped agent files + gate-checks.json + sprint_context.md template.
7
+ # GREEN: passes after Developer evicts those policy tokens (M1 §5b / §7.2).
8
+ #
9
+ # SCOPE NARROWED per M1 plan §5b + §7.2:
10
+ # - Grep ONLY the three §3-scoped agents + gate-checks.json + sprint_context.md
11
+ # (NOT bare 'cleargate-cli' token — it legitimately appears in devops/reporter/
12
+ # wiki agents as meta-repo path references and in qa.md:98/116 + architect.md:64
13
+ # as meta-repo runtime-lane heuristics; asserting zero there forces deleting
14
+ # correct framework content — see M1 plan §7 Risk 2).
15
+ # - The gate-checks.json IS in scope because its `cd cleargate-cli` literal IS
16
+ # the F6 leak.
17
+ #
18
+ # Mirrors the test_prep_qa_context.sh bash-harness pattern
19
+ # (.cleargate/scripts/test/ shape, mktemp-free since we read live files).
20
+ # macOS bash 3.2 portable.
21
+ #
22
+ # Exit 0 = PASS (all assertions pass); exit 1 = one or more FAIL.
23
+ set -uo pipefail
24
+
25
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
26
+
27
+ PASS=0
28
+ FAIL=0
29
+
30
+ pass() {
31
+ echo "PASS: $1"
32
+ PASS=$((PASS + 1))
33
+ }
34
+
35
+ fail() {
36
+ echo "FAIL: $1"
37
+ echo " detail: $2"
38
+ FAIL=$((FAIL + 1))
39
+ }
40
+
41
+ # ── File paths (absolute) ─────────────────────────────────────────────────────
42
+ DEVELOPER_MD="${REPO_ROOT}/cleargate-planning/.claude/agents/developer.md"
43
+ QA_MD="${REPO_ROOT}/cleargate-planning/.claude/agents/qa.md"
44
+ ARCHITECT_MD="${REPO_ROOT}/cleargate-planning/.claude/agents/architect.md"
45
+ GATE_CHECKS="${REPO_ROOT}/cleargate-planning/.cleargate/scripts/gate-checks.json"
46
+ SPRINT_CONTEXT_TEMPLATE="${REPO_ROOT}/cleargate-planning/.cleargate/templates/sprint_context.md"
47
+
48
+ # ── Guard: verify scoped files exist ─────────────────────────────────────────
49
+ for f in "$DEVELOPER_MD" "$QA_MD" "$ARCHITECT_MD" "$GATE_CHECKS" "$SPRINT_CONTEXT_TEMPLATE"; do
50
+ if [[ ! -f "$f" ]]; then
51
+ fail "scoped file exists" "MISSING: $f"
52
+ fi
53
+ done
54
+
55
+ # ── Scenario: EPIC-028 policy tokens evicted from scoped files ───────────────
56
+ #
57
+ # Tokens: node:test | vitest | check:no-vitest | tsx --test | node.test.ts
58
+ # (the bare 'cleargate-cli' token is EXCLUDED from this grep — see SCOPE note above)
59
+ #
60
+ # Expected result after Developer implements CR-077: ZERO matches.
61
+ # Right now (clean baseline) this returns matches → the test FAILS RED.
62
+
63
+ MATCH_COUNT=$(grep -rniE \
64
+ "node:test|vitest|check:no-vitest|tsx --test|node\.test\.ts" \
65
+ "$DEVELOPER_MD" \
66
+ "$QA_MD" \
67
+ "$ARCHITECT_MD" \
68
+ "$GATE_CHECKS" \
69
+ "$SPRINT_CONTEXT_TEMPLATE" \
70
+ 2>/dev/null | wc -l | tr -d '[:space:]')
71
+
72
+ if [[ "$MATCH_COUNT" -eq 0 ]]; then
73
+ pass "eviction grep — zero EPIC-028 policy tokens in scoped files"
74
+ else
75
+ # Print the actual matches to help the Developer understand what remains
76
+ MATCH_LINES=$(grep -rniE \
77
+ "node:test|vitest|check:no-vitest|tsx --test|node\.test\.ts" \
78
+ "$DEVELOPER_MD" \
79
+ "$QA_MD" \
80
+ "$ARCHITECT_MD" \
81
+ "$GATE_CHECKS" \
82
+ "$SPRINT_CONTEXT_TEMPLATE" \
83
+ 2>/dev/null | head -20)
84
+ fail "eviction grep — EPIC-028 policy tokens still present (expect ZERO, got ${MATCH_COUNT})" \
85
+ "First matches:
86
+ ${MATCH_LINES}"
87
+ fi
88
+
89
+ # ── Sub-scenario: gate-checks.json specifically has no "cd cleargate-cli" ────
90
+ # The F6 literal must be gone from the shipped payload gate-checks.json.
91
+ if ! grep -q "cd cleargate-cli" "$GATE_CHECKS" 2>/dev/null; then
92
+ pass "gate-checks.json — no 'cd cleargate-cli' F6 literal in shipped payload"
93
+ else
94
+ fail "gate-checks.json — F6 literal 'cd cleargate-cli' still present (count: $(grep -c 'cd cleargate-cli' "$GATE_CHECKS"))" \
95
+ "$(grep -n 'cd cleargate-cli' "$GATE_CHECKS")"
96
+ fi
97
+
98
+ # ── Sub-scenario: sprint_context.md template has a ## Test Stack block ───────
99
+ # The template must contain the structured test-stack block introduced by CR-077 §3b.
100
+ if grep -q "## Test Stack" "$SPRINT_CONTEXT_TEMPLATE" 2>/dev/null; then
101
+ pass "sprint_context.md template — ## Test Stack block present"
102
+ else
103
+ fail "sprint_context.md template — ## Test Stack block MISSING" \
104
+ "grep '## Test Stack' returned no match in ${SPRINT_CONTEXT_TEMPLATE}"
105
+ fi
106
+
107
+ # ── Summary ───────────────────────────────────────────────────────────────────
108
+ echo ""
109
+ echo "cr077_eviction.red.sh: ${PASS} passed, ${FAIL} failed"
110
+ if [[ "$FAIL" -gt 0 ]]; then
111
+ exit 1
112
+ fi
113
+ exit 0