@ai-sdlc/orchestrator 0.9.0 → 0.10.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/dist/__test-helpers/git-env.d.ts +40 -0
- package/dist/__test-helpers/git-env.js +73 -0
- package/dist/admission-composite.d.ts +22 -0
- package/dist/admission-composite.js +85 -2
- package/dist/admission-enrichment.js +12 -2
- package/dist/admission-hc.d.ts +20 -1
- package/dist/admission-hc.js +12 -1
- package/dist/admission-score.d.ts +14 -0
- package/dist/admission-score.js +7 -1
- package/dist/cli/commands/git-remote.d.ts +25 -0
- package/dist/cli/commands/git-remote.js +80 -1
- package/dist/cli/commands/init-features.d.ts +208 -0
- package/dist/cli/commands/init-features.js +473 -0
- package/dist/cli/commands/init-templates.d.ts +104 -0
- package/dist/cli/commands/init-templates.js +399 -0
- package/dist/cli/commands/init.d.ts +22 -0
- package/dist/cli/commands/init.js +158 -1
- package/dist/design-authority.d.ts +13 -0
- package/dist/design-authority.js +13 -0
- package/dist/execute.js +8 -5
- package/dist/models/classifier.d.ts +3 -1
- package/dist/models/classifier.js +61 -5
- package/dist/pillar-breakdown.d.ts +23 -1
- package/dist/pillar-breakdown.js +9 -0
- package/dist/runtime/attestations.d.ts +502 -10
- package/dist/runtime/attestations.js +759 -24
- package/dist/runtime/parallelism-flag.d.ts +21 -3
- package/dist/runtime/parallelism-flag.js +27 -6
- package/dist/runtime/port-allocator.d.ts +11 -0
- package/dist/runtime/port-allocator.js +30 -0
- package/package.json +3 -2
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-sdlc init` interactive wizard + feature dispatcher (AISDLC-143).
|
|
3
|
+
*
|
|
4
|
+
* Per Q4(b) of the operator-ratified quality-gate redesign, `ai-sdlc init`
|
|
5
|
+
* is a wizard by default with `--yes` for non-interactive (CI/scripts) and
|
|
6
|
+
* `--with-X` flags for explicit opt-in (`--with-dor`, `--with-attestation`,
|
|
7
|
+
* `--with-classifier`, `--with-branch-protection`). This module owns:
|
|
8
|
+
*
|
|
9
|
+
* 1. The ordered prompt list (resolveFeatureSelection).
|
|
10
|
+
* 2. The feature-toggle → file-write dispatcher (applyFeatureSelection).
|
|
11
|
+
* 3. The branch-protection helper (applyBranchProtection) including the
|
|
12
|
+
* `--dry-run` JSON-print path required by AC #6.
|
|
13
|
+
* 4. The "next steps" summary printed at the end of init (AC #5).
|
|
14
|
+
*
|
|
15
|
+
* Test surface: every public function takes a small options bag with
|
|
16
|
+
* injectable side-effect adapters (prompter, writeFile, runCommand) so the
|
|
17
|
+
* test suite can drive every wizard branch hermetically without spinning
|
|
18
|
+
* up a TTY or shelling out to `gh`. Production callers in `init.ts` pass
|
|
19
|
+
* the real adapters.
|
|
20
|
+
*/
|
|
21
|
+
/** Per-feature on/off bits derived from prompts + flags. */
|
|
22
|
+
export interface FeatureSelection {
|
|
23
|
+
dor: boolean;
|
|
24
|
+
attestation: boolean;
|
|
25
|
+
classifier: boolean;
|
|
26
|
+
branchProtection: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** All feature flags off — used as the initial state before flags + prompts. */
|
|
29
|
+
export declare const NO_FEATURES: FeatureSelection;
|
|
30
|
+
/** All features on — the answer used by `--yes` (accept all defaults). */
|
|
31
|
+
export declare const ALL_FEATURES: FeatureSelection;
|
|
32
|
+
/** Flag-bag controlling wizard behavior (already parsed from argv). */
|
|
33
|
+
export interface WizardFlags {
|
|
34
|
+
/** `--yes` short-circuits the wizard; treats every prompt as "yes". */
|
|
35
|
+
yes: boolean;
|
|
36
|
+
/** `--with-dor` forces the DoR feature on without prompting. */
|
|
37
|
+
withDor: boolean;
|
|
38
|
+
/** `--with-attestation` forces attestation infra on without prompting. */
|
|
39
|
+
withAttestation: boolean;
|
|
40
|
+
/** `--with-classifier` forces the classifier on without prompting. */
|
|
41
|
+
withClassifier: boolean;
|
|
42
|
+
/** `--with-branch-protection` forces branch-protection on without prompting. */
|
|
43
|
+
withBranchProtection: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* `--add <feature>` extends an already-initialized repo with a single
|
|
46
|
+
* feature without re-prompting. AC #7 (idempotent extension). When set,
|
|
47
|
+
* the wizard short-circuits to scaffold ONLY this feature.
|
|
48
|
+
*/
|
|
49
|
+
add?: 'dor' | 'attestation' | 'classifier' | 'branch-protection';
|
|
50
|
+
/** `--dry-run` — print what would be done, don't write. */
|
|
51
|
+
dryRun: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Single-question prompter contract — accepts a question + default and
|
|
55
|
+
* returns the user's answer. The production adapter wraps `@inquirer/prompts`
|
|
56
|
+
* so the user gets a real readline TTY; tests inject a stub that returns
|
|
57
|
+
* scripted answers without touching stdin.
|
|
58
|
+
*
|
|
59
|
+
* Why a single-question primitive instead of "ask all questions at once":
|
|
60
|
+
* the prompts are conditional in some cases (e.g. branch-protection only
|
|
61
|
+
* makes sense after the user has chosen which CI gates exist). Keeping
|
|
62
|
+
* the primitive small lets `resolveFeatureSelection` decide ordering +
|
|
63
|
+
* skip questions whose answer is already determined by a `--with-X` flag.
|
|
64
|
+
*/
|
|
65
|
+
export type Prompter = (question: string, defaultYes: boolean) => Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* Side-effect adapter bag — every part of the dispatcher that touches
|
|
68
|
+
* disk or shells out goes through this so tests can assert on intents
|
|
69
|
+
* without mocking `node:fs` globally.
|
|
70
|
+
*/
|
|
71
|
+
export interface FeatureAdapters {
|
|
72
|
+
/** Resolve to an interactive prompt answer. */
|
|
73
|
+
prompt: Prompter;
|
|
74
|
+
/** Write a file. Production = `node:fs.writeFileSync`. */
|
|
75
|
+
writeFile: (path: string, contents: string) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Append `contents` to `path` exactly once: if `sentinel` is already
|
|
78
|
+
* present in the file, no-op. If the file doesn't exist, behaves like
|
|
79
|
+
* a write. Used for the husky pre-push sign block + CLAUDE.md pointer
|
|
80
|
+
* (both of which need to coexist with user-edited content).
|
|
81
|
+
*/
|
|
82
|
+
appendOnce: (path: string, contents: string, sentinel: string) => 'appended' | 'skipped';
|
|
83
|
+
/** mkdir -p. Production = `node:fs.mkdirSync({ recursive: true })`. */
|
|
84
|
+
mkdirp: (path: string) => void;
|
|
85
|
+
/** Test for path existence. Production = `node:fs.existsSync`. */
|
|
86
|
+
exists: (path: string) => boolean;
|
|
87
|
+
/** Run a shell command (used for `gh api`). Production = `execSync`. */
|
|
88
|
+
runCommand: (cmd: string, args: string[]) => {
|
|
89
|
+
stdout: string;
|
|
90
|
+
exitCode: number;
|
|
91
|
+
};
|
|
92
|
+
/** Sink for operator-visible output (defaults to console.log). */
|
|
93
|
+
log: (line: string) => void;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build the production adapter bag. Pulled into a factory so tests can
|
|
97
|
+
* compose a partial override bag (e.g. only override `prompt`) and let
|
|
98
|
+
* the rest fall through to real disk writes.
|
|
99
|
+
*
|
|
100
|
+
* The `prompt` adapter is a lazy import of `@inquirer/prompts.confirm`
|
|
101
|
+
* so that:
|
|
102
|
+
* 1. Tests don't pay the import cost when they inject their own stub.
|
|
103
|
+
* 2. `--yes` runs (which never call `prompt`) don't pay it either.
|
|
104
|
+
* 3. The orchestrator's runtime `dist/` is smaller for the common case.
|
|
105
|
+
*/
|
|
106
|
+
export declare function buildProductionAdapters(): FeatureAdapters;
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the per-feature on/off vector by combining (in priority order):
|
|
109
|
+
* 1. `--add <feature>` — if set, ONLY that feature is on; everything
|
|
110
|
+
* else is suppressed (idempotent extension, AC #7).
|
|
111
|
+
* 2. `--yes` — accept every default (every feature on).
|
|
112
|
+
* 3. `--with-X` flags — opt-in without prompting.
|
|
113
|
+
* 4. Interactive prompts for any feature still undetermined.
|
|
114
|
+
*
|
|
115
|
+
* Returns a fully-determined FeatureSelection. The dispatcher then writes
|
|
116
|
+
* exactly the union of features marked true.
|
|
117
|
+
*/
|
|
118
|
+
export declare function resolveFeatureSelection(flags: WizardFlags, adapters: Pick<FeatureAdapters, 'prompt' | 'log'>): Promise<FeatureSelection>;
|
|
119
|
+
/** Return value of `applyFeatureSelection` — what was actually written. */
|
|
120
|
+
export interface ApplyResult {
|
|
121
|
+
/** Files that were newly created on this run. */
|
|
122
|
+
created: string[];
|
|
123
|
+
/** Files that already existed and were left untouched (idempotent). */
|
|
124
|
+
skipped: string[];
|
|
125
|
+
/** Files that would have been created if not for `--dry-run`. */
|
|
126
|
+
wouldCreate: string[];
|
|
127
|
+
/** Branch-protection result, if attempted. */
|
|
128
|
+
branchProtection?: BranchProtectionResult;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Write the union of feature templates into the project dir. AC #4 says
|
|
132
|
+
* the BASELINE workflow templates (gate workflow) are always written; the
|
|
133
|
+
* per-feature template sets are written only when their toggle is on.
|
|
134
|
+
*
|
|
135
|
+
* Idempotent: any file that already exists at the target path is skipped
|
|
136
|
+
* with a "skip" log line. This is what makes `--add <feature>` safe to
|
|
137
|
+
* run on an already-initialized repo (AC #7).
|
|
138
|
+
*/
|
|
139
|
+
export declare function applyFeatureSelection(projectDir: string, selection: FeatureSelection, flags: WizardFlags, adapters: FeatureAdapters): Promise<ApplyResult>;
|
|
140
|
+
export interface BranchProtectionResult {
|
|
141
|
+
/** Whether the rule was actually applied. False in dry-run. */
|
|
142
|
+
applied: boolean;
|
|
143
|
+
/** The PUT body as a JSON string (always populated for visibility). */
|
|
144
|
+
bodyJson: string;
|
|
145
|
+
/** Error message from `gh api`, if non-zero exit. */
|
|
146
|
+
error?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Recommended branch-protection ruleset for AI-SDLC adopters. AC #1 #4:
|
|
150
|
+
* the required checks are `ai-sdlc/pr-ready` (the gate aggregator) and
|
|
151
|
+
* `codecov/patch` (the de facto coverage signal). Other AI-SDLC apps
|
|
152
|
+
* post their own statuses but they're all rolled into pr-ready.
|
|
153
|
+
*
|
|
154
|
+
* The body conforms to the GitHub REST API
|
|
155
|
+
* `PUT /repos/{owner}/{repo}/branches/{branch}/protection` schema.
|
|
156
|
+
*/
|
|
157
|
+
export declare const RECOMMENDED_BRANCH_PROTECTION_BODY: {
|
|
158
|
+
required_status_checks: {
|
|
159
|
+
strict: boolean;
|
|
160
|
+
contexts: string[];
|
|
161
|
+
};
|
|
162
|
+
enforce_admins: boolean;
|
|
163
|
+
required_pull_request_reviews: {
|
|
164
|
+
dismiss_stale_reviews: boolean;
|
|
165
|
+
require_code_owner_reviews: boolean;
|
|
166
|
+
required_approving_review_count: number;
|
|
167
|
+
};
|
|
168
|
+
restrictions: null;
|
|
169
|
+
allow_force_pushes: boolean;
|
|
170
|
+
allow_deletions: boolean;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Apply (or print, in dry-run) the recommended branch protection rule
|
|
174
|
+
* to the `main` branch of the repo at `projectDir`. AC #6 explicitly
|
|
175
|
+
* requires that `--dry-run` print the JSON without applying.
|
|
176
|
+
*
|
|
177
|
+
* The repo identity (`owner/repo`) is resolved by shelling out to
|
|
178
|
+
* `gh repo view --json nameWithOwner -q .nameWithOwner`. We could parse
|
|
179
|
+
* the git remote ourselves (see git-remote.ts) but `gh` already resolves
|
|
180
|
+
* forks + renames + custom default branches consistently, and the user
|
|
181
|
+
* needs `gh` on PATH for the PUT to work anyway.
|
|
182
|
+
*/
|
|
183
|
+
export declare function applyBranchProtection(projectDir: string, flags: WizardFlags, adapters: Pick<FeatureAdapters, 'runCommand' | 'log'>): Promise<BranchProtectionResult>;
|
|
184
|
+
/**
|
|
185
|
+
* Print the structured "next steps" summary at the end of init. AC #5:
|
|
186
|
+
* the summary must include operator action items conditional on which
|
|
187
|
+
* features were chosen (e.g. `gh secret set` commands when attestation
|
|
188
|
+
* was opted in).
|
|
189
|
+
*
|
|
190
|
+
* Returns the rendered summary as a string in addition to logging it,
|
|
191
|
+
* so tests can assert on it without re-stringifying console output.
|
|
192
|
+
*/
|
|
193
|
+
export declare function renderNextSteps(selection: FeatureSelection, result: ApplyResult, adapters: Pick<FeatureAdapters, 'log'>): string;
|
|
194
|
+
/**
|
|
195
|
+
* The pointer block we append to CLAUDE.md so a freshly-initialized repo's
|
|
196
|
+
* Claude Code sessions know where to find the AI-SDLC quality-gate docs.
|
|
197
|
+
* Idempotent — guarded by a sentinel so re-running init doesn't duplicate
|
|
198
|
+
* the block.
|
|
199
|
+
*/
|
|
200
|
+
export declare const CLAUDE_MD_POINTER = "\n<!-- ai-sdlc:recommendation-pointer -->\n## AI-SDLC quality gate\n\nThis repo is bootstrapped with the AI-SDLC framework. The single PR-ready\nmerge gate is `ai-sdlc/pr-ready` (see `.github/workflows/ai-sdlc-gate.yml`).\nRun `ai-sdlc health` to verify your local config; see\n`docs/operations/init.md` for the adopter guide.\n<!-- end ai-sdlc:recommendation-pointer -->\n";
|
|
201
|
+
/** Sentinel marker used by the CLAUDE.md pointer for idempotency. */
|
|
202
|
+
export declare const CLAUDE_MD_SENTINEL = "<!-- ai-sdlc:recommendation-pointer -->";
|
|
203
|
+
/**
|
|
204
|
+
* Append the recommendation pointer to CLAUDE.md (or create the file if
|
|
205
|
+
* missing). Idempotent: if the sentinel is already present we no-op.
|
|
206
|
+
*/
|
|
207
|
+
export declare function ensureClaudeMdPointer(projectDir: string, adapters: Pick<FeatureAdapters, 'exists' | 'writeFile' | 'appendOnce' | 'log'>, dryRun: boolean): void;
|
|
208
|
+
//# sourceMappingURL=init-features.d.ts.map
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-sdlc init` interactive wizard + feature dispatcher (AISDLC-143).
|
|
3
|
+
*
|
|
4
|
+
* Per Q4(b) of the operator-ratified quality-gate redesign, `ai-sdlc init`
|
|
5
|
+
* is a wizard by default with `--yes` for non-interactive (CI/scripts) and
|
|
6
|
+
* `--with-X` flags for explicit opt-in (`--with-dor`, `--with-attestation`,
|
|
7
|
+
* `--with-classifier`, `--with-branch-protection`). This module owns:
|
|
8
|
+
*
|
|
9
|
+
* 1. The ordered prompt list (resolveFeatureSelection).
|
|
10
|
+
* 2. The feature-toggle → file-write dispatcher (applyFeatureSelection).
|
|
11
|
+
* 3. The branch-protection helper (applyBranchProtection) including the
|
|
12
|
+
* `--dry-run` JSON-print path required by AC #6.
|
|
13
|
+
* 4. The "next steps" summary printed at the end of init (AC #5).
|
|
14
|
+
*
|
|
15
|
+
* Test surface: every public function takes a small options bag with
|
|
16
|
+
* injectable side-effect adapters (prompter, writeFile, runCommand) so the
|
|
17
|
+
* test suite can drive every wizard branch hermetically without spinning
|
|
18
|
+
* up a TTY or shelling out to `gh`. Production callers in `init.ts` pass
|
|
19
|
+
* the real adapters.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { join, dirname } from 'node:path';
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
import { ATTESTATION_TEMPLATES, BASELINE_WORKFLOW_TEMPLATES, CLASSIFIER_TEMPLATES, DOR_TEMPLATES, HUSKY_PREPUSH_SIGN_SNIPPET, } from './init-templates.js';
|
|
25
|
+
/** All feature flags off — used as the initial state before flags + prompts. */
|
|
26
|
+
export const NO_FEATURES = {
|
|
27
|
+
dor: false,
|
|
28
|
+
attestation: false,
|
|
29
|
+
classifier: false,
|
|
30
|
+
branchProtection: false,
|
|
31
|
+
};
|
|
32
|
+
/** All features on — the answer used by `--yes` (accept all defaults). */
|
|
33
|
+
export const ALL_FEATURES = {
|
|
34
|
+
dor: true,
|
|
35
|
+
attestation: true,
|
|
36
|
+
classifier: true,
|
|
37
|
+
branchProtection: true,
|
|
38
|
+
};
|
|
39
|
+
// ── Production adapter factory ───────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Build the production adapter bag. Pulled into a factory so tests can
|
|
42
|
+
* compose a partial override bag (e.g. only override `prompt`) and let
|
|
43
|
+
* the rest fall through to real disk writes.
|
|
44
|
+
*
|
|
45
|
+
* The `prompt` adapter is a lazy import of `@inquirer/prompts.confirm`
|
|
46
|
+
* so that:
|
|
47
|
+
* 1. Tests don't pay the import cost when they inject their own stub.
|
|
48
|
+
* 2. `--yes` runs (which never call `prompt`) don't pay it either.
|
|
49
|
+
* 3. The orchestrator's runtime `dist/` is smaller for the common case.
|
|
50
|
+
*/
|
|
51
|
+
export function buildProductionAdapters() {
|
|
52
|
+
return {
|
|
53
|
+
prompt: async (question, defaultYes) => {
|
|
54
|
+
// Lazy import — see docblock above for why.
|
|
55
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
56
|
+
return confirm({ message: question, default: defaultYes });
|
|
57
|
+
},
|
|
58
|
+
writeFile: (path, contents) => writeFileSync(path, contents, 'utf-8'),
|
|
59
|
+
appendOnce: (path, contents, sentinel) => {
|
|
60
|
+
// Make sure the parent dir exists (appendFileSync errors with ENOENT
|
|
61
|
+
// otherwise; we may be writing into a freshly-created `.husky/`).
|
|
62
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
63
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf-8') : '';
|
|
64
|
+
if (existing.includes(sentinel))
|
|
65
|
+
return 'skipped';
|
|
66
|
+
const sep = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
67
|
+
writeFileSync(path, existing + sep + contents, 'utf-8');
|
|
68
|
+
return 'appended';
|
|
69
|
+
},
|
|
70
|
+
mkdirp: (path) => mkdirSync(path, { recursive: true }),
|
|
71
|
+
exists: (path) => existsSync(path),
|
|
72
|
+
runCommand: (cmd, args) => {
|
|
73
|
+
try {
|
|
74
|
+
// Use `execFileSync` (no shell) so args are passed as a true
|
|
75
|
+
// argv array — never word-split or shell-interpreted. The prior
|
|
76
|
+
// `execSync(\`${cmd} ${args.join(' ')}\`)` form ran the command
|
|
77
|
+
// through `/bin/sh -c` and silently broke whenever any argument
|
|
78
|
+
// contained whitespace (e.g. macOS users with `~/Documents/My
|
|
79
|
+
// Project/` in their projectDir, where the `--input <tmpPath>`
|
|
80
|
+
// arg to `gh api` would word-split and `gh` would see two
|
|
81
|
+
// unrelated tokens — branch protection would silently fail or
|
|
82
|
+
// apply wrong content). Switching to `execFileSync` eliminates
|
|
83
|
+
// both word-splitting AND any shell-injection surface in one
|
|
84
|
+
// change. Stderr stays muted so users still get the clean
|
|
85
|
+
// single-line error rendered by `applyBranchProtection`.
|
|
86
|
+
const stdout = execFileSync(cmd, args, {
|
|
87
|
+
encoding: 'utf-8',
|
|
88
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
89
|
+
});
|
|
90
|
+
return { stdout, exitCode: 0 };
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const e = err;
|
|
94
|
+
return {
|
|
95
|
+
stdout: typeof e.stdout === 'string' ? e.stdout : (e.stdout?.toString() ?? ''),
|
|
96
|
+
exitCode: e.status ?? 1,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
log: (line) => console.log(line),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// ── Wizard ───────────────────────────────────────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Resolve the per-feature on/off vector by combining (in priority order):
|
|
106
|
+
* 1. `--add <feature>` — if set, ONLY that feature is on; everything
|
|
107
|
+
* else is suppressed (idempotent extension, AC #7).
|
|
108
|
+
* 2. `--yes` — accept every default (every feature on).
|
|
109
|
+
* 3. `--with-X` flags — opt-in without prompting.
|
|
110
|
+
* 4. Interactive prompts for any feature still undetermined.
|
|
111
|
+
*
|
|
112
|
+
* Returns a fully-determined FeatureSelection. The dispatcher then writes
|
|
113
|
+
* exactly the union of features marked true.
|
|
114
|
+
*/
|
|
115
|
+
export async function resolveFeatureSelection(flags, adapters) {
|
|
116
|
+
// ── Path 1: `--add` — single-feature extension ────────────────────────
|
|
117
|
+
if (flags.add) {
|
|
118
|
+
const sel = { ...NO_FEATURES };
|
|
119
|
+
switch (flags.add) {
|
|
120
|
+
case 'dor':
|
|
121
|
+
sel.dor = true;
|
|
122
|
+
break;
|
|
123
|
+
case 'attestation':
|
|
124
|
+
sel.attestation = true;
|
|
125
|
+
break;
|
|
126
|
+
case 'classifier':
|
|
127
|
+
sel.classifier = true;
|
|
128
|
+
break;
|
|
129
|
+
case 'branch-protection':
|
|
130
|
+
sel.branchProtection = true;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
return sel;
|
|
134
|
+
}
|
|
135
|
+
// ── Path 2: `--yes` — all defaults on, no prompts ─────────────────────
|
|
136
|
+
if (flags.yes) {
|
|
137
|
+
return { ...ALL_FEATURES };
|
|
138
|
+
}
|
|
139
|
+
// ── Path 3+4: `--with-X` overrides + prompts for the rest ─────────────
|
|
140
|
+
const sel = { ...NO_FEATURES };
|
|
141
|
+
// DoR
|
|
142
|
+
if (flags.withDor) {
|
|
143
|
+
sel.dor = true;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
sel.dor = await adapters.prompt('Will this repo use Definition-of-Ready gates?', true);
|
|
147
|
+
}
|
|
148
|
+
// Attestation
|
|
149
|
+
if (flags.withAttestation) {
|
|
150
|
+
sel.attestation = true;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
sel.attestation = await adapters.prompt('Do you want attestation infrastructure (audit-only)?', true);
|
|
154
|
+
}
|
|
155
|
+
// Classifier
|
|
156
|
+
if (flags.withClassifier) {
|
|
157
|
+
sel.classifier = true;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
sel.classifier = await adapters.prompt('Add review classifier for cost-optimized reviews?', true);
|
|
161
|
+
}
|
|
162
|
+
// Branch protection
|
|
163
|
+
if (flags.withBranchProtection) {
|
|
164
|
+
sel.branchProtection = true;
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
sel.branchProtection = await adapters.prompt('Apply recommended branch protection? (required: ai-sdlc/pr-ready + codecov/patch)', true);
|
|
168
|
+
}
|
|
169
|
+
return sel;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Write the union of feature templates into the project dir. AC #4 says
|
|
173
|
+
* the BASELINE workflow templates (gate workflow) are always written; the
|
|
174
|
+
* per-feature template sets are written only when their toggle is on.
|
|
175
|
+
*
|
|
176
|
+
* Idempotent: any file that already exists at the target path is skipped
|
|
177
|
+
* with a "skip" log line. This is what makes `--add <feature>` safe to
|
|
178
|
+
* run on an already-initialized repo (AC #7).
|
|
179
|
+
*/
|
|
180
|
+
export async function applyFeatureSelection(projectDir, selection, flags, adapters) {
|
|
181
|
+
const result = { created: [], skipped: [], wouldCreate: [] };
|
|
182
|
+
// Build the union of templates to write.
|
|
183
|
+
const templateSets = [];
|
|
184
|
+
// `--add` mode: skip the baseline (we're EXTENDING an existing init).
|
|
185
|
+
if (!flags.add) {
|
|
186
|
+
templateSets.push(BASELINE_WORKFLOW_TEMPLATES);
|
|
187
|
+
}
|
|
188
|
+
if (selection.dor)
|
|
189
|
+
templateSets.push(DOR_TEMPLATES);
|
|
190
|
+
if (selection.attestation)
|
|
191
|
+
templateSets.push(ATTESTATION_TEMPLATES);
|
|
192
|
+
if (selection.classifier)
|
|
193
|
+
templateSets.push(CLASSIFIER_TEMPLATES);
|
|
194
|
+
for (const set of templateSets) {
|
|
195
|
+
for (const [relPath, contents] of Object.entries(set.files)) {
|
|
196
|
+
const absPath = join(projectDir, relPath);
|
|
197
|
+
if (flags.dryRun) {
|
|
198
|
+
result.wouldCreate.push(relPath);
|
|
199
|
+
adapters.log(` would create ${relPath}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (adapters.exists(absPath)) {
|
|
203
|
+
result.skipped.push(relPath);
|
|
204
|
+
adapters.log(` skip ${relPath} (already exists)`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
// mkdir -p the parent
|
|
208
|
+
adapters.mkdirp(dirname(absPath));
|
|
209
|
+
adapters.writeFile(absPath, contents);
|
|
210
|
+
result.created.push(relPath);
|
|
211
|
+
adapters.log(` created ${relPath}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Husky pre-push sign hook is a separate concern from the
|
|
215
|
+
// FeatureTemplateSet because it's an APPEND (not a write-from-empty)
|
|
216
|
+
// — adopters often already have a .husky/pre-push from their existing
|
|
217
|
+
// tooling and we don't want to clobber it. Only fired when attestation
|
|
218
|
+
// is on.
|
|
219
|
+
if (selection.attestation && !flags.dryRun) {
|
|
220
|
+
const hookPath = join(projectDir, '.husky', 'pre-push');
|
|
221
|
+
if (!adapters.exists(hookPath)) {
|
|
222
|
+
// No existing hook — write a minimal one with the sign block.
|
|
223
|
+
adapters.mkdirp(dirname(hookPath));
|
|
224
|
+
adapters.writeFile(hookPath, `#!/usr/bin/env bash\nset -euo pipefail\n\n${HUSKY_PREPUSH_SIGN_SNIPPET}`);
|
|
225
|
+
result.created.push('.husky/pre-push');
|
|
226
|
+
adapters.log(` created .husky/pre-push`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const status = adapters.appendOnce(hookPath, HUSKY_PREPUSH_SIGN_SNIPPET, '# ai-sdlc:attestation-sign-block');
|
|
230
|
+
if (status === 'appended') {
|
|
231
|
+
adapters.log(` updated .husky/pre-push (appended sign block)`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
result.skipped.push('.husky/pre-push');
|
|
235
|
+
adapters.log(` skip .husky/pre-push (sign block already present)`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (selection.attestation && flags.dryRun) {
|
|
240
|
+
result.wouldCreate.push('.husky/pre-push');
|
|
241
|
+
adapters.log(' would update .husky/pre-push (sign block)');
|
|
242
|
+
}
|
|
243
|
+
// Branch protection (always last — depends on the gate workflow being
|
|
244
|
+
// present so the required check exists when the rule is applied).
|
|
245
|
+
if (selection.branchProtection) {
|
|
246
|
+
result.branchProtection = await applyBranchProtection(projectDir, flags, adapters);
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Recommended branch-protection ruleset for AI-SDLC adopters. AC #1 #4:
|
|
252
|
+
* the required checks are `ai-sdlc/pr-ready` (the gate aggregator) and
|
|
253
|
+
* `codecov/patch` (the de facto coverage signal). Other AI-SDLC apps
|
|
254
|
+
* post their own statuses but they're all rolled into pr-ready.
|
|
255
|
+
*
|
|
256
|
+
* The body conforms to the GitHub REST API
|
|
257
|
+
* `PUT /repos/{owner}/{repo}/branches/{branch}/protection` schema.
|
|
258
|
+
*/
|
|
259
|
+
export const RECOMMENDED_BRANCH_PROTECTION_BODY = {
|
|
260
|
+
required_status_checks: {
|
|
261
|
+
strict: true,
|
|
262
|
+
contexts: ['ai-sdlc/pr-ready', 'codecov/patch'],
|
|
263
|
+
},
|
|
264
|
+
enforce_admins: false,
|
|
265
|
+
required_pull_request_reviews: {
|
|
266
|
+
dismiss_stale_reviews: true,
|
|
267
|
+
require_code_owner_reviews: false,
|
|
268
|
+
required_approving_review_count: 1,
|
|
269
|
+
},
|
|
270
|
+
restrictions: null,
|
|
271
|
+
allow_force_pushes: false,
|
|
272
|
+
allow_deletions: false,
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* Apply (or print, in dry-run) the recommended branch protection rule
|
|
276
|
+
* to the `main` branch of the repo at `projectDir`. AC #6 explicitly
|
|
277
|
+
* requires that `--dry-run` print the JSON without applying.
|
|
278
|
+
*
|
|
279
|
+
* The repo identity (`owner/repo`) is resolved by shelling out to
|
|
280
|
+
* `gh repo view --json nameWithOwner -q .nameWithOwner`. We could parse
|
|
281
|
+
* the git remote ourselves (see git-remote.ts) but `gh` already resolves
|
|
282
|
+
* forks + renames + custom default branches consistently, and the user
|
|
283
|
+
* needs `gh` on PATH for the PUT to work anyway.
|
|
284
|
+
*/
|
|
285
|
+
export async function applyBranchProtection(projectDir, flags, adapters) {
|
|
286
|
+
const bodyJson = JSON.stringify(RECOMMENDED_BRANCH_PROTECTION_BODY, null, 2);
|
|
287
|
+
if (flags.dryRun) {
|
|
288
|
+
adapters.log('');
|
|
289
|
+
adapters.log('Branch-protection dry-run — would PUT the following body:');
|
|
290
|
+
adapters.log(bodyJson);
|
|
291
|
+
adapters.log('');
|
|
292
|
+
adapters.log(' endpoint: PUT /repos/{owner}/{repo}/branches/main/protection');
|
|
293
|
+
adapters.log(' apply with: gh api -X PUT repos/{owner}/{repo}/branches/main/protection ...');
|
|
294
|
+
return { applied: false, bodyJson };
|
|
295
|
+
}
|
|
296
|
+
// Resolve owner/repo via gh.
|
|
297
|
+
const ownerRepo = adapters.runCommand('gh', [
|
|
298
|
+
'repo',
|
|
299
|
+
'view',
|
|
300
|
+
'--json',
|
|
301
|
+
'nameWithOwner',
|
|
302
|
+
'-q',
|
|
303
|
+
'.nameWithOwner',
|
|
304
|
+
]);
|
|
305
|
+
if (ownerRepo.exitCode !== 0) {
|
|
306
|
+
return {
|
|
307
|
+
applied: false,
|
|
308
|
+
bodyJson,
|
|
309
|
+
error: `gh repo view failed: ${ownerRepo.stdout.trim() || 'unknown error'}`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const slug = ownerRepo.stdout.trim();
|
|
313
|
+
if (!slug) {
|
|
314
|
+
return {
|
|
315
|
+
applied: false,
|
|
316
|
+
bodyJson,
|
|
317
|
+
error: 'gh repo view returned empty owner/repo',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// Use the file-based input form so we don't have to thread quoted JSON
|
|
321
|
+
// through a shell. We write to a tmpfile, point gh at it, and let the
|
|
322
|
+
// adapter's runCommand spawn `gh` directly.
|
|
323
|
+
const tmpPath = join(projectDir, '.ai-sdlc', 'branch-protection-body.json');
|
|
324
|
+
// adapters.runCommand can't write files, so we use the underlying
|
|
325
|
+
// primitives directly here — branch protection is a one-shot operation
|
|
326
|
+
// and doesn't need to be hermetic in the same way as the file writes.
|
|
327
|
+
// Tests inject a stub that intercepts the runCommand call and never
|
|
328
|
+
// touches this path. See test for the contract.
|
|
329
|
+
try {
|
|
330
|
+
mkdirSync(dirname(tmpPath), { recursive: true });
|
|
331
|
+
writeFileSync(tmpPath, bodyJson, 'utf-8');
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
return {
|
|
335
|
+
applied: false,
|
|
336
|
+
bodyJson,
|
|
337
|
+
error: `failed to stage branch-protection body: ${err.message}`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const apply = adapters.runCommand('gh', [
|
|
341
|
+
'api',
|
|
342
|
+
'-X',
|
|
343
|
+
'PUT',
|
|
344
|
+
`repos/${slug}/branches/main/protection`,
|
|
345
|
+
'--input',
|
|
346
|
+
tmpPath,
|
|
347
|
+
]);
|
|
348
|
+
if (apply.exitCode !== 0) {
|
|
349
|
+
return {
|
|
350
|
+
applied: false,
|
|
351
|
+
bodyJson,
|
|
352
|
+
error: `gh api PUT failed: ${apply.stdout.trim() || 'unknown error'}`,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
adapters.log(` applied branch protection to ${slug}:main`);
|
|
356
|
+
return { applied: true, bodyJson };
|
|
357
|
+
}
|
|
358
|
+
// ── Next-steps summary ───────────────────────────────────────────────────
|
|
359
|
+
/**
|
|
360
|
+
* Print the structured "next steps" summary at the end of init. AC #5:
|
|
361
|
+
* the summary must include operator action items conditional on which
|
|
362
|
+
* features were chosen (e.g. `gh secret set` commands when attestation
|
|
363
|
+
* was opted in).
|
|
364
|
+
*
|
|
365
|
+
* Returns the rendered summary as a string in addition to logging it,
|
|
366
|
+
* so tests can assert on it without re-stringifying console output.
|
|
367
|
+
*/
|
|
368
|
+
export function renderNextSteps(selection, result, adapters) {
|
|
369
|
+
const lines = [];
|
|
370
|
+
lines.push('');
|
|
371
|
+
lines.push('━━━ Next steps ━━━');
|
|
372
|
+
lines.push('');
|
|
373
|
+
// Always present (baseline gate)
|
|
374
|
+
lines.push('1. Commit the scaffolded files:');
|
|
375
|
+
lines.push(' git add .ai-sdlc .github/workflows package.json');
|
|
376
|
+
lines.push(' git commit -m "chore: bootstrap AI-SDLC config"');
|
|
377
|
+
lines.push('');
|
|
378
|
+
let stepN = 2;
|
|
379
|
+
if (selection.dor) {
|
|
380
|
+
lines.push(`${stepN}. DoR (Definition-of-Ready) is in WARN-ONLY mode by default.`);
|
|
381
|
+
lines.push(' Tune .ai-sdlc/dor-config.yaml then flip evaluationMode: enforce');
|
|
382
|
+
lines.push(' after a soak window confirms the false-positive rate is low.');
|
|
383
|
+
lines.push('');
|
|
384
|
+
stepN++;
|
|
385
|
+
}
|
|
386
|
+
if (selection.attestation) {
|
|
387
|
+
lines.push(`${stepN}. Attestation infrastructure was scaffolded in AUDIT-ONLY mode.`);
|
|
388
|
+
lines.push(' a) Bootstrap your signing key: /ai-sdlc init-signing-key');
|
|
389
|
+
lines.push(' b) Open a PR adding the printed YAML block to');
|
|
390
|
+
lines.push(' .ai-sdlc/trusted-reviewers.yaml');
|
|
391
|
+
// AISDLC-152: removed the optional CI-side signer step (the AISDLC-87
|
|
392
|
+
// CI-attestor was retired in AISDLC-140 sub-4 alongside attestation
|
|
393
|
+
// becoming audit-only). New adopters no longer need to provision the
|
|
394
|
+
// AI_SDLC_CI_ATTESTOR_PRIVATE_KEY secret.
|
|
395
|
+
lines.push('');
|
|
396
|
+
stepN++;
|
|
397
|
+
}
|
|
398
|
+
if (selection.classifier) {
|
|
399
|
+
lines.push(`${stepN}. Review classifier config was scaffolded.`);
|
|
400
|
+
lines.push(' The classifier RUNTIME ships in AISDLC-141 (follow-up). Until');
|
|
401
|
+
lines.push(' that lands, .ai-sdlc/review-classifier.yaml is advisory only.');
|
|
402
|
+
lines.push('');
|
|
403
|
+
stepN++;
|
|
404
|
+
}
|
|
405
|
+
if (selection.branchProtection) {
|
|
406
|
+
if (result.branchProtection?.applied) {
|
|
407
|
+
lines.push(`${stepN}. Branch protection on \`main\` was updated.`);
|
|
408
|
+
lines.push(' Required checks: ai-sdlc/pr-ready, codecov/patch');
|
|
409
|
+
}
|
|
410
|
+
else if (result.branchProtection?.error) {
|
|
411
|
+
lines.push(`${stepN}. Branch protection was NOT applied:`);
|
|
412
|
+
lines.push(` ${result.branchProtection.error}`);
|
|
413
|
+
lines.push(' After resolving (gh auth login, repo permissions, etc.) re-run:');
|
|
414
|
+
lines.push(' ai-sdlc init --add branch-protection');
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
lines.push(`${stepN}. Branch protection (dry-run) — see JSON above; apply with:`);
|
|
418
|
+
lines.push(' ai-sdlc init --add branch-protection');
|
|
419
|
+
}
|
|
420
|
+
lines.push('');
|
|
421
|
+
stepN++;
|
|
422
|
+
}
|
|
423
|
+
lines.push(`${stepN}. Verify your configuration: ai-sdlc health`);
|
|
424
|
+
lines.push('');
|
|
425
|
+
lines.push('Adopter docs: https://github.com/ai-sdlc-framework/ai-sdlc/blob/main/docs/operations/init.md');
|
|
426
|
+
const out = lines.join('\n');
|
|
427
|
+
for (const line of lines)
|
|
428
|
+
adapters.log(line);
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
// ── CLAUDE.md recommendation pointer (AC #4) ─────────────────────────────
|
|
432
|
+
/**
|
|
433
|
+
* The pointer block we append to CLAUDE.md so a freshly-initialized repo's
|
|
434
|
+
* Claude Code sessions know where to find the AI-SDLC quality-gate docs.
|
|
435
|
+
* Idempotent — guarded by a sentinel so re-running init doesn't duplicate
|
|
436
|
+
* the block.
|
|
437
|
+
*/
|
|
438
|
+
export const CLAUDE_MD_POINTER = `
|
|
439
|
+
<!-- ai-sdlc:recommendation-pointer -->
|
|
440
|
+
## AI-SDLC quality gate
|
|
441
|
+
|
|
442
|
+
This repo is bootstrapped with the AI-SDLC framework. The single PR-ready
|
|
443
|
+
merge gate is \`ai-sdlc/pr-ready\` (see \`.github/workflows/ai-sdlc-gate.yml\`).
|
|
444
|
+
Run \`ai-sdlc health\` to verify your local config; see
|
|
445
|
+
\`docs/operations/init.md\` for the adopter guide.
|
|
446
|
+
<!-- end ai-sdlc:recommendation-pointer -->
|
|
447
|
+
`;
|
|
448
|
+
/** Sentinel marker used by the CLAUDE.md pointer for idempotency. */
|
|
449
|
+
export const CLAUDE_MD_SENTINEL = '<!-- ai-sdlc:recommendation-pointer -->';
|
|
450
|
+
/**
|
|
451
|
+
* Append the recommendation pointer to CLAUDE.md (or create the file if
|
|
452
|
+
* missing). Idempotent: if the sentinel is already present we no-op.
|
|
453
|
+
*/
|
|
454
|
+
export function ensureClaudeMdPointer(projectDir, adapters, dryRun) {
|
|
455
|
+
const path = join(projectDir, 'CLAUDE.md');
|
|
456
|
+
if (dryRun) {
|
|
457
|
+
adapters.log(' would update CLAUDE.md (recommendation pointer)');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (!adapters.exists(path)) {
|
|
461
|
+
adapters.writeFile(path, `# Project instructions\n${CLAUDE_MD_POINTER}`);
|
|
462
|
+
adapters.log(' created CLAUDE.md');
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const status = adapters.appendOnce(path, CLAUDE_MD_POINTER, CLAUDE_MD_SENTINEL);
|
|
466
|
+
if (status === 'appended') {
|
|
467
|
+
adapters.log(' updated CLAUDE.md (recommendation pointer)');
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
adapters.log(' skip CLAUDE.md (recommendation pointer already present)');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
//# sourceMappingURL=init-features.js.map
|