@juangcarmona/lighthouse-sdlc 0.18.11 → 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. Parents with an existing PR need a separate continuation path.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse-sdlc",
3
- "version": "0.18.11",
3
+ "version": "0.18.12",
4
4
  "type": "module",
5
5
  "description": "Provider-neutral SDLC skills, commands and route contracts for Lighthouse adopters",
6
6
  "repository": {
@@ -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}`);
@@ -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 (!proposalReview && !implementationReview && !productResume && !epicImplementation && revision.sha !== (dispatch?.request.sourceRevision ?? env.GITHUB_SHA)) throw Error('Workflow must run against the current default branch');
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.pullRequest && !snapshot.productParent.review ? '\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.';
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');
@@ -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;
@@ -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.pullRequest || request.productParent.review || envelope.result?.status !== 'passed' || envelope.result.output?.productChange?.outcome !== 'aligned') return false;
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.pullRequest || parent.review) return null;
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
- return { issue: snapshot.issue, stage: parent.stage, parent, alignmentRun: { runId: String(run.id), attempt: run.run_attempt }, baseRevision };
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