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,164 @@
1
+ /**
2
+ * Minimal Handlebars-like template engine
3
+ * Supports: {{var}}, {{nested.var}}, {{#if}}, {{#each}}, {{#unless}}, {{else}}
4
+ * Handles code fences and inline code containing template-like syntax.
5
+ */
6
+
7
+ /**
8
+ * Resolve a dotted path like "integrations.ga4.property_id" from context object
9
+ */
10
+ function resolve(ctx, path) {
11
+ return path.split('.').reduce((obj, key) => obj?.[key], ctx);
12
+ }
13
+
14
+ // ── Code fence protection ──────────────────────────────
15
+ // Temporarily replace {{ }} tokens inside code fences and inline code
16
+ // so they aren't matched by block regexes.
17
+
18
+ const PLACEHOLDER_PREFIX = '\x00HBS_';
19
+
20
+ function protect(template) {
21
+ const map = new Map();
22
+ let counter = 0;
23
+
24
+ // Protect fenced code blocks: ```...```
25
+ let result = template.replace(/```[\s\S]*?```/g, (match) => {
26
+ return match.replace(/\{\{[\s\S]*?\}\}/g, (token) => {
27
+ const key = `${PLACEHOLDER_PREFIX}${counter++}\x00`;
28
+ map.set(key, token);
29
+ return key;
30
+ });
31
+ });
32
+
33
+ // Protect inline code: `...`
34
+ result = result.replace(/`[^`\n]+`/g, (match) => {
35
+ return match.replace(/\{\{[\s\S]*?\}\}/g, (token) => {
36
+ const key = `${PLACEHOLDER_PREFIX}${counter++}\x00`;
37
+ map.set(key, token);
38
+ return key;
39
+ });
40
+ });
41
+
42
+ return { text: result, map };
43
+ }
44
+
45
+ function restore(str, map) {
46
+ let result = str;
47
+ for (const [key, original] of map) {
48
+ result = result.replaceAll(key, original);
49
+ }
50
+ return result;
51
+ }
52
+
53
+ // ── Main render ────────────────────────────────────────
54
+
55
+ /**
56
+ * Process a template string with the given context
57
+ * @param {string} template - Template string with {{var}}, {{#if}}, {{#each}} blocks
58
+ * @param {object} ctx - Context object for variable resolution
59
+ * @returns {string} Processed string
60
+ */
61
+ export function render(template, ctx) {
62
+ // Protect code-fence contents from block matching
63
+ const { text: protected_, map } = protect(template);
64
+
65
+ // Process {{#each}}, {{#if}}, {{#unless}} blocks
66
+ let result = processBlocks(protected_, ctx);
67
+
68
+ // Process simple {{var}} replacements
69
+ result = result.replace(/\{\{([a-zA-Z0-9_.]+)\}\}/g, (_, path) => {
70
+ const val = resolve(ctx, path);
71
+ return val != null ? String(val) : '';
72
+ });
73
+
74
+ // Restore protected tokens
75
+ result = restore(result, map);
76
+
77
+ return result;
78
+ }
79
+
80
+ /**
81
+ * Internal render for block bodies — does NOT protect/restore again
82
+ * since the outer render() already did it.
83
+ */
84
+ function renderInner(template, ctx) {
85
+ let result = processBlocks(template, ctx);
86
+
87
+ result = result.replace(/\{\{([a-zA-Z0-9_.]+)\}\}/g, (_, path) => {
88
+ const val = resolve(ctx, path);
89
+ return val != null ? String(val) : '';
90
+ });
91
+
92
+ return result;
93
+ }
94
+
95
+ function processBlocks(str, ctx) {
96
+ let result = str;
97
+ let changed = true;
98
+
99
+ while (changed) {
100
+ changed = false;
101
+
102
+ // {{#each path}}...{{/each}}
103
+ result = result.replace(
104
+ /\{\{#each\s+([a-zA-Z0-9_.]+)\}\}([\s\S]*?)\{\{\/each\}\}/,
105
+ (_, path, body) => {
106
+ changed = true;
107
+ const arr = resolve(ctx, path);
108
+ if (!Array.isArray(arr) || arr.length === 0) return '';
109
+
110
+ // Trim exactly one leading and one trailing newline from body
111
+ // so that template formatting newlines don't accumulate per item
112
+ const trimmedBody = body.replace(/^\n/, '').replace(/\n$/, '');
113
+
114
+ return arr.map((item, index) => {
115
+ const itemCtx = typeof item === 'object'
116
+ ? { ...ctx, ...item, '@index': index, '@first': index === 0, '@last': index === arr.length - 1 }
117
+ : { ...ctx, this: item, '@index': index, '@first': index === 0, '@last': index === arr.length - 1 };
118
+ return renderInner(trimmedBody, itemCtx);
119
+ }).join('\n');
120
+ }
121
+ );
122
+
123
+ // {{#if path}}...{{else}}...{{/if}}
124
+ result = result.replace(
125
+ /\{\{#if\s+([a-zA-Z0-9_.]+)\}\}([\s\S]*?)\{\{else\}\}([\s\S]*?)\{\{\/if\}\}/,
126
+ (_, path, ifBody, elseBody) => {
127
+ changed = true;
128
+ const val = resolve(ctx, path);
129
+ return isTruthy(val) ? renderInner(ifBody, ctx) : renderInner(elseBody, ctx);
130
+ }
131
+ );
132
+
133
+ // {{#if path}}...{{/if}} (no else)
134
+ result = result.replace(
135
+ /\{\{#if\s+([a-zA-Z0-9_.]+)\}\}([\s\S]*?)\{\{\/if\}\}/,
136
+ (_, path, body) => {
137
+ changed = true;
138
+ const val = resolve(ctx, path);
139
+ return isTruthy(val) ? renderInner(body, ctx) : '';
140
+ }
141
+ );
142
+
143
+ // {{#unless path}}...{{/unless}}
144
+ result = result.replace(
145
+ /\{\{#unless\s+([a-zA-Z0-9_.]+)\}\}([\s\S]*?)\{\{\/unless\}\}/,
146
+ (_, path, body) => {
147
+ changed = true;
148
+ const val = resolve(ctx, path);
149
+ return !isTruthy(val) ? renderInner(body, ctx) : '';
150
+ }
151
+ );
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ function isTruthy(val) {
158
+ if (val == null) return false;
159
+ if (val === false) return false;
160
+ if (val === '' ) return false;
161
+ if (val === 0) return false;
162
+ if (Array.isArray(val) && val.length === 0) return false;
163
+ return true;
164
+ }
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Minimal YAML parser — zero dependencies.
3
+ * Supports the subset used by project.yaml, _registry.yaml, and provider YAML files:
4
+ * - Scalars (string, number, boolean, null)
5
+ * - Inline flow sequences: [a, b, c]
6
+ * - Inline flow mappings: { key: val, key2: val2 }
7
+ * - Block sequences (- item)
8
+ * - Nested maps (indented keys)
9
+ * - Multi-line literal blocks (|)
10
+ * - Comments (#)
11
+ * - Quoted strings ("..." and '...')
12
+ *
13
+ * NOT supported: anchors/aliases (&, *), tags (!!), complex flow styles, merge keys (<<)
14
+ */
15
+
16
+ /**
17
+ * Parse a YAML string into a JS object.
18
+ * @param {string} yaml
19
+ * @returns {any}
20
+ */
21
+ export function parse(yaml) {
22
+ const lines = yaml.split('\n');
23
+ const { value } = parseNode(lines, 0, -1);
24
+ return value;
25
+ }
26
+
27
+ /**
28
+ * Serialize a JS object to YAML string.
29
+ * @param {any} obj
30
+ * @param {number} [indent=0]
31
+ * @returns {string}
32
+ */
33
+ export function stringify(obj, indent = 0) {
34
+ if (obj == null) return 'null';
35
+ if (typeof obj === 'boolean') return obj ? 'true' : 'false';
36
+ if (typeof obj === 'number') return String(obj);
37
+ if (typeof obj === 'string') return stringifyString(obj, indent);
38
+ if (Array.isArray(obj)) return stringifyArray(obj, indent);
39
+ return stringifyMap(obj, indent);
40
+ }
41
+
42
+ // ── Parser internals ────────────────────────────────────
43
+
44
+ function parseNode(lines, startIdx, parentIndent) {
45
+ if (startIdx >= lines.length) return { value: null, nextIdx: startIdx };
46
+
47
+ // Skip blank lines and comments to find the first meaningful line
48
+ let idx = startIdx;
49
+ while (idx < lines.length) {
50
+ const stripped = lines[idx].replace(/#.*$/, '').trim();
51
+ if (stripped !== '') break;
52
+ idx++;
53
+ }
54
+ if (idx >= lines.length) return { value: null, nextIdx: idx };
55
+
56
+ const line = lines[idx];
57
+ const lineIndent = getIndent(line);
58
+ const content = stripComment(line).trim();
59
+
60
+ // If this line starts a sequence item at the current level
61
+ if (content.startsWith('- ') || content === '-') {
62
+ return parseBlockSequence(lines, idx, lineIndent);
63
+ }
64
+
65
+ // If this line is a mapping key
66
+ if (content.includes(':')) {
67
+ return parseBlockMapping(lines, idx, lineIndent);
68
+ }
69
+
70
+ // Standalone scalar
71
+ return { value: parseScalar(content), nextIdx: idx + 1 };
72
+ }
73
+
74
+ function parseBlockMapping(lines, startIdx, mapIndent) {
75
+ const result = {};
76
+ let idx = startIdx;
77
+
78
+ while (idx < lines.length) {
79
+ // Skip blank lines and comments
80
+ const raw = lines[idx];
81
+ const stripped = stripComment(raw).trim();
82
+ if (stripped === '') { idx++; continue; }
83
+
84
+ const lineIndent = getIndent(raw);
85
+
86
+ // If dedented past our map level, we're done
87
+ if (lineIndent < mapIndent) break;
88
+ // If at a deeper indent and we're not at the start, it's a child — break
89
+ if (lineIndent > mapIndent && idx !== startIdx) break;
90
+ // If at the same indent but not a key, break
91
+ if (lineIndent === mapIndent && !stripped.includes(':') && !stripped.startsWith('#')) break;
92
+
93
+ // Handle comment-only lines
94
+ if (stripped.startsWith('#')) { idx++; continue; }
95
+
96
+ // Extract key:value
97
+ const colonMatch = stripped.match(/^([^\s:][^:]*?|"[^"]*"|'[^']*')\s*:\s*(.*)/);
98
+ if (!colonMatch) { idx++; continue; }
99
+
100
+ let key = colonMatch[1].trim();
101
+ // Unquote key
102
+ if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
103
+ key = key.slice(1, -1);
104
+ }
105
+ const inlineVal = colonMatch[2].trim();
106
+
107
+ if (inlineVal === '|') {
108
+ // Literal block scalar
109
+ const { value, nextIdx } = parseLiteralBlock(lines, idx + 1, mapIndent);
110
+ result[key] = value;
111
+ idx = nextIdx;
112
+ } else if (inlineVal === '>') {
113
+ // Folded block scalar
114
+ const { value, nextIdx } = parseFoldedBlock(lines, idx + 1, mapIndent);
115
+ result[key] = value;
116
+ idx = nextIdx;
117
+ } else if (inlineVal) {
118
+ // Inline value
119
+ result[key] = parseInlineValue(inlineVal);
120
+ idx++;
121
+ } else {
122
+ // Value on next lines (nested map or sequence)
123
+ idx++;
124
+ // Find next non-blank, non-comment line to determine child structure
125
+ let childIdx = idx;
126
+ while (childIdx < lines.length) {
127
+ const cs = stripComment(lines[childIdx]).trim();
128
+ if (cs !== '') break;
129
+ childIdx++;
130
+ }
131
+ if (childIdx >= lines.length || getIndent(lines[childIdx]) <= mapIndent) {
132
+ // No child — empty value
133
+ result[key] = null;
134
+ } else {
135
+ const childIndent = getIndent(lines[childIdx]);
136
+ const childContent = stripComment(lines[childIdx]).trim();
137
+ if (childContent.startsWith('- ') || childContent === '-') {
138
+ const { value, nextIdx } = parseBlockSequence(lines, childIdx, childIndent);
139
+ result[key] = value;
140
+ idx = nextIdx;
141
+ } else {
142
+ const { value, nextIdx } = parseBlockMapping(lines, childIdx, childIndent);
143
+ result[key] = value;
144
+ idx = nextIdx;
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ return { value: result, nextIdx: idx };
151
+ }
152
+
153
+ function parseBlockSequence(lines, startIdx, seqIndent) {
154
+ const result = [];
155
+ let idx = startIdx;
156
+
157
+ while (idx < lines.length) {
158
+ const raw = lines[idx];
159
+ const stripped = stripComment(raw).trim();
160
+ if (stripped === '') { idx++; continue; }
161
+
162
+ const lineIndent = getIndent(raw);
163
+ if (lineIndent < seqIndent) break;
164
+ if (lineIndent > seqIndent) break; // unexpected deeper indent outside a sequence item
165
+
166
+ if (!stripped.startsWith('-')) break;
167
+
168
+ // Get the content after "- "
169
+ const afterDash = stripped.slice(1).trim();
170
+
171
+ if (afterDash === '' || afterDash === '|') {
172
+ if (afterDash === '|') {
173
+ const { value, nextIdx } = parseLiteralBlock(lines, idx + 1, seqIndent);
174
+ result.push(value);
175
+ idx = nextIdx;
176
+ } else {
177
+ // Block item with nested content on next lines
178
+ idx++;
179
+ let childIdx = idx;
180
+ while (childIdx < lines.length && stripComment(lines[childIdx]).trim() === '') childIdx++;
181
+ if (childIdx < lines.length && getIndent(lines[childIdx]) > seqIndent) {
182
+ const childIndent = getIndent(lines[childIdx]);
183
+ const childContent = stripComment(lines[childIdx]).trim();
184
+ if (childContent.startsWith('- ')) {
185
+ const { value, nextIdx } = parseBlockSequence(lines, childIdx, childIndent);
186
+ result.push(value);
187
+ idx = nextIdx;
188
+ } else {
189
+ const { value, nextIdx } = parseBlockMapping(lines, childIdx, childIndent);
190
+ result.push(value);
191
+ idx = nextIdx;
192
+ }
193
+ } else {
194
+ result.push(null);
195
+ }
196
+ }
197
+ } else if (afterDash.startsWith('{')) {
198
+ // Inline flow mapping
199
+ result.push(parseInlineValue(afterDash));
200
+ idx++;
201
+ } else if (afterDash.startsWith('[')) {
202
+ result.push(parseInlineValue(afterDash));
203
+ idx++;
204
+ } else if (afterDash.includes(':')) {
205
+ // Mapping starting on the "- " line
206
+ // e.g., "- key: value" — parse as a mapping item
207
+ // The first key-value is on this line, more may follow indented
208
+ const itemIndent = seqIndent + 2; // standard indent after "- "
209
+ // Reconstruct as if the key:value is at itemIndent
210
+ const tempLine = ' '.repeat(itemIndent) + afterDash;
211
+ const tempLines = [tempLine, ...lines.slice(idx + 1)];
212
+ const { value, nextIdx } = parseBlockMapping(tempLines, 0, itemIndent);
213
+ // nextIdx is relative to tempLines; adjust
214
+ idx = idx + 1 + (nextIdx - 1);
215
+ result.push(value);
216
+ } else {
217
+ // Simple scalar item
218
+ result.push(parseScalar(afterDash));
219
+ idx++;
220
+ }
221
+ }
222
+
223
+ return { value: result, nextIdx: idx };
224
+ }
225
+
226
+ function parseLiteralBlock(lines, startIdx, parentIndent) {
227
+ let idx = startIdx;
228
+ // Determine block indent from first non-empty line
229
+ while (idx < lines.length && lines[idx].trim() === '') idx++;
230
+ if (idx >= lines.length) return { value: '', nextIdx: idx };
231
+
232
+ const blockIndent = getIndent(lines[idx]);
233
+ if (blockIndent <= parentIndent) return { value: '', nextIdx: idx };
234
+
235
+ const collected = [];
236
+ while (idx < lines.length) {
237
+ const raw = lines[idx];
238
+ if (raw.trim() === '') {
239
+ collected.push('');
240
+ idx++;
241
+ continue;
242
+ }
243
+ const li = getIndent(raw);
244
+ if (li < blockIndent) break;
245
+ collected.push(raw.slice(blockIndent));
246
+ idx++;
247
+ }
248
+
249
+ // Trim trailing empty lines
250
+ while (collected.length > 0 && collected[collected.length - 1] === '') collected.pop();
251
+ return { value: collected.join('\n'), nextIdx: idx };
252
+ }
253
+
254
+ function parseFoldedBlock(lines, startIdx, parentIndent) {
255
+ // Same as literal for our purposes, but join with spaces instead of newlines
256
+ const { value, nextIdx } = parseLiteralBlock(lines, startIdx, parentIndent);
257
+ // For simplicity, keep as-is (folded blocks in our providers use it like literal)
258
+ return { value, nextIdx };
259
+ }
260
+
261
+ // ── Inline value parsing ────────────────────────────────
262
+
263
+ function parseInlineValue(str) {
264
+ const s = str.trim();
265
+ if (s.startsWith('[') && s.endsWith(']')) return parseFlowSequence(s);
266
+ if (s.startsWith('{') && s.endsWith('}')) return parseFlowMapping(s);
267
+ return parseScalar(s);
268
+ }
269
+
270
+ function parseFlowSequence(str) {
271
+ const inner = str.slice(1, -1).trim();
272
+ if (inner === '') return [];
273
+ return splitFlow(inner).map(item => parseInlineValue(item.trim()));
274
+ }
275
+
276
+ function parseFlowMapping(str) {
277
+ const inner = str.slice(1, -1).trim();
278
+ if (inner === '') return {};
279
+ const result = {};
280
+ const pairs = splitFlow(inner);
281
+ for (const pair of pairs) {
282
+ const colonIdx = pair.indexOf(':');
283
+ if (colonIdx === -1) continue;
284
+ let key = pair.slice(0, colonIdx).trim();
285
+ if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
286
+ key = key.slice(1, -1);
287
+ }
288
+ const val = pair.slice(colonIdx + 1).trim();
289
+ result[key] = parseInlineValue(val);
290
+ }
291
+ return result;
292
+ }
293
+
294
+ /** Split flow-style items, respecting nested brackets and quotes */
295
+ function splitFlow(str) {
296
+ const items = [];
297
+ let depth = 0;
298
+ let current = '';
299
+ let inQuote = null;
300
+
301
+ for (let i = 0; i < str.length; i++) {
302
+ const ch = str[i];
303
+ if (inQuote) {
304
+ current += ch;
305
+ if (ch === inQuote) inQuote = null;
306
+ } else if (ch === '"' || ch === "'") {
307
+ inQuote = ch;
308
+ current += ch;
309
+ } else if (ch === '[' || ch === '{') {
310
+ depth++;
311
+ current += ch;
312
+ } else if (ch === ']' || ch === '}') {
313
+ depth--;
314
+ current += ch;
315
+ } else if (ch === ',' && depth === 0) {
316
+ items.push(current);
317
+ current = '';
318
+ } else {
319
+ current += ch;
320
+ }
321
+ }
322
+ if (current.trim()) items.push(current);
323
+ return items;
324
+ }
325
+
326
+ function parseScalar(str) {
327
+ let s = str.trim();
328
+
329
+ // Handle comments after value
330
+ // But not inside quotes
331
+ if (!s.startsWith('"') && !s.startsWith("'")) {
332
+ const commentIdx = findCommentStart(s);
333
+ if (commentIdx > 0) s = s.slice(0, commentIdx).trim();
334
+ }
335
+
336
+ // Quoted strings
337
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
338
+ return s.slice(1, -1);
339
+ }
340
+ // Boolean
341
+ if (s === 'true') return true;
342
+ if (s === 'false') return false;
343
+ // Null
344
+ if (s === 'null' || s === '~' || s === '') return null;
345
+ // Number
346
+ if (/^-?\d+(\.\d+)?$/.test(s)) return Number(s);
347
+ return s;
348
+ }
349
+
350
+ function findCommentStart(str) {
351
+ // Find # that's preceded by whitespace and not inside quotes
352
+ for (let i = 1; i < str.length; i++) {
353
+ if (str[i] === '#' && /\s/.test(str[i - 1])) return i;
354
+ }
355
+ return -1;
356
+ }
357
+
358
+ // ── Comment stripping ───────────────────────────────────
359
+
360
+ function stripComment(line) {
361
+ // Don't strip inside quotes
362
+ let inQuote = null;
363
+ for (let i = 0; i < line.length; i++) {
364
+ const ch = line[i];
365
+ if (inQuote) {
366
+ if (ch === inQuote) inQuote = null;
367
+ } else if (ch === '"' || ch === "'") {
368
+ inQuote = ch;
369
+ } else if (ch === '#') {
370
+ // Only treat as comment if preceded by whitespace or at start
371
+ if (i === 0 || /\s/.test(line[i - 1])) {
372
+ return line.slice(0, i);
373
+ }
374
+ }
375
+ }
376
+ return line;
377
+ }
378
+
379
+ function getIndent(line) {
380
+ const match = line.match(/^(\s*)/);
381
+ return match ? match[1].length : 0;
382
+ }
383
+
384
+ // ── Serializer internals ────────────────────────────────
385
+
386
+ function stringifyString(str, indent) {
387
+ // Use literal block for multi-line strings
388
+ if (str.includes('\n')) {
389
+ const pad = ' '.repeat(indent + 2);
390
+ return '|\n' + str.split('\n').map(l => pad + l).join('\n');
391
+ }
392
+ // Quote if contains special chars
393
+ if (needsQuoting(str)) return `"${str.replace(/"/g, '\\"')}"`;
394
+ return str;
395
+ }
396
+
397
+ function needsQuoting(str) {
398
+ if (str === '') return true;
399
+ if (str === 'true' || str === 'false' || str === 'null' || str === '~') return true;
400
+ if (/^-?\d+(\.\d+)?$/.test(str)) return true;
401
+ if (/[:{}\[\],&*?|>!%@`#]/.test(str)) return true;
402
+ if (str.startsWith(' ') || str.endsWith(' ')) return true;
403
+ return false;
404
+ }
405
+
406
+ function stringifyArray(arr, indent) {
407
+ if (arr.length === 0) return '[]';
408
+ // For arrays of simple scalars, use inline flow
409
+ if (arr.every(v => v == null || typeof v !== 'object')) {
410
+ const items = arr.map(v => {
411
+ if (v == null) return 'null';
412
+ if (typeof v === 'string') return needsQuoting(v) ? `"${v}"` : v;
413
+ return String(v);
414
+ });
415
+ return `[${items.join(', ')}]`;
416
+ }
417
+ // Block sequence for complex items
418
+ const pad = ' '.repeat(indent);
419
+ return arr.map(item => {
420
+ if (item == null) return `${pad}- null`;
421
+ if (typeof item !== 'object') {
422
+ return `${pad}- ${stringify(item, indent + 2)}`;
423
+ }
424
+ // Object item: render keys at indent+2 (after "- "), first key on same line as "-"
425
+ const entries = Object.entries(item);
426
+ const lines = entries.map(([key, val]) => {
427
+ const qKey = needsQuoting(key) ? `"${key}"` : key;
428
+ if (val == null) return `${qKey}: null`;
429
+ if (typeof val === 'string' && val.includes('\n')) {
430
+ return `${qKey}: ${stringifyString(val, indent + 2)}`;
431
+ }
432
+ if (typeof val !== 'object') {
433
+ return `${qKey}: ${stringify(val, indent + 4)}`;
434
+ }
435
+ if (Array.isArray(val)) {
436
+ if (val.length === 0) return `${qKey}: []`;
437
+ if (val.every(v => v == null || typeof v !== 'object')) {
438
+ return `${qKey}: ${stringify(val, indent + 4)}`;
439
+ }
440
+ return `${qKey}:\n${stringify(val, indent + 4)}`;
441
+ }
442
+ return `${qKey}:\n${stringify(val, indent + 4)}`;
443
+ });
444
+ const contentPad = ' '.repeat(indent + 2);
445
+ let result = `${pad}- ${lines[0]}`;
446
+ for (let i = 1; i < lines.length; i++) {
447
+ result += `\n${contentPad}${lines[i]}`;
448
+ }
449
+ return result;
450
+ }).join('\n');
451
+ }
452
+
453
+ function stringifyMap(obj, indent) {
454
+ const pad = ' '.repeat(indent);
455
+ const entries = Object.entries(obj);
456
+ if (entries.length === 0) return '{}';
457
+ return entries.map(([key, val]) => {
458
+ const qKey = needsQuoting(key) ? `"${key}"` : key;
459
+ if (val == null) return `${pad}${qKey}: null`;
460
+ if (typeof val === 'string' && val.includes('\n')) {
461
+ return `${pad}${qKey}: ${stringifyString(val, indent)}`;
462
+ }
463
+ if (typeof val !== 'object') {
464
+ return `${pad}${qKey}: ${stringify(val, indent + 2)}`;
465
+ }
466
+ if (Array.isArray(val)) {
467
+ if (val.length === 0) return `${pad}${qKey}: []`;
468
+ if (val.every(v => v == null || typeof v !== 'object')) {
469
+ return `${pad}${qKey}: ${stringify(val, indent + 2)}`;
470
+ }
471
+ return `${pad}${qKey}:\n${stringify(val, indent + 2)}`;
472
+ }
473
+ // Nested map
474
+ return `${pad}${qKey}:\n${stringify(val, indent + 2)}`;
475
+ }).join('\n');
476
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "popilot",
3
+ "version": "0.2.0",
4
+ "description": "Multi-agent PO/PM system scaffold for Claude Code",
5
+ "type": "module",
6
+ "bin": {
7
+ "popilot": "./bin/cli.mjs"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test test/*.test.mjs"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "lib/",
15
+ "scaffold/",
16
+ "adapters/",
17
+ "README.md"
18
+ ],
19
+ "keywords": [
20
+ "claude-code",
21
+ "po",
22
+ "pm",
23
+ "multi-agent",
24
+ "scaffold"
25
+ ],
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18"
29
+ }
30
+ }
@@ -0,0 +1,20 @@
1
+ # 민감 정보 - 이 파일은 gitignore 대상입니다.
2
+ # 필요한 값을 채워주세요.
3
+ # Setup Wizard가 연동 설정에 따라 이 파일을 안내합니다.
4
+
5
+ # 예시 (프로젝트에 따라 다름)
6
+ INTEGRATIONS:
7
+ # GA4_PROPERTY_ID: ""
8
+ # NOTION_WORKSPACE: ""
9
+ # CHANNEL_IO_ACCESS_KEY: ""
10
+ # CHANNEL_IO_ACCESS_SECRET: ""
11
+ # CORTI_API_KEY: "" # Corti MCP Bearer token (MCP 설정에서 사용)
12
+
13
+ # 노션 태스크 연동 (선택)
14
+ NOTION_TASK:
15
+ # USER_ID: ""
16
+ # SPRINT_ID: ""
17
+
18
+ _META:
19
+ UPDATED_AT: ""
20
+ NOTE: "이 파일은 Git에 커밋되지 않습니다."