popilot 0.2.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 (136) hide show
  1. package/README.md +372 -0
  2. package/adapters/claude-code/.claude/commands/_domain.md.hbs +32 -0
  3. package/adapters/claude-code/.claude/commands/analytics.md.hbs +55 -0
  4. package/adapters/claude-code/.claude/commands/daily.md.hbs +301 -0
  5. package/adapters/claude-code/.claude/commands/dev.md.hbs +62 -0
  6. package/adapters/claude-code/.claude/commands/handoff.md +258 -0
  7. package/adapters/claude-code/.claude/commands/market.md +120 -0
  8. package/adapters/claude-code/.claude/commands/metrics.md +123 -0
  9. package/adapters/claude-code/.claude/commands/oscar-loop.md +436 -0
  10. package/adapters/claude-code/.claude/commands/party.md +85 -0
  11. package/adapters/claude-code/.claude/commands/plan.md +43 -0
  12. package/adapters/claude-code/.claude/commands/research.md +203 -0
  13. package/adapters/claude-code/.claude/commands/retro.md +68 -0
  14. package/adapters/claude-code/.claude/commands/save.md +440 -0
  15. package/adapters/claude-code/.claude/commands/sessions.md +139 -0
  16. package/adapters/claude-code/.claude/commands/sprint.md +106 -0
  17. package/adapters/claude-code/.claude/commands/start.md +368 -0
  18. package/adapters/claude-code/.claude/commands/strategy.md +41 -0
  19. package/adapters/claude-code/.claude/commands/task.md +220 -0
  20. package/adapters/claude-code/.claude/commands/tracking.md +116 -0
  21. package/adapters/claude-code/.claude/commands/validate.md +58 -0
  22. package/adapters/claude-code/CLAUDE.md.hbs +208 -0
  23. package/adapters/claude-code/manifest.yaml +36 -0
  24. package/bin/cli.mjs +218 -0
  25. package/lib/adapter.mjs +68 -0
  26. package/lib/doctor.mjs +161 -0
  27. package/lib/hydrate.mjs +421 -0
  28. package/lib/prompt.mjs +78 -0
  29. package/lib/scaffold.mjs +155 -0
  30. package/lib/setup-wizard.mjs +331 -0
  31. package/lib/template-engine.mjs +164 -0
  32. package/lib/yaml-lite.mjs +476 -0
  33. package/package.json +30 -0
  34. package/scaffold/.context/.secrets.yaml.example +20 -0
  35. package/scaffold/.context/WORKFLOW.md.hbs +332 -0
  36. package/scaffold/.context/agents/TEMPLATE.md +115 -0
  37. package/scaffold/.context/agents/analyst.md.hbs +362 -0
  38. package/scaffold/.context/agents/developer.md.hbs +390 -0
  39. package/scaffold/.context/agents/handoff-specialist.md.hbs +292 -0
  40. package/scaffold/.context/agents/market-researcher.md.hbs +288 -0
  41. package/scaffold/.context/agents/ollie.md +323 -0
  42. package/scaffold/.context/agents/operations.md.hbs +293 -0
  43. package/scaffold/.context/agents/orchestrator.md.hbs +434 -0
  44. package/scaffold/.context/agents/planner.md.hbs +405 -0
  45. package/scaffold/.context/agents/qa.md.hbs +409 -0
  46. package/scaffold/.context/agents/researcher.md.hbs +330 -0
  47. package/scaffold/.context/agents/sage.md +349 -0
  48. package/scaffold/.context/agents/strategist.md.hbs +339 -0
  49. package/scaffold/.context/agents/tracking-governor.md.hbs +291 -0
  50. package/scaffold/.context/agents/validator.md.hbs +365 -0
  51. package/scaffold/.context/integrations/_registry.yaml +38 -0
  52. package/scaffold/.context/integrations/providers/channel_io.yaml +38 -0
  53. package/scaffold/.context/integrations/providers/corti.yaml +203 -0
  54. package/scaffold/.context/integrations/providers/ga4.yaml +116 -0
  55. package/scaffold/.context/integrations/providers/intercom.yaml +47 -0
  56. package/scaffold/.context/integrations/providers/linear.yaml +46 -0
  57. package/scaffold/.context/integrations/providers/mixpanel.yaml +73 -0
  58. package/scaffold/.context/integrations/providers/notebooklm.yaml +74 -0
  59. package/scaffold/.context/integrations/providers/notion.yaml +129 -0
  60. package/scaffold/.context/integrations/providers/prod_db.yaml +183 -0
  61. package/scaffold/.context/oscar/workflows/multi-agent.md +82 -0
  62. package/scaffold/.context/oscar/workflows/ollie-sage.md +128 -0
  63. package/scaffold/.context/oscar/workflows/session-git.md +71 -0
  64. package/scaffold/.context/oscar/workflows/setup.md +663 -0
  65. package/scaffold/.context/oscar/workflows/tracking.md +118 -0
  66. package/scaffold/.context/project.yaml.example +102 -0
  67. package/scaffold/.context/templates/dev-guide.md +217 -0
  68. package/scaffold/.context/templates/epic-spec.md +225 -0
  69. package/scaffold/.context/templates/guardrail.md +94 -0
  70. package/scaffold/.context/templates/handoff-checklist.md +197 -0
  71. package/scaffold/.context/templates/prd.md +80 -0
  72. package/scaffold/.context/templates/retrospective.md +78 -0
  73. package/scaffold/.context/templates/screen-spec.md +714 -0
  74. package/scaffold/.context/templates/sprint-plan.md +72 -0
  75. package/scaffold/.context/templates/sprint-status.yaml +109 -0
  76. package/scaffold/.context/templates/story-v2.md +228 -0
  77. package/scaffold/.context/templates/validation-report.md +99 -0
  78. package/scaffold/.gitignore.append +7 -0
  79. package/scaffold/spec-site/env.d.ts +7 -0
  80. package/scaffold/spec-site/index.html +14 -0
  81. package/scaffold/spec-site/package.json +20 -0
  82. package/scaffold/spec-site/src/App.vue +27 -0
  83. package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +10 -0
  84. package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +10 -0
  85. package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +14 -0
  86. package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +14 -0
  87. package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +21 -0
  88. package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +21 -0
  89. package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +20 -0
  90. package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +20 -0
  91. package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +11 -0
  92. package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +11 -0
  93. package/scaffold/spec-site/src/components/Accordion.vue +108 -0
  94. package/scaffold/spec-site/src/components/AppHeader.vue +304 -0
  95. package/scaffold/spec-site/src/components/Badge.vue +25 -0
  96. package/scaffold/spec-site/src/components/CoachingCard.vue +112 -0
  97. package/scaffold/spec-site/src/components/MemoSidebar.vue +239 -0
  98. package/scaffold/spec-site/src/components/MockupShell.vue +100 -0
  99. package/scaffold/spec-site/src/components/RuleTable.vue +99 -0
  100. package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +103 -0
  101. package/scaffold/spec-site/src/components/SpecNav.vue +26 -0
  102. package/scaffold/spec-site/src/components/SpecSection.vue +59 -0
  103. package/scaffold/spec-site/src/components/SummaryGrid.vue +39 -0
  104. package/scaffold/spec-site/src/components/VersionBadge.vue +38 -0
  105. package/scaffold/spec-site/src/composables/useActiveSection.ts +53 -0
  106. package/scaffold/spec-site/src/composables/useMemo.ts +138 -0
  107. package/scaffold/spec-site/src/composables/useRetro.ts +313 -0
  108. package/scaffold/spec-site/src/composables/useScenario.ts +43 -0
  109. package/scaffold/spec-site/src/composables/useScenarioStore.ts +102 -0
  110. package/scaffold/spec-site/src/composables/useTurso.ts +160 -0
  111. package/scaffold/spec-site/src/composables/useUser.ts +25 -0
  112. package/scaffold/spec-site/src/data/navigation.ts +59 -0
  113. package/scaffold/spec-site/src/data/types.ts +90 -0
  114. package/scaffold/spec-site/src/data/wireframeRegistry.ts +25 -0
  115. package/scaffold/spec-site/src/layouts/SplitPaneLayout.vue +79 -0
  116. package/scaffold/spec-site/src/main.ts +10 -0
  117. package/scaffold/spec-site/src/pages/IndexPage.vue +66 -0
  118. package/scaffold/spec-site/src/pages/PolicyDetail.vue +215 -0
  119. package/scaffold/spec-site/src/pages/PolicyIndex.vue +74 -0
  120. package/scaffold/spec-site/src/pages/retro/RetroActions.vue +191 -0
  121. package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +192 -0
  122. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +131 -0
  123. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +287 -0
  124. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +178 -0
  125. package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +34 -0
  126. package/scaffold/spec-site/src/pages/shared/PlaceholderContent.vue +22 -0
  127. package/scaffold/spec-site/src/pages/shared/PlaceholderSpecPanel.vue +16 -0
  128. package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +145 -0
  129. package/scaffold/spec-site/src/pages/wireframe/WireframeShell.vue +151 -0
  130. package/scaffold/spec-site/src/router.ts +85 -0
  131. package/scaffold/spec-site/src/styles/base.css +21 -0
  132. package/scaffold/spec-site/src/styles/split-pane.css +143 -0
  133. package/scaffold/spec-site/src/styles/variables.css +47 -0
  134. package/scaffold/spec-site/src/utils/markdown.ts +197 -0
  135. package/scaffold/spec-site/tsconfig.json +20 -0
  136. package/scaffold/spec-site/vite.config.ts +18 -0
package/lib/prompt.mjs ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Zero-dep interactive prompt helpers using Node.js readline/promises.
3
+ * All functions accept an optional `rl` parameter for testing/injection.
4
+ */
5
+
6
+ import { createInterface } from 'node:readline/promises';
7
+ import { stdin, stdout } from 'node:process';
8
+
9
+ /** Create a shared readline interface */
10
+ export function createPrompt() {
11
+ return createInterface({ input: stdin, output: stdout });
12
+ }
13
+
14
+ /**
15
+ * Ask a single question, return the answer (or default).
16
+ * @param {import('node:readline/promises').Interface} rl
17
+ * @param {string} question
18
+ * @param {string} [defaultValue]
19
+ * @returns {Promise<string>}
20
+ */
21
+ export async function ask(rl, question, defaultValue) {
22
+ const suffix = defaultValue != null ? ` [${defaultValue}]` : '';
23
+ const answer = (await rl.question(` ${question}${suffix}: `)).trim();
24
+ return answer || defaultValue || '';
25
+ }
26
+
27
+ /**
28
+ * Y/n confirmation.
29
+ * @param {import('node:readline/promises').Interface} rl
30
+ * @param {string} question
31
+ * @param {boolean} [defaultYes=false]
32
+ * @returns {Promise<boolean>}
33
+ */
34
+ export async function confirm(rl, question, defaultYes = false) {
35
+ const hint = defaultYes ? 'Y/n' : 'y/N';
36
+ const answer = (await rl.question(` ${question} (${hint}): `)).trim().toLowerCase();
37
+ if (answer === '') return defaultYes;
38
+ return answer === 'y' || answer === 'yes';
39
+ }
40
+
41
+ /**
42
+ * Numbered single-select menu.
43
+ * @param {import('node:readline/promises').Interface} rl
44
+ * @param {string} question
45
+ * @param {{ label: string, value: any }[]} options
46
+ * @param {number} [defaultIndex=0] - 0-based default index
47
+ * @returns {Promise<any>} selected value
48
+ */
49
+ export async function select(rl, question, options, defaultIndex = 0) {
50
+ console.log(` ${question}`);
51
+ options.forEach((opt, i) => {
52
+ console.log(` ${i + 1}. ${opt.label}`);
53
+ });
54
+ const answer = (await rl.question(` Select [${defaultIndex + 1}]: `)).trim();
55
+ const idx = answer ? parseInt(answer, 10) - 1 : defaultIndex;
56
+ if (idx >= 0 && idx < options.length) return options[idx].value;
57
+ return options[defaultIndex].value;
58
+ }
59
+
60
+ /**
61
+ * Numbered multi-select menu (comma-separated input).
62
+ * @param {import('node:readline/promises').Interface} rl
63
+ * @param {string} question
64
+ * @param {{ label: string, value: any }[]} options
65
+ * @returns {Promise<any[]>} selected values
66
+ */
67
+ export async function multiSelect(rl, question, options) {
68
+ console.log(` ${question}`);
69
+ options.forEach((opt, i) => {
70
+ console.log(` ${i + 1}. ${opt.label}`);
71
+ });
72
+ const answer = (await rl.question(' Select (comma-separated, Enter to skip): ')).trim();
73
+ if (!answer) return [];
74
+ const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1);
75
+ return indices
76
+ .filter(i => i >= 0 && i < options.length)
77
+ .map(i => options[i].value);
78
+ }
@@ -0,0 +1,155 @@
1
+ import { readdir, readFile, writeFile, mkdir, stat, access } from 'node:fs/promises';
2
+ import { join, dirname, relative } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { getAdapterDir, getDefaultAdapter } from './adapter.mjs';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const SCAFFOLD_DIR = join(__dirname, '..', 'scaffold');
8
+
9
+ /**
10
+ * Copy scaffold directory to target, preserving structure.
11
+ * Two-pass: 1) core scaffold, 2) adapter overlay.
12
+ *
13
+ * - .hbs files are copied as-is (hydration happens in Setup Wizard)
14
+ * - .append files are returned for post-processing
15
+ * - All other files copied directly
16
+ *
17
+ * @param {string} targetDir - Destination directory
18
+ * @param {object} options - { skipSpecSite, overwriteExisting, platform }
19
+ * @returns {Promise<{ copied: string[], overwritten: string[], skipped: string[], appends: { file: string, content: string }[] }>}
20
+ */
21
+ export async function copyScaffold(targetDir, options = {}) {
22
+ const copied = [];
23
+ const overwritten = [];
24
+ const skipped = [];
25
+ const appends = [];
26
+
27
+ // Pass 1: Core scaffold
28
+ await walk(SCAFFOLD_DIR, targetDir, targetDir, options, copied, overwritten, skipped, appends);
29
+
30
+ // Pass 2: Adapter overlay
31
+ const platform = options.platform || getDefaultAdapter();
32
+ const adapterDir = getAdapterDir(platform);
33
+ try {
34
+ await access(adapterDir);
35
+ await walk(adapterDir, targetDir, targetDir, options, copied, overwritten, skipped, appends);
36
+ } catch {
37
+ // Adapter dir not found — skip
38
+ }
39
+
40
+ return { copied, overwritten, skipped, appends };
41
+ }
42
+
43
+ async function walk(srcDir, destDir, targetDir, options, copied, overwritten, skipped, appends) {
44
+ let entries;
45
+ try {
46
+ entries = await readdir(srcDir, { withFileTypes: true });
47
+ } catch {
48
+ return;
49
+ }
50
+
51
+ for (const entry of entries) {
52
+ const srcPath = join(srcDir, entry.name);
53
+ const relPath = relative(srcDir, srcPath);
54
+
55
+ // Skip spec-site if requested
56
+ if (options.skipSpecSite && relPath.startsWith('spec-site')) {
57
+ continue;
58
+ }
59
+
60
+ // Skip manifest.yaml (adapter metadata, not a project file)
61
+ if (entry.name === 'manifest.yaml') {
62
+ continue;
63
+ }
64
+
65
+ if (entry.isDirectory()) {
66
+ const destPath = join(destDir, entry.name);
67
+ await mkdir(destPath, { recursive: true });
68
+ await walk(srcPath, destPath, targetDir, options, copied, overwritten, skipped, appends);
69
+ } else if (entry.name.endsWith('.append')) {
70
+ const content = await readFile(srcPath, 'utf-8');
71
+ appends.push({ file: entry.name.replace('.append', ''), content });
72
+ } else {
73
+ const destPath = join(destDir, entry.name);
74
+ await mkdir(dirname(destPath), { recursive: true });
75
+
76
+ // Existing file handling
77
+ try {
78
+ await access(destPath);
79
+ if (!options.overwriteExisting) {
80
+ skipped.push(relative(targetDir, destPath));
81
+ continue;
82
+ }
83
+ const content = await readFile(srcPath);
84
+ await writeFile(destPath, content);
85
+ overwritten.push(relative(targetDir, destPath));
86
+ continue;
87
+ } catch {
88
+ // File doesn't exist — copy
89
+ }
90
+
91
+ const content = await readFile(srcPath);
92
+ await writeFile(destPath, content);
93
+ copied.push(relative(targetDir, destPath));
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Append content to a file (create if doesn't exist)
100
+ */
101
+ export async function appendToFile(filePath, content) {
102
+ let existing = '';
103
+ try {
104
+ existing = await readFile(filePath, 'utf-8');
105
+ } catch {
106
+ // File doesn't exist
107
+ }
108
+
109
+ // Avoid duplicate lines
110
+ const lines = content.split('\n').filter(l => l.trim());
111
+ const newLines = lines.filter(l => !existing.includes(l));
112
+
113
+ if (newLines.length > 0) {
114
+ const separator = existing.endsWith('\n') || existing === '' ? '' : '\n';
115
+ await writeFile(filePath, existing + separator + newLines.join('\n') + '\n');
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Check if target directory already has Popilot structure.
121
+ * Uses adapter detection markers if available.
122
+ */
123
+ export async function detectExisting(targetDir, platform) {
124
+ let markers;
125
+ if (platform) {
126
+ try {
127
+ const { loadManifest } = await import('./adapter.mjs');
128
+ const manifest = await loadManifest(platform);
129
+ markers = manifest.detection_markers || [];
130
+ } catch {
131
+ markers = [];
132
+ }
133
+ }
134
+
135
+ if (!markers || markers.length === 0) {
136
+ markers = ['.claude', '.context', 'CLAUDE.md'];
137
+ }
138
+
139
+ // Always include .context
140
+ if (!markers.includes('.context')) {
141
+ markers.push('.context');
142
+ }
143
+
144
+ const found = [];
145
+ for (const marker of markers) {
146
+ try {
147
+ await stat(join(targetDir, marker));
148
+ found.push(marker);
149
+ } catch {
150
+ // Not found
151
+ }
152
+ }
153
+
154
+ return found;
155
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Interactive setup wizard — terminal interview → project.yaml + related files.
3
+ *
4
+ * Collects only essential config. AI deep interview is deferred to Claude Code
5
+ * via `_meta.needs_deep_interview: true`.
6
+ */
7
+
8
+ import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+ import { ask, confirm, select, createPrompt } from './prompt.mjs';
11
+ import { parse as parseYaml, stringify as stringifyYaml } from './yaml-lite.mjs';
12
+
13
+ /**
14
+ * Run the interactive setup wizard.
15
+ *
16
+ * @param {string} targetDir - Project root (scaffold already copied)
17
+ * @param {object} [opts]
18
+ * @param {import('node:readline/promises').Interface} [opts.rl] - Inject readline for testing
19
+ * @returns {Promise<void>}
20
+ */
21
+ export async function runSetupWizard(targetDir, opts = {}) {
22
+ const rl = opts.rl || createPrompt();
23
+ const ownsRl = !opts.rl;
24
+
25
+ try {
26
+ console.log();
27
+ console.log(' ──────────────────────────────────────');
28
+ console.log(' 📝 Project Setup');
29
+ console.log(' ──────────────────────────────────────');
30
+ console.log();
31
+
32
+ // ── Phase 0: User preferences ──────────────────────
33
+ console.log(' 👤 User Preferences');
34
+ console.log();
35
+ const userName = await ask(rl, 'What should the agents call you?', 'there');
36
+ const userRole = await ask(rl, 'Your role/title (optional)');
37
+ const preferredLanguage = await select(rl, 'Preferred response language:', [
38
+ { label: 'Korean (ko)', value: 'ko' },
39
+ { label: 'English (en)', value: 'en' },
40
+ { label: 'Japanese (ja)', value: 'ja' },
41
+ ], 0);
42
+ const communicationStyle = await select(rl, 'Communication style:', [
43
+ { label: 'Concise', value: 'concise' },
44
+ { label: 'Detailed', value: 'detailed' },
45
+ { label: 'Casual', value: 'casual' },
46
+ ], 0);
47
+
48
+ console.log();
49
+
50
+ // ── Phase 1: Basic info ──────────────────────────
51
+ const projectName = await ask(rl, 'Project name', getDefaultProjectName(targetDir));
52
+ const tagline = await ask(rl, 'Tagline (optional)');
53
+ const projectType = await select(rl, 'Project type:', [
54
+ { label: 'brownfield (existing codebase)', value: 'brownfield' },
55
+ { label: 'greenfield (new project)', value: 'greenfield' },
56
+ ], 0);
57
+
58
+ console.log();
59
+
60
+ // ── Phase 2: Domains ─────────────────────────────
61
+ const hasDomains = await confirm(rl, 'Do you have work domains?', false);
62
+ let domains = [];
63
+ if (hasDomains) {
64
+ const domainsInput = await ask(rl, 'Domains (id:name, comma-separated)', '');
65
+ domains = parseDomainInput(domainsInput);
66
+ }
67
+
68
+ console.log();
69
+
70
+ // ── Phase 3: Dev scope ───────────────────────────
71
+ const hasDevRepo = await confirm(rl, 'Do you have a separate dev repo?', false);
72
+ let devScope = { repo_name: '', service_repo: '' };
73
+ if (hasDevRepo) {
74
+ devScope.repo_name = await ask(rl, 'Dashboard repo name');
75
+ devScope.service_repo = await ask(rl, 'Service repo name');
76
+ }
77
+
78
+ console.log();
79
+
80
+ // ── Phase 4: Integrations ────────────────────────
81
+ const integrations = await collectIntegrations(rl, targetDir);
82
+
83
+ // ── Generate files ───────────────────────────────
84
+ console.log();
85
+
86
+ // project.yaml
87
+ const projectYaml = buildProjectYaml({
88
+ projectName, tagline, projectType,
89
+ domains, devScope, integrations,
90
+ platform: opts.platform || null,
91
+ });
92
+ const contextDir = join(targetDir, '.context');
93
+ await mkdir(contextDir, { recursive: true });
94
+ await writeFile(join(contextDir, 'project.yaml'), stringifyYaml(projectYaml));
95
+
96
+ // user-context.yaml
97
+ const userContext = {
98
+ user: {
99
+ name: userName,
100
+ },
101
+ _meta: {
102
+ source: 'cli',
103
+ created_at: new Date().toISOString(),
104
+ },
105
+ };
106
+ if (userRole) userContext.user.role = userRole;
107
+ if (preferredLanguage) userContext.user.language = preferredLanguage;
108
+ if (communicationStyle) userContext.user.communication_style = communicationStyle;
109
+
110
+ await writeFile(join(contextDir, 'user-context.yaml'), stringifyYaml(userContext));
111
+
112
+ // sessions/index.yaml
113
+ const sessionsDir = join(contextDir, 'sessions');
114
+ await mkdir(join(sessionsDir, 'active'), { recursive: true });
115
+ await mkdir(join(sessionsDir, 'archive'), { recursive: true });
116
+ await writeFile(join(sessionsDir, 'index.yaml'), stringifyYaml({
117
+ sessions: [],
118
+ _meta: { last_updated: new Date().toISOString() },
119
+ }));
120
+
121
+ // sprints/s1/context.md
122
+ const sprintDir = join(contextDir, 'sprints', 's1');
123
+ await mkdir(sprintDir, { recursive: true });
124
+ await writeFile(join(sprintDir, 'context.md'), `# Sprint 1\n\n> Write your sprint context here.\n`);
125
+
126
+ // Domain folders
127
+ if (domains.length > 0) {
128
+ for (const d of domains) {
129
+ const domainDir = join(contextDir, 'domains', d.id);
130
+ await mkdir(domainDir, { recursive: true });
131
+ await writeFile(join(domainDir, 'index.md'), `# ${d.name}\n\n> Write your domain context here.\n`);
132
+ }
133
+ }
134
+
135
+ // Ensure standard directories
136
+ for (const dir of ['global/product', 'global/database', 'global/tracking', 'metrics', 'daily']) {
137
+ await mkdir(join(contextDir, dir), { recursive: true });
138
+ }
139
+
140
+ } finally {
141
+ if (ownsRl) rl.close();
142
+ }
143
+ }
144
+
145
+ // ── Integration collection ──────────────────────────────
146
+
147
+ async function collectIntegrations(rl, targetDir) {
148
+ const registryPath = join(targetDir, '.context', 'integrations', '_registry.yaml');
149
+ const providersDir = join(targetDir, '.context', 'integrations', 'providers');
150
+
151
+ let registry;
152
+ try {
153
+ registry = parseYaml(await readFile(registryPath, 'utf-8'));
154
+ } catch {
155
+ return {}; // No registry — skip integrations
156
+ }
157
+
158
+ // Load all provider definitions
159
+ const providerFiles = await safeReaddir(providersDir);
160
+ const providers = [];
161
+ for (const file of providerFiles) {
162
+ if (!file.endsWith('.yaml') || file.startsWith('_')) continue;
163
+ try {
164
+ const yaml = parseYaml(await readFile(join(providersDir, file), 'utf-8'));
165
+ providers.push(yaml);
166
+ } catch { /* skip */ }
167
+ }
168
+
169
+ console.log(' ──────────────────────────────────────');
170
+ console.log(' 🔌 Integrations');
171
+ console.log(' ──────────────────────────────────────');
172
+ console.log();
173
+
174
+ const integrations = {};
175
+ const categories = registry.categories || {};
176
+
177
+ // Group providers by category
178
+ for (const [catId, cat] of Object.entries(categories)) {
179
+ const catProviders = providers.filter(p => p.category === catId);
180
+ if (catProviders.length === 0) continue;
181
+
182
+ const options = [
183
+ ...catProviders.map(p => ({ label: `${p.name}`, value: p.id })),
184
+ { label: 'None', value: null },
185
+ ];
186
+
187
+ const selected = await select(rl, `[${cat.label}]`, options, options.length - 1);
188
+ console.log();
189
+
190
+ if (!selected) continue;
191
+
192
+ const provider = catProviders.find(p => p.id === selected);
193
+ if (!provider) continue;
194
+
195
+ // Ask setup questions
196
+ const config = { enabled: true };
197
+ const questions = provider.setup_questions || [];
198
+ for (const q of questions) {
199
+ if (q.type === 'object_list') {
200
+ // Special: collect table/risk/reason list
201
+ const items = await collectObjectList(rl, q);
202
+ config[q.key] = items;
203
+ } else {
204
+ const hint = q.example ? ` (e.g. ${q.example})` : '';
205
+ const label = q.required ? `${q.label}${hint}` : `${q.label}${hint} (optional)`;
206
+ const answer = await ask(rl, label);
207
+ if (answer) config[q.key] = answer;
208
+ }
209
+ }
210
+
211
+ integrations[selected] = config;
212
+ }
213
+
214
+ return integrations;
215
+ }
216
+
217
+ async function collectObjectList(rl, question) {
218
+ const items = [];
219
+ const fields = question.fields || [];
220
+ const hasMore = await confirm(rl, `Add ${question.label} entries?`, false);
221
+ if (!hasMore) return items;
222
+
223
+ let adding = true;
224
+ while (adding) {
225
+ const item = {};
226
+ for (const field of fields) {
227
+ item[field] = await ask(rl, ` ${field}`);
228
+ }
229
+ items.push(item);
230
+ adding = await confirm(rl, ' Add another?', false);
231
+ }
232
+ return items;
233
+ }
234
+
235
+ // ── project.yaml builder ────────────────────────────────
236
+
237
+ function buildProjectYaml({ projectName, tagline, projectType, domains, devScope, integrations, platform }) {
238
+ // Build the full integrations block with all known providers
239
+ const allProviders = ['ga4', 'mixpanel', 'notion', 'linear', 'channel_io', 'intercom', 'prod_db', 'notebooklm', 'corti'];
240
+ const integrationsBlock = {};
241
+
242
+ for (const id of allProviders) {
243
+ if (integrations[id]) {
244
+ integrationsBlock[id] = integrations[id];
245
+ } else {
246
+ integrationsBlock[id] = { enabled: false };
247
+ }
248
+ }
249
+
250
+ return {
251
+ project: {
252
+ name: projectName,
253
+ tagline: tagline || '',
254
+ type: projectType,
255
+ },
256
+ problem: {
257
+ core: '',
258
+ target: '',
259
+ alternatives: [],
260
+ timing: '',
261
+ },
262
+ solution: {
263
+ approach: '',
264
+ differentiation: '',
265
+ outcome: [],
266
+ },
267
+ current_state: {
268
+ stage: '',
269
+ focus: '',
270
+ uncertainty: '',
271
+ next_milestone: '',
272
+ },
273
+ validation: {
274
+ confirmed: [],
275
+ unknown: [],
276
+ customer_feedback: '',
277
+ },
278
+ operations: {
279
+ sprint: {
280
+ enabled: true,
281
+ current: 1,
282
+ duration_weeks: 2,
283
+ d_day: '',
284
+ },
285
+ domains: domains.map(d => ({
286
+ id: d.id,
287
+ name: d.name,
288
+ path: `domains/${d.id}/`,
289
+ })),
290
+ integrations: integrationsBlock,
291
+ dev_scope: devScope,
292
+ spec_site: {
293
+ title: `${projectName} Spec`,
294
+ deploy_url: '',
295
+ },
296
+ },
297
+ _meta: {
298
+ created_at: new Date().toISOString(),
299
+ created_by: 'popilot init',
300
+ needs_deep_interview: true,
301
+ last_interview: null,
302
+ version: '1.0.0',
303
+ ...(platform ? { platform } : {}),
304
+ },
305
+ };
306
+ }
307
+
308
+ // ── Helpers ─────────────────────────────────────────────
309
+
310
+ function getDefaultProjectName(targetDir) {
311
+ const parts = targetDir.split('/');
312
+ const last = parts[parts.length - 1];
313
+ return last === '.' ? parts[parts.length - 2] || 'my-project' : last;
314
+ }
315
+
316
+ function parseDomainInput(input) {
317
+ if (!input.trim()) return [];
318
+ return input.split(',').map(s => {
319
+ const trimmed = s.trim();
320
+ const colonIdx = trimmed.indexOf(':');
321
+ if (colonIdx > 0) {
322
+ return { id: trimmed.slice(0, colonIdx).trim(), name: trimmed.slice(colonIdx + 1).trim() };
323
+ }
324
+ // No colon — use as both id and name
325
+ return { id: trimmed, name: trimmed };
326
+ }).filter(d => d.id);
327
+ }
328
+
329
+ async function safeReaddir(dir) {
330
+ try { return await readdir(dir); } catch { return []; }
331
+ }