godpowers 0.15.16 → 0.15.18

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/lib/pillars.js ADDED
@@ -0,0 +1,722 @@
1
+ /**
2
+ * Pillars integration helpers.
3
+ *
4
+ * Godpowers uses Pillars as its native project context layer. This module
5
+ * treats files as pillars only when they have `pillar:` frontmatter, which
6
+ * keeps Godpowers specialist agents from being mistaken for project context.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const PILLARS_FENCE_BEGIN = '<!-- pillars:begin -->';
13
+ const PILLARS_FENCE_END = '<!-- pillars:end -->';
14
+ const PILLAR_SYNC_FENCE_BEGIN = '<!-- godpowers:pillar-sync:begin -->';
15
+ const PILLAR_SYNC_FENCE_END = '<!-- godpowers:pillar-sync:end -->';
16
+
17
+ const CORE_PILLARS = {
18
+ stack: {
19
+ covers: ['tech choices', 'dependencies', 'version constraints'],
20
+ triggers: ['stack', 'framework', 'library', 'dependency', 'package', 'version']
21
+ },
22
+ arch: {
23
+ covers: ['system architecture', 'services', 'boundaries', 'data flow'],
24
+ triggers: ['architecture', 'service', 'module', 'boundary', 'design', 'system']
25
+ },
26
+ data: {
27
+ covers: ['data model', 'schema', 'migrations', 'queries', 'storage'],
28
+ triggers: ['database', 'schema', 'migration', 'query', 'table', 'column', 'model']
29
+ },
30
+ api: {
31
+ covers: ['api contract', 'http', 'rpc', 'request response shapes'],
32
+ triggers: ['api', 'endpoint', 'route', 'request', 'response', 'http', 'rpc']
33
+ },
34
+ ui: {
35
+ covers: ['visual ui', 'components', 'design tokens', 'interaction patterns'],
36
+ triggers: ['ui', 'component', 'screen', 'page', 'design', 'css', 'accessibility']
37
+ },
38
+ auth: {
39
+ covers: ['identity', 'sessions', 'access control', 'authorization'],
40
+ triggers: ['auth', 'login', 'session', 'permission', 'role', 'invite', 'access']
41
+ },
42
+ quality: {
43
+ covers: ['testing', 'error handling', 'code style', 'naming'],
44
+ triggers: ['test', 'testing', 'lint', 'quality', 'error', 'style', 'refactor']
45
+ },
46
+ deploy: {
47
+ covers: ['environments', 'promotion', 'rollback', 'release process'],
48
+ triggers: ['deploy', 'deployment', 'environment', 'release', 'rollback', 'ci']
49
+ },
50
+ observe: {
51
+ covers: ['logging', 'metrics', 'tracing', 'alerts', 'runbooks'],
52
+ triggers: ['log', 'logging', 'metric', 'trace', 'alert', 'observability', 'monitor']
53
+ }
54
+ };
55
+
56
+ const ALWAYS_PILLARS = {
57
+ context: {
58
+ covers: ['project identity', 'domain language', 'product invariants', 'glossary'],
59
+ triggers: [],
60
+ see_also: ['repo']
61
+ },
62
+ repo: {
63
+ covers: ['file layout', 'naming conventions', 'where things go', 'repository structure'],
64
+ triggers: [],
65
+ see_also: ['context']
66
+ }
67
+ };
68
+
69
+ const COMMON_PILLARS = {
70
+ security: {
71
+ covers: ['input validation', 'threat model', 'dependency risk', 'adversarial concerns'],
72
+ triggers: ['security', 'threat', 'vulnerability', 'owasp', 'cve', 'validation']
73
+ }
74
+ };
75
+
76
+ const KNOWN_PILLARS = {
77
+ ...ALWAYS_PILLARS,
78
+ ...CORE_PILLARS,
79
+ ...COMMON_PILLARS
80
+ };
81
+
82
+ const ARTIFACT_PILLAR_MAP = [
83
+ { pattern: /(^|\/)prd\/PRD\.md$/i, pillars: ['context'] },
84
+ { pattern: /(^|\/)arch\/ARCH\.md$/i, pillars: ['arch'] },
85
+ { pattern: /(^|\/)arch\/adr\//i, pillars: ['arch'] },
86
+ { pattern: /(^|\/)stack\/DECISION\.md$/i, pillars: ['stack'] },
87
+ { pattern: /(^|\/)roadmap\/ROADMAP\.md$/i, pillars: ['context', 'quality'] },
88
+ { pattern: /(^|\/)build\/PLAN\.md$/i, pillars: ['quality', 'repo'] },
89
+ { pattern: /(^|\/)deploy\/STATE\.md$/i, pillars: ['deploy'] },
90
+ { pattern: /(^|\/)observe\/STATE\.md$/i, pillars: ['observe'] },
91
+ { pattern: /(^|\/)harden\/FINDINGS\.md$/i, pillars: ['security', 'auth'] },
92
+ { pattern: /(^|\/)design\/DESIGN\.md$/i, pillars: ['ui'] },
93
+ { pattern: /(^|\/)design\/PRODUCT\.md$/i, pillars: ['context', 'ui'] }
94
+ ];
95
+
96
+ const GODPOWERS_ARTIFACTS = [
97
+ '.godpowers/prd/PRD.md',
98
+ '.godpowers/arch/ARCH.md',
99
+ '.godpowers/stack/DECISION.md',
100
+ '.godpowers/roadmap/ROADMAP.md',
101
+ '.godpowers/build/PLAN.md',
102
+ '.godpowers/deploy/STATE.md',
103
+ '.godpowers/observe/STATE.md',
104
+ '.godpowers/harden/FINDINGS.md',
105
+ '.godpowers/design/DESIGN.md',
106
+ '.godpowers/design/PRODUCT.md'
107
+ ];
108
+
109
+ function stripQuotes(value) {
110
+ return String(value).trim().replace(/^['"]|['"]$/g, '');
111
+ }
112
+
113
+ function parseInlineList(value) {
114
+ const trimmed = value.trim();
115
+ if (trimmed === '[]') return [];
116
+ if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return null;
117
+ const body = trimmed.slice(1, -1).trim();
118
+ if (!body) return [];
119
+ return body.split(',').map(part => stripQuotes(part)).filter(Boolean);
120
+ }
121
+
122
+ function parseScalar(value) {
123
+ const trimmed = value.trim();
124
+ if (trimmed === 'true') return true;
125
+ if (trimmed === 'false') return false;
126
+ const list = parseInlineList(trimmed);
127
+ if (list) return list;
128
+ return stripQuotes(trimmed);
129
+ }
130
+
131
+ function parseFrontmatter(raw) {
132
+ if (!raw.startsWith('---\n')) return null;
133
+ const end = raw.indexOf('\n---', 4);
134
+ if (end === -1) return null;
135
+
136
+ const frontmatter = {};
137
+ const lines = raw.slice(4, end).split('\n');
138
+ let currentKey = null;
139
+ for (const line of lines) {
140
+ const match = line.match(/^([\w-]+):\s*(.*)$/);
141
+ if (match) {
142
+ currentKey = match[1];
143
+ frontmatter[currentKey] = parseScalar(match[2]);
144
+ continue;
145
+ }
146
+ const itemMatch = line.match(/^\s*-\s*(.+)$/);
147
+ if (currentKey && itemMatch) {
148
+ if (!Array.isArray(frontmatter[currentKey])) frontmatter[currentKey] = [];
149
+ frontmatter[currentKey].push(stripQuotes(itemMatch[1]));
150
+ }
151
+ }
152
+ return frontmatter;
153
+ }
154
+
155
+ function walkMarkdown(dir) {
156
+ if (!fs.existsSync(dir)) return [];
157
+ const out = [];
158
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
159
+ const full = path.join(dir, entry.name);
160
+ if (entry.isDirectory()) {
161
+ out.push(...walkMarkdown(full));
162
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
163
+ out.push(full);
164
+ }
165
+ }
166
+ return out.sort();
167
+ }
168
+
169
+ function parsePillarFile(filePath) {
170
+ const raw = fs.readFileSync(filePath, 'utf8');
171
+ const frontmatter = parseFrontmatter(raw);
172
+ if (!frontmatter || !frontmatter.pillar) return null;
173
+ return {
174
+ path: filePath,
175
+ name: frontmatter.pillar,
176
+ status: frontmatter.status || 'present',
177
+ always_load: frontmatter.always_load === true,
178
+ covers: Array.isArray(frontmatter.covers) ? frontmatter.covers : [],
179
+ triggers: Array.isArray(frontmatter.triggers) ? frontmatter.triggers : [],
180
+ must_read_with: Array.isArray(frontmatter.must_read_with) ? frontmatter.must_read_with : [],
181
+ see_also: Array.isArray(frontmatter.see_also) ? frontmatter.see_also : [],
182
+ frontmatter,
183
+ raw
184
+ };
185
+ }
186
+
187
+ function listPillars(projectRoot) {
188
+ const agentsDir = path.join(projectRoot, 'agents');
189
+ return walkMarkdown(agentsDir)
190
+ .map(file => parsePillarFile(file))
191
+ .filter(Boolean);
192
+ }
193
+
194
+ function readAgents(projectRoot) {
195
+ const file = path.join(projectRoot, 'AGENTS.md');
196
+ if (!fs.existsSync(file)) return '';
197
+ return fs.readFileSync(file, 'utf8');
198
+ }
199
+
200
+ function hasPillarsProtocol(projectRoot) {
201
+ const content = readAgents(projectRoot).toLowerCase();
202
+ return content.includes('pillars') &&
203
+ (content.includes('always-pillars') || content.includes('always_load: true') || content.includes('load every pillar')) &&
204
+ content.includes('excluded');
205
+ }
206
+
207
+ function readExclusions(projectRoot) {
208
+ const content = readAgents(projectRoot);
209
+ const exclusions = new Set();
210
+ const inline = content.match(/excluded:\s*\[([^\]]*)\]/);
211
+ if (inline) {
212
+ for (const item of inline[1].split(',')) {
213
+ const value = stripQuotes(item);
214
+ if (value) exclusions.add(value);
215
+ }
216
+ }
217
+ const nameMatches = content.matchAll(/^\s*-\s*name:\s*([A-Za-z0-9_-]+)/gm);
218
+ for (const match of nameMatches) exclusions.add(match[1]);
219
+ return exclusions;
220
+ }
221
+
222
+ function validatePillar(projectRoot, pillar) {
223
+ const issues = [];
224
+ const rel = path.relative(path.join(projectRoot, 'agents'), pillar.path);
225
+ const expectedName = path.basename(rel, '.md');
226
+ if (pillar.name !== expectedName) {
227
+ issues.push(`frontmatter pillar does not match filename for ${rel}`);
228
+ }
229
+ if (!Array.isArray(pillar.covers) || pillar.covers.length === 0) {
230
+ issues.push(`${rel} missing covers list`);
231
+ }
232
+ if (!pillar.always_load && !Array.isArray(pillar.triggers)) {
233
+ issues.push(`${rel} missing triggers list`);
234
+ }
235
+ return issues;
236
+ }
237
+
238
+ function detect(projectRoot) {
239
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
240
+ const agentsDir = path.join(projectRoot, 'agents');
241
+ const hasAgents = fs.existsSync(agentsPath);
242
+ const hasAgentsDir = fs.existsSync(agentsDir);
243
+ const protocol = hasPillarsProtocol(projectRoot);
244
+ const pillars = listPillars(projectRoot);
245
+ const byName = new Map(pillars.map(p => [p.name, p]));
246
+ const issues = [];
247
+
248
+ if (!hasAgents) issues.push('AGENTS.md missing');
249
+ if (!hasAgentsDir) issues.push('agents directory missing');
250
+ if (hasAgents && !protocol) issues.push('AGENTS.md does not declare Pillars protocol');
251
+
252
+ for (const name of ['context', 'repo']) {
253
+ if (!byName.has(name)) {
254
+ issues.push(`agents/${name}.md missing`);
255
+ } else if (!byName.get(name).always_load) {
256
+ issues.push(`agents/${name}.md must set always_load: true`);
257
+ }
258
+ }
259
+
260
+ for (const pillar of pillars) {
261
+ issues.push(...validatePillar(projectRoot, pillar));
262
+ }
263
+
264
+ const hasAnySignal = hasAgents || hasAgentsDir || pillars.length > 0;
265
+ const valid = protocol && byName.has('context') && byName.has('repo') &&
266
+ byName.get('context').always_load && byName.get('repo').always_load &&
267
+ issues.filter(issue => issue.includes('frontmatter') || issue.includes('missing') || issue.includes('must set')).length === 0;
268
+
269
+ return {
270
+ status: valid ? 'present' : (hasAnySignal ? 'partial' : 'absent'),
271
+ valid,
272
+ hasAgents,
273
+ hasAgentsDir,
274
+ protocol,
275
+ pillars,
276
+ issues
277
+ };
278
+ }
279
+
280
+ function containsTerm(taskText, term) {
281
+ const task = taskText.toLowerCase();
282
+ const normalized = String(term).toLowerCase().replace(/[-_]/g, ' ');
283
+ return task.includes(normalized) || task.includes(String(term).toLowerCase());
284
+ }
285
+
286
+ function matchesTask(pillar, taskText) {
287
+ const terms = [pillar.name, ...pillar.triggers, ...pillar.covers];
288
+ return terms.some(term => containsTerm(taskText, term));
289
+ }
290
+
291
+ function addLoad(load, pillar, reason) {
292
+ if (!load.has(pillar.name)) {
293
+ load.set(pillar.name, { pillar, reasons: [] });
294
+ }
295
+ load.get(pillar.name).reasons.push(reason);
296
+ }
297
+
298
+ function computeLoadSet(projectRoot, taskText) {
299
+ const pillars = listPillars(projectRoot);
300
+ const byName = new Map(pillars.map(p => [p.name, p]));
301
+ const excluded = readExclusions(projectRoot);
302
+ const load = new Map();
303
+ const primaryNames = [];
304
+ const missing = [];
305
+
306
+ for (const name of ['context', 'repo']) {
307
+ if (!byName.has(name) && !excluded.has(name)) missing.push({ pillar: name, reason: 'floor pillar missing' });
308
+ }
309
+
310
+ for (const pillar of pillars) {
311
+ if (excluded.has(pillar.name)) continue;
312
+ if (pillar.always_load) addLoad(load, pillar, 'always_load');
313
+ }
314
+
315
+ for (const pillar of pillars) {
316
+ if (pillar.always_load || excluded.has(pillar.name)) continue;
317
+ if (matchesTask(pillar, taskText)) {
318
+ addLoad(load, pillar, 'task match');
319
+ primaryNames.push(pillar.name);
320
+ }
321
+ }
322
+
323
+ for (const primaryName of primaryNames) {
324
+ const primary = byName.get(primaryName);
325
+ for (const dep of primary.must_read_with) {
326
+ if (excluded.has(dep)) continue;
327
+ if (byName.has(dep)) {
328
+ addLoad(load, byName.get(dep), `must_read_with from ${primaryName}`);
329
+ } else {
330
+ missing.push({ pillar: dep, reason: `must_read_with from ${primaryName}` });
331
+ }
332
+ }
333
+ }
334
+
335
+ for (const item of [...load.values()]) {
336
+ for (const soft of item.pillar.see_also) {
337
+ if (excluded.has(soft)) continue;
338
+ if (containsTerm(taskText, soft)) {
339
+ if (byName.has(soft)) addLoad(load, byName.get(soft), `see_also from ${item.pillar.name}`);
340
+ else missing.push({ pillar: soft, reason: `see_also from ${item.pillar.name}` });
341
+ }
342
+ }
343
+ }
344
+
345
+ for (const [name, meta] of Object.entries(CORE_PILLARS)) {
346
+ if (byName.has(name) || excluded.has(name)) continue;
347
+ if (matchesTask({ name, ...meta, must_read_with: [], see_also: [] }, taskText)) {
348
+ missing.push({ pillar: name, reason: 'relevant known pillar absent' });
349
+ }
350
+ }
351
+
352
+ return {
353
+ loadSet: [...load.values()].map(item => ({
354
+ name: item.pillar.name,
355
+ path: item.pillar.path,
356
+ status: item.pillar.status,
357
+ reasons: [...new Set(item.reasons)]
358
+ })),
359
+ missing: dedupeMissing(missing),
360
+ excluded: [...excluded].sort()
361
+ };
362
+ }
363
+
364
+ function dedupeMissing(missing) {
365
+ const seen = new Set();
366
+ const out = [];
367
+ for (const item of missing) {
368
+ const key = `${item.pillar}:${item.reason}`;
369
+ if (seen.has(key)) continue;
370
+ seen.add(key);
371
+ out.push(item);
372
+ }
373
+ return out;
374
+ }
375
+
376
+ function buildProtocolContent(exclusions = []) {
377
+ const lines = [];
378
+ lines.push('# Pillars: Agent Protocol');
379
+ lines.push('');
380
+ lines.push('This project follows the Pillars standard. Coding agents read project context from `./agents/*.md` before changing code.');
381
+ lines.push('');
382
+ lines.push('## At the start of any task');
383
+ lines.push('');
384
+ lines.push('1. Load every pillar whose frontmatter has `always_load: true`.');
385
+ lines.push('2. Scan remaining pillar frontmatter and select task-relevant primaries from `triggers` and `covers`.');
386
+ lines.push('3. Add each primary pillar direct `must_read_with` dependencies, depth 1 only.');
387
+ lines.push('4. Read every pillar body in the computed load set.');
388
+ lines.push('5. Read `see_also` pillars only when the task explicitly touches that area.');
389
+ lines.push('6. Follow Rules, apply Workflows, heed Watchouts, and ask before deciding open Gaps.');
390
+ lines.push('');
391
+ lines.push('## Handling missing pillars');
392
+ lines.push('');
393
+ lines.push('| State | Action |');
394
+ lines.push('|---|---|');
395
+ lines.push('| `status: present` | Load and comply. |');
396
+ lines.push('| `status: stub` | Treat the concern as acknowledged but undecided. Ask before making domain decisions. |');
397
+ lines.push('| Name in `excluded:` | Treat as intentionally not applicable. |');
398
+ lines.push('| Relevant but absent | Infer from code, state the assumption, and recommend authoring the pillar. |');
399
+ lines.push('');
400
+ lines.push('If `context.md` or `repo.md` is missing, pause and create stubs before continuing.');
401
+ lines.push('');
402
+ lines.push('## Excluded pillars');
403
+ lines.push('');
404
+ lines.push('```yaml');
405
+ if (exclusions.length === 0) {
406
+ lines.push('excluded: []');
407
+ } else {
408
+ lines.push('excluded:');
409
+ for (const item of exclusions) {
410
+ lines.push(` - name: ${item.name}`);
411
+ lines.push(` reason: ${item.reason}`);
412
+ }
413
+ }
414
+ lines.push('```');
415
+ return lines.join('\n');
416
+ }
417
+
418
+ function writeFenced(filePath, begin, end, content) {
419
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
420
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
421
+ const block = `${begin}\n${content}\n${end}`;
422
+ const start = existing.indexOf(begin);
423
+ const finish = existing.indexOf(end);
424
+ let next;
425
+ if (start !== -1 && finish !== -1 && finish > start) {
426
+ next = `${existing.slice(0, start)}${block}${existing.slice(finish + end.length)}`;
427
+ } else if (existing.trim()) {
428
+ next = `${existing.replace(/\s*$/, '')}\n\n${block}\n`;
429
+ } else {
430
+ next = `${block}\n`;
431
+ }
432
+ fs.writeFileSync(filePath, next);
433
+ }
434
+
435
+ function artifactToPillars(artifactPath) {
436
+ const normalized = artifactPath.replace(/\\/g, '/');
437
+ const pillars = [];
438
+ for (const entry of ARTIFACT_PILLAR_MAP) {
439
+ if (entry.pattern.test(normalized)) pillars.push(...entry.pillars);
440
+ }
441
+ return [...new Set(pillars)];
442
+ }
443
+
444
+ function sanitizeSignal(text) {
445
+ return String(text)
446
+ .replace(/\u2014|\u2013/g, '-')
447
+ .replace(/[\u{1F300}-\u{1FAFF}]/gu, '')
448
+ .replace(/\s+/g, ' ')
449
+ .trim();
450
+ }
451
+
452
+ function stripMarkdownPrefix(line) {
453
+ return line
454
+ .replace(/^\s{0,3}[-*+]\s+/, '')
455
+ .replace(/^\s{0,3}\d+\.\s+/, '')
456
+ .replace(/^#+\s+/, '')
457
+ .replace(/^\|+|\|+$/g, '')
458
+ .trim();
459
+ }
460
+
461
+ function extractDurableSignalsFromText(text, opts = {}) {
462
+ const maxSignals = opts.maxSignals || 8;
463
+ const signals = [];
464
+ const seen = new Set();
465
+ const regex = /\[(DECISION|HYPOTHESIS|OPEN QUESTION)\]\s*([^\n]+)/g;
466
+ let match;
467
+ while ((match = regex.exec(text)) !== null) {
468
+ const label = match[1];
469
+ const body = sanitizeSignal(stripMarkdownPrefix(match[2]));
470
+ if (!body) continue;
471
+ const key = `${label}:${body.toLowerCase()}`;
472
+ if (seen.has(key)) continue;
473
+ seen.add(key);
474
+ signals.push({ label, body });
475
+ if (signals.length >= maxSignals) break;
476
+ }
477
+ return signals;
478
+ }
479
+
480
+ function extractDurableSignals(projectRoot, artifactPath, opts = {}) {
481
+ const fullPath = path.join(projectRoot, artifactPath);
482
+ if (!fs.existsSync(fullPath)) return [];
483
+ return extractDurableSignalsFromText(fs.readFileSync(fullPath, 'utf8'), opts);
484
+ }
485
+
486
+ function discoverGodpowersArtifacts(projectRoot) {
487
+ return GODPOWERS_ARTIFACTS
488
+ .filter(rel => fs.existsSync(path.join(projectRoot, rel)))
489
+ .map(rel => ({
490
+ path: rel,
491
+ pillars: artifactToPillars(rel)
492
+ }))
493
+ .filter(item => item.pillars.length > 0);
494
+ }
495
+
496
+ function pillarStub(name, meta, opts = {}) {
497
+ const always = opts.always === true;
498
+ const status = opts.status || 'stub';
499
+ const seeAlso = opts.see_also || meta.see_also || [];
500
+ const mustReadWith = opts.must_read_with || meta.must_read_with || [];
501
+ const lines = [];
502
+ lines.push('---');
503
+ lines.push(`pillar: ${name}`);
504
+ lines.push(`status: ${status}`);
505
+ lines.push(`always_load: ${always ? 'true' : 'false'}`);
506
+ lines.push(`covers: [${meta.covers.join(', ')}]`);
507
+ lines.push(`triggers: [${(meta.triggers || []).join(', ')}]`);
508
+ lines.push(`must_read_with: [${mustReadWith.join(', ')}]`);
509
+ lines.push(`see_also: [${seeAlso.join(', ')}]`);
510
+ lines.push('---');
511
+ lines.push('');
512
+ lines.push('## Scope');
513
+ lines.push('');
514
+ lines.push(`(stub) Capture project-specific guidance for ${name}.`);
515
+ lines.push('');
516
+ lines.push('## Context');
517
+ lines.push('');
518
+ lines.push('(stub) Fill with facts an agent cannot reliably infer from code alone.');
519
+ lines.push('');
520
+ lines.push('## Decisions');
521
+ lines.push('');
522
+ lines.push('(none)');
523
+ lines.push('');
524
+ lines.push('## Rules');
525
+ lines.push('');
526
+ lines.push('(none)');
527
+ lines.push('');
528
+ lines.push('## Workflows');
529
+ lines.push('');
530
+ lines.push('(none)');
531
+ lines.push('');
532
+ lines.push('## Watchouts');
533
+ lines.push('');
534
+ lines.push('(none)');
535
+ lines.push('');
536
+ lines.push('## Touchpoints');
537
+ lines.push('');
538
+ lines.push('(none)');
539
+ lines.push('');
540
+ lines.push('## Gaps');
541
+ lines.push('');
542
+ lines.push(`- This pillar is a stub. Ask before making durable ${name} decisions.`);
543
+ lines.push('');
544
+ return lines.join('\n');
545
+ }
546
+
547
+ function ensurePillar(projectRoot, name, meta, opts = {}) {
548
+ const dir = path.join(projectRoot, 'agents');
549
+ fs.mkdirSync(dir, { recursive: true });
550
+ const file = path.join(dir, `${name}.md`);
551
+ if (fs.existsSync(file)) return { path: file, action: 'kept' };
552
+ fs.writeFileSync(file, pillarStub(name, meta, opts));
553
+ return { path: file, action: 'created' };
554
+ }
555
+
556
+ function init(projectRoot, opts = {}) {
557
+ const agentsFile = path.join(projectRoot, 'AGENTS.md');
558
+ const exclusions = opts.exclusions || [];
559
+ writeFenced(agentsFile, PILLARS_FENCE_BEGIN, PILLARS_FENCE_END, buildProtocolContent(exclusions));
560
+
561
+ const results = [];
562
+ results.push(ensurePillar(projectRoot, 'context', ALWAYS_PILLARS.context, { always: true }));
563
+ results.push(ensurePillar(projectRoot, 'repo', ALWAYS_PILLARS.repo, { always: true }));
564
+
565
+ const coreNames = opts.corePillars || Object.keys(CORE_PILLARS);
566
+ for (const name of coreNames) {
567
+ results.push(ensurePillar(projectRoot, name, CORE_PILLARS[name]));
568
+ }
569
+
570
+ return {
571
+ agentsFile,
572
+ results,
573
+ detection: detect(projectRoot)
574
+ };
575
+ }
576
+
577
+ function buildPillarSyncContent(pillarName, artifactEntries, opts = {}) {
578
+ const mode = opts.yolo ? 'auto-applied by yolo' : 'proposed for review';
579
+ const entries = artifactEntries.map(entry =>
580
+ typeof entry === 'string' ? { artifact: entry, signals: [] } : entry
581
+ );
582
+ const unique = [...new Set(entries.map(entry => entry.artifact))].sort();
583
+ const lines = [];
584
+ lines.push('## Godpowers artifact sources');
585
+ lines.push('');
586
+ lines.push(`- Sync mode: ${mode}.`);
587
+ for (const artifact of unique) {
588
+ lines.push(`- Related artifact: \`${artifact}\`.`);
589
+ }
590
+ lines.push(`- Rule: keep this pillar aligned when these artifacts change durable ${pillarName} truth.`);
591
+ const signalEntries = entries.filter(entry => entry.signals && entry.signals.length > 0);
592
+ if (signalEntries.length > 0) {
593
+ lines.push('');
594
+ lines.push('## Extracted durable signals');
595
+ for (const entry of signalEntries) {
596
+ lines.push('');
597
+ lines.push(`From \`${entry.artifact}\`:`);
598
+ for (const signal of entry.signals) {
599
+ lines.push(`- [${signal.label}] ${signal.body}`);
600
+ }
601
+ }
602
+ }
603
+ return lines.join('\n');
604
+ }
605
+
606
+ function applyArtifactSync(projectRoot, artifactPaths, opts = {}) {
607
+ const paths = Array.isArray(artifactPaths) ? artifactPaths : [artifactPaths];
608
+ const byPillar = new Map();
609
+ const results = [];
610
+
611
+ init(projectRoot, { corePillars: opts.corePillars });
612
+
613
+ for (const artifact of paths.filter(Boolean)) {
614
+ for (const pillarName of artifactToPillars(artifact)) {
615
+ if (!byPillar.has(pillarName)) byPillar.set(pillarName, []);
616
+ byPillar.get(pillarName).push({
617
+ artifact,
618
+ signals: extractDurableSignals(projectRoot, artifact, opts)
619
+ });
620
+ }
621
+ }
622
+
623
+ for (const [pillarName, entries] of byPillar.entries()) {
624
+ const meta = KNOWN_PILLARS[pillarName] || {
625
+ covers: [`${pillarName} project context`],
626
+ triggers: [pillarName]
627
+ };
628
+ const ensured = ensurePillar(projectRoot, pillarName, meta, {
629
+ always: pillarName === 'context' || pillarName === 'repo'
630
+ });
631
+ writeFenced(
632
+ ensured.path,
633
+ PILLAR_SYNC_FENCE_BEGIN,
634
+ PILLAR_SYNC_FENCE_END,
635
+ buildPillarSyncContent(pillarName, entries, opts)
636
+ );
637
+ results.push({
638
+ pillar: pillarName,
639
+ path: ensured.path,
640
+ artifacts: [...new Set(entries.map(entry => entry.artifact))].sort(),
641
+ signals: entries.reduce((sum, entry) => sum + entry.signals.length, 0),
642
+ action: opts.yolo ? 'auto-applied' : 'applied'
643
+ });
644
+ }
645
+
646
+ return results;
647
+ }
648
+
649
+ function pillarizeExisting(projectRoot, opts = {}) {
650
+ const before = detect(projectRoot);
651
+ const initialized = init(projectRoot, opts);
652
+ const artifacts = discoverGodpowersArtifacts(projectRoot);
653
+ const synced = applyArtifactSync(
654
+ projectRoot,
655
+ artifacts.map(item => item.path),
656
+ opts
657
+ );
658
+
659
+ return {
660
+ before,
661
+ after: detect(projectRoot),
662
+ initialized,
663
+ artifacts,
664
+ synced
665
+ };
666
+ }
667
+
668
+ function planArtifactSync(projectRoot, artifactPaths, opts = {}) {
669
+ const yolo = opts.yolo === true;
670
+ const existing = new Set(listPillars(projectRoot).map(p => p.name));
671
+ const paths = Array.isArray(artifactPaths) ? artifactPaths : [artifactPaths];
672
+ const proposals = [];
673
+
674
+ for (const artifact of paths.filter(Boolean)) {
675
+ const normalized = artifact.replace(/\\/g, '/');
676
+ for (const entry of ARTIFACT_PILLAR_MAP) {
677
+ if (!entry.pattern.test(normalized)) continue;
678
+ for (const pillar of entry.pillars) {
679
+ proposals.push({
680
+ artifact,
681
+ pillar,
682
+ pillarExists: existing.has(pillar),
683
+ action: yolo ? 'auto-apply' : 'propose',
684
+ reason: yolo
685
+ ? 'YOLO mode auto-applies durable context updates'
686
+ : 'Default mode proposes durable context updates for review'
687
+ });
688
+ }
689
+ }
690
+ }
691
+
692
+ return proposals;
693
+ }
694
+
695
+ module.exports = {
696
+ PILLARS_FENCE_BEGIN,
697
+ PILLARS_FENCE_END,
698
+ PILLAR_SYNC_FENCE_BEGIN,
699
+ PILLAR_SYNC_FENCE_END,
700
+ CORE_PILLARS,
701
+ ALWAYS_PILLARS,
702
+ COMMON_PILLARS,
703
+ KNOWN_PILLARS,
704
+ parseFrontmatter,
705
+ parsePillarFile,
706
+ listPillars,
707
+ readExclusions,
708
+ hasPillarsProtocol,
709
+ detect,
710
+ computeLoadSet,
711
+ buildProtocolContent,
712
+ pillarStub,
713
+ init,
714
+ artifactToPillars,
715
+ sanitizeSignal,
716
+ extractDurableSignalsFromText,
717
+ extractDurableSignals,
718
+ discoverGodpowersArtifacts,
719
+ planArtifactSync,
720
+ applyArtifactSync,
721
+ pillarizeExisting
722
+ };