@juangcarmona/lighthouse-sdlc 0.13.0 → 0.15.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.
Files changed (57) hide show
  1. package/README.md +65 -12
  2. package/capabilities.json +3 -0
  3. package/completion.md +6 -4
  4. package/execution-environments.md +43 -0
  5. package/model-profiles.md +101 -0
  6. package/package.json +9 -1
  7. package/request-triage.md +30 -0
  8. package/schemas/estimate.schema.json +23 -0
  9. package/schemas/execution-environment.schema.json +12 -0
  10. package/schemas/request-analysis.schema.json +37 -0
  11. package/schemas/triage.schema.json +1 -0
  12. package/skills/concise-results/SKILL.md +10 -0
  13. package/skills/implement/SKILL.md +9 -8
  14. package/skills/refine/SKILL.md +1 -1
  15. package/skills/triage/SKILL.md +3 -3
  16. package/src/actions-artifact.mjs +17 -0
  17. package/src/completion-actions.mjs +30 -10
  18. package/src/completion.mjs +20 -1
  19. package/src/delivery-events.mjs +42 -0
  20. package/src/execution-environment.mjs +78 -0
  21. package/src/host-contract.mjs +3 -0
  22. package/src/implementation-runtime.mjs +56 -32
  23. package/src/implementation.mjs +36 -19
  24. package/src/install-workflows.mjs +16 -7
  25. package/src/integration-evidence.mjs +25 -1
  26. package/src/issue-actions.mjs +315 -49
  27. package/src/model-profiles.mjs +58 -0
  28. package/src/opencode-execute.mjs +35 -12
  29. package/src/pr-state-actions.mjs +106 -0
  30. package/src/pr-state.mjs +75 -0
  31. package/src/process-publication.mjs +1 -1
  32. package/src/profile-delegation.mjs +111 -0
  33. package/src/proposal-review.mjs +91 -0
  34. package/src/proposal.mjs +22 -8
  35. package/src/request-triage.mjs +42 -0
  36. package/src/stage-dispatch.mjs +164 -0
  37. package/src/stage-transition.mjs +35 -0
  38. package/src/triage.mjs +4 -0
  39. package/src/worker-maintenance.mjs +83 -0
  40. package/src/worker-recovery.mjs +106 -0
  41. package/src/worker-reports.mjs +150 -0
  42. package/templates/change-request.yml +24 -0
  43. package/workflows/lighthouse-sdlc-apply-review.lock.yml +1335 -0
  44. package/workflows/lighthouse-sdlc-apply-review.md +206 -0
  45. package/workflows/lighthouse-sdlc-implement.lock.yml +51 -90
  46. package/workflows/lighthouse-sdlc-implement.md +40 -11
  47. package/workflows/lighthouse-sdlc-integrate.lock.yml +36 -83
  48. package/workflows/lighthouse-sdlc-integrate.md +25 -2
  49. package/workflows/lighthouse-sdlc-propose.lock.yml +44 -83
  50. package/workflows/lighthouse-sdlc-propose.md +33 -4
  51. package/workflows/lighthouse-sdlc-refine.lock.yml +45 -83
  52. package/workflows/lighthouse-sdlc-refine.md +37 -2
  53. package/workflows/lighthouse-sdlc-review.lock.yml +36 -83
  54. package/workflows/lighthouse-sdlc-review.md +25 -2
  55. package/workflows/lighthouse-sdlc-router.yml +104 -63
  56. package/workflows/lighthouse-sdlc-triage.md +174 -28
  57. package/workflows/lighthouse-worker-maintenance.yml +126 -0
@@ -0,0 +1,150 @@
1
+ import { digest } from './host-contract.mjs';
2
+ import { workers } from './stage-dispatch.mjs';
3
+
4
+ export const causes = Object.freeze({
5
+ 'worker-technical-failure': ['Technical execution failure', 'Inspect the failed step and retry budget; unresolved work needs an owner decision.'],
6
+ 'worker-stalled': ['Execution has stopped updating', 'Inspect the runner and execution; do not duplicate active work.'],
7
+ 'dispatch-unconfirmed': ['Dispatch confirmation is missing', 'Reconcile the recorded request with Actions before requesting more work.'],
8
+ 'execution-rerun': ['Execution was rerun outside recovery', 'Inspect all attempts and the original claim.'],
9
+ 'authority-changed': ['Source or owner intent changed', 'Request fresh work against the current source and intent.'],
10
+ 'manual-recovery': ['Outcome requires operator recovery', 'Inspect actual effects and make a new owner decision.'],
11
+ 'publication-uncertain': ['Publication effects are uncertain', 'Inspect the PR, issue and publication job before another request.'],
12
+ 'execution-time-unknown': ['Execution timestamp is unavailable', 'Inspect the Actions execution timestamps.'],
13
+ 'evidence-unavailable': ['Recovery evidence is unavailable', 'Inspect the maintenance run and original request artifacts.'],
14
+ });
15
+ const states = ['retryable', 'delayed', 'exhausted', 'uncertain', 'stalled', 'blocked', 'recovered'];
16
+ const positive = x => Number.isSafeInteger(Number(x)) && /^[1-9][0-9]*$/.test(String(x));
17
+ const hash = x => /^[a-f0-9]{64}$/.test(x ?? '');
18
+ export const reportSignature = (repository, worker, code) => digest(`${repository}:${worker}:${code}`);
19
+ const marker = 'lighthouse-worker-report';
20
+ const indexStart = '<!-- lighthouse-worker-report-index:start -->';
21
+ const indexEnd = '<!-- lighthouse-worker-report-index:end -->';
22
+
23
+ function parseMarker(body, name) {
24
+ if (typeof body !== 'string' || Buffer.byteLength(body) > 128000) throw Error('Report body exceeds bounds');
25
+ const values = [...body.matchAll(new RegExp(`<!-- ${name}:(\\{[^\\n]*\\}) -->`, 'g'))];
26
+ if (values.length !== 1) throw Error('Missing or ambiguous report marker');
27
+ return JSON.parse(values[0][1]);
28
+ }
29
+
30
+ function validateEntry(entry) {
31
+ if (!positive(entry.issue) || !hash(entry.rootId) || !Number.isSafeInteger(entry.attempt) || entry.attempt < 0 || entry.attempt > 2 || !states.includes(entry.state) || (entry.runId !== null && !positive(entry.runId)) || entry.key !== (entry.runId ? `run:${entry.runId}` : `request:${entry.rootId}`)) throw Error('Invalid report occurrence');
32
+ if (Object.keys(entry).sort().join(',') !== 'attempt,issue,key,rootId,runId,state') throw Error('Unexpected report occurrence fields');
33
+ }
34
+
35
+ export function readReport(issue, repository, worker, code) {
36
+ if (issue.pull_request || issue.user?.login !== 'github-actions[bot]' || issue.user.type !== 'Bot') throw Error('Report is not bot-owned');
37
+ const report = parseMarker(issue.body, marker);
38
+ if (report.schemaVersion !== 1 || report.worker !== worker || report.code !== code || report.signature !== reportSignature(repository, worker, code) || !Array.isArray(report.entries) || report.entries.length > 200 || Object.keys(report).sort().join(',') !== 'code,entries,schemaVersion,signature,worker') throw Error('Invalid report identity');
39
+ report.entries.forEach(validateEntry);
40
+ if (new Set(report.entries.map(e => e.key)).size !== report.entries.length) throw Error('Duplicate report occurrences');
41
+ return report;
42
+ }
43
+
44
+ function normalizedObservation(repository, observation) {
45
+ if (!positive(observation.issue)) throw Error('Invalid observed issue');
46
+ const worker = observation.worker ?? 'maintenance';
47
+ if (![...workers, 'maintenance'].includes(worker)) throw Error('Unknown observed worker');
48
+ const rootId = observation.rootId ?? digest(`${repository}:${observation.issue}:unavailable`);
49
+ if (!hash(rootId)) throw Error('Invalid observed request');
50
+ if (observation.state === 'succeeded') return { worker, rootId, succeeded: true };
51
+ if (!Object.hasOwn(causes, observation.code) || !states.includes(observation.state) || observation.state === 'recovered') return null;
52
+ const runId = observation.runId ?? observation.observedRunId ?? null;
53
+ const entry = { key: runId ? `run:${runId}` : `request:${rootId}`, rootId, issue: observation.issue, runId: runId === null ? null : String(runId), attempt: observation.attempt ?? 0, state: observation.state };
54
+ validateEntry(entry);
55
+ return { worker, code: observation.code, entry };
56
+ }
57
+
58
+ export function mergeObservations(repository, previous, observations) {
59
+ const entries = new Map(previous.entries.map(e => [e.key, { ...e }]));
60
+ const recoveredRoots = new Set(previous.entries.filter(e => e.state === 'recovered').map(e => e.rootId));
61
+ const normalized = observations.map(o => normalizedObservation(repository, o)).filter(Boolean);
62
+ for (const o of normalized) if (o.worker === previous.worker && o.code === previous.code) entries.set(o.entry.key, { ...o.entry, ...(recoveredRoots.has(o.entry.rootId) ? { state: 'recovered' } : {}) });
63
+ for (const o of normalized) if (o.succeeded && o.worker === previous.worker) for (const entry of entries.values()) if (entry.rootId === o.rootId) entry.state = 'recovered';
64
+ if (entries.size > 200) throw Error('Report exceeds 200 distinct observations; operator archival is required');
65
+ return { ...previous, entries: [...entries.values()].sort((a, b) => a.key.localeCompare(b.key)) };
66
+ }
67
+
68
+ export function reportBody(repository, report) {
69
+ const active = report.entries.filter(e => e.state !== 'recovered').length;
70
+ const rows = report.entries.map(e => `| [#${e.issue}](https://github.com/${repository}/issues/${e.issue}) | ${e.runId ? `[Execution](https://github.com/${repository}/actions/runs/${e.runId})` : 'Unconfirmed request'} | ${e.attempt}/2 | ${e.state} |`);
71
+ const body = `## ${active ? '❌ Attention required' : '✅ Recovery confirmed'}\n\nWorker: \`${report.worker}\`. Cause: ${causes[report.code][0]}.\n\n${report.entries.length} distinct observed occurrence(s); ${active} unresolved. Counts cover retained observations, not all repository history.\n\nNext action: ${active ? causes[report.code][1] : 'No action for these occurrences. A new failure reuses this report.'}\n\n| Work item | Evidence | Retry | State |\n| --- | --- | --- | --- |\n${rows.join('\n')}\n\n<!-- ${marker}:${JSON.stringify(report)} -->`;
72
+ if (Buffer.byteLength(body) > 60000) throw Error('Report body exceeds issue bounds; operator archival is required');
73
+ return body;
74
+ }
75
+
76
+ function readIndex(issue, owner) {
77
+ if (issue.pull_request || issue.user?.login !== owner || issue.state !== 'open') throw Error('Report index must be an open owner-authored issue');
78
+ const body = issue.body ?? '';
79
+ if (!body.includes(indexStart) && !body.includes(indexEnd) && !body.includes('<!-- lighthouse-worker-report-index:')) return { schemaVersion: 1, reports: {} };
80
+ if (body.split(indexStart).length !== 2 || body.split(indexEnd).length !== 2 || body.indexOf(indexStart) > body.indexOf(indexEnd)) throw Error('Ambiguous index block');
81
+ const index = parseMarker(body, 'lighthouse-worker-report-index');
82
+ if (index.schemaVersion !== 1 || !index.reports || typeof index.reports !== 'object' || Array.isArray(index.reports) || Object.keys(index.reports).length > 100) throw Error('Invalid report index');
83
+ return index;
84
+ }
85
+
86
+ async function writeIndex(api, issue, index) {
87
+ const current = await api(`issues/${issue.number}`);
88
+ if (current.body !== issue.body || current.state !== 'open') throw Error('Report index changed; no overwrite');
89
+ const links = Object.values(index.reports).map(r => r.issue ? `- #${r.issue}: ${r.worker} / ${r.code}` : `- Pending confirmation: ${r.worker} / ${r.code}`);
90
+ const block = `${indexStart}\n## Worker error reports\n\n${links.join('\n')}\n\n<!-- lighthouse-worker-report-index:${JSON.stringify(index)} -->\n${indexEnd}`;
91
+ const body = (issue.body ?? '').includes(indexStart) ? issue.body.slice(0, issue.body.indexOf(indexStart)) + block + issue.body.slice(issue.body.indexOf(indexEnd) + indexEnd.length) : `${issue.body ?? ''}\n\n${block}`;
92
+ if (Buffer.byteLength(body) > 60000) throw Error('Report index exceeds issue bounds');
93
+ const updated = await api(`issues/${issue.number}`, 'PATCH', { body });
94
+ return updated;
95
+ }
96
+
97
+ async function reportIssues(api) {
98
+ const all = [];
99
+ for (let page = 1; page <= 5; page++) {
100
+ const rows = await api(`issues?state=all&per_page=100&page=${page}`);
101
+ all.push(...rows.filter(i => !i.pull_request && i.body?.includes(`<!-- ${marker}:`)));
102
+ if (rows.length < 100) return all;
103
+ }
104
+ throw Error('Report reconciliation exceeds 500 issue/PR entries');
105
+ }
106
+
107
+ // Caller serializes all report publication with the repository-wide report job.
108
+ export async function publishReports(api, repository, owner, indexNumber, observations) {
109
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repository) || !positive(indexNumber) || !Array.isArray(observations) || observations.length > 1000) throw Error('Invalid report publication input');
110
+ let indexIssue = await api(`issues/${indexNumber}`);
111
+ const index = readIndex(indexIssue, owner), existing = await reportIssues(api), desired = new Map();
112
+ for (const observation of observations) {
113
+ const o = normalizedObservation(repository, observation);
114
+ if (o?.entry) desired.set(reportSignature(repository, o.worker, o.code), { worker: o.worker, code: o.code });
115
+ }
116
+ for (const [signature, row] of Object.entries(index.reports)) {
117
+ if (![...workers, 'maintenance'].includes(row.worker) || !Object.hasOwn(causes, row.code) || signature !== reportSignature(repository, row.worker, row.code) || row.issue !== null && !positive(row.issue)) throw Error('Invalid indexed report');
118
+ desired.set(signature, row);
119
+ }
120
+ if (desired.size > 100) throw Error('Report index exceeds 100 causes');
121
+ const results = [];
122
+ for (const [signature, definition] of desired) {
123
+ const { worker, code } = definition;
124
+ const matches = existing.filter(i => { try { return parseMarker(i.body, marker).signature === signature; } catch { return false; } });
125
+ if (matches.length > 1) throw Error('Ambiguous report identity; inspect duplicates');
126
+ let issue = index.reports[signature]?.issue ? await api(`issues/${index.reports[signature].issue}`) : matches[0];
127
+ if (issue) readReport(issue, repository, worker, code);
128
+ if (!issue && Object.hasOwn(index.reports, signature)) throw Error('Report creation is unconfirmed; inspect the index before any resend');
129
+ if (!issue) {
130
+ const report = mergeObservations(repository, { schemaVersion: 1, signature, worker, code, entries: [] }, observations);
131
+ const body = reportBody(repository, report);
132
+ index.reports[signature] = { worker, code, issue: null };
133
+ indexIssue = await writeIndex(api, indexIssue, index); // Durable intent precedes the non-idempotent POST.
134
+ issue = await api('issues', 'POST', { title: `Worker ${worker}: ${causes[code][0]}`, body });
135
+ readReport(issue, repository, worker, code);
136
+ existing.push(issue);
137
+ }
138
+ if (index.reports[signature]?.issue !== issue.number) {
139
+ index.reports[signature] = { worker, code, issue: issue.number };
140
+ indexIssue = await writeIndex(api, indexIssue, index);
141
+ }
142
+ const previous = readReport(issue, repository, worker, code), report = mergeObservations(repository, previous, observations);
143
+ const body = reportBody(repository, report), active = report.entries.some(e => e.state !== 'recovered');
144
+ const newlyFailed = report.entries.some(e => e.state !== 'recovered' && !previous.entries.some(p => p.key === e.key && p.state !== 'recovered'));
145
+ const state = !active ? 'closed' : newlyFailed ? 'open' : issue.state;
146
+ if (issue.body !== body || issue.state !== state) await api(`issues/${issue.number}`, 'PATCH', { body, state });
147
+ results.push({ number: issue.number, signature, count: report.entries.length, state });
148
+ }
149
+ return results;
150
+ }
@@ -0,0 +1,24 @@
1
+ name: Request a change
2
+ description: Tell us what is difficult and what would help.
3
+ body:
4
+ - type: textarea
5
+ id: problem
6
+ attributes:
7
+ label: What is difficult today?
8
+ description: Describe the problem and who it affects.
9
+ validations:
10
+ required: true
11
+ - type: textarea
12
+ id: result
13
+ attributes:
14
+ label: What result would help?
15
+ description: Describe what you would like to be different.
16
+ validations:
17
+ required: true
18
+ - type: textarea
19
+ id: context
20
+ attributes:
21
+ label: Examples or context (optional)
22
+ description: Add examples, links or screenshots if useful. No technical plan is needed.
23
+ validations:
24
+ required: false