@juangcarmona/lighthouse-sdlc 0.18.8 → 0.18.9

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 and the result did not follow a product answer. 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 or a later product answer 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. 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.
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.8",
3
+ "version": "0.18.9",
4
4
  "type": "module",
5
5
  "description": "Provider-neutral SDLC skills, commands and route contracts for Lighthouse adopters",
6
6
  "repository": {
@@ -351,7 +351,7 @@ async function main(command) {
351
351
  const metadata = await api(''), revision = (await api(`commits/${metadata.default_branch}`)).sha;
352
352
  if ((!productEntry && !eligibleIssueAuthor(issue, owner, stage)) || issue.state !== 'open' && stage !== 'review' || issue.pull_request || issue.labels.some(x => x.name === 'human-working' || x.name === 'agent-working' && !queue)) throw Error('Issue not eligible for dispatch');
353
353
  if (route.productResume && (route.productResume.baseRevision !== revision || (route.productResume.parent.feedback?.taskDigest ?? route.productResume.parent.taskDigest) !== taskDigest(issue, answers) || issue.labels.some(x => ['future', 'needs-info', 'waiting-review'].includes(x.name)))) throw Error('Applied product parent, intent or authorization changed');
354
- if (route.productAlignment && (route.productAlignment.baseRevision !== revision || route.productAlignment.parent.taskDigest !== taskDigest(issue, answers) || issue.labels.some(x => ['future', 'needs-info', 'waiting-review'].includes(x.name)))) throw Error('Aligned product parent, intent or authorization changed');
354
+ if (route.productAlignment && (route.productAlignment.baseRevision !== revision || (route.productAlignment.parent.feedback?.taskDigest ?? route.productAlignment.parent.taskDigest) !== taskDigest(issue, answers) || issue.labels.some(x => ['future', 'needs-info', 'waiting-review'].includes(x.name)))) throw Error('Aligned product parent, intent or authorization changed');
355
355
  if (route.productParent && (route.productParent.baseRevision !== revision || route.productParent.taskDigest !== taskDigest(issue, answers) || issue.labels.some(x => ['future', 'needs-info', 'waiting-review'].includes(x.name)))) throw Error('Product prerequisite parent, intent or authorization changed');
356
356
  if (handoff) {
357
357
  const prior = await readActionsJson(api, repo, route.predecessor.runId, `lighthouse-input-${issueNumber}-${route.predecessor.attempt}`, 'request.json');
@@ -707,7 +707,7 @@ async function main(command) {
707
707
  reviewTarget = true;
708
708
  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
709
  } 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 && !snapshot.replyTo ? '\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.';
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.';
711
711
  } else {
712
712
  proposalLink = product.question ? `\n\nProduct preparation is waiting for one owner decision: ${product.question}` : `\n\nProduct preparation is blocked: ${product.blocker}`;
713
713
  }
@@ -154,7 +154,7 @@ export async function loadDispatch(api, repository, owner, issue, commentId, wor
154
154
  if (request.productAlignment) {
155
155
  const run = await api(`actions/runs/${request.productAlignment.alignmentRun.runId}`);
156
156
  const observed = await (await import('./stage-transition.mjs')).productAlignmentAfterRun(api, repository, owner, { action: 'completed', workflow_run: { id: run.id, head_sha: run.head_sha } }, readArtifact);
157
- if (JSON.stringify(observed) !== JSON.stringify(request.productAlignment) || observed.baseRevision !== request.sourceRevision || observed.parent.taskDigest !== request.taskDigest || observed.stage !== request.stage) throw Error('Product alignment or parent authorization changed');
157
+ if (JSON.stringify(observed) !== JSON.stringify(request.productAlignment) || observed.baseRevision !== request.sourceRevision || (observed.parent.feedback?.taskDigest ?? observed.parent.taskDigest) !== request.taskDigest || observed.stage !== request.stage) throw Error('Product alignment or parent authorization changed');
158
158
  }
159
159
  if (request.productResume) {
160
160
  const apply = await api(`actions/runs/${request.productResume.applyRun.runId}`);
@@ -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 || request.replyTo || envelope.result?.status !== 'passed' || envelope.result.output?.productChange?.outcome !== 'aligned') return false;
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;
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) } });
@@ -116,7 +116,7 @@ export async function productGapAfterRun(api, repository, owner, event, readArti
116
116
 
117
117
  // Recheck the original parent and, when present, the authenticated answer to a
118
118
  // product question. The answer changes context; it does not approve product intent.
119
- export async function verifyProductParent(api, repository, owner, parent, issue, reports, currentBase, readArtifact = readActionsJson) {
119
+ export async function verifyProductParent(api, repository, owner, parent, issue, reports, currentBase, readArtifact = readActionsJson, allowAligned = false) {
120
120
  const { feedback, ...original } = parent;
121
121
  const parentRun = await api(`actions/runs/${original.predecessor.runId}`);
122
122
  const observed = await productGapAfterRun(api, repository, owner, { action: 'completed', workflow_run: { id: parentRun.id, head_sha: parentRun.head_sha } }, readArtifact);
@@ -127,7 +127,7 @@ export async function verifyProductParent(api, repository, owner, parent, issue,
127
127
  if (run.path !== stagePath('product-change') || run.status !== 'completed' || run.conclusion !== 'success' || run.run_attempt !== feedback.attempt) throw Error('Product answer execution is unavailable');
128
128
  const snapshot = await readArtifact(api, repository, run.id, `lighthouse-input-${issue.number}-${run.run_attempt}`, 'request.json');
129
129
  const envelope = await readArtifact(api, repository, run.id, `lighthouse-result-${issue.number}-${run.run_attempt}`, 'envelope.json');
130
- if (!snapshot.dispatch || snapshot.issue !== issue.number || String(snapshot.workflowRunId) !== String(run.id) || snapshot.taskDigest !== feedback.taskDigest || snapshot.replyTo !== feedback.replyTo || snapshot.commentId !== feedback.commentId || JSON.stringify(snapshot.productParent) !== JSON.stringify(original) || validateStageEnvelope(envelope, snapshot).length || envelope.result.status !== 'passed' || envelope.result.output?.productChange?.outcome !== 'candidate') throw Error('Product answer execution does not bind the parent and candidate');
130
+ if (!snapshot.dispatch || snapshot.issue !== issue.number || String(snapshot.workflowRunId) !== String(run.id) || snapshot.taskDigest !== feedback.taskDigest || snapshot.replyTo !== feedback.replyTo || snapshot.commentId !== feedback.commentId || JSON.stringify(snapshot.productParent) !== JSON.stringify(original) || validateStageEnvelope(envelope, snapshot).length || envelope.result.status !== 'passed' || envelope.result.output?.productChange?.outcome !== 'candidate' && !(allowAligned && envelope.result.output?.productChange?.outcome === 'aligned')) throw Error('Product answer execution does not bind the parent and outcome');
131
131
  const origin = await loadDispatch(api, repository, owner, issue.number, snapshot.dispatch.commentId, run, readArtifact);
132
132
  await checkDispatchClaim(api, repository, issue.number, snapshot.dispatch, run.id);
133
133
  const answer = origin.request.event?.comment;
@@ -163,19 +163,19 @@ export async function productAlignmentAfterRun(api, repository, owner, event, re
163
163
  const inputs = artifacts.artifacts.filter(a => /^lighthouse-input-[1-9][0-9]*-[1-9][0-9]*$/.test(a.name) && a.name.endsWith(`-${run.run_attempt}`) && !a.expired);
164
164
  if (inputs.length !== 1) return null;
165
165
  const snapshot = await readArtifact(api, repository, run.id, inputs[0].name, 'request.json');
166
- if (snapshot.worker !== 'product-change' || !snapshot.dispatch || !snapshot.productParent || snapshot.replyTo || inputs[0].name !== `lighthouse-input-${snapshot.issue}-${run.run_attempt}` || String(snapshot.workflowRunId) !== String(run.id)) return null;
166
+ if (snapshot.worker !== 'product-change' || !snapshot.dispatch || !snapshot.productParent || inputs[0].name !== `lighthouse-input-${snapshot.issue}-${run.run_attempt}` || String(snapshot.workflowRunId) !== String(run.id)) return null;
167
167
  const envelope = await readArtifact(api, repository, run.id, `lighthouse-result-${snapshot.issue}-${run.run_attempt}`, 'envelope.json');
168
168
  if (validateStageEnvelope(envelope, snapshot).length) throw Error('Invalid product alignment result');
169
- if (envelope.result.status !== 'passed' || envelope.result.output?.productChange?.outcome !== 'aligned' || envelope.result.output.productChange.entry !== 'parent-handoff') return null;
169
+ if (envelope.result.status !== 'passed' || envelope.result.output?.productChange?.outcome !== 'aligned' || envelope.result.output.productChange.entry !== (snapshot.replyTo ? 'feedback' : 'parent-handoff')) return null;
170
170
  const origin = await loadDispatch(api, repository, owner, snapshot.issue, snapshot.dispatch.commentId, run, readArtifact);
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 || JSON.stringify(origin.request.productParent) !== JSON.stringify(snapshot.productParent) || origin.request.taskDigest !== snapshot.taskDigest) throw Error('Product alignment authority changed');
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
- const parent = snapshot.productParent;
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
174
  if (parent.pullRequest || parent.review) 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
- await verifyProductParent(api, repository, owner, parent, issue, reports, baseRevision, readArtifact);
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
  return { issue: snapshot.issue, stage: parent.stage, parent, alignmentRun: { runId: String(run.id), attempt: run.run_attempt }, baseRevision };
181
181
  }