@juangcarmona/lighthouse 0.9.2 → 0.11.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
@@ -16,6 +16,8 @@ Use `lighthouse reconcile` to preview missing Lighthouse-managed paths in an exi
16
16
 
17
17
  Use `lighthouse upgrade` to preview a versioned foundation update. The command is dry-run by default, accepts `--scope harness`, `--scope sdlc` or `--scope all`, preserves customized projections and stops when ownership evidence is missing. Add `--apply` only after reviewing the plan, then run `lighthouse verify`.
18
18
 
19
+ Agent workflows are opt-in through the SDLC package installer. Initialization does not create an entire worker catalog. Upgrading an older adoption releases workflow ownership while preserving the existing files for explicit review and cleanup.
20
+
19
21
  The harness package owns the baseline command and verification contract. The SDLC package owns lifecycle skills, commands, metacommands, routes and gates, and `lighthouse init` records its selected version in `.lighthouse/sdlc.json`. The UI package owns visual tokens, native recipes, `DESIGN.md` and its agent skill. Selecting `visual-theme` projects the guide and skill into the adopter and enables its deterministic guardrail. Stack profiles own their source templates, guidance and checks; the .NET creator is the packaged entry point for the selected backend profile.
20
22
 
21
23
  ProductShape, ArcShape and OpenSpec keep their own files and commands. Add them through their supported integration routes when the project needs them, and keep their versions and managed paths visible in the project configuration. In an existing repository, run `lighthouse authorities --adopt-existing` to validate the current ProductShape and ArcShape surfaces without running either initializer; review the dry-run plan and add `--apply` only to record both exact versions in Lighthouse-owned configuration.
@@ -72,3 +74,7 @@ Only `passed` and `not-applicable` satisfy a required guardrail; `pending-review
72
74
  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
75
 
74
76
  A new deterministic checker mechanism or a new engineering practice is outside this catalog and requires a separate Product Change.
77
+
78
+ ## Optional GitHub repository profile
79
+
80
+ 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.11.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.12.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"
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, rmdir, writeFile } from 'node:fs/promises';
3
- import { createHash } from 'node:crypto';
4
3
  import { dirname, join, resolve } from 'node:path';
5
4
  import { fileURLToPath } from 'node:url';
6
5
  import { createRequire } from 'node:module';
@@ -10,23 +9,6 @@ const templateRoot = resolve(packageRoot, 'assets/dotnet-template');
10
9
  const cliPackage = JSON.parse(await readFile(resolve(packageRoot, 'package.json'), 'utf8'));
11
10
  const require = createRequire(import.meta.url);
12
11
  const sdlcContract = JSON.parse(await readFile(require.resolve('@juangcarmona/lighthouse-sdlc/contract.json'), 'utf8'));
13
- const sdlcPackageRoot = dirname(require.resolve('@juangcarmona/lighthouse-sdlc/contract.json'));
14
- const sdlcWorkflowPaths = [
15
- '.github/workflows/lighthouse-sdlc-router.yml',
16
- '.github/workflows/lighthouse-sdlc-triage.md',
17
- '.github/workflows/lighthouse-sdlc-triage.lock.yml',
18
- '.github/workflows/lighthouse-sdlc-refine.md',
19
- '.github/workflows/lighthouse-sdlc-refine.lock.yml',
20
- '.github/workflows/lighthouse-sdlc-propose.md',
21
- '.github/workflows/lighthouse-sdlc-propose.lock.yml',
22
- '.github/workflows/lighthouse-sdlc-implement.md',
23
- '.github/workflows/lighthouse-sdlc-implement.lock.yml',
24
- '.github/workflows/lighthouse-sdlc-integrate.md',
25
- '.github/workflows/lighthouse-sdlc-integrate.lock.yml',
26
- '.github/workflows/lighthouse-sdlc-review.md',
27
- '.github/workflows/lighthouse-sdlc-review.lock.yml',
28
- '.github/workflows/shared/lighthouse-sdlc-stage.md',
29
- ];
30
12
  const args = process.argv.slice(2);
31
13
  const target = args[0] ? resolve(args[0]) : null;
32
14
  const name = args.includes('--name') ? args[args.indexOf('--name') + 1] : null;
@@ -93,6 +75,7 @@ try {
93
75
 
94
76
  await copy(resolve(packageRoot, 'src/lighthouse.mjs'), resolve(stage, 'scripts/lighthouse.mjs'));
95
77
  await copy(resolve(packageRoot, 'src/aliases.mjs'), resolve(stage, 'scripts/aliases.mjs'));
78
+ await copy(resolve(packageRoot, 'src/github-profile.mjs'), resolve(stage, 'scripts/github-profile.mjs'));
96
79
  await copy(resolve(packageRoot, 'src/init-plan.mjs'), resolve(stage, 'scripts/init-plan.mjs'));
97
80
  await copy(resolve(packageRoot, 'src/guardrails.mjs'), resolve(stage, 'scripts/guardrails.mjs'));
98
81
  await copy(resolve(packageRoot, 'src/profiles.mjs'), resolve(stage, 'scripts/profiles.mjs'));
@@ -108,7 +91,7 @@ try {
108
91
  product: name, application: { shape: 'backend', stacks: ['dotnet'] }, profiles: ['dotnet-backend'],
109
92
  integrations: { productShape: null, arcShape: null },
110
93
  packages: { sdlc: { name: '@juangcarmona/lighthouse-sdlc', version: sdlcContract.version } },
111
- managed: ['LIGHTHOUSE.md', 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', ...sdlcWorkflowPaths, '.agents/skills/lighthouse-init/SKILL.md', '.lighthouse/upgrade.json', '.lighthouse/commands.json', 'docs/profiles/dotnet/README.md', 'docs/profiles/dotnet/adoption.md', 'docs/profiles/dotnet/architecture.md', 'docs/profiles/dotnet/guardrails.md', 'docs/profiles/dotnet/practices.md', 'docs/profiles/dotnet/testing-and-development.md', 'docs/profiles/dotnet/use-cases.md', '.agents/skills/lighthouse-dotnet/SKILL.md'],
94
+ managed: ['LIGHTHOUSE.md', 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.lighthouse/sdlc.json', '.agents/skills/lighthouse-init/SKILL.md', '.lighthouse/upgrade.json', '.lighthouse/commands.json', 'docs/profiles/dotnet/README.md', 'docs/profiles/dotnet/adoption.md', 'docs/profiles/dotnet/architecture.md', 'docs/profiles/dotnet/guardrails.md', 'docs/profiles/dotnet/practices.md', 'docs/profiles/dotnet/testing-and-development.md', 'docs/profiles/dotnet/use-cases.md', '.agents/skills/lighthouse-dotnet/SKILL.md'],
112
95
  repository: {
113
96
  layout: ['src', 'docs', 'infra', 'scripts'], publicSite: { enabled: true },
114
97
  conventions: { commits: 'conventional', branchPattern: '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$' },
@@ -125,14 +108,6 @@ try {
125
108
 
126
109
  const initialized = (await import('node:child_process')).spawnSync(process.execPath, [resolve(stage, 'scripts/lighthouse.mjs'), 'init'], { cwd: stage, encoding: 'utf8' });
127
110
  if (initialized.status !== 0) throw new Error(initialized.stderr || 'Projection generation failed.');
128
- await copy(resolve(sdlcPackageRoot, 'workflows'), resolve(stage, '.github/workflows'));
129
- const upgradePath = resolve(stage, '.lighthouse/upgrade.json');
130
- const upgrade = JSON.parse(await readFile(upgradePath, 'utf8'));
131
- for (const path of sdlcWorkflowPaths) {
132
- const content = await readFile(resolve(stage, path), 'utf8');
133
- upgrade.files[path] = { hash: createHash('sha256').update(content).digest('hex'), scope: 'sdlc' };
134
- }
135
- await writeFile(upgradePath, `${JSON.stringify(upgrade, null, 2)}\n`);
136
111
  if (existing) await rmdir(target);
137
112
  await rename(stage, target);
138
113
  process.stdout.write(`Created ${name} in ${target}. Run dotnet restore ${name}.slnx, then npm run verify.\n`);
@@ -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;
@@ -40,22 +41,6 @@ const upgradeStatePath = resolve(root, upgradeStateRelative);
40
41
  const layout = ['src', 'docs', 'infra', 'scripts'];
41
42
  const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: installedPackageVersion('@juangcarmona/lighthouse-sdlc', 'contract.json') ?? '0.1.0' };
42
43
  const uiPackage = { name: '@juangcarmona/lighthouse-ui', version: installedPackageVersion('@juangcarmona/lighthouse-ui', 'DESIGN.md') ?? '0.1.0' };
43
- const sdlcWorkflowPaths = Object.freeze([
44
- '.github/workflows/lighthouse-sdlc-router.yml',
45
- '.github/workflows/lighthouse-sdlc-triage.md',
46
- '.github/workflows/lighthouse-sdlc-triage.lock.yml',
47
- '.github/workflows/lighthouse-sdlc-refine.md',
48
- '.github/workflows/lighthouse-sdlc-refine.lock.yml',
49
- '.github/workflows/lighthouse-sdlc-propose.md',
50
- '.github/workflows/lighthouse-sdlc-propose.lock.yml',
51
- '.github/workflows/lighthouse-sdlc-implement.md',
52
- '.github/workflows/lighthouse-sdlc-implement.lock.yml',
53
- '.github/workflows/lighthouse-sdlc-integrate.md',
54
- '.github/workflows/lighthouse-sdlc-integrate.lock.yml',
55
- '.github/workflows/lighthouse-sdlc-review.md',
56
- '.github/workflows/lighthouse-sdlc-review.lock.yml',
57
- '.github/workflows/shared/lighthouse-sdlc-stage.md',
58
- ]);
59
44
  const managedPaths = [
60
45
  projection,
61
46
  'scripts/lighthouse-guard.mjs',
@@ -63,7 +48,6 @@ const managedPaths = [
63
48
  '.husky/pre-push',
64
49
  '.github/workflows/lighthouse.yml',
65
50
  '.lighthouse/sdlc.json',
66
- ...sdlcWorkflowPaths,
67
51
  '.agents/skills/lighthouse-init/SKILL.md',
68
52
  upgradeStateRelative,
69
53
  '.lighthouse/commands.json',
@@ -101,7 +85,7 @@ function managedPathsFor(manifest) {
101
85
  function upgradeScopesFor(manifest) {
102
86
  return {
103
87
  harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', '.agents/skills/lighthouse-init/SKILL.md', ...((manifest?.profiles ?? []).flatMap((profile) => profileManagedPaths[profile] ?? []))],
104
- sdlc: ['.lighthouse/sdlc.json', '.lighthouse/commands.json', ...sdlcWorkflowPaths],
88
+ sdlc: ['.lighthouse/sdlc.json', '.lighthouse/commands.json'],
105
89
  };
106
90
  }
107
91
 
@@ -110,18 +94,6 @@ function packageAsset(packageName, entry) {
110
94
  return readFileSync(path, 'utf8');
111
95
  }
112
96
 
113
- function sdlcWorkflowFiles() {
114
- const files = {};
115
- for (const target of sdlcWorkflowPaths) {
116
- try {
117
- files[target] = packageAsset('@juangcarmona/lighthouse-sdlc', `workflows/${target.slice('.github/workflows/'.length)}`);
118
- } catch {
119
- return {};
120
- }
121
- }
122
- return files;
123
- }
124
-
125
97
  function profileAssets(manifest) {
126
98
  const assets = {};
127
99
  if ((manifest?.profiles ?? []).includes('visual-theme')) {
@@ -271,7 +243,7 @@ function capabilityAdoption(previous = null) {
271
243
  ? `lighthouse capabilities select --id ${capability.id} --provider ${capability.provider}`
272
244
  : ['adopted', 'declined'].includes(status)
273
245
  ? null
274
- : `lighthouse capabilities plan --id ${capability.id} --provider ${capability.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${capability.id}.plan.json`;
246
+ : `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
247
  return [capability.id, {
276
248
  id: capability.id,
277
249
  status,
@@ -456,7 +428,6 @@ function managedFiles(manifest) {
456
428
  '.agents/skills/lighthouse-init/SKILL.md': readFileSync(new URL('../assets/skills/init/SKILL.md', import.meta.url), 'utf8'),
457
429
  };
458
430
  Object.assign(files, profileAssets(manifest));
459
- Object.assign(files, sdlcWorkflowFiles());
460
431
  const controlPlane = sdlcControlPlane(manifest);
461
432
  if (controlPlane) files['.lighthouse/sdlc.json'] = controlPlane;
462
433
  files['.lighthouse/commands.json'] = commandAliasProjectionText(commandAliasesProjection());
@@ -541,6 +512,10 @@ function classifyUpgradeFile(path, current, expected, recordedHash) {
541
512
  return { classification: 'conflicting', action: 'review', reason: 'ownership is not recorded for this path' };
542
513
  }
543
514
 
515
+ function isOptionalWorkflow(path) {
516
+ return /^\.github\/workflows\/lighthouse-sdlc-(router\.yml|(?:triage|refine|propose|implement|integrate|review)\.(?:md|lock\.yml))$/.test(path) || path === '.github/workflows/shared/lighthouse-sdlc-stage.md';
517
+ }
518
+
544
519
  async function planUpgrade(manifest, scopes) {
545
520
  const state = await readUpgradeState();
546
521
  const problems = stateProblems(state);
@@ -556,6 +531,9 @@ async function planUpgrade(manifest, scopes) {
556
531
  const result = classifyUpgradeFile(path, current, expected, recordedHash);
557
532
  changes.push({ path, scope: Object.entries(upgradeScopes).find(([, values]) => values.includes(path))?.[0], ...result });
558
533
  }
534
+ if (scopes.has('sdlc') && manifest.managed.some(isOptionalWorkflow)) {
535
+ changes.push({ path: 'lighthouse.json', scope: 'sdlc', classification: 'adoptable', action: 'update', reason: 'release optional workflow ownership; preserve all workflow files' });
536
+ }
559
537
  if (state?.baseline && state.baseline !== manifest.baseline) {
560
538
  changes.unshift({ path: 'baseline', scope: 'harness', classification: 'breaking', action: 'review', reason: `baseline ${state.baseline} is not compatible with ${manifest.baseline}` });
561
539
  }
@@ -687,7 +665,11 @@ function validate(manifest) {
687
665
  const preSemanticWithoutUpgrade = Array.isArray(manifest.managed)
688
666
  && manifest.managed.length === preSemanticWithoutUpgradePaths.length
689
667
  && manifest.managed.every((entry, index) => entry === preSemanticWithoutUpgradePaths[index]);
690
- if (!legacyManaged && !profilelessManaged && !previousCompleteManaged && !completeManaged && !preSemanticManaged && !preSemanticWithoutUpgrade) {
668
+ // Earlier foundations installed every workflow. Accept that ownership inventory during upgrade;
669
+ // the foundation now leaves those files to explicit workflow adoption and never deletes them.
670
+ const withoutOldWorkflows = Array.isArray(manifest.managed) ? manifest.managed.filter(path => !isOptionalWorkflow(path)) : [];
671
+ const previousWorkflowManaged = withoutOldWorkflows.length === expectedManagedPaths.length && withoutOldWorkflows.every((path, index) => path === expectedManagedPaths[index]);
672
+ if (!previousWorkflowManaged && !legacyManaged && !profilelessManaged && !previousCompleteManaged && !completeManaged && !preSemanticManaged && !preSemanticWithoutUpgrade) {
691
673
  problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the selected profile set`);
692
674
  }
693
675
  if ('repository' in manifest) {
@@ -989,7 +971,10 @@ async function applyUpgrade(manifest, plan) {
989
971
  for (const change of updates) {
990
972
  const target = resolve(root, change.path);
991
973
  await mkdir(dirname(target), { recursive: true });
992
- await writeTextAtomically(target, files[change.path]);
974
+ const content = change.path === 'lighthouse.json'
975
+ ? `${JSON.stringify({ ...manifest, managed: manifest.managed.filter(path => !isOptionalWorkflow(path)) }, null, 2)}\n`
976
+ : files[change.path];
977
+ await writeTextAtomically(target, content);
993
978
  }
994
979
  const next = upgradeState(manifest, files, { ...previous, lastAppliedMigration: `upgrade-${cliPackage.version}` });
995
980
  for (const path of Object.keys(next.files)) {
@@ -1408,7 +1393,7 @@ async function verify(manifest, format = 'text') {
1408
1393
  for (const item of adoption) {
1409
1394
  if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
1410
1395
  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`;
1396
+ 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
1397
  }
1413
1398
  if (adoption.length) {
1414
1399
  const pending = adoption.filter((item) => !['adopted', 'declined', 'available'].includes(item.status));
@@ -1421,13 +1406,16 @@ async function verify(manifest, format = 'text') {
1421
1406
  kind: 'hard',
1422
1407
  verification: 'deterministic',
1423
1408
  status: pending.length ? 'unconfigured' : available || declined ? 'not-applicable' : 'passed',
1424
- adoptionStatus: pending.length ? pending[0].status : available ? 'available' : declined ? 'declined' : 'adopted',
1409
+ adoptionStatus: pending.length ? pending[0].status : declined ? 'declined' : available ? 'available' : 'adopted',
1425
1410
  evidence: adoption.map((item) => ({ capability: item.id, catalogVersion: item.catalogVersion })),
1426
1411
  problems: pending.map((item) => `${item.id} is ${item.status}; run ${item.nextAction}`),
1427
1412
  nextAction: pending[0]?.nextAction ?? null,
1428
1413
  });
1429
1414
  report.summary.unconfigured = (report.summary.unconfigured ?? 0) + (pending.length ? 1 : 0);
1430
1415
  }
1416
+ const githubProfileResult = await localGithubProfileStatus(root);
1417
+ report.results.push(githubProfileResult);
1418
+ report.summary[githubProfileResult.status] = (report.summary[githubProfileResult.status] ?? 0) + 1;
1431
1419
  const evidenceResult = await verifyEvidenceIndex();
1432
1420
  report.results.push(evidenceResult);
1433
1421
  report.summary[evidenceResult.status] = (report.summary[evidenceResult.status] ?? 0) + 1;
@@ -1574,6 +1562,11 @@ async function capabilities(args) {
1574
1562
  const inputPath = args.includes('--input') ? args[args.indexOf('--input') + 1] : args.find((arg) => arg.startsWith('--input='))?.slice(8);
1575
1563
  const format = formatFromArgs(args);
1576
1564
  const planFile = args.includes('--plan-file') ? args[args.indexOf('--plan-file') + 1] : args.find((arg) => arg.startsWith('--plan-file='))?.slice(12);
1565
+ const isGithubPlan = action === 'apply' && planFile && JSON.parse(await readFile(resolve(root, planFile), 'utf8')).kind === 'github-profile';
1566
+ if ((id === 'github-profile' && ['plan', 'apply', 'verify'].includes(action)) || isGithubPlan) {
1567
+ if (provider !== 'github') throw new Error('The GitHub profile requires provider github.');
1568
+ return githubProfileCommand(args, { root, catalog: sdlcCapabilityCatalog() });
1569
+ }
1577
1570
  if (['select', 'decline'].includes(action)) {
1578
1571
  const catalog = sdlcCapabilityCatalog();
1579
1572
  if (!catalog) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before selecting capabilities.');
@@ -1590,7 +1583,7 @@ async function capabilities(args) {
1590
1583
  provider: capability.provider,
1591
1584
  version: capability.version,
1592
1585
  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`,
1586
+ 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
1587
  };
1595
1588
  await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
1596
1589
  const result = { schemaVersion: 1, capability: id, provider, status, applied: true };
@@ -1669,7 +1662,7 @@ async function capabilities(args) {
1669
1662
  if (format === 'json') console.log(JSON.stringify(result, null, 2)); else process.stdout.write(capabilityPlanText(result));
1670
1663
  return;
1671
1664
  }
1672
- throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply> ...');
1665
+ throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply|verify> ...');
1673
1666
  }
1674
1667
 
1675
1668
  function formatFromArgs(args) {
@@ -1689,7 +1682,7 @@ if (command === 'create-dotnet') {
1689
1682
  let invalidArgument = false;
1690
1683
  for (let index = 0; index < commandArgs.length; index += 1) {
1691
1684
  const arg = commandArgs[index];
1692
- if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
1685
+ if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply', 'verify'].includes(arg)) continue;
1693
1686
  if (arg === '--dry-run') continue;
1694
1687
  if (arg === '--apply') { if (!['init', 'authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
1695
1688
  if (arg === '--adopt-existing') { if (command !== 'authorities') invalidArgument = true; continue; }