project-governance-init 0.1.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/bin/init.mjs ADDED
@@ -0,0 +1,514 @@
1
+ #!/usr/bin/env node
2
+ // ponytail: single-file, zero-dependency CLI. Node stdlib only (fs, path, readline).
3
+ // Detects stack automatically; asks only what can't be detected; writes AGENTS.md
4
+ // plus thin pointer files so Claude/Gemini/Cursor/Codex all read the same source of truth.
5
+
6
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
7
+ import { join, relative } from 'node:path';
8
+ import readline from 'node:readline';
9
+
10
+ const cwd = process.cwd();
11
+ const args = process.argv.slice(2);
12
+ const dryRun = args.includes('--dry-run');
13
+ const force = args.includes('--force');
14
+ const reviewMode = args.includes('--review');
15
+ const START_MARKER = '<!-- project-governance-init:start -->';
16
+ const END_MARKER = '<!-- project-governance-init:end -->';
17
+
18
+ function readJson(path) {
19
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
20
+ }
21
+
22
+ function discoverGuidanceFiles() {
23
+ const files = [];
24
+ const roots = ['.aiassistant', 'docs', 'skills'];
25
+ const walk = (dir, depth) => {
26
+ if (depth > 2 || files.length >= 40) return;
27
+ let entries;
28
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
29
+ for (const entry of entries) {
30
+ if (entry.name === 'node_modules' || entry.name.startsWith('.git')) continue;
31
+ const path = join(dir, entry.name);
32
+ if (entry.isDirectory()) walk(path, depth + 1);
33
+ else if (/\.md$/i.test(entry.name)) files.push(relative(cwd, path));
34
+ }
35
+ };
36
+ for (const root of roots) if (existsSync(join(cwd, root))) walk(join(cwd, root), 0);
37
+ for (const file of ['CONTRIBUTING.md', 'SECURITY.md', 'PRODUCT_CONSTITUTION.md', 'PRODUCT_STRATEGY.md', 'CONSTITUTION_V1.md']) {
38
+ if (existsSync(join(cwd, file))) files.push(file);
39
+ }
40
+ for (const dir of ['.agents', '.claude', '.codex', '.superpowers']) if (existsSync(join(cwd, dir))) files.push(`${dir}/`);
41
+ return [...new Set(files)].sort();
42
+ }
43
+
44
+ function detectStack() {
45
+ const pkg = readJson(join(cwd, 'package.json'));
46
+ const hasRootSource = readdirSync(cwd).some((name) => /\.(c|cc|cpp|cs|go|java|js|jsx|kt|php|py|rb|rs|swift|ts|tsx)$/.test(name));
47
+ const readmeDescription = (() => {
48
+ try {
49
+ return readFileSync(join(cwd, 'README.md'), 'utf8').split(/\r?\n/)
50
+ .map((line) => line.trim()).find((line) => line && !line.startsWith('#')) ?? null;
51
+ } catch { return null; }
52
+ })();
53
+ const detected = {
54
+ name: pkg?.name ?? null,
55
+ description: pkg?.description ?? readmeDescription,
56
+ guidanceFiles: discoverGuidanceFiles(),
57
+ packageManager: existsSync(join(cwd, 'pnpm-lock.yaml')) ? 'pnpm'
58
+ : existsSync(join(cwd, 'yarn.lock')) ? 'yarn'
59
+ : existsSync(join(cwd, 'bun.lockb')) ? 'bun'
60
+ : pkg ? 'npm'
61
+ : existsSync(join(cwd, 'uv.lock')) ? 'uv'
62
+ : existsSync(join(cwd, 'poetry.lock')) ? 'poetry'
63
+ : existsSync(join(cwd, 'go.mod')) ? 'go'
64
+ : existsSync(join(cwd, 'Cargo.toml')) ? 'cargo'
65
+ : existsSync(join(cwd, 'pom.xml')) ? 'maven'
66
+ : existsSync(join(cwd, 'build.gradle')) || existsSync(join(cwd, 'build.gradle.kts')) ? 'gradle'
67
+ : existsSync(join(cwd, 'Package.swift')) ? 'swift'
68
+ : null,
69
+ scripts: pkg?.scripts ?? {},
70
+ languages: [],
71
+ monorepo: existsSync(join(cwd, 'pnpm-workspace.yaml')) || Boolean(pkg?.workspaces),
72
+ ci: existsSync(join(cwd, '.github', 'workflows')) &&
73
+ readdirSync(join(cwd, '.github', 'workflows')).length > 0,
74
+ git: existsSync(join(cwd, '.git')),
75
+ };
76
+ if (pkg) detected.languages.push('javascript/typescript');
77
+ if (existsSync(join(cwd, 'pyproject.toml')) || existsSync(join(cwd, 'requirements.txt'))) detected.languages.push('python');
78
+ if (existsSync(join(cwd, 'go.mod'))) detected.languages.push('go');
79
+ if (existsSync(join(cwd, 'Cargo.toml'))) detected.languages.push('rust');
80
+ if (existsSync(join(cwd, 'pom.xml')) || existsSync(join(cwd, 'build.gradle')) || existsSync(join(cwd, 'build.gradle.kts'))) detected.languages.push('java/kotlin');
81
+ detected.isProject = Boolean(pkg || detected.languages.length || detected.description || hasRootSource || existsSync(join(cwd, 'Dockerfile')) || existsSync(join(cwd, 'terraform')) || ['src', 'app', 'lib'].some((dir) => existsSync(join(cwd, dir))));
82
+
83
+ const allDeps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
84
+ const pythonApi = detected.languages.includes('python') && (() => {
85
+ const text = ['pyproject.toml', 'requirements.txt'].map((file) => { try { return readFileSync(join(cwd, file), 'utf8'); } catch { return ''; } }).join('\n');
86
+ return /fastapi|flask|django|starlette/i.test(text);
87
+ })();
88
+ const nativeMobile = existsSync(join(cwd, 'Package.swift')) || existsSync(join(cwd, 'Podfile')) || existsSync(join(cwd, 'AndroidManifest.xml')) || existsSync(join(cwd, 'app', 'build.gradle')) || existsSync(join(cwd, 'app', 'build.gradle.kts'));
89
+ detected.projectType = nativeMobile || allDeps['react-native'] || allDeps.expo ? 'mobile'
90
+ : allDeps.react || allDeps.next || allDeps.vue || existsSync(join(cwd, 'index.html')) ? 'web'
91
+ : allDeps.express || allDeps.fastify || allDeps['@nestjs/core'] ? 'api'
92
+ : pythonApi ? 'api'
93
+ : pkg?.bin ? 'cli'
94
+ : pkg?.exports || pkg?.main ? 'library' : 'general';
95
+ const dependencyNames = Object.keys(allDeps).map((name) => name.toLowerCase());
96
+ const hasDependency = (patterns) => dependencyNames.some((name) => patterns.some((pattern) => name.includes(pattern)));
97
+ detected.signals = [
98
+ hasDependency(['prisma', 'sequelize', 'mongoose', 'typeorm', 'pg', 'mysql', 'supabase']) && 'database',
99
+ hasDependency(['stripe', 'paypal', 'braintree']) && 'payments',
100
+ hasDependency(['passport', 'jsonwebtoken', 'auth0', 'firebase-admin', 'clerk']) && 'authentication',
101
+ hasDependency(['react', 'vue', 'svelte', 'angular']) && 'ui',
102
+ ].filter(Boolean);
103
+ detected.dependencyScanning = existsSync(join(cwd, '.github', 'dependabot.yml')) || existsSync(join(cwd, 'renovate.json')) || existsSync(join(cwd, '.renovaterc'));
104
+ detected.secretScanning = existsSync(join(cwd, '.pre-commit-config.yaml')) || existsSync(join(cwd, '.gitleaks.toml')) || existsSync(join(cwd, '.husky'));
105
+ detected.errorMonitoring = Boolean(allDeps['@sentry/node'] || allDeps['@sentry/nextjs'] || allDeps['@sentry/react']);
106
+ detected.multiTenantSignal = existsSync(join(cwd, 'supabase')) || Boolean(allDeps['@supabase/supabase-js']);
107
+ detected.hasAgentsMd = existsSync(join(cwd, 'AGENTS.md'));
108
+
109
+ const pick = (...names) => names.find((n) => detected.scripts[n]);
110
+ detected.commands = {
111
+ dev: pick('dev', 'start'),
112
+ build: pick('build'),
113
+ lint: pick('lint'),
114
+ typecheck: pick('typecheck', 'type-check'),
115
+ test: pick('test'),
116
+ testCov: pick('test:cov', 'coverage'),
117
+ e2e: pick('test:e2e', 'e2e'),
118
+ checkAll: pick('check:all', 'ci'),
119
+ };
120
+ return detected;
121
+ }
122
+
123
+ function ask(rl, question) {
124
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
125
+ }
126
+
127
+ function knownAnswersFromAgents(content) {
128
+ const fields = { users: 'Primary user', success: 'Success outcome', deployment: 'Deployment and release owner', criticalFlows: 'Critical flow', integrations: 'External dependencies', constraints: 'Non-negotiables', risks: 'Known risk or unfinished area', visualDirection: 'Visual direction', contentVoice: 'Content voice', seoTargets: 'SEO audience or targets' };
129
+ return Object.fromEntries(Object.entries(fields).flatMap(([key, label]) => {
130
+ const match = content.match(new RegExp(`^- ${label}:\\s*(.+)$`, 'mi'));
131
+ return match && match[1] && !match[1].startsWith('(') ? [[key, match[1].trim()]] : [];
132
+ }));
133
+ }
134
+
135
+ async function gatherAnswers(detected, known = {}, existingRl = null) {
136
+ const ownsRl = !existingRl;
137
+ const rl = existingRl ?? readline.createInterface({ input: process.stdin, output: process.stdout });
138
+ const answer = (key, question) => known[key] || ask(rl, question);
139
+ const description = detected.description || known.description || await ask(rl, 'One-line description of what this project does: ');
140
+ const users = await answer('users', 'Who is the primary user or customer: ');
141
+ const success = await answer('success', 'What outcome proves this project is successful: ');
142
+ const sensitiveRaw = await ask(rl, 'Does this project handle sensitive data (PII, payments, health, credentials)? [y/N]: ');
143
+ const multiTenantRaw = await ask(rl, 'Is this multi-tenant (per-tenant data isolation matters)? [y/N]: ');
144
+ const deployment = await answer('deployment', 'Where is it deployed, and who owns releases: ');
145
+ const criticalFlows = await answer('criticalFlows', 'What user flow must never break: ');
146
+ const integrations = await answer('integrations', 'Which external systems are trusted dependencies (or none): ');
147
+ const constraints = await answer('constraints', 'What is non-negotiable (deadline, budget, privacy, compatibility, performance): ');
148
+ const risks = await answer('risks', 'What is the biggest known risk or unfinished area: ');
149
+ const visualDirection = ['web', 'mobile'].includes(detected.projectType) ? await answer('visualDirection', 'What visual direction should guide UI choices (for example: glass, flat, editorial, dense, minimal): ') : '';
150
+ const contentVoice = ['web', 'mobile'].includes(detected.projectType) ? await answer('contentVoice', 'What should user-facing copy sound like (for example: direct, warm, technical, restrained): ') : '';
151
+ const seoTargets = detected.projectType === 'web' ? await answer('seoTargets', 'Who should find the public-facing site, and what topics or search intent matter: ') : '';
152
+ if (ownsRl) rl.close();
153
+ return {
154
+ description: description || '(describe the project here)',
155
+ users: users || '(identify the primary user)',
156
+ success: success || '(define a measurable outcome)',
157
+ sensitive: /^y/i.test(sensitiveRaw),
158
+ multiTenant: /^y/i.test(multiTenantRaw),
159
+ deployment: deployment || '(document deployment and release ownership)',
160
+ criticalFlows: criticalFlows || '(identify the critical user flow)',
161
+ integrations: integrations || 'none declared',
162
+ constraints: constraints || 'none declared',
163
+ risks: risks || 'none declared',
164
+ visualDirection: visualDirection || '(choose visual direction before UI work)',
165
+ contentVoice: contentVoice || '(choose a direct, audience-appropriate voice)',
166
+ seoTargets: seoTargets || '(define public audience and search intent, if applicable)',
167
+ };
168
+ }
169
+
170
+ const CAPABILITY_DEFINITIONS = {
171
+ 'correctness-review': {
172
+ when: 'Every non-trivial change.',
173
+ use: 'the installed reviewer, agent, or plugin that checks correctness and error handling.',
174
+ fallback: 'Review the changed flow, failure paths, and observable behavior manually.',
175
+ },
176
+ 'security-review': {
177
+ when: 'Authentication, databases, payments, sensitive data, or tenant isolation are involved.',
178
+ use: 'the installed security reviewer, agent, or plugin for trust-boundary and OWASP analysis.',
179
+ fallback: 'Apply the OWASP checklist in this file and verify every authorization check server-side.',
180
+ },
181
+ 'privacy-review': {
182
+ when: 'Personal, health, payment, credential, or otherwise sensitive data is handled.',
183
+ use: 'the installed privacy or data-governance reviewer.',
184
+ fallback: 'Minimize collection, prevent secrets/PII in logs, document retention, and verify deletion/export behavior.',
185
+ },
186
+ 'ui-accessibility-review': {
187
+ when: 'Rendered UI or interaction changes are involved.',
188
+ use: 'the installed UI and accessibility reviewer.',
189
+ fallback: 'Verify keyboard access, focus, contrast, responsive behavior, and real browser/device behavior.',
190
+ },
191
+ 'design-preferences': {
192
+ when: 'A web or mobile interface is being created or changed.',
193
+ use: 'the installed design or frontend skill that asks for visual direction, density, surfaces, motion, typography, and accessibility preferences before implementation.',
194
+ fallback: 'Read ./skills/design-preferences/SKILL.md, then ask the user to choose a visual direction (such as glass, flat, editorial, dense, or minimal) and record the decision before coding.',
195
+ },
196
+ 'authentic-writing': {
197
+ when: 'User-facing product, marketing, help, or error copy is created or edited.',
198
+ use: 'the installed writing skill that removes vague, inflated, and formulaic AI prose while preserving the project voice.',
199
+ fallback: 'Read ./skills/authentic-writing/SKILL.md, then write specific, direct copy; name the user, action, and outcome; remove puffery and generic claims.',
200
+ },
201
+ 'seo-review': {
202
+ when: 'A public-facing web page or site needs discoverability review.',
203
+ use: 'the installed SEO audit skill or equivalent, with live evidence where available.',
204
+ fallback: 'Read ./skills/seo-review/SKILL.md, then check titles, descriptions, headings, canonical/robots/sitemap behavior, structured data, accessibility, and page performance; do not claim findings without evidence.',
205
+ },
206
+ 'mobile-device-validation': {
207
+ when: 'A mobile application or device lifecycle is involved.',
208
+ use: 'the installed mobile testing or device-validation reviewer.',
209
+ fallback: 'Test on a real device where possible, including offline behavior, permissions, lifecycle, and release builds.',
210
+ },
211
+ 'test-verification': {
212
+ when: 'A test command exists or a critical user flow is changing.',
213
+ use: 'the installed test-design or verification reviewer.',
214
+ fallback: 'Run the smallest relevant test first, then the full available suite; cover unhappy paths.',
215
+ },
216
+ 'release-verification': {
217
+ when: 'Deployment or CI configuration is involved.',
218
+ use: 'the installed release or delivery reviewer.',
219
+ fallback: 'Verify build artifacts, environment separation, rollback steps, and the production smoke path.',
220
+ },
221
+ };
222
+
223
+ function recommendedCapabilities(detected, answers = {}) {
224
+ const signals = detected.signals ?? [];
225
+ const ids = ['correctness-review'];
226
+ if (answers.sensitive || answers.multiTenant || signals.some((signal) => ['authentication', 'database', 'payments'].includes(signal))) ids.push('security-review');
227
+ if (answers.sensitive) ids.push('privacy-review');
228
+ if (signals.includes('ui') || ['web', 'mobile'].includes(detected.projectType)) ids.push('ui-accessibility-review');
229
+ if (['web', 'mobile'].includes(detected.projectType) || signals.includes('ui')) ids.push('design-preferences');
230
+ if (['web', 'mobile'].includes(detected.projectType) || signals.includes('ui')) ids.push('authentic-writing');
231
+ if (detected.projectType === 'web') ids.push('seo-review');
232
+ if (detected.projectType === 'mobile') ids.push('mobile-device-validation');
233
+ if (detected.commands?.test || answers.criticalFlows) ids.push('test-verification');
234
+ if (detected.ci || answers.deployment) ids.push('release-verification');
235
+ return ids.map((id) => ({ id, ...CAPABILITY_DEFINITIONS[id] }));
236
+ }
237
+
238
+ function cmd(pm, script) {
239
+ if (!script) return null;
240
+ if (pm === 'pnpm') return `pnpm ${script}`;
241
+ if (pm === 'yarn') return `yarn ${script}`;
242
+ return `npm run ${script}`;
243
+ }
244
+
245
+ function buildAgentsMd(detected, answers) {
246
+ const pm = detected.packageManager ?? 'npm';
247
+ const c = detected.commands;
248
+ const lines = [];
249
+
250
+ lines.push('# AGENTS.md', '', 'Guidance for AI coding agents working in this repository.', '');
251
+ lines.push('## Project Overview', '', answers.description, '');
252
+ lines.push('## Success Context', '');
253
+ lines.push(`- Primary user: ${answers.users}`);
254
+ lines.push(`- Success outcome: ${answers.success}`);
255
+ lines.push(`- Critical flow: ${answers.criticalFlows}`);
256
+ lines.push(`- Deployment and release owner: ${answers.deployment}`);
257
+ lines.push(`- External dependencies: ${answers.integrations}`);
258
+ lines.push(`- Non-negotiables: ${answers.constraints}`);
259
+ lines.push(`- Known risk or unfinished area: ${answers.risks}`, '');
260
+ if (['web', 'mobile'].includes(detected.projectType)) {
261
+ lines.push('## Front-facing preferences', '', `- Visual direction: ${answers.visualDirection ?? '(choose before UI work)'}`, `- Content voice: ${answers.contentVoice ?? '(choose before writing user-facing copy)'}`, `- SEO audience or targets: ${answers.seoTargets ?? '(define for public web work, otherwise mark not applicable)'}`, '');
262
+ }
263
+ lines.push('## Definition of ready', '', '- User or business outcome is stated.', '- Affected surfaces and ownership are identified.', '- Acceptance criteria, failure cases, security impact, and out-of-scope work are clear.', '');
264
+ lines.push('## Definition of done', '', '- Implementation matches the agreed outcome and preserves existing contracts.', '- Validation, error handling, security, accessibility, and relevant tests are covered.', '- Documentation, configuration, migrations, and release impact are updated.', '- The change was verified with the available checks, not only reviewed by diff.', '');
265
+ if (detected.guidanceFiles?.length) {
266
+ lines.push('## Existing project guidance', '', 'Read these repository-owned guides before changing the areas they govern. Keep their detailed rules in place instead of duplicating them here.');
267
+ for (const file of detected.guidanceFiles) lines.push(`- [${file}](./${file.replaceAll('\\', '/')})`);
268
+ lines.push('');
269
+ }
270
+ lines.push('## Recommended Capabilities', '', 'Use the local skill, agent, or plugin that matches each capability when one is available. These names are portable labels, not vendor requirements.', '');
271
+ for (const capability of recommendedCapabilities(detected, answers)) {
272
+ lines.push(`### ${capability.id}`, `When: ${capability.when}`, `Use: ${capability.use}`, `Fallback: ${capability.fallback}`, '');
273
+ }
274
+ if (answers.sensitive) {
275
+ lines.push('This project handles sensitive data. Treat every change touching auth, input handling, storage, or third-party integrations as security-relevant (see OWASP checklist below) and never let secrets, keys, or credentials be hardcoded or logged.', '');
276
+ }
277
+
278
+ lines.push('## Tech Stack', '');
279
+ lines.push(`- Languages: ${detected.languages.length ? detected.languages.join(', ') : '(none auto-detected — fill in manually)'}`);
280
+ lines.push(`- Project type: ${detected.projectType}`);
281
+ lines.push(`- Integration signals: ${detected.signals?.length ? detected.signals.join(', ') : 'none detected'}`);
282
+ lines.push(`- Package manager: ${detected.packageManager ?? '(none detected)'}`);
283
+ lines.push(`- Monorepo: ${detected.monorepo ? 'yes' : 'no'}`);
284
+ lines.push('');
285
+
286
+ const typeGuidance = {
287
+ web: 'Web changes: verify keyboard access, focus, contrast, responsive layouts, and real-browser behavior.',
288
+ api: 'API changes: verify authentication and authorization server-side, input validation, rate limits, and migration safety.',
289
+ mobile: 'Mobile changes: verify on a real device where possible, including offline behavior, permissions, lifecycle, and release builds.',
290
+ cli: 'CLI changes: preserve useful exit codes, readable errors, non-interactive usage, and backwards-compatible flags.',
291
+ library: 'Library changes: treat exported APIs as contracts; check compatibility, documentation, and package artifacts.',
292
+ general: 'Document runtime assumptions, user-visible behavior, and the smallest reliable verification command for each change.',
293
+ };
294
+ lines.push('## Project-Type Guardrails', '', typeGuidance[detected.projectType], '');
295
+
296
+ lines.push('## Build, Run, Test', '');
297
+ const commandLines = [
298
+ ['Dev', c.dev], ['Build', c.build], ['Lint', c.lint], ['Typecheck', c.typecheck],
299
+ ['Test', c.test], ['Coverage', c.testCov], ['E2E', c.e2e], ['Full CI gate', c.checkAll],
300
+ ].filter(([, script]) => script);
301
+ if (commandLines.length === 0) {
302
+ const nativeTest = { uv: 'uv run pytest', poetry: 'poetry run pytest', go: 'go test ./...', cargo: 'cargo test', maven: 'mvn test', gradle: 'gradle test' }[pm];
303
+ lines.push(nativeTest ? `- Test: ${nativeTest}` : '(no scripts auto-detected in package.json — add build/lint/typecheck/test commands here manually)');
304
+ } else {
305
+ for (const [label, script] of commandLines) lines.push(`- ${label}: ${cmd(pm, script)}`);
306
+ }
307
+ lines.push('');
308
+
309
+ lines.push('## Subagent Workflow (Delegation, Review, Verification)', '');
310
+ lines.push('Move non-trivial changes through these stages. Use whatever specialized review agents/skills are installed for the current tool rather than skipping steps; do not build new ones for coverage that already exists.', '');
311
+ lines.push('1. **Plan** — restate the change and, for anything crossing a trust boundary (client vs. server, tenant vs. tenant), state which side owns the authoritative check.');
312
+ lines.push('2. **Delegate implementation** — break independent sub-tasks out rather than doing everything inline in one long pass.');
313
+ lines.push('3. **Review — correctness & quality** — run the relevant language/framework reviewer for the touched surface; check error handling isn\'t silently swallowing failures.');
314
+ lines.push('4. **Review — UX/UI & accessibility** — for any change to rendered UI: check keyboard navigation, focus management, contrast, and responsive behavior; verify interactively rather than trusting the diff.');
315
+ const owasp = [
316
+ 'A01 broken access control (never trust client-supplied IDs/roles; verify server-side)',
317
+ 'A02 cryptographic failures (secrets never in code or logs; verified token signing)',
318
+ 'A03 injection (parameterized queries, input validation/whitelisting)',
319
+ 'A04 insecure design (server-authoritative business rules, not client-enforced)',
320
+ 'A05 security misconfiguration (env separation, no debug/secrets leaking to client)',
321
+ 'A08 data integrity (immutable records where correctness depends on it)',
322
+ 'A09 logging/monitoring gaps (security-relevant events are actually logged)',
323
+ ];
324
+ lines.push(`5. **Review — security (OWASP)** — check for: ${owasp.join('; ')}. Scan every staged diff for hardcoded secrets before committing — treat this as manual and non-optional unless a pre-commit secret scanner is actually installed.`);
325
+ if (answers.multiTenant) {
326
+ lines.push(' - This project is multi-tenant: any change to authorization, database row-level security policies, or tenant-scoped queries needs explicit cross-tenant-access testing, not just a normal code review.');
327
+ }
328
+ lines.push('6. **Static checks & test verification** — run lint/typecheck first (cheap, catches most mistakes early), then confirm tests cover real behavior, not just the happy path. Check coverage reports for security/money/scoring-critical logic specifically — passing tests is not the same as adequate coverage.');
329
+ lines.push('7. **Completion & sign-off** — confirm the change actually works as intended (not just "tests pass"), and update any living task-tracking docs this project keeps.');
330
+ lines.push('');
331
+ lines.push('### Known gaps to flag rather than assume are covered');
332
+ lines.push('');
333
+ if (!detected.ci) {
334
+ lines.push('- **No CI detected** (`.github/workflows` is empty or absent) — nothing enforces these gates automatically. Recommend adding CI before relying on this workflow as a hard gate.');
335
+ } else {
336
+ lines.push('- CI workflows detected under `.github/workflows` — confirm they actually run lint/typecheck/test/e2e before treating CI as a backstop.');
337
+ }
338
+ lines.push(detected.dependencyScanning
339
+ ? '- Dependency scanning configuration detected — confirm it runs on every dependency change.'
340
+ : '- No dependency vulnerability scanning detected by this tool — check for Dependabot/Renovate config, or run the package manager\'s audit command periodically.');
341
+ lines.push(detected.secretScanning
342
+ ? '- Secret-scanning configuration detected — confirm it runs locally and in CI.'
343
+ : '- No automated secret scanning detected — treat secret-scanning in stage 5 as manual until a pre-commit hook or CI step exists.');
344
+ lines.push('');
345
+
346
+ lines.push('## Coding Conventions', '');
347
+ lines.push('(Fill in: language style rules, where shared types/DTOs live, test file naming convention, any "never do X" domain rules.)', '');
348
+
349
+ lines.push('## Gotchas and In-Progress Work', '');
350
+ lines.push('(Fill in from any living project docs, e.g. CURRENT_TASK.md/NEXT_STEPS.md if this project keeps them — what\'s mid-flight, what\'s deliberately deferred.)', '');
351
+
352
+ return `${START_MARKER}\n${lines.join('\n')}\n${END_MARKER}\n`;
353
+ }
354
+
355
+ function updateAgentsMd(existing, generated, mode) {
356
+ if (mode === 'replace') return generated;
357
+ const block = new RegExp(`${START_MARKER}[\\s\\S]*?${END_MARKER}`);
358
+ if (block.test(existing)) return existing.replace(block, generated.trimEnd());
359
+ return `${existing.trimEnd()}\n\n${generated}`;
360
+ }
361
+
362
+ const POINTER = (target) => `# ${target}\n\nThis project's AI agent instructions live in [AGENTS.md](./AGENTS.md) — read that file first. This file exists only so ${target === 'CLAUDE.md' ? 'Claude Code' : 'Gemini'} picks up the same instructions without duplicating them.\n`;
363
+
364
+ function reviewExisting(detected) {
365
+ const agentsPath = join(cwd, 'AGENTS.md');
366
+ const suggestions = [];
367
+
368
+ if (!detected.isProject) {
369
+ console.log('No project signals found. Start with the full setup interview.');
370
+ return;
371
+ }
372
+
373
+ console.log('Project analysis\n');
374
+ if (!detected.hasAgentsMd) {
375
+ console.log('No AGENTS.md found. Recommendations below are read-only.\n');
376
+ }
377
+
378
+ const content = detected.hasAgentsMd ? readFileSync(agentsPath, 'utf8') : '';
379
+ const has = (needle) => content.toLowerCase().includes(needle.toLowerCase());
380
+ if (content.length > 12000) {
381
+ suggestions.push('Root AGENTS.md is large — keep the shared contract here and move detailed domain, product, or platform rules into linked guides.');
382
+ }
383
+
384
+ const signalAdvice = {
385
+ database: 'Database integration detected — document migration/rollback ownership and verify tenant or authorization filters at the data boundary.',
386
+ payments: 'Payment integration detected — verify webhook authenticity, idempotency, secret handling, and failure/reconciliation paths.',
387
+ authentication: 'Authentication integration detected — verify token/session validation, authorization ownership, and account recovery paths server-side.',
388
+ ui: 'UI integration detected — verify keyboard access, focus behavior, contrast, responsive layouts, and real-browser behavior.',
389
+ };
390
+ for (const signal of detected.signals ?? []) {
391
+ if (!has(signalAdvice[signal])) suggestions.push(signalAdvice[signal]);
392
+ }
393
+
394
+ if (!has('subagent workflow') && !has('review workflow')) {
395
+ suggestions.push('No delegation/review workflow section found — add plan → delegate → review → security → verification stages.');
396
+ }
397
+ if (!has('owasp')) {
398
+ suggestions.push('No OWASP checklist referenced — add one if this project has auth, user input, or payment flows.');
399
+ }
400
+ if (!has('coverage') && (detected.commands.testCov || detected.commands.test)) {
401
+ suggestions.push('Coverage command exists in package.json but AGENTS.md doesn\'t mention checking coverage reports — add it.');
402
+ }
403
+ if (!has('secret')) {
404
+ suggestions.push('No mention of secret scanning — add a manual-check requirement, or wire up a real pre-commit scanner (none detected on disk).');
405
+ }
406
+ if (!has('accessib') && !has('a11y') && !has('ux')) {
407
+ suggestions.push('No UX/UI or accessibility review step found — add one if this project has a frontend.');
408
+ }
409
+ if (detected.multiTenantSignal && !has('tenant') && !has('row-level security') && !has('rls')) {
410
+ suggestions.push('Supabase detected but AGENTS.md doesn\'t mention tenant isolation / RLS — confirm whether this project is multi-tenant and document the review requirement if so.');
411
+ }
412
+ if (!detected.ci && !has('no ci')) {
413
+ suggestions.push('No CI detected and AGENTS.md doesn\'t flag it as a known gap — add a note so agents don\'t assume gates are enforced automatically.');
414
+ }
415
+ if (!detected.dependencyScanning && !has('dependency') ) {
416
+ suggestions.push('No Dependabot/Renovate config detected and AGENTS.md doesn\'t mention dependency scanning — add a note or wire one up.');
417
+ }
418
+ if (!detected.errorMonitoring && !has('sentry') && !has('error monitoring')) {
419
+ suggestions.push('No error-monitoring SDK (e.g. Sentry) detected — consider adding one, or note the gap.');
420
+ }
421
+
422
+ console.log(`${detected.hasAgentsMd ? `Reviewed ${agentsPath}` : 'Analysis complete'}\n`);
423
+ console.log('Recommended capabilities\n');
424
+ for (const capability of recommendedCapabilities(detected, { sensitive: detected.signals?.some((signal) => ['authentication', 'database', 'payments'].includes(signal)) })) {
425
+ console.log(`- ${capability.id}: ${capability.when}`);
426
+ }
427
+ console.log('');
428
+ if (detected.guidanceFiles?.length) {
429
+ console.log('Existing project guidance\n');
430
+ for (const file of detected.guidanceFiles) console.log(`- ${file}`);
431
+ console.log('');
432
+ }
433
+ if (suggestions.length === 0) {
434
+ console.log('No gaps found against the standard checklist. Looks solid.');
435
+ return;
436
+ }
437
+ console.log(`${suggestions.length} suggestion(s):\n`);
438
+ for (const s of suggestions) console.log(`- ${s}`);
439
+ }
440
+
441
+ async function main() {
442
+ const detected = detectStack();
443
+ console.log('Detected:', JSON.stringify(detected, null, 2));
444
+
445
+ if (reviewMode) {
446
+ reviewExisting(detected);
447
+ return;
448
+ }
449
+
450
+ const agentsPath = join(cwd, 'AGENTS.md');
451
+ let approvedExistingProjectWrite = false;
452
+ let updateMode = null;
453
+ let interactionRl = null;
454
+ if (detected.isProject) {
455
+ reviewExisting(detected);
456
+ interactionRl = readline.createInterface({ input: process.stdin, output: process.stdout });
457
+ if (!existsSync(agentsPath)) {
458
+ const apply = await ask(interactionRl, '\nApply a generated AGENTS.md from these recommendations? [y/N]: ');
459
+ updateMode = /^y/i.test(apply) ? 'replace' : null;
460
+ } else if (readFileSync(agentsPath, 'utf8').includes(START_MARKER)) {
461
+ const apply = await ask(interactionRl, '\nUpdate the managed governance section? [y/N]: ');
462
+ updateMode = /^y/i.test(apply) ? 'markers' : null;
463
+ } else {
464
+ const choice = await ask(interactionRl, '\nAGENTS.md is unmarked. Choose [a]ppend, [r]eplace, or [c]ancel [c]: ');
465
+ updateMode = /^a/i.test(choice) ? 'append' : /^r/i.test(choice) ? 'replace' : null;
466
+ }
467
+ approvedExistingProjectWrite = true;
468
+ if (!updateMode) {
469
+ interactionRl.close();
470
+ console.log('No files changed. Re-run when you are ready to apply the recommendations.');
471
+ return;
472
+ }
473
+ }
474
+
475
+ if (existsSync(agentsPath) && !force && !approvedExistingProjectWrite) {
476
+ console.log('\nAGENTS.md already exists. Re-run with --force to overwrite, or merge manually.');
477
+ process.exit(1);
478
+ }
479
+
480
+ const known = existsSync(agentsPath) ? knownAnswersFromAgents(readFileSync(agentsPath, 'utf8')) : {};
481
+ const answers = await gatherAnswers(detected, known, interactionRl);
482
+ const agentsMd = buildAgentsMd(detected, answers);
483
+
484
+ const files = {
485
+ 'AGENTS.md': agentsMd,
486
+ 'CLAUDE.md': POINTER('CLAUDE.md'),
487
+ 'GEMINI.md': POINTER('GEMINI.md'),
488
+ };
489
+
490
+ console.log('\n--- Files to write ---');
491
+ for (const name of Object.keys(files)) console.log(`- ${join(cwd, name)}`);
492
+
493
+ if (dryRun) {
494
+ console.log('\n(dry run — nothing written; re-run without --dry-run to apply)');
495
+ return;
496
+ }
497
+
498
+ for (const [name, content] of Object.entries(files)) {
499
+ if (name === 'AGENTS.md' && updateMode && existsSync(agentsPath)) {
500
+ writeFileSync(agentsPath, updateAgentsMd(readFileSync(agentsPath, 'utf8'), content, updateMode), 'utf8');
501
+ continue;
502
+ }
503
+ if (name !== 'AGENTS.md' && existsSync(join(cwd, name)) && !force) {
504
+ console.log(`Skipping ${name} (already exists; use --force to overwrite)`);
505
+ continue;
506
+ }
507
+ writeFileSync(join(cwd, name), content, 'utf8');
508
+ }
509
+ console.log('\nDone. Codex and other AGENTS.md-aware tools read AGENTS.md directly; Cursor users can add a .cursorrules pointer the same way.');
510
+ }
511
+
512
+ if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replaceAll('\\', '/'))) main();
513
+
514
+ export { buildAgentsMd, knownAnswersFromAgents, recommendedCapabilities, updateAgentsMd };
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "project-governance-init",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency CLI that generates a portable AGENTS.md (plus CLAUDE.md/GEMINI.md pointers) with a repeatable subagent workflow: plan, delegate, review, UX/a11y, OWASP security, static checks, verification.",
5
+ "type": "module",
6
+ "bin": {
7
+ "project-governance-init": "bin/init.mjs"
8
+ },
9
+ "files": ["bin", "skills", "README.md"],
10
+ "scripts": {
11
+ "start": "node bin/init.mjs",
12
+ "test": "node --test"
13
+ },
14
+ "license": "MIT"
15
+ }