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
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Adapter resolution — locate and load adapter manifests.
3
+ *
4
+ * An adapter contains platform-specific files (system prompts, commands)
5
+ * that are layered on top of the core scaffold during init/hydrate.
6
+ */
7
+
8
+ import { readdir, readFile } from 'node:fs/promises';
9
+ import { join, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { parse as parseYaml } from './yaml-lite.mjs';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const ADAPTERS_DIR = join(__dirname, '..', 'adapters');
15
+
16
+ const DEFAULT_ADAPTER = 'claude-code';
17
+
18
+ /**
19
+ * Get the directory path for an adapter.
20
+ * @param {string} adapterId
21
+ * @returns {string}
22
+ */
23
+ export function getAdapterDir(adapterId) {
24
+ return join(ADAPTERS_DIR, adapterId);
25
+ }
26
+
27
+ /**
28
+ * Load and parse an adapter's manifest.yaml.
29
+ * @param {string} adapterId
30
+ * @returns {Promise<object>}
31
+ */
32
+ export async function loadManifest(adapterId) {
33
+ const manifestPath = join(getAdapterDir(adapterId), 'manifest.yaml');
34
+ const content = await readFile(manifestPath, 'utf-8');
35
+ return parseYaml(content);
36
+ }
37
+
38
+ /**
39
+ * List all available adapter IDs by scanning the adapters/ directory.
40
+ * @returns {Promise<string[]>}
41
+ */
42
+ export async function listAdapters() {
43
+ try {
44
+ const entries = await readdir(ADAPTERS_DIR, { withFileTypes: true });
45
+ const ids = [];
46
+ for (const entry of entries) {
47
+ if (entry.isDirectory()) {
48
+ try {
49
+ await readFile(join(ADAPTERS_DIR, entry.name, 'manifest.yaml'));
50
+ ids.push(entry.name);
51
+ } catch {
52
+ // No manifest — skip
53
+ }
54
+ }
55
+ }
56
+ return ids;
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get the default adapter ID.
64
+ * @returns {string}
65
+ */
66
+ export function getDefaultAdapter() {
67
+ return DEFAULT_ADAPTER;
68
+ }
package/lib/doctor.mjs ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Installation diagnostics — check that Popilot is properly set up.
3
+ */
4
+
5
+ import { readdir, readFile, stat, access } from 'node:fs/promises';
6
+ import { join } from 'node:path';
7
+ import { loadManifest, getDefaultAdapter } from './adapter.mjs';
8
+
9
+ /**
10
+ * Run diagnostics on a Popilot installation.
11
+ *
12
+ * @param {string} targetDir - Project root
13
+ * @param {object} [opts]
14
+ * @param {boolean} [opts.skipSpecSite]
15
+ * @param {string} [opts.platform]
16
+ * @returns {Promise<{ passed: string[], failed: string[], warnings: string[] }>}
17
+ */
18
+ export async function runDoctor(targetDir, opts = {}) {
19
+ const passed = [];
20
+ const failed = [];
21
+ const warnings = [];
22
+
23
+ // Load adapter manifest for platform-specific checks
24
+ const platform = opts.platform || getDefaultAdapter();
25
+ let manifest;
26
+ try {
27
+ manifest = await loadManifest(platform);
28
+ } catch {
29
+ manifest = null;
30
+ }
31
+
32
+ console.log();
33
+ console.log(' ──────────────────────────────────────');
34
+ console.log(' 🩺 Popilot Doctor');
35
+ console.log(' ──────────────────────────────────────');
36
+ console.log();
37
+
38
+ // 1. project.yaml exists (core check)
39
+ await check('project.yaml exists', async () => {
40
+ await access(join(targetDir, '.context', 'project.yaml'));
41
+ }, passed, failed);
42
+
43
+ // 2. Platform-specific checks from manifest
44
+ if (manifest?.doctor_checks) {
45
+ for (const dc of manifest.doctor_checks) {
46
+ await check(dc.name, async () => {
47
+ await access(join(targetDir, dc.file));
48
+ if (dc.no_hbs) {
49
+ try {
50
+ await access(join(targetDir, dc.file + '.hbs'));
51
+ throw new Error(`${dc.file}.hbs still exists — hydration incomplete`);
52
+ } catch (e) {
53
+ if (e.message.includes('hydration')) throw e;
54
+ // .hbs not found — good
55
+ }
56
+ }
57
+ }, passed, failed);
58
+ }
59
+ } else {
60
+ // Fallback: CLAUDE.md check (backward compat)
61
+ await check('CLAUDE.md exists (hydrated)', async () => {
62
+ await access(join(targetDir, 'CLAUDE.md'));
63
+ try {
64
+ await access(join(targetDir, 'CLAUDE.md.hbs'));
65
+ throw new Error('CLAUDE.md.hbs still exists — hydration incomplete');
66
+ } catch (e) {
67
+ if (e.message.includes('hydration')) throw e;
68
+ }
69
+ }, passed, failed);
70
+ }
71
+
72
+ // 3. No residual .hbs files
73
+ await check('No residual .hbs files', async () => {
74
+ const hbsFiles = await findHbsFiles(targetDir);
75
+ if (hbsFiles.length > 0) {
76
+ throw new Error(`${hbsFiles.length} .hbs files remain: ${hbsFiles.slice(0, 3).join(', ')}`);
77
+ }
78
+ }, passed, failed);
79
+
80
+ // 4. agents/ has .md files
81
+ await check('agents/*.md files exist', async () => {
82
+ const agentsDir = join(targetDir, '.context', 'agents');
83
+ const files = await readdir(agentsDir);
84
+ const mdFiles = files.filter(f => f.endsWith('.md'));
85
+ if (mdFiles.length === 0) throw new Error('No .md files in agents/');
86
+ }, passed, failed);
87
+
88
+ // 5. sessions/index.yaml exists
89
+ await check('sessions/index.yaml exists', async () => {
90
+ await access(join(targetDir, '.context', 'sessions', 'index.yaml'));
91
+ }, passed, failed);
92
+
93
+ // 6. spec-site/node_modules exists (unless skipped)
94
+ if (!opts.skipSpecSite) {
95
+ await check('spec-site/node_modules exists', async () => {
96
+ await access(join(targetDir, 'spec-site', 'node_modules'));
97
+ }, passed, failed, warnings);
98
+ }
99
+
100
+ // 7. .gitignore entries
101
+ await check('.gitignore includes user-context.yaml', async () => {
102
+ const content = await readFile(join(targetDir, '.gitignore'), 'utf-8');
103
+ if (!content.includes('user-context.yaml')) {
104
+ throw new Error('user-context.yaml not in .gitignore');
105
+ }
106
+ }, passed, failed, warnings);
107
+
108
+ await check('.gitignore includes .secrets.yaml', async () => {
109
+ const content = await readFile(join(targetDir, '.gitignore'), 'utf-8');
110
+ if (!content.includes('.secrets.yaml')) {
111
+ throw new Error('.secrets.yaml not in .gitignore');
112
+ }
113
+ }, passed, failed, warnings);
114
+
115
+ // Summary
116
+ console.log();
117
+ console.log(' ──────────────────────────────────────');
118
+ if (failed.length === 0 && warnings.length === 0) {
119
+ console.log(' ✅ All checks passed!');
120
+ } else {
121
+ console.log(` ${passed.length} passed, ${failed.length} failed, ${warnings.length} warnings`);
122
+ }
123
+ console.log();
124
+
125
+ return { passed, failed, warnings };
126
+ }
127
+
128
+ async function check(name, fn, passed, failed, warningsList) {
129
+ try {
130
+ await fn();
131
+ console.log(` ✅ ${name}`);
132
+ passed.push(name);
133
+ } catch (e) {
134
+ if (warningsList) {
135
+ console.log(` ⚠️ ${name}: ${e.message}`);
136
+ warningsList.push(name);
137
+ } else {
138
+ console.log(` ❌ ${name}: ${e.message}`);
139
+ failed.push(name);
140
+ }
141
+ }
142
+ }
143
+
144
+ async function findHbsFiles(dir) {
145
+ const results = [];
146
+ async function walk(d) {
147
+ let entries;
148
+ try { entries = await readdir(d, { withFileTypes: true }); } catch { return; }
149
+ for (const entry of entries) {
150
+ const fullPath = join(d, entry.name);
151
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
152
+ if (entry.isDirectory()) {
153
+ await walk(fullPath);
154
+ } else if (entry.name.endsWith('.hbs')) {
155
+ results.push(fullPath.replace(dir + '/', ''));
156
+ }
157
+ }
158
+ }
159
+ await walk(dir);
160
+ return results;
161
+ }
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Hydration engine — Integration registry processing + .hbs → .md rendering.
3
+ *
4
+ * Step A: Collect enabled providers, build capabilities map, inject INTEGRATION_* markers.
5
+ * Step B: Render standard Handlebars templates via template-engine.mjs.
6
+ */
7
+
8
+ import { readdir, readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
9
+ import { join, basename, relative } from 'node:path';
10
+ import { parse as parseYaml } from './yaml-lite.mjs';
11
+ import { render } from './template-engine.mjs';
12
+ import { loadManifest, getDefaultAdapter } from './adapter.mjs';
13
+
14
+ /**
15
+ * Run full hydration on a project directory.
16
+ *
17
+ * @param {string} targetDir - Root project directory
18
+ * @param {object} [opts]
19
+ * @param {boolean} [opts.skipSpecSite]
20
+ * @param {string} [opts.platform]
21
+ * @returns {Promise<{ hydrated: string[], domains: string[] }>}
22
+ */
23
+ export async function hydrate(targetDir, opts = {}) {
24
+ const projectYaml = await readYamlFile(join(targetDir, '.context', 'project.yaml'));
25
+ const registry = await readYamlFile(join(targetDir, '.context', 'integrations', '_registry.yaml'));
26
+
27
+ // Load user-context.yaml (optional, gitignored)
28
+ let userYaml = {};
29
+ try {
30
+ userYaml = await readYamlFile(join(targetDir, '.context', 'user-context.yaml'));
31
+ } catch { /* optional */ }
32
+
33
+ // Load adapter manifest
34
+ const platform = opts.platform || projectYaml._meta?.platform || getDefaultAdapter();
35
+ let manifest;
36
+ try {
37
+ manifest = await loadManifest(platform);
38
+ } catch {
39
+ manifest = null;
40
+ }
41
+
42
+ // 1. Collect enabled providers
43
+ const integrations = projectYaml.operations?.integrations || {};
44
+ const enabledProviders = [];
45
+
46
+ for (const [id, cfg] of Object.entries(integrations)) {
47
+ if (cfg?.enabled) {
48
+ try {
49
+ const providerYaml = await readYamlFile(
50
+ join(targetDir, '.context', 'integrations', 'providers', `${id}.yaml`)
51
+ );
52
+ enabledProviders.push({ id, config: cfg, provider: providerYaml });
53
+ } catch {
54
+ // Provider file not found — skip
55
+ }
56
+ }
57
+ }
58
+
59
+ // 2. Build capabilities map
60
+ const capabilities = {};
61
+ for (const { provider } of enabledProviders) {
62
+ if (provider.category && registry.categories?.[provider.category]) {
63
+ capabilities[provider.category] = true;
64
+ }
65
+ }
66
+
67
+ // 3. Build template context (merge adapter template_vars)
68
+ const ctx = buildContext(projectYaml, capabilities, userYaml, manifest);
69
+
70
+ // 4. Process all .hbs files
71
+ const hydrated = [];
72
+
73
+ // Agent .hbs files
74
+ const agentMap = {
75
+ 'orchestrator.md.hbs': 'orchestrator',
76
+ 'strategist.md.hbs': 'strategist',
77
+ 'market-researcher.md.hbs': 'market-researcher',
78
+ 'planner.md.hbs': 'planner',
79
+ 'handoff-specialist.md.hbs': 'handoff-specialist',
80
+ 'validator.md.hbs': 'validator',
81
+ 'analyst.md.hbs': 'analyst',
82
+ 'researcher.md.hbs': 'researcher',
83
+ 'tracking-governor.md.hbs': 'tracking-governor',
84
+ 'operations.md.hbs': 'operations',
85
+ 'developer.md.hbs': 'developer',
86
+ 'qa.md.hbs': 'qa',
87
+ };
88
+
89
+ const agentsDir = join(targetDir, '.context', 'agents');
90
+ for (const [file, agentName] of Object.entries(agentMap)) {
91
+ const filePath = join(agentsDir, file);
92
+ const result = await hydrateFile(filePath, {
93
+ ctx,
94
+ registry,
95
+ enabledProviders,
96
+ targetType: 'agent',
97
+ targetName: agentName,
98
+ });
99
+ if (result) hydrated.push(relative(targetDir, result));
100
+ }
101
+
102
+ // Command .hbs files — from manifest or fallback
103
+ const commandMap = manifest?.commands?.hydration_map || {
104
+ 'analytics.md.hbs': 'analytics',
105
+ 'daily.md.hbs': 'daily',
106
+ 'dev.md.hbs': 'dev',
107
+ };
108
+
109
+ const commandTargetDir = manifest?.commands?.target_dir || '.claude/commands';
110
+ const commandsDir = join(targetDir, commandTargetDir);
111
+ for (const [file, cmdName] of Object.entries(commandMap)) {
112
+ const filePath = join(commandsDir, file);
113
+ const result = await hydrateFile(filePath, {
114
+ ctx,
115
+ registry,
116
+ enabledProviders,
117
+ targetType: 'command',
118
+ targetName: cmdName,
119
+ });
120
+ if (result) hydrated.push(relative(targetDir, result));
121
+ }
122
+
123
+ // System .hbs files — from manifest or fallback
124
+ const systemFiles = [];
125
+ if (manifest?.system_prompt) {
126
+ const sp = manifest.system_prompt;
127
+ systemFiles.push({
128
+ path: join(targetDir, sp.source),
129
+ name: sp.hydration?.name || sp.target,
130
+ });
131
+ } else {
132
+ systemFiles.push({ path: join(targetDir, 'CLAUDE.md.hbs'), name: 'CLAUDE.md' });
133
+ }
134
+ systemFiles.push({ path: join(targetDir, '.context', 'WORKFLOW.md.hbs'), name: 'WORKFLOW.md' });
135
+
136
+ for (const { path: filePath, name } of systemFiles) {
137
+ const result = await hydrateFile(filePath, {
138
+ ctx,
139
+ registry,
140
+ enabledProviders,
141
+ targetType: 'system',
142
+ targetName: name,
143
+ });
144
+ if (result) hydrated.push(relative(targetDir, result));
145
+ }
146
+
147
+ // Domain command generation
148
+ const domains = projectYaml.operations?.domains || [];
149
+ const domainResults = [];
150
+ const domainTemplateFile = manifest?.commands?.domain_template || '_domain.md.hbs';
151
+ const domainTemplatePath = join(commandsDir, domainTemplateFile);
152
+ let domainTemplate;
153
+ try {
154
+ domainTemplate = await readFile(domainTemplatePath, 'utf-8');
155
+ } catch {
156
+ domainTemplate = null;
157
+ }
158
+
159
+ if (domainTemplate && domains.length > 0) {
160
+ for (const domain of domains) {
161
+ const domainCtx = { ...ctx, ...domain };
162
+ const rendered = render(domainTemplate, domainCtx);
163
+ const outPath = join(commandsDir, `${domain.id}.md`);
164
+ await writeFile(outPath, rendered);
165
+ domainResults.push(relative(targetDir, outPath));
166
+ }
167
+ }
168
+
169
+ // Clean up _domain.md.hbs
170
+ if (domainTemplate) {
171
+ try { await unlink(domainTemplatePath); } catch {}
172
+ }
173
+
174
+ return { hydrated, domains: domainResults };
175
+ }
176
+
177
+ // ── Internals ──────────────────────────────────────────
178
+
179
+ /**
180
+ * Hydrate a single .hbs file:
181
+ * 1. Inject INTEGRATION_* markers
182
+ * 2. Run Handlebars render
183
+ * 3. Write .md, delete .hbs
184
+ *
185
+ * @returns {Promise<string|null>} output path, or null if source not found
186
+ */
187
+ async function hydrateFile(hbsPath, { ctx, registry, enabledProviders, targetType, targetName }) {
188
+ let template;
189
+ try {
190
+ template = await readFile(hbsPath, 'utf-8');
191
+ } catch {
192
+ return null; // .hbs not found
193
+ }
194
+
195
+ // Step A: Integration marker injection
196
+ template = injectMarkers(template, { registry, enabledProviders, targetType, targetName, ctx });
197
+
198
+ // Step B: Handlebars rendering
199
+ const output = render(template, ctx);
200
+
201
+ // Write output
202
+ const outPath = hbsPath.replace(/\.hbs$/, '');
203
+ await writeFile(outPath, output);
204
+
205
+ // Remove .hbs source
206
+ try { await unlink(hbsPath); } catch {}
207
+
208
+ return outPath;
209
+ }
210
+
211
+ /**
212
+ * Inject INTEGRATION_* markers into a template string.
213
+ */
214
+ function injectMarkers(template, { registry, enabledProviders, targetType, targetName, ctx }) {
215
+ let result = template;
216
+
217
+ if (targetType === 'agent') {
218
+ // {{INTEGRATION_PROMPTS}} — collect agent_prompts.{agentName} from relevant providers
219
+ const prompts = collectPromptsForAgent(targetName, registry, enabledProviders, ctx);
220
+ result = result.replace('{{INTEGRATION_PROMPTS}}', prompts);
221
+
222
+ // {{INTEGRATION_TOOLS_FOOTER}} — collect footer_tool_line from all enabled providers for this agent
223
+ const footers = collectFootersForAgent(targetName, registry, enabledProviders, ctx);
224
+ result = result.replace('{{INTEGRATION_TOOLS_FOOTER}}', footers);
225
+
226
+ // {{INTEGRATION_CAUTION_LIST}} — from database providers
227
+ const cautions = collectCautionList(registry, enabledProviders, ctx);
228
+ result = result.replace('{{INTEGRATION_CAUTION_LIST}}', cautions);
229
+ } else if (targetType === 'command') {
230
+ // {{INTEGRATION_PROMPTS}} — collect command_prompts.{cmdName} from relevant providers
231
+ const prompts = collectPromptsForCommand(targetName, registry, enabledProviders, ctx);
232
+ result = result.replace('{{INTEGRATION_PROMPTS}}', prompts);
233
+ } else if (targetType === 'system') {
234
+ if (targetName === 'CLAUDE.md') {
235
+ const safetyRules = collectSystemContent('safety_rules', registry, enabledProviders, ctx);
236
+ result = result.replace('{{INTEGRATION_SAFETY_RULES}}', safetyRules);
237
+ } else if (targetName === 'WORKFLOW.md') {
238
+ const workflowRules = collectSystemContent('workflow_rules', registry, enabledProviders, ctx);
239
+ result = result.replace('{{INTEGRATION_WORKFLOW_RULES}}', workflowRules);
240
+ }
241
+ }
242
+
243
+ return result;
244
+ }
245
+
246
+ /**
247
+ * Find providers relevant to an agent name, then collect their agent_prompts.
248
+ */
249
+ function collectPromptsForAgent(agentName, registry, enabledProviders, ctx) {
250
+ const categories = registry.categories || {};
251
+ // Which categories list this agent?
252
+ const relevantCategories = new Set();
253
+ for (const [catId, cat] of Object.entries(categories)) {
254
+ const agents = cat.agents || [];
255
+ if (agents.includes(agentName)) relevantCategories.add(catId);
256
+ }
257
+
258
+ const parts = [];
259
+ for (const { provider, config } of enabledProviders) {
260
+ if (!relevantCategories.has(provider.category)) continue;
261
+ const prompt = provider.agent_prompts?.[agentName];
262
+ if (prompt) parts.push(resolveConfigVars(prompt, config, ctx));
263
+ }
264
+ return parts.join('\n');
265
+ }
266
+
267
+ /**
268
+ * Collect footer tool lines for an agent.
269
+ */
270
+ function collectFootersForAgent(agentName, registry, enabledProviders, ctx) {
271
+ const categories = registry.categories || {};
272
+ const relevantCategories = new Set();
273
+ for (const [catId, cat] of Object.entries(categories)) {
274
+ if ((cat.agents || []).includes(agentName)) relevantCategories.add(catId);
275
+ }
276
+
277
+ const lines = [];
278
+ for (const { provider, config } of enabledProviders) {
279
+ if (!relevantCategories.has(provider.category)) continue;
280
+ if (provider.footer_tool_line) {
281
+ lines.push(resolveConfigVars(provider.footer_tool_line, config, ctx));
282
+ }
283
+ }
284
+ return lines.join(', ');
285
+ }
286
+
287
+ /**
288
+ * Collect caution_list from database-category providers.
289
+ */
290
+ function collectCautionList(registry, enabledProviders, ctx) {
291
+ const parts = [];
292
+ for (const { provider, config } of enabledProviders) {
293
+ if (provider.category !== 'database') continue;
294
+ if (provider.caution_list) {
295
+ parts.push(resolveConfigVars(provider.caution_list, config, ctx));
296
+ }
297
+ }
298
+ return parts.join('\n');
299
+ }
300
+
301
+ /**
302
+ * Collect prompts for a command name.
303
+ */
304
+ function collectPromptsForCommand(cmdName, registry, enabledProviders, ctx) {
305
+ const categories = registry.categories || {};
306
+ const relevantCategories = new Set();
307
+ for (const [catId, cat] of Object.entries(categories)) {
308
+ if ((cat.commands || []).includes(cmdName)) relevantCategories.add(catId);
309
+ }
310
+
311
+ const parts = [];
312
+ for (const { provider, config } of enabledProviders) {
313
+ if (!relevantCategories.has(provider.category)) continue;
314
+ const prompt = provider.command_prompts?.[cmdName];
315
+ if (prompt) parts.push(resolveConfigVars(prompt, config, ctx));
316
+ }
317
+ return parts.join('\n');
318
+ }
319
+
320
+ /**
321
+ * Collect system-level content (safety_rules or workflow_rules).
322
+ */
323
+ function collectSystemContent(fieldName, registry, enabledProviders, ctx) {
324
+ const categories = registry.categories || {};
325
+ // Which categories affect system files?
326
+ const relevantCategories = new Set();
327
+ for (const [catId, cat] of Object.entries(categories)) {
328
+ const sysFiles = cat.system_files || [];
329
+ const targetFile = fieldName === 'safety_rules' ? 'CLAUDE.md' : 'WORKFLOW.md';
330
+ if (sysFiles.includes(targetFile)) relevantCategories.add(catId);
331
+ }
332
+
333
+ const parts = [];
334
+ for (const { provider, config } of enabledProviders) {
335
+ if (!relevantCategories.has(provider.category)) continue;
336
+ const rules = provider[fieldName];
337
+ if (Array.isArray(rules)) {
338
+ for (const rule of rules) {
339
+ if (rule.content) parts.push(resolveConfigVars(rule.content, config, ctx));
340
+ }
341
+ }
342
+ }
343
+ return parts.join('\n');
344
+ }
345
+
346
+ /**
347
+ * Resolve {{config.KEY}} and {{#each config.KEY}} within a provider prompt string.
348
+ * Also handles {{{config.KEY}}} (triple-brace, unescaped — same as double for us).
349
+ */
350
+ function resolveConfigVars(str, providerConfig, ctx) {
351
+ let result = str;
352
+
353
+ // Handle triple-brace (unescaped) — treat same as double
354
+ result = result.replace(/\{\{\{config\.([a-zA-Z0-9_]+)\}\}\}/g, (_, key) => {
355
+ return providerConfig[key] ?? '';
356
+ });
357
+
358
+ // Handle {{#each config.KEY}}...{{/each}} blocks
359
+ result = result.replace(
360
+ /\{\{#each config\.([a-zA-Z0-9_]+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
361
+ (_, key, body) => {
362
+ const arr = providerConfig[key];
363
+ if (!Array.isArray(arr) || arr.length === 0) return '';
364
+ return arr.map(item => {
365
+ let line = body;
366
+ // Replace {{this.field}} references
367
+ line = line.replace(/\{\{this\.([a-zA-Z0-9_]+)\}\}/g, (__, field) => {
368
+ return item[field] ?? '';
369
+ });
370
+ return line;
371
+ }).join('');
372
+ }
373
+ );
374
+
375
+ // Handle simple {{config.KEY}} placeholders
376
+ result = result.replace(/\{\{config\.([a-zA-Z0-9_]+)\}\}/g, (_, key) => {
377
+ return providerConfig[key] ?? '';
378
+ });
379
+
380
+ return result;
381
+ }
382
+
383
+ /**
384
+ * Build a flat context object for Handlebars rendering.
385
+ * Merges adapter template_vars if provided.
386
+ */
387
+ function buildContext(projectYaml, capabilities, userYaml = {}, manifest = null) {
388
+ const project = projectYaml.project || {};
389
+ const ops = projectYaml.operations || {};
390
+ const domains = ops.domains || [];
391
+ const devScope = ops.dev_scope || {};
392
+ const specSite = ops.spec_site || {};
393
+ const integrations = ops.integrations || {};
394
+
395
+ const ctx = {
396
+ project,
397
+ domains,
398
+ dev_scope: devScope,
399
+ spec_site: specSite,
400
+ integrations,
401
+ capabilities,
402
+ user: userYaml.user || {},
403
+ };
404
+
405
+ // Merge adapter template variables
406
+ if (manifest?.template_vars) {
407
+ for (const [key, value] of Object.entries(manifest.template_vars)) {
408
+ ctx[key] = value;
409
+ }
410
+ }
411
+
412
+ return ctx;
413
+ }
414
+
415
+ /**
416
+ * Read and parse a YAML file.
417
+ */
418
+ async function readYamlFile(filePath) {
419
+ const content = await readFile(filePath, 'utf-8');
420
+ return parseYaml(content);
421
+ }