@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,743 @@
1
+ /**
2
+ * Guided onboarding flow for Envoi.
3
+ *
4
+ * Goals:
5
+ * - Ensure workspace is initialized
6
+ * - Capture project brief (PRD)
7
+ * - Configure execution mode + builder + optional reviewer
8
+ * - Validate role connectivity
9
+ * - Generate initial roadmap snapshot
10
+ * - Optionally start execution immediately
11
+ */
12
+ import { mkdir, writeFile, readFile } from 'node:fs/promises';
13
+ import { dirname, resolve, join } from 'node:path';
14
+ import { spawnSync } from 'node:child_process';
15
+ import readline from 'node:readline/promises';
16
+ import { stdin as input, stdout as output } from 'node:process';
17
+ import { atomicReadJson, atomicWriteJson } from '../lib/fs.js';
18
+ import { CONFIG_FILE_NAME, findConfigFile, loadConfig, validateConfig, ConfigError } from '../lib/config.js';
19
+ import { checkCursorAgent, checkCodexCli } from '../lib/doctor.js';
20
+ import { getGitTopLevel, isGitRepo, getHeadCommit } from '../lib/git.js';
21
+ import { CLI_NAME, PRODUCT_NAME, PACKAGE_NAME } from '../lib/branding.js';
22
+ import { createInitialState } from '../lib/state.js';
23
+ import { initCommand } from './init.js';
24
+ const NON_INTERACTIVE_START_EXAMPLES = [
25
+ `npx -y ${PACKAGE_NAME}@latest start --mode milestone --builder cursor --prd-file ./PRD.md --no-auto-run`,
26
+ `cat ./PRD.md | npx -y ${PACKAGE_NAME}@latest start --mode milestone --builder cursor --no-auto-run`,
27
+ ];
28
+ export function validateNonInteractiveOptions(options) {
29
+ const missing = [];
30
+ if (!options.mode || options.mode.trim() === '') {
31
+ missing.push('--mode <task|milestone|autonomous>');
32
+ }
33
+ if (!options.builder || options.builder.trim() === '') {
34
+ missing.push('--builder <cursor|claude_code>');
35
+ }
36
+ if (missing.length === 0)
37
+ return;
38
+ throw new Error([
39
+ 'Non-interactive onboarding requires explicit flags.',
40
+ `Missing: ${missing.join(', ')}`,
41
+ 'Run one of:',
42
+ ...NON_INTERACTIVE_START_EXAMPLES.map((example) => ` ${example}`),
43
+ ].join('\n'));
44
+ }
45
+ export function validateNonInteractivePrdCapture(prdCapture) {
46
+ const validSource = prdCapture.source === 'file' || prdCapture.source === 'stdin';
47
+ if (validSource && isNonEmptyContent(prdCapture.text))
48
+ return;
49
+ throw new Error([
50
+ 'Non-interactive onboarding requires PRD input via --prd-file or stdin.',
51
+ 'Run one of:',
52
+ ...NON_INTERACTIVE_START_EXAMPLES.map((example) => ` ${example}`),
53
+ ].join('\n'));
54
+ }
55
+ function withDefaultCursorConfig(existing) {
56
+ const defaultArgs = ['agent', '--print', '--output-format', 'text', '--workspace', '.', '--force'];
57
+ return {
58
+ driver_kind: (existing?.driver_kind ?? 'cursor_agent'),
59
+ command: existing?.command ?? 'cursor',
60
+ args: existing?.args?.length ? existing.args : defaultArgs,
61
+ timeout_seconds: existing?.timeout_seconds ?? 300,
62
+ output_file: existing?.output_file ?? 'BUILDER_RESULT.json',
63
+ };
64
+ }
65
+ function withDefaultReviewerConfig(workspaceDir, existing) {
66
+ return {
67
+ enabled: true,
68
+ engine: existing?.engine ?? 'codex',
69
+ command: existing?.command ?? 'codex',
70
+ model: existing?.model ?? 'gpt-5',
71
+ max_turns: existing?.max_turns ?? 2,
72
+ max_budget_usd: existing?.max_budget_usd ?? 0.6,
73
+ auth: {
74
+ mode: existing?.auth?.mode ?? 'auto',
75
+ ci_requires_api_key: existing?.auth?.ci_requires_api_key ?? true,
76
+ api_key_env: existing?.auth?.api_key_env ?? 'CODEX_API_KEY',
77
+ },
78
+ trigger: {
79
+ on_verify_fail: existing?.trigger?.on_verify_fail ?? true,
80
+ on_repeated_stop: existing?.trigger?.on_repeated_stop ?? true,
81
+ stop_window_ticks: existing?.trigger?.stop_window_ticks ?? 5,
82
+ max_stops_in_window: existing?.trigger?.max_stops_in_window ?? 2,
83
+ on_high_risk_paths: existing?.trigger?.on_high_risk_paths ?? true,
84
+ high_risk_globs: existing?.trigger?.high_risk_globs ?? ['src/**', 'app/**', 'packages/**', 'infra/**', '**/*.sql'],
85
+ diff_fraction_threshold: existing?.trigger?.diff_fraction_threshold ?? 0.25,
86
+ },
87
+ schema_file: existing?.schema_file ?? `${workspaceDir}/schemas/reviewer_result.schema.json`,
88
+ system_prompt_file: existing?.system_prompt_file ?? `${workspaceDir}/prompts/reviewer.system.txt`,
89
+ user_prompt_file: existing?.user_prompt_file ?? `${workspaceDir}/prompts/reviewer.user.txt`,
90
+ };
91
+ }
92
+ function isNonEmptyContent(value) {
93
+ return value.trim().length > 0;
94
+ }
95
+ export function isMeaningfulPrdText(value) {
96
+ const trimmed = value.trim();
97
+ if (trimmed.length === 0)
98
+ return false;
99
+ const compact = trimmed
100
+ .replace(/^#\s*PRD\s*$/im, '')
101
+ .replace(/\(Paste the user PRD here\. This is the source of truth\.\)/gi, '')
102
+ .trim();
103
+ return compact.length >= 30;
104
+ }
105
+ function normalizeReviewerChoice(value) {
106
+ if (!value || value.trim() === '')
107
+ return 'none';
108
+ const normalized = value.trim().toLowerCase();
109
+ if (normalized === 'none' || normalized === 'off' || normalized === 'disabled' || normalized === 'no') {
110
+ return 'none';
111
+ }
112
+ if (normalized === 'codex' || normalized === 'on' || normalized === 'enabled' || normalized === 'yes') {
113
+ return 'codex';
114
+ }
115
+ throw new Error(`Invalid reviewer value: ${value}. Must be 'none' or 'codex'.`);
116
+ }
117
+ async function ensureRepoRootCwd() {
118
+ if (isGitRepo()) {
119
+ const top = getGitTopLevel();
120
+ if (top)
121
+ process.chdir(top);
122
+ }
123
+ }
124
+ async function resolveConfigPath(configPath) {
125
+ if (configPath)
126
+ return resolve(configPath);
127
+ const found = await findConfigFile();
128
+ if (!found) {
129
+ throw new ConfigError(`Configuration file not found. Expected ${CONFIG_FILE_NAME} in current directory or parent directories.`);
130
+ }
131
+ return found;
132
+ }
133
+ async function ensureInitialized(forceInit) {
134
+ const existing = await findConfigFile();
135
+ if (existing && !forceInit)
136
+ return;
137
+ await initCommand({ force: forceInit, showNextSteps: false });
138
+ }
139
+ async function promptMultiline(label) {
140
+ const rl = readline.createInterface({ input, output });
141
+ try {
142
+ console.log(`\nPaste ${label} now. End input with a line containing only: END\n`);
143
+ const lines = [];
144
+ // eslint-disable-next-line no-constant-condition
145
+ while (true) {
146
+ const line = await rl.question('');
147
+ if (line.trim() === 'END')
148
+ break;
149
+ lines.push(line);
150
+ }
151
+ return lines.join('\n').trim();
152
+ }
153
+ finally {
154
+ rl.close();
155
+ }
156
+ }
157
+ async function promptYesNo(question, defaultValue) {
158
+ if (!input.isTTY)
159
+ return defaultValue;
160
+ const rl = readline.createInterface({ input, output });
161
+ try {
162
+ const hint = defaultValue ? 'Y/n' : 'y/N';
163
+ const answer = (await rl.question(`${question} [${hint}]: `)).trim().toLowerCase();
164
+ if (!answer)
165
+ return defaultValue;
166
+ if (answer === 'y' || answer === 'yes')
167
+ return true;
168
+ if (answer === 'n' || answer === 'no')
169
+ return false;
170
+ return defaultValue;
171
+ }
172
+ finally {
173
+ rl.close();
174
+ }
175
+ }
176
+ export const PRD_DISCOVERY_CANDIDATES = [
177
+ 'PRD.md',
178
+ 'prd.md',
179
+ 'docs/PRD.md',
180
+ 'docs/prd.md',
181
+ 'specs/PRD.md',
182
+ 'product/PRD.md',
183
+ ];
184
+ export async function discoverLocalPrdCandidates(repoRoot) {
185
+ const existing = [];
186
+ const seen = new Set();
187
+ for (const relativePath of PRD_DISCOVERY_CANDIDATES) {
188
+ const fullPath = join(repoRoot, relativePath);
189
+ try {
190
+ const content = (await readFile(fullPath, 'utf-8')).trim();
191
+ if (content.length > 0) {
192
+ const key = fullPath.toLowerCase();
193
+ if (!seen.has(key)) {
194
+ seen.add(key);
195
+ existing.push(fullPath);
196
+ }
197
+ }
198
+ }
199
+ catch {
200
+ // Ignore missing/unreadable candidates.
201
+ }
202
+ }
203
+ return existing;
204
+ }
205
+ async function choosePrdCandidate(paths, defaultPath) {
206
+ if (paths.length === 0)
207
+ return null;
208
+ if (!input.isTTY || paths.length === 1)
209
+ return paths[0];
210
+ const rl = readline.createInterface({ input, output });
211
+ try {
212
+ console.log('\nFound multiple PRD files:');
213
+ paths.forEach((path, index) => {
214
+ console.log(` ${index + 1}. ${path}`);
215
+ });
216
+ console.log(` ${paths.length + 1}. Open ${defaultPath} in editor instead`);
217
+ const answer = (await rl.question(`Select [1-${paths.length + 1}] (default 1): `)).trim();
218
+ if (answer === '')
219
+ return paths[0];
220
+ const index = Number.parseInt(answer, 10);
221
+ if (index >= 1 && index <= paths.length) {
222
+ return paths[index - 1];
223
+ }
224
+ if (index === paths.length + 1) {
225
+ return null;
226
+ }
227
+ throw new Error('Invalid selection');
228
+ }
229
+ finally {
230
+ rl.close();
231
+ }
232
+ }
233
+ function getEditorCommand() {
234
+ const editor = (process.env.VISUAL ?? process.env.EDITOR ?? '').trim();
235
+ return editor.length > 0 ? editor : null;
236
+ }
237
+ async function openEditorForPrd(prdPath) {
238
+ const editor = getEditorCommand();
239
+ if (!editor) {
240
+ return {
241
+ ok: false,
242
+ content: '',
243
+ reason: 'No $VISUAL or $EDITOR configured.',
244
+ };
245
+ }
246
+ const parts = editor.split(/\s+/).filter(Boolean);
247
+ const [editorCmd, ...editorArgs] = parts;
248
+ if (!editorCmd) {
249
+ return {
250
+ ok: false,
251
+ content: '',
252
+ reason: 'Invalid editor command.',
253
+ };
254
+ }
255
+ const result = spawnSync(editorCmd, [...editorArgs, prdPath], { stdio: 'inherit' });
256
+ if (result.status !== 0) {
257
+ return {
258
+ ok: false,
259
+ content: '',
260
+ reason: `Editor exited with code ${result.status ?? 'unknown'}`,
261
+ };
262
+ }
263
+ const content = (await readFile(prdPath, 'utf-8')).trim();
264
+ return { ok: true, content };
265
+ }
266
+ async function readPipedStdin() {
267
+ const chunks = [];
268
+ for await (const chunk of input)
269
+ chunks.push(Buffer.from(chunk));
270
+ return Buffer.concat(chunks).toString('utf-8').trim();
271
+ }
272
+ async function promptChoice(question, options, defaultValue) {
273
+ const rl = readline.createInterface({ input, output });
274
+ try {
275
+ console.log(`\n${question}`);
276
+ options.forEach((option, index) => {
277
+ const suffix = defaultValue === option.value ? ' (default)' : '';
278
+ console.log(` ${index + 1}. ${option.label}${suffix}`);
279
+ if (option.desc)
280
+ console.log(` ${option.desc}`);
281
+ });
282
+ const answer = (await rl.question(`Select [1-${options.length}]${defaultValue ? ` (Enter for ${defaultValue})` : ''}: `)).trim();
283
+ if (answer === '' && defaultValue)
284
+ return defaultValue;
285
+ const index = Number.parseInt(answer, 10);
286
+ if (index >= 1 && index <= options.length)
287
+ return options[index - 1].value;
288
+ const direct = options.find((option) => option.value === answer);
289
+ if (direct)
290
+ return direct.value;
291
+ throw new Error('Invalid selection');
292
+ }
293
+ finally {
294
+ rl.close();
295
+ }
296
+ }
297
+ async function capturePrdFromInteractiveMenu(workspacePrdPath) {
298
+ const repoRoot = process.cwd();
299
+ const discovered = await discoverLocalPrdCandidates(repoRoot);
300
+ const sourceChoiceOptions = [
301
+ {
302
+ value: 'paste',
303
+ label: 'Paste PRD inline',
304
+ desc: 'Fastest when you already have the brief in chat or clipboard.',
305
+ },
306
+ ];
307
+ if (discovered.length > 0) {
308
+ sourceChoiceOptions.push({
309
+ value: 'existing',
310
+ label: `Use existing PRD file (${discovered.length} found)`,
311
+ desc: 'Reuses your local PRD document.',
312
+ });
313
+ }
314
+ sourceChoiceOptions.push({
315
+ value: 'editor',
316
+ label: 'Open editor for envoi/PRD.md',
317
+ desc: 'Writes directly to workspace source-of-truth.',
318
+ });
319
+ sourceChoiceOptions.push({
320
+ value: 'skip',
321
+ label: 'Skip for now',
322
+ desc: 'Setup continues; planner will wait for your brief.',
323
+ });
324
+ const sourceChoice = await promptChoice('How do you want to describe your project?', sourceChoiceOptions, discovered.length > 0 ? 'existing' : 'paste');
325
+ if (sourceChoice === 'existing') {
326
+ const selected = await choosePrdCandidate(discovered, workspacePrdPath);
327
+ if (selected) {
328
+ const content = (await readFile(selected, 'utf-8')).trim();
329
+ return { text: content, source: 'existing' };
330
+ }
331
+ }
332
+ if (sourceChoice === 'editor') {
333
+ console.log(`\nOpening ${workspacePrdPath}...`);
334
+ const editorResult = await openEditorForPrd(workspacePrdPath);
335
+ if (editorResult.ok)
336
+ return { text: editorResult.content, source: 'editor' };
337
+ console.log(`\n[WARN] Could not open editor: ${editorResult.reason}`);
338
+ const fallback = await promptMultiline('your PRD (markdown)');
339
+ return { text: fallback, source: 'paste' };
340
+ }
341
+ if (sourceChoice === 'skip') {
342
+ return { text: '', source: 'skip' };
343
+ }
344
+ return {
345
+ text: await promptMultiline('your PRD (markdown)'),
346
+ source: 'paste',
347
+ };
348
+ }
349
+ async function collectPrdText(options, workspacePrdPath) {
350
+ if (options.prdFile) {
351
+ return {
352
+ text: (await readFile(resolve(options.prdFile), 'utf-8')).trim(),
353
+ source: 'file',
354
+ };
355
+ }
356
+ if (!input.isTTY) {
357
+ const piped = await readPipedStdin();
358
+ if (isNonEmptyContent(piped)) {
359
+ return { text: piped, source: 'stdin' };
360
+ }
361
+ return { text: '', source: 'skip' };
362
+ }
363
+ return await capturePrdFromInteractiveMenu(workspacePrdPath);
364
+ }
365
+ async function promptStartIntentIfNeeded(showTourPrompt) {
366
+ if (!showTourPrompt || !input.isTTY)
367
+ return 'setup';
368
+ const choice = await promptChoice('How would you like to begin?', [
369
+ { value: 'tour', label: 'Tour first', desc: 'Quick walkthrough of roles, commands, and flow.' },
370
+ { value: 'setup', label: 'Setup now', desc: 'Go straight to project onboarding.' },
371
+ ], 'setup');
372
+ return choice === 'tour' ? 'tour' : 'setup';
373
+ }
374
+ function printTour() {
375
+ console.log(`\n${PRODUCT_NAME} tour`);
376
+ console.log('- You decide direction, approve, and unblock.');
377
+ console.log('- Planner sequences milestones and budgets.');
378
+ console.log('- Builder implements scoped tasks.');
379
+ console.log('- Checker verifies and blocks unsafe output.');
380
+ console.log('- One tick = plan -> edit -> verify -> stop.');
381
+ console.log(`- Main commands: ${CLI_NAME} tick, ${CLI_NAME} loop, ${CLI_NAME} status, ${CLI_NAME} undo.`);
382
+ }
383
+ function checkCommandVersion(command) {
384
+ const first = spawnSync(command, ['--version'], { stdio: 'pipe' });
385
+ if (first.status === 0) {
386
+ const version = `${first.stdout ?? ''}`.trim() || undefined;
387
+ return { available: true, version };
388
+ }
389
+ const second = spawnSync(command, ['-v'], { stdio: 'pipe' });
390
+ if (second.status === 0) {
391
+ const version = `${second.stdout ?? ''}`.trim() || undefined;
392
+ return { available: true, version };
393
+ }
394
+ return { available: false };
395
+ }
396
+ export function extractRoadmapMilestones(prompt) {
397
+ const lines = prompt
398
+ .split(/\r?\n/)
399
+ .map((line) => line.trim())
400
+ .filter((line) => line.length > 0);
401
+ const milestones = [];
402
+ const seen = new Set();
403
+ for (const line of lines) {
404
+ const normalized = line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim();
405
+ const tagged = normalized.match(/^(M\d+[A-Za-z0-9_-]*)\s*[:\-]\s*(.+)$/i);
406
+ if (tagged) {
407
+ const entry = `${tagged[1].toUpperCase()}: ${tagged[2].trim()}`;
408
+ if (!seen.has(entry.toLowerCase())) {
409
+ seen.add(entry.toLowerCase());
410
+ milestones.push(entry);
411
+ }
412
+ continue;
413
+ }
414
+ if (/^milestone\b/i.test(normalized) && normalized.includes(':')) {
415
+ if (!seen.has(normalized.toLowerCase())) {
416
+ seen.add(normalized.toLowerCase());
417
+ milestones.push(normalized);
418
+ }
419
+ }
420
+ }
421
+ return milestones.slice(0, 7);
422
+ }
423
+ export function extractRoadmapQuestions(prompt) {
424
+ const lines = prompt
425
+ .split(/\r?\n/)
426
+ .map((line) => line.trim())
427
+ .filter((line) => line.length > 0);
428
+ const questions = [];
429
+ const seen = new Set();
430
+ for (const line of lines) {
431
+ const normalized = line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim();
432
+ if (!normalized.endsWith('?'))
433
+ continue;
434
+ if (!seen.has(normalized.toLowerCase())) {
435
+ seen.add(normalized.toLowerCase());
436
+ questions.push(normalized);
437
+ }
438
+ }
439
+ return questions.slice(0, 8);
440
+ }
441
+ async function generateRoadmapDraft(config, mode) {
442
+ const baseCommit = (() => {
443
+ try {
444
+ return getHeadCommit();
445
+ }
446
+ catch {
447
+ return '0000000000000000000000000000000000000000';
448
+ }
449
+ })();
450
+ const state = createInitialState(config, baseCommit);
451
+ const { runOrchestrator } = await import('../runner/orchestrator.js');
452
+ const orchestratorResult = await runOrchestrator(state);
453
+ if (!orchestratorResult.success || !orchestratorResult.task) {
454
+ return {
455
+ reason: orchestratorResult.error ?? 'orchestrator returned no task',
456
+ };
457
+ }
458
+ const task = orchestratorResult.task;
459
+ const prompt = task.question?.prompt?.trim() || task.intent;
460
+ const milestones = extractRoadmapMilestones(prompt);
461
+ const clarifyingQuestions = extractRoadmapQuestions(prompt);
462
+ return {
463
+ draft: {
464
+ generated_at: new Date().toISOString(),
465
+ source: task.task_kind === 'question' ? 'orchestrator_question' : 'orchestrator_execute',
466
+ task_id: task.task_id,
467
+ milestone_id: task.milestone_id,
468
+ mode,
469
+ milestones,
470
+ clarifying_questions: clarifyingQuestions,
471
+ prompt,
472
+ choices: task.question?.choices ?? [],
473
+ token_usage: orchestratorResult.tokenUsage ?? null,
474
+ },
475
+ };
476
+ }
477
+ function printRoadmapPreview(roadmap) {
478
+ console.log('\nPlanner snapshot (ROADMAP.json):');
479
+ console.log(`- Source: ${roadmap.source}`);
480
+ console.log(`- Task: ${roadmap.task_id} (${roadmap.milestone_id})`);
481
+ if (roadmap.milestones.length > 0) {
482
+ console.log('- Proposed milestones:');
483
+ for (const milestone of roadmap.milestones) {
484
+ console.log(` - ${milestone}`);
485
+ }
486
+ }
487
+ if (roadmap.clarifying_questions.length > 0) {
488
+ console.log('- Clarifying questions:');
489
+ for (const question of roadmap.clarifying_questions) {
490
+ console.log(` - ${question}`);
491
+ }
492
+ }
493
+ if (roadmap.choices.length > 0) {
494
+ console.log('- Suggested choices:');
495
+ for (const choice of roadmap.choices) {
496
+ console.log(` - ${choice}`);
497
+ }
498
+ }
499
+ if (roadmap.token_usage) {
500
+ const promptTokens = roadmap.token_usage.input_tokens ?? 'n/a';
501
+ const completionTokens = roadmap.token_usage.output_tokens ?? 'n/a';
502
+ const totalTokens = roadmap.token_usage.total_tokens ?? 'n/a';
503
+ console.log(`- Orchestrator tokens: in=${promptTokens} out=${completionTokens} total=${totalTokens}`);
504
+ }
505
+ }
506
+ async function runConnectivityChecks(config) {
507
+ console.log('\nRole connectivity checks:');
508
+ const plannerCli = checkCommandVersion(config.claude_code_cli.command);
509
+ if (plannerCli.available) {
510
+ console.log(`- planner: CLEARED (${config.claude_code_cli.command}${plannerCli.version ? ` ${plannerCli.version}` : ''})`);
511
+ }
512
+ else {
513
+ console.log(`- planner: BLOCKED (${config.claude_code_cli.command} not available)`);
514
+ }
515
+ if (config.builder.default_mode === 'cursor') {
516
+ const cursor = await checkCursorAgent(config);
517
+ if (!cursor.cli_available) {
518
+ console.log(`- builder: BLOCKED (cursor CLI '${cursor.command}' missing)`);
519
+ }
520
+ else if (!cursor.agent_available) {
521
+ console.log(`- builder: BLOCKED ('${cursor.command} agent' unavailable)`);
522
+ }
523
+ else if (cursor.auth_status === 'unauthenticated') {
524
+ console.log(`- builder: STANDBY (cursor agent not authenticated)`);
525
+ }
526
+ else {
527
+ console.log(`- builder: CLEARED (cursor agent ready)`);
528
+ }
529
+ }
530
+ else {
531
+ const builderCli = checkCommandVersion(config.claude_code_cli.command);
532
+ if (builderCli.available) {
533
+ console.log(`- builder: CLEARED (${config.claude_code_cli.command}${builderCli.version ? ` ${builderCli.version}` : ''})`);
534
+ }
535
+ else {
536
+ console.log(`- builder: BLOCKED (${config.claude_code_cli.command} not available)`);
537
+ }
538
+ }
539
+ if (config.reviewer?.enabled) {
540
+ if (config.reviewer.command === 'codex') {
541
+ const codex = await checkCodexCli(config);
542
+ if (!codex.cli_available) {
543
+ console.log('- checker: BLOCKED (codex CLI not available)');
544
+ }
545
+ else if (codex.auth_status === 'api_key_present') {
546
+ console.log('- checker: CLEARED (codex CLI + API key detected)');
547
+ }
548
+ else {
549
+ console.log('- checker: STANDBY (codex CLI available; auth status unknown)');
550
+ }
551
+ }
552
+ else {
553
+ const reviewerCli = checkCommandVersion(config.reviewer.command);
554
+ if (reviewerCli.available) {
555
+ console.log(`- checker: CLEARED (${config.reviewer.command}${reviewerCli.version ? ` ${reviewerCli.version}` : ''})`);
556
+ }
557
+ else {
558
+ console.log(`- checker: BLOCKED (${config.reviewer.command} not available)`);
559
+ }
560
+ }
561
+ }
562
+ else {
563
+ console.log('- checker: STANDBY (optional reviewer disabled)');
564
+ }
565
+ }
566
+ async function maybeStartRun(config, mode, autoRun) {
567
+ if (!autoRun) {
568
+ console.log('\nSetup complete. Waiting for you.');
569
+ console.log(`- Run '${CLI_NAME} tick' for one bounded cycle`);
570
+ console.log(`- Or run '${CLI_NAME} loop --mode ${mode}'`);
571
+ return;
572
+ }
573
+ console.log(`\nStarting now using mode '${mode}'...`);
574
+ if (mode === 'task') {
575
+ const { runTick } = await import('../runner/tick.js');
576
+ const report = await runTick(config);
577
+ console.log(`- First tick verdict: ${report.verdict.toUpperCase()} (${report.code})`);
578
+ return;
579
+ }
580
+ const { runLoop } = await import('../runner/loop.js');
581
+ const loopResult = await runLoop(config, { mode });
582
+ console.log(`- Loop finished: ${loopResult.final_verdict} (${loopResult.stop_reason})`);
583
+ }
584
+ export async function onboardCommand(options) {
585
+ await ensureRepoRootCwd();
586
+ await ensureInitialized(Boolean(options.forceInit));
587
+ const configPath = await resolveConfigPath(options.configPath);
588
+ const config = await loadConfig(configPath);
589
+ const raw = await atomicReadJson(configPath);
590
+ const nonInteractive = !input.isTTY;
591
+ if (nonInteractive) {
592
+ validateNonInteractiveOptions({ mode: options.mode, builder: options.builder });
593
+ }
594
+ const intent = await promptStartIntentIfNeeded(Boolean(options.showTourPrompt));
595
+ if (intent === 'tour') {
596
+ printTour();
597
+ const continueSetup = await promptYesNo('Continue with setup now?', true);
598
+ if (!continueSetup) {
599
+ console.log('Setup paused. Run envoi start again when ready.');
600
+ return;
601
+ }
602
+ }
603
+ // 1) Capture PRD
604
+ const prdPath = join(config.workspace_dir, 'PRD.md');
605
+ await mkdir(config.workspace_dir, { recursive: true });
606
+ const prdCapture = await collectPrdText({ prdFile: options.prdFile }, prdPath);
607
+ if (nonInteractive) {
608
+ validateNonInteractivePrdCapture(prdCapture);
609
+ }
610
+ if (isNonEmptyContent(prdCapture.text)) {
611
+ await writeFile(prdPath, `${prdCapture.text.trim()}\n`, 'utf-8');
612
+ }
613
+ // 2) Ask mode
614
+ const mode = options.mode ??
615
+ (input.isTTY
616
+ ? (await promptChoice('How should Envoi run by default?', [
617
+ { value: 'task', label: 'task', desc: 'One bounded cycle each time (lowest drift).' },
618
+ { value: 'milestone', label: 'milestone (recommended)', desc: 'Runs until milestone boundary, then pauses.' },
619
+ { value: 'autonomous', label: 'autonomous', desc: 'Runs continuously until blocked or limited.' },
620
+ ], raw.runner.default_loop_mode ?? 'milestone'))
621
+ : (raw.runner.default_loop_mode ?? 'milestone'));
622
+ raw.runner = { ...raw.runner, default_loop_mode: mode };
623
+ // 3) Ask builder
624
+ const builderChoice = options.builder ??
625
+ (input.isTTY
626
+ ? (await promptChoice('Which builder should implement code changes?', [
627
+ { value: 'cursor', label: 'cursor agent (recommended)', desc: 'Fast iterative execution, low token pressure.' },
628
+ { value: 'claude_code', label: 'claude code', desc: 'Single CLI stack with simpler setup.' },
629
+ ], raw.builder.default_mode === 'cursor' ? 'cursor' : 'claude_code'))
630
+ : (raw.builder.default_mode === 'cursor' ? 'cursor' : 'claude_code'));
631
+ // 4) Role model defaults (kept inside currently supported model set)
632
+ const orchestratorModel = input.isTTY
633
+ ? await promptChoice('Planner model?', [
634
+ { value: 'opus', label: 'opus (recommended)', desc: 'Best planning quality.' },
635
+ { value: 'sonnet', label: 'sonnet', desc: 'Balanced speed/cost.' },
636
+ { value: 'haiku', label: 'haiku', desc: 'Fastest/lowest cost.' },
637
+ ], raw.models.orchestrator_model || 'opus')
638
+ : (raw.models.orchestrator_model || 'opus');
639
+ const builderModel = builderChoice === 'claude_code'
640
+ ? (input.isTTY
641
+ ? await promptChoice('Builder model?', [
642
+ { value: 'sonnet', label: 'sonnet (recommended)', desc: 'Strong coding throughput.' },
643
+ { value: 'opus', label: 'opus', desc: 'Higher quality, higher cost.' },
644
+ { value: 'haiku', label: 'haiku', desc: 'Lower cost for simpler edits.' },
645
+ ], raw.models.builder_model || 'sonnet')
646
+ : (raw.models.builder_model || 'sonnet'))
647
+ : (raw.models.builder_model || 'sonnet');
648
+ raw.models = {
649
+ ...raw.models,
650
+ orchestrator_model: orchestratorModel,
651
+ orchestrator_fallback_model: raw.models.orchestrator_fallback_model || 'sonnet',
652
+ builder_model: builderModel,
653
+ builder_fallback_model: raw.models.builder_fallback_model || 'haiku',
654
+ };
655
+ // 5) Builder config application
656
+ if (builderChoice === 'cursor') {
657
+ const cursor = withDefaultCursorConfig(raw.builder.cursor);
658
+ raw.builder = {
659
+ ...raw.builder,
660
+ default_mode: 'cursor',
661
+ cursor: {
662
+ driver_kind: cursor.driver_kind ?? 'cursor_agent',
663
+ command: cursor.command,
664
+ args: cursor.args,
665
+ timeout_seconds: cursor.timeout_seconds,
666
+ output_file: cursor.output_file,
667
+ },
668
+ };
669
+ }
670
+ else {
671
+ raw.builder = { ...raw.builder, default_mode: 'claude_code' };
672
+ }
673
+ // 6) Optional reviewer
674
+ const reviewerChoice = options.reviewer !== undefined
675
+ ? normalizeReviewerChoice(options.reviewer)
676
+ : (input.isTTY
677
+ ? normalizeReviewerChoice(await promptChoice('Optional reviewer?', [
678
+ { value: 'none', label: 'No reviewer (recommended to start)', desc: 'Less friction, fastest onboarding.' },
679
+ { value: 'codex', label: 'Use Codex reviewer', desc: 'Adds second-pass checks for risky changes.' },
680
+ ], raw.reviewer?.enabled ? 'codex' : 'none'))
681
+ : 'none');
682
+ if (reviewerChoice === 'codex') {
683
+ const reviewerModel = input.isTTY
684
+ ? await promptChoice('Reviewer model?', [
685
+ { value: 'gpt-5', label: 'gpt-5 (recommended)', desc: 'Strong review quality for complex diffs.' },
686
+ { value: 'o3', label: 'o3', desc: 'Reasoning-heavy fallback.' },
687
+ { value: 'gpt-5-mini', label: 'gpt-5-mini', desc: 'Lower-cost reviewer option.' },
688
+ ], raw.reviewer?.model || 'gpt-5')
689
+ : (raw.reviewer?.model || 'gpt-5');
690
+ raw.reviewer = {
691
+ ...withDefaultReviewerConfig(config.workspace_dir, raw.reviewer),
692
+ enabled: true,
693
+ command: raw.reviewer?.command || 'codex',
694
+ model: reviewerModel,
695
+ };
696
+ }
697
+ else if (raw.reviewer) {
698
+ raw.reviewer = { ...raw.reviewer, enabled: false };
699
+ }
700
+ // Ensure PRD is runner-owned (protects from builder edits)
701
+ if (Array.isArray(raw.runner.runner_owned_globs) && !raw.runner.runner_owned_globs.includes(`${config.workspace_dir}/PRD.md`)) {
702
+ raw.runner.runner_owned_globs = [...raw.runner.runner_owned_globs, `${config.workspace_dir}/PRD.md`];
703
+ }
704
+ if (!validateConfig(raw)) {
705
+ throw new Error('Updated config is invalid (validateConfig failed).');
706
+ }
707
+ await atomicWriteJson(configPath, raw);
708
+ console.log(`\n${PRODUCT_NAME} onboarding complete.`);
709
+ console.log(`- Workspace: ${config.workspace_dir}`);
710
+ console.log(`- PRD source: ${prdCapture.source}`);
711
+ console.log(`- Default mode: ${mode}`);
712
+ console.log(`- Builder: ${builderChoice}`);
713
+ console.log(`- Reviewer: ${raw.reviewer?.enabled ? 'enabled' : 'disabled'}`);
714
+ await runConnectivityChecks(raw);
715
+ const roadmapPath = join(dirname(configPath), 'ROADMAP.json');
716
+ let existingPrdContent = '';
717
+ try {
718
+ existingPrdContent = (await readFile(prdPath, 'utf-8')).trim();
719
+ }
720
+ catch {
721
+ existingPrdContent = '';
722
+ }
723
+ const shouldAttemptRoadmap = isMeaningfulPrdText(prdCapture.text) || isMeaningfulPrdText(existingPrdContent);
724
+ if (shouldAttemptRoadmap) {
725
+ const draftResult = await generateRoadmapDraft(raw, mode);
726
+ if (draftResult.draft) {
727
+ await writeFile(roadmapPath, `${JSON.stringify(draftResult.draft, null, 2)}\n`, 'utf-8');
728
+ printRoadmapPreview(draftResult.draft);
729
+ console.log(`- Saved: ${roadmapPath}`);
730
+ }
731
+ else {
732
+ console.log(`\n[WARN] Could not generate roadmap snapshot: ${draftResult.reason}`);
733
+ }
734
+ }
735
+ else {
736
+ console.log('\nNeeds you: add a meaningful project brief (PRD) to generate roadmap guidance.');
737
+ console.log(`- Add context in ${prdPath}`);
738
+ }
739
+ const autoRunRequested = options.autoRun ?? input.isTTY;
740
+ const shouldRunNow = input.isTTY ? await promptYesNo(`Start now with mode '${mode}'?`, autoRunRequested) : false;
741
+ await maybeStartRun(raw, mode, shouldRunNow);
742
+ }
743
+ //# sourceMappingURL=onboard.js.map