@juangcarmona/lighthouse 0.9.2 → 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/README.md CHANGED
@@ -72,3 +72,7 @@ Only `passed` and `not-applicable` satisfy a required guardrail; `pending-review
72
72
  To add a future guardrail, the catalog owner adds a JSON entry under the packaged `assets/guardrails/` surface, validates it against `assets/schemas/guardrails.schema.json`, declares its profile applicability and repair hint, and uses an existing checker or an explicit semantic or human verification method; in this delivery the surface belongs to the CLI package, and the CLI core does not change.
73
73
 
74
74
  A new deterministic checker mechanism or a new engineering practice is outside this catalog and requires a separate Product Change.
75
+
76
+ ## Optional GitHub repository profile
77
+
78
+ The recommended `github-profile` capability is opt-in. Adopters declare checks, contribution policy and reasoned exceptions in `.lighthouse/github.json`, then use `lighthouse capabilities plan`, `apply` and `verify --id github-profile` to reconcile live settings. No remote settings change during installation or ordinary upgrades. See the [GitHub profile guide](github-profile.md) for configuration, supported controls, authorization and limitations.
@@ -0,0 +1,499 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/juangcarmona/lighthouse/schemas/github-profile.schema.json",
4
+ "title": "Optional Lighthouse GitHub repository profile",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schemaVersion",
9
+ "profileVersion",
10
+ "repository",
11
+ "intake",
12
+ "requiredChecks",
13
+ "requiredApprovals"
14
+ ],
15
+ "properties": {
16
+ "schemaVersion": {
17
+ "const": 1
18
+ },
19
+ "profileVersion": {
20
+ "const": 1
21
+ },
22
+ "repository": {
23
+ "type": "string",
24
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9_.-]+$"
25
+ },
26
+ "intake": {
27
+ "enum": [
28
+ "preserve",
29
+ "public",
30
+ "collaborators"
31
+ ]
32
+ },
33
+ "requiredApprovals": {
34
+ "type": "integer",
35
+ "minimum": 0,
36
+ "maximum": 6
37
+ },
38
+ "requiredChecks": {
39
+ "type": "array",
40
+ "minItems": 1,
41
+ "items": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": [
45
+ "context",
46
+ "integrationId"
47
+ ],
48
+ "properties": {
49
+ "context": {
50
+ "type": "string",
51
+ "minLength": 1
52
+ },
53
+ "integrationId": {
54
+ "type": "integer",
55
+ "minimum": 1
56
+ }
57
+ }
58
+ }
59
+ },
60
+ "evidenceBranch": {
61
+ "type": [
62
+ "string",
63
+ "null"
64
+ ],
65
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9_/-]*$"
66
+ },
67
+ "releaseWorkflowReady": {
68
+ "type": "boolean"
69
+ },
70
+ "overrides": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "properties": {
74
+ "repository.has_issues": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": [
78
+ "value",
79
+ "reason"
80
+ ],
81
+ "properties": {
82
+ "value": {
83
+ "type": [
84
+ "boolean",
85
+ "null"
86
+ ]
87
+ },
88
+ "reason": {
89
+ "type": "string",
90
+ "minLength": 1
91
+ }
92
+ }
93
+ },
94
+ "repository.has_discussions": {
95
+ "type": "object",
96
+ "additionalProperties": false,
97
+ "required": [
98
+ "value",
99
+ "reason"
100
+ ],
101
+ "properties": {
102
+ "value": {
103
+ "type": [
104
+ "boolean",
105
+ "null"
106
+ ]
107
+ },
108
+ "reason": {
109
+ "type": "string",
110
+ "minLength": 1
111
+ }
112
+ }
113
+ },
114
+ "repository.is_template": {
115
+ "type": "object",
116
+ "additionalProperties": false,
117
+ "required": [
118
+ "value",
119
+ "reason"
120
+ ],
121
+ "properties": {
122
+ "value": {
123
+ "type": [
124
+ "boolean",
125
+ "null"
126
+ ]
127
+ },
128
+ "reason": {
129
+ "type": "string",
130
+ "minLength": 1
131
+ }
132
+ }
133
+ },
134
+ "repository.allow_squash_merge": {
135
+ "type": "object",
136
+ "additionalProperties": false,
137
+ "required": [
138
+ "value",
139
+ "reason"
140
+ ],
141
+ "properties": {
142
+ "value": {
143
+ "type": [
144
+ "boolean",
145
+ "null"
146
+ ]
147
+ },
148
+ "reason": {
149
+ "type": "string",
150
+ "minLength": 1
151
+ }
152
+ }
153
+ },
154
+ "repository.allow_merge_commit": {
155
+ "type": "object",
156
+ "additionalProperties": false,
157
+ "required": [
158
+ "value",
159
+ "reason"
160
+ ],
161
+ "properties": {
162
+ "value": {
163
+ "type": [
164
+ "boolean",
165
+ "null"
166
+ ]
167
+ },
168
+ "reason": {
169
+ "type": "string",
170
+ "minLength": 1
171
+ }
172
+ }
173
+ },
174
+ "repository.allow_rebase_merge": {
175
+ "type": "object",
176
+ "additionalProperties": false,
177
+ "required": [
178
+ "value",
179
+ "reason"
180
+ ],
181
+ "properties": {
182
+ "value": {
183
+ "type": [
184
+ "boolean",
185
+ "null"
186
+ ]
187
+ },
188
+ "reason": {
189
+ "type": "string",
190
+ "minLength": 1
191
+ }
192
+ }
193
+ },
194
+ "repository.allow_auto_merge": {
195
+ "type": "object",
196
+ "additionalProperties": false,
197
+ "required": [
198
+ "value",
199
+ "reason"
200
+ ],
201
+ "properties": {
202
+ "value": {
203
+ "type": [
204
+ "boolean",
205
+ "null"
206
+ ]
207
+ },
208
+ "reason": {
209
+ "type": "string",
210
+ "minLength": 1
211
+ }
212
+ }
213
+ },
214
+ "repository.allow_update_branch": {
215
+ "type": "object",
216
+ "additionalProperties": false,
217
+ "required": [
218
+ "value",
219
+ "reason"
220
+ ],
221
+ "properties": {
222
+ "value": {
223
+ "type": [
224
+ "boolean",
225
+ "null"
226
+ ]
227
+ },
228
+ "reason": {
229
+ "type": "string",
230
+ "minLength": 1
231
+ }
232
+ }
233
+ },
234
+ "repository.delete_branch_on_merge": {
235
+ "type": "object",
236
+ "additionalProperties": false,
237
+ "required": [
238
+ "value",
239
+ "reason"
240
+ ],
241
+ "properties": {
242
+ "value": {
243
+ "type": [
244
+ "boolean",
245
+ "null"
246
+ ]
247
+ },
248
+ "reason": {
249
+ "type": "string",
250
+ "minLength": 1
251
+ }
252
+ }
253
+ },
254
+ "repository.web_commit_signoff_required": {
255
+ "type": "object",
256
+ "additionalProperties": false,
257
+ "required": [
258
+ "value",
259
+ "reason"
260
+ ],
261
+ "properties": {
262
+ "value": {
263
+ "type": [
264
+ "boolean",
265
+ "null"
266
+ ]
267
+ },
268
+ "reason": {
269
+ "type": "string",
270
+ "minLength": 1
271
+ }
272
+ }
273
+ },
274
+ "repository.squash_merge_commit_title": {
275
+ "type": "object",
276
+ "additionalProperties": false,
277
+ "required": [
278
+ "value",
279
+ "reason"
280
+ ],
281
+ "properties": {
282
+ "value": {
283
+ "enum": [
284
+ "PR_TITLE",
285
+ "COMMIT_OR_PR_TITLE",
286
+ null
287
+ ]
288
+ },
289
+ "reason": {
290
+ "type": "string",
291
+ "minLength": 1
292
+ }
293
+ }
294
+ },
295
+ "repository.squash_merge_commit_message": {
296
+ "type": "object",
297
+ "additionalProperties": false,
298
+ "required": [
299
+ "value",
300
+ "reason"
301
+ ],
302
+ "properties": {
303
+ "value": {
304
+ "enum": [
305
+ "PR_BODY",
306
+ "COMMIT_MESSAGES",
307
+ "BLANK",
308
+ null
309
+ ]
310
+ },
311
+ "reason": {
312
+ "type": "string",
313
+ "minLength": 1
314
+ }
315
+ }
316
+ },
317
+ "actions.workflow": {
318
+ "type": "object",
319
+ "additionalProperties": false,
320
+ "required": [
321
+ "value",
322
+ "reason"
323
+ ],
324
+ "properties": {
325
+ "value": {
326
+ "enum": [
327
+ null,
328
+ {
329
+ "default_workflow_permissions": "read",
330
+ "can_approve_pull_request_reviews": false
331
+ }
332
+ ]
333
+ },
334
+ "reason": {
335
+ "type": "string",
336
+ "minLength": 1
337
+ }
338
+ }
339
+ },
340
+ "actions.forkApproval": {
341
+ "type": "object",
342
+ "additionalProperties": false,
343
+ "required": [
344
+ "value",
345
+ "reason"
346
+ ],
347
+ "properties": {
348
+ "value": {
349
+ "enum": [
350
+ "all_external_contributors",
351
+ "first_time_contributors",
352
+ "first_time_contributors_new_to_github",
353
+ null
354
+ ]
355
+ },
356
+ "reason": {
357
+ "type": "string",
358
+ "minLength": 1
359
+ }
360
+ }
361
+ },
362
+ "security.alerts": {
363
+ "type": "object",
364
+ "additionalProperties": false,
365
+ "required": [
366
+ "value",
367
+ "reason"
368
+ ],
369
+ "properties": {
370
+ "value": {
371
+ "enum": [
372
+ true,
373
+ null
374
+ ]
375
+ },
376
+ "reason": {
377
+ "type": "string",
378
+ "minLength": 1
379
+ }
380
+ }
381
+ },
382
+ "security.updates": {
383
+ "type": "object",
384
+ "additionalProperties": false,
385
+ "required": [
386
+ "value",
387
+ "reason"
388
+ ],
389
+ "properties": {
390
+ "value": {
391
+ "enum": [
392
+ true,
393
+ null
394
+ ]
395
+ },
396
+ "reason": {
397
+ "type": "string",
398
+ "minLength": 1
399
+ }
400
+ }
401
+ },
402
+ "security.secret_scanning": {
403
+ "type": "object",
404
+ "additionalProperties": false,
405
+ "required": [
406
+ "value",
407
+ "reason"
408
+ ],
409
+ "properties": {
410
+ "value": {
411
+ "enum": [
412
+ "enabled",
413
+ null
414
+ ]
415
+ },
416
+ "reason": {
417
+ "type": "string",
418
+ "minLength": 1
419
+ }
420
+ }
421
+ },
422
+ "security.secret_scanning_push_protection": {
423
+ "type": "object",
424
+ "additionalProperties": false,
425
+ "required": [
426
+ "value",
427
+ "reason"
428
+ ],
429
+ "properties": {
430
+ "value": {
431
+ "enum": [
432
+ "enabled",
433
+ null
434
+ ]
435
+ },
436
+ "reason": {
437
+ "type": "string",
438
+ "minLength": 1
439
+ }
440
+ }
441
+ },
442
+ "releases.immutable": {
443
+ "type": "object",
444
+ "additionalProperties": false,
445
+ "required": [
446
+ "value",
447
+ "reason"
448
+ ],
449
+ "properties": {
450
+ "value": {
451
+ "enum": [
452
+ true,
453
+ null
454
+ ]
455
+ },
456
+ "reason": {
457
+ "type": "string",
458
+ "minLength": 1
459
+ }
460
+ }
461
+ },
462
+ "rules.main": {
463
+ "type": "object",
464
+ "additionalProperties": false,
465
+ "required": [
466
+ "value",
467
+ "reason"
468
+ ],
469
+ "properties": {
470
+ "value": {
471
+ "type": "null"
472
+ },
473
+ "reason": {
474
+ "type": "string",
475
+ "minLength": 1
476
+ }
477
+ }
478
+ },
479
+ "rules.evidence": {
480
+ "type": "object",
481
+ "additionalProperties": false,
482
+ "required": [
483
+ "value",
484
+ "reason"
485
+ ],
486
+ "properties": {
487
+ "value": {
488
+ "type": "null"
489
+ },
490
+ "reason": {
491
+ "type": "string",
492
+ "minLength": 1
493
+ }
494
+ }
495
+ }
496
+ }
497
+ }
498
+ }
499
+ }
@@ -0,0 +1,91 @@
1
+ # Recommended GitHub repository profile
2
+
3
+ The `github-profile` capability is an optional recommendation for GitHub.com repositories. Any Lighthouse adopter can select it, supply its own policy, inspect changes, apply an authorized plan and verify live settings. Leaving it unselected or declining it does not fail the general foundation check. It is independent of application profiles and does not require a factory runtime.
4
+
5
+ The profile is an implementation of [profile adoption](../../../docs/product/model/use-cases/uc-adopt-profile.md). GitHub remains the authority for remote settings; the adopter owns configuration and exceptions. Installation, initialization and upgrades never apply this profile remotely.
6
+
7
+ ## Configure adoption
8
+
9
+ Create `.lighthouse/github.json` in the consuming repository using the [configuration schema](assets/schemas/github-profile.schema.json). Keep this file in version control and review it like other repository policy. Select unconditional CI job names and their actual GitHub App IDs; do not make a path-filtered job a universal required check. Approvals and contribution policy are explicit choices rather than assumptions about a solo maintainer or an open-source community.
10
+
11
+ ```json
12
+ {
13
+ "schemaVersion": 1,
14
+ "profileVersion": 1,
15
+ "repository": "example/project",
16
+ "intake": "preserve",
17
+ "requiredChecks": [{ "context": "verify", "integrationId": 15368 }],
18
+ "requiredApprovals": 1,
19
+ "evidenceBranch": null,
20
+ "releaseWorkflowReady": false,
21
+ "overrides": {
22
+ "releases.immutable": {
23
+ "value": null,
24
+ "reason": "No release publisher is configured for this project."
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ Choose `collaborators`, `public` or `preserve` for issue and PR creation. A solo maintainer may choose zero required approvals, but that setting does not enforce a human integration decision. Keep that decision in the host's authorization policy and keep publication credentials outside implementation workers.
31
+
32
+ Set `evidenceBranch` to a literal permanent branch name when the project uses one. The adapter protects it without creating it, copying application files or changing its tip. Set `releaseWorkflowReady: true` only after checking that the publisher creates a draft, attaches all assets and then publishes the release. Otherwise preserve that setting with a documented exception.
33
+
34
+ An override has a supported control ID, a typed `value` and a non-empty `reason`. `null` means preserve and exclude that control from compliance, reported as `excepted`. A different supported value records a project-specific policy. A `rules.main` or `rules.evidence` override accepts only `null` with a reason; its result explicitly excludes that protection from compliance and does not claim a protected branch. The adapter never disables secret scanning, alerts, security updates or release immutability. It never removes or weakens an existing ruleset, including when the project asks for fewer approvals or checks.
35
+
36
+ ## Plan, apply and verify
37
+
38
+ ```sh
39
+ lighthouse capabilities select --id github-profile --provider github
40
+ profile_plan=$(mktemp)
41
+ lighthouse capabilities plan --id github-profile --input .lighthouse/github.json --plan-file "$profile_plan"
42
+ lighthouse capabilities apply --plan-file "$profile_plan" --authorize <plan-hash> --format json
43
+ lighthouse capabilities verify --id github-profile --input .lighthouse/github.json --format json
44
+ rm -- "$profile_plan"
45
+ ```
46
+
47
+ Selection records a local adoption choice when Lighthouse ownership state exists. Planning and live verification can also run against the explicit configuration before full foundation adoption. The default input path is `.lighthouse/github.json`. An optional `--repo` must match the configured repository; there is no implicit target derived from a Git remote or current login.
48
+
49
+ Planning reads GitHub and writes only the requested local plan. The plan records before/after values, exceptions, repository identity, applicable rules and a SHA-256 authorization hash. Apply validates that exact plan and its permitted operations, rereads the provider before each write, and refuses unexpected drift. Already-satisfied operations are skipped on retry. The adapter fixes its API host to `github.com`; GitHub Enterprise Server is outside this version's supported scope.
50
+
51
+ Apply adds missing protection before changing settings. Compatible rules with no bypass satisfy the minimum; stronger rules, additional checks and unrelated settings remain intact. Missing requirements receive a separate additive ruleset. Existing policies are never replaced, deleted or automatically adopted as Lighthouse-owned. GitHub administration permission is required when changes are planned; narrower credentials may inspect the controls their permissions expose. Missing permissions, unavailable APIs and plan restrictions produce `unavailable` or `blocked`, never a success claim. Private repositories remain private.
52
+
53
+ Apply reports its result on standard output, including completed operations and errors on partial failure; `--format json` makes this machine-readable. It creates no result files or history in the repository. Successful adoption updates only the existing `.lighthouse/upgrade.json` entry. Keep the configuration in Git; use a temporary path for the reviewed plan and remove it when finished. If an operator or CI needs durable evidence, capture the output in its existing logs or artifact storage. Failed or uncertain requests require fresh verification before retry; the adapter does not roll back protections or blindly repeat a write. API operations are not a transaction, and GitHub does not provide a compare-and-swap operation for repository settings. Avoid concurrent administrators applying conflicting policies.
54
+
55
+ Live verification exits nonzero for drift or unavailable selected controls and makes no remote writes. General `lighthouse verify` validates local configuration and points to the live command; it does not make network requests or claim that a previous receipt proves today's remote settings. A result with exceptions covers only the selected automated controls. Manual recommendations are always listed separately.
56
+
57
+ ## Automated controls
58
+
59
+ | Control | Recommendation | Application |
60
+ | --- | --- | --- |
61
+ | `repository.has_issues` | `true` | Enable issues. |
62
+ | `repository.has_discussions` | `false` | Preserve an existing community through a reasoned override. |
63
+ | `repository.is_template` | `false` | Override for an intentional starter repository. |
64
+ | `repository.allow_squash_merge` | `true` | Default integration method. |
65
+ | `repository.allow_merge_commit` | `false` | Override when branch ancestry matters. |
66
+ | `repository.allow_rebase_merge` | `false` | Override for a deliberate rebase workflow. |
67
+ | `repository.squash_merge_commit_title` | `PR_TITLE` | Validate PR titles in the project's CI. |
68
+ | `repository.squash_merge_commit_message` | `PR_BODY` | Preserve the reviewed change description. |
69
+ | `repository.allow_auto_merge` | `true` | Makes the feature available; does not enable it on any PR or authorize integration. |
70
+ | `repository.allow_update_branch` | `true` | Offer branch updates. |
71
+ | `repository.delete_branch_on_merge` | `true` | Clean merged feature branches; protect permanent branches separately. |
72
+ | `repository.web_commit_signoff_required` | `false` | Override if the project adopts DCO. |
73
+ | `intake` | Adopter choice | Apply the chosen issue and PR creation policy together, or preserve it. |
74
+ | `rules.main` | PR, resolved conversations, stale-review dismissal, explicit checks and approvals, strict base, no deletion/force-push/bypass | Resolve the current default branch without renaming it. |
75
+ | `rules.evidence` | No deletion, force-push or bypass | Only when an evidence branch is declared. |
76
+ | `actions.workflow` | Read token, no PR approval | Default token permissions only; workflows can request additional permissions. |
77
+ | `actions.forkApproval` | `all_external_contributors` | Public repositories; private-fork policy needs separate review. |
78
+ | `security.alerts` | `true` | Enable Dependabot alerts before security updates. |
79
+ | `security.updates` | `true` | Enable Dependabot security PRs; does not configure version-update scheduling. |
80
+ | `security.secret_scanning` | `enabled` | Require availability or an explicit exception. |
81
+ | `security.secret_scanning_push_protection` | `enabled` | Require availability or an explicit exception. |
82
+ | `releases.immutable` | `true` | Requires confirmed publisher readiness or a reasoned exception. |
83
+
84
+ The adapter does not manage visibility, repository ownership/name, default-branch renaming, Wiki, Projects, sponsorship, social preview, archive participation, commit comments, linked-issue auto-close, LFS archives, multi-ref push limits, Actions retention, issue templates or workflow contents. These remain product decisions or manual checks; they are not silently treated as compliant. Security availability can depend on GitHub plan and permissions; this capability never purchases a feature or changes visibility to unlock it.
85
+
86
+ ## References
87
+
88
+ - [GitHub repository settings API](https://docs.github.com/en/rest/repos/repos)
89
+ - [GitHub rulesets API](https://docs.github.com/en/rest/repos/rules)
90
+ - [GitHub Actions permissions API](https://docs.github.com/en/rest/actions/permissions)
91
+ - [Immutable release publication](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Lighthouse adoption and verification CLI",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  "@arcshape/cli": "0.1.6",
24
24
  "@juangcarmona/lighthouse-ui": "0.2.0",
25
25
  "@prodshape/cli": "0.23.0",
26
- "@juangcarmona/lighthouse-sdlc": "0.10.0"
26
+ "@juangcarmona/lighthouse-sdlc": "0.11.0"
27
27
  },
28
28
  "files": [
29
29
  "src/lighthouse.mjs",
@@ -35,7 +35,9 @@
35
35
  "src/profiles.mjs",
36
36
  "src/create-dotnet.mjs",
37
37
  "src/verify-dotnet.mjs",
38
- "upgrades"
38
+ "upgrades",
39
+ "src/github-profile.mjs",
40
+ "github-profile.md"
39
41
  ],
40
42
  "bin": {
41
43
  "lighthouse": "./src/lighthouse.mjs"
@@ -93,6 +93,7 @@ try {
93
93
 
94
94
  await copy(resolve(packageRoot, 'src/lighthouse.mjs'), resolve(stage, 'scripts/lighthouse.mjs'));
95
95
  await copy(resolve(packageRoot, 'src/aliases.mjs'), resolve(stage, 'scripts/aliases.mjs'));
96
+ await copy(resolve(packageRoot, 'src/github-profile.mjs'), resolve(stage, 'scripts/github-profile.mjs'));
96
97
  await copy(resolve(packageRoot, 'src/init-plan.mjs'), resolve(stage, 'scripts/init-plan.mjs'));
97
98
  await copy(resolve(packageRoot, 'src/guardrails.mjs'), resolve(stage, 'scripts/guardrails.mjs'));
98
99
  await copy(resolve(packageRoot, 'src/profiles.mjs'), resolve(stage, 'scripts/profiles.mjs'));
@@ -0,0 +1,84 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+
5
+ const option = (args, name) => args.includes(name) ? args[args.indexOf(name) + 1] : args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
6
+
7
+ async function writeJson(path, value) {
8
+ await mkdir(dirname(path), { recursive: true });
9
+ const temporary = `${path}.${process.pid}.tmp`;
10
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
11
+ await rename(temporary, path);
12
+ }
13
+
14
+ export async function githubProfileCommand(args, { root, catalog }) {
15
+ const adapter = await import('@juangcarmona/lighthouse-sdlc/github-profile');
16
+ const action = args[0];
17
+ const format = option(args, '--format') ?? 'text';
18
+ const configPath = resolve(root, option(args, '--input') ?? '.lighthouse/github.json');
19
+ const planPath = option(args, '--plan-file');
20
+ const repo = option(args, '--repo');
21
+ const output = (value) => {
22
+ if (format === 'json') console.log(JSON.stringify(value, null, 2));
23
+ else {
24
+ console.log(`GitHub profile: ${value.status} (${value.repository.repo})`);
25
+ if (value.completed) console.log(`Completed operations: ${value.completed.join(', ') || 'none'}`);
26
+ if (value.error) console.log(`Error: ${value.error}`);
27
+ for (const control of value.controls ?? []) console.log(` ${control.status} ${control.id}${control.reason ? `: ${control.reason}` : ''}`);
28
+ if (value.authorization) console.log(`Plan hash: ${value.authorization.planHash}\nRemote writes: none`);
29
+ if (value.status === 'drift' && !value.canApply) console.log('Apply requires repository administration permission.');
30
+ for (const item of value.manualRecommendations ?? []) console.log(` manual: ${item}`);
31
+ }
32
+ };
33
+ if (['plan', 'verify'].includes(action)) {
34
+ if (action === 'plan' && !planPath) throw new Error('GitHub profile planning requires --plan-file <path>.');
35
+ const config = JSON.parse(await readFile(configPath, 'utf8'));
36
+ if (repo && repo.toLowerCase() !== config.repository?.toLowerCase()) throw new Error('--repo does not match the repository declared in the GitHub profile.');
37
+ const plan = adapter.buildGithubProfilePlan(config, await adapter.readGithubProfile(config));
38
+ if (action === 'plan') await writeJson(resolve(root, planPath), plan);
39
+ output({ ...plan, checkedAt: new Date().toISOString(), manualRecommendations: adapter.githubProfileManualRecommendations });
40
+ if ((action === 'verify' && plan.status !== 'passed') || (action === 'plan' && !plan.canApply)) process.exitCode = 1;
41
+ return;
42
+ }
43
+ if (action !== 'apply' || !planPath || !option(args, '--authorize')) throw new Error('Use capabilities plan|verify --id github-profile, or capabilities apply --plan-file <path> --authorize <hash>.');
44
+ const plan = JSON.parse(await readFile(resolve(root, planPath), 'utf8'));
45
+ if (repo && repo.toLowerCase() !== plan.configuration?.repository?.toLowerCase()) throw new Error('--repo does not match the authorized plan.');
46
+ let result;
47
+ try { result = await adapter.applyGithubProfile(plan, option(args, '--authorize')); }
48
+ catch (error) {
49
+ if (!error.result) throw error;
50
+ output(error.result);
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+ const upgradePath = resolve(root, '.lighthouse/upgrade.json');
55
+ let state;
56
+ try { state = JSON.parse(await readFile(upgradePath, 'utf8')); } catch (error) { if (error.code !== 'ENOENT') throw error; }
57
+ if (state) {
58
+ const capability = catalog.capabilities.find((item) => item.id === 'github-profile');
59
+ state.adoption ??= {};
60
+ state.adoption['github-profile'] = { id: capability.id, status: 'adopted', selection: capability.selection, provider: capability.provider, version: capability.version, catalogVersion: catalog.package.version, nextAction: null };
61
+ await writeJson(upgradePath, state);
62
+ }
63
+ output({ ...result, applied: true, manualRecommendations: adapter.githubProfileManualRecommendations });
64
+ }
65
+
66
+ export async function localGithubProfileStatus(root) {
67
+ const result = { id: 'foundation.github-profile', owner: '@juangcarmona/lighthouse-sdlc', level: 'recommended', kind: 'hard', verification: 'deterministic', status: 'not-applicable', evidence: [], problems: [], nextAction: null };
68
+ let config;
69
+ try { config = JSON.parse(await readFile(resolve(root, '.lighthouse/github.json'), 'utf8')); }
70
+ catch (error) {
71
+ if (error.code === 'ENOENT') return result;
72
+ return { ...result, status: 'failed', problems: ['Cannot parse .lighthouse/github.json.'] };
73
+ }
74
+ try {
75
+ const adapter = await import('@juangcarmona/lighthouse-sdlc/github-profile');
76
+ const errors = adapter.validateGithubProfile(config);
77
+ result.status = errors.length ? 'failed' : 'unconfigured';
78
+ result.problems = errors;
79
+ result.nextAction = 'lighthouse capabilities verify --id github-profile --format json';
80
+ // Local verification must not turn an old receipt into a claim about live GitHub.
81
+ result.evidence = [{ path: '.lighthouse/github.json', scope: 'local-configuration', sha256: createHash('sha256').update(JSON.stringify(config)).digest('hex') }];
82
+ } catch { result.status = 'unavailable'; result.problems = ['Install the SDLC GitHub profile adapter to validate this configuration.']; }
83
+ return result;
84
+ }
@@ -9,6 +9,7 @@ import { evaluateGuardrails, readGuardrailCatalog } from './guardrails.mjs';
9
9
  import { composeProfiles, normalizeApplication, profileCompositionResult, readProfileCatalog } from './profiles.mjs';
10
10
  import { hasUnresolvedQuestions, manifestFromInitPlan, validateInitPlan } from './init-plan.mjs';
11
11
  import { readCommandAliases, verifyCommandAliases } from './aliases.mjs';
12
+ import { githubProfileCommand, localGithubProfileStatus } from './github-profile.mjs';
12
13
  let sdlcCapabilities;
13
14
  let sdlcReconcile;
14
15
  let sdlcGithub;
@@ -271,7 +272,7 @@ function capabilityAdoption(previous = null) {
271
272
  ? `lighthouse capabilities select --id ${capability.id} --provider ${capability.provider}`
272
273
  : ['adopted', 'declined'].includes(status)
273
274
  ? null
274
- : `lighthouse capabilities plan --id ${capability.id} --provider ${capability.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${capability.id}.plan.json`;
275
+ : `lighthouse capabilities plan --id ${capability.id} --provider ${capability.provider} --repo <owner/repo> --plan-file ${capability.id === 'github-profile' ? '<temporary-plan-path>' : `.lighthouse/reconciliation/${capability.id}.plan.json`}`;
275
276
  return [capability.id, {
276
277
  id: capability.id,
277
278
  status,
@@ -1408,7 +1409,7 @@ async function verify(manifest, format = 'text') {
1408
1409
  for (const item of adoption) {
1409
1410
  if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
1410
1411
  item.status = 'failed';
1411
- item.nextAction = `lighthouse capabilities plan --id ${item.id} --provider ${item.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${item.id}.plan.json`;
1412
+ item.nextAction = `lighthouse capabilities plan --id ${item.id} --provider ${item.provider} --repo <owner/repo> --plan-file ${item.id === 'github-profile' ? '<temporary-plan-path>' : `.lighthouse/reconciliation/${item.id}.plan.json`}`;
1412
1413
  }
1413
1414
  if (adoption.length) {
1414
1415
  const pending = adoption.filter((item) => !['adopted', 'declined', 'available'].includes(item.status));
@@ -1421,13 +1422,16 @@ async function verify(manifest, format = 'text') {
1421
1422
  kind: 'hard',
1422
1423
  verification: 'deterministic',
1423
1424
  status: pending.length ? 'unconfigured' : available || declined ? 'not-applicable' : 'passed',
1424
- adoptionStatus: pending.length ? pending[0].status : available ? 'available' : declined ? 'declined' : 'adopted',
1425
+ adoptionStatus: pending.length ? pending[0].status : declined ? 'declined' : available ? 'available' : 'adopted',
1425
1426
  evidence: adoption.map((item) => ({ capability: item.id, catalogVersion: item.catalogVersion })),
1426
1427
  problems: pending.map((item) => `${item.id} is ${item.status}; run ${item.nextAction}`),
1427
1428
  nextAction: pending[0]?.nextAction ?? null,
1428
1429
  });
1429
1430
  report.summary.unconfigured = (report.summary.unconfigured ?? 0) + (pending.length ? 1 : 0);
1430
1431
  }
1432
+ const githubProfileResult = await localGithubProfileStatus(root);
1433
+ report.results.push(githubProfileResult);
1434
+ report.summary[githubProfileResult.status] = (report.summary[githubProfileResult.status] ?? 0) + 1;
1431
1435
  const evidenceResult = await verifyEvidenceIndex();
1432
1436
  report.results.push(evidenceResult);
1433
1437
  report.summary[evidenceResult.status] = (report.summary[evidenceResult.status] ?? 0) + 1;
@@ -1574,6 +1578,11 @@ async function capabilities(args) {
1574
1578
  const inputPath = args.includes('--input') ? args[args.indexOf('--input') + 1] : args.find((arg) => arg.startsWith('--input='))?.slice(8);
1575
1579
  const format = formatFromArgs(args);
1576
1580
  const planFile = args.includes('--plan-file') ? args[args.indexOf('--plan-file') + 1] : args.find((arg) => arg.startsWith('--plan-file='))?.slice(12);
1581
+ const isGithubPlan = action === 'apply' && planFile && JSON.parse(await readFile(resolve(root, planFile), 'utf8')).kind === 'github-profile';
1582
+ if ((id === 'github-profile' && ['plan', 'apply', 'verify'].includes(action)) || isGithubPlan) {
1583
+ if (provider !== 'github') throw new Error('The GitHub profile requires provider github.');
1584
+ return githubProfileCommand(args, { root, catalog: sdlcCapabilityCatalog() });
1585
+ }
1577
1586
  if (['select', 'decline'].includes(action)) {
1578
1587
  const catalog = sdlcCapabilityCatalog();
1579
1588
  if (!catalog) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before selecting capabilities.');
@@ -1590,7 +1599,7 @@ async function capabilities(args) {
1590
1599
  provider: capability.provider,
1591
1600
  version: capability.version,
1592
1601
  catalogVersion: catalog.package.version,
1593
- nextAction: status === 'declined' ? null : `lighthouse capabilities plan --id ${id} --provider ${provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${id}.plan.json`,
1602
+ nextAction: status === 'declined' ? null : `lighthouse capabilities plan --id ${id} --provider ${provider} --repo <owner/repo> --plan-file ${id === 'github-profile' ? '<temporary-plan-path>' : `.lighthouse/reconciliation/${id}.plan.json`}`,
1594
1603
  };
1595
1604
  await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
1596
1605
  const result = { schemaVersion: 1, capability: id, provider, status, applied: true };
@@ -1669,7 +1678,7 @@ async function capabilities(args) {
1669
1678
  if (format === 'json') console.log(JSON.stringify(result, null, 2)); else process.stdout.write(capabilityPlanText(result));
1670
1679
  return;
1671
1680
  }
1672
- throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply> ...');
1681
+ throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply|verify> ...');
1673
1682
  }
1674
1683
 
1675
1684
  function formatFromArgs(args) {
@@ -1689,7 +1698,7 @@ if (command === 'create-dotnet') {
1689
1698
  let invalidArgument = false;
1690
1699
  for (let index = 0; index < commandArgs.length; index += 1) {
1691
1700
  const arg = commandArgs[index];
1692
- if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
1701
+ if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply', 'verify'].includes(arg)) continue;
1693
1702
  if (arg === '--dry-run') continue;
1694
1703
  if (arg === '--apply') { if (!['init', 'authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
1695
1704
  if (arg === '--adopt-existing') { if (command !== 'authorities') invalidArgument = true; continue; }