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.
- package/README.md +372 -0
- package/adapters/claude-code/.claude/commands/_domain.md.hbs +32 -0
- package/adapters/claude-code/.claude/commands/analytics.md.hbs +55 -0
- package/adapters/claude-code/.claude/commands/daily.md.hbs +301 -0
- package/adapters/claude-code/.claude/commands/dev.md.hbs +62 -0
- package/adapters/claude-code/.claude/commands/handoff.md +258 -0
- package/adapters/claude-code/.claude/commands/market.md +120 -0
- package/adapters/claude-code/.claude/commands/metrics.md +123 -0
- package/adapters/claude-code/.claude/commands/oscar-loop.md +436 -0
- package/adapters/claude-code/.claude/commands/party.md +85 -0
- package/adapters/claude-code/.claude/commands/plan.md +43 -0
- package/adapters/claude-code/.claude/commands/research.md +203 -0
- package/adapters/claude-code/.claude/commands/retro.md +68 -0
- package/adapters/claude-code/.claude/commands/save.md +440 -0
- package/adapters/claude-code/.claude/commands/sessions.md +139 -0
- package/adapters/claude-code/.claude/commands/sprint.md +106 -0
- package/adapters/claude-code/.claude/commands/start.md +368 -0
- package/adapters/claude-code/.claude/commands/strategy.md +41 -0
- package/adapters/claude-code/.claude/commands/task.md +220 -0
- package/adapters/claude-code/.claude/commands/tracking.md +116 -0
- package/adapters/claude-code/.claude/commands/validate.md +58 -0
- package/adapters/claude-code/CLAUDE.md.hbs +208 -0
- package/adapters/claude-code/manifest.yaml +36 -0
- package/bin/cli.mjs +218 -0
- package/lib/adapter.mjs +68 -0
- package/lib/doctor.mjs +161 -0
- package/lib/hydrate.mjs +421 -0
- package/lib/prompt.mjs +78 -0
- package/lib/scaffold.mjs +155 -0
- package/lib/setup-wizard.mjs +331 -0
- package/lib/template-engine.mjs +164 -0
- package/lib/yaml-lite.mjs +476 -0
- package/package.json +30 -0
- package/scaffold/.context/.secrets.yaml.example +20 -0
- package/scaffold/.context/WORKFLOW.md.hbs +332 -0
- package/scaffold/.context/agents/TEMPLATE.md +115 -0
- package/scaffold/.context/agents/analyst.md.hbs +362 -0
- package/scaffold/.context/agents/developer.md.hbs +390 -0
- package/scaffold/.context/agents/handoff-specialist.md.hbs +292 -0
- package/scaffold/.context/agents/market-researcher.md.hbs +288 -0
- package/scaffold/.context/agents/ollie.md +323 -0
- package/scaffold/.context/agents/operations.md.hbs +293 -0
- package/scaffold/.context/agents/orchestrator.md.hbs +434 -0
- package/scaffold/.context/agents/planner.md.hbs +405 -0
- package/scaffold/.context/agents/qa.md.hbs +409 -0
- package/scaffold/.context/agents/researcher.md.hbs +330 -0
- package/scaffold/.context/agents/sage.md +349 -0
- package/scaffold/.context/agents/strategist.md.hbs +339 -0
- package/scaffold/.context/agents/tracking-governor.md.hbs +291 -0
- package/scaffold/.context/agents/validator.md.hbs +365 -0
- package/scaffold/.context/integrations/_registry.yaml +38 -0
- package/scaffold/.context/integrations/providers/channel_io.yaml +38 -0
- package/scaffold/.context/integrations/providers/corti.yaml +203 -0
- package/scaffold/.context/integrations/providers/ga4.yaml +116 -0
- package/scaffold/.context/integrations/providers/intercom.yaml +47 -0
- package/scaffold/.context/integrations/providers/linear.yaml +46 -0
- package/scaffold/.context/integrations/providers/mixpanel.yaml +73 -0
- package/scaffold/.context/integrations/providers/notebooklm.yaml +74 -0
- package/scaffold/.context/integrations/providers/notion.yaml +129 -0
- package/scaffold/.context/integrations/providers/prod_db.yaml +183 -0
- package/scaffold/.context/oscar/workflows/multi-agent.md +82 -0
- package/scaffold/.context/oscar/workflows/ollie-sage.md +128 -0
- package/scaffold/.context/oscar/workflows/session-git.md +71 -0
- package/scaffold/.context/oscar/workflows/setup.md +663 -0
- package/scaffold/.context/oscar/workflows/tracking.md +118 -0
- package/scaffold/.context/project.yaml.example +102 -0
- package/scaffold/.context/templates/dev-guide.md +217 -0
- package/scaffold/.context/templates/epic-spec.md +225 -0
- package/scaffold/.context/templates/guardrail.md +94 -0
- package/scaffold/.context/templates/handoff-checklist.md +197 -0
- package/scaffold/.context/templates/prd.md +80 -0
- package/scaffold/.context/templates/retrospective.md +78 -0
- package/scaffold/.context/templates/screen-spec.md +714 -0
- package/scaffold/.context/templates/sprint-plan.md +72 -0
- package/scaffold/.context/templates/sprint-status.yaml +109 -0
- package/scaffold/.context/templates/story-v2.md +228 -0
- package/scaffold/.context/templates/validation-report.md +99 -0
- package/scaffold/.gitignore.append +7 -0
- package/scaffold/spec-site/env.d.ts +7 -0
- package/scaffold/spec-site/index.html +14 -0
- package/scaffold/spec-site/package.json +20 -0
- package/scaffold/spec-site/src/App.vue +27 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +10 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +10 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +14 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +14 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +21 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +21 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +20 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +20 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +11 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +11 -0
- package/scaffold/spec-site/src/components/Accordion.vue +108 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +304 -0
- package/scaffold/spec-site/src/components/Badge.vue +25 -0
- package/scaffold/spec-site/src/components/CoachingCard.vue +112 -0
- package/scaffold/spec-site/src/components/MemoSidebar.vue +239 -0
- package/scaffold/spec-site/src/components/MockupShell.vue +100 -0
- package/scaffold/spec-site/src/components/RuleTable.vue +99 -0
- package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +103 -0
- package/scaffold/spec-site/src/components/SpecNav.vue +26 -0
- package/scaffold/spec-site/src/components/SpecSection.vue +59 -0
- package/scaffold/spec-site/src/components/SummaryGrid.vue +39 -0
- package/scaffold/spec-site/src/components/VersionBadge.vue +38 -0
- package/scaffold/spec-site/src/composables/useActiveSection.ts +53 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +138 -0
- package/scaffold/spec-site/src/composables/useRetro.ts +313 -0
- package/scaffold/spec-site/src/composables/useScenario.ts +43 -0
- package/scaffold/spec-site/src/composables/useScenarioStore.ts +102 -0
- package/scaffold/spec-site/src/composables/useTurso.ts +160 -0
- package/scaffold/spec-site/src/composables/useUser.ts +25 -0
- package/scaffold/spec-site/src/data/navigation.ts +59 -0
- package/scaffold/spec-site/src/data/types.ts +90 -0
- package/scaffold/spec-site/src/data/wireframeRegistry.ts +25 -0
- package/scaffold/spec-site/src/layouts/SplitPaneLayout.vue +79 -0
- package/scaffold/spec-site/src/main.ts +10 -0
- package/scaffold/spec-site/src/pages/IndexPage.vue +66 -0
- package/scaffold/spec-site/src/pages/PolicyDetail.vue +215 -0
- package/scaffold/spec-site/src/pages/PolicyIndex.vue +74 -0
- package/scaffold/spec-site/src/pages/retro/RetroActions.vue +191 -0
- package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +192 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +131 -0
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +287 -0
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +178 -0
- package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +34 -0
- package/scaffold/spec-site/src/pages/shared/PlaceholderContent.vue +22 -0
- package/scaffold/spec-site/src/pages/shared/PlaceholderSpecPanel.vue +16 -0
- package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +145 -0
- package/scaffold/spec-site/src/pages/wireframe/WireframeShell.vue +151 -0
- package/scaffold/spec-site/src/router.ts +85 -0
- package/scaffold/spec-site/src/styles/base.css +21 -0
- package/scaffold/spec-site/src/styles/split-pane.css +143 -0
- package/scaffold/spec-site/src/styles/variables.css +47 -0
- package/scaffold/spec-site/src/utils/markdown.ts +197 -0
- package/scaffold/spec-site/tsconfig.json +20 -0
- package/scaffold/spec-site/vite.config.ts +18 -0
package/lib/adapter.mjs
ADDED
|
@@ -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
|
+
}
|
package/lib/hydrate.mjs
ADDED
|
@@ -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
|
+
}
|