@ttfw/envoi 1.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 (283) hide show
  1. package/README.md +238 -0
  2. package/dist/commands/app.d.ts +2 -0
  3. package/dist/commands/app.d.ts.map +1 -0
  4. package/dist/commands/app.js +31 -0
  5. package/dist/commands/app.js.map +1 -0
  6. package/dist/commands/autonomy.d.ts +6 -0
  7. package/dist/commands/autonomy.d.ts.map +1 -0
  8. package/dist/commands/autonomy.js +89 -0
  9. package/dist/commands/autonomy.js.map +1 -0
  10. package/dist/commands/builder.d.ts +13 -0
  11. package/dist/commands/builder.d.ts.map +1 -0
  12. package/dist/commands/builder.js +142 -0
  13. package/dist/commands/builder.js.map +1 -0
  14. package/dist/commands/idea.d.ts +12 -0
  15. package/dist/commands/idea.d.ts.map +1 -0
  16. package/dist/commands/idea.js +79 -0
  17. package/dist/commands/idea.js.map +1 -0
  18. package/dist/commands/init.d.ts +18 -0
  19. package/dist/commands/init.d.ts.map +1 -0
  20. package/dist/commands/init.js +423 -0
  21. package/dist/commands/init.js.map +1 -0
  22. package/dist/commands/mode.d.ts +13 -0
  23. package/dist/commands/mode.d.ts.map +1 -0
  24. package/dist/commands/mode.js +96 -0
  25. package/dist/commands/mode.js.map +1 -0
  26. package/dist/commands/onboard.d.ts +37 -0
  27. package/dist/commands/onboard.d.ts.map +1 -0
  28. package/dist/commands/onboard.js +743 -0
  29. package/dist/commands/onboard.js.map +1 -0
  30. package/dist/commands/pr-note.d.ts +8 -0
  31. package/dist/commands/pr-note.d.ts.map +1 -0
  32. package/dist/commands/pr-note.js +27 -0
  33. package/dist/commands/pr-note.js.map +1 -0
  34. package/dist/commands/undo.d.ts +7 -0
  35. package/dist/commands/undo.d.ts.map +1 -0
  36. package/dist/commands/undo.js +59 -0
  37. package/dist/commands/undo.js.map +1 -0
  38. package/dist/commands/update.d.ts +24 -0
  39. package/dist/commands/update.d.ts.map +1 -0
  40. package/dist/commands/update.js +248 -0
  41. package/dist/commands/update.js.map +1 -0
  42. package/dist/constants/report_codes.d.ts +29 -0
  43. package/dist/constants/report_codes.d.ts.map +1 -0
  44. package/dist/constants/report_codes.js +69 -0
  45. package/dist/constants/report_codes.js.map +1 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +675 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/lib/autonomy.d.ts +16 -0
  51. package/dist/lib/autonomy.d.ts.map +1 -0
  52. package/dist/lib/autonomy.js +38 -0
  53. package/dist/lib/autonomy.js.map +1 -0
  54. package/dist/lib/blocked.d.ts +87 -0
  55. package/dist/lib/blocked.d.ts.map +1 -0
  56. package/dist/lib/blocked.js +134 -0
  57. package/dist/lib/blocked.js.map +1 -0
  58. package/dist/lib/branding.d.ts +13 -0
  59. package/dist/lib/branding.d.ts.map +1 -0
  60. package/dist/lib/branding.js +19 -0
  61. package/dist/lib/branding.js.map +1 -0
  62. package/dist/lib/claude.d.ts +42 -0
  63. package/dist/lib/claude.d.ts.map +1 -0
  64. package/dist/lib/claude.js +291 -0
  65. package/dist/lib/claude.js.map +1 -0
  66. package/dist/lib/config.d.ts +71 -0
  67. package/dist/lib/config.d.ts.map +1 -0
  68. package/dist/lib/config.js +410 -0
  69. package/dist/lib/config.js.map +1 -0
  70. package/dist/lib/diff.d.ts +150 -0
  71. package/dist/lib/diff.d.ts.map +1 -0
  72. package/dist/lib/diff.js +257 -0
  73. package/dist/lib/diff.js.map +1 -0
  74. package/dist/lib/doctor.d.ts +67 -0
  75. package/dist/lib/doctor.d.ts.map +1 -0
  76. package/dist/lib/doctor.js +211 -0
  77. package/dist/lib/doctor.js.map +1 -0
  78. package/dist/lib/fingerprint.d.ts +27 -0
  79. package/dist/lib/fingerprint.d.ts.map +1 -0
  80. package/dist/lib/fingerprint.js +116 -0
  81. package/dist/lib/fingerprint.js.map +1 -0
  82. package/dist/lib/fs.d.ts +93 -0
  83. package/dist/lib/fs.d.ts.map +1 -0
  84. package/dist/lib/fs.js +179 -0
  85. package/dist/lib/fs.js.map +1 -0
  86. package/dist/lib/git.d.ts +177 -0
  87. package/dist/lib/git.d.ts.map +1 -0
  88. package/dist/lib/git.js +355 -0
  89. package/dist/lib/git.js.map +1 -0
  90. package/dist/lib/git_branching.d.ts +84 -0
  91. package/dist/lib/git_branching.d.ts.map +1 -0
  92. package/dist/lib/git_branching.js +327 -0
  93. package/dist/lib/git_branching.js.map +1 -0
  94. package/dist/lib/gitignore.d.ts +26 -0
  95. package/dist/lib/gitignore.d.ts.map +1 -0
  96. package/dist/lib/gitignore.js +119 -0
  97. package/dist/lib/gitignore.js.map +1 -0
  98. package/dist/lib/guardrails.d.ts +232 -0
  99. package/dist/lib/guardrails.d.ts.map +1 -0
  100. package/dist/lib/guardrails.js +323 -0
  101. package/dist/lib/guardrails.js.map +1 -0
  102. package/dist/lib/history.d.ts +110 -0
  103. package/dist/lib/history.d.ts.map +1 -0
  104. package/dist/lib/history.js +236 -0
  105. package/dist/lib/history.js.map +1 -0
  106. package/dist/lib/index.d.ts +29 -0
  107. package/dist/lib/index.d.ts.map +1 -0
  108. package/dist/lib/index.js +29 -0
  109. package/dist/lib/index.js.map +1 -0
  110. package/dist/lib/json-extract.d.ts +42 -0
  111. package/dist/lib/json-extract.d.ts.map +1 -0
  112. package/dist/lib/json-extract.js +201 -0
  113. package/dist/lib/json-extract.js.map +1 -0
  114. package/dist/lib/judge.d.ts +237 -0
  115. package/dist/lib/judge.d.ts.map +1 -0
  116. package/dist/lib/judge.js +501 -0
  117. package/dist/lib/judge.js.map +1 -0
  118. package/dist/lib/lock.d.ts +79 -0
  119. package/dist/lib/lock.d.ts.map +1 -0
  120. package/dist/lib/lock.js +254 -0
  121. package/dist/lib/lock.js.map +1 -0
  122. package/dist/lib/migration.d.ts +9 -0
  123. package/dist/lib/migration.d.ts.map +1 -0
  124. package/dist/lib/migration.js +74 -0
  125. package/dist/lib/migration.js.map +1 -0
  126. package/dist/lib/paths.d.ts +18 -0
  127. package/dist/lib/paths.d.ts.map +1 -0
  128. package/dist/lib/paths.js +27 -0
  129. package/dist/lib/paths.js.map +1 -0
  130. package/dist/lib/preflight.d.ts +33 -0
  131. package/dist/lib/preflight.d.ts.map +1 -0
  132. package/dist/lib/preflight.js +177 -0
  133. package/dist/lib/preflight.js.map +1 -0
  134. package/dist/lib/prompt_budget.d.ts +18 -0
  135. package/dist/lib/prompt_budget.d.ts.map +1 -0
  136. package/dist/lib/prompt_budget.js +36 -0
  137. package/dist/lib/prompt_budget.js.map +1 -0
  138. package/dist/lib/report.d.ts +102 -0
  139. package/dist/lib/report.d.ts.map +1 -0
  140. package/dist/lib/report.js +347 -0
  141. package/dist/lib/report.js.map +1 -0
  142. package/dist/lib/reviewer-flow.d.ts +80 -0
  143. package/dist/lib/reviewer-flow.d.ts.map +1 -0
  144. package/dist/lib/reviewer-flow.js +138 -0
  145. package/dist/lib/reviewer-flow.js.map +1 -0
  146. package/dist/lib/reviewer.d.ts +53 -0
  147. package/dist/lib/reviewer.d.ts.map +1 -0
  148. package/dist/lib/reviewer.js +199 -0
  149. package/dist/lib/reviewer.js.map +1 -0
  150. package/dist/lib/risk.d.ts +127 -0
  151. package/dist/lib/risk.d.ts.map +1 -0
  152. package/dist/lib/risk.js +192 -0
  153. package/dist/lib/risk.js.map +1 -0
  154. package/dist/lib/rollback.d.ts +143 -0
  155. package/dist/lib/rollback.d.ts.map +1 -0
  156. package/dist/lib/rollback.js +244 -0
  157. package/dist/lib/rollback.js.map +1 -0
  158. package/dist/lib/schema.d.ts +47 -0
  159. package/dist/lib/schema.d.ts.map +1 -0
  160. package/dist/lib/schema.js +91 -0
  161. package/dist/lib/schema.js.map +1 -0
  162. package/dist/lib/scope.d.ts +89 -0
  163. package/dist/lib/scope.d.ts.map +1 -0
  164. package/dist/lib/scope.js +135 -0
  165. package/dist/lib/scope.js.map +1 -0
  166. package/dist/lib/self_update.d.ts +13 -0
  167. package/dist/lib/self_update.d.ts.map +1 -0
  168. package/dist/lib/self_update.js +172 -0
  169. package/dist/lib/self_update.js.map +1 -0
  170. package/dist/lib/state.d.ts +143 -0
  171. package/dist/lib/state.d.ts.map +1 -0
  172. package/dist/lib/state.js +258 -0
  173. package/dist/lib/state.js.map +1 -0
  174. package/dist/lib/tick.d.ts +310 -0
  175. package/dist/lib/tick.d.ts.map +1 -0
  176. package/dist/lib/tick.js +424 -0
  177. package/dist/lib/tick.js.map +1 -0
  178. package/dist/lib/transport.d.ts +145 -0
  179. package/dist/lib/transport.d.ts.map +1 -0
  180. package/dist/lib/transport.js +237 -0
  181. package/dist/lib/transport.js.map +1 -0
  182. package/dist/lib/verdict_labels.d.ts +5 -0
  183. package/dist/lib/verdict_labels.d.ts.map +1 -0
  184. package/dist/lib/verdict_labels.js +25 -0
  185. package/dist/lib/verdict_labels.js.map +1 -0
  186. package/dist/lib/verify-safety.d.ts +63 -0
  187. package/dist/lib/verify-safety.d.ts.map +1 -0
  188. package/dist/lib/verify-safety.js +123 -0
  189. package/dist/lib/verify-safety.js.map +1 -0
  190. package/dist/lib/verify.d.ts +139 -0
  191. package/dist/lib/verify.d.ts.map +1 -0
  192. package/dist/lib/verify.js +311 -0
  193. package/dist/lib/verify.js.map +1 -0
  194. package/dist/lib/workspace_state.d.ts +79 -0
  195. package/dist/lib/workspace_state.d.ts.map +1 -0
  196. package/dist/lib/workspace_state.js +283 -0
  197. package/dist/lib/workspace_state.js.map +1 -0
  198. package/dist/runner/builder.d.ts +58 -0
  199. package/dist/runner/builder.d.ts.map +1 -0
  200. package/dist/runner/builder.js +775 -0
  201. package/dist/runner/builder.js.map +1 -0
  202. package/dist/runner/builder_parse.d.ts +37 -0
  203. package/dist/runner/builder_parse.d.ts.map +1 -0
  204. package/dist/runner/builder_parse.js +76 -0
  205. package/dist/runner/builder_parse.js.map +1 -0
  206. package/dist/runner/index.d.ts +9 -0
  207. package/dist/runner/index.d.ts.map +1 -0
  208. package/dist/runner/index.js +7 -0
  209. package/dist/runner/index.js.map +1 -0
  210. package/dist/runner/loop.d.ts +51 -0
  211. package/dist/runner/loop.d.ts.map +1 -0
  212. package/dist/runner/loop.js +221 -0
  213. package/dist/runner/loop.js.map +1 -0
  214. package/dist/runner/orchestrator.d.ts +67 -0
  215. package/dist/runner/orchestrator.d.ts.map +1 -0
  216. package/dist/runner/orchestrator.js +376 -0
  217. package/dist/runner/orchestrator.js.map +1 -0
  218. package/dist/runner/tick.d.ts +10 -0
  219. package/dist/runner/tick.d.ts.map +1 -0
  220. package/dist/runner/tick.js +1639 -0
  221. package/dist/runner/tick.js.map +1 -0
  222. package/dist/types/blocked.d.ts +52 -0
  223. package/dist/types/blocked.d.ts.map +1 -0
  224. package/dist/types/blocked.js +8 -0
  225. package/dist/types/blocked.js.map +1 -0
  226. package/dist/types/builder.d.ts +25 -0
  227. package/dist/types/builder.d.ts.map +1 -0
  228. package/dist/types/builder.js +7 -0
  229. package/dist/types/builder.js.map +1 -0
  230. package/dist/types/claude.d.ts +86 -0
  231. package/dist/types/claude.d.ts.map +1 -0
  232. package/dist/types/claude.js +48 -0
  233. package/dist/types/claude.js.map +1 -0
  234. package/dist/types/config.d.ts +384 -0
  235. package/dist/types/config.d.ts.map +1 -0
  236. package/dist/types/config.js +7 -0
  237. package/dist/types/config.js.map +1 -0
  238. package/dist/types/index.d.ts +18 -0
  239. package/dist/types/index.d.ts.map +1 -0
  240. package/dist/types/index.js +8 -0
  241. package/dist/types/index.js.map +1 -0
  242. package/dist/types/lock.d.ts +21 -0
  243. package/dist/types/lock.d.ts.map +1 -0
  244. package/dist/types/lock.js +8 -0
  245. package/dist/types/lock.js.map +1 -0
  246. package/dist/types/preflight.d.ts +49 -0
  247. package/dist/types/preflight.d.ts.map +1 -0
  248. package/dist/types/preflight.js +8 -0
  249. package/dist/types/preflight.js.map +1 -0
  250. package/dist/types/report.d.ts +161 -0
  251. package/dist/types/report.d.ts.map +1 -0
  252. package/dist/types/report.js +8 -0
  253. package/dist/types/report.js.map +1 -0
  254. package/dist/types/reviewer.d.ts +66 -0
  255. package/dist/types/reviewer.d.ts.map +1 -0
  256. package/dist/types/reviewer.js +5 -0
  257. package/dist/types/reviewer.js.map +1 -0
  258. package/dist/types/state.d.ts +124 -0
  259. package/dist/types/state.d.ts.map +1 -0
  260. package/dist/types/state.js +20 -0
  261. package/dist/types/state.js.map +1 -0
  262. package/dist/types/task.d.ts +117 -0
  263. package/dist/types/task.d.ts.map +1 -0
  264. package/dist/types/task.js +7 -0
  265. package/dist/types/task.js.map +1 -0
  266. package/dist/types/workspace_state.d.ts +125 -0
  267. package/dist/types/workspace_state.d.ts.map +1 -0
  268. package/dist/types/workspace_state.js +10 -0
  269. package/dist/types/workspace_state.js.map +1 -0
  270. package/envoi.config.json +191 -0
  271. package/package.json +52 -0
  272. package/relais/prompts/.gitkeep +0 -0
  273. package/relais/prompts/builder.system.txt +13 -0
  274. package/relais/prompts/builder.user.txt +15 -0
  275. package/relais/prompts/orchestrator.system.txt +37 -0
  276. package/relais/prompts/orchestrator.user.txt +34 -0
  277. package/relais/prompts/reviewer.system.txt +33 -0
  278. package/relais/prompts/reviewer.user.txt +35 -0
  279. package/relais/schemas/.gitkeep +0 -0
  280. package/relais/schemas/builder_result.schema.json +29 -0
  281. package/relais/schemas/report.schema.json +195 -0
  282. package/relais/schemas/reviewer_result.schema.json +70 -0
  283. package/relais/schemas/task.schema.json +155 -0
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Verification command execution module.
3
+ *
4
+ * Provides functions to execute verification commands safely using argv arrays
5
+ * (no shell) with parameter interpolation and timeout handling.
6
+ */
7
+ import { spawn } from 'node:child_process';
8
+ /**
9
+ * Validates a single parameter value against security constraints.
10
+ *
11
+ * Checks for:
12
+ * - Length exceeding max_param_len
13
+ * - Whitespace (if reject_whitespace_in_params is true)
14
+ * - Path traversal '..' (if reject_dotdot is true)
15
+ * - Shell metacharacters (using reject_metachars_regex)
16
+ *
17
+ * @param paramName - Name of the parameter being validated
18
+ * @param value - The parameter value to validate (converted to string)
19
+ * @param config - Verification configuration with validation rules
20
+ * @returns ParamValidationError if validation fails, null if valid
21
+ */
22
+ export function validateParam(paramName, value, config) {
23
+ // Convert value to string for validation
24
+ const strValue = value === null ? '' : String(value);
25
+ // Check length
26
+ if (strValue.length > config.max_param_len) {
27
+ return {
28
+ param_name: paramName,
29
+ value: strValue,
30
+ reason: 'too_long',
31
+ };
32
+ }
33
+ // Check for whitespace if enabled
34
+ if (config.reject_whitespace_in_params && /\s/.test(strValue)) {
35
+ return {
36
+ param_name: paramName,
37
+ value: strValue,
38
+ reason: 'whitespace',
39
+ };
40
+ }
41
+ // Check for path traversal '..' if enabled
42
+ if (config.reject_dotdot) {
43
+ // Match: ../, ^..$, /..$
44
+ if (/\.\.\/|^\.\.$|\/\.\.$/.test(strValue)) {
45
+ return {
46
+ param_name: paramName,
47
+ value: strValue,
48
+ reason: 'dotdot',
49
+ };
50
+ }
51
+ }
52
+ // Check for shell metacharacters
53
+ try {
54
+ const metacharRegex = new RegExp(config.reject_metachars_regex);
55
+ if (metacharRegex.test(strValue)) {
56
+ return {
57
+ param_name: paramName,
58
+ value: strValue,
59
+ reason: 'metachar',
60
+ };
61
+ }
62
+ }
63
+ catch (error) {
64
+ // If regex is invalid, treat as validation failure
65
+ // This shouldn't happen with valid config, but be defensive
66
+ return {
67
+ param_name: paramName,
68
+ value: strValue,
69
+ reason: 'metachar',
70
+ };
71
+ }
72
+ return null;
73
+ }
74
+ /**
75
+ * Validates all verification parameters for a task.
76
+ *
77
+ * Validates all parameters in task.verification.params against the security
78
+ * constraints defined in the verification config.
79
+ *
80
+ * @param task - Task containing verification configuration and parameters
81
+ * @param config - Verification configuration with validation rules
82
+ * @returns ParamValidationResult with ok=true if all params valid, errors otherwise
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * const result = validateVerificationParams(task, config.verification);
87
+ * if (!result.ok) {
88
+ * // Handle validation errors
89
+ * console.error('Invalid parameters:', result.errors);
90
+ * }
91
+ * ```
92
+ */
93
+ export function validateVerificationParams(task, config) {
94
+ const errors = [];
95
+ const verification = task.verification;
96
+ const params = verification.params ?? {};
97
+ // Validate all parameters for all templates
98
+ for (const [templateId, templateParams] of Object.entries(params)) {
99
+ for (const [paramName, paramValue] of Object.entries(templateParams)) {
100
+ const error = validateParam(paramName, paramValue, config);
101
+ if (error) {
102
+ // Prefix param name with template ID for clarity
103
+ errors.push({
104
+ ...error,
105
+ param_name: `${templateId}.${error.param_name}`,
106
+ });
107
+ }
108
+ }
109
+ }
110
+ return {
111
+ ok: errors.length === 0,
112
+ errors,
113
+ };
114
+ }
115
+ /**
116
+ * Interpolates parameter placeholders in command arguments.
117
+ *
118
+ * Replaces {{param_name}} placeholders with values from the params object.
119
+ *
120
+ * @param args - Array of command arguments that may contain {{param}} placeholders
121
+ * @param params - Object mapping parameter names to values
122
+ * @returns Array of arguments with placeholders replaced
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * interpolateArgs(['--filter', '{{pkg}}'], { pkg: 'my-package' })
127
+ * // Returns: ['--filter', 'my-package']
128
+ * ```
129
+ */
130
+ export function interpolateArgs(args, params) {
131
+ return args.map((arg) => {
132
+ // Replace {{param_name}} placeholders
133
+ return arg.replace(/\{\{(\w+)\}\}/g, (match, paramName) => {
134
+ const value = params[paramName];
135
+ if (value === undefined) {
136
+ throw new Error(`Missing parameter '${paramName}' for template argument '${arg}'`);
137
+ }
138
+ // Convert to string, handling null explicitly
139
+ return value === null ? '' : String(value);
140
+ });
141
+ });
142
+ }
143
+ /**
144
+ * Executes a single verification command.
145
+ *
146
+ * Runs the command using spawn with shell:false for security. Captures stdout
147
+ * and stderr, enforces timeout, and returns execution results.
148
+ *
149
+ * @param template - Verification template containing cmd and args
150
+ * @param params - Parameters for template argument interpolation
151
+ * @param timeoutMs - Timeout in milliseconds (0 means no timeout)
152
+ * @returns Promise resolving to VerificationRun result
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * const result = await executeVerification(
157
+ * { id: 'lint', cmd: 'pnpm', args: ['-w', 'lint'] },
158
+ * {},
159
+ * 90000
160
+ * );
161
+ * ```
162
+ */
163
+ export async function executeVerification(template, params, timeoutMs) {
164
+ const startTime = Date.now();
165
+ let stdout = '';
166
+ let stderr = '';
167
+ // Interpolate parameters in args
168
+ const args = interpolateArgs(template.args, params);
169
+ return new Promise((resolve, reject) => {
170
+ let timeoutId = null;
171
+ let child = null;
172
+ // Set up timeout handler
173
+ const handleTimeout = () => {
174
+ if (child) {
175
+ child.kill('SIGTERM');
176
+ // Give it a moment to terminate gracefully, then force kill
177
+ setTimeout(() => {
178
+ if (child && !child.killed) {
179
+ child.kill('SIGKILL');
180
+ }
181
+ }, 1000);
182
+ }
183
+ const durationMs = Date.now() - startTime;
184
+ resolve({
185
+ template_id: template.id,
186
+ exit_code: 124, // Standard timeout exit code
187
+ stdout,
188
+ stderr: stderr + '\n[Command timed out]',
189
+ duration_ms: durationMs,
190
+ success: false,
191
+ });
192
+ };
193
+ // Set up timeout if specified
194
+ if (timeoutMs > 0) {
195
+ timeoutId = setTimeout(handleTimeout, timeoutMs);
196
+ }
197
+ try {
198
+ // Spawn the process with shell:false for security
199
+ child = spawn(template.cmd, args, {
200
+ shell: false,
201
+ stdio: ['pipe', 'pipe', 'pipe'],
202
+ });
203
+ // Capture stdout
204
+ child.stdout?.on('data', (data) => {
205
+ stdout += data.toString();
206
+ });
207
+ // Capture stderr
208
+ child.stderr?.on('data', (data) => {
209
+ stderr += data.toString();
210
+ });
211
+ // Handle process completion
212
+ child.on('close', (code) => {
213
+ if (timeoutId) {
214
+ clearTimeout(timeoutId);
215
+ }
216
+ const durationMs = Date.now() - startTime;
217
+ const exitCode = code ?? 0;
218
+ // If process was killed by timeout, result was already resolved
219
+ if (code === null && timeoutId) {
220
+ return;
221
+ }
222
+ resolve({
223
+ template_id: template.id,
224
+ exit_code: exitCode,
225
+ stdout,
226
+ stderr,
227
+ duration_ms: durationMs,
228
+ success: exitCode === 0,
229
+ });
230
+ });
231
+ // Handle process errors
232
+ child.on('error', (error) => {
233
+ if (timeoutId) {
234
+ clearTimeout(timeoutId);
235
+ }
236
+ const durationMs = Date.now() - startTime;
237
+ resolve({
238
+ template_id: template.id,
239
+ exit_code: 1,
240
+ stdout,
241
+ stderr: stderr + `\n[Process error: ${error.message}]`,
242
+ duration_ms: durationMs,
243
+ success: false,
244
+ });
245
+ });
246
+ }
247
+ catch (error) {
248
+ if (timeoutId) {
249
+ clearTimeout(timeoutId);
250
+ }
251
+ const durationMs = Date.now() - startTime;
252
+ resolve({
253
+ template_id: template.id,
254
+ exit_code: 1,
255
+ stdout,
256
+ stderr: stderr + `\n[Failed to spawn process: ${error instanceof Error ? error.message : String(error)}]`,
257
+ duration_ms: durationMs,
258
+ success: false,
259
+ });
260
+ }
261
+ });
262
+ }
263
+ /**
264
+ * Runs verification commands for a task.
265
+ *
266
+ * Executes fast verifications first, then slow verifications. Each verification
267
+ * uses the appropriate timeout from config. Parameters are taken from the task's
268
+ * verification.params object, keyed by template ID.
269
+ *
270
+ * @param templates - Map of template ID to VerificationTemplate
271
+ * @param task - Task containing verification configuration
272
+ * @param config - Verification configuration with timeouts
273
+ * @returns Promise resolving to array of VerificationRun results
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * const templates = new Map([
278
+ * ['lint', { id: 'lint', cmd: 'pnpm', args: ['-w', 'lint'] }]
279
+ * ]);
280
+ * const runs = await runVerifications(templates, task, config.verification);
281
+ * ```
282
+ */
283
+ export async function runVerifications(templates, task, config) {
284
+ const results = [];
285
+ const verification = task.verification;
286
+ const params = verification.params ?? {};
287
+ // Run fast verifications first
288
+ for (const templateId of verification.fast) {
289
+ const template = templates.get(templateId);
290
+ if (!template) {
291
+ throw new Error(`Verification template '${templateId}' not found in config`);
292
+ }
293
+ const templateParams = params[templateId] ?? {};
294
+ const timeoutMs = config.timeout_fast_seconds * 1000;
295
+ const result = await executeVerification(template, templateParams, timeoutMs);
296
+ results.push(result);
297
+ }
298
+ // Then run slow verifications
299
+ for (const templateId of verification.slow) {
300
+ const template = templates.get(templateId);
301
+ if (!template) {
302
+ throw new Error(`Verification template '${templateId}' not found in config`);
303
+ }
304
+ const templateParams = params[templateId] ?? {};
305
+ const timeoutMs = config.timeout_slow_seconds * 1000;
306
+ const result = await executeVerification(template, templateParams, timeoutMs);
307
+ results.push(result);
308
+ }
309
+ return results;
310
+ }
311
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/lib/verify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Workspace state persistence library.
3
+ *
4
+ * Manages reading and writing STATE.json to track milestone progress
5
+ * and budget consumption across restarts.
6
+ *
7
+ * @see docs/NEW-PLAN.md lines 48-59
8
+ */
9
+ import type { WorkspaceState, BudgetDeltas, IdeaTestabilityNeed } from '../types/workspace_state.js';
10
+ import type { PerMilestoneBudgets } from '../types/config.js';
11
+ /**
12
+ * Creates a default empty WorkspaceState.
13
+ *
14
+ * @returns Default state with null milestone_id, zero budgets, false budget_warning
15
+ */
16
+ export declare function createDefaultState(): WorkspaceState;
17
+ /**
18
+ * Reads and parses STATE.json from workspaceDir.
19
+ *
20
+ * Returns default state if file doesn't exist.
21
+ *
22
+ * @param workspaceDir - The workspace directory containing STATE.json
23
+ * @returns Parsed WorkspaceState or default if file doesn't exist
24
+ */
25
+ export declare function readWorkspaceState(workspaceDir: string): Promise<WorkspaceState>;
26
+ /**
27
+ * Writes WorkspaceState to STATE.json atomically.
28
+ *
29
+ * Uses atomic write pattern for crash safety.
30
+ *
31
+ * @param workspaceDir - The workspace directory to write STATE.json to
32
+ * @param state - The WorkspaceState to persist
33
+ */
34
+ export declare function writeWorkspaceState(workspaceDir: string, state: WorkspaceState): Promise<void>;
35
+ /**
36
+ * Ensures state has the given milestone_id.
37
+ *
38
+ * If milestone changes, resets budgets to zero.
39
+ *
40
+ * @param state - Current WorkspaceState
41
+ * @param milestoneId - Milestone ID to ensure
42
+ * @returns Updated state and whether milestone changed
43
+ */
44
+ export declare function ensureMilestone(state: WorkspaceState, milestoneId: string): {
45
+ state: WorkspaceState;
46
+ changed: boolean;
47
+ };
48
+ /**
49
+ * Applies incremental budget changes to state.
50
+ *
51
+ * Creates new state with updated budgets (immutable update).
52
+ *
53
+ * @param state - Current WorkspaceState
54
+ * @param deltas - Partial budget counts to add
55
+ * @returns New state with updated budgets
56
+ */
57
+ export declare function applyDeltas(state: WorkspaceState, deltas: BudgetDeltas): WorkspaceState;
58
+ export interface NewIdeaInput {
59
+ text: string;
60
+ source: 'interactive' | 'cli' | 'api';
61
+ target_by?: string | null;
62
+ testability_need?: IdeaTestabilityNeed;
63
+ }
64
+ /**
65
+ * Appends a new idea entry to workspace state.
66
+ */
67
+ export declare function appendIdeaEntry(state: WorkspaceState, input: NewIdeaInput): WorkspaceState;
68
+ /**
69
+ * Checks if any budget is approaching its limit.
70
+ *
71
+ * Returns true if any budget count >= max * warnAtFraction.
72
+ *
73
+ * @param state - Current WorkspaceState
74
+ * @param perMilestone - Per-milestone budget limits
75
+ * @param warnAtFraction - Fraction (0-1) at which to warn
76
+ * @returns True if any budget is at warning level
77
+ */
78
+ export declare function computeBudgetWarning(state: WorkspaceState, perMilestone: PerMilestoneBudgets, warnAtFraction: number): boolean;
79
+ //# sourceMappingURL=workspace_state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace_state.d.ts","sourceRoot":"","sources":["../../src/lib/workspace_state.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EACV,cAAc,EAEd,YAAY,EAGZ,mBAAmB,EAEpB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,cAAc,CAgBnD;AA2GD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CA4BtF;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,MAAM,GAClB;IAAE,KAAK,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAmB7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,GAAG,cAAc,CAYvF;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,GAAG,KAAK,GAAG,KAAK,CAAC;IACtC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gBAAgB,CAAC,EAAE,mBAAmB,CAAC;CACxC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,GAAG,cAAc,CAqB1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,cAAc,EACrB,YAAY,EAAE,mBAAmB,EACjC,cAAc,EAAE,MAAM,GACrB,OAAO,CAsBT"}
@@ -0,0 +1,283 @@
1
+ /**
2
+ * Workspace state persistence library.
3
+ *
4
+ * Manages reading and writing STATE.json to track milestone progress
5
+ * and budget consumption across restarts.
6
+ *
7
+ * @see docs/NEW-PLAN.md lines 48-59
8
+ */
9
+ import { join } from 'node:path';
10
+ import { atomicReadJson, atomicWriteJson, AtomicFsError } from './fs.js';
11
+ /**
12
+ * Creates a default empty WorkspaceState.
13
+ *
14
+ * @returns Default state with null milestone_id, zero budgets, false budget_warning
15
+ */
16
+ export function createDefaultState() {
17
+ return {
18
+ milestone_id: null,
19
+ budgets: {
20
+ ticks: 0,
21
+ orchestrator_calls: 0,
22
+ builder_calls: 0,
23
+ verify_runs: 0,
24
+ },
25
+ budget_warning: false,
26
+ last_run_id: null,
27
+ last_verdict: null,
28
+ idea_inbox: [],
29
+ planning_digest: null,
30
+ open_product_questions: [],
31
+ };
32
+ }
33
+ /**
34
+ * Runtime shape guard for idea status values.
35
+ */
36
+ function isIdeaStatus(value) {
37
+ return value === 'new' || value === 'triaged' || value === 'scheduled' || value === 'deferred' || value === 'done';
38
+ }
39
+ /**
40
+ * Runtime shape guard for idea testability values.
41
+ */
42
+ function isIdeaTestabilityNeed(value) {
43
+ return value === 'soon' || value === 'later' || value === 'unknown';
44
+ }
45
+ function normalizeIdeaInboxEntry(value) {
46
+ if (typeof value !== 'object' || value === null)
47
+ return null;
48
+ const candidate = value;
49
+ if (typeof candidate.id !== 'string' || candidate.id.length === 0)
50
+ return null;
51
+ if (typeof candidate.text !== 'string' || candidate.text.length === 0)
52
+ return null;
53
+ if (typeof candidate.submitted_at !== 'string' || candidate.submitted_at.length === 0)
54
+ return null;
55
+ if (candidate.source !== 'interactive' && candidate.source !== 'cli' && candidate.source !== 'api')
56
+ return null;
57
+ if (!isIdeaStatus(candidate.status))
58
+ return null;
59
+ return {
60
+ id: candidate.id,
61
+ text: candidate.text,
62
+ submitted_at: candidate.submitted_at,
63
+ source: candidate.source,
64
+ status: candidate.status,
65
+ target_by: typeof candidate.target_by === 'string' ? candidate.target_by : null,
66
+ testability_need: isIdeaTestabilityNeed(candidate.testability_need) ? candidate.testability_need : undefined,
67
+ triaged_by_task_id: typeof candidate.triaged_by_task_id === 'string' ? candidate.triaged_by_task_id : undefined,
68
+ triaged_at: typeof candidate.triaged_at === 'string' ? candidate.triaged_at : undefined,
69
+ };
70
+ }
71
+ function normalizeOpenProductQuestion(value) {
72
+ if (typeof value !== 'object' || value === null)
73
+ return null;
74
+ const candidate = value;
75
+ if (typeof candidate.id !== 'string' || candidate.id.length === 0)
76
+ return null;
77
+ if (typeof candidate.prompt !== 'string' || candidate.prompt.length === 0)
78
+ return null;
79
+ if (typeof candidate.created_at !== 'string' || candidate.created_at.length === 0)
80
+ return null;
81
+ if (typeof candidate.resolved !== 'boolean')
82
+ return null;
83
+ const choices = Array.isArray(candidate.choices)
84
+ ? candidate.choices.filter((entry) => typeof entry === 'string' && entry.length > 0)
85
+ : undefined;
86
+ return {
87
+ id: candidate.id,
88
+ prompt: candidate.prompt,
89
+ choices,
90
+ created_at: candidate.created_at,
91
+ resolved: candidate.resolved,
92
+ resolved_at: typeof candidate.resolved_at === 'string' ? candidate.resolved_at : undefined,
93
+ resolution: typeof candidate.resolution === 'string' ? candidate.resolution : undefined,
94
+ };
95
+ }
96
+ /**
97
+ * Normalizes persisted state to include optional modern planning fields while
98
+ * preserving legacy fields present in older STATE.json files.
99
+ */
100
+ function normalizeWorkspaceState(rawState) {
101
+ const source = rawState;
102
+ const ideaInbox = Array.isArray(source.idea_inbox)
103
+ ? source.idea_inbox
104
+ .map((entry) => normalizeIdeaInboxEntry(entry))
105
+ .filter((entry) => entry !== null)
106
+ : [];
107
+ const openQuestions = Array.isArray(source.open_product_questions)
108
+ ? source.open_product_questions
109
+ .map((entry) => normalizeOpenProductQuestion(entry))
110
+ .filter((entry) => entry !== null)
111
+ : [];
112
+ const planningDigestRaw = source.planning_digest;
113
+ const planningDigest = typeof planningDigestRaw === 'object' &&
114
+ planningDigestRaw !== null &&
115
+ typeof planningDigestRaw.updated_at === 'string' &&
116
+ typeof planningDigestRaw.summary === 'string'
117
+ ? {
118
+ updated_at: String(planningDigestRaw.updated_at),
119
+ summary: String(planningDigestRaw.summary),
120
+ last_task_id: typeof planningDigestRaw.last_task_id === 'string'
121
+ ? String(planningDigestRaw.last_task_id)
122
+ : undefined,
123
+ suggested_milestone: typeof planningDigestRaw.suggested_milestone === 'string'
124
+ ? String(planningDigestRaw.suggested_milestone)
125
+ : undefined,
126
+ }
127
+ : null;
128
+ return {
129
+ ...source,
130
+ idea_inbox: ideaInbox,
131
+ planning_digest: planningDigest,
132
+ open_product_questions: openQuestions,
133
+ };
134
+ }
135
+ /**
136
+ * Reads and parses STATE.json from workspaceDir.
137
+ *
138
+ * Returns default state if file doesn't exist.
139
+ *
140
+ * @param workspaceDir - The workspace directory containing STATE.json
141
+ * @returns Parsed WorkspaceState or default if file doesn't exist
142
+ */
143
+ export async function readWorkspaceState(workspaceDir) {
144
+ const filePath = join(workspaceDir, 'STATE.json');
145
+ try {
146
+ const state = await atomicReadJson(filePath);
147
+ // Validate shape matches WorkspaceState interface
148
+ // TypeScript will catch type mismatches, but we ensure required fields exist
149
+ if (typeof state === 'object' &&
150
+ state !== null &&
151
+ 'milestone_id' in state &&
152
+ 'budgets' in state &&
153
+ 'budget_warning' in state &&
154
+ 'last_run_id' in state &&
155
+ 'last_verdict' in state) {
156
+ return normalizeWorkspaceState(state);
157
+ }
158
+ // If shape doesn't match, return default
159
+ return createDefaultState();
160
+ }
161
+ catch (error) {
162
+ // If file doesn't exist or can't be read, return default state
163
+ if (error instanceof AtomicFsError) {
164
+ return createDefaultState();
165
+ }
166
+ // Re-throw unexpected errors
167
+ throw error;
168
+ }
169
+ }
170
+ /**
171
+ * Writes WorkspaceState to STATE.json atomically.
172
+ *
173
+ * Uses atomic write pattern for crash safety.
174
+ *
175
+ * @param workspaceDir - The workspace directory to write STATE.json to
176
+ * @param state - The WorkspaceState to persist
177
+ */
178
+ export async function writeWorkspaceState(workspaceDir, state) {
179
+ const filePath = join(workspaceDir, 'STATE.json');
180
+ await atomicWriteJson(filePath, state);
181
+ }
182
+ /**
183
+ * Ensures state has the given milestone_id.
184
+ *
185
+ * If milestone changes, resets budgets to zero.
186
+ *
187
+ * @param state - Current WorkspaceState
188
+ * @param milestoneId - Milestone ID to ensure
189
+ * @returns Updated state and whether milestone changed
190
+ */
191
+ export function ensureMilestone(state, milestoneId) {
192
+ if (state.milestone_id === milestoneId) {
193
+ return { state, changed: false };
194
+ }
195
+ // Milestone is different or null - create new state with reset budgets
196
+ const newState = {
197
+ ...state,
198
+ milestone_id: milestoneId,
199
+ budgets: {
200
+ ticks: 0,
201
+ orchestrator_calls: 0,
202
+ builder_calls: 0,
203
+ verify_runs: 0,
204
+ },
205
+ budget_warning: false,
206
+ };
207
+ return { state: newState, changed: true };
208
+ }
209
+ /**
210
+ * Applies incremental budget changes to state.
211
+ *
212
+ * Creates new state with updated budgets (immutable update).
213
+ *
214
+ * @param state - Current WorkspaceState
215
+ * @param deltas - Partial budget counts to add
216
+ * @returns New state with updated budgets
217
+ */
218
+ export function applyDeltas(state, deltas) {
219
+ const newBudgets = {
220
+ ticks: state.budgets.ticks + (deltas.ticks ?? 0),
221
+ orchestrator_calls: state.budgets.orchestrator_calls + (deltas.orchestrator_calls ?? 0),
222
+ builder_calls: state.budgets.builder_calls + (deltas.builder_calls ?? 0),
223
+ verify_runs: state.budgets.verify_runs + (deltas.verify_runs ?? 0),
224
+ };
225
+ return {
226
+ ...state,
227
+ budgets: newBudgets,
228
+ };
229
+ }
230
+ /**
231
+ * Appends a new idea entry to workspace state.
232
+ */
233
+ export function appendIdeaEntry(state, input) {
234
+ const trimmedText = input.text.trim();
235
+ if (!trimmedText)
236
+ return state;
237
+ const now = new Date().toISOString();
238
+ const nextId = `idea-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
239
+ const entry = {
240
+ id: nextId,
241
+ text: trimmedText,
242
+ submitted_at: now,
243
+ source: input.source,
244
+ status: 'new',
245
+ target_by: input.target_by ?? null,
246
+ testability_need: input.testability_need ?? 'unknown',
247
+ };
248
+ const inbox = [...(state.idea_inbox ?? []), entry];
249
+ return {
250
+ ...state,
251
+ idea_inbox: inbox,
252
+ };
253
+ }
254
+ /**
255
+ * Checks if any budget is approaching its limit.
256
+ *
257
+ * Returns true if any budget count >= max * warnAtFraction.
258
+ *
259
+ * @param state - Current WorkspaceState
260
+ * @param perMilestone - Per-milestone budget limits
261
+ * @param warnAtFraction - Fraction (0-1) at which to warn
262
+ * @returns True if any budget is at warning level
263
+ */
264
+ export function computeBudgetWarning(state, perMilestone, warnAtFraction) {
265
+ // Check ticks
266
+ if (state.budgets.ticks >= perMilestone.max_ticks * warnAtFraction) {
267
+ return true;
268
+ }
269
+ // Check orchestrator_calls
270
+ if (state.budgets.orchestrator_calls >= perMilestone.max_orchestrator_calls * warnAtFraction) {
271
+ return true;
272
+ }
273
+ // Check builder_calls
274
+ if (state.budgets.builder_calls >= perMilestone.max_builder_calls * warnAtFraction) {
275
+ return true;
276
+ }
277
+ // Check verify_runs
278
+ if (state.budgets.verify_runs >= perMilestone.max_verify_runs * warnAtFraction) {
279
+ return true;
280
+ }
281
+ return false;
282
+ }
283
+ //# sourceMappingURL=workspace_state.js.map