popeye-cli 1.10.0 → 2.0.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 (253) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CONTRIBUTING.md +15 -1
  3. package/README.md +57 -0
  4. package/dist/pipeline/artifact-manager.d.ts +47 -0
  5. package/dist/pipeline/artifact-manager.d.ts.map +1 -0
  6. package/dist/pipeline/artifact-manager.js +251 -0
  7. package/dist/pipeline/artifact-manager.js.map +1 -0
  8. package/dist/pipeline/artifact-validators.d.ts +29 -0
  9. package/dist/pipeline/artifact-validators.d.ts.map +1 -0
  10. package/dist/pipeline/artifact-validators.js +173 -0
  11. package/dist/pipeline/artifact-validators.js.map +1 -0
  12. package/dist/pipeline/change-request.d.ts +47 -0
  13. package/dist/pipeline/change-request.d.ts.map +1 -0
  14. package/dist/pipeline/change-request.js +91 -0
  15. package/dist/pipeline/change-request.js.map +1 -0
  16. package/dist/pipeline/check-runner.d.ts +47 -0
  17. package/dist/pipeline/check-runner.d.ts.map +1 -0
  18. package/dist/pipeline/check-runner.js +417 -0
  19. package/dist/pipeline/check-runner.js.map +1 -0
  20. package/dist/pipeline/command-resolver.d.ts +9 -0
  21. package/dist/pipeline/command-resolver.d.ts.map +1 -0
  22. package/dist/pipeline/command-resolver.js +140 -0
  23. package/dist/pipeline/command-resolver.js.map +1 -0
  24. package/dist/pipeline/consensus/consensus-runner.d.ts +44 -0
  25. package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -0
  26. package/dist/pipeline/consensus/consensus-runner.js +212 -0
  27. package/dist/pipeline/consensus/consensus-runner.js.map +1 -0
  28. package/dist/pipeline/constitution.d.ts +45 -0
  29. package/dist/pipeline/constitution.d.ts.map +1 -0
  30. package/dist/pipeline/constitution.js +82 -0
  31. package/dist/pipeline/constitution.js.map +1 -0
  32. package/dist/pipeline/gate-engine.d.ts +55 -0
  33. package/dist/pipeline/gate-engine.d.ts.map +1 -0
  34. package/dist/pipeline/gate-engine.js +270 -0
  35. package/dist/pipeline/gate-engine.js.map +1 -0
  36. package/dist/pipeline/index.d.ts +26 -0
  37. package/dist/pipeline/index.d.ts.map +1 -0
  38. package/dist/pipeline/index.js +35 -0
  39. package/dist/pipeline/index.js.map +1 -0
  40. package/dist/pipeline/migration.d.ts +15 -0
  41. package/dist/pipeline/migration.d.ts.map +1 -0
  42. package/dist/pipeline/migration.js +76 -0
  43. package/dist/pipeline/migration.js.map +1 -0
  44. package/dist/pipeline/orchestrator.d.ts +28 -0
  45. package/dist/pipeline/orchestrator.d.ts.map +1 -0
  46. package/dist/pipeline/orchestrator.js +238 -0
  47. package/dist/pipeline/orchestrator.js.map +1 -0
  48. package/dist/pipeline/packets/audit-report-builder.d.ts +11 -0
  49. package/dist/pipeline/packets/audit-report-builder.d.ts.map +1 -0
  50. package/dist/pipeline/packets/audit-report-builder.js +32 -0
  51. package/dist/pipeline/packets/audit-report-builder.js.map +1 -0
  52. package/dist/pipeline/packets/consensus-packet-builder.d.ts +35 -0
  53. package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -0
  54. package/dist/pipeline/packets/consensus-packet-builder.js +80 -0
  55. package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -0
  56. package/dist/pipeline/packets/index.d.ts +12 -0
  57. package/dist/pipeline/packets/index.d.ts.map +1 -0
  58. package/dist/pipeline/packets/index.js +8 -0
  59. package/dist/pipeline/packets/index.js.map +1 -0
  60. package/dist/pipeline/packets/plan-packet-builder.d.ts +21 -0
  61. package/dist/pipeline/packets/plan-packet-builder.d.ts.map +1 -0
  62. package/dist/pipeline/packets/plan-packet-builder.js +27 -0
  63. package/dist/pipeline/packets/plan-packet-builder.js.map +1 -0
  64. package/dist/pipeline/packets/rca-packet-builder.d.ts +19 -0
  65. package/dist/pipeline/packets/rca-packet-builder.d.ts.map +1 -0
  66. package/dist/pipeline/packets/rca-packet-builder.js +22 -0
  67. package/dist/pipeline/packets/rca-packet-builder.js.map +1 -0
  68. package/dist/pipeline/phases/architecture.d.ts +7 -0
  69. package/dist/pipeline/phases/architecture.d.ts.map +1 -0
  70. package/dist/pipeline/phases/architecture.js +60 -0
  71. package/dist/pipeline/phases/architecture.js.map +1 -0
  72. package/dist/pipeline/phases/audit.d.ts +8 -0
  73. package/dist/pipeline/phases/audit.d.ts.map +1 -0
  74. package/dist/pipeline/phases/audit.js +144 -0
  75. package/dist/pipeline/phases/audit.js.map +1 -0
  76. package/dist/pipeline/phases/consensus-architecture.d.ts +7 -0
  77. package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -0
  78. package/dist/pipeline/phases/consensus-architecture.js +84 -0
  79. package/dist/pipeline/phases/consensus-architecture.js.map +1 -0
  80. package/dist/pipeline/phases/consensus-master-plan.d.ts +7 -0
  81. package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -0
  82. package/dist/pipeline/phases/consensus-master-plan.js +81 -0
  83. package/dist/pipeline/phases/consensus-master-plan.js.map +1 -0
  84. package/dist/pipeline/phases/consensus-role-plans.d.ts +7 -0
  85. package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -0
  86. package/dist/pipeline/phases/consensus-role-plans.js +85 -0
  87. package/dist/pipeline/phases/consensus-role-plans.js.map +1 -0
  88. package/dist/pipeline/phases/done.d.ts +7 -0
  89. package/dist/pipeline/phases/done.d.ts.map +1 -0
  90. package/dist/pipeline/phases/done.js +45 -0
  91. package/dist/pipeline/phases/done.js.map +1 -0
  92. package/dist/pipeline/phases/implementation.d.ts +8 -0
  93. package/dist/pipeline/phases/implementation.d.ts.map +1 -0
  94. package/dist/pipeline/phases/implementation.js +42 -0
  95. package/dist/pipeline/phases/implementation.js.map +1 -0
  96. package/dist/pipeline/phases/index.d.ts +20 -0
  97. package/dist/pipeline/phases/index.d.ts.map +1 -0
  98. package/dist/pipeline/phases/index.js +19 -0
  99. package/dist/pipeline/phases/index.js.map +1 -0
  100. package/dist/pipeline/phases/intake.d.ts +8 -0
  101. package/dist/pipeline/phases/intake.d.ts.map +1 -0
  102. package/dist/pipeline/phases/intake.js +40 -0
  103. package/dist/pipeline/phases/intake.js.map +1 -0
  104. package/dist/pipeline/phases/phase-context.d.ts +30 -0
  105. package/dist/pipeline/phases/phase-context.d.ts.map +1 -0
  106. package/dist/pipeline/phases/phase-context.js +33 -0
  107. package/dist/pipeline/phases/phase-context.js.map +1 -0
  108. package/dist/pipeline/phases/production-gate.d.ts +8 -0
  109. package/dist/pipeline/phases/production-gate.d.ts.map +1 -0
  110. package/dist/pipeline/phases/production-gate.js +84 -0
  111. package/dist/pipeline/phases/production-gate.js.map +1 -0
  112. package/dist/pipeline/phases/qa-validation.d.ts +7 -0
  113. package/dist/pipeline/phases/qa-validation.d.ts.map +1 -0
  114. package/dist/pipeline/phases/qa-validation.js +50 -0
  115. package/dist/pipeline/phases/qa-validation.js.map +1 -0
  116. package/dist/pipeline/phases/recovery-loop.d.ts +7 -0
  117. package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -0
  118. package/dist/pipeline/phases/recovery-loop.js +91 -0
  119. package/dist/pipeline/phases/recovery-loop.js.map +1 -0
  120. package/dist/pipeline/phases/review.d.ts +8 -0
  121. package/dist/pipeline/phases/review.d.ts.map +1 -0
  122. package/dist/pipeline/phases/review.js +127 -0
  123. package/dist/pipeline/phases/review.js.map +1 -0
  124. package/dist/pipeline/phases/role-planning.d.ts +7 -0
  125. package/dist/pipeline/phases/role-planning.d.ts.map +1 -0
  126. package/dist/pipeline/phases/role-planning.js +75 -0
  127. package/dist/pipeline/phases/role-planning.js.map +1 -0
  128. package/dist/pipeline/phases/stuck.d.ts +7 -0
  129. package/dist/pipeline/phases/stuck.d.ts.map +1 -0
  130. package/dist/pipeline/phases/stuck.js +51 -0
  131. package/dist/pipeline/phases/stuck.js.map +1 -0
  132. package/dist/pipeline/repo-snapshot.d.ts +24 -0
  133. package/dist/pipeline/repo-snapshot.d.ts.map +1 -0
  134. package/dist/pipeline/repo-snapshot.js +343 -0
  135. package/dist/pipeline/repo-snapshot.js.map +1 -0
  136. package/dist/pipeline/role-execution-adapter.d.ts +59 -0
  137. package/dist/pipeline/role-execution-adapter.d.ts.map +1 -0
  138. package/dist/pipeline/role-execution-adapter.js +159 -0
  139. package/dist/pipeline/role-execution-adapter.js.map +1 -0
  140. package/dist/pipeline/skill-loader.d.ts +34 -0
  141. package/dist/pipeline/skill-loader.d.ts.map +1 -0
  142. package/dist/pipeline/skill-loader.js +156 -0
  143. package/dist/pipeline/skill-loader.js.map +1 -0
  144. package/dist/pipeline/skills/defaults.d.ts +16 -0
  145. package/dist/pipeline/skills/defaults.d.ts.map +1 -0
  146. package/dist/pipeline/skills/defaults.js +189 -0
  147. package/dist/pipeline/skills/defaults.js.map +1 -0
  148. package/dist/pipeline/type-defs/artifacts.d.ts +202 -0
  149. package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -0
  150. package/dist/pipeline/type-defs/artifacts.js +66 -0
  151. package/dist/pipeline/type-defs/artifacts.js.map +1 -0
  152. package/dist/pipeline/type-defs/audit.d.ts +256 -0
  153. package/dist/pipeline/type-defs/audit.d.ts.map +1 -0
  154. package/dist/pipeline/type-defs/audit.js +54 -0
  155. package/dist/pipeline/type-defs/audit.js.map +1 -0
  156. package/dist/pipeline/type-defs/checks.d.ts +81 -0
  157. package/dist/pipeline/type-defs/checks.d.ts.map +1 -0
  158. package/dist/pipeline/type-defs/checks.js +38 -0
  159. package/dist/pipeline/type-defs/checks.js.map +1 -0
  160. package/dist/pipeline/type-defs/enums.d.ts +43 -0
  161. package/dist/pipeline/type-defs/enums.d.ts.map +1 -0
  162. package/dist/pipeline/type-defs/enums.js +55 -0
  163. package/dist/pipeline/type-defs/enums.js.map +1 -0
  164. package/dist/pipeline/type-defs/index.d.ts +12 -0
  165. package/dist/pipeline/type-defs/index.d.ts.map +1 -0
  166. package/dist/pipeline/type-defs/index.js +12 -0
  167. package/dist/pipeline/type-defs/index.js.map +1 -0
  168. package/dist/pipeline/type-defs/packets.d.ts +806 -0
  169. package/dist/pipeline/type-defs/packets.d.ts.map +1 -0
  170. package/dist/pipeline/type-defs/packets.js +109 -0
  171. package/dist/pipeline/type-defs/packets.js.map +1 -0
  172. package/dist/pipeline/type-defs/snapshot.d.ts +52 -0
  173. package/dist/pipeline/type-defs/snapshot.d.ts.map +1 -0
  174. package/dist/pipeline/type-defs/snapshot.js +35 -0
  175. package/dist/pipeline/type-defs/snapshot.js.map +1 -0
  176. package/dist/pipeline/type-defs/state.d.ts +449 -0
  177. package/dist/pipeline/type-defs/state.d.ts.map +1 -0
  178. package/dist/pipeline/type-defs/state.js +88 -0
  179. package/dist/pipeline/type-defs/state.js.map +1 -0
  180. package/dist/pipeline/types.d.ts +16 -0
  181. package/dist/pipeline/types.d.ts.map +1 -0
  182. package/dist/pipeline/types.js +16 -0
  183. package/dist/pipeline/types.js.map +1 -0
  184. package/dist/types/audit.d.ts +6 -6
  185. package/dist/workflow/index.d.ts.map +1 -1
  186. package/dist/workflow/index.js +48 -0
  187. package/dist/workflow/index.js.map +1 -1
  188. package/package.json +1 -1
  189. package/skills/PHASE_GATE_ENGINE_SPEC.md +113 -20
  190. package/skills/POPEYE_FULL_AUTONOMY_PIPELINE.md +66 -13
  191. package/src/pipeline/artifact-manager.ts +339 -0
  192. package/src/pipeline/artifact-validators.ts +224 -0
  193. package/src/pipeline/change-request.ts +119 -0
  194. package/src/pipeline/check-runner.ts +504 -0
  195. package/src/pipeline/command-resolver.ts +168 -0
  196. package/src/pipeline/consensus/consensus-runner.ts +317 -0
  197. package/src/pipeline/constitution.ts +109 -0
  198. package/src/pipeline/gate-engine.ts +347 -0
  199. package/src/pipeline/index.ts +82 -0
  200. package/src/pipeline/migration.ts +91 -0
  201. package/src/pipeline/orchestrator.ts +314 -0
  202. package/src/pipeline/packets/audit-report-builder.ts +47 -0
  203. package/src/pipeline/packets/consensus-packet-builder.ts +112 -0
  204. package/src/pipeline/packets/index.ts +15 -0
  205. package/src/pipeline/packets/plan-packet-builder.ts +52 -0
  206. package/src/pipeline/packets/rca-packet-builder.ts +38 -0
  207. package/src/pipeline/phases/architecture.ts +73 -0
  208. package/src/pipeline/phases/audit.ts +193 -0
  209. package/src/pipeline/phases/consensus-architecture.ts +104 -0
  210. package/src/pipeline/phases/consensus-master-plan.ts +100 -0
  211. package/src/pipeline/phases/consensus-role-plans.ts +105 -0
  212. package/src/pipeline/phases/done.ts +68 -0
  213. package/src/pipeline/phases/implementation.ts +48 -0
  214. package/src/pipeline/phases/index.ts +21 -0
  215. package/src/pipeline/phases/intake.ts +54 -0
  216. package/src/pipeline/phases/phase-context.ts +86 -0
  217. package/src/pipeline/phases/production-gate.ts +113 -0
  218. package/src/pipeline/phases/qa-validation.ts +63 -0
  219. package/src/pipeline/phases/recovery-loop.ts +118 -0
  220. package/src/pipeline/phases/review.ts +149 -0
  221. package/src/pipeline/phases/role-planning.ts +92 -0
  222. package/src/pipeline/phases/stuck.ts +62 -0
  223. package/src/pipeline/repo-snapshot.ts +395 -0
  224. package/src/pipeline/role-execution-adapter.ts +238 -0
  225. package/src/pipeline/skill-loader.ts +192 -0
  226. package/src/pipeline/skills/defaults.ts +215 -0
  227. package/src/pipeline/type-defs/artifacts.ts +81 -0
  228. package/src/pipeline/type-defs/audit.ts +67 -0
  229. package/src/pipeline/type-defs/checks.ts +47 -0
  230. package/src/pipeline/type-defs/enums.ts +62 -0
  231. package/src/pipeline/type-defs/index.ts +12 -0
  232. package/src/pipeline/type-defs/packets.ts +131 -0
  233. package/src/pipeline/type-defs/snapshot.ts +55 -0
  234. package/src/pipeline/type-defs/state.ts +165 -0
  235. package/src/pipeline/types.ts +16 -0
  236. package/src/workflow/index.ts +48 -0
  237. package/tests/pipeline/artifact-manager.test.ts +183 -0
  238. package/tests/pipeline/artifact-validators.test.ts +207 -0
  239. package/tests/pipeline/change-request.test.ts +180 -0
  240. package/tests/pipeline/check-runner.test.ts +157 -0
  241. package/tests/pipeline/command-resolver.test.ts +159 -0
  242. package/tests/pipeline/consensus-runner.test.ts +206 -0
  243. package/tests/pipeline/consensus-scoring.test.ts +163 -0
  244. package/tests/pipeline/constitution.test.ts +122 -0
  245. package/tests/pipeline/gate-engine.test.ts +195 -0
  246. package/tests/pipeline/migration.test.ts +133 -0
  247. package/tests/pipeline/orchestrator.test.ts +614 -0
  248. package/tests/pipeline/packets/builders.test.ts +347 -0
  249. package/tests/pipeline/repo-snapshot.test.ts +189 -0
  250. package/tests/pipeline/role-execution-adapter.test.ts +299 -0
  251. package/tests/pipeline/skill-loader.test.ts +186 -0
  252. package/tests/pipeline/start-env-checks.test.ts +123 -0
  253. package/tests/pipeline/types.test.ts +156 -0
@@ -0,0 +1,504 @@
1
+ /**
2
+ * Check Runner — executes build/test/lint/typecheck commands
3
+ * and produces GateCheckResult artifacts.
4
+ *
5
+ * Safety: command sanitization, cwd enforcement, stream caps,
6
+ * configurable timeouts (P2-G).
7
+ */
8
+
9
+ import { exec } from 'node:child_process';
10
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
11
+ import { join, extname } from 'node:path';
12
+
13
+ import type {
14
+ GateCheckResult,
15
+ GateCheckType,
16
+ ResolvedCommands,
17
+ RepoSnapshot,
18
+ ArtifactEntry,
19
+ PipelinePhase,
20
+ } from './types.js';
21
+ import { ArtifactManager } from './artifact-manager.js';
22
+
23
+ // ─── Constants ───────────────────────────────────────────
24
+
25
+ /** Default timeout per check type in milliseconds */
26
+ const DEFAULT_TIMEOUTS: Record<string, number> = {
27
+ build: 20 * 60 * 1000, // 20 minutes
28
+ test: 10 * 60 * 1000, // 10 minutes
29
+ lint: 5 * 60 * 1000, // 5 minutes
30
+ typecheck: 5 * 60 * 1000, // 5 minutes
31
+ migration: 5 * 60 * 1000, // 5 minutes
32
+ };
33
+
34
+ /** Max stdout/stderr capture in bytes */
35
+ const MAX_OUTPUT_SIZE = 1024 * 1024; // 1 MB
36
+
37
+ /** Dangerous command patterns to reject */
38
+ const DANGEROUS_PATTERNS = [
39
+ /rm\s+-rf\s+\//,
40
+ /sudo\s+/,
41
+ />\s*\/dev\//,
42
+ />\s*\/etc\//,
43
+ />\s*\/usr\//,
44
+ /;\s*rm\s/,
45
+ /&&\s*rm\s/,
46
+ /\|\s*sh$/,
47
+ /\|\s*bash$/,
48
+ ];
49
+
50
+ /** Placeholder patterns for scanning */
51
+ const PLACEHOLDER_PATTERNS = [
52
+ /\bTODO\b/i,
53
+ /\bFIXME\b/i,
54
+ /\bHACK\b/i,
55
+ /\bXXX\b/i,
56
+ /placeholder/i,
57
+ /\bmock\b(?!\.)/i, // 'mock' but not 'mock.' (import paths)
58
+ /\btemp\b(?!late)/i, // 'temp' but not 'template'
59
+ /lorem ipsum/i,
60
+ /example\.com/i,
61
+ ];
62
+
63
+ // ─── Command Sanitization ────────────────────────────────
64
+
65
+ function sanitizeCommand(command: string): { safe: boolean; reason?: string } {
66
+ for (const pattern of DANGEROUS_PATTERNS) {
67
+ if (pattern.test(command)) {
68
+ return { safe: false, reason: `Matches dangerous pattern: ${pattern.source}` };
69
+ }
70
+ }
71
+ return { safe: true };
72
+ }
73
+
74
+ // ─── Check Execution ─────────────────────────────────────
75
+
76
+ /** Execute a single check command */
77
+ export async function runCheck(
78
+ checkType: GateCheckType,
79
+ command: string,
80
+ projectDir: string,
81
+ timeoutOverride?: number,
82
+ ): Promise<GateCheckResult> {
83
+ const startTime = Date.now();
84
+
85
+ // Sanitize command
86
+ const { safe, reason } = sanitizeCommand(command);
87
+ if (!safe) {
88
+ return {
89
+ check_type: checkType,
90
+ status: 'fail',
91
+ command,
92
+ exit_code: -1,
93
+ stderr_summary: `Command rejected: ${reason}`,
94
+ duration_ms: 0,
95
+ timestamp: new Date().toISOString(),
96
+ };
97
+ }
98
+
99
+ const timeout = timeoutOverride ?? DEFAULT_TIMEOUTS[checkType] ?? 5 * 60 * 1000;
100
+
101
+ return new Promise<GateCheckResult>((resolve) => {
102
+ const proc = exec(command, {
103
+ cwd: projectDir,
104
+ timeout,
105
+ maxBuffer: MAX_OUTPUT_SIZE,
106
+ env: {
107
+ ...process.env,
108
+ NODE_ENV: 'test',
109
+ CI: 'true',
110
+ },
111
+ }, (error, _stdout, stderr) => {
112
+ const duration = Date.now() - startTime;
113
+ const exitCode = error ? (error as NodeJS.ErrnoException & { code?: number }).code ?? 1 : 0;
114
+
115
+ // Truncate output for summary
116
+ const stderrSummary = stderr
117
+ ? stderr.slice(0, 2000) + (stderr.length > 2000 ? '\n... (truncated)' : '')
118
+ : undefined;
119
+
120
+ resolve({
121
+ check_type: checkType,
122
+ status: exitCode === 0 ? 'pass' : 'fail',
123
+ command,
124
+ exit_code: typeof exitCode === 'number' ? exitCode : 1,
125
+ stdout_artifact: undefined, // Filled by storeCheckResults if needed
126
+ stderr_summary: stderrSummary,
127
+ duration_ms: duration,
128
+ timestamp: new Date().toISOString(),
129
+ });
130
+ });
131
+
132
+ // Safety: kill after timeout (backup for exec timeout)
133
+ setTimeout(() => {
134
+ try { proc.kill('SIGTERM'); } catch { /* already dead */ }
135
+ }, timeout + 5000);
136
+ });
137
+ }
138
+
139
+ /** Run all applicable checks based on resolved commands */
140
+ export async function runAllChecks(
141
+ resolvedCommands: ResolvedCommands,
142
+ projectDir: string,
143
+ ): Promise<GateCheckResult[]> {
144
+ const results: GateCheckResult[] = [];
145
+ const checkMap: [GateCheckType, string | undefined][] = [
146
+ ['build', resolvedCommands.build],
147
+ ['test', resolvedCommands.test],
148
+ ['lint', resolvedCommands.lint],
149
+ ['typecheck', resolvedCommands.typecheck],
150
+ ['migration', resolvedCommands.migrations],
151
+ ];
152
+
153
+ for (const [checkType, command] of checkMap) {
154
+ if (!command) {
155
+ results.push({
156
+ check_type: checkType,
157
+ status: 'skip',
158
+ command: '',
159
+ exit_code: 0,
160
+ duration_ms: 0,
161
+ timestamp: new Date().toISOString(),
162
+ });
163
+ continue;
164
+ }
165
+
166
+ const result = await runCheck(checkType, command, projectDir);
167
+ results.push(result);
168
+ }
169
+
170
+ return results;
171
+ }
172
+
173
+ /** Store check results as artifacts */
174
+ export function storeCheckResults(
175
+ results: GateCheckResult[],
176
+ artifactManager: ArtifactManager,
177
+ phase: PipelinePhase,
178
+ ): ArtifactEntry[] {
179
+ const artifacts: ArtifactEntry[] = [];
180
+
181
+ for (const result of results) {
182
+ if (result.status === 'skip') continue;
183
+
184
+ // Only store meaningful output
185
+ const content = JSON.stringify(result, null, 2);
186
+ if (content.length > 100) {
187
+ const entry = artifactManager.createAndStoreJson(
188
+ mapCheckTypeToArtifactType(result.check_type),
189
+ result,
190
+ phase,
191
+ );
192
+ artifacts.push(entry);
193
+ }
194
+ }
195
+
196
+ return artifacts;
197
+ }
198
+
199
+ function mapCheckTypeToArtifactType(
200
+ checkType: GateCheckType,
201
+ ): 'build_check' | 'test_check' | 'lint_check' | 'typecheck_check' | 'placeholder_scan' {
202
+ switch (checkType) {
203
+ case 'build': return 'build_check';
204
+ case 'test': return 'test_check';
205
+ case 'lint': return 'lint_check';
206
+ case 'typecheck': return 'typecheck_check';
207
+ case 'placeholder_scan': return 'placeholder_scan';
208
+ default: return 'build_check';
209
+ }
210
+ }
211
+
212
+ // ─── Placeholder Scanner (P2-2) ──────────────────────────
213
+
214
+ /** Scan project for placeholder/TODO/mock content */
215
+ export function runPlaceholderScan(
216
+ projectDir: string,
217
+ allowlistPath?: string,
218
+ ): GateCheckResult {
219
+ const startTime = Date.now();
220
+ const findings: string[] = [];
221
+
222
+ // Load allowlist if present
223
+ const allowlist = loadAllowlist(
224
+ allowlistPath ?? join(projectDir, '.popeye-placeholder-allowlist'),
225
+ );
226
+
227
+ // Scan source directories
228
+ const scanDirs = ['src', 'app', 'pages', 'components', 'lib', 'server', 'api'];
229
+
230
+ for (const dir of scanDirs) {
231
+ const fullDir = join(projectDir, dir);
232
+ if (!existsSync(fullDir)) continue;
233
+ scanDirForPlaceholders(fullDir, projectDir, allowlist, findings);
234
+ }
235
+
236
+ const duration = Date.now() - startTime;
237
+
238
+ return {
239
+ check_type: 'placeholder_scan',
240
+ status: findings.length > 0 ? 'fail' : 'pass',
241
+ command: 'placeholder-scan',
242
+ exit_code: findings.length > 0 ? 1 : 0,
243
+ stderr_summary: findings.length > 0
244
+ ? `Found ${findings.length} placeholder(s):\n${findings.slice(0, 20).join('\n')}`
245
+ : undefined,
246
+ duration_ms: duration,
247
+ timestamp: new Date().toISOString(),
248
+ };
249
+ }
250
+
251
+ function scanDirForPlaceholders(
252
+ dir: string,
253
+ projectDir: string,
254
+ allowlist: Set<string>,
255
+ findings: string[],
256
+ ): void {
257
+ const codeExts = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs']);
258
+
259
+ try {
260
+ const entries = readdirSync(dir, { withFileTypes: true });
261
+ for (const entry of entries) {
262
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
263
+
264
+ const fullPath = join(dir, entry.name);
265
+
266
+ if (entry.isDirectory()) {
267
+ scanDirForPlaceholders(fullPath, projectDir, allowlist, findings);
268
+ } else if (codeExts.has(extname(entry.name))) {
269
+ const relativePath = fullPath.replace(projectDir + '/', '');
270
+ if (allowlist.has(relativePath)) continue;
271
+
272
+ try {
273
+ const content = readFileSync(fullPath, 'utf-8');
274
+ const lines = content.split('\n');
275
+
276
+ for (let i = 0; i < lines.length; i++) {
277
+ for (const pattern of PLACEHOLDER_PATTERNS) {
278
+ if (pattern.test(lines[i])) {
279
+ findings.push(`${relativePath}:${i + 1}: ${lines[i].trim().slice(0, 80)}`);
280
+ break; // One finding per line
281
+ }
282
+ }
283
+ }
284
+ } catch {
285
+ // Skip unreadable files
286
+ }
287
+ }
288
+ }
289
+ } catch {
290
+ // Skip unreadable directories
291
+ }
292
+ }
293
+
294
+ function loadAllowlist(path: string): Set<string> {
295
+ if (!existsSync(path)) return new Set();
296
+ try {
297
+ const content = readFileSync(path, 'utf-8');
298
+ return new Set(
299
+ content.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')),
300
+ );
301
+ } catch {
302
+ return new Set();
303
+ }
304
+ }
305
+
306
+ // ─── Start Check (v1.1 Gap #5) ──────────────────────────
307
+
308
+ /**
309
+ * Attempt to start the application and verify it does not crash immediately.
310
+ * Optionally checks a health endpoint if a port is detected.
311
+ *
312
+ * Args:
313
+ * startCommand: The command to start the app (e.g., "npm run start").
314
+ * projectDir: Project root directory.
315
+ * options: Optional port, health path, and timeout.
316
+ *
317
+ * Returns:
318
+ * GateCheckResult with pass/fail status.
319
+ */
320
+ export async function runStartCheck(
321
+ startCommand: string,
322
+ projectDir: string,
323
+ options?: { port?: number; healthPath?: string; timeoutMs?: number },
324
+ ): Promise<GateCheckResult> {
325
+ const startTime = Date.now();
326
+ const timeout = options?.timeoutMs ?? 15000;
327
+
328
+ // Sanitize command
329
+ const { safe, reason } = sanitizeCommand(startCommand);
330
+ if (!safe) {
331
+ return {
332
+ check_type: 'start',
333
+ status: 'fail',
334
+ command: startCommand,
335
+ exit_code: -1,
336
+ stderr_summary: `Command rejected: ${reason}`,
337
+ duration_ms: 0,
338
+ timestamp: new Date().toISOString(),
339
+ };
340
+ }
341
+
342
+ return new Promise<GateCheckResult>((resolve) => {
343
+ let stderr = '';
344
+ let resolved = false;
345
+
346
+ const proc = exec(startCommand, {
347
+ cwd: projectDir,
348
+ timeout: timeout + 5000,
349
+ maxBuffer: MAX_OUTPUT_SIZE,
350
+ env: { ...process.env, NODE_ENV: 'production' },
351
+ }, (error, _stdout, stderrOutput) => {
352
+ if (resolved) return;
353
+ resolved = true;
354
+
355
+ const duration = Date.now() - startTime;
356
+ stderr = stderrOutput ?? '';
357
+
358
+ // Process exited — if it exited within timeout, it crashed
359
+ resolve({
360
+ check_type: 'start',
361
+ status: 'fail',
362
+ command: startCommand,
363
+ exit_code: error ? (typeof (error as NodeJS.ErrnoException & { code?: number }).code === 'number'
364
+ ? (error as NodeJS.ErrnoException & { code?: number }).code!
365
+ : 1) : 0,
366
+ stderr_summary: stderr ? stderr.slice(0, 2000) : 'Process exited prematurely',
367
+ duration_ms: duration,
368
+ timestamp: new Date().toISOString(),
369
+ });
370
+ });
371
+
372
+ // If process survives for the timeout period, consider it passing
373
+ setTimeout(() => {
374
+ if (resolved) return;
375
+ resolved = true;
376
+
377
+ // Kill the process
378
+ try { proc.kill('SIGTERM'); } catch { /* already dead */ }
379
+
380
+ const duration = Date.now() - startTime;
381
+ resolve({
382
+ check_type: 'start',
383
+ status: 'pass',
384
+ command: startCommand,
385
+ exit_code: 0,
386
+ stderr_summary: stderr ? stderr.slice(0, 500) : undefined,
387
+ duration_ms: duration,
388
+ timestamp: new Date().toISOString(),
389
+ });
390
+ }, timeout);
391
+ });
392
+ }
393
+
394
+ // ─── Env Check (v1.1 Gap #5) ────────────────────────────
395
+
396
+ /**
397
+ * Validate that required environment variables exist.
398
+ * Reads .env.example for required var names and checks .env has them set.
399
+ *
400
+ * Args:
401
+ * projectDir: Project root directory.
402
+ * _snapshot: Repo snapshot (for future use).
403
+ *
404
+ * Returns:
405
+ * GateCheckResult with pass/fail status.
406
+ */
407
+ export function runEnvCheck(
408
+ projectDir: string,
409
+ _snapshot?: RepoSnapshot,
410
+ ): GateCheckResult {
411
+ const startTime = Date.now();
412
+ const examplePath = join(projectDir, '.env.example');
413
+ const envPath = join(projectDir, '.env');
414
+ const missingVars: string[] = [];
415
+ const emptyVars: string[] = [];
416
+
417
+ // If no .env.example, skip check
418
+ if (!existsSync(examplePath)) {
419
+ return {
420
+ check_type: 'env_check',
421
+ status: 'pass',
422
+ command: 'env-check',
423
+ exit_code: 0,
424
+ stderr_summary: 'No .env.example found — skipping env validation',
425
+ duration_ms: Date.now() - startTime,
426
+ timestamp: new Date().toISOString(),
427
+ };
428
+ }
429
+
430
+ // Parse .env.example for required var names
431
+ const exampleContent = readFileSync(examplePath, 'utf-8');
432
+ const requiredVars = parseEnvVarNames(exampleContent);
433
+
434
+ // Check .env exists
435
+ if (!existsSync(envPath)) {
436
+ return {
437
+ check_type: 'env_check',
438
+ status: 'fail',
439
+ command: 'env-check',
440
+ exit_code: 1,
441
+ stderr_summary: `.env file not found. Required vars from .env.example: ${requiredVars.join(', ')}`,
442
+ duration_ms: Date.now() - startTime,
443
+ timestamp: new Date().toISOString(),
444
+ };
445
+ }
446
+
447
+ // Parse .env and check all required vars are present and non-empty
448
+ const envContent = readFileSync(envPath, 'utf-8');
449
+ const envVars = parseEnvVarValues(envContent);
450
+
451
+ for (const varName of requiredVars) {
452
+ if (!(varName in envVars)) {
453
+ missingVars.push(varName);
454
+ } else if (!envVars[varName]) {
455
+ emptyVars.push(varName);
456
+ }
457
+ }
458
+
459
+ const duration = Date.now() - startTime;
460
+ const hasFail = missingVars.length > 0;
461
+ const summaryParts: string[] = [];
462
+
463
+ if (missingVars.length > 0) {
464
+ summaryParts.push(`Missing vars: ${missingVars.join(', ')}`);
465
+ }
466
+ if (emptyVars.length > 0) {
467
+ summaryParts.push(`Empty vars (warning): ${emptyVars.join(', ')}`);
468
+ }
469
+
470
+ return {
471
+ check_type: 'env_check',
472
+ status: hasFail ? 'fail' : 'pass',
473
+ command: 'env-check',
474
+ exit_code: hasFail ? 1 : 0,
475
+ stderr_summary: summaryParts.length > 0 ? summaryParts.join('; ') : undefined,
476
+ duration_ms: duration,
477
+ timestamp: new Date().toISOString(),
478
+ };
479
+ }
480
+
481
+ /** Parse env var names from .env.example (lines like KEY=value or KEY=) */
482
+ function parseEnvVarNames(content: string): string[] {
483
+ return content
484
+ .split('\n')
485
+ .map((line) => line.trim())
486
+ .filter((line) => line && !line.startsWith('#'))
487
+ .map((line) => line.split('=')[0].trim())
488
+ .filter((name) => name.length > 0);
489
+ }
490
+
491
+ /** Parse env vars into key-value map from .env content */
492
+ function parseEnvVarValues(content: string): Record<string, string> {
493
+ const vars: Record<string, string> = {};
494
+ for (const line of content.split('\n')) {
495
+ const trimmed = line.trim();
496
+ if (!trimmed || trimmed.startsWith('#')) continue;
497
+ const eqIndex = trimmed.indexOf('=');
498
+ if (eqIndex === -1) continue;
499
+ const key = trimmed.slice(0, eqIndex).trim();
500
+ const value = trimmed.slice(eqIndex + 1).trim().replace(/^["']|["']$/g, '');
501
+ vars[key] = value;
502
+ }
503
+ return vars;
504
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Command Resolver — detects project-type-specific build/test/lint/typecheck
3
+ * commands from a RepoSnapshot. Used by CheckRunner and ProductionGate.
4
+ */
5
+
6
+ import type { RepoSnapshot, ResolvedCommands } from './types.js';
7
+
8
+ // ─── Project Type Detection ──────────────────────────────
9
+
10
+ export type ProjectType = 'node' | 'python' | 'mixed' | 'unknown';
11
+
12
+ export function detectProjectType(snapshot: RepoSnapshot): ProjectType {
13
+ const hasNode = snapshot.config_files.some((c) => c.type === 'package.json');
14
+ const hasPython = snapshot.config_files.some(
15
+ (c) => c.type === 'pyproject.toml' || c.type === 'requirements.txt' || c.type === 'setup.py',
16
+ );
17
+
18
+ if (hasNode && hasPython) return 'mixed';
19
+ if (hasNode) return 'node';
20
+ if (hasPython) return 'python';
21
+ return 'unknown';
22
+ }
23
+
24
+ // ─── Command Resolution ──────────────────────────────────
25
+
26
+ export function resolveCommands(
27
+ snapshot: RepoSnapshot,
28
+ overrides?: Partial<ResolvedCommands>,
29
+ ): ResolvedCommands {
30
+ const projectType = detectProjectType(snapshot);
31
+ const pm = snapshot.package_manager ?? 'npm';
32
+ const scripts = snapshot.scripts;
33
+
34
+ let resolved: ResolvedCommands;
35
+
36
+ switch (projectType) {
37
+ case 'node':
38
+ resolved = resolveNodeCommands(pm, scripts, snapshot);
39
+ break;
40
+ case 'python':
41
+ resolved = resolvePythonCommands(snapshot);
42
+ break;
43
+ case 'mixed':
44
+ // Prefer Node commands, augment with Python where Node is missing
45
+ resolved = resolveNodeCommands(pm, scripts, snapshot);
46
+ if (!resolved.test) {
47
+ const pyResolved = resolvePythonCommands(snapshot);
48
+ resolved.test = pyResolved.test;
49
+ }
50
+ break;
51
+ default:
52
+ resolved = { resolved_from: 'none' };
53
+ }
54
+
55
+ // Apply overrides
56
+ if (overrides) {
57
+ if (overrides.build) resolved.build = overrides.build;
58
+ if (overrides.test) resolved.test = overrides.test;
59
+ if (overrides.lint) resolved.lint = overrides.lint;
60
+ if (overrides.typecheck) resolved.typecheck = overrides.typecheck;
61
+ if (overrides.migrations) resolved.migrations = overrides.migrations;
62
+ if (overrides.start) resolved.start = overrides.start;
63
+ }
64
+
65
+ return resolved;
66
+ }
67
+
68
+ // ─── Node Resolution ─────────────────────────────────────
69
+
70
+ function resolveNodeCommands(
71
+ pm: string,
72
+ scripts: Record<string, string>,
73
+ snapshot: RepoSnapshot,
74
+ ): ResolvedCommands {
75
+ const run = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : `${pm} run`;
76
+ const npx = pm === 'pnpm' ? 'pnpm exec' : pm === 'yarn' ? 'yarn' : 'npx';
77
+
78
+ const resolved: ResolvedCommands = {
79
+ resolved_from: 'package.json',
80
+ };
81
+
82
+ // Build
83
+ if (scripts.build) {
84
+ resolved.build = `${run} build`;
85
+ }
86
+
87
+ // Test
88
+ if (scripts.test) {
89
+ resolved.test = `${run} test`;
90
+ } else if (snapshot.test_framework === 'vitest') {
91
+ resolved.test = `${npx} vitest run`;
92
+ } else if (snapshot.test_framework === 'jest') {
93
+ resolved.test = `${npx} jest`;
94
+ }
95
+
96
+ // Lint
97
+ if (scripts.lint) {
98
+ resolved.lint = `${run} lint`;
99
+ }
100
+
101
+ // Typecheck
102
+ if (scripts.typecheck) {
103
+ resolved.typecheck = `${run} typecheck`;
104
+ } else if (snapshot.languages_detected.includes('typescript')) {
105
+ resolved.typecheck = `${npx} tsc --noEmit`;
106
+ }
107
+
108
+ // Migrations
109
+ const hasPrisma = snapshot.config_files.some(
110
+ (c) => c.type === 'prisma/schema.prisma',
111
+ );
112
+ if (hasPrisma) {
113
+ resolved.migrations = `${npx} prisma migrate deploy`;
114
+ }
115
+
116
+ // Start
117
+ if (scripts.start) {
118
+ resolved.start = `${run} start`;
119
+ } else if (scripts.dev) {
120
+ resolved.start = `${run} dev`;
121
+ }
122
+
123
+ return resolved;
124
+ }
125
+
126
+ // ─── Python Resolution ───────────────────────────────────
127
+
128
+ function resolvePythonCommands(snapshot: RepoSnapshot): ResolvedCommands {
129
+ const resolved: ResolvedCommands = {
130
+ resolved_from: snapshot.config_files
131
+ .find((c) => c.type === 'pyproject.toml' || c.type === 'requirements.txt')
132
+ ?.path ?? 'python-defaults',
133
+ };
134
+
135
+ // Test
136
+ if (snapshot.test_framework === 'pytest') {
137
+ resolved.test = 'pytest tests/';
138
+ } else {
139
+ resolved.test = 'pytest tests/'; // default for Python
140
+ }
141
+
142
+ // Lint
143
+ const hasPyproject = snapshot.config_files.some((c) => c.type === 'pyproject.toml');
144
+ if (hasPyproject) {
145
+ resolved.lint = 'ruff check .';
146
+ } else {
147
+ resolved.lint = 'flake8 src/';
148
+ }
149
+
150
+ // Typecheck
151
+ if (snapshot.languages_detected.includes('python')) {
152
+ resolved.typecheck = 'mypy src/';
153
+ }
154
+
155
+ // Build
156
+ resolved.build = 'python -m build';
157
+
158
+ // Migrations
159
+ const hasAlembic = snapshot.config_files.some((c) => c.type === 'alembic.ini');
160
+ if (hasAlembic) {
161
+ resolved.migrations = 'alembic upgrade head';
162
+ }
163
+
164
+ // Start
165
+ resolved.start = 'uvicorn main:app --host 0.0.0.0 --port 8000';
166
+
167
+ return resolved;
168
+ }