@juangcarmona/lighthouse-sdlc 0.18.10 → 0.18.12
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/github-actions.md
CHANGED
|
@@ -50,7 +50,7 @@ Successful refinement explicitly wakes the router after publication and reservat
|
|
|
50
50
|
|
|
51
51
|
For acceptance or deliberate event replay, the owner can dispatch the router with `issue`, the expected next `stage` and `after_run` set to the completed execution ID. The bot can request authenticated refinement-to-implementation, product-prerequisite and applied-product parent continuations. Each request is a wake-up: the router verifies the originating run and its retained authority. Both use the same fresh observations and deduplication as `workflow_run`; neither can force a transition or resend an uncertain request. If the bounded wait expires, inspect the source run and replay after it completes. An expired request artifact, changed source, changed intent or edited receipt blocks admission. Request artifacts last 14 days. Inspect origin and worker runs before reconciling uncertainty; there is no database, persistent dispatcher or SoFa dependency.
|
|
52
52
|
|
|
53
|
-
When an authenticated Product Change worker finds that accepted product intent already covers its parent request, the router can resume the original stage if the parent has no existing PR. It verifies any owner answer against its original question. The original scope and automatic implementation setting still apply. A repeated product gap stops for maintainer review instead of starting another alignment cycle.
|
|
53
|
+
When an authenticated Product Change worker finds that accepted product intent already covers its parent request, the router can resume the original stage if the parent has no existing PR, or if its implementation stage has an unchanged draft implementation PR without pending review feedback. It verifies any owner answer against its original question and preserves the existing PR. The original scope and automatic implementation setting still apply. A repeated product gap stops for maintainer review instead of starting another alignment cycle. Other parents with an existing PR need a separate continuation path.
|
|
54
54
|
|
|
55
55
|
The worker creates the OpenSpec plan, implements it and runs checks in one workspace. The PR contains both the plan and code. A separate proposal PR remains available for design-only work; merging one is not an implementation prerequisite.
|
|
56
56
|
|
package/package.json
CHANGED
package/src/implementation.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { createHash } from 'node:crypto';
|
|
|
6
6
|
import { proposalRoot, changeName } from './proposal.mjs';
|
|
7
7
|
import { validateEnvironment } from './execution-environment.mjs';
|
|
8
8
|
import { checkProductResumePullRequest, productImplementationProof } from './product-change.mjs';
|
|
9
|
+
import { checkAlignedImplementationPullRequest } from './proposal-review.mjs';
|
|
9
10
|
|
|
10
11
|
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
11
12
|
const safePath = path => typeof path === 'string' && path.length < 300 && !path.startsWith('/') && !path.includes('\\') && !path.split('/').some(p => !p || p === '.' || p === '..' || p === '.git');
|
|
@@ -154,6 +155,16 @@ export async function createImplementationPullRequest(api, snapshot, changes, bo
|
|
|
154
155
|
await beforeWrite();
|
|
155
156
|
await checkEpicBase();
|
|
156
157
|
const description = marker => `${body.trim()}\n\nRefs #${snapshot.issue}.\n\nNext: ${draft ? 'complete the candidate and its checks' : 'wait for required CI, then review this PR'}. [Run and evidence](${runUrl}).\n\n<!-- lighthouse-implementation:${marker} -->`;
|
|
158
|
+
if (snapshot.productAlignment?.parent.pullRequest) {
|
|
159
|
+
const prior = snapshot.productAlignment.parent.pullRequest;
|
|
160
|
+
const current = await api(`pulls/${prior.number}`);
|
|
161
|
+
checkAlignedImplementationPullRequest(current, snapshot.repository, snapshot.productAlignment);
|
|
162
|
+
await api(`git/refs/heads/${prior.branch}`, 'PATCH', { sha: created.sha, force: false });
|
|
163
|
+
const proof = productImplementationProof(current);
|
|
164
|
+
const preserved = proof ? current.body.replace(/<!-- lighthouse-product-implementation:\{[^\n]*\} -->/, `<!-- lighthouse-product-implementation:${JSON.stringify({ ...proof, runId: snapshot.runId, head: created.sha })} -->`) : current.body;
|
|
165
|
+
await api(`pulls/${prior.number}`, 'PATCH', { body: `${preserved}\n\n${body.trim()}\n\n[Implementation evidence](${runUrl}).` });
|
|
166
|
+
return { number: prior.number, html_url: current.html_url, head: { sha: created.sha, ref: prior.branch } };
|
|
167
|
+
}
|
|
157
168
|
if (snapshot.productResume) {
|
|
158
169
|
const resume = snapshot.productResume;
|
|
159
170
|
const current = await api(`pulls/${resume.pullRequest}`);
|
package/src/issue-actions.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { resolve, join } from 'node:path';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { isDeepStrictEqual } from 'node:util';
|
|
10
|
-
import { feedbackKey, feedbackWorker, checkProposal, reviewThread, reviewTarget, loadProposalReview, loadProposalFiles, checkReviewCurrent, updateProposal } from './proposal-review.mjs';
|
|
10
|
+
import { feedbackKey, feedbackWorker, checkAlignedImplementationPullRequest, checkProposal, reviewThread, reviewTarget, loadProposalReview, loadProposalFiles, checkReviewCurrent, updateProposal } from './proposal-review.mjs';
|
|
11
11
|
import { loadImplementation, loadImplementationSeed, changesDigest, validateChanges, validateCheckReceipt, createImplementationPullRequest } from './implementation.mjs';
|
|
12
12
|
import { digest, validateStageEnvelope } from './host-contract.mjs';
|
|
13
13
|
import { loadRefinement, loadStageReport, prepareProposal, proposalRoot, createProposalPullRequest } from './proposal.mjs';
|
|
@@ -393,6 +393,8 @@ async function main(command) {
|
|
|
393
393
|
if (handoff && !after) route = { run: env.GITHUB_ACTOR === owner && !reviewTarget(event, env.GITHUB_EVENT_NAME) && ['issues', 'issue_comment', 'workflow_dispatch'].includes(env.GITHUB_EVENT_NAME), stage: 'implement', issue: issueNumber };
|
|
394
394
|
const productWorker = dispatch?.request.worker === 'product-change';
|
|
395
395
|
const productResume = dispatch?.request.productResume;
|
|
396
|
+
const productAlignment = dispatch?.request.productAlignment;
|
|
397
|
+
const alignedPr = productAlignment?.parent.pullRequest;
|
|
396
398
|
if (!route.run || issueNumber !== route.issue || env.GITHUB_TRIGGERING_ACTOR !== owner && !dispatch) throw Error('Stage not authorized');
|
|
397
399
|
const issue = await api(issuePath);
|
|
398
400
|
if (!productWorker && !eligibleIssueAuthor(issue, owner, route.stage) || issue.state !== 'open' || issue.pull_request || issue.labels.some(x => ['agent-working', 'human-working'].includes(x.name))) throw Error('Issue not eligible');
|
|
@@ -409,8 +411,9 @@ async function main(command) {
|
|
|
409
411
|
await native.checkEpicCapacity({ api, repository: repo, owner, child: issueNumber, current: epic, capacity: Number(env.LIGHTHOUSE_EPIC_CAPACITY || '1'), excludeRequest: dispatch.request.id });
|
|
410
412
|
} else if (baseSha !== undefined) throw Error('Unexpected epic implementation base');
|
|
411
413
|
}
|
|
412
|
-
const revision = { sha: productResume ? productResume.head : implementationReview ? implementationReview.head : epicImplementation ? dispatch.request.epic.baseSha : proposalReview?.productProposal ? proposalReview.head : baseRevision };
|
|
413
|
-
if (
|
|
414
|
+
const revision = { sha: productResume ? productResume.head : alignedPr ? alignedPr.head : implementationReview ? implementationReview.head : epicImplementation ? dispatch.request.epic.baseSha : proposalReview?.productProposal ? proposalReview.head : baseRevision };
|
|
415
|
+
if (alignedPr) checkAlignedImplementationPullRequest(await api(`pulls/${alignedPr.number}`), repo, productAlignment);
|
|
416
|
+
if (!proposalReview && !implementationReview && !productResume && !alignedPr && !epicImplementation && revision.sha !== (dispatch?.request.sourceRevision ?? env.GITHUB_SHA)) throw Error('Workflow must run against the current default branch');
|
|
414
417
|
const all = await comments(), answers = ownerAnswers(all);
|
|
415
418
|
if (dispatch && (dispatch.request.sourceRevision !== baseRevision || dispatch.request.taskDigest !== taskDigest(issue, answers))) throw Error('Dispatched source or owner intent changed');
|
|
416
419
|
if (dispatch && route.stage !== dispatch.request.stage) throw Error('Dispatched request no longer selects its stage');
|
|
@@ -445,7 +448,7 @@ async function main(command) {
|
|
|
445
448
|
if (report) {
|
|
446
449
|
triage = await loadStageReport(api, repo, report, { owner, issue: issueNumber });
|
|
447
450
|
const errors = validateStageEnvelope(triage.envelope, triage.request);
|
|
448
|
-
if (errors.length || triage.request.stage !== 'triage' || triage.envelope.result.status !== 'passed' || triage.request.sourceRevision !== (productResume?.baseRevision ?? (epicImplementation ? baseRevision : revision.sha)) || triage.request.originalRequest !== (issue.body ?? '')) throw Error('Triage evidence is invalid or stale; request triage again');
|
|
451
|
+
if (errors.length || triage.request.stage !== 'triage' || triage.envelope.result.status !== 'passed' || triage.request.sourceRevision !== (productResume?.baseRevision ?? (epicImplementation || alignedPr ? baseRevision : revision.sha)) || triage.request.originalRequest !== (issue.body ?? '')) throw Error('Triage evidence is invalid or stale; request triage again');
|
|
449
452
|
}
|
|
450
453
|
}
|
|
451
454
|
if (stage === 'implement' && !productWorker) {
|
|
@@ -456,11 +459,11 @@ async function main(command) {
|
|
|
456
459
|
checkHandoff(prior, result, issue, answers, revision.sha, dispatch?.request.predecessor?.runId ?? dispatch?.request.routerRunId ?? env.GITHUB_RUN_ID);
|
|
457
460
|
}
|
|
458
461
|
if (issue.labels.some(x => x.name === 'future')) throw Error('Implementation paused by future label');
|
|
459
|
-
if (issue.user.login !== owner && !handoff && !implementationReview && !productResume) await loadRefinement(api, repo, all, { owner, issue: issueNumber, sourceRevision: epicImplementation ? baseRevision : revision.sha, taskDigest: taskDigest(issue, answers) });
|
|
462
|
+
if (issue.user.login !== owner && !handoff && !implementationReview && !productResume) await loadRefinement(api, repo, all, { owner, issue: issueNumber, sourceRevision: epicImplementation || alignedPr ? baseRevision : revision.sha, taskDigest: taskDigest(issue, answers) });
|
|
460
463
|
implementation = await loadImplementation(api, issueNumber, owner, baseRevision, repository.default_branch);
|
|
461
|
-
seed = implementationReview ? null : await loadImplementationSeed(api, implementation.policy, revision.sha);
|
|
464
|
+
seed = implementationReview || alignedPr ? null : await loadImplementationSeed(api, implementation.policy, revision.sha);
|
|
462
465
|
const pulls = await api('pulls?state=open&per_page=100');
|
|
463
|
-
if (pulls.length >= 100 || pulls.some(pr => pr.number !== (implementationReview?.number ?? productResume?.pullRequest) && pr.head.ref.startsWith(`lighthouse/implement-${issueNumber}-`))) throw Error('Review the existing implementation PR before requesting another');
|
|
466
|
+
if (pulls.length >= 100 || pulls.some(pr => pr.number !== (implementationReview?.number ?? productResume?.pullRequest ?? alignedPr?.number) && pr.head.ref.startsWith(`lighthouse/implement-${issueNumber}-`))) throw Error('Review the existing implementation PR before requesting another');
|
|
464
467
|
const proposal = pulls.find(pr => pr.head.ref.startsWith(`lighthouse/propose-${issueNumber}-`));
|
|
465
468
|
if (proposal && !implementationReview) {
|
|
466
469
|
planningReference = checkProposal(await api(`pulls/${proposal.number}`), repo);
|
|
@@ -485,7 +488,7 @@ async function main(command) {
|
|
|
485
488
|
const productChangeName = productWorker ? dispatch.request.productFeedback?.changeName ?? dispatch.request.event.inputs?.change_name?.trim() ?? `issue-${issueNumber}-product-change` : undefined;
|
|
486
489
|
if (productWorker && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(productChangeName)) throw Error('Invalid Product Change name');
|
|
487
490
|
const baselineDigest = productWorker ? await productModelDigest(process.cwd()) : undefined;
|
|
488
|
-
const snapshot = { ...(dispatch?.request.epic ? { epic: dispatch.request.epic } : {}), ...(dispatch && executionModelSelection(dispatch.request) ? { modelSelection: executionModelSelection(dispatch.request) } : {}), autoImplement: env.LIGHTHOUSE_AUTO_IMPLEMENT === 'true', productChangeEnabled: dispatch?.request.productChangeEnabled === true, ...(dispatch ? { worker: dispatch.request.worker, workflowRevision: env.GITHUB_SHA, dispatch: { commentId: dispatch.commentId, requestId: dispatch.request.id, digest: dispatch.receipt.digest, routerRunId: dispatch.request.routerRunId, routerAttempt: dispatch.request.routerAttempt } } : {}), ...(productWorker ? { authorizationDigest, policyDigest, productChangeName, baselineDigest, productEntry: dispatch.request.productFeedback ? 'feedback' : dispatch.request.productParent ? 'parent-handoff' : 'idea', ...((dispatch.request.productParent ?? dispatch.request.productReview?.parent ?? continuation?.request.productParent) ? { productParent: dispatch.request.productParent ?? dispatch.request.productReview?.parent ?? continuation.request.productParent } : {}) } : {}), ...(planningReference ? { planningReference } : {}), ...(productResume ? { productResume, baseRevision } : {}), ...(dispatch?.request.productAlignment ? { productAlignment: dispatch.request.productAlignment } : {}), ...(implementationReview ? { implementationReview, baseRevision } : epicImplementation ? { baseRevision } : {}), ...(proposalReview ? { proposalReview, ...(proposalReview.productProposal ? { baseRevision } : {}) } : {}), repository: repo, runId: id, issue: issueNumber, stage, title: issue.title, defaultBranch: repository.default_branch, sourceRevision: revision.sha, taskDigest: taskDigest(issue, answers), workflowRunId: env.GITHUB_RUN_ID, ...(predecessor ? { refinement: predecessor } : {}), ...(continuation ? { replyTo: continuation.request.runId, commentId: continuationAnswer.id, answerHistory: continuationHistory(continuation, continuationAnswer) } : {}), ...(implementation ? { implementation } : {}), ...(seed ? { seedDigest: changesDigest(seed) } : {}) };
|
|
491
|
+
const snapshot = { ...(dispatch?.request.epic ? { epic: dispatch.request.epic } : {}), ...(dispatch && executionModelSelection(dispatch.request) ? { modelSelection: executionModelSelection(dispatch.request) } : {}), autoImplement: env.LIGHTHOUSE_AUTO_IMPLEMENT === 'true', productChangeEnabled: dispatch?.request.productChangeEnabled === true, ...(dispatch ? { worker: dispatch.request.worker, workflowRevision: env.GITHUB_SHA, dispatch: { commentId: dispatch.commentId, requestId: dispatch.request.id, digest: dispatch.receipt.digest, routerRunId: dispatch.request.routerRunId, routerAttempt: dispatch.request.routerAttempt } } : {}), ...(productWorker ? { authorizationDigest, policyDigest, productChangeName, baselineDigest, productEntry: dispatch.request.productFeedback ? 'feedback' : dispatch.request.productParent ? 'parent-handoff' : 'idea', ...((dispatch.request.productParent ?? dispatch.request.productReview?.parent ?? continuation?.request.productParent) ? { productParent: dispatch.request.productParent ?? dispatch.request.productReview?.parent ?? continuation.request.productParent } : {}) } : {}), ...(planningReference ? { planningReference } : {}), ...(productResume ? { productResume, baseRevision } : {}), ...(dispatch?.request.productAlignment ? { productAlignment: dispatch.request.productAlignment, ...(alignedPr ? { baseRevision } : {}) } : {}), ...(implementationReview ? { implementationReview, baseRevision } : epicImplementation ? { baseRevision } : {}), ...(proposalReview ? { proposalReview, ...(proposalReview.productProposal ? { baseRevision } : {}) } : {}), repository: repo, runId: id, issue: issueNumber, stage, title: issue.title, defaultBranch: repository.default_branch, sourceRevision: revision.sha, taskDigest: taskDigest(issue, answers), workflowRunId: env.GITHUB_RUN_ID, ...(predecessor ? { refinement: predecessor } : {}), ...(continuation ? { replyTo: continuation.request.runId, commentId: continuationAnswer.id, answerHistory: continuationHistory(continuation, continuationAnswer) } : {}), ...(implementation ? { implementation } : {}), ...(seed ? { seedDigest: changesDigest(seed) } : {}) };
|
|
489
492
|
if (stage === 'triage') snapshot.originalRequest = issue.body ?? '';
|
|
490
493
|
if (triage) snapshot.triageEstimate = triage.envelope.result.output.estimate;
|
|
491
494
|
await output('source_revision', revision.sha);
|
|
@@ -496,6 +499,7 @@ async function main(command) {
|
|
|
496
499
|
const context = JSON.parse(execFileSync(process.execPath, [contract, 'context', join(dir, 'request.json'), join(dir, 'issue.json')], { encoding: 'utf8' }));
|
|
497
500
|
if (productResume) { context.productResume = productResume; context.contract.rules += ' The approved Product Change was applied to the supplied issue branch. Resume only the original parent stage and its existing authorization. Preserve the applied product files and branch; product approval does not authorize merge or broaden implementation scope.'; }
|
|
498
501
|
if (snapshot.productAlignment) { context.productAlignment = snapshot.productAlignment; context.contract.rules += ' The authenticated Product Change worker found that accepted product intent already covers this request. Resume only the original parent stage and its existing authorization. This observation grants no new product decision, implementation scope or merge authority.'; }
|
|
502
|
+
if (alignedPr) context.contract.rules += ' Continue the existing draft implementation PR from its authenticated head. Preserve its content and the authorized scope.';
|
|
499
503
|
if (!productWorker && ['triage', 'refine', 'propose', 'implement'].includes(stage)) context.contract.rules += ' If the authorized issue requires an adjustment to accepted product intent, stop with status blocked and return output.productGap with only summary and scope. Treat that observation as context, not approval or scope authority. Do not use a handoff question for a technical product prerequisite. Set nextAction to: Workflow maintainer: ensure the optional Product Change workflow is installed and follow its authenticated handoff. If a product or scope decision itself is missing, ask one explicit owner question instead.';
|
|
500
504
|
if (continuation) {
|
|
501
505
|
context.continuation = continuationContext(continuation, continuationAnswer);
|
|
@@ -707,7 +711,7 @@ async function main(command) {
|
|
|
707
711
|
reviewTarget = true;
|
|
708
712
|
await summary(`### Product Change review\n[PR #${published.pullRequest.number}](${published.pullRequest.html_url})\n\nProvider overlay and OpenSpec validation passed. Waiting for the explicit owner product decision.`);
|
|
709
713
|
} else if (product.outcome === 'aligned') {
|
|
710
|
-
proposalLink = !snapshot.productParent ? '\n\nAccepted product intent already covers this idea. No Product Change was created; delivery still needs its own authorization.' : !snapshot.productParent.
|
|
714
|
+
proposalLink = !snapshot.productParent ? '\n\nAccepted product intent already covers this idea. No Product Change was created; delivery still needs its own authorization.' : !snapshot.productParent.review && (!snapshot.productParent.pullRequest || snapshot.productParent.stage === 'implement') ? '\n\nAccepted product intent already covers this request. The workflow will verify the original authorization before resuming its stage.' : '\n\nAccepted product intent already covers this request. The workflow maintainer must reconcile the parent continuation; no new delivery is authorized by this result.';
|
|
711
715
|
} else {
|
|
712
716
|
proposalLink = product.question ? `\n\nProduct preparation is waiting for one owner decision: ${product.question}` : `\n\nProduct preparation is blocked: ${product.blocker}`;
|
|
713
717
|
}
|
|
@@ -746,12 +750,13 @@ async function main(command) {
|
|
|
746
750
|
if (JSON.stringify(fresh) !== JSON.stringify(snapshot.implementation)) throw Error('Implementation authority changed');
|
|
747
751
|
const pulls = await api('pulls?state=open&per_page=100');
|
|
748
752
|
if (snapshot.productResume) checkProductResumePullRequest(await api(`pulls/${snapshot.productResume.pullRequest}`), repo, snapshot.productResume);
|
|
753
|
+
if (snapshot.productAlignment?.parent.pullRequest) checkAlignedImplementationPullRequest(await api(`pulls/${snapshot.productAlignment.parent.pullRequest.number}`), repo, snapshot.productAlignment);
|
|
749
754
|
if (snapshot.implementationReview) {
|
|
750
755
|
if (snapshot.productResume) { if (digest(JSON.stringify(await api(snapshot.implementationReview.path))) !== snapshot.implementationReview.feedbackDigest) throw Error('Implementation feedback changed'); }
|
|
751
756
|
else await checkReviewCurrent(api, repo, snapshot.implementationReview, true);
|
|
752
757
|
if (digest(JSON.stringify(await reviewThread(api, snapshot.implementationReview.number, owner))) !== snapshot.implementationReview.threadDigest) throw Error('PR feedback changed');
|
|
753
758
|
}
|
|
754
|
-
if (pulls.length >= 100 || pulls.some(pr => pr.number !== (snapshot.implementationReview?.number ?? snapshot.productResume?.pullRequest) && pr.head.ref.startsWith(`lighthouse/implement-${issueNumber}-`))) throw Error('Existing implementation PR requires review');
|
|
759
|
+
if (pulls.length >= 100 || pulls.some(pr => pr.number !== (snapshot.implementationReview?.number ?? snapshot.productResume?.pullRequest ?? snapshot.productAlignment?.parent.pullRequest?.number) && pr.head.ref.startsWith(`lighthouse/implement-${issueNumber}-`))) throw Error('Existing implementation PR requires review');
|
|
755
760
|
};
|
|
756
761
|
const candidateComplete = value.result.status === 'passed' && checksPassed;
|
|
757
762
|
const checks = receipt.checks.map(c => `- \`${c.command.join(' ')}\`: exit ${c.exitCode}`).join('\n');
|
package/src/proposal-review.mjs
CHANGED
|
@@ -31,6 +31,12 @@ export function checkImplementationPullRequest(pr, repo, expected) {
|
|
|
31
31
|
return { number: pr.number, issue: Number(match[1]), head: pr.head.sha, branch: pr.head.ref, base: pr.base.ref, url: pr.html_url };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export function checkAlignedImplementationPullRequest(pr, repository, alignment) {
|
|
35
|
+
const parent = alignment?.parent, expected = parent?.pullRequest;
|
|
36
|
+
if (parent?.stage !== 'implement' || parent.review || !expected || pr?.number !== expected.number || !pr.draft) throw Error('Aligned implementation PR is not eligible');
|
|
37
|
+
return checkImplementationPullRequest(pr, repository, { issue: parent.issue, head: expected.head, branch: expected.branch, base: alignment.defaultBranch });
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
export async function loadProposalReview(api, repo, owner, event, eventName, implementation = false) {
|
|
35
41
|
const number = reviewTarget(event, eventName);
|
|
36
42
|
if (!number) return null;
|
package/src/stage-transition.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export async function signalProductApplyContinuation(api, issue, runId, checkpoi
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export async function signalProductAlignment(api, request, envelope) {
|
|
34
|
-
if (request.worker !== 'product-change' || !request.dispatch || !request.productParent || request.productParent.
|
|
34
|
+
if (request.worker !== 'product-change' || !request.dispatch || !request.productParent || request.productParent.review || request.productParent.pullRequest && request.productParent.stage !== 'implement' || envelope.result?.status !== 'passed' || envelope.result.output?.productChange?.outcome !== 'aligned') return false;
|
|
35
35
|
if (validateStageEnvelope(envelope, request).length) throw Error('Invalid product alignment result');
|
|
36
36
|
const repository = await api('');
|
|
37
37
|
await api('actions/workflows/lighthouse-sdlc-router.yml/dispatches', 'POST', { ref: repository.default_branch, inputs: { issue: String(request.issue), stage: request.productParent.stage, workflow: 'resume-aligned', after_run: String(request.workflowRunId) } });
|
|
@@ -171,13 +171,15 @@ export async function productAlignmentAfterRun(api, repository, owner, event, re
|
|
|
171
171
|
if (origin.request.worker !== 'product-change' || origin.request.id !== snapshot.dispatch.requestId || origin.request.stage !== snapshot.stage || origin.request.sourceRevision !== snapshot.sourceRevision || origin.request.workflowRevision !== snapshot.workflowRevision || !snapshot.replyTo && JSON.stringify(origin.request.productParent) !== JSON.stringify(snapshot.productParent) || origin.request.taskDigest !== snapshot.taskDigest) throw Error('Product alignment authority changed');
|
|
172
172
|
await checkDispatchClaim(api, repository, snapshot.issue, snapshot.dispatch, run.id);
|
|
173
173
|
const parent = snapshot.replyTo ? { ...snapshot.productParent, feedback: { runId: String(run.id), attempt: run.run_attempt, taskDigest: snapshot.taskDigest, replyTo: snapshot.replyTo, commentId: snapshot.commentId } } : snapshot.productParent;
|
|
174
|
-
if (parent.
|
|
174
|
+
if (parent.review || parent.pullRequest && parent.stage !== 'implement') return null;
|
|
175
175
|
const reports = await issueComments(api, snapshot.issue);
|
|
176
176
|
if (reports.filter(c => c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && c.body.includes(`<!-- lighthouse-actions:${snapshot.runId} -->`) && c.body.includes(`<!-- lighthouse-stage:${snapshot.stage}:passed -->`)).length !== 1) throw Error('Product alignment publication is unavailable or ambiguous');
|
|
177
177
|
const repo = await api(''), baseRevision = (await api(`commits/${encodeURIComponent(repo.default_branch)}`)).sha, issue = await api(`issues/${snapshot.issue}`);
|
|
178
178
|
await verifyProductParent(api, repository, owner, parent, issue, reports, baseRevision, readArtifact, true);
|
|
179
179
|
if (issue.state !== 'open' || issue.labels?.some(l => ['future', 'needs-info', 'waiting-review', 'human-working', 'agent-working'].includes(l.name))) throw Error('Product alignment continuation is paused');
|
|
180
|
-
|
|
180
|
+
const alignment = { issue: snapshot.issue, stage: parent.stage, parent, alignmentRun: { runId: String(run.id), attempt: run.run_attempt }, baseRevision, ...(parent.pullRequest ? { defaultBranch: repo.default_branch } : {}) };
|
|
181
|
+
if (parent.pullRequest) (await import('./proposal-review.mjs')).checkAlignedImplementationPullRequest(await api(`pulls/${parent.pullRequest.number}`), repository, alignment);
|
|
182
|
+
return alignment;
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
// A product apply is usable by a parent only when its original approval, provider
|
|
@@ -131,7 +131,7 @@ jobs:
|
|
|
131
131
|
if: >-
|
|
132
132
|
vars.LIGHTHOUSE_AUTOMATION_ENABLED == 'true' &&
|
|
133
133
|
(github.actor == github.repository_owner || github.event_name == 'workflow_run' ||
|
|
134
|
-
(github.event_name == 'workflow_dispatch' && github.actor == 'github-actions[bot]' && ((inputs.after_run != '' && ((inputs.stage == 'implement' && inputs.workflow != 'product-change') || inputs.workflow == 'product-change' || inputs.workflow == 'resume-product')) || (inputs.stage == 'triage' && inputs.epic_parent != '' && inputs.epic_run != '')))) &&
|
|
134
|
+
(github.event_name == 'workflow_dispatch' && github.actor == 'github-actions[bot]' && ((inputs.after_run != '' && ((inputs.stage == 'implement' && inputs.workflow != 'product-change') || inputs.workflow == 'product-change' || inputs.workflow == 'resume-product' || inputs.workflow == 'resume-aligned')) || (inputs.stage == 'triage' && inputs.epic_parent != '' && inputs.epic_run != '')))) &&
|
|
135
135
|
(github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run' ||
|
|
136
136
|
(github.event_name == 'issues' && (github.event.label.name == 'triage' || github.event.label.name == 'refine' || github.event.label.name == 'propose' || github.event.label.name == 'implement' || (github.event.action == 'unlabeled' && github.event.label.name == 'future'))) ||
|
|
137
137
|
github.event_name == 'issue_comment' || github.event_name == 'pull_request_review' || github.event_name == 'pull_request_review_comment')
|