@juangcarmona/lighthouse 0.3.1 → 0.5.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
@@ -4,13 +4,13 @@ This package is the core Lighthouse harness. It provides the `lighthouse` comman
4
4
 
5
5
  ## Initialize a repository
6
6
 
7
- Install the exact Lighthouse package artifact selected for the release and run `lighthouse init --name <product>` in a new repository. Lighthouse creates the manifest, the `src/`, `docs/`, `infra/` and `scripts/` boundaries, the repository guardrails and the generated `LIGHTHOUSE.md`. Inspect the plan and projection, then run `lighthouse verify` before adding project-specific policy or exceptions.
7
+ Install the exact Lighthouse package artifact selected for the release and run `lighthouse init --name <product>` in a new repository. Add `--profile visual-theme` when the project wants the Lighthouse UI contract. Lighthouse creates the manifest, the `src/`, `docs/`, `infra/` and `scripts/` boundaries, the repository guardrails and the generated `LIGHTHOUSE.md`. Inspect the plan and projection, then run `lighthouse verify` before adding project-specific policy or exceptions.
8
8
 
9
9
  Use `lighthouse reconcile` to preview missing Lighthouse-managed paths in an existing adoption. It does not write by default; add `--apply` only after reviewing the plan. Existing files whose content differs from the generated projection are conflicts and are never overwritten by reconciliation.
10
10
 
11
11
  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`.
12
12
 
13
- 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 and `DESIGN.md`. Stack profiles own their source templates, guidance and checks.
13
+ 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.
14
14
 
15
15
  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.
16
16
 
@@ -38,12 +38,14 @@ Initialization reports missing ProductShape and ArcShape authority inputs as ado
38
38
 
39
39
  ## Current command scope
40
40
 
41
- The command supports `init`, `authorities`, `reconcile`, `upgrade` and `verify`. It checks the manifest, repository boundaries, Conventional Commits metadata, branch naming, the selected SDLC package projection and generated content. Lifecycle stages, tracker operations, agent runs and CodeGraph checks require their own configured integrations.
41
+ The command supports `init`, `authorities`, `reconcile`, `upgrade`, `capabilities` and `verify`. It checks the manifest, repository boundaries, Conventional Commits metadata, branch naming, the selected SDLC package projection and generated content. Lifecycle stages, tracker operations, agent runs and CodeGraph checks require their own configured integrations.
42
42
 
43
43
  Initialization installs `.husky/commit-msg`, `.husky/pre-push`, `scripts/lighthouse-guard.mjs` and `.github/workflows/lighthouse.yml` when those paths are free. Existing files remain project-owned and are reported as conflicts. Reconciliation can add missing managed paths after adoption, but it does not overwrite a path that has drifted or belongs to another tool.
44
44
 
45
45
  The upgrade route records the ownership hashes in `.lighthouse/upgrade.json`. A missing or stale state file is an adoption gap; reconcile it first instead of allowing a full re-adoption. Upgrade plans report the source and target package and baseline, selected scopes, classifications, preserved paths, verification and recovery action.
46
46
 
47
+ The installed SDLC package also publishes a versioned capability catalog. `selected-by-default` capabilities appear as pending until the adopter selects or declines them; `opt-in` capabilities remain available without blocking `upgrade` or `verify`. `lighthouse capabilities select` and `lighthouse capabilities decline` record an explicit local choice, and `lighthouse verify --format json` exposes the adoption status and next command. For the GitHub labels slice, generate a local plan with `lighthouse capabilities plan --id labels --provider github --repo <owner/repo> --plan-file .lighthouse/reconciliation/labels.plan.json`, then apply it only with the plan hash returned by that command.
48
+
47
49
  The package does not contain the repository gallery, provider-owned integrations or reference source. Those remain with the source repository or the profile that owns them.
48
50
 
49
51
  ## Guardrail catalog
@@ -41,6 +41,16 @@
41
41
  "verification": "deterministic",
42
42
  "check": "branch",
43
43
  "failureHint": "Use the branch pattern declared in lighthouse.json."
44
+ },
45
+ {
46
+ "id": "visual.design-contract",
47
+ "owner": "@juangcarmona/lighthouse-ui",
48
+ "appliesTo": { "baseline": false, "profiles": ["visual-theme"] },
49
+ "level": "required",
50
+ "kind": "hard",
51
+ "verification": "deterministic",
52
+ "check": "ui-design",
53
+ "failureHint": "Run lighthouse reconcile after reviewing the visual-theme profile plan, then run lighthouse verify again."
44
54
  }
45
55
  ]
46
56
  }
@@ -12,12 +12,13 @@
12
12
  "product": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$"},
13
13
  "profiles": {"type": "array", "items": {"enum": ["node-tooling", "dotnet-backend", "visual-theme"]}, "uniqueItems": true},
14
14
  "integrations": {"type": "object", "additionalProperties": false, "required": ["productShape", "arcShape"], "properties": {"productShape": {"$ref": "#/$defs/pin"}, "arcShape": {"$ref": "#/$defs/pin"}}},
15
- "packages": {"type": "object", "additionalProperties": false, "required": ["sdlc"], "properties": {"sdlc": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-sdlc"}, "version": {"$ref": "#/$defs/pin"}}}}},
15
+ "packages": {"type": "object", "additionalProperties": false, "required": ["sdlc"], "properties": {"sdlc": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-sdlc"}, "version": {"$ref": "#/$defs/pin"}}}, "ui": {"type": "object", "additionalProperties": false, "required": ["name", "version"], "properties": {"name": {"const": "@juangcarmona/lighthouse-ui"}, "version": {"$ref": "#/$defs/pin"}}}}},
16
16
  "managed": {"anyOf": [
17
17
  {"const": ["LIGHTHOUSE.md"]},
18
18
  {"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml"]},
19
19
  {"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json"]},
20
- {"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json"]}
20
+ {"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json"]},
21
+ {"const": ["LIGHTHOUSE.md", "scripts/lighthouse-guard.mjs", ".husky/commit-msg", ".husky/pre-push", ".github/workflows/lighthouse.yml", ".lighthouse/sdlc.json", ".lighthouse/upgrade.json", "docs/design/DESIGN.md", ".agents/skills/lighthouse-ui/SKILL.md"]}
21
22
  ]},
22
23
  "repository": {
23
24
  "type": "object",
@@ -16,6 +16,24 @@
16
16
  },
17
17
  "baseline": {"type": "string", "minLength": 1},
18
18
  "capabilities": {"$ref": "#/$defs/versions"},
19
+ "adoption": {
20
+ "type": "object",
21
+ "additionalProperties": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": ["id", "status", "provider", "version", "catalogVersion", "nextAction"],
25
+ "properties": {
26
+ "id": {"type": "string"},
27
+ "status": {"enum": ["available", "pending-adoption", "adopted", "declined", "drifted", "blocked", "failed", "unavailable"]},
28
+ "selection": {"enum": ["selected-by-default", "opt-in"]},
29
+ "provider": {"type": "string"},
30
+ "version": {"type": "string"},
31
+ "catalogVersion": {"type": "string"},
32
+ "nextAction": {"type": ["string", "null"]},
33
+ "recordPath": {"type": "string"}
34
+ }
35
+ }
36
+ },
19
37
  "projections": {"$ref": "#/$defs/versions"},
20
38
  "lastAppliedMigration": {"type": "string", "minLength": 1},
21
39
  "files": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Lighthouse adoption and verification CLI",
6
6
  "repository": {
@@ -21,8 +21,9 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@arcshape/cli": "0.1.3",
24
+ "@juangcarmona/lighthouse-ui": "0.2.0",
24
25
  "@prodshape/cli": "0.22.0",
25
- "@juangcarmona/lighthouse-sdlc": "0.4.0"
26
+ "@juangcarmona/lighthouse-sdlc": "0.5.0"
26
27
  },
27
28
  "files": [
28
29
  "src/lighthouse.mjs",
@@ -7,6 +7,7 @@ const allowedLevels = new Set(['required', 'recommended', 'optional']);
7
7
  const allowedKinds = new Set(['hard', 'soft']);
8
8
  const allowedVerification = new Set(['deterministic', 'semantic', 'human']);
9
9
  const allowedStatuses = new Set(['passed', 'failed', 'drift', 'unavailable', 'unconfigured', 'pending-review', 'not-applicable']);
10
+ const requiredVisualTokens = ['--lh-canvas', '--lh-surface', '--lh-text', '--lh-action', '--lh-border', '--lh-focus'];
10
11
 
11
12
  function descriptorProblems(descriptor) {
12
13
  const problems = [];
@@ -102,16 +103,30 @@ async function runDeterministicCheck(descriptor, context) {
102
103
  const problems = context.validateGitConventions(context.repository);
103
104
  return result(descriptor, problems.length ? 'failed' : 'passed', [{ value: context.branch ?? '' }, { pattern: context.repository.conventions.branchPattern }], problems);
104
105
  }
106
+ if (descriptor.check === 'ui-design') {
107
+ const expected = ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'];
108
+ const missing = [];
109
+ const drifted = [];
110
+ for (const path of expected) {
111
+ const target = context.resolve(path);
112
+ if (!context.managedFiles[path] || !(await context.exists(target))) missing.push(path);
113
+ else if (await context.readFile(target, 'utf8') !== context.managedFiles[path]) drifted.push(`${path} has drifted; regenerate it from the visual-theme profile.`);
114
+ }
115
+ const tokenProblems = requiredVisualTokens.filter((token) => !context.uiTokens?.includes(token)).map((token) => `${token} is missing from the installed Lighthouse UI token source.`);
116
+ const problems = [...missing.map((path) => `${path} is missing; run lighthouse reconcile after reviewing the visual-theme profile plan.`), ...drifted, ...tokenProblems];
117
+ const status = drifted.length ? 'drift' : problems.length ? 'failed' : 'passed';
118
+ return result(descriptor, status, [...expected.map((path) => ({ path })), ...requiredVisualTokens.map((token) => ({ token }))], problems);
119
+ }
105
120
  return result(descriptor, 'unavailable', [], [`No deterministic checker is registered for ${descriptor.id}.`]);
106
121
  }
107
122
 
108
- export async function evaluateGuardrails({ catalog, manifest, manifestProblems, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch }) {
123
+ export async function evaluateGuardrails({ catalog, manifest, manifestProblems, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch, uiTokens }) {
109
124
  if (manifestProblems.length) {
110
125
  const descriptor = catalog.guardrails.find((item) => item.check === 'manifest') ?? catalog.guardrails[0];
111
126
  const failed = result(descriptor, 'failed', [{ path: 'lighthouse.json' }], manifestProblems.map((problem) => `Invalid lighthouse.json: ${problem}`));
112
127
  return report([failed]);
113
128
  }
114
- const context = { manifest, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch };
129
+ const context = { manifest, repository, managedFiles, resolve, exists, isDirectory, readFile, validateGitConventions, branch, uiTokens };
115
130
  const results = [];
116
131
  for (const descriptor of catalog.guardrails) {
117
132
  if (!appliesTo(descriptor, manifest)) {
@@ -6,6 +6,16 @@ import { createRequire } from 'node:module';
6
6
  import { basename, dirname, resolve } from 'node:path';
7
7
  import { execFileSync, spawnSync } from 'node:child_process';
8
8
  import { evaluateGuardrails, readGuardrailCatalog } from './guardrails.mjs';
9
+ let sdlcCapabilities;
10
+ let sdlcReconcile;
11
+ let sdlcGithub;
12
+ try {
13
+ [sdlcCapabilities, sdlcReconcile, sdlcGithub] = await Promise.all([
14
+ import('@juangcarmona/lighthouse-sdlc/capabilities'),
15
+ import('@juangcarmona/lighthouse-sdlc/reconcile'),
16
+ import('@juangcarmona/lighthouse-sdlc/github'),
17
+ ]);
18
+ } catch { /* Standalone generated starters can run without the optional SDLC package. */ }
9
19
 
10
20
  const root = process.cwd();
11
21
  const packageRequire = createRequire(import.meta.url);
@@ -22,6 +32,7 @@ const upgradeStateRelative = '.lighthouse/upgrade.json';
22
32
  const upgradeStatePath = resolve(root, upgradeStateRelative);
23
33
  const layout = ['src', 'docs', 'infra', 'scripts'];
24
34
  const sdlcPackage = { name: '@juangcarmona/lighthouse-sdlc', version: installedPackageVersion('@juangcarmona/lighthouse-sdlc', 'contract.json') ?? '0.1.0' };
35
+ const uiPackage = { name: '@juangcarmona/lighthouse-ui', version: installedPackageVersion('@juangcarmona/lighthouse-ui', 'DESIGN.md') ?? '0.1.0' };
25
36
  const managedPaths = [
26
37
  projection,
27
38
  'scripts/lighthouse-guard.mjs',
@@ -31,16 +42,16 @@ const managedPaths = [
31
42
  '.lighthouse/sdlc.json',
32
43
  upgradeStateRelative,
33
44
  ];
45
+ const profileManagedPaths = Object.freeze({
46
+ 'visual-theme': ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'],
47
+ });
34
48
  const previousManagedPaths = managedPaths.slice(0, -1);
35
49
  const legacyManagedPaths = [projection];
50
+ const supportedProfiles = ['node-tooling', 'dotnet-backend', 'visual-theme'];
36
51
  const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
37
52
  const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.3' };
38
- const upgradeScopes = {
39
- harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml'],
40
- sdlc: ['.lighthouse/sdlc.json'],
41
- };
42
53
  const upgradeSchema = './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-upgrade.schema.json';
43
- const usage = 'Usage: lighthouse <init|authorities|reconcile|upgrade|verify> [--name <product>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
54
+ const usage = 'Usage: lighthouse <init|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--profile <profile>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
44
55
 
45
56
  const defaultRepository = () => ({
46
57
  layout: [...layout],
@@ -48,16 +59,59 @@ const defaultRepository = () => ({
48
59
  conventions: { commits: 'conventional', branchPattern },
49
60
  });
50
61
 
51
- function defaultManifest(product) {
62
+ function managedPathsFor(manifest) {
63
+ const profiles = manifest?.profiles ?? [];
64
+ return [...managedPaths, ...profiles.flatMap((profile) => profileManagedPaths[profile] ?? [])];
65
+ }
66
+
67
+ function upgradeScopesFor(manifest) {
68
+ return {
69
+ harness: [projection, 'scripts/lighthouse-guard.mjs', '.husky/commit-msg', '.husky/pre-push', '.github/workflows/lighthouse.yml', ...((manifest?.profiles ?? []).flatMap((profile) => profileManagedPaths[profile] ?? []))],
70
+ sdlc: ['.lighthouse/sdlc.json'],
71
+ };
72
+ }
73
+
74
+ function packageAsset(packageName, entry) {
75
+ const path = packageRequire.resolve(`${packageName}/${entry}`);
76
+ return readFileSync(path, 'utf8');
77
+ }
78
+
79
+ function profileAssets(manifest) {
80
+ if (!(manifest?.profiles ?? []).includes('visual-theme')) return {};
81
+ try {
82
+ return {
83
+ 'docs/design/DESIGN.md': packageAsset('@juangcarmona/lighthouse-ui', 'DESIGN.md'),
84
+ '.agents/skills/lighthouse-ui/SKILL.md': packageAsset('@juangcarmona/lighthouse-ui', 'skills/ui-design/SKILL.md'),
85
+ };
86
+ } catch (error) {
87
+ throw new Error(`Cannot read the installed Lighthouse UI profile: ${error.message}`);
88
+ }
89
+ }
90
+
91
+ function profileArguments(args) {
92
+ const selected = [];
93
+ for (let index = 0; index < args.length; index += 1) {
94
+ if (args[index] !== '--profile' && !args[index].startsWith('--profile=')) continue;
95
+ const value = args[index] === '--profile' ? args[++index] : args[index].slice('--profile='.length);
96
+ if (!value || !supportedProfiles.includes(value)) throw new Error(`Unknown profile ${value ?? ''}; use ${supportedProfiles.join(', ')}.`);
97
+ if (!selected.includes(value)) selected.push(value);
98
+ }
99
+ return selected;
100
+ }
101
+
102
+ function defaultManifest(product, profiles = []) {
103
+ const draft = { profiles: [...profiles] };
104
+ const packages = { sdlc: { ...sdlcPackage } };
105
+ if (profiles.includes('visual-theme')) packages.ui = { ...uiPackage };
52
106
  return {
53
107
  $schema: './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-manifest.schema.json',
54
108
  schemaVersion: 1,
55
109
  baseline: '0.1.0',
56
110
  product,
57
- profiles: [],
111
+ profiles: [...profiles],
58
112
  integrations: { productShape: null, arcShape: null },
59
- packages: { sdlc: { ...sdlcPackage } },
60
- managed: [...managedPaths],
113
+ packages,
114
+ managed: managedPathsFor(draft),
61
115
  repository: defaultRepository(),
62
116
  };
63
117
  }
@@ -96,6 +150,45 @@ function sdlcContract() {
96
150
  }
97
151
  }
98
152
 
153
+ function sdlcCapabilityCatalog() {
154
+ if (!sdlcCapabilities) return null;
155
+ try {
156
+ const path = packageRequire.resolve('@juangcarmona/lighthouse-sdlc/capabilities.json');
157
+ const catalog = JSON.parse(readFileSync(path, 'utf8'));
158
+ const errors = sdlcCapabilities.validateCapabilityCatalog(catalog);
159
+ if (errors.length) throw new Error(errors.map((error) => error.message).join('; '));
160
+ return catalog;
161
+ } catch (error) {
162
+ throw new Error(`Cannot read the installed SDLC capability catalog: ${error.message}`);
163
+ }
164
+ }
165
+
166
+ function capabilityAdoption(previous = null) {
167
+ const catalog = sdlcCapabilityCatalog();
168
+ if (!catalog) return {};
169
+ return Object.fromEntries(catalog.capabilities.map((capability) => {
170
+ const current = previous?.adoption?.[capability.id];
171
+ const status = current?.catalogVersion === catalog.package.version
172
+ ? current.status
173
+ : sdlcCapabilities.initialAdoptionStatus?.(capability) ?? 'pending-adoption';
174
+ const nextAction = status === 'available'
175
+ ? `lighthouse capabilities select --id ${capability.id} --provider ${capability.provider}`
176
+ : ['adopted', 'declined'].includes(status)
177
+ ? null
178
+ : `lighthouse capabilities plan --id ${capability.id} --provider ${capability.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${capability.id}.plan.json`;
179
+ return [capability.id, {
180
+ id: capability.id,
181
+ status,
182
+ selection: capability.selection,
183
+ provider: capability.provider,
184
+ version: capability.version,
185
+ catalogVersion: catalog.package.version,
186
+ nextAction,
187
+ ...(current?.recordPath ? { recordPath: current.recordPath } : {}),
188
+ }];
189
+ }));
190
+ }
191
+
99
192
  function sdlcControlPlane(manifest) {
100
193
  const selected = normalizeSdlcPackage(manifest);
101
194
  if (!selected) return null;
@@ -118,7 +211,9 @@ function render(manifest) {
118
211
  const publicSite = repository.publicSite.enabled ? 'enabled' : 'disabled';
119
212
  const selectedSdlc = normalizeSdlcPackage(manifest);
120
213
  const sdlcLine = selectedSdlc ? `- SDLC package: \`${selectedSdlc.name}@${selectedSdlc.version}\`\n` : '';
121
- return `<!-- Generated by Lighthouse. Edit lighthouse.json and regenerate this file. -->\n\n# ${manifest.product} repository baseline\n\n- Baseline: \`${manifest.baseline}\`\n- Product: \`${manifest.product}\`\n- Profiles: ${manifest.profiles.length ? manifest.profiles.map((x) => `\`${x}\``).join(', ') : 'none'}\n${sdlcLine}- ProductShape: ${manifest.integrations.productShape ?? 'not adopted'}\n- ArcShape: ${manifest.integrations.arcShape ?? 'not adopted'}\n- Public site: \`${publicSite}\`\n- Repository roots: ${repository.layout.map((x) => `\`${x}/\``).join(', ')}\n- Commit convention: \`${repository.conventions.commits}\`\n\n## Delivery contract\n\nProductShape remains the source of product intent and Product Changes. ArcShape remains the source of architecture documentation and decisions. Lighthouse installs the repository baseline and verifies the selected integrations without replacing either authority.\n\nThe repository keeps source code in \`src/\`, documentation in \`docs/\`, infrastructure in \`infra/\` and repository automation in \`scripts/\`. A project may record a narrow, owned exception, but an initializer never overwrites an existing project file.\n\nThe selected SDLC package owns lifecycle skills, commands, meta-commands, routes and gates. The \`.lighthouse/\` control plane records the selected package and host adapters without storing agent runtime data.\n\nWhen the public site is enabled, its publication is allowed only after ProductShape and ArcShape validate successfully. A missing or invalid authority stops the build before deployment and reports the command that can repair the missing input.\n\nThis file is managed. Put repository-specific policy in its own documentation and keep this projection generated from \`lighthouse.json\`.\n`;
214
+ const selectedUi = manifest.packages?.ui;
215
+ const uiLine = selectedUi ? `- UI package: \`${selectedUi.name}@${selectedUi.version}\`\n` : '';
216
+ return `<!-- Generated by Lighthouse. Edit lighthouse.json and regenerate this file. -->\n\n# ${manifest.product} repository baseline\n\n- Baseline: \`${manifest.baseline}\`\n- Product: \`${manifest.product}\`\n- Profiles: ${manifest.profiles.length ? manifest.profiles.map((x) => `\`${x}\``).join(', ') : 'none'}\n${sdlcLine}${uiLine}- ProductShape: ${manifest.integrations.productShape ?? 'not adopted'}\n- ArcShape: ${manifest.integrations.arcShape ?? 'not adopted'}\n- Public site: \`${publicSite}\`\n- Repository roots: ${repository.layout.map((x) => `\`${x}/\``).join(', ')}\n- Commit convention: \`${repository.conventions.commits}\`\n\n## Delivery contract\n\nProductShape remains the source of product intent and Product Changes. ArcShape remains the source of architecture documentation and decisions. Lighthouse installs the repository baseline and verifies the selected integrations without replacing either authority.\n\nThe repository keeps source code in \`src/\`, documentation in \`docs/\`, infrastructure in \`infra/\` and repository automation in \`scripts/\`. A project may record a narrow, owned exception, but an initializer never overwrites an existing project file.\n\nThe selected SDLC package owns lifecycle skills, commands, meta-commands, routes and gates. The \`.lighthouse/\` control plane records the selected package and host adapters without storing agent runtime data.\n\nWhen the public site is enabled, its publication is allowed only after ProductShape and ArcShape validate successfully. A missing or invalid authority stops the build before deployment and reports the command that can repair the missing input.\n\nThis file is managed. Put repository-specific policy in its own documentation and keep this projection generated from \`lighthouse.json\`.\n`;
122
217
  }
123
218
 
124
219
  function guardrailScript(manifest) {
@@ -210,6 +305,7 @@ function managedFiles(manifest) {
210
305
  '.husky/pre-push': prePushHook(),
211
306
  '.github/workflows/lighthouse.yml': workflow(),
212
307
  };
308
+ Object.assign(files, profileAssets(manifest));
213
309
  const controlPlane = sdlcControlPlane(manifest);
214
310
  if (controlPlane) files['.lighthouse/sdlc.json'] = controlPlane;
215
311
  return files;
@@ -220,9 +316,10 @@ function contentHash(content) {
220
316
  }
221
317
 
222
318
  function upgradeState(manifest, files = managedFiles(manifest), previous = null) {
319
+ const scopes = upgradeScopesFor(manifest);
223
320
  const entries = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, {
224
321
  hash: contentHash(content),
225
- scope: Object.entries(upgradeScopes).find(([, paths]) => paths.includes(path))?.[0] ?? 'harness',
322
+ scope: Object.entries(scopes).find(([, paths]) => paths.includes(path))?.[0] ?? 'harness',
226
323
  }]));
227
324
  return {
228
325
  $schema: upgradeSchema,
@@ -230,6 +327,7 @@ function upgradeState(manifest, files = managedFiles(manifest), previous = null)
230
327
  package: { name: cliPackage.name, version: cliPackage.version },
231
328
  baseline: manifest.baseline,
232
329
  capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
330
+ adoption: capabilityAdoption(previous),
233
331
  projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
234
332
  lastAppliedMigration: previous?.lastAppliedMigration ?? 'foundation-init-1',
235
333
  files: entries,
@@ -259,7 +357,8 @@ function stateProblems(state) {
259
357
  return problems;
260
358
  }
261
359
 
262
- function scopesFromArgs(args) {
360
+ function scopesFromArgs(args, manifest) {
361
+ const upgradeScopes = upgradeScopesFor(manifest);
263
362
  const selected = new Set();
264
363
  for (let index = 0; index < args.length; index += 1) {
265
364
  if (args[index] !== '--scope' && !args[index].startsWith('--scope=')) continue;
@@ -275,7 +374,8 @@ function scopesFromArgs(args) {
275
374
  return selected.size ? selected : new Set(Object.keys(upgradeScopes));
276
375
  }
277
376
 
278
- function selectedPaths(scopes) {
377
+ function selectedPaths(scopes, manifest) {
378
+ const upgradeScopes = upgradeScopesFor(manifest);
279
379
  return new Set([...scopes].flatMap((scope) => upgradeScopes[scope]));
280
380
  }
281
381
 
@@ -293,7 +393,8 @@ async function planUpgrade(manifest, scopes) {
293
393
  const state = await readUpgradeState();
294
394
  const problems = stateProblems(state);
295
395
  const files = managedFiles(manifest);
296
- const paths = selectedPaths(scopes);
396
+ const paths = selectedPaths(scopes, manifest);
397
+ const upgradeScopes = upgradeScopesFor(manifest);
297
398
  const changes = [];
298
399
  for (const path of Object.keys(files).filter((candidate) => paths.has(candidate))) {
299
400
  const target = resolve(root, path);
@@ -311,6 +412,7 @@ async function planUpgrade(manifest, scopes) {
311
412
  package: state?.package ?? null,
312
413
  baseline: state?.baseline ?? null,
313
414
  capabilities: state?.capabilities ?? null,
415
+ adoption: state?.adoption ?? null,
314
416
  projections: state?.projections ?? null,
315
417
  lastAppliedMigration: state?.lastAppliedMigration ?? null,
316
418
  },
@@ -318,6 +420,7 @@ async function planUpgrade(manifest, scopes) {
318
420
  package: { name: cliPackage.name, version: cliPackage.version },
319
421
  baseline: manifest.baseline,
320
422
  capabilities: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
423
+ adoption: capabilityAdoption(state),
321
424
  projections: { harness: '1', sdlc: files['.lighthouse/sdlc.json'] ? '1' : null },
322
425
  },
323
426
  scopes: [...scopes],
@@ -325,6 +428,11 @@ async function planUpgrade(manifest, scopes) {
325
428
  changes,
326
429
  verification: ['lighthouse verify'],
327
430
  recovery: 'Restore the pre-update commit or rerun lighthouse upgrade after resolving the reported ownership conflict.',
431
+ adoption: {
432
+ status: Object.values(capabilityAdoption(state)).some((item) => !['adopted', 'declined', 'available'].includes(item.status)) ? 'pending' : 'complete',
433
+ capabilities: capabilityAdoption(state),
434
+ externalWrites: 'none',
435
+ },
328
436
  problems,
329
437
  };
330
438
  }
@@ -336,6 +444,12 @@ function printUpgradePlan(plan) {
336
444
  console.log(` scopes ${plan.scopes.join(', ')}`);
337
445
  for (const prerequisite of plan.prerequisites) console.log(` prerequisite ${prerequisite.status}: ${prerequisite.message}`);
338
446
  for (const change of plan.changes) console.log(` ${change.classification} ${change.action} ${change.path}${change.reason ? ` (${change.reason})` : ''}`);
447
+ if (plan.adoption) {
448
+ console.log(` capability adoption ${plan.adoption.status} (external writes: ${plan.adoption.externalWrites})`);
449
+ for (const capability of Object.values(plan.adoption.capabilities)) {
450
+ console.log(` capability ${capability.id ?? capability.provider} ${capability.status}${capability.nextAction ? `; next: ${capability.nextAction}` : ''}`);
451
+ }
452
+ }
339
453
  if (plan.problems.length) for (const problem of plan.problems) console.error(` adoption gap ${problem}`);
340
454
  console.log(` verify ${plan.verification[0]}`);
341
455
  console.log(` recovery ${plan.recovery}`);
@@ -355,7 +469,7 @@ function validate(manifest) {
355
469
  if (typeof manifest.product !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(manifest.product)) {
356
470
  problems.push('product must be a readable name of 1–64 characters');
357
471
  }
358
- const profiles = ['node-tooling', 'dotnet-backend', 'visual-theme'];
472
+ const profiles = supportedProfiles;
359
473
  if (!Array.isArray(manifest.profiles) || manifest.profiles.some((p) => !profiles.includes(p))
360
474
  || new Set(manifest.profiles).size !== manifest.profiles.length) {
361
475
  problems.push('profiles must be unique supported profile names');
@@ -372,26 +486,42 @@ function validate(manifest) {
372
486
  if ('packages' in manifest) {
373
487
  const packages = manifest.packages;
374
488
  const selected = packages?.sdlc;
489
+ const selectedUi = packages?.ui;
490
+ const hasVisualProfile = manifest.profiles?.includes('visual-theme');
375
491
  if (!packages || typeof packages !== 'object' || Array.isArray(packages)
376
- || Object.keys(packages).length !== 1
492
+ || !Object.keys(packages).every((key) => ['sdlc', 'ui'].includes(key))
493
+ || Object.keys(packages).length < 1 || Object.keys(packages).length > 2
377
494
  || !selected || typeof selected !== 'object' || Array.isArray(selected)
378
495
  || Object.keys(selected).length !== 2
379
496
  || selected.name !== sdlcPackage.name
380
497
  || typeof selected.version !== 'string' || !pin.test(selected.version)) {
381
498
  problems.push(`packages.sdlc must declare ${sdlcPackage.name} and an exact version or commit hash`);
382
499
  }
500
+ if (hasVisualProfile && (!selectedUi || typeof selectedUi !== 'object' || Array.isArray(selectedUi)
501
+ || Object.keys(selectedUi).length !== 2
502
+ || selectedUi.name !== uiPackage.name
503
+ || typeof selectedUi.version !== 'string' || !pin.test(selectedUi.version))) {
504
+ problems.push(`packages.ui must declare ${uiPackage.name} and an exact version or commit hash when visual-theme is selected`);
505
+ }
506
+ if (!hasVisualProfile && selectedUi) problems.push('packages.ui requires the visual-theme profile');
507
+ } else if (manifest.profiles?.includes('visual-theme')) {
508
+ problems.push(`packages must declare ${sdlcPackage.name} and ${uiPackage.name} when visual-theme is selected`);
383
509
  }
384
510
  const legacyManaged = Array.isArray(manifest.managed)
385
511
  && manifest.managed.length === legacyManagedPaths.length
386
512
  && manifest.managed.every((entry, index) => entry === legacyManagedPaths[index]);
513
+ const expectedManagedPaths = managedPathsFor(manifest);
387
514
  const completeManaged = Array.isArray(manifest.managed)
515
+ && manifest.managed.length === expectedManagedPaths.length
516
+ && manifest.managed.every((entry, index) => entry === expectedManagedPaths[index]);
517
+ const profilelessManaged = Array.isArray(manifest.managed)
388
518
  && manifest.managed.length === managedPaths.length
389
519
  && manifest.managed.every((entry, index) => entry === managedPaths[index]);
390
520
  const previousCompleteManaged = Array.isArray(manifest.managed)
391
521
  && manifest.managed.length === previousManagedPaths.length
392
522
  && manifest.managed.every((entry, index) => entry === previousManagedPaths[index]);
393
- if (!legacyManaged && !previousCompleteManaged && !completeManaged) {
394
- problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the current complete set`);
523
+ if (!legacyManaged && !profilelessManaged && !previousCompleteManaged && !completeManaged) {
524
+ problems.push(`managed must be ${legacyManagedPaths.join(', ')}, the previous Lighthouse guardrail set or the selected profile set`);
395
525
  }
396
526
  if ('repository' in manifest) {
397
527
  const repository = manifest.repository;
@@ -500,7 +630,12 @@ function printReconciliationPlan(plan) {
500
630
  async function initialize(args) {
501
631
  let manifest = await readManifest();
502
632
  const manifestMissing = !manifest;
503
- if (!manifest) manifest = defaultManifest(productNameFromArgs(args));
633
+ const selectedProfiles = profileArguments(args);
634
+ if (!manifest) manifest = defaultManifest(productNameFromArgs(args), selectedProfiles);
635
+ else if (selectedProfiles.some((profile) => !(manifest.profiles ?? []).includes(profile))) {
636
+ const missing = selectedProfiles.filter((profile) => !(manifest.profiles ?? []).includes(profile));
637
+ throw new Error(`The existing lighthouse.json does not select ${missing.join(', ')}; edit its profiles and rerun lighthouse init.`);
638
+ }
504
639
  const problems = validate(manifest);
505
640
  if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
506
641
  const plan = await planInitialization(manifest, manifestMissing);
@@ -619,7 +754,7 @@ async function upgrade(args) {
619
754
  if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
620
755
  const problems = validate(manifest);
621
756
  if (problems.length) throw new Error(problems.map((problem) => `Invalid lighthouse.json: ${problem}`).join('\n'));
622
- const plan = await planUpgrade(manifest, scopesFromArgs(args));
757
+ const plan = await planUpgrade(manifest, scopesFromArgs(args, manifest));
623
758
  const format = formatFromArgs(args);
624
759
  const apply = args.includes('--apply');
625
760
  const dryRun = args.includes('--dry-run') || !apply;
@@ -786,7 +921,32 @@ async function verify(manifest, format = 'text') {
786
921
  readFile,
787
922
  validateGitConventions,
788
923
  branch: gitBranch(),
924
+ uiTokens: manifest?.profiles?.includes('visual-theme') ? packageAsset('@juangcarmona/lighthouse-ui', 'tokens.css') : null,
789
925
  });
926
+ const adoption = Object.values(capabilityAdoption(await readUpgradeState()));
927
+ for (const item of adoption) {
928
+ if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
929
+ item.status = 'failed';
930
+ item.nextAction = `lighthouse capabilities plan --id ${item.id} --provider ${item.provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${item.id}.plan.json`;
931
+ }
932
+ if (adoption.length) {
933
+ const pending = adoption.filter((item) => !['adopted', 'declined', 'available'].includes(item.status));
934
+ const available = adoption.some((item) => item.status === 'available');
935
+ const declined = adoption.some((item) => item.status === 'declined');
936
+ report.results.push({
937
+ id: 'foundation.capability-adoption',
938
+ owner: '@juangcarmona/lighthouse-sdlc',
939
+ level: 'recommended',
940
+ kind: 'hard',
941
+ verification: 'deterministic',
942
+ status: pending.length ? 'unconfigured' : available || declined ? 'not-applicable' : 'passed',
943
+ adoptionStatus: pending.length ? pending[0].status : available ? 'available' : declined ? 'declined' : 'adopted',
944
+ evidence: adoption.map((item) => ({ capability: item.id, catalogVersion: item.catalogVersion })),
945
+ problems: pending.map((item) => `${item.id} is ${item.status}; run ${item.nextAction}`),
946
+ nextAction: pending[0]?.nextAction ?? null,
947
+ });
948
+ report.summary.unconfigured = (report.summary.unconfigured ?? 0) + (pending.length ? 1 : 0);
949
+ }
790
950
  if (format === 'json') {
791
951
  console.log(JSON.stringify(report, null, 2));
792
952
  if (report.problems.length) process.exitCode = 1;
@@ -796,6 +956,158 @@ async function verify(manifest, format = 'text') {
796
956
  console.log('Lighthouse manifest, repository boundaries and managed projection are valid.');
797
957
  }
798
958
 
959
+ function planHash(plan) {
960
+ const { authorization, ...payload } = plan;
961
+ return contentHash(JSON.stringify(payload));
962
+ }
963
+
964
+ function capabilityPlanText(plan) {
965
+ const lines = [
966
+ `Capability adoption: ${plan.status}`,
967
+ `Capability: ${plan.capability.id}@${plan.capability.version} (${plan.capability.provider})`,
968
+ `Repository: ${plan.repository.repo}`,
969
+ `Remote writes: none`,
970
+ `Authorization: ${plan.authorization.planHash}`,
971
+ ];
972
+ for (const change of plan.reconciliation.changes) lines.push(` ${change.status} ${change.name}`);
973
+ if (plan.nextAction) lines.push(`Next: ${plan.nextAction}`);
974
+ return `${lines.join('\n')}\n`;
975
+ }
976
+
977
+ async function writeCapabilityPlan(path, plan) {
978
+ const target = resolve(root, path);
979
+ await mkdir(dirname(target), { recursive: true });
980
+ await writeTextAtomically(target, `${JSON.stringify(plan, null, 2)}\n`);
981
+ }
982
+
983
+ async function writeCapabilityEvidence(plan, status, extra = {}) {
984
+ const path = resolve(root, '.lighthouse/reconciliation', `${plan.capability.id}.json`);
985
+ await mkdir(dirname(path), { recursive: true });
986
+ const evidence = {
987
+ schemaVersion: 1,
988
+ capability: plan.capability,
989
+ catalogVersion: plan.catalogVersion,
990
+ provider: plan.repository,
991
+ desiredHash: plan.reconciliation.desiredHash,
992
+ observedHash: extra.observedHash ?? plan.reconciliation.observedHash,
993
+ targetHash: extra.targetHash ?? plan.reconciliation.targetHash,
994
+ operations: extra.operations ?? plan.reconciliation.operations,
995
+ operationPolicy: plan.reconciliation.operationPolicy ?? plan.operationPolicy ?? null,
996
+ blockedOperations: extra.blockedOperations ?? plan.reconciliation.blockedOperations ?? [],
997
+ conflicts: extra.conflicts ?? plan.reconciliation.changes.filter((change) => change.status === 'conflict'),
998
+ status,
999
+ verified: status === 'adopted',
1000
+ };
1001
+ await writeTextAtomically(path, `${JSON.stringify(evidence, null, 2)}\n`);
1002
+ return path;
1003
+ }
1004
+
1005
+ async function capabilities(args) {
1006
+ const action = args[0];
1007
+ const id = args.includes('--id') ? args[args.indexOf('--id') + 1] : args.find((arg) => arg.startsWith('--id='))?.slice(5) ?? 'labels';
1008
+ const provider = args.includes('--provider') ? args[args.indexOf('--provider') + 1] : args.find((arg) => arg.startsWith('--provider='))?.slice(11) ?? 'github';
1009
+ const repo = args.includes('--repo') ? args[args.indexOf('--repo') + 1] : args.find((arg) => arg.startsWith('--repo='))?.slice(7);
1010
+ const inputPath = args.includes('--input') ? args[args.indexOf('--input') + 1] : args.find((arg) => arg.startsWith('--input='))?.slice(8);
1011
+ const format = formatFromArgs(args);
1012
+ const planFile = args.includes('--plan-file') ? args[args.indexOf('--plan-file') + 1] : args.find((arg) => arg.startsWith('--plan-file='))?.slice(12);
1013
+ if (['select', 'decline'].includes(action)) {
1014
+ const catalog = sdlcCapabilityCatalog();
1015
+ if (!catalog) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before selecting capabilities.');
1016
+ const capability = catalog.capabilities.find((item) => item.id === id && item.provider === provider);
1017
+ if (!capability) throw new Error(`Capability ${id} for provider ${provider} is not declared by the installed SDLC package.`);
1018
+ const state = await readUpgradeState();
1019
+ if (!state) throw new Error('Cannot select a capability before Lighthouse ownership evidence exists; run lighthouse init or reconcile --apply.');
1020
+ state.adoption = state.adoption ?? {};
1021
+ const status = action === 'decline' ? 'declined' : 'pending-adoption';
1022
+ state.adoption[id] = {
1023
+ id,
1024
+ status,
1025
+ selection: capability.selection,
1026
+ provider: capability.provider,
1027
+ version: capability.version,
1028
+ catalogVersion: catalog.package.version,
1029
+ nextAction: status === 'declined' ? null : `lighthouse capabilities plan --id ${id} --provider ${provider} --repo <owner/repo> --plan-file .lighthouse/reconciliation/${id}.plan.json`,
1030
+ };
1031
+ await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
1032
+ const result = { schemaVersion: 1, capability: id, provider, status, applied: true };
1033
+ if (format === 'json') console.log(JSON.stringify(result, null, 2)); else console.log(`Capability ${id}: ${status}.`);
1034
+ return;
1035
+ }
1036
+ if (action === 'plan') {
1037
+ if (!repo || !planFile) throw new Error('Usage: lighthouse capabilities plan --id <capability> --provider github --repo <owner/repo> --plan-file <path> [--input <capabilities.json>] [--format <text|json>]');
1038
+ const catalog = sdlcCapabilityCatalog();
1039
+ if (!catalog || !sdlcCapabilities || !sdlcReconcile || !sdlcGithub) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before planning adoption.');
1040
+ const capability = catalog.capabilities.find((item) => item.id === id && item.provider === provider);
1041
+ if (!capability) throw new Error(`Capability ${id} for provider ${provider} is not declared by the installed SDLC package.`);
1042
+ const declaration = inputPath ? JSON.parse(await readFile(resolve(root, inputPath), 'utf8')) : sdlcCapabilities.declarationFromCatalog(catalog, id);
1043
+ const labels = await sdlcGithub.readGithubLabels(repo);
1044
+ const reconciliation = sdlcReconcile.buildLabelReconciliationPlan(declaration, labels, await (async () => {
1045
+ const path = resolve(root, '.lighthouse/reconciliation', `${id}.json`);
1046
+ try { return JSON.parse(await readFile(path, 'utf8')); } catch { return null; }
1047
+ })());
1048
+ const plan = {
1049
+ schemaVersion: 1,
1050
+ status: reconciliation.status,
1051
+ capability: { id: capability.id, provider: capability.provider, selection: capability.selection, version: capability.version },
1052
+ catalogVersion: catalog.package.version,
1053
+ operationPolicy: capability.operations,
1054
+ repository: { provider, repo },
1055
+ declaration,
1056
+ reconciliation,
1057
+ authorization: { required: true, planHash: '' },
1058
+ nextAction: reconciliation.canApply ? `lighthouse capabilities apply --plan-file ${planFile} --authorize <plan-hash>` : 'Resolve the reported conflicts before authorizing this plan.',
1059
+ };
1060
+ plan.authorization.planHash = planHash(plan);
1061
+ await writeCapabilityPlan(planFile, plan);
1062
+ if (format === 'json') console.log(JSON.stringify(plan, null, 2)); else process.stdout.write(capabilityPlanText(plan));
1063
+ if (!reconciliation.valid || !reconciliation.canApply) process.exitCode = 1;
1064
+ return;
1065
+ }
1066
+ if (action === 'apply') {
1067
+ const authorization = args.includes('--authorize') ? args[args.indexOf('--authorize') + 1] : args.find((arg) => arg.startsWith('--authorize='))?.slice(12);
1068
+ if (!planFile || !authorization) throw new Error('Usage: lighthouse capabilities apply --plan-file <path> --authorize <plan-hash> [--format <text|json>]');
1069
+ const plan = JSON.parse(await readFile(resolve(root, planFile), 'utf8'));
1070
+ if (authorization !== plan.authorization?.planHash || planHash(plan) !== authorization) throw new Error('Capability plan authorization does not match the plan file; generate a new plan.');
1071
+ if (!plan.reconciliation?.canApply) throw new Error('Capability plan is blocked; resolve its conflicts and generate a new plan.');
1072
+ if (!sdlcReconcile || !sdlcGithub) throw new Error('The installed SDLC package does not publish the capability adoption contract; upgrade it before applying adoption.');
1073
+ const current = await sdlcGithub.readGithubLabels(plan.repository.repo);
1074
+ let reconciliation = sdlcReconcile.buildLabelReconciliationPlan(plan.declaration, current, plan.reconciliation.record);
1075
+ const originalOperations = new Set(plan.reconciliation.operations.map((operation) => operation.name));
1076
+ const plannedTarget = new Map((plan.reconciliation.target ?? []).map((label) => [label.name, JSON.stringify(label)]));
1077
+ const outsidePlanDrift = current.some((label) => !plannedTarget.has(label.name) || (plannedTarget.has(label.name) && !originalOperations.has(label.name) && JSON.stringify(label) !== plannedTarget.get(label.name)));
1078
+ if (reconciliation.observedHash !== plan.reconciliation.observedHash && (outsidePlanDrift || reconciliation.operations.some((operation) => !originalOperations.has(operation.name)))) {
1079
+ throw new Error('GitHub labels changed outside this plan; refusing to apply. Generate a new plan.');
1080
+ }
1081
+ const completed = [];
1082
+ try {
1083
+ for (const operation of reconciliation.operations) {
1084
+ await sdlcGithub.createGithubLabel(plan.repository.repo, operation, { dryRun: false });
1085
+ completed.push(operation);
1086
+ }
1087
+ } catch (error) {
1088
+ await writeCapabilityEvidence(plan, 'failed', { operations: completed, observedHash: reconciliation.observedHash });
1089
+ throw new Error(`Capability adoption stopped after ${completed.length} operation(s): ${error.message}`);
1090
+ }
1091
+ const finalLabels = await sdlcGithub.readGithubLabels(plan.repository.repo);
1092
+ const final = sdlcReconcile.buildLabelReconciliationPlan(plan.declaration, finalLabels, plan.reconciliation.record);
1093
+ if (!final.valid || final.operations.length || final.changes.some((change) => change.status === 'conflict')) {
1094
+ await writeCapabilityEvidence(plan, 'blocked', { operations: completed, observedHash: final.observedHash, targetHash: final.targetHash, conflicts: final.changes.filter((change) => change.status === 'conflict') });
1095
+ throw new Error('Capability adoption did not reach the declared state; inspect the evidence and generate a new plan.');
1096
+ }
1097
+ const evidencePath = await writeCapabilityEvidence(plan, 'adopted', { operations: completed, observedHash: final.observedHash, targetHash: final.targetHash });
1098
+ const state = await readUpgradeState();
1099
+ if (state) {
1100
+ state.adoption = state.adoption ?? {};
1101
+ state.adoption[id] = { id, status: 'adopted', selection: plan.capability.selection, provider: plan.capability.provider, version: plan.capability.version, catalogVersion: plan.catalogVersion, nextAction: null, recordPath: '.lighthouse/reconciliation/labels.json' };
1102
+ await writeTextAtomically(upgradeStatePath, `${JSON.stringify(state, null, 2)}\n`);
1103
+ }
1104
+ const result = { ...plan, status: 'adopted', evidencePath, reconciliation: final, applied: true };
1105
+ if (format === 'json') console.log(JSON.stringify(result, null, 2)); else process.stdout.write(capabilityPlanText(result));
1106
+ return;
1107
+ }
1108
+ throw new Error('Usage: lighthouse capabilities <select|decline|plan|apply> ...');
1109
+ }
1110
+
799
1111
  function formatFromArgs(args) {
800
1112
  const index = args.indexOf('--format');
801
1113
  if (index !== -1) return args[index + 1];
@@ -808,17 +1120,27 @@ const commandArgs = args.slice(1);
808
1120
  let invalidArgument = false;
809
1121
  for (let index = 0; index < commandArgs.length; index += 1) {
810
1122
  const arg = commandArgs[index];
1123
+ if (command === 'capabilities' && index === 0 && ['select', 'decline', 'plan', 'apply'].includes(arg)) continue;
811
1124
  if (arg === '--dry-run') continue;
812
1125
  if (arg === '--apply') { if (!['authorities', 'reconcile', 'upgrade'].includes(command)) invalidArgument = true; continue; }
813
1126
  if (arg === '--name') { if (!commandArgs[index + 1]) invalidArgument = true; else index += 1; continue; }
814
1127
  if (arg.startsWith('--name=') && arg.length > '--name='.length) continue;
1128
+ if (arg === '--profile') {
1129
+ if (command !== 'init' || !commandArgs[index + 1]) invalidArgument = true;
1130
+ else index += 1;
1131
+ continue;
1132
+ }
1133
+ if (arg.startsWith('--profile=') && arg.length > '--profile='.length) {
1134
+ if (command !== 'init') invalidArgument = true;
1135
+ continue;
1136
+ }
815
1137
  if (arg === '--format') {
816
- if (!['verify', 'upgrade'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
1138
+ if (!['verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
817
1139
  else index += 1;
818
1140
  continue;
819
1141
  }
820
1142
  if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
821
- if (!['verify', 'upgrade'].includes(command)) invalidArgument = true;
1143
+ if (!['verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
822
1144
  continue;
823
1145
  }
824
1146
  if (arg === '--scope') {
@@ -830,9 +1152,15 @@ for (let index = 0; index < commandArgs.length; index += 1) {
830
1152
  if (command !== 'upgrade') invalidArgument = true;
831
1153
  continue;
832
1154
  }
1155
+ if (['--id', '--provider', '--repo', '--input', '--plan-file', '--authorize'].includes(arg)) {
1156
+ if (!commandArgs[index + 1]) invalidArgument = true;
1157
+ else index += 1;
1158
+ continue;
1159
+ }
1160
+ if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) continue;
833
1161
  invalidArgument = true;
834
1162
  }
835
- if (!['init', 'authorities', 'reconcile', 'upgrade', 'verify'].includes(command) || invalidArgument) {
1163
+ if (!['init', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
836
1164
  console.error(usage);
837
1165
  process.exit(2);
838
1166
  }
@@ -841,6 +1169,7 @@ try {
841
1169
  else if (command === 'authorities') await authorities(args.slice(1));
842
1170
  else if (command === 'reconcile') await reconcile(args.slice(1));
843
1171
  else if (command === 'upgrade') await upgrade(args.slice(1));
1172
+ else if (command === 'capabilities') await capabilities(args.slice(1));
844
1173
  else {
845
1174
  const manifest = await readManifest();
846
1175
  if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
@@ -0,0 +1,7 @@
1
+ # Lighthouse 0.4.0
2
+
3
+ This release discovers the installed SDLC capability catalog during `lighthouse upgrade` and records pending adoption in `.lighthouse/upgrade.json`.
4
+
5
+ The upgrade remains local and dry-run by default. Review the plan, generate a provider plan with `lighthouse capabilities plan`, then authorize its application explicitly with the returned plan hash.
6
+
7
+ No package installation, local upgrade or implicit hook writes to GitHub.