@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 +1 -1
- package/package.json +1 -1
- package/src/issue-actions.mjs +2 -2
- package/src/stage-dispatch.mjs +1 -1
- package/src/stage-transition.mjs +8 -8
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
|
|
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
package/src/issue-actions.mjs
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/stage-dispatch.mjs
CHANGED
|
@@ -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}`);
|
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.pullRequest || request.productParent.review ||
|
|
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
|
|
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 ||
|
|
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
|
}
|