@juangcarmona/lighthouse-sdlc 0.14.0 → 0.15.1

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 (46) hide show
  1. package/README.md +48 -125
  2. package/capabilities.json +14 -8
  3. package/completion.md +2 -2
  4. package/github-actions.md +104 -0
  5. package/host-contract.md +1 -1
  6. package/model-profiles.md +101 -0
  7. package/package.json +9 -1
  8. package/request-triage.md +32 -0
  9. package/schemas/estimate.schema.json +23 -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/refine/SKILL.md +1 -1
  14. package/skills/triage/SKILL.md +3 -3
  15. package/src/completion.mjs +1 -1
  16. package/src/host-contract.mjs +3 -0
  17. package/src/implementation.mjs +6 -1
  18. package/src/install-workflows.mjs +11 -5
  19. package/src/integration-evidence.mjs +1 -1
  20. package/src/issue-actions.mjs +62 -18
  21. package/src/model-profiles.mjs +58 -0
  22. package/src/opencode-execute.mjs +17 -4
  23. package/src/pr-state-actions.mjs +106 -0
  24. package/src/pr-state.mjs +75 -0
  25. package/src/profile-delegation.mjs +111 -0
  26. package/src/proposal.mjs +1 -1
  27. package/src/request-triage.mjs +42 -0
  28. package/src/stage-dispatch.mjs +21 -5
  29. package/src/stage-transition.mjs +10 -0
  30. package/src/triage.mjs +4 -0
  31. package/templates/change-request.yml +24 -0
  32. package/workflows/lighthouse-sdlc-apply-review.lock.yml +14 -13
  33. package/workflows/lighthouse-sdlc-apply-review.md +2 -1
  34. package/workflows/lighthouse-sdlc-implement.lock.yml +13 -13
  35. package/workflows/lighthouse-sdlc-implement.md +1 -1
  36. package/workflows/lighthouse-sdlc-integrate.lock.yml +13 -13
  37. package/workflows/lighthouse-sdlc-integrate.md +1 -1
  38. package/workflows/lighthouse-sdlc-propose.lock.yml +13 -13
  39. package/workflows/lighthouse-sdlc-propose.md +1 -1
  40. package/workflows/lighthouse-sdlc-refine.lock.yml +18 -13
  41. package/workflows/lighthouse-sdlc-refine.md +6 -1
  42. package/workflows/lighthouse-sdlc-review.lock.yml +13 -13
  43. package/workflows/lighthouse-sdlc-review.md +1 -1
  44. package/workflows/lighthouse-sdlc-router.yml +38 -5
  45. package/workflows/lighthouse-sdlc-triage.lock.yml +283 -1169
  46. package/workflows/lighthouse-sdlc-triage.md +175 -29
@@ -0,0 +1,106 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { checkImplementationPullRequest } from './proposal-review.mjs';
3
+ import { authorizedCiCallback, projectImplementationPrState, reconcileImplementationPr, scheduledImplementationPulls, setPullRequestDraft } from './pr-state.mjs';
4
+
5
+ const apiFor = (repo, token) => async (path, method = 'GET', body, optional = false) => {
6
+ const response = await fetch(`https://api.github.com/repos/${repo}${path ? `/${path}` : ''}`, {
7
+ method, headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', ...(body ? { 'Content-Type': 'application/json' } : {}) },
8
+ ...(body ? { body: JSON.stringify(body) } : {}), signal: AbortSignal.timeout(30000),
9
+ });
10
+ if (optional && response.status === 404) return null;
11
+ if (!response.ok) throw Error(`GitHub ${method} ${path}: ${response.status}`);
12
+ return response.status === 204 ? null : response.json();
13
+ };
14
+ const commentsFor = async (api, issue) => {
15
+ const comments = [];
16
+ for (let page = 1; page <= 10; page++) {
17
+ const batch = await api(`issues/${issue}/comments?per_page=100&page=${page}`);
18
+ comments.push(...batch);
19
+ if (batch.length < 100) return comments;
20
+ }
21
+ throw Error('Issue comment history exceeds bound');
22
+ };
23
+ const requiredChecks = profile => {
24
+ const checks = profile.requiredChecks;
25
+ if (!Array.isArray(checks) || !checks.length || checks.length > 20 || checks.some(item => !item.context || item.integrationId !== null && !Number.isSafeInteger(item.integrationId))) throw Error('Invalid required-check profile');
26
+ return checks.map(item => ({ context: item.context, app: item.integrationId }));
27
+ };
28
+ async function main() {
29
+ const env = process.env, repo = env.GITHUB_REPOSITORY, owner = env.GITHUB_REPOSITORY_OWNER;
30
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repo ?? '') || !env.GH_TOKEN) throw Error('Missing repository or token');
31
+ const api = apiFor(repo, env.GH_TOKEN);
32
+ const event = JSON.parse(await readFile(env.GITHUB_EVENT_PATH, 'utf8'));
33
+ const metadata = await api('');
34
+ const config = await api(`contents/.lighthouse/github.json?ref=${metadata.default_branch}`, 'GET', undefined, true);
35
+ if (!config) return;
36
+ const profile = JSON.parse(Buffer.from(config.content, 'base64').toString());
37
+ if (profile.repository !== repo) throw Error('Repository profile differs from the event');
38
+ let candidates = [];
39
+ const ciCallback = env.GITHUB_EVENT_NAME === 'workflow_dispatch' && env.LIGHTHOUSE_CI_RECONCILE === 'true';
40
+ if (env.GITHUB_EVENT_NAME === 'pull_request') {
41
+ if (env.GITHUB_ACTOR !== owner || !['ready_for_review', 'converted_to_draft', 'closed', 'reopened'].includes(event.action)) return;
42
+ candidates = [event.pull_request?.number];
43
+ } else if (env.GITHUB_EVENT_NAME === 'workflow_run') {
44
+ const id = event.workflow_run?.id;
45
+ if (event.action !== 'completed' || !Number.isSafeInteger(id)) return;
46
+ const run = await api(`actions/runs/${id}`);
47
+ if (run.status !== 'completed' || run.conclusion !== 'success' || run.head_sha !== event.workflow_run.head_sha || run.repository?.full_name !== repo || !profile.completion?.postMergeWorkflows?.some(name => run.path === `.github/workflows/${name}`)) return;
48
+ const pulls = await api(`commits/${run.head_sha}/pulls?per_page=100`);
49
+ if (pulls.length >= 100) throw Error('PR lookup exceeds bound');
50
+ candidates = pulls.filter(pr => pr.head?.sha === run.head_sha).map(pr => pr.number);
51
+ } else if (env.GITHUB_EVENT_NAME === 'schedule') {
52
+ candidates = await scheduledImplementationPulls(api);
53
+ } else if (ciCallback) {
54
+ const id = Number(env.GITHUB_RUN_ID);
55
+ if (!Number.isSafeInteger(id) || !/^[a-f0-9]{40}$/.test(env.GITHUB_SHA ?? '')) throw Error('CI callback lacks a run or head');
56
+ const run = await api(`actions/runs/${id}`);
57
+ if (!authorizedCiCallback(run, { id, sha: env.GITHUB_SHA, repo, workflows: profile.completion?.postMergeWorkflows })) return;
58
+ const pulls = await api(`commits/${run.head_sha}/pulls?per_page=100`);
59
+ if (pulls.length >= 100) throw Error('PR lookup exceeds bound');
60
+ candidates = pulls.filter(pr => pr.head?.sha === run.head_sha).map(pr => pr.number);
61
+ } else return;
62
+ for (const number of candidates) {
63
+ if (!Number.isSafeInteger(number)) continue;
64
+ const pr = await api(`pulls/${number}`);
65
+ if (env.GITHUB_EVENT_NAME === 'pull_request' && pr.head.sha !== event.pull_request.head.sha) continue;
66
+ if (env.GITHUB_EVENT_NAME === 'workflow_run' && pr.head.sha !== event.workflow_run.head_sha) continue;
67
+ if (ciCallback && pr.head.sha !== env.GITHUB_SHA) continue;
68
+ if (pr.base.ref !== metadata.default_branch) continue;
69
+ let target;
70
+ try { target = checkImplementationPullRequest({ ...pr, state: 'open', merged: false }, repo); }
71
+ catch {
72
+ const match = pr.head.ref.match(/^lighthouse\/implement-([1-9][0-9]*)-[a-f0-9]{12}$/);
73
+ if (!match || pr.user?.login !== 'github-actions[bot]' || pr.head.repo?.full_name !== repo || pr.base.repo?.full_name !== repo || pr.state !== 'open') continue;
74
+ const issue = await api(`issues/${match[1]}`);
75
+ if (issue.state !== 'open') continue;
76
+ if (!pr.draft) await setPullRequestDraft(env.GH_TOKEN, pr, true);
77
+ await projectImplementationPrState(api, issue.number, true);
78
+ const notice = `<!-- lighthouse-pr-link-changed:${pr.number}:${pr.head.sha} -->`;
79
+ if (!(await commentsFor(api, issue.number)).some(comment => comment.user?.login === 'github-actions[bot]' && comment.body?.includes(notice))) await api(`issues/${issue.number}/comments`, 'POST', { body: `PR #${pr.number} is back in draft because its Lighthouse link changed. Restore the PR description before review. [Open PR](${pr.html_url}).\n\n${notice}` });
80
+ continue;
81
+ }
82
+ const issue = await api(`issues/${target.issue}`);
83
+ if (issue.state !== 'open') continue;
84
+ if (pr.state !== 'open' || pr.merged) {
85
+ for (const name of ['pr-draft', 'pr-ready']) if (issue.labels.some(label => label.name === name)) await api(`issues/${issue.number}/labels/${name}`, 'DELETE', undefined, true);
86
+ if (!pr.merged) {
87
+ await api(`issues/${issue.number}/labels`, 'POST', { labels: ['waiting-review'] });
88
+ const notice = `<!-- lighthouse-pr-closed:${pr.number}:${pr.head.sha} -->`;
89
+ if (!(await commentsFor(api, issue.number)).some(comment => comment.user?.login === 'github-actions[bot]' && comment.body?.includes(notice))) await api(`issues/${issue.number}/comments`, 'POST', { body: `PR #${pr.number} was closed without merging. Reopen it or request a new implementation. [Open PR](${pr.html_url}).\n\n${notice}` });
90
+ }
91
+ continue;
92
+ }
93
+ const comments = await commentsFor(api, target.issue);
94
+ if (event.action === 'converted_to_draft') {
95
+ const hold = `<!-- lighthouse-pr-hold:${pr.number}:${pr.head.sha} -->`;
96
+ if (!comments.some(comment => comment.user?.login === 'github-actions[bot]' && comment.body?.includes(hold))) await api(`issues/${target.issue}/comments`, 'POST', { body: `PR #${pr.number} is a draft by owner choice. [Open PR](${pr.html_url}).\n\n${hold}` });
97
+ await projectImplementationPrState(api, target.issue, true);
98
+ continue;
99
+ }
100
+ const hold = comments.some(comment => comment.user?.login === 'github-actions[bot]' && comment.body?.includes(`<!-- lighthouse-pr-hold:${pr.number}:${pr.head.sha} -->`));
101
+ const auto = (['workflow_run', 'schedule'].includes(env.GITHUB_EVENT_NAME) || ciCallback) && !hold;
102
+ const result = await reconcileImplementationPr(api, repo, pr, issue, comments, requiredChecks(profile), (current, draft) => setPullRequestDraft(env.GH_TOKEN, current, draft), { auto });
103
+ if (event.action === 'ready_for_review' && result.status === 'draft') await api(`issues/${target.issue}/comments`, 'POST', { body: `PR #${pr.number} remains a draft: ${result.reason}. [Open PR](${pr.html_url}).` });
104
+ }
105
+ }
106
+ main().catch(error => { console.error(`::error::${error.message}`); process.exitCode = 1; });
@@ -0,0 +1,75 @@
1
+ import { checkImplementationPullRequest } from './proposal-review.mjs';
2
+ import { checkCommit } from './completion.mjs';
3
+
4
+ const marker = /<!-- lighthouse-pr-state:(\{[^\n]*\}) -->/;
5
+
6
+ export async function scheduledImplementationPulls(api) {
7
+ const numbers = [];
8
+ for (let page = 1; page <= 10; page++) {
9
+ const pulls = await api(`pulls?state=open&per_page=100&page=${page}`);
10
+ numbers.push(...pulls.filter(pr => /^lighthouse\/implement-[1-9][0-9]*-[a-f0-9]{12}$/.test(pr.head?.ref ?? '') && pr.user?.login === 'github-actions[bot]').map(pr => pr.number));
11
+ if (pulls.length < 100) return numbers;
12
+ }
13
+ throw Error('Open PR history exceeds reconciliation bound');
14
+ }
15
+
16
+ export function authorizedCiCallback(run, { id, sha, repo, workflows }) {
17
+ return Number.isSafeInteger(id) && /^[a-f0-9]{40}$/.test(sha ?? '') && run.id === id && run.event === 'workflow_dispatch' && run.head_sha === sha && run.repository?.full_name === repo && workflows?.some(name => run.path === `.github/workflows/${name}`) === true;
18
+ }
19
+
20
+ export async function setPullRequestDraft(token, pr, draft) {
21
+ if (!pr.node_id) throw Error('PR lacks GraphQL identity');
22
+ const field = draft ? 'convertPullRequestToDraft' : 'markPullRequestReadyForReview';
23
+ const response = await fetch('https://api.github.com/graphql', {
24
+ method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({ query: `mutation($id:ID!){${field}(input:{pullRequestId:$id}){pullRequest{id isDraft}}}`, variables: { id: pr.node_id } }),
26
+ signal: AbortSignal.timeout(30000),
27
+ });
28
+ const result = await response.json();
29
+ if (!response.ok || result.errors?.length || result.data?.[field]?.pullRequest?.isDraft !== draft) throw Error('GitHub could not change PR draft state');
30
+ }
31
+
32
+ export function implementationPrProof(comments, pr) {
33
+ for (const comment of [...comments].reverse()) {
34
+ if (comment.user?.login !== 'github-actions[bot]' || comment.user.type !== 'Bot') continue;
35
+ const raw = comment.body?.match(marker)?.[1];
36
+ if (!raw) continue;
37
+ let proof;
38
+ try { proof = JSON.parse(raw); } catch { continue; }
39
+ if (proof.pr === pr.number && proof.head === pr.head.sha && typeof proof.complete === 'boolean' && /^[1-9][0-9]*$/.test(String(proof.run))) return proof;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ export async function projectImplementationPrState(api, issue, draft) {
45
+ const current = await api(`issues/${issue}`);
46
+ if (current.state !== 'open') return;
47
+ const wanted = draft ? 'pr-draft' : 'pr-ready';
48
+ for (const label of ['ready', 'waiting-review', draft ? 'pr-ready' : 'pr-draft']) {
49
+ if (current.labels.some(item => item.name === label)) await api(`issues/${issue}/labels/${label}`, 'DELETE', undefined, true);
50
+ }
51
+ if (!current.labels.some(item => item.name === wanted)) await api(`issues/${issue}/labels`, 'POST', { labels: [wanted] });
52
+ }
53
+
54
+ export async function reconcileImplementationPr(api, repo, pr, issue, comments, requiredChecks, setDraft, { auto = false } = {}) {
55
+ const target = checkImplementationPullRequest(pr, repo);
56
+ if (target.issue !== issue.number || issue.state !== 'open') return { status: 'ignored', reason: 'Issue or PR changed' };
57
+ const proof = implementationPrProof(comments, pr);
58
+ const checks = await checkCommit(api, pr.head.sha, requiredChecks);
59
+ const reserved = issue.labels.some(label => ['agent-working', 'human-working'].includes(label.name));
60
+ const complete = !reserved && proof?.complete === true && checks.length > 0 && checks.every(check => check.status === 'passed');
61
+ if (pr.draft && auto && complete) {
62
+ try { await setDraft(pr, false); }
63
+ catch (error) { if ((await api(`pulls/${pr.number}`)).draft) throw error; }
64
+ await projectImplementationPrState(api, issue.number, false);
65
+ return { status: 'ready', reason: 'Candidate and required checks passed' };
66
+ }
67
+ if (!pr.draft && !complete) {
68
+ try { await setDraft(pr, true); }
69
+ catch (error) { if (!(await api(`pulls/${pr.number}`)).draft) throw error; }
70
+ await projectImplementationPrState(api, issue.number, true);
71
+ return { status: 'draft', reason: reserved ? 'Implementation or owner work is still active' : !proof?.complete ? 'Implementation is incomplete' : 'Required checks have not passed' };
72
+ }
73
+ await projectImplementationPrState(api, issue.number, pr.draft);
74
+ return { status: pr.draft ? 'draft' : 'ready', reason: pr.draft ? 'PR is a draft' : 'Candidate and required checks passed' };
75
+ }
@@ -0,0 +1,111 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { validateLaunchRequest } from './control-plane.mjs';
3
+ import { withinScope } from './implementation.mjs';
4
+ import { resolveModelProfile, modelExecutionReceipt } from './model-profiles.mjs';
5
+
6
+ const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
7
+ const name = value => typeof value === 'string' && /^[a-z][a-z0-9-]{0,63}$/.test(value);
8
+ const validScope = value => typeof value === 'string' && !/[?*\[\]{}\\]/.test(value) && withinScope(value.replace(/\/$/, ''), { paths: ['*'] });
9
+ const contains = (scope, file) => scope === '*' || (scope.endsWith('/') ? file.startsWith(scope) : file === scope);
10
+ const overlaps = (a, b) => a.replace(/\/$/, '') === b.replace(/\/$/, '') || contains(a, b) || contains(b, a);
11
+ const boundedLimits = limits => record(limits) && Number.isInteger(limits.timeoutSeconds) && limits.timeoutSeconds > 0 && limits.timeoutSeconds <= 1800 && Number.isInteger(limits.maxAttempts) && limits.maxAttempts === 1;
12
+
13
+ export function prepareDelegatedTasks({ tasks, parent, controlPlane, project = null, operator, maxParallel }) {
14
+ if (!Number.isInteger(maxParallel) || maxParallel < 1 || maxParallel > 8) throw Error('Host parallel limit must be 1..8');
15
+ if (!record(parent) || !record(controlPlane?.roles?.[parent.role]) || validateLaunchRequest(parent, controlPlane).length || !boundedLimits(parent.limits)) throw Error('Invalid bounded parent launch contract');
16
+ if (!Array.isArray(tasks) || !tasks.length || tasks.length > 100) throw Error('Delegation requires 1..100 tasks');
17
+ const ids = new Set();
18
+ const prepared = structuredClone(tasks).map(task => {
19
+ if (!record(task) || Object.keys(task).some(key => !['id', 'specialist', 'profile', 'dependencies', 'affectedFiles', 'input', 'tools', 'outputSchema', 'limits', 'stopConditions'].includes(key)) || !name(task.id) || ids.has(task.id) || !name(task.specialist)) throw Error('Invalid or duplicate delegated task');
20
+ ids.add(task.id);
21
+ if (!Array.isArray(task.dependencies) || task.dependencies.some(id => !name(id)) || new Set(task.dependencies).size !== task.dependencies.length) throw Error('Task dependencies must be unique task IDs');
22
+ if (!Array.isArray(task.affectedFiles) || !task.affectedFiles.length || task.affectedFiles.length > 100 || new Set(task.affectedFiles).size !== task.affectedFiles.length || task.affectedFiles.some(file => !validScope(file) || !parent.writeScope.some(scope => contains(scope, file)))) throw Error('Task affected files exceed parent scope or use unsupported paths');
23
+ if (task.tools !== undefined && !Array.isArray(task.tools) || task.stopConditions !== undefined && !Array.isArray(task.stopConditions)) throw Error('Task tools and stop conditions must be arrays');
24
+ if (task.limits !== undefined && (!record(task.limits) || Object.keys(task.limits).some(key => !['timeoutSeconds', 'maxAttempts'].includes(key)))) throw Error('Task limits may only narrow timeout and attempts');
25
+ if (!record(controlPlane.roles[task.specialist])) throw Error('Task specialist needs a declared launch contract');
26
+ const input = JSON.stringify(task.input ?? {});
27
+ if (Buffer.byteLength(input) > 65536) throw Error('Task context exceeds 64 KiB');
28
+ const child = {
29
+ runId: `${parent.runId}-${task.id}-${randomUUID()}`, parentRun: parent.runId, role: task.specialist,
30
+ input: JSON.parse(input), outputSchema: task.outputSchema ?? controlPlane.roles[task.specialist].outputSchema,
31
+ tools: task.tools ?? [...parent.tools], writeScope: [...task.affectedFiles], limits: { ...parent.limits, ...task.limits },
32
+ stopConditions: [...parent.stopConditions, ...(task.stopConditions ?? [])],
33
+ };
34
+ if (!boundedLimits(child.limits) || child.limits.timeoutSeconds > parent.limits.timeoutSeconds || child.tools.some(tool => !parent.tools.includes(tool)) || validateLaunchRequest(child, controlPlane).length) throw Error('Task launch exceeds specialist or parent contract');
35
+ const selection = resolveModelProfile({ project, operator, worker: task.specialist, profile: task.profile });
36
+ if (selection.mode !== 'profile' || !task.profile) throw Error('Delegated task must declare a profile');
37
+ return { id: task.id, dependencies: task.dependencies, affectedFiles: task.affectedFiles, child, selection };
38
+ });
39
+ for (const task of prepared) if (task.dependencies.some(id => !ids.has(id) || id === task.id)) throw Error('Unknown or self task dependency');
40
+ const visited = new Set(), active = new Set(), byId = new Map(prepared.map(task => [task.id, task]));
41
+ function visit(task) {
42
+ if (active.has(task.id)) throw Error('Delegated task dependency cycle');
43
+ if (visited.has(task.id)) return;
44
+ active.add(task.id);
45
+ for (const id of task.dependencies) visit(byId.get(id));
46
+ active.delete(task.id); visited.add(task.id);
47
+ }
48
+ for (const task of prepared) visit(task);
49
+ return prepared;
50
+ }
51
+
52
+ // Callbacks are host-owned: enforce paths/tools, stop on abort, validate output schemas,
53
+ // retain evidence and check actual file changes. This scheduler grants no permissions.
54
+ export async function runDelegatedTasks(options) {
55
+ const { launch, verifyWave, signal } = options;
56
+ if (typeof launch !== 'function' || typeof verifyWave !== 'function') throw Error('Host launch and wave verification callbacks required');
57
+ const tasks = prepareDelegatedTasks(options), results = new Map(), waves = [];
58
+ const deadline = Date.now() + options.parent.limits.timeoutSeconds * 1000;
59
+ const controller = new AbortController();
60
+ const abort = () => controller.abort();
61
+ signal?.addEventListener('abort', abort, { once: true });
62
+ if (signal?.aborted) abort();
63
+ const timer = setTimeout(abort, Math.max(1, deadline - Date.now()));
64
+ const validResult = result => result?.status === 'passed' && Array.isArray(result.evidence) && result.evidence.length > 0;
65
+ async function bounded(callback, seconds) {
66
+ if (controller.signal.aborted) throw Error('Delegation stopped');
67
+ let timeout, onAbort;
68
+ try {
69
+ return await Promise.race([
70
+ Promise.resolve().then(() => callback(controller.signal)),
71
+ new Promise((_, reject) => {
72
+ onAbort = () => reject(Error('Delegation stopped'));
73
+ controller.signal.addEventListener('abort', onAbort, { once: true });
74
+ timeout = setTimeout(() => { controller.abort(); reject(Error('Delegation deadline exceeded')); }, Math.min(seconds * 1000, Math.max(1, deadline - Date.now())));
75
+ }),
76
+ ]);
77
+ } finally { clearTimeout(timeout); controller.signal.removeEventListener('abort', onAbort); }
78
+ }
79
+ try {
80
+ while (results.size < tasks.length && !controller.signal.aborted) {
81
+ // Propagate failures transitively before selecting eligible tasks.
82
+ let changed;
83
+ do {
84
+ changed = false;
85
+ for (const task of tasks) if (!results.has(task.id) && task.dependencies.some(id => results.has(id) && results.get(id).status !== 'passed')) {
86
+ results.set(task.id, { id: task.id, status: 'blocked', reason: 'dependency-failed' }); changed = true;
87
+ }
88
+ } while (changed);
89
+ const wave = [];
90
+ for (const task of tasks) {
91
+ if (results.has(task.id) || task.dependencies.some(id => results.get(id)?.status !== 'passed')) continue;
92
+ if (wave.length < options.maxParallel && !wave.some(other => other.affectedFiles.some(a => task.affectedFiles.some(b => overlaps(a, b))))) wave.push(task);
93
+ }
94
+ if (!wave.length) break;
95
+ const executions = await Promise.all(wave.map(async task => {
96
+ try {
97
+ const outcome = await bounded(signal => launch({ ...structuredClone(task.child), model: structuredClone(task.selection), signal }), task.child.limits.timeoutSeconds);
98
+ return { id: task.id, status: validResult(outcome) ? 'passed' : 'failed', reason: validResult(outcome) ? undefined : 'launch-result-failed', outcome, modelExecution: modelExecutionReceipt(task.selection) };
99
+ } catch { return { id: task.id, status: 'failed', reason: 'launch-failed-or-stopped', modelExecution: modelExecutionReceipt(task.selection) }; }
100
+ }));
101
+ let verification;
102
+ try { verification = await bounded(signal => verifyWave({ tasks: structuredClone(wave), results: structuredClone(executions), signal }), options.parent.limits.timeoutSeconds); }
103
+ catch { verification = { status: 'failed', evidence: [] }; }
104
+ const passed = validResult(verification);
105
+ for (const execution of executions) results.set(execution.id, execution.status === 'passed' && !passed ? { ...execution, status: 'failed', reason: 'wave-verification-failed' } : execution);
106
+ waves.push({ tasks: wave.map(task => task.id), status: passed && executions.every(result => result.status === 'passed') ? 'passed' : 'failed', verification });
107
+ }
108
+ for (const task of tasks) if (!results.has(task.id)) results.set(task.id, { id: task.id, status: 'blocked', reason: controller.signal.aborted ? 'execution-stopped' : 'dependency-failed' });
109
+ return { status: [...results.values()].every(result => result.status === 'passed') ? 'passed' : 'blocked', waves, tasks: tasks.map(task => results.get(task.id)) };
110
+ } finally { clearTimeout(timer); signal?.removeEventListener('abort', abort); }
111
+ }
package/src/proposal.mjs CHANGED
@@ -80,7 +80,7 @@ export async function loadStageReport(api, repo, report, current) {
80
80
  }
81
81
  const request = values.input;
82
82
  if (request.dispatch) {
83
- if (!['refine', 'propose', 'implement'].includes(request.stage) || !report.body.includes(`<!-- lighthouse-stage:${request.stage}:`)) throw Error('Unexpected dispatched stage');
83
+ if (!['triage', 'refine', 'propose', 'implement'].includes(request.stage) || !report.body.includes(`<!-- lighthouse-stage:${request.stage}:`)) throw Error('Unexpected dispatched stage');
84
84
  const dispatch = await loadDispatch(api, repo, current.owner, current.issue, request.dispatch.commentId, run);
85
85
  if (dispatch.request.stage !== request.stage || dispatch.request.id !== request.dispatch.requestId || dispatch.request.sourceRevision !== request.sourceRevision || dispatch.request.workflowRevision !== request.workflowRevision) throw Error('Dispatched report identity mismatch');
86
86
  await checkDispatchClaim(api, repo, current.issue, request.dispatch, run.id);
@@ -0,0 +1,42 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+
3
+ // Semantic assessments come from the agent; these checks enforce the public contract.
4
+ export const storyPoints = Object.freeze([1, 2, 3, 5, 8, 13]);
5
+ const text = value => typeof value === 'string' && value.trim().length > 0;
6
+ const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
7
+
8
+ export function validateEstimate(estimate, { provisional = false } = {}) {
9
+ const errors = [];
10
+ if (!record(estimate)) return ['estimate must be an object'];
11
+ if (!storyPoints.includes(estimate.points)) errors.push('estimate.points must be 1, 2, 3, 5, 8 or 13');
12
+ if (!['low', 'medium', 'high'].includes(estimate.confidence)) errors.push('estimate.confidence must be low, medium or high');
13
+ if (!['provisional', 'confirmed'].includes(estimate.status) || provisional && estimate.status !== 'provisional') errors.push('estimate.status must match the provisional triage or confirmed refinement assessment');
14
+ for (const factor of ['scope', 'dependencies', 'verification']) if (!text(estimate.rationale?.[factor])) errors.push(`estimate.rationale.${factor} must explain the estimate`);
15
+ if (!Array.isArray(estimate.uncertainty) || estimate.uncertainty.some(item => !text(item))) errors.push('estimate.uncertainty must be a separate array of descriptions');
16
+ return errors;
17
+ }
18
+
19
+ export function validateRequestTriage(output, originalRequest) {
20
+ if (!record(output)) return ['triage output must be an object'];
21
+ const errors = validateEstimate(output.estimate, { provisional: true });
22
+ const analysis = output.requestAnalysis;
23
+ if (!record(analysis)) return [...errors, 'requestAnalysis must preserve the request and distinguish inferences, unknowns and blocking questions'];
24
+ if (typeof analysis.originalRequest !== 'string' || originalRequest !== undefined && analysis.originalRequest !== originalRequest) errors.push('requestAnalysis.originalRequest must preserve the exact original request');
25
+ for (const field of ['problem', 'desiredResult']) if (!text(analysis[field])) errors.push(`requestAnalysis.${field} is required`);
26
+ if (!Array.isArray(analysis.inferences) || analysis.inferences.some(item => !text(item?.inference) || !text(item?.source))) errors.push('each inference needs a description and evidence source');
27
+ if (!Array.isArray(analysis.unknowns) || analysis.unknowns.some(item => !text(item))) errors.push('unknowns must be a separate array of descriptions');
28
+ if (!Array.isArray(analysis.questions) || analysis.questions.some(item => !text(item?.question) || !text(item?.blocks))) errors.push('each question must state the outcome it blocks');
29
+ if (analysis.questions?.length) errors.push('blocking questions prevent passed triage');
30
+ return errors;
31
+ }
32
+
33
+ export function validateEstimateRevision(output, prior) {
34
+ if (!prior) return [];
35
+ const errors = validateEstimate(output?.estimate);
36
+ if (!isDeepStrictEqual(output?.estimateRevision?.previous, prior)) errors.push('refinement must preserve the prior estimate');
37
+ if (!text(output?.estimateRevision?.reason)) errors.push('refinement must explain whether and why the estimate changed');
38
+ return errors;
39
+ }
40
+
41
+ // Later owner requests on external issues need the triage receipt checked in preparation.
42
+ export const eligibleIssueAuthor = (issue, owner, stage) => issue?.user?.login === owner || stage === 'triage' || issue?.labels?.some(label => label.name === 'triaged');
@@ -2,13 +2,14 @@ import { digest } from './host-contract.mjs';
2
2
  import { readActionsJson } from './actions-artifact.mjs';
3
3
 
4
4
  const routerPath = '.github/workflows/lighthouse-sdlc-router.yml';
5
- export const stages = ['refine', 'propose', 'implement', 'integrate', 'review'];
5
+ export const stages = ['triage', 'refine', 'propose', 'implement', 'integrate', 'review'];
6
6
  export const workers = [...stages, 'apply-review'];
7
7
  export const workerFor = request => request.worker ?? request.stage ?? 'implement';
8
8
  export const stagePath = stage => { if (!workers.includes(stage)) throw Error('Unknown dispatch worker'); return `.github/workflows/lighthouse-sdlc-${stage}.lock.yml`; };
9
9
  export const implementPath = stagePath('implement');
10
10
  export const statusText = (stage, status) => `${({ requested: '📥 Requested', running: '▶️ Execution admitted', completed: '✅ Completed', waiting: '⏸️ Waiting for your response', failed: '❌ Failed or cancelled' })[status]} · ${stage}`;
11
11
  const positive = value => /^[1-9][0-9]*$/.test(String(value));
12
+ export const isBotContinuation = (event, eventName, actor, triggeringActor) => eventName === 'workflow_dispatch' && actor === 'github-actions[bot]' && triggeringActor === actor && event.inputs?.stage === 'implement' && positive(event.inputs?.issue) && positive(event.inputs?.after_run);
12
13
  const sha = value => /^[a-f0-9]{40}$/.test(value ?? '');
13
14
  const hash = value => /^[a-f0-9]{64}$/.test(value ?? '');
14
15
  export const dispatchId = (repository, runId, stage = 'implement') => digest(`${repository}:${runId}:${stage}`);
@@ -77,7 +78,8 @@ export function verifyDispatchOrigin(request, receipt, parent, worker, repositor
77
78
  const recovery = !!request.recovery;
78
79
  const originPath = recovery ? '.github/workflows/lighthouse-worker-maintenance.yml' : routerPath;
79
80
  const originEvent = recovery ? request.routerEventName : request.eventName;
80
- if (String(parent.id) !== String(receipt.routerRunId) || parent.run_attempt !== receipt.routerAttempt || request.routerRunId !== receipt.routerRunId || request.routerAttempt !== receipt.routerAttempt || parent.path !== originPath || (parent.event !== 'workflow_run' && (parent.actor?.login !== owner || parent.triggering_actor?.login !== owner)) || !(recovery ? ['schedule', 'workflow_dispatch'] : events).includes(parent.event) || parent.event !== originEvent || parent.head_sha !== (request.routerRevision ?? request.workflowRevision) || parent.head_branch !== (request.routerRef ?? request.ref) || parent.repository?.full_name !== repository) throw Error('Dispatch origin is not an authorized router execution');
81
+ const continuation = !recovery && isBotContinuation(request.event, parent.event, parent.actor?.login, parent.triggering_actor?.login) && request.stage === 'implement' && request.handoff && !request.worker && !request.feedbackKey && Number(request.event.inputs.issue) === issue && request.predecessor?.runId === request.event.inputs.after_run;
82
+ if (String(parent.id) !== String(receipt.routerRunId) || parent.run_attempt !== receipt.routerAttempt || request.routerRunId !== receipt.routerRunId || request.routerAttempt !== receipt.routerAttempt || parent.path !== originPath || (parent.event !== 'workflow_run' && !continuation && (parent.actor?.login !== owner || parent.triggering_actor?.login !== owner)) || !(recovery ? ['schedule', 'workflow_dispatch'] : events).includes(parent.event) || parent.event !== originEvent || parent.head_sha !== (request.routerRevision ?? request.workflowRevision) || parent.head_branch !== (request.routerRef ?? request.ref) || parent.repository?.full_name !== repository) throw Error('Dispatch origin is not an authorized router execution');
81
83
  const eventRunId = request.eventName === 'workflow_run' ? request.event?.workflow_run?.id : request.event?.inputs?.after_run;
82
84
  if (String(eventRunId ?? '') !== String(receipt.eventRunId ?? '') || String(request.eventRunId ?? '') !== String(receipt.eventRunId ?? '')) throw Error('Completion event identity mismatch');
83
85
  if (worker.path !== stagePath(workerFor(request)) || worker.event !== 'workflow_dispatch' || worker.head_sha !== request.workflowRevision || worker.head_branch !== request.ref || worker.repository?.full_name !== repository || ![owner, 'github-actions[bot]'].includes(worker.actor?.login) || ![owner, 'github-actions[bot]'].includes(worker.triggering_actor?.login)) throw Error('Dispatch worker identity mismatch');
@@ -91,6 +93,11 @@ export async function loadDispatch(api, repository, owner, issue, commentId, wor
91
93
  const parent = await api(`actions/runs/${receipt.routerRunId}/attempts/${receipt.routerAttempt}`);
92
94
  const request = await readArtifact(api, repository, receipt.routerRunId, dispatchArtifact(receipt), 'dispatch.json');
93
95
  verifyDispatchOrigin(request, receipt, parent, worker, repository, owner, issue);
96
+ if (isBotContinuation(request.event, parent.event, parent.actor?.login, parent.triggering_actor?.login)) {
97
+ const predecessor = await api(`actions/runs/${request.predecessor.runId}`);
98
+ const next = await (await import('./stage-transition.mjs')).refinementAfterRun(api, repository, owner, { action: 'completed', workflow_run: { id: predecessor.id, head_sha: predecessor.head_sha } }, readArtifact);
99
+ if (next?.issue !== issue || JSON.stringify(next.predecessor) !== JSON.stringify(request.predecessor)) throw Error('Bot continuation lacks an authenticated refinement');
100
+ }
94
101
  const dispatch = { request, receipt, commentId: Number(commentId) };
95
102
  if (request.recovery) await (await import('./worker-recovery.mjs')).verifyRecoveryOrigin(api, repository, owner, dispatch, parent, readArtifact, depth);
96
103
  return dispatch;
@@ -138,11 +145,20 @@ export async function finishDispatch(api, repository, issue, commentId, workerId
138
145
 
139
146
  // An owner can replay a completed run on a reviewed workflow branch for acceptance.
140
147
  // Automated events and manual replays use the same fresh GitHub observation.
141
- export async function completedEvent(api, event, env) {
148
+ export async function completedEvent(api, event, env, wait = ms => new Promise(resolve => setTimeout(resolve, ms))) {
142
149
  if (env.GITHUB_EVENT_NAME === 'workflow_run') return event;
143
150
  const id = event.inputs?.after_run;
144
151
  if (!id) return null;
145
- if (env.GITHUB_EVENT_NAME !== 'workflow_dispatch' || env.GITHUB_ACTOR !== env.GITHUB_REPOSITORY_OWNER || env.GITHUB_TRIGGERING_ACTOR !== env.GITHUB_REPOSITORY_OWNER || !positive(id)) throw Error('Completion replay requires an owner and a run ID');
146
- const run = await api(`actions/runs/${id}`);
152
+ const bot = isBotContinuation(event, env.GITHUB_EVENT_NAME, env.GITHUB_ACTOR, env.GITHUB_TRIGGERING_ACTOR);
153
+ if (!bot && (env.GITHUB_EVENT_NAME !== 'workflow_dispatch' || env.GITHUB_ACTOR !== env.GITHUB_REPOSITORY_OWNER || env.GITHUB_TRIGGERING_ACTOR !== env.GITHUB_REPOSITORY_OWNER || !positive(id))) throw Error('Completion replay requires an owner and a run ID, or a verified refinement continuation');
154
+ let run = await api(`actions/runs/${id}`);
155
+ if (bot) {
156
+ if (run.path !== stagePath('refine')) throw Error('Bot continuation must reference a refinement');
157
+ // The publisher signals before its own workflow has finished its final jobs.
158
+ for (let attempt = 0; run.status !== 'completed' && attempt < 24; attempt++) {
159
+ await wait(5000); run = await api(`actions/runs/${id}`);
160
+ }
161
+ if (run.status !== 'completed') throw Error('Refinement completion timed out; replay after the execution completes');
162
+ }
147
163
  return { action: 'completed', workflow_run: { id: run.id, head_sha: run.head_sha } };
148
164
  }
@@ -2,6 +2,16 @@ import { readActionsJson } from './actions-artifact.mjs';
2
2
  import { loadDispatch, checkDispatchClaim, stagePath, issueComments } from './stage-dispatch.mjs';
3
3
  import { validateStageEnvelope } from './host-contract.mjs';
4
4
 
5
+ // Explicit dispatch also works when GitHub suppresses follow-up token events.
6
+ // This is a wake-up only; the router rechecks the completed run and owner intent.
7
+ export async function signalRefinementContinuation(api, request, envelope) {
8
+ if (request.stage !== 'refine' || !request.dispatch || request.autoImplement !== true || envelope.result?.status !== 'passed' || envelope.result?.gate !== 'ready') return false;
9
+ if (validateStageEnvelope(envelope, request).length) throw Error('Invalid refinement result');
10
+ const repository = await api('');
11
+ await api('actions/workflows/lighthouse-sdlc-router.yml/dispatches', 'POST', { ref: repository.default_branch, inputs: { issue: String(request.issue), stage: 'implement', after_run: String(request.workflowRunId) } });
12
+ return true;
13
+ }
14
+
5
15
  // The completed execution is an observation. Its retained owner request grants the handoff.
6
16
  export async function refinementAfterRun(api, repository, owner, event, readArtifact = readActionsJson) {
7
17
  if (event.action !== 'completed' || !Number.isSafeInteger(event.workflow_run?.id)) return null;
package/src/triage.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { validateEstimate } from './request-triage.mjs';
2
+
1
3
  const DIMENSIONS = Object.freeze({
2
4
  size: ['S', 'M', 'L', 'XL'],
3
5
  urgency: ['normal', 'asap', 'now'],
@@ -126,6 +128,7 @@ function validateAssessment(assessment, errors) {
126
128
  addIssue(errors, 'assessment.invalid', 'assessment must be an object', 'assessment');
127
129
  return;
128
130
  }
131
+ if (assessment.estimate !== undefined) for (const message of validateEstimate(assessment.estimate, { provisional: true })) addIssue(errors, 'assessment.estimate', message, 'assessment.estimate');
129
132
  for (const dimension of Object.keys(DIMENSIONS)) {
130
133
  const value = normalizedValue(dimension, assessment[dimension === 'blast-radius' ? 'blastRadius' : dimension]);
131
134
  if (!DIMENSIONS[dimension].includes(value) && !(dimension === 'blast-radius' && value === 'unknown')) {
@@ -254,6 +257,7 @@ export function buildTriageReport(input) {
254
257
  canApply: status === 'ready',
255
258
  workItem: input.workItem ?? null,
256
259
  assessment: {
260
+ ...(assessment.estimate !== undefined ? { estimate: assessment.estimate } : {}),
257
261
  size: normalizedValue('size', assessment.size),
258
262
  urgency: normalizedValue('urgency', assessment.urgency),
259
263
  impact: normalizedValue('impact', assessment.impact),
@@ -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
@@ -1,4 +1,4 @@
1
- # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b53e69b59b107e49da418b572f7b91a908ecc6640f2e9a5ecf8af1e418467b0c","body_hash":"8151890e19043f86541decb00d0dc86741daec051c004feb7beab27b025e3621","compiler_version":"v0.93.2","agent_id":"opencode","engine_versions":{"opencode":"2.1.0"},"agent_image_runner":"[\"self-hosted\",\"linux\",\"lighthouse-workflow\"]"}
1
+ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"9c9a74c51b01ca45a33547aa564ae10c46766c84093b5f96471b20bce2a505ac","body_hash":"8151890e19043f86541decb00d0dc86741daec051c004feb7beab27b025e3621","compiler_version":"v0.93.2","agent_id":"opencode","engine_versions":{"opencode":"2.1.0"},"agent_image_runner":"[\"self-hosted\",\"linux\",\"lighthouse-workflow\"]"}
2
2
  # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/checkout","sha":"fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09","version":"v5"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/setup-node","sha":"a0853c24544627f65ddf259abe73b1d18a591444","version":"v5"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"},{"repo":"github/gh-aw-actions/setup","sha":"d9128eb3ea40c18b77c76184eda03c0ff2608d79","version":"v0.93.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:41835613992df88f51e923bad486f96dfb2e6cda29f13e217df41833284ae382","pinned_image":"ghcr.io/github/gh-aw-node@sha256:41835613992df88f51e923bad486f96dfb2e6cda29f13e217df41833284ae382"}],"mcp_servers":[{"name":"safeoutputs","tools":["noop"]}],"threat_detection":{"mode":"disabled"},"admission":false}
3
3
  # This file was automatically generated by gh-aw (v0.93.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
4
4
  #
@@ -55,7 +55,7 @@
55
55
  # Container images used:
56
56
  # - ghcr.io/github/gh-aw-node@sha256:41835613992df88f51e923bad486f96dfb2e6cda29f13e217df41833284ae382
57
57
 
58
- name: "Lighthouse SDLC apply-review"
58
+ name: "apply review"
59
59
  on:
60
60
  # roles: all # Roles processed as role check in pre-activation job
61
61
  workflow_dispatch:
@@ -90,7 +90,7 @@ env:
90
90
  LIGHTHOUSE_WORKER: apply-review
91
91
  OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.GH_AW_DEFAULT_OTLP_ENDPOINT || vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}
92
92
  OTEL_SERVICE_NAME: gh-aw.lighthouse-sdlc-apply-review
93
- OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Lighthouse%20SDLC%20apply-review,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=opencode'
93
+ OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=apply%20review,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=opencode'
94
94
  OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}
95
95
  GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ secrets.GH_AW_DEFAULT_OTLP_ENDPOINT || vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]'
96
96
  GH_AW_OTLP_IF_MISSING: ignore
@@ -139,7 +139,7 @@ jobs:
139
139
  parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }}
140
140
  safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }}
141
141
  env:
142
- GH_AW_SETUP_WORKFLOW_NAME: "Lighthouse SDLC apply-review"
142
+ GH_AW_SETUP_WORKFLOW_NAME: "apply review"
143
143
  GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/lighthouse-sdlc-apply-review.lock.yml@${{ github.ref }}
144
144
  GH_AW_INFO_VERSION: "2.1.0"
145
145
  GH_AW_INFO_ENGINE_ID: "opencode"
@@ -155,7 +155,7 @@ jobs:
155
155
  GH_AW_INFO_VERSION: "2.1.0"
156
156
  GH_AW_INFO_AGENT_VERSION: "2.1.0"
157
157
  GH_AW_INFO_CLI_VERSION: "v0.93.2"
158
- GH_AW_INFO_WORKFLOW_NAME: "Lighthouse SDLC apply-review"
158
+ GH_AW_INFO_WORKFLOW_NAME: "apply review"
159
159
  GH_AW_INFO_WORKFLOW_ID: ${{ job.workflow_ref }}
160
160
  GH_AW_INFO_EXPERIMENTAL: "false"
161
161
  GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
@@ -204,7 +204,7 @@ jobs:
204
204
  if: ${{ env.GH_AW_ADMISSION_RESULT != 'admitted' && env.GH_AW_MAX_DAILY_AI_CREDITS != '' }}
205
205
  uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
206
206
  env:
207
- GH_AW_WORKFLOW_NAME: "Lighthouse SDLC apply-review"
207
+ GH_AW_WORKFLOW_NAME: "apply review"
208
208
  GH_AW_WORKFLOW_ID: "lighthouse-sdlc-apply-review"
209
209
  GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
210
210
  GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }}
@@ -278,7 +278,7 @@ jobs:
278
278
  GH_AW_COMPILED_VERSION: "v0.93.2"
279
279
  GH_AW_HAS_SAFE_OUTPUTS: "true"
280
280
  GH_AW_BLOCKED_VERSION_REPORT_AS_ISSUE: "false"
281
- GH_AW_WORKFLOW_NAME: "Lighthouse SDLC apply-review"
281
+ GH_AW_WORKFLOW_NAME: "apply review"
282
282
  with:
283
283
  script: |
284
284
  const path = require('path');
@@ -458,7 +458,7 @@ jobs:
458
458
  trace-id: ${{ needs.activation.outputs.setup-trace-id }}
459
459
  parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
460
460
  env:
461
- GH_AW_SETUP_WORKFLOW_NAME: "Lighthouse SDLC apply-review"
461
+ GH_AW_SETUP_WORKFLOW_NAME: "apply review"
462
462
  GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/lighthouse-sdlc-apply-review.lock.yml@${{ github.ref }}
463
463
  GH_AW_INFO_VERSION: "2.1.0"
464
464
  GH_AW_INFO_ENGINE_ID: "opencode"
@@ -981,7 +981,7 @@ jobs:
981
981
  trace-id: ${{ needs.activation.outputs.setup-trace-id }}
982
982
  parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
983
983
  env:
984
- GH_AW_SETUP_WORKFLOW_NAME: "Lighthouse SDLC apply-review"
984
+ GH_AW_SETUP_WORKFLOW_NAME: "apply review"
985
985
  GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/lighthouse-sdlc-apply-review.lock.yml@${{ github.ref }}
986
986
  GH_AW_INFO_VERSION: "2.1.0"
987
987
  GH_AW_INFO_ENGINE_ID: "opencode"
@@ -1018,7 +1018,7 @@ jobs:
1018
1018
  env:
1019
1019
  GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
1020
1020
  GH_AW_NOOP_MAX: "2"
1021
- GH_AW_WORKFLOW_NAME: "Lighthouse SDLC apply-review"
1021
+ GH_AW_WORKFLOW_NAME: "apply review"
1022
1022
  GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/lighthouse-sdlc-apply-review.md"
1023
1023
  GH_AW_ENGINE_ID: "opencode"
1024
1024
  GH_AW_ENGINE_VERSION: "2.1.0"
@@ -1044,7 +1044,7 @@ jobs:
1044
1044
  uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
1045
1045
  env:
1046
1046
  GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
1047
- GH_AW_WORKFLOW_NAME: "Lighthouse SDLC apply-review"
1047
+ GH_AW_WORKFLOW_NAME: "apply review"
1048
1048
  GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/lighthouse-sdlc-apply-review.md"
1049
1049
  GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
1050
1050
  GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
@@ -1086,7 +1086,7 @@ jobs:
1086
1086
  uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
1087
1087
  env:
1088
1088
  GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
1089
- GH_AW_WORKFLOW_NAME: "Lighthouse SDLC apply-review"
1089
+ GH_AW_WORKFLOW_NAME: "apply review"
1090
1090
  GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/lighthouse-sdlc-apply-review.md"
1091
1091
  GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
1092
1092
  GH_AW_REPORT_FAILED_JOBS: "true"
@@ -1217,7 +1217,7 @@ jobs:
1217
1217
  destination: ${{ runner.temp }}/gh-aw/actions
1218
1218
  job-name: ${{ github.job }}
1219
1219
  env:
1220
- GH_AW_SETUP_WORKFLOW_NAME: "Lighthouse SDLC apply-review"
1220
+ GH_AW_SETUP_WORKFLOW_NAME: "apply review"
1221
1221
  GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/lighthouse-sdlc-apply-review.lock.yml@${{ github.ref }}
1222
1222
  GH_AW_INFO_VERSION: "2.1.0"
1223
1223
  GH_AW_INFO_ENGINE_ID: "opencode"
@@ -1231,6 +1231,7 @@ jobs:
1231
1231
  actions: read
1232
1232
  contents: read
1233
1233
  issues: write
1234
+ pull-requests: write
1234
1235
  env:
1235
1236
  LIGHTHOUSE_ISSUE: ${{ inputs.issue }}
1236
1237
  outputs: