@ai-sdlc/orchestrator 0.9.0 → 0.13.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/adapters.d.ts +42 -3
- package/dist/adapters.js +133 -3
- package/dist/admission-composite.d.ts +134 -1
- package/dist/admission-composite.js +170 -6
- package/dist/admission-enrichment.d.ts +12 -3
- package/dist/admission-enrichment.js +71 -10
- package/dist/admission-hc.d.ts +20 -1
- package/dist/admission-hc.js +12 -1
- package/dist/admission-score.d.ts +44 -0
- package/dist/admission-score.js +11 -2
- package/dist/backlog-adapter.d.ts +45 -0
- package/dist/backlog-adapter.js +145 -1
- package/dist/calibration.d.ts +81 -0
- package/dist/calibration.js +76 -0
- 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 +507 -0
- package/dist/cli/commands/init-features.js +1100 -0
- package/dist/cli/commands/init-templates.d.ts +301 -0
- package/dist/cli/commands/init-templates.js +1341 -0
- package/dist/cli/commands/init.d.ts +67 -0
- package/dist/cli/commands/init.js +301 -2
- package/dist/cli/commands/run.js +9 -1
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.js +42 -8
- package/dist/compliance/composer.d.ts +79 -0
- package/dist/compliance/composer.js +258 -0
- package/dist/compliance/errors.d.ts +64 -0
- package/dist/compliance/errors.js +85 -0
- package/dist/compliance/loader.d.ts +52 -0
- package/dist/compliance/loader.js +124 -0
- package/dist/compliance/types.d.ts +184 -0
- package/dist/compliance/types.js +41 -0
- package/dist/compliance-clearance.d.ts +269 -0
- package/dist/compliance-clearance.js +269 -0
- package/dist/config.js +17 -0
- package/dist/cost-tracker.d.ts +22 -0
- package/dist/cost-tracker.js +41 -0
- package/dist/database/adapters/external.js +5 -1
- package/dist/design-authority.d.ts +13 -0
- package/dist/design-authority.js +13 -0
- package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
- package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
- package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
- package/dist/embedding/consumers/tessellation-drift.js +76 -0
- package/dist/embedding/cross-provider.d.ts +78 -0
- package/dist/embedding/cross-provider.js +75 -0
- package/dist/embedding/deprecation.d.ts +151 -0
- package/dist/embedding/deprecation.js +229 -0
- package/dist/embedding/errors.d.ts +90 -0
- package/dist/embedding/errors.js +150 -0
- package/dist/embedding/index.d.ts +29 -0
- package/dist/embedding/index.js +24 -0
- package/dist/embedding/pipeline-load.d.ts +146 -0
- package/dist/embedding/pipeline-load.js +178 -0
- package/dist/embedding/registry.d.ts +45 -0
- package/dist/embedding/registry.js +61 -0
- package/dist/embedding/stale-vector.d.ts +110 -0
- package/dist/embedding/stale-vector.js +92 -0
- package/dist/embedding/storage/index.d.ts +51 -0
- package/dist/embedding/storage/index.js +43 -0
- package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
- package/dist/embedding/storage/jsonl-backend.js +332 -0
- package/dist/embedding/storage/types.d.ts +135 -0
- package/dist/embedding/storage/types.js +13 -0
- package/dist/embedding/types.d.ts +180 -0
- package/dist/embedding/types.js +10 -0
- package/dist/execute.d.ts +29 -2
- package/dist/execute.js +179 -35
- package/dist/index.d.ts +14 -2
- package/dist/index.js +19 -2
- package/dist/journey/inheritance-validator.d.ts +396 -0
- package/dist/journey/inheritance-validator.js +370 -0
- package/dist/journey/state-id-drift-rule.d.ts +137 -0
- package/dist/journey/state-id-drift-rule.js +245 -0
- package/dist/journey-sa2-router.d.ts +395 -0
- package/dist/journey-sa2-router.js +308 -0
- 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/runners/review-agent.js +6 -2
- package/dist/runners/runner-registry.d.ts +36 -0
- package/dist/runners/runner-registry.js +90 -0
- package/dist/runtime/attestations.d.ts +663 -11
- package/dist/runtime/attestations.js +971 -24
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- 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/dist/sa-scoring/layer3-llm.js +6 -1
- package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
- package/dist/sa-scoring/revision-proposal-config.js +198 -0
- package/dist/sa-scoring/revision-proposal.d.ts +285 -0
- package/dist/sa-scoring/revision-proposal.js +417 -0
- package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
- package/dist/signal-ingestion/adapters/community-thread.js +55 -0
- package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
- package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
- package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
- package/dist/signal-ingestion/adapters/manual.js +112 -0
- package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
- package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
- package/dist/signal-ingestion/classifier.d.ts +205 -0
- package/dist/signal-ingestion/classifier.js +494 -0
- package/dist/signal-ingestion/clustering-types.d.ts +36 -0
- package/dist/signal-ingestion/clustering-types.js +14 -0
- package/dist/signal-ingestion/clustering.d.ts +200 -0
- package/dist/signal-ingestion/clustering.js +413 -0
- package/dist/signal-ingestion/config.d.ts +351 -0
- package/dist/signal-ingestion/config.js +587 -0
- package/dist/signal-ingestion/d1.d.ts +252 -0
- package/dist/signal-ingestion/d1.js +235 -0
- package/dist/signal-ingestion/errors.d.ts +73 -0
- package/dist/signal-ingestion/errors.js +108 -0
- package/dist/signal-ingestion/governance-events.d.ts +181 -0
- package/dist/signal-ingestion/governance-events.js +189 -0
- package/dist/signal-ingestion/index.d.ts +35 -0
- package/dist/signal-ingestion/index.js +53 -0
- package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
- package/dist/signal-ingestion/manual-share-metric.js +106 -0
- package/dist/signal-ingestion/registry.d.ts +40 -0
- package/dist/signal-ingestion/registry.js +137 -0
- package/dist/signal-ingestion/residency.d.ts +227 -0
- package/dist/signal-ingestion/residency.js +238 -0
- package/dist/signal-ingestion/significance.d.ts +554 -0
- package/dist/signal-ingestion/significance.js +555 -0
- package/dist/signal-ingestion/types.d.ts +191 -0
- package/dist/signal-ingestion/types.js +8 -0
- package/dist/substrate/drift-composition.d.ts +270 -0
- package/dist/substrate/drift-composition.js +306 -0
- package/dist/substrate/drift-tui-surface.d.ts +61 -0
- package/dist/substrate/drift-tui-surface.js +102 -0
- package/dist/substrate/identity-class.d.ts +176 -0
- package/dist/substrate/identity-class.js +201 -0
- package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
- package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
- package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
- package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
- package/dist/tessellation/rule-registry.d.ts +269 -0
- package/dist/tessellation/rule-registry.js +92 -0
- package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
- package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
- package/dist/tessellation-admission.d.ts +162 -0
- package/dist/tessellation-admission.js +146 -0
- package/dist/tessellation-drift.d.ts +246 -0
- package/dist/tessellation-drift.js +250 -0
- package/dist/validate-config.js +13 -0
- package/dist/validate-issue.js +2 -2
- package/dist/variant/cardinality-activation.d.ts +126 -0
- package/dist/variant/cardinality-activation.js +101 -0
- package/dist/variant/deprecation-lifecycle.d.ts +184 -0
- package/dist/variant/deprecation-lifecycle.js +208 -0
- package/dist/variant/drift-extension.d.ts +136 -0
- package/dist/variant/drift-extension.js +164 -0
- package/dist/variant/engineering-review.d.ts +185 -0
- package/dist/variant/engineering-review.js +142 -0
- package/dist/variant/index.d.ts +32 -0
- package/dist/variant/index.js +32 -0
- package/dist/variant/inheritance-validator.d.ts +165 -0
- package/dist/variant/inheritance-validator.js +139 -0
- package/dist/variant/internal-adopter/index.d.ts +11 -0
- package/dist/variant/internal-adopter/index.js +10 -0
- package/dist/variant/internal-adopter/products.d.ts +156 -0
- package/dist/variant/internal-adopter/products.js +366 -0
- package/dist/variant-admission.d.ts +316 -0
- package/dist/variant-admission.js +247 -0
- package/package.json +11 -8
|
@@ -0,0 +1,1100 @@
|
|
|
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
|
+
* 5. The compliance-posture wizard step (RFC-0022 §7 / AISDLC-324).
|
|
15
|
+
*
|
|
16
|
+
* Test surface: every public function takes a small options bag with
|
|
17
|
+
* injectable side-effect adapters (prompter, writeFile, runCommand) so the
|
|
18
|
+
* test suite can drive every wizard branch hermetically without spinning
|
|
19
|
+
* up a TTY or shelling out to `gh`. Production callers in `init.ts` pass
|
|
20
|
+
* the real adapters.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { join, dirname, basename } from 'node:path';
|
|
24
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
25
|
+
import { BASELINE_DERIVED_GATES } from '../../compliance/types.js';
|
|
26
|
+
import { ATTESTATION_TEMPLATES, BASELINE_WORKFLOW_TEMPLATES, CLASSIFIER_TEMPLATES, DOR_TEMPLATES, HUSKY_PREPUSH_SIGN_SNIPPET, SIGNAL_INGESTION_TEMPLATES, WORKFLOWS_TEMPLATES, } from './init-templates.js';
|
|
27
|
+
/**
|
|
28
|
+
* Canonical list of regulatory regimes presented in the compliance wizard.
|
|
29
|
+
* Mirrors the entries in spec/compliance/regime-mappings.yaml.
|
|
30
|
+
*/
|
|
31
|
+
export const COMPLIANCE_REGIME_CHOICES = [
|
|
32
|
+
{ value: 'SOC2-T2', label: 'SOC2 Type 2 (Service Organization Control)' },
|
|
33
|
+
{ value: 'HIPAA', label: 'HIPAA (Health Insurance Portability and Accountability Act)' },
|
|
34
|
+
{ value: 'PCI-DSS-L1', label: 'PCI-DSS Level 1 (Payment Card Industry Data Security Standard)' },
|
|
35
|
+
{ value: 'GDPR', label: 'GDPR (General Data Protection Regulation; EU)' },
|
|
36
|
+
{ value: 'FedRAMP-Moderate', label: 'FedRAMP Moderate (US federal)' },
|
|
37
|
+
{ value: 'ISO-27001:2022', label: 'ISO 27001:2022' },
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Inline regime → DerivedGates mapping for the init wizard.
|
|
41
|
+
* Mirrors spec/compliance/regime-mappings.yaml so the wizard works in
|
|
42
|
+
* installed-package contexts (where the YAML file is not bundled).
|
|
43
|
+
*
|
|
44
|
+
* NOTE: This data is intentionally duplicated from regime-mappings.yaml to
|
|
45
|
+
* avoid a runtime file-read dependency in the init wizard. When regime-mappings.yaml
|
|
46
|
+
* is updated, this table MUST be updated in sync. The RFC-0022 §13 Q7 PR-template
|
|
47
|
+
* compliance-impact checkbox enforces this via reviewer check.
|
|
48
|
+
*/
|
|
49
|
+
const INIT_WIZARD_REGIME_GATES = {
|
|
50
|
+
'SOC2-T2': {
|
|
51
|
+
databaseBranchPool: 'per-shard',
|
|
52
|
+
secretScanStrictness: 'strict',
|
|
53
|
+
attestationRequired: true,
|
|
54
|
+
auditRetentionDays: 2555,
|
|
55
|
+
reviewerAuthorityModel: 'allowlist+role',
|
|
56
|
+
},
|
|
57
|
+
HIPAA: {
|
|
58
|
+
databaseBranchPool: 'per-shard',
|
|
59
|
+
secretScanStrictness: 'strict',
|
|
60
|
+
attestationRequired: true,
|
|
61
|
+
auditRetentionDays: 2190,
|
|
62
|
+
reviewerAuthorityModel: 'allowlist+role',
|
|
63
|
+
},
|
|
64
|
+
'PCI-DSS-L1': {
|
|
65
|
+
databaseBranchPool: 'per-shard',
|
|
66
|
+
secretScanStrictness: 'strict',
|
|
67
|
+
attestationRequired: true,
|
|
68
|
+
auditRetentionDays: 365,
|
|
69
|
+
reviewerAuthorityModel: 'allowlist+role',
|
|
70
|
+
},
|
|
71
|
+
GDPR: {
|
|
72
|
+
databaseBranchPool: 'per-shard',
|
|
73
|
+
secretScanStrictness: 'standard',
|
|
74
|
+
attestationRequired: true,
|
|
75
|
+
auditRetentionDays: 365,
|
|
76
|
+
reviewerAuthorityModel: 'allowlist',
|
|
77
|
+
},
|
|
78
|
+
'FedRAMP-Moderate': {
|
|
79
|
+
databaseBranchPool: 'per-shard',
|
|
80
|
+
secretScanStrictness: 'strict',
|
|
81
|
+
attestationRequired: true,
|
|
82
|
+
auditRetentionDays: 1095,
|
|
83
|
+
reviewerAuthorityModel: 'allowlist+role',
|
|
84
|
+
},
|
|
85
|
+
'ISO-27001:2022': {
|
|
86
|
+
databaseBranchPool: 'per-shard',
|
|
87
|
+
secretScanStrictness: 'strict',
|
|
88
|
+
attestationRequired: true,
|
|
89
|
+
auditRetentionDays: 365,
|
|
90
|
+
reviewerAuthorityModel: 'allowlist+role',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Ordinal scales for tightest-wins composition.
|
|
95
|
+
*/
|
|
96
|
+
const SECRET_SCAN_ORDINAL = {
|
|
97
|
+
minimal: 0,
|
|
98
|
+
standard: 1,
|
|
99
|
+
strict: 2,
|
|
100
|
+
};
|
|
101
|
+
const REVIEWER_AUTHORITY_ORDINAL = {
|
|
102
|
+
open: 0,
|
|
103
|
+
allowlist: 1,
|
|
104
|
+
'allowlist+role': 2,
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Compute DerivedGates from a list of regime IDs using tightest-wins semantics.
|
|
108
|
+
* Unknown regime IDs are skipped (the wizard's informational display degrades
|
|
109
|
+
* gracefully rather than throwing).
|
|
110
|
+
*
|
|
111
|
+
* This is a lightweight inline computation used exclusively by the init wizard.
|
|
112
|
+
* The canonical composer (orchestrator/src/compliance/composer.ts) is used for
|
|
113
|
+
* all post-init runtime gate resolution.
|
|
114
|
+
*/
|
|
115
|
+
export function computeInitWizardDerivedGates(regimes) {
|
|
116
|
+
if (regimes.length === 0)
|
|
117
|
+
return { ...BASELINE_DERIVED_GATES };
|
|
118
|
+
let accumulated = { ...BASELINE_DERIVED_GATES };
|
|
119
|
+
for (const regimeId of regimes) {
|
|
120
|
+
const entry = INIT_WIZARD_REGIME_GATES[regimeId];
|
|
121
|
+
if (!entry)
|
|
122
|
+
continue; // Unknown regime — skip gracefully
|
|
123
|
+
// databaseBranchPool: per-shard beats shared-with-rls
|
|
124
|
+
if (entry.databaseBranchPool === 'per-shard') {
|
|
125
|
+
accumulated = { ...accumulated, databaseBranchPool: 'per-shard' };
|
|
126
|
+
}
|
|
127
|
+
// secretScanStrictness: ordinal max
|
|
128
|
+
if (SECRET_SCAN_ORDINAL[entry.secretScanStrictness] >
|
|
129
|
+
SECRET_SCAN_ORDINAL[accumulated.secretScanStrictness]) {
|
|
130
|
+
accumulated = { ...accumulated, secretScanStrictness: entry.secretScanStrictness };
|
|
131
|
+
}
|
|
132
|
+
// attestationRequired: boolean OR
|
|
133
|
+
if (entry.attestationRequired) {
|
|
134
|
+
accumulated = { ...accumulated, attestationRequired: true };
|
|
135
|
+
}
|
|
136
|
+
// auditRetentionDays: max
|
|
137
|
+
if (entry.auditRetentionDays > accumulated.auditRetentionDays) {
|
|
138
|
+
accumulated = { ...accumulated, auditRetentionDays: entry.auditRetentionDays };
|
|
139
|
+
}
|
|
140
|
+
// reviewerAuthorityModel: ordinal max
|
|
141
|
+
if (REVIEWER_AUTHORITY_ORDINAL[entry.reviewerAuthorityModel] >
|
|
142
|
+
REVIEWER_AUTHORITY_ORDINAL[accumulated.reviewerAuthorityModel]) {
|
|
143
|
+
accumulated = { ...accumulated, reviewerAuthorityModel: entry.reviewerAuthorityModel };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return accumulated;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Apply the RFC-0009 §8.7 / OQ-11 trigger checklist on top of regime-derived
|
|
150
|
+
* `DerivedGates`. Upgrades `databaseBranchPool` from `shared-with-rls` to
|
|
151
|
+
* `per-shard` when ANY of the three triggers fires:
|
|
152
|
+
*
|
|
153
|
+
* - Regulatory: already reflected in `inputGates.databaseBranchPool === 'per-shard'`
|
|
154
|
+
* (set by `computeInitWizardDerivedGates` when a per-shard-forcing regime was
|
|
155
|
+
* declared). When this is the case, 'regulatory' is recorded in `triggers`.
|
|
156
|
+
* - Customer contract: `answers.customerContract === true`.
|
|
157
|
+
* - Operator security review: `answers.operatorSecurityReview === true`.
|
|
158
|
+
*
|
|
159
|
+
* The function is monotonic — once `databaseBranchPool` is `per-shard`, it stays
|
|
160
|
+
* `per-shard`. Other DerivedGates fields are passed through unchanged.
|
|
161
|
+
*
|
|
162
|
+
* Pure function — no I/O, no side-effects. The wizard wires it into
|
|
163
|
+
* `runComplianceStep`; tests pin behavior directly.
|
|
164
|
+
*/
|
|
165
|
+
export function applyOq11TriggerChecklistUpgrade(inputGates, answers) {
|
|
166
|
+
const triggers = [];
|
|
167
|
+
// Trigger 1 (regulatory) — surfaces only when the gate was already
|
|
168
|
+
// upgraded upstream by a per-shard-forcing regime declaration.
|
|
169
|
+
if (inputGates.databaseBranchPool === 'per-shard') {
|
|
170
|
+
triggers.push('regulatory');
|
|
171
|
+
}
|
|
172
|
+
if (answers.customerContract) {
|
|
173
|
+
triggers.push('customer-contract');
|
|
174
|
+
}
|
|
175
|
+
if (answers.operatorSecurityReview) {
|
|
176
|
+
triggers.push('operator-security-review');
|
|
177
|
+
}
|
|
178
|
+
const upgraded = triggers.length > 0;
|
|
179
|
+
return {
|
|
180
|
+
derivedGates: upgraded ? { ...inputGates, databaseBranchPool: 'per-shard' } : { ...inputGates },
|
|
181
|
+
triggers,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Human-readable line for each trigger kind, shown in the wizard summary so
|
|
186
|
+
* the operator can see WHY the framework selected `per-shard`.
|
|
187
|
+
*/
|
|
188
|
+
export function describeOq11Trigger(kind) {
|
|
189
|
+
switch (kind) {
|
|
190
|
+
case 'regulatory':
|
|
191
|
+
return 'regulatory regime declared in .ai-sdlc/compliance.yaml (RFC-0022 derivedGates)';
|
|
192
|
+
case 'customer-contract':
|
|
193
|
+
return 'customer contract requires tenant physical isolation (operator-declared)';
|
|
194
|
+
case 'operator-security-review':
|
|
195
|
+
return 'operator security review identified a risk RLS cannot mitigate (operator-declared)';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Build the .ai-sdlc/compliance.yaml content for a given compliance declaration.
|
|
200
|
+
*
|
|
201
|
+
* The written file contains the declared `spec.regimes` with attestation metadata.
|
|
202
|
+
* The computed `derivedGates` are added as YAML comments (read-only reference)
|
|
203
|
+
* so the loader continues to compute them from regimes (not from spec.derivedGates,
|
|
204
|
+
* which is the operator-override field requiring _notes for each entry).
|
|
205
|
+
*
|
|
206
|
+
* Pure function — no filesystem side-effects, fully testable.
|
|
207
|
+
*/
|
|
208
|
+
export function buildComplianceYaml(opts) {
|
|
209
|
+
const { projectName, regimes, attestedBy, attestedAt, attestedNotes, derivedGates } = opts;
|
|
210
|
+
// AISDLC-324 review fix: quote attestedBy/id/attestedAt so an operator
|
|
211
|
+
// git config user.email containing ": " or other YAML-significant chars
|
|
212
|
+
// can't break the YAML structure. attestedNotes already quoted+escaped.
|
|
213
|
+
const quotedAttestedBy = `"${attestedBy.replace(/"/g, '\\"')}"`;
|
|
214
|
+
const regimeItems = regimes
|
|
215
|
+
.map((id) => {
|
|
216
|
+
// id is from hardcoded COMPLIANCE_REGIME_CHOICES (validated upstream)
|
|
217
|
+
// so it's safe to emit unquoted; YAML-significant chars never reach here.
|
|
218
|
+
const lines = [
|
|
219
|
+
` - id: ${id}`,
|
|
220
|
+
` attestedBy: ${quotedAttestedBy}`,
|
|
221
|
+
` attestedAt: "${attestedAt}"`,
|
|
222
|
+
];
|
|
223
|
+
if (attestedNotes) {
|
|
224
|
+
lines.push(` attestedNotes: "${attestedNotes.replace(/"/g, '\\"')}"`);
|
|
225
|
+
}
|
|
226
|
+
return lines.join('\n');
|
|
227
|
+
})
|
|
228
|
+
.join('\n');
|
|
229
|
+
const derivedGatesComment = [
|
|
230
|
+
'# --- Derived gates (computed from declared regimes; read-only) ---',
|
|
231
|
+
`# databaseBranchPool: ${derivedGates.databaseBranchPool}`,
|
|
232
|
+
`# secretScanStrictness: ${derivedGates.secretScanStrictness}`,
|
|
233
|
+
`# attestationRequired: ${derivedGates.attestationRequired}`,
|
|
234
|
+
`# auditRetentionDays: ${derivedGates.auditRetentionDays}`,
|
|
235
|
+
`# reviewerAuthorityModel: ${derivedGates.reviewerAuthorityModel}`,
|
|
236
|
+
'#',
|
|
237
|
+
'# To override a gate, add spec.derivedGates.<field> with a sibling _notes entry.',
|
|
238
|
+
'# See docs/operations/compliance-posture.md for override patterns.',
|
|
239
|
+
].join('\n');
|
|
240
|
+
// AISDLC-324 review fix: avoid duplicate `regimes:` key when empty.
|
|
241
|
+
// Inline `[]` for empty case; emit block under `regimes:` for non-empty.
|
|
242
|
+
const regimesSection = regimes.length === 0 ? ` regimes: []` : ` regimes:\n${regimeItems}`;
|
|
243
|
+
return [
|
|
244
|
+
`apiVersion: ai-sdlc.io/v1alpha1`,
|
|
245
|
+
`kind: CompliancePosture`,
|
|
246
|
+
`metadata:`,
|
|
247
|
+
` name: "${projectName.replace(/"/g, '\\"')}"`,
|
|
248
|
+
`spec:`,
|
|
249
|
+
regimesSection,
|
|
250
|
+
` auditExports: []`,
|
|
251
|
+
'',
|
|
252
|
+
derivedGatesComment,
|
|
253
|
+
'',
|
|
254
|
+
].join('\n');
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Format the derived gates for console display (the "✓ Wrote ... with derived gates:" block).
|
|
258
|
+
*/
|
|
259
|
+
export function formatDerivedGatesDisplay(derivedGates) {
|
|
260
|
+
return [
|
|
261
|
+
` databaseBranchPool: ${derivedGates.databaseBranchPool}`,
|
|
262
|
+
` secretScanStrictness: ${derivedGates.secretScanStrictness}`,
|
|
263
|
+
` attestationRequired: ${derivedGates.attestationRequired}`,
|
|
264
|
+
` auditRetentionDays: ${derivedGates.auditRetentionDays}`,
|
|
265
|
+
` reviewerAuthorityModel: ${derivedGates.reviewerAuthorityModel}`,
|
|
266
|
+
].join('\n');
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* DB-pool rationale displayed when a compliance regime forces `per-shard`.
|
|
270
|
+
* Returns null if the DB-pool is `shared-with-rls` (no rationale needed).
|
|
271
|
+
*/
|
|
272
|
+
export function getDbPoolRationale(regimes, derivedGates) {
|
|
273
|
+
if (derivedGates.databaseBranchPool !== 'per-shard')
|
|
274
|
+
return null;
|
|
275
|
+
if (regimes.length === 0)
|
|
276
|
+
return null;
|
|
277
|
+
const forcingRegimes = regimes.filter((id) => INIT_WIZARD_REGIME_GATES[id]?.databaseBranchPool === 'per-shard');
|
|
278
|
+
if (forcingRegimes.length === 0)
|
|
279
|
+
return null;
|
|
280
|
+
return ` (${forcingRegimes.join(', ')} declared at .ai-sdlc/compliance.yaml requires per-shard isolation)`;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Run the compliance posture wizard step (RFC-0022 §7 / AISDLC-324).
|
|
284
|
+
*
|
|
285
|
+
* Inserted into the init wizard BEFORE the gate-config feature prompts.
|
|
286
|
+
* Always runs — even for unregulated projects (declaring "(none)" is the
|
|
287
|
+
* explicit choice; the resulting compliance.yaml carries that decision).
|
|
288
|
+
*
|
|
289
|
+
* Flow:
|
|
290
|
+
* 1. Auto-detect git config user.email for attestedBy default.
|
|
291
|
+
* 2. If --yes or non-TTY: use baseline (no regimes), auto-fill attestedBy.
|
|
292
|
+
* 3. Otherwise: multi-select regimes, text-input attestedBy + notes.
|
|
293
|
+
* 4. Compute derivedGates via inline tightest-wins composition.
|
|
294
|
+
* 5. Write .ai-sdlc/compliance.yaml (skips if file already exists and not
|
|
295
|
+
* in --add compliance mode).
|
|
296
|
+
* 6. Log derived gates + DB-pool rationale.
|
|
297
|
+
*/
|
|
298
|
+
export async function runComplianceStep(projectDir, flags, adapters) {
|
|
299
|
+
const compliancePath = join(projectDir, '.ai-sdlc', 'compliance.yaml');
|
|
300
|
+
// ── Auto-detect git email for attestedBy default ──────────────────────
|
|
301
|
+
let gitEmail = '';
|
|
302
|
+
try {
|
|
303
|
+
const result = adapters.runCommand('git', ['config', 'user.email']);
|
|
304
|
+
if (result.exitCode === 0) {
|
|
305
|
+
gitEmail = result.stdout.trim();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Ignore — git config may not be set; attestedBy will be empty default
|
|
310
|
+
}
|
|
311
|
+
// Resolve project name from git remote or directory basename.
|
|
312
|
+
const projectName = (() => {
|
|
313
|
+
try {
|
|
314
|
+
const r = adapters.runCommand('git', ['remote', 'get-url', 'origin']);
|
|
315
|
+
if (r.exitCode === 0) {
|
|
316
|
+
// Extract repo name from remote URL (ssh: git@github.com:org/repo.git, https: .../org/repo.git)
|
|
317
|
+
const m = r.stdout.trim().match(/\/([^/]+?)(\.git)?$/);
|
|
318
|
+
if (m)
|
|
319
|
+
return m[1];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Ignore
|
|
324
|
+
}
|
|
325
|
+
return basename(projectDir);
|
|
326
|
+
})();
|
|
327
|
+
// ── --yes / non-TTY path: baseline (no regimes, no triggers) ──────────
|
|
328
|
+
if (flags.yes || !process.stdin.isTTY) {
|
|
329
|
+
const derivedGates = computeInitWizardDerivedGates([]);
|
|
330
|
+
const attestedAt = new Date().toISOString();
|
|
331
|
+
const written = writeComplianceYaml({
|
|
332
|
+
projectDir,
|
|
333
|
+
compliancePath,
|
|
334
|
+
projectName,
|
|
335
|
+
regimes: [],
|
|
336
|
+
attestedBy: gitEmail,
|
|
337
|
+
attestedAt,
|
|
338
|
+
attestedNotes: undefined,
|
|
339
|
+
derivedGates,
|
|
340
|
+
flags,
|
|
341
|
+
adapters,
|
|
342
|
+
});
|
|
343
|
+
adapters.log('');
|
|
344
|
+
adapters.log(` Compliance posture: no regimes declared (unregulated baseline)`);
|
|
345
|
+
adapters.log(` derivedGates: ${JSON.stringify(derivedGates)}`);
|
|
346
|
+
return {
|
|
347
|
+
regimes: [],
|
|
348
|
+
attestedBy: gitEmail,
|
|
349
|
+
attestedAt,
|
|
350
|
+
attestedNotes: undefined,
|
|
351
|
+
derivedGates,
|
|
352
|
+
oq11Triggers: [],
|
|
353
|
+
yamlPath: compliancePath,
|
|
354
|
+
written,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// ── Interactive path ──────────────────────────────────────────────────
|
|
358
|
+
adapters.log('> Compliance posture');
|
|
359
|
+
adapters.log('');
|
|
360
|
+
const selectedRegimes = await adapters.multiSelect('Which regulatory regimes apply to this project? (space to toggle, enter to confirm)', COMPLIANCE_REGIME_CHOICES);
|
|
361
|
+
const attestedBy = await adapters.textInput('Who is attesting these regimes apply?', gitEmail || undefined);
|
|
362
|
+
const attestedNotesRaw = await adapters.textInput('Notes on the attestation (optional, audit-visible):', undefined);
|
|
363
|
+
const attestedNotes = attestedNotesRaw.trim() || undefined;
|
|
364
|
+
const attestedAt = new Date().toISOString();
|
|
365
|
+
const regimeDerivedGates = computeInitWizardDerivedGates(selectedRegimes);
|
|
366
|
+
// ── RFC-0009 §8.7 / OQ-11 trigger checklist (AISDLC-319) ──────────────
|
|
367
|
+
// Trigger 1 (regulatory) is already reflected in `regimeDerivedGates`;
|
|
368
|
+
// collect triggers 2 + 3 from the operator. The framework cannot auto-detect
|
|
369
|
+
// these (per RFC-0009 §8.7) — operator declares.
|
|
370
|
+
adapters.log('');
|
|
371
|
+
adapters.log('> DatabaseBranchPool trigger checklist (RFC-0009 §8.7)');
|
|
372
|
+
adapters.log(' Default is shared pool with row-level-security isolation. Answer Yes only');
|
|
373
|
+
adapters.log(' if the trigger actually applies — per-soul pools add operational complexity.');
|
|
374
|
+
const customerContract = await adapters.prompt('Does any customer contract require tenant physical isolation (independent of regulatory baseline)?', false);
|
|
375
|
+
const operatorSecurityReview = await adapters.prompt('Has operator security review identified a risk that RLS cannot mitigate?', false);
|
|
376
|
+
const triggerResult = applyOq11TriggerChecklistUpgrade(regimeDerivedGates, {
|
|
377
|
+
customerContract,
|
|
378
|
+
operatorSecurityReview,
|
|
379
|
+
});
|
|
380
|
+
const derivedGates = triggerResult.derivedGates;
|
|
381
|
+
const oq11Triggers = triggerResult.triggers;
|
|
382
|
+
const written = writeComplianceYaml({
|
|
383
|
+
projectDir,
|
|
384
|
+
compliancePath,
|
|
385
|
+
projectName,
|
|
386
|
+
regimes: selectedRegimes,
|
|
387
|
+
attestedBy,
|
|
388
|
+
attestedAt,
|
|
389
|
+
attestedNotes,
|
|
390
|
+
derivedGates,
|
|
391
|
+
flags,
|
|
392
|
+
adapters,
|
|
393
|
+
});
|
|
394
|
+
adapters.log('');
|
|
395
|
+
if (written) {
|
|
396
|
+
adapters.log(`✓ Wrote .ai-sdlc/compliance.yaml with derived gates:`);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
adapters.log(` .ai-sdlc/compliance.yaml already exists — skipped (run --add compliance to update)`);
|
|
400
|
+
adapters.log(` Derived gates (from existing compliance.yaml, if unchanged):`);
|
|
401
|
+
}
|
|
402
|
+
adapters.log(formatDerivedGatesDisplay(derivedGates));
|
|
403
|
+
// DB-pool rationale: surface BOTH the regime-derived rationale (AC #6) and
|
|
404
|
+
// the OQ-11 trigger checklist rationale (AISDLC-319 AC #2/#4).
|
|
405
|
+
const dbRationale = getDbPoolRationale(selectedRegimes, derivedGates);
|
|
406
|
+
if (dbRationale) {
|
|
407
|
+
adapters.log('');
|
|
408
|
+
adapters.log(` DatabaseBranchPool pre-selection: per-shard`);
|
|
409
|
+
adapters.log(dbRationale);
|
|
410
|
+
}
|
|
411
|
+
else if (oq11Triggers.length > 0) {
|
|
412
|
+
// Triggers 2 / 3 fired but no regulatory trigger — explain the upgrade.
|
|
413
|
+
adapters.log('');
|
|
414
|
+
adapters.log(` DatabaseBranchPool pre-selection: per-shard`);
|
|
415
|
+
adapters.log(` (RFC-0009 §8.7 trigger fired:`);
|
|
416
|
+
for (const t of oq11Triggers) {
|
|
417
|
+
if (t === 'regulatory')
|
|
418
|
+
continue; // already covered by dbRationale path
|
|
419
|
+
adapters.log(` - ${describeOq11Trigger(t)}`);
|
|
420
|
+
}
|
|
421
|
+
adapters.log(` )`);
|
|
422
|
+
}
|
|
423
|
+
adapters.log('');
|
|
424
|
+
adapters.log(` Review with: cat .ai-sdlc/compliance.yaml`);
|
|
425
|
+
adapters.log(` Override any field with the attestedNotes pattern (see docs/operations/compliance-posture.md).`);
|
|
426
|
+
return {
|
|
427
|
+
regimes: selectedRegimes,
|
|
428
|
+
attestedBy,
|
|
429
|
+
attestedAt,
|
|
430
|
+
attestedNotes,
|
|
431
|
+
derivedGates,
|
|
432
|
+
oq11Triggers,
|
|
433
|
+
yamlPath: compliancePath,
|
|
434
|
+
written,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/** Helper: write .ai-sdlc/compliance.yaml (or skip if exists). Returns true if written. */
|
|
438
|
+
function writeComplianceYaml(opts) {
|
|
439
|
+
const { compliancePath, flags, adapters } = opts;
|
|
440
|
+
if (flags.dryRun) {
|
|
441
|
+
adapters.log(` would write .ai-sdlc/compliance.yaml`);
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (adapters.exists(compliancePath)) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const yamlContent = buildComplianceYaml({
|
|
448
|
+
projectName: opts.projectName,
|
|
449
|
+
regimes: opts.regimes,
|
|
450
|
+
attestedBy: opts.attestedBy,
|
|
451
|
+
attestedAt: opts.attestedAt,
|
|
452
|
+
attestedNotes: opts.attestedNotes,
|
|
453
|
+
derivedGates: opts.derivedGates,
|
|
454
|
+
});
|
|
455
|
+
adapters.mkdirp(join(opts.projectDir, '.ai-sdlc'));
|
|
456
|
+
adapters.writeFile(compliancePath, yamlContent);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
/** All feature flags off — used as the initial state before flags + prompts. */
|
|
460
|
+
export const NO_FEATURES = {
|
|
461
|
+
dor: false,
|
|
462
|
+
attestation: false,
|
|
463
|
+
classifier: false,
|
|
464
|
+
branchProtection: false,
|
|
465
|
+
workflows: false,
|
|
466
|
+
signalIngestion: false,
|
|
467
|
+
};
|
|
468
|
+
/**
|
|
469
|
+
* All features on — the answer used by `--yes` (accept all defaults).
|
|
470
|
+
*
|
|
471
|
+
* Note: `signalIngestion` is deliberately FALSE in this set even though
|
|
472
|
+
* `--yes` accepts all *defaults*. RFC-0030's pipeline is gated by the
|
|
473
|
+
* `AI_SDLC_SIGNAL_INGESTION` env flag during its soak window, and the
|
|
474
|
+
* shipped default for the flag is OFF. Scaffolding the config stub on a
|
|
475
|
+
* fresh adopter who hasn't opted in would be noise; the file is only
|
|
476
|
+
* meaningful when the operator has explicit interest. Adopters opt in
|
|
477
|
+
* via `--with-signal-ingestion` or `--add signal-ingestion`.
|
|
478
|
+
*/
|
|
479
|
+
export const ALL_FEATURES = {
|
|
480
|
+
dor: true,
|
|
481
|
+
attestation: true,
|
|
482
|
+
classifier: true,
|
|
483
|
+
branchProtection: true,
|
|
484
|
+
workflows: true,
|
|
485
|
+
signalIngestion: false,
|
|
486
|
+
};
|
|
487
|
+
/**
|
|
488
|
+
* Resolve the AI-SDLC install target directory for the current invocation
|
|
489
|
+
* (AISDLC-262).
|
|
490
|
+
*
|
|
491
|
+
* ## Default behavior (no `--workspace` flag)
|
|
492
|
+
*
|
|
493
|
+
* 1. Shell out to `git rev-parse --show-toplevel` to find the repo root.
|
|
494
|
+
* - If the cwd is not inside a git repo, install at cwd (plain-dir
|
|
495
|
+
* fallback — same as the pre-AISDLC-262 behavior).
|
|
496
|
+
* 2. If `<git-root>/.ai-sdlc/` **already exists**, refuse with a clear
|
|
497
|
+
* "already installed at <root>; pass --workspace <name>" message so
|
|
498
|
+
* the operator knows exactly what to do next.
|
|
499
|
+
* 3. Otherwise install at the git root.
|
|
500
|
+
*
|
|
501
|
+
* ## `--workspace <name>` mode
|
|
502
|
+
*
|
|
503
|
+
* The operator explicitly wants a per-workspace install at
|
|
504
|
+
* `packages/<name>/.ai-sdlc/` (or `<name>/.ai-sdlc/` if `packages/` does
|
|
505
|
+
* not exist under the git root). No nesting check is performed — the
|
|
506
|
+
* operator has opted in.
|
|
507
|
+
*
|
|
508
|
+
* ## Dry-run output
|
|
509
|
+
*
|
|
510
|
+
* The resolved `installDir` is logged by the caller on the FIRST output
|
|
511
|
+
* line so adopters can sanity-check the target before any files are written.
|
|
512
|
+
*/
|
|
513
|
+
export function resolveInstallTarget(opts = {}) {
|
|
514
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
515
|
+
const configDirName = opts.configDirName ?? '.ai-sdlc';
|
|
516
|
+
const exists = opts.exists ?? existsSync;
|
|
517
|
+
// Resolve git root (or fall back to cwd when not in a git repo).
|
|
518
|
+
let gitRoot;
|
|
519
|
+
try {
|
|
520
|
+
const getToplevel = opts.gitShowToplevel ??
|
|
521
|
+
((dir) => execSync(`git rev-parse --show-toplevel`, {
|
|
522
|
+
cwd: dir,
|
|
523
|
+
encoding: 'utf-8',
|
|
524
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
525
|
+
}).trim());
|
|
526
|
+
gitRoot = getToplevel(cwd);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
// Not inside a git repo — install at cwd (plain-dir fallback).
|
|
530
|
+
gitRoot = cwd;
|
|
531
|
+
}
|
|
532
|
+
if (opts.workspace) {
|
|
533
|
+
// Per-workspace install: `<git-root>/packages/<name>` if packages/ exists,
|
|
534
|
+
// otherwise `<git-root>/<name>`.
|
|
535
|
+
const packagesDir = join(gitRoot, 'packages');
|
|
536
|
+
const workspaceBase = exists(packagesDir) ? packagesDir : gitRoot;
|
|
537
|
+
const installDir = join(workspaceBase, opts.workspace);
|
|
538
|
+
return { installDir, resolved: installDir !== cwd };
|
|
539
|
+
}
|
|
540
|
+
// Default: install at git root. If `.ai-sdlc/` already exists there,
|
|
541
|
+
// refuse with a helpful message — UNLESS `skipExistingCheck` is set
|
|
542
|
+
// (used by `--add <feature>`, which intentionally extends an existing
|
|
543
|
+
// install and thus expects the directory to already be present).
|
|
544
|
+
const rootConfigDir = join(gitRoot, configDirName);
|
|
545
|
+
if (exists(rootConfigDir) && !opts.skipExistingCheck) {
|
|
546
|
+
return {
|
|
547
|
+
installDir: gitRoot,
|
|
548
|
+
resolved: gitRoot !== cwd,
|
|
549
|
+
error: `AI-SDLC is already installed at ${gitRoot}; ` +
|
|
550
|
+
`pass --workspace <name> to add a child install at packages/<name>/.ai-sdlc/`,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return { installDir: gitRoot, resolved: gitRoot !== cwd };
|
|
554
|
+
}
|
|
555
|
+
// ── Production adapter factory ───────────────────────────────────────────
|
|
556
|
+
/**
|
|
557
|
+
* Build the production adapter bag. Pulled into a factory so tests can
|
|
558
|
+
* compose a partial override bag (e.g. only override `prompt`) and let
|
|
559
|
+
* the rest fall through to real disk writes.
|
|
560
|
+
*
|
|
561
|
+
* The `prompt` adapter is a lazy import of `@inquirer/prompts.confirm`
|
|
562
|
+
* so that:
|
|
563
|
+
* 1. Tests don't pay the import cost when they inject their own stub.
|
|
564
|
+
* 2. `--yes` runs (which never call `prompt`) don't pay it either.
|
|
565
|
+
* 3. The orchestrator's runtime `dist/` is smaller for the common case.
|
|
566
|
+
*/
|
|
567
|
+
export function buildProductionAdapters() {
|
|
568
|
+
return {
|
|
569
|
+
prompt: async (question, defaultYes) => {
|
|
570
|
+
// Lazy import — see docblock above for why.
|
|
571
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
572
|
+
return confirm({ message: question, default: defaultYes });
|
|
573
|
+
},
|
|
574
|
+
multiSelect: async (question, choices) => {
|
|
575
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
576
|
+
return checkbox({
|
|
577
|
+
message: question,
|
|
578
|
+
choices: choices.map((c) => ({ name: c.label, value: c.value })),
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
textInput: async (question, defaultValue) => {
|
|
582
|
+
const { input } = await import('@inquirer/prompts');
|
|
583
|
+
return input({ message: question, default: defaultValue });
|
|
584
|
+
},
|
|
585
|
+
writeFile: (path, contents) => writeFileSync(path, contents, 'utf-8'),
|
|
586
|
+
appendOnce: (path, contents, sentinel) => {
|
|
587
|
+
// Make sure the parent dir exists (appendFileSync errors with ENOENT
|
|
588
|
+
// otherwise; we may be writing into a freshly-created `.husky/`).
|
|
589
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
590
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf-8') : '';
|
|
591
|
+
if (existing.includes(sentinel))
|
|
592
|
+
return 'skipped';
|
|
593
|
+
const sep = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
594
|
+
writeFileSync(path, existing + sep + contents, 'utf-8');
|
|
595
|
+
return 'appended';
|
|
596
|
+
},
|
|
597
|
+
mkdirp: (path) => mkdirSync(path, { recursive: true }),
|
|
598
|
+
exists: (path) => existsSync(path),
|
|
599
|
+
runCommand: (cmd, args) => {
|
|
600
|
+
try {
|
|
601
|
+
// Use `execFileSync` (no shell) so args are passed as a true
|
|
602
|
+
// argv array — never word-split or shell-interpreted. The prior
|
|
603
|
+
// `execSync(\`${cmd} ${args.join(' ')}\`)` form ran the command
|
|
604
|
+
// through `/bin/sh -c` and silently broke whenever any argument
|
|
605
|
+
// contained whitespace (e.g. macOS users with `~/Documents/My
|
|
606
|
+
// Project/` in their projectDir, where the `--input <tmpPath>`
|
|
607
|
+
// arg to `gh api` would word-split and `gh` would see two
|
|
608
|
+
// unrelated tokens — branch protection would silently fail or
|
|
609
|
+
// apply wrong content). Switching to `execFileSync` eliminates
|
|
610
|
+
// both word-splitting AND any shell-injection surface in one
|
|
611
|
+
// change. Stderr stays muted so users still get the clean
|
|
612
|
+
// single-line error rendered by `applyBranchProtection`.
|
|
613
|
+
const stdout = execFileSync(cmd, args, {
|
|
614
|
+
encoding: 'utf-8',
|
|
615
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
616
|
+
});
|
|
617
|
+
return { stdout, exitCode: 0 };
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
const e = err;
|
|
621
|
+
return {
|
|
622
|
+
stdout: typeof e.stdout === 'string' ? e.stdout : (e.stdout?.toString() ?? ''),
|
|
623
|
+
exitCode: e.status ?? 1,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
log: (line) => console.log(line),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
// ── Wizard ───────────────────────────────────────────────────────────────
|
|
631
|
+
/**
|
|
632
|
+
* Resolve the per-feature on/off vector by combining (in priority order):
|
|
633
|
+
* 1. `--add <feature>` — if set, ONLY that feature is on; everything
|
|
634
|
+
* else is suppressed (idempotent extension, AC #7).
|
|
635
|
+
* 2. `--yes` — accept every default (every feature on).
|
|
636
|
+
* 3. `--with-X` flags — opt-in without prompting.
|
|
637
|
+
* 4. Interactive prompts for any feature still undetermined.
|
|
638
|
+
*
|
|
639
|
+
* Returns a fully-determined FeatureSelection. The dispatcher then writes
|
|
640
|
+
* exactly the union of features marked true.
|
|
641
|
+
*/
|
|
642
|
+
export async function resolveFeatureSelection(flags, adapters) {
|
|
643
|
+
// ── Path 1: `--add` — single-feature extension ────────────────────────
|
|
644
|
+
if (flags.add) {
|
|
645
|
+
const sel = { ...NO_FEATURES };
|
|
646
|
+
switch (flags.add) {
|
|
647
|
+
case 'dor':
|
|
648
|
+
sel.dor = true;
|
|
649
|
+
break;
|
|
650
|
+
case 'attestation':
|
|
651
|
+
sel.attestation = true;
|
|
652
|
+
break;
|
|
653
|
+
case 'classifier':
|
|
654
|
+
sel.classifier = true;
|
|
655
|
+
break;
|
|
656
|
+
case 'branch-protection':
|
|
657
|
+
sel.branchProtection = true;
|
|
658
|
+
break;
|
|
659
|
+
case 'workflows':
|
|
660
|
+
sel.workflows = true;
|
|
661
|
+
break;
|
|
662
|
+
case 'signal-ingestion':
|
|
663
|
+
sel.signalIngestion = true;
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
return sel;
|
|
667
|
+
}
|
|
668
|
+
// ── Path 2: `--yes` — all defaults on, no prompts ─────────────────────
|
|
669
|
+
if (flags.yes) {
|
|
670
|
+
return { ...ALL_FEATURES };
|
|
671
|
+
}
|
|
672
|
+
// ── Path 2b: non-TTY auto-fall-through ────────────────────────────────
|
|
673
|
+
// When stdin is not a TTY (CI runners, agent bash, piped input, Docker
|
|
674
|
+
// containers without `-it`), interactive prompts hang then throw an
|
|
675
|
+
// unhandled ExitPromptError. Auto-fall-through to ALL_FEATURES (same
|
|
676
|
+
// as --yes) so adopters can run `ai-sdlc init` in CI without any
|
|
677
|
+
// additional flags. This matches the documented behavior of --yes, which
|
|
678
|
+
// is defined as "accept all defaults non-interactively".
|
|
679
|
+
if (!process.stdin.isTTY) {
|
|
680
|
+
adapters.log('Non-TTY stdin detected — auto-accepting all feature defaults (equivalent to --yes).');
|
|
681
|
+
adapters.log('Pass --yes explicitly to suppress this message.');
|
|
682
|
+
return { ...ALL_FEATURES };
|
|
683
|
+
}
|
|
684
|
+
// ── Path 3+4: `--with-X` overrides + prompts for the rest ─────────────
|
|
685
|
+
const sel = { ...NO_FEATURES };
|
|
686
|
+
// DoR
|
|
687
|
+
if (flags.withDor) {
|
|
688
|
+
sel.dor = true;
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
sel.dor = await adapters.prompt('Will this repo use Definition-of-Ready gates?', true);
|
|
692
|
+
}
|
|
693
|
+
// Attestation
|
|
694
|
+
if (flags.withAttestation) {
|
|
695
|
+
sel.attestation = true;
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
sel.attestation = await adapters.prompt('Do you want attestation infrastructure (audit-only)?', true);
|
|
699
|
+
}
|
|
700
|
+
// Classifier
|
|
701
|
+
if (flags.withClassifier) {
|
|
702
|
+
sel.classifier = true;
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
sel.classifier = await adapters.prompt('Add review classifier for cost-optimized reviews?', true);
|
|
706
|
+
}
|
|
707
|
+
// Branch protection
|
|
708
|
+
if (flags.withBranchProtection) {
|
|
709
|
+
sel.branchProtection = true;
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
sel.branchProtection = await adapters.prompt('Apply recommended branch protection? (required: ai-sdlc/pr-ready + codecov/patch)', true);
|
|
713
|
+
}
|
|
714
|
+
// GitHub Actions workflows bundle (AISDLC-261)
|
|
715
|
+
if (flags.withWorkflows) {
|
|
716
|
+
sel.workflows = true;
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
sel.workflows = await adapters.prompt('Scaffold GitHub Actions workflows (gate, review, attestation, auto-merge)?', true);
|
|
720
|
+
}
|
|
721
|
+
// RFC-0030 Signal Ingestion Pipeline (AISDLC-348)
|
|
722
|
+
// Default to FALSE in the interactive prompt: the pipeline is in a soak
|
|
723
|
+
// window gated by AI_SDLC_SIGNAL_INGESTION; scaffolding the config on a
|
|
724
|
+
// fresh adopter who hasn't opted in is noise. Opt-in path: --with-signal-
|
|
725
|
+
// ingestion or --add signal-ingestion.
|
|
726
|
+
if (flags.withSignalIngestion) {
|
|
727
|
+
sel.signalIngestion = true;
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
sel.signalIngestion = await adapters.prompt('Scaffold RFC-0030 signal-ingestion config (default OFF; opt in via AI_SDLC_SIGNAL_INGESTION soak)?', false);
|
|
731
|
+
}
|
|
732
|
+
return sel;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Write the union of feature templates into the project dir. AC #4 says
|
|
736
|
+
* the BASELINE workflow templates (gate workflow) are always written; the
|
|
737
|
+
* per-feature template sets are written only when their toggle is on.
|
|
738
|
+
*
|
|
739
|
+
* Idempotent: any file that already exists at the target path is skipped
|
|
740
|
+
* with a "skip" log line. This is what makes `--add <feature>` safe to
|
|
741
|
+
* run on an already-initialized repo (AC #7).
|
|
742
|
+
*
|
|
743
|
+
* The `workflows` feature (AISDLC-261) supports `--force` to overwrite
|
|
744
|
+
* existing workflow files with the current template versions. Use this
|
|
745
|
+
* to upgrade a pre-261 repo to the full workflow bundle.
|
|
746
|
+
*/
|
|
747
|
+
export async function applyFeatureSelection(projectDir, selection, flags, adapters) {
|
|
748
|
+
const result = { created: [], skipped: [], wouldCreate: [] };
|
|
749
|
+
// Build the union of templates to write.
|
|
750
|
+
const templateSets = [];
|
|
751
|
+
// `--add` mode: skip the baseline (we're EXTENDING an existing init).
|
|
752
|
+
// Exception: `--add workflows` must write the workflows bundle even in
|
|
753
|
+
// --add mode (the bundle IS the feature; it's not an extension of baseline).
|
|
754
|
+
if (!flags.add) {
|
|
755
|
+
templateSets.push(BASELINE_WORKFLOW_TEMPLATES);
|
|
756
|
+
}
|
|
757
|
+
if (selection.dor)
|
|
758
|
+
templateSets.push(DOR_TEMPLATES);
|
|
759
|
+
if (selection.attestation)
|
|
760
|
+
templateSets.push(ATTESTATION_TEMPLATES);
|
|
761
|
+
if (selection.classifier)
|
|
762
|
+
templateSets.push(CLASSIFIER_TEMPLATES);
|
|
763
|
+
if (selection.workflows)
|
|
764
|
+
templateSets.push(WORKFLOWS_TEMPLATES);
|
|
765
|
+
if (selection.signalIngestion)
|
|
766
|
+
templateSets.push(SIGNAL_INGESTION_TEMPLATES);
|
|
767
|
+
// AISDLC-261 PR #480 review fix: dedupe across template sets so e.g.
|
|
768
|
+
// `ai-sdlc-gate.yml` (in BOTH BASELINE_WORKFLOW_TEMPLATES and
|
|
769
|
+
// WORKFLOWS_TEMPLATES) doesn't get written twice on
|
|
770
|
+
// `--with-workflows --force` (which would log "overwrite ${relPath}
|
|
771
|
+
// (--force)" against the duplicate, doubling result.created entries).
|
|
772
|
+
// First-set wins (BASELINE comes first, then feature bundles).
|
|
773
|
+
const seenRelPaths = new Set();
|
|
774
|
+
for (const set of templateSets) {
|
|
775
|
+
for (const [relPath, contents] of Object.entries(set.files)) {
|
|
776
|
+
if (seenRelPaths.has(relPath))
|
|
777
|
+
continue;
|
|
778
|
+
seenRelPaths.add(relPath);
|
|
779
|
+
const absPath = join(projectDir, relPath);
|
|
780
|
+
if (flags.dryRun) {
|
|
781
|
+
const isWorkflowFileDry = relPath.startsWith('.github/workflows/');
|
|
782
|
+
const wouldForce = isWorkflowFileDry && flags.force && adapters.exists(absPath);
|
|
783
|
+
result.wouldCreate.push(relPath);
|
|
784
|
+
adapters.log(wouldForce ? ` would overwrite ${relPath} (--force)` : ` would create ${relPath}`);
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
// For the workflows feature: `--force` overwrites existing files.
|
|
788
|
+
// For all other features: existing files are always skipped (idempotent).
|
|
789
|
+
const isWorkflowFile = relPath.startsWith('.github/workflows/');
|
|
790
|
+
const shouldForce = isWorkflowFile && flags.force;
|
|
791
|
+
const alreadyExists = adapters.exists(absPath);
|
|
792
|
+
if (alreadyExists && !shouldForce) {
|
|
793
|
+
result.skipped.push(relPath);
|
|
794
|
+
adapters.log(` skip ${relPath} (already exists)`);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
// mkdir -p the parent
|
|
798
|
+
adapters.mkdirp(dirname(absPath));
|
|
799
|
+
adapters.writeFile(absPath, contents);
|
|
800
|
+
result.created.push(relPath);
|
|
801
|
+
if (alreadyExists && shouldForce) {
|
|
802
|
+
adapters.log(` overwrite ${relPath} (--force)`);
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
adapters.log(` created ${relPath}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// Husky pre-push sign hook is a separate concern from the
|
|
810
|
+
// FeatureTemplateSet because it's an APPEND (not a write-from-empty)
|
|
811
|
+
// — adopters often already have a .husky/pre-push from their existing
|
|
812
|
+
// tooling and we don't want to clobber it. Only fired when attestation
|
|
813
|
+
// is on.
|
|
814
|
+
if (selection.attestation && !flags.dryRun) {
|
|
815
|
+
const hookPath = join(projectDir, '.husky', 'pre-push');
|
|
816
|
+
if (!adapters.exists(hookPath)) {
|
|
817
|
+
// No existing hook — write a minimal one with the sign block.
|
|
818
|
+
adapters.mkdirp(dirname(hookPath));
|
|
819
|
+
adapters.writeFile(hookPath, `#!/usr/bin/env bash\nset -euo pipefail\n\n${HUSKY_PREPUSH_SIGN_SNIPPET}`);
|
|
820
|
+
result.created.push('.husky/pre-push');
|
|
821
|
+
adapters.log(` created .husky/pre-push`);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
const status = adapters.appendOnce(hookPath, HUSKY_PREPUSH_SIGN_SNIPPET, '# ai-sdlc:attestation-sign-block');
|
|
825
|
+
if (status === 'appended') {
|
|
826
|
+
adapters.log(` updated .husky/pre-push (appended sign block)`);
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
result.skipped.push('.husky/pre-push');
|
|
830
|
+
adapters.log(` skip .husky/pre-push (sign block already present)`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
else if (selection.attestation && flags.dryRun) {
|
|
835
|
+
result.wouldCreate.push('.husky/pre-push');
|
|
836
|
+
adapters.log(' would update .husky/pre-push (sign block)');
|
|
837
|
+
}
|
|
838
|
+
// Branch protection (always last — depends on the gate workflow being
|
|
839
|
+
// present so the required check exists when the rule is applied).
|
|
840
|
+
if (selection.branchProtection) {
|
|
841
|
+
result.branchProtection = await applyBranchProtection(projectDir, flags, adapters);
|
|
842
|
+
}
|
|
843
|
+
return result;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Recommended branch-protection ruleset for AI-SDLC adopters. AC #1 #4:
|
|
847
|
+
* the required checks are `ai-sdlc/pr-ready` (the gate aggregator) and
|
|
848
|
+
* `codecov/patch` (the de facto coverage signal). Other AI-SDLC apps
|
|
849
|
+
* post their own statuses but they're all rolled into pr-ready.
|
|
850
|
+
*
|
|
851
|
+
* The body conforms to the GitHub REST API
|
|
852
|
+
* `PUT /repos/{owner}/{repo}/branches/{branch}/protection` schema.
|
|
853
|
+
*/
|
|
854
|
+
export const RECOMMENDED_BRANCH_PROTECTION_BODY = {
|
|
855
|
+
required_status_checks: {
|
|
856
|
+
strict: true,
|
|
857
|
+
contexts: ['ai-sdlc/pr-ready', 'codecov/patch'],
|
|
858
|
+
},
|
|
859
|
+
enforce_admins: false,
|
|
860
|
+
required_pull_request_reviews: {
|
|
861
|
+
dismiss_stale_reviews: true,
|
|
862
|
+
require_code_owner_reviews: false,
|
|
863
|
+
required_approving_review_count: 1,
|
|
864
|
+
},
|
|
865
|
+
restrictions: null,
|
|
866
|
+
allow_force_pushes: false,
|
|
867
|
+
allow_deletions: false,
|
|
868
|
+
};
|
|
869
|
+
/**
|
|
870
|
+
* Apply (or print, in dry-run) the recommended branch protection rule
|
|
871
|
+
* to the `main` branch of the repo at `projectDir`. AC #6 explicitly
|
|
872
|
+
* requires that `--dry-run` print the JSON without applying.
|
|
873
|
+
*
|
|
874
|
+
* The repo identity (`owner/repo`) is resolved by shelling out to
|
|
875
|
+
* `gh repo view --json nameWithOwner -q .nameWithOwner`. We could parse
|
|
876
|
+
* the git remote ourselves (see git-remote.ts) but `gh` already resolves
|
|
877
|
+
* forks + renames + custom default branches consistently, and the user
|
|
878
|
+
* needs `gh` on PATH for the PUT to work anyway.
|
|
879
|
+
*/
|
|
880
|
+
export async function applyBranchProtection(projectDir, flags, adapters) {
|
|
881
|
+
const bodyJson = JSON.stringify(RECOMMENDED_BRANCH_PROTECTION_BODY, null, 2);
|
|
882
|
+
if (flags.dryRun) {
|
|
883
|
+
adapters.log('');
|
|
884
|
+
adapters.log('Branch-protection dry-run — would PUT the following body:');
|
|
885
|
+
adapters.log(bodyJson);
|
|
886
|
+
adapters.log('');
|
|
887
|
+
adapters.log(' endpoint: PUT /repos/{owner}/{repo}/branches/main/protection');
|
|
888
|
+
adapters.log(' apply with: gh api -X PUT repos/{owner}/{repo}/branches/main/protection ...');
|
|
889
|
+
return { applied: false, bodyJson };
|
|
890
|
+
}
|
|
891
|
+
// Resolve owner/repo via gh.
|
|
892
|
+
const ownerRepo = adapters.runCommand('gh', [
|
|
893
|
+
'repo',
|
|
894
|
+
'view',
|
|
895
|
+
'--json',
|
|
896
|
+
'nameWithOwner',
|
|
897
|
+
'-q',
|
|
898
|
+
'.nameWithOwner',
|
|
899
|
+
]);
|
|
900
|
+
if (ownerRepo.exitCode !== 0) {
|
|
901
|
+
return {
|
|
902
|
+
applied: false,
|
|
903
|
+
bodyJson,
|
|
904
|
+
error: `gh repo view failed: ${ownerRepo.stdout.trim() || 'unknown error'}`,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
const slug = ownerRepo.stdout.trim();
|
|
908
|
+
if (!slug) {
|
|
909
|
+
return {
|
|
910
|
+
applied: false,
|
|
911
|
+
bodyJson,
|
|
912
|
+
error: 'gh repo view returned empty owner/repo',
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
// Use the file-based input form so we don't have to thread quoted JSON
|
|
916
|
+
// through a shell. We write to a tmpfile, point gh at it, and let the
|
|
917
|
+
// adapter's runCommand spawn `gh` directly.
|
|
918
|
+
const tmpPath = join(projectDir, '.ai-sdlc', 'branch-protection-body.json');
|
|
919
|
+
// adapters.runCommand can't write files, so we use the underlying
|
|
920
|
+
// primitives directly here — branch protection is a one-shot operation
|
|
921
|
+
// and doesn't need to be hermetic in the same way as the file writes.
|
|
922
|
+
// Tests inject a stub that intercepts the runCommand call and never
|
|
923
|
+
// touches this path. See test for the contract.
|
|
924
|
+
try {
|
|
925
|
+
mkdirSync(dirname(tmpPath), { recursive: true });
|
|
926
|
+
writeFileSync(tmpPath, bodyJson, 'utf-8');
|
|
927
|
+
}
|
|
928
|
+
catch (err) {
|
|
929
|
+
return {
|
|
930
|
+
applied: false,
|
|
931
|
+
bodyJson,
|
|
932
|
+
error: `failed to stage branch-protection body: ${err.message}`,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
const apply = adapters.runCommand('gh', [
|
|
936
|
+
'api',
|
|
937
|
+
'-X',
|
|
938
|
+
'PUT',
|
|
939
|
+
`repos/${slug}/branches/main/protection`,
|
|
940
|
+
'--input',
|
|
941
|
+
tmpPath,
|
|
942
|
+
]);
|
|
943
|
+
if (apply.exitCode !== 0) {
|
|
944
|
+
return {
|
|
945
|
+
applied: false,
|
|
946
|
+
bodyJson,
|
|
947
|
+
error: `gh api PUT failed: ${apply.stdout.trim() || 'unknown error'}`,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
adapters.log(` applied branch protection to ${slug}:main`);
|
|
951
|
+
return { applied: true, bodyJson };
|
|
952
|
+
}
|
|
953
|
+
// ── Next-steps summary ───────────────────────────────────────────────────
|
|
954
|
+
/**
|
|
955
|
+
* Print the structured "next steps" summary at the end of init. AC #5:
|
|
956
|
+
* the summary must include operator action items conditional on which
|
|
957
|
+
* features were chosen (e.g. `gh secret set` commands when attestation
|
|
958
|
+
* was opted in).
|
|
959
|
+
*
|
|
960
|
+
* Returns the rendered summary as a string in addition to logging it,
|
|
961
|
+
* so tests can assert on it without re-stringifying console output.
|
|
962
|
+
*/
|
|
963
|
+
export function renderNextSteps(selection, result, adapters) {
|
|
964
|
+
const lines = [];
|
|
965
|
+
lines.push('');
|
|
966
|
+
lines.push('━━━ Next steps ━━━');
|
|
967
|
+
lines.push('');
|
|
968
|
+
// Always present (baseline gate)
|
|
969
|
+
lines.push('1. Commit the scaffolded files:');
|
|
970
|
+
lines.push(' git add .ai-sdlc .github/workflows package.json');
|
|
971
|
+
lines.push(' git commit -m "chore: bootstrap AI-SDLC config"');
|
|
972
|
+
lines.push('');
|
|
973
|
+
let stepN = 2;
|
|
974
|
+
if (selection.dor) {
|
|
975
|
+
lines.push(`${stepN}. DoR (Definition-of-Ready) is in WARN-ONLY mode by default.`);
|
|
976
|
+
lines.push(' Tune .ai-sdlc/dor-config.yaml then flip evaluationMode: enforce');
|
|
977
|
+
lines.push(' after a soak window confirms the false-positive rate is low.');
|
|
978
|
+
lines.push('');
|
|
979
|
+
stepN++;
|
|
980
|
+
}
|
|
981
|
+
if (selection.attestation) {
|
|
982
|
+
lines.push(`${stepN}. Attestation infrastructure was scaffolded in AUDIT-ONLY mode.`);
|
|
983
|
+
lines.push(' a) Bootstrap your signing key: /ai-sdlc init-signing-key');
|
|
984
|
+
lines.push(' b) Open a PR adding the printed YAML block to');
|
|
985
|
+
lines.push(' .ai-sdlc/trusted-reviewers.yaml');
|
|
986
|
+
// AISDLC-152: removed the optional CI-side signer step (the AISDLC-87
|
|
987
|
+
// CI-attestor was retired in AISDLC-140 sub-4 alongside attestation
|
|
988
|
+
// becoming audit-only). New adopters no longer need to provision the
|
|
989
|
+
// AI_SDLC_CI_ATTESTOR_PRIVATE_KEY secret.
|
|
990
|
+
lines.push('');
|
|
991
|
+
stepN++;
|
|
992
|
+
}
|
|
993
|
+
if (selection.classifier) {
|
|
994
|
+
lines.push(`${stepN}. Review classifier config was scaffolded.`);
|
|
995
|
+
lines.push(' The classifier RUNTIME ships in AISDLC-141 (follow-up). Until');
|
|
996
|
+
lines.push(' that lands, .ai-sdlc/review-classifier.yaml is advisory only.');
|
|
997
|
+
lines.push('');
|
|
998
|
+
stepN++;
|
|
999
|
+
}
|
|
1000
|
+
if (selection.workflows) {
|
|
1001
|
+
lines.push(`${stepN}. GitHub Actions workflows were scaffolded into .github/workflows/.`);
|
|
1002
|
+
lines.push(' Four files are now in place:');
|
|
1003
|
+
lines.push(' - ai-sdlc-gate.yml — ai-sdlc/pr-ready rollup check');
|
|
1004
|
+
lines.push(' - verify-attestation.yml — DSSE attestation verifier (audit-only)');
|
|
1005
|
+
lines.push(' - ai-sdlc-review.yml — PR review status (stub: wire your reviewers)');
|
|
1006
|
+
lines.push(' - auto-enable-auto-merge.yml — arms auto-merge on same-repo PRs');
|
|
1007
|
+
lines.push(' To activate auto-merge:');
|
|
1008
|
+
lines.push(' a) Enable "Allow auto-merge" in GitHub Settings → General.');
|
|
1009
|
+
lines.push(' b) Set the AI_SDLC_PAT secret with write access to the repo.');
|
|
1010
|
+
lines.push(' Re-run with --force to overwrite workflows on a pre-261 repo:');
|
|
1011
|
+
lines.push(' ai-sdlc init --add workflows --force');
|
|
1012
|
+
lines.push('');
|
|
1013
|
+
stepN++;
|
|
1014
|
+
}
|
|
1015
|
+
if (selection.signalIngestion) {
|
|
1016
|
+
lines.push(`${stepN}. RFC-0030 signal-ingestion config was scaffolded (DISABLED by default).`);
|
|
1017
|
+
lines.push(' .ai-sdlc/signal-ingestion.yaml ships every block commented-out under');
|
|
1018
|
+
lines.push(' enabled: false. The pipeline is gated by BOTH the YAML toggle AND the');
|
|
1019
|
+
lines.push(' AI_SDLC_SIGNAL_INGESTION env flag during the soak window.');
|
|
1020
|
+
lines.push(' To opt in:');
|
|
1021
|
+
lines.push(' a) Read docs/operations/signal-ingestion.md (adapter setup +');
|
|
1022
|
+
lines.push(' tier-multiplier + SA-threshold tuning + manual entry workflow).');
|
|
1023
|
+
lines.push(' b) Set AI_SDLC_SIGNAL_INGESTION=1 in your shell / CI env.');
|
|
1024
|
+
lines.push(' c) Edit .ai-sdlc/signal-ingestion.yaml, set spec.enabled: true,');
|
|
1025
|
+
lines.push(' uncomment the adapters block, and confirm tier multipliers.');
|
|
1026
|
+
lines.push(' Config edits emit SignalIngestionConfigChanged governance events to');
|
|
1027
|
+
lines.push(' <ARTIFACTS_DIR>/_orchestrator/events-YYYY-MM-DD.jsonl.');
|
|
1028
|
+
lines.push(' Promotion runbook: docs/operations/signal-ingestion-promotion.md');
|
|
1029
|
+
lines.push('');
|
|
1030
|
+
stepN++;
|
|
1031
|
+
}
|
|
1032
|
+
if (selection.branchProtection) {
|
|
1033
|
+
if (result.branchProtection?.applied) {
|
|
1034
|
+
lines.push(`${stepN}. Branch protection on \`main\` was updated.`);
|
|
1035
|
+
lines.push(' Required checks: ai-sdlc/pr-ready, codecov/patch');
|
|
1036
|
+
}
|
|
1037
|
+
else if (result.branchProtection?.error) {
|
|
1038
|
+
lines.push(`${stepN}. Branch protection was NOT applied:`);
|
|
1039
|
+
lines.push(` ${result.branchProtection.error}`);
|
|
1040
|
+
lines.push(' After resolving (gh auth login, repo permissions, etc.) re-run:');
|
|
1041
|
+
lines.push(' ai-sdlc init --add branch-protection');
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
lines.push(`${stepN}. Branch protection (dry-run) — see JSON above; apply with:`);
|
|
1045
|
+
lines.push(' ai-sdlc init --add branch-protection');
|
|
1046
|
+
}
|
|
1047
|
+
lines.push('');
|
|
1048
|
+
stepN++;
|
|
1049
|
+
}
|
|
1050
|
+
lines.push(`${stepN}. Verify your configuration: ai-sdlc health`);
|
|
1051
|
+
lines.push('');
|
|
1052
|
+
lines.push('Adopter docs: https://github.com/ai-sdlc-framework/ai-sdlc/blob/main/docs/operations/init.md');
|
|
1053
|
+
const out = lines.join('\n');
|
|
1054
|
+
for (const line of lines)
|
|
1055
|
+
adapters.log(line);
|
|
1056
|
+
return out;
|
|
1057
|
+
}
|
|
1058
|
+
// ── CLAUDE.md recommendation pointer (AC #4) ─────────────────────────────
|
|
1059
|
+
/**
|
|
1060
|
+
* The pointer block we append to CLAUDE.md so a freshly-initialized repo's
|
|
1061
|
+
* Claude Code sessions know where to find the AI-SDLC quality-gate docs.
|
|
1062
|
+
* Idempotent — guarded by a sentinel so re-running init doesn't duplicate
|
|
1063
|
+
* the block.
|
|
1064
|
+
*/
|
|
1065
|
+
export const CLAUDE_MD_POINTER = `
|
|
1066
|
+
<!-- ai-sdlc:recommendation-pointer -->
|
|
1067
|
+
## AI-SDLC quality gate
|
|
1068
|
+
|
|
1069
|
+
This repo is bootstrapped with the AI-SDLC framework. The single PR-ready
|
|
1070
|
+
merge gate is \`ai-sdlc/pr-ready\` (see \`.github/workflows/ai-sdlc-gate.yml\`).
|
|
1071
|
+
Run \`ai-sdlc health\` to verify your local config; see
|
|
1072
|
+
\`docs/operations/init.md\` for the adopter guide.
|
|
1073
|
+
<!-- end ai-sdlc:recommendation-pointer -->
|
|
1074
|
+
`;
|
|
1075
|
+
/** Sentinel marker used by the CLAUDE.md pointer for idempotency. */
|
|
1076
|
+
export const CLAUDE_MD_SENTINEL = '<!-- ai-sdlc:recommendation-pointer -->';
|
|
1077
|
+
/**
|
|
1078
|
+
* Append the recommendation pointer to CLAUDE.md (or create the file if
|
|
1079
|
+
* missing). Idempotent: if the sentinel is already present we no-op.
|
|
1080
|
+
*/
|
|
1081
|
+
export function ensureClaudeMdPointer(projectDir, adapters, dryRun) {
|
|
1082
|
+
const path = join(projectDir, 'CLAUDE.md');
|
|
1083
|
+
if (dryRun) {
|
|
1084
|
+
adapters.log(' would update CLAUDE.md (recommendation pointer)');
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (!adapters.exists(path)) {
|
|
1088
|
+
adapters.writeFile(path, `# Project instructions\n${CLAUDE_MD_POINTER}`);
|
|
1089
|
+
adapters.log(' created CLAUDE.md');
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
const status = adapters.appendOnce(path, CLAUDE_MD_POINTER, CLAUDE_MD_SENTINEL);
|
|
1093
|
+
if (status === 'appended') {
|
|
1094
|
+
adapters.log(' updated CLAUDE.md (recommendation pointer)');
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
adapters.log(' skip CLAUDE.md (recommendation pointer already present)');
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
//# sourceMappingURL=init-features.js.map
|