@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.
- package/README.md +48 -125
- package/capabilities.json +14 -8
- package/completion.md +2 -2
- package/github-actions.md +104 -0
- package/host-contract.md +1 -1
- package/model-profiles.md +101 -0
- package/package.json +9 -1
- package/request-triage.md +32 -0
- package/schemas/estimate.schema.json +23 -0
- package/schemas/request-analysis.schema.json +37 -0
- package/schemas/triage.schema.json +1 -0
- package/skills/concise-results/SKILL.md +10 -0
- package/skills/refine/SKILL.md +1 -1
- package/skills/triage/SKILL.md +3 -3
- package/src/completion.mjs +1 -1
- package/src/host-contract.mjs +3 -0
- package/src/implementation.mjs +6 -1
- package/src/install-workflows.mjs +11 -5
- package/src/integration-evidence.mjs +1 -1
- package/src/issue-actions.mjs +62 -18
- package/src/model-profiles.mjs +58 -0
- package/src/opencode-execute.mjs +17 -4
- package/src/pr-state-actions.mjs +106 -0
- package/src/pr-state.mjs +75 -0
- package/src/profile-delegation.mjs +111 -0
- package/src/proposal.mjs +1 -1
- package/src/request-triage.mjs +42 -0
- package/src/stage-dispatch.mjs +21 -5
- package/src/stage-transition.mjs +10 -0
- package/src/triage.mjs +4 -0
- package/templates/change-request.yml +24 -0
- package/workflows/lighthouse-sdlc-apply-review.lock.yml +14 -13
- package/workflows/lighthouse-sdlc-apply-review.md +2 -1
- package/workflows/lighthouse-sdlc-implement.lock.yml +13 -13
- package/workflows/lighthouse-sdlc-implement.md +1 -1
- package/workflows/lighthouse-sdlc-integrate.lock.yml +13 -13
- package/workflows/lighthouse-sdlc-integrate.md +1 -1
- package/workflows/lighthouse-sdlc-propose.lock.yml +13 -13
- package/workflows/lighthouse-sdlc-propose.md +1 -1
- package/workflows/lighthouse-sdlc-refine.lock.yml +18 -13
- package/workflows/lighthouse-sdlc-refine.md +6 -1
- package/workflows/lighthouse-sdlc-review.lock.yml +13 -13
- package/workflows/lighthouse-sdlc-review.md +1 -1
- package/workflows/lighthouse-sdlc-router.yml +38 -5
- package/workflows/lighthouse-sdlc-triage.lock.yml +283 -1169
- 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; });
|
package/src/pr-state.mjs
ADDED
|
@@ -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');
|
package/src/stage-dispatch.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
}
|
package/src/stage-transition.mjs
CHANGED
|
@@ -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":"
|
|
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: "
|
|
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=
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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:
|