@juangcarmona/lighthouse-sdlc 0.18.0 → 0.18.2
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 +3 -1
- package/package.json +1 -1
- package/src/handoff.mjs +3 -3
- package/src/issue-actions.mjs +10 -2
- package/src/worker-recovery.mjs +13 -10
package/github-actions.md
CHANGED
|
@@ -109,7 +109,9 @@ An active or queued execution without an Actions update for two hours is reporte
|
|
|
109
109
|
|
|
110
110
|
Read-only observation runs independently of the issue's execution lease, so reporting can inspect a stalled worker. Dispatch and reconciliation wait for that same per-issue lease before changing receipts, labels or handoffs. A separate report records the reconciliation outcome after the lease is available.
|
|
111
111
|
|
|
112
|
-
For the latest authenticated unattended run, maintenance reports
|
|
112
|
+
For the latest authenticated unattended run, maintenance reports a completed execution whose last handoff still says it is working or queued. It identifies confirmed failures and cancellations, and treats publication effects as unknown until inspected. A successful execution without its terminal handoff also receives an uncertain-outcome notice. Maintenance does not release a reservation or request a retry in these cases.
|
|
113
|
+
|
|
114
|
+
If publication succeeds but reservation cleanup fails, maintenance preserves the published result and asks the maintainer to inspect the reservation before releasing it.
|
|
113
115
|
|
|
114
116
|
### Worker error reports
|
|
115
117
|
|
package/package.json
CHANGED
package/src/handoff.mjs
CHANGED
|
@@ -54,7 +54,7 @@ export function handoff({ status, stage, happened, action, next, link, details }
|
|
|
54
54
|
|
|
55
55
|
// A newer authenticated dispatch owns current status, even if an older cleanup finishes late.
|
|
56
56
|
export async function publishObservedStop(api, { repository, issue, stage, run, attempt, dispatch, observation, sourceRevision, answerRetained = false, preparationReason }) {
|
|
57
|
-
if (!['failure', 'cancelled', 'pre-agent-cancelled', 'preparation-failure', 'provider-preparation-failure', 'preparation-cancelled', 'activation-failure', 'provider-failure', 'provider-cancelled', 'publication-unknown'].includes(observation) || !dispatch?.commentId) return false;
|
|
57
|
+
if (!['failure', 'cancelled', 'pre-agent-cancelled', 'preparation-failure', 'provider-preparation-failure', 'preparation-cancelled', 'activation-failure', 'provider-failure', 'provider-cancelled', 'publication-unknown', 'cleanup-failure'].includes(observation) || !dispatch?.commentId) return false;
|
|
58
58
|
const { checkDispatchClaim, issueComments, readDispatchReceipt } = await import('./stage-dispatch.mjs');
|
|
59
59
|
await checkDispatchClaim(api, repository, issue, dispatch, run);
|
|
60
60
|
const comments = await issueComments(api, issue);
|
|
@@ -77,9 +77,9 @@ export async function publishObservedStop(api, { repository, issue, stage, run,
|
|
|
77
77
|
}
|
|
78
78
|
const available = adopted && await api('labels/execution-failed', 'GET', undefined, true);
|
|
79
79
|
if (available) await api(`issues/${issue}/labels`, 'POST', { labels: ['execution-failed'] });
|
|
80
|
-
const status = ['cancelled', 'pre-agent-cancelled', 'preparation-cancelled', 'provider-cancelled'].includes(observation) ? 'cancelled' : observation === 'publication-unknown' ? 'unknown' : 'failed';
|
|
80
|
+
const status = ['cancelled', 'pre-agent-cancelled', 'preparation-cancelled', 'provider-cancelled'].includes(observation) ? 'cancelled' : observation === 'publication-unknown' ? 'unknown' : observation === 'cleanup-failure' ? 'blocked' : 'failed';
|
|
81
81
|
const missingChecks = observation === 'preparation-failure' && preparationReason === 'missing-implementation-checks';
|
|
82
|
-
const result = observation === 'publication-unknown' ? 'Publication did not complete cleanly; its effects and final result are not confirmed.' : missingChecks ? 'Preparation stopped because the repository has no implementation verification policy; the agent did not start.' : observation === 'preparation-failure' ? 'Preparation failed before the agent started; no validated result was published.' : observation === 'provider-preparation-failure' ? 'Product Change preparation failed before the provider started.' : observation === 'preparation-cancelled' ? 'Preparation was cancelled before work started; no validated result was published.' : observation === 'activation-failure' ? 'The execution environment failed before the agent started; no validated result was published.' : observation === 'provider-failure' ? 'The credential-free Product Change provider failed before publication.' : observation === 'provider-cancelled' ? 'The credential-free Product Change provider was cancelled before publication.' : observation === 'pre-agent-cancelled' ? 'The execution was cancelled before the agent started; no validated result was published.' : observation === 'failure' ? 'The agent execution failed before a validated result could be published.' : 'The execution was cancelled before a validated result could be published.';
|
|
82
|
+
const result = observation === 'cleanup-failure' ? 'The validated result was published, but reservation cleanup did not complete.' : observation === 'publication-unknown' ? 'Publication did not complete cleanly; its effects and final result are not confirmed.' : missingChecks ? 'Preparation stopped because the repository has no implementation verification policy; the agent did not start.' : observation === 'preparation-failure' ? 'Preparation failed before the agent started; no validated result was published.' : observation === 'provider-preparation-failure' ? 'Product Change preparation failed before the provider started.' : observation === 'preparation-cancelled' ? 'Preparation was cancelled before work started; no validated result was published.' : observation === 'activation-failure' ? 'The execution environment failed before the agent started; no validated result was published.' : observation === 'provider-failure' ? 'The credential-free Product Change provider failed before publication.' : observation === 'provider-cancelled' ? 'The credential-free Product Change provider was cancelled before publication.' : observation === 'pre-agent-cancelled' ? 'The execution was cancelled before the agent started; no validated result was published.' : observation === 'failure' ? 'The agent execution failed before a validated result could be published.' : 'The execution was cancelled before a validated result could be published.';
|
|
83
83
|
const gap = ['failure', 'preparation-failure', 'provider-preparation-failure', 'activation-failure', 'provider-failure'].includes(observation) && !available ? ' The execution-failed label is not adopted or available; the maintainer must review label capability adoption.' : '';
|
|
84
84
|
const reservation = item.labels.some(l => l.name === 'agent-working') ? ' The maintainer must also inspect the remaining agent-working reservation and release it only after verifying that no newer worker owns it.' : '';
|
|
85
85
|
const action = (missingChecks ? 'The repository maintainer must configure checks in .lighthouse/implement.json or add npm run verify before requesting implementation again.' : observation === 'publication-unknown' ? 'The workflow maintainer must inspect the linked execution and compare the issue and any linked PR with its publication steps.' : 'The workflow maintainer must inspect the failed or cancelled execution and the configured recovery policy.') + reservation;
|
package/src/issue-actions.mjs
CHANGED
|
@@ -122,6 +122,13 @@ export function continuationHistory(continuation, answer) {
|
|
|
122
122
|
export function checkProductEntry(product, snapshot) {
|
|
123
123
|
if (!product || product.entry !== (snapshot.productEntry ?? 'idea')) throw Error('Product Change worker returned an outcome for a different authorized entry');
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
export function productGapNextAction({ gap, enabled, dispatched, paused }) {
|
|
127
|
+
if (!gap) return null;
|
|
128
|
+
if (paused) return { action: 'Owner: remove the future label when work may resume.', next: !enabled ? 'The workflow maintainer must also install and enable the Product Change workflow, then request a new authorized stage execution.' : dispatched ? 'Then replay this run through the router using this issue, stage and run ID as after_run.' : 'Then request a new authorized stage execution.' };
|
|
129
|
+
if (enabled && dispatched) return { action: 'None. Wait for the Product Change preparation outcome.', next: 'The workflow will authenticate this prerequisite and check whether product preparation is authorized; implementation remains stopped.' };
|
|
130
|
+
return { action: enabled ? 'Workflow maintainer: request a new authorized stage execution in the router.' : 'Workflow maintainer: install and enable the Product Change workflow.', next: 'Request a new authorized stage execution in the router; implementation remains stopped until the product prerequisite is approved and applied.' };
|
|
131
|
+
}
|
|
125
132
|
export function productApplyRequest({ dispatch, current, issueNumber, issue, repository, owner }) {
|
|
126
133
|
if (dispatch?.request.worker !== 'apply-product-change' || !dispatch.request.productApproval) throw Error('Trusted Product Change apply dispatch is missing');
|
|
127
134
|
const requested = dispatch.request.productApproval;
|
|
@@ -762,8 +769,9 @@ async function main(command) {
|
|
|
762
769
|
const projection = resultHandoffState({ status: value.result.status, stage: snapshot.stage, question, reviewTarget, draft: !!prStateProof });
|
|
763
770
|
const isReview = projection.review;
|
|
764
771
|
const productRecovery = snapshot.worker === 'product-change' ? value.result.output?.productChange?.recovery : null;
|
|
765
|
-
const
|
|
766
|
-
const
|
|
772
|
+
const productGap = value.result.status === 'blocked' && snapshot.worker !== 'product-change' ? productGapNextAction({ gap: value.result.output?.productGap, enabled: snapshot.productChangeEnabled === true, dispatched: !!snapshot.dispatch, paused: publicationIssue.labels.some(label => label.name === 'future') }) : null;
|
|
773
|
+
const action = question ? `Owner: ${question} Reply here.` : isReview ? snapshot.worker === 'product-change' ? 'Owner: review the linked Product Change PR and approve the proposal or request changes there.' : 'Owner: open the linked result and record your decision there.' : value.result.status === 'blocked' ? productRecovery ? `Workflow maintainer: ${productRecovery}` : productGap?.action ?? visibleNextAction(value.result.nextAction) : refinementAction?.action ?? 'None.';
|
|
774
|
+
const whatNext = question ? `The answer will be checked against this question before ${snapshot.stage} continues.` : isReview ? 'The workflow waits for the required decision before continuing.' : value.result.status === 'blocked' ? productGap?.next ?? 'Work remains stopped until the blocker is resolved and continuation is authorized.' : refinementAction?.next ?? 'Any continuation follows the configured authorization.';
|
|
767
775
|
const happened = productPublicationMessage({ worker: snapshot.worker, outcome: value.result.output?.productChange?.outcome, reviewTarget, agentMessage: value.effect.body });
|
|
768
776
|
const body = renderHandoff({ status: projection.status, stage: snapshot.stage, happened, action, next: whatNext, link: proposalLink.trim() || undefined, details: `Outcome: ${value.result.status}\n\n[Workflow run](${runUrl}) · source \`${snapshot.sourceRevision}\`\n\n${marker}\n<!-- lighthouse-handoff:v1 -->\n${prStateProof}\n<!-- lighthouse-stage:${snapshot.stage}:${value.result.status} -->${question ? '\n<!-- lighthouse-question -->' : ''}${productWorkflowMarker}${continuationHint}` });
|
|
769
777
|
const handoff = snapshot.worker !== 'product-change' && snapshot.stage === 'refine' && value.result.status === 'passed' && env.LIGHTHOUSE_AUTO_IMPLEMENT === 'true' && !decompositionPlanned;
|
package/src/worker-recovery.mjs
CHANGED
|
@@ -7,6 +7,7 @@ export const maintenancePath = '.github/workflows/lighthouse-worker-maintenance.
|
|
|
7
7
|
export const retryLimit = 2;
|
|
8
8
|
export const retryDelayMs = 30 * 60 * 1000;
|
|
9
9
|
export const recoveryId = (repository, rootId, attempt) => digest(`${repository}:${rootId}:retry:${attempt}`);
|
|
10
|
+
const publishStep = job => job?.steps?.find(step => ['Recheck authority and publish the validated result', 'Recheck authority and publish'].includes(step.name));
|
|
10
11
|
const identity = r => ({ issue: r.request.issue, worker: workerFor(r.request), runId: String(r.run.id), rootId: r.request.recovery?.rootId ?? r.request.id, attempt: r.request.recovery?.attempt ?? 0 });
|
|
11
12
|
const ownerAnswers = comments => comments.filter(c => c.user?.login === c.owner && !c.body.includes('<!-- lighthouse-run:') && !c.body.includes('<!-- lighthouse-actions:'));
|
|
12
13
|
const intent = (issue, comments, owner) => digest(JSON.stringify({ title: issue.title, body: issue.body, comments: ownerAnswers(comments.map(c => ({ ...c, owner }))).map(c => ({ id: c.id, body: c.body, updated_at: c.updated_at })) }));
|
|
@@ -29,9 +30,9 @@ export function recoveryDecision({ request, receipt, run, jobs, issue, comments,
|
|
|
29
30
|
if (!['refine', 'propose', 'implement'].includes(request.stage) || !['failure', 'timed_out'].includes(run.conclusion)) return result('uncertain', 'manual-recovery', 'Inspect the outcome and request a new authorized execution if needed.');
|
|
30
31
|
const preparation = jobs.find(j => j.name === 'Authorize and reserve issue');
|
|
31
32
|
const publication = jobs.find(j => j.name === 'Validate, publish and release');
|
|
32
|
-
const
|
|
33
|
+
const publish = publishStep(publication);
|
|
33
34
|
const failedExecution = jobs.some(j => ['pre_activation', 'activation', 'agent'].includes(j.name) && ['failure', 'timed_out'].includes(j.conclusion));
|
|
34
|
-
if (preparation?.conclusion !== 'success' || !failedExecution ||
|
|
35
|
+
if (preparation?.conclusion !== 'success' || !failedExecution || publish?.conclusion !== 'skipped') return result('uncertain', 'publication-uncertain', 'Inspect publication and remote effects before creating another request.');
|
|
35
36
|
if (base.attempt >= retryLimit) return result('exhausted', 'worker-technical-failure', 'Retry budget exhausted. Inspect the failure and make a new owner decision.');
|
|
36
37
|
const ended = Date.parse(run.updated_at);
|
|
37
38
|
if (!Number.isFinite(ended) || !Number.isFinite(now)) return result('uncertain', 'execution-time-unknown', 'Inspect the execution timestamps.');
|
|
@@ -63,14 +64,16 @@ export async function observeDispatch(api, repository, owner, comment, issue, co
|
|
|
63
64
|
const workflowRevision = (await api(`commits/${encodeURIComponent(dispatch.request.ref)}`)).sha;
|
|
64
65
|
const agent = jobs.jobs.find(job => job.name === 'agent');
|
|
65
66
|
const publishJob = jobs.jobs.find(job => job.name === 'Validate, publish and release');
|
|
66
|
-
const publish = publishJob
|
|
67
|
+
const publish = publishStep(publishJob);
|
|
68
|
+
const cleanup = publishJob?.steps?.find(step => ['Release the issue reservation', 'Release reservation'].includes(step.name));
|
|
67
69
|
const prepare = jobs.jobs.find(job => job.name === 'Authorize and reserve issue');
|
|
68
70
|
const unpublished = publish?.conclusion === 'skipped' || publishJob?.conclusion === 'skipped';
|
|
69
71
|
const notStarted = !agent || agent.conclusion === 'skipped';
|
|
70
72
|
const activationFailed = jobs.jobs.some(job => ['pre_activation', 'activation'].includes(job.name) && ['failure', 'timed_out'].includes(job.conclusion));
|
|
71
|
-
const observation = ['
|
|
73
|
+
const observation = publish?.conclusion === 'success' && ['failure', 'timed_out', 'cancelled'].includes(cleanup?.conclusion) ? 'cleanup-failure'
|
|
74
|
+
: ['completed', 'waiting', 'blocked'].includes(receipt.status) ? null
|
|
72
75
|
: workerFor(dispatch.request) === 'apply-product-change' ? productApplyObservation({ prepare: jobs.jobs.find(job => job.name === 'Recheck product approval')?.conclusion, apply: jobs.jobs.find(job => job.name === 'Apply with the pinned ProductShape provider')?.conclusion, publish: jobs.jobs.find(job => job.name === 'Recheck and publish provider output')?.conclusion })
|
|
73
|
-
: agent?.conclusion === 'success' && ['failure', 'timed_out', 'cancelled'].includes(publish?.conclusion) ? 'publication-unknown'
|
|
76
|
+
: agent?.conclusion === 'success' && (!publishJob || ['failure', 'timed_out', 'cancelled', 'skipped'].includes(publish?.conclusion) || ['failure', 'timed_out', 'cancelled', 'skipped'].includes(publishJob.conclusion)) ? 'publication-unknown'
|
|
74
77
|
: ['failure', 'timed_out'].includes(prepare?.conclusion) && notStarted ? 'preparation-failure'
|
|
75
78
|
: prepare?.conclusion === 'success' && activationFailed && notStarted && unpublished ? 'activation-failure'
|
|
76
79
|
: unpublished && ['failure', 'timed_out'].includes(agent?.conclusion) ? 'failure'
|
|
@@ -118,7 +121,7 @@ export async function reconcileAbandonedReservation(api, repository, owner, issu
|
|
|
118
121
|
const prepare = jobs.find(job => job.name === 'Authorize and reserve issue');
|
|
119
122
|
const agent = jobs.find(job => job.name === 'agent');
|
|
120
123
|
const publish = jobs.find(job => job.name === 'Validate, publish and release');
|
|
121
|
-
const step =
|
|
124
|
+
const step = publishStep(publish);
|
|
122
125
|
if (prepare?.conclusion !== 'success' || !['failure', 'timed_out', 'cancelled', 'skipped'].includes(agent?.conclusion) || !(publish?.conclusion === 'skipped' || step?.conclusion === 'skipped')) return false;
|
|
123
126
|
let snapshot;
|
|
124
127
|
try { snapshot = await readArtifact(api, repository, run.id, `lighthouse-input-${issue.number}-${run.run_attempt}`, 'request.json'); } catch { return false; }
|
|
@@ -167,7 +170,7 @@ export async function reconcilePublishedStage(api, repository, owner, issue, com
|
|
|
167
170
|
const { dispatch, run, jobs = [], revision, workflowRevision } = observation;
|
|
168
171
|
if (!dispatch || !run) return 'not-applicable';
|
|
169
172
|
const publication = jobs.find(j => j.name === 'Validate, publish and release');
|
|
170
|
-
const publish = publication
|
|
173
|
+
const publish = publishStep(publication);
|
|
171
174
|
const failedPublication = run.conclusion === 'failure' && jobs.find(j => j.name === 'agent')?.conclusion === 'success' && publish?.conclusion === 'failure';
|
|
172
175
|
const interrupted = failedPublication && ['running', 'unknown'].includes(dispatch.receipt.status);
|
|
173
176
|
const completed = (run.conclusion === 'success' || failedPublication) && ['completed', 'waiting', 'blocked'].includes(dispatch.receipt.status);
|
|
@@ -235,12 +238,12 @@ export async function reconcileUnattendedStop(api, repository, owner, issue, com
|
|
|
235
238
|
if (!last.body.includes(marker)) return 'uncertain';
|
|
236
239
|
const run = await api(`actions/runs/${runId}`);
|
|
237
240
|
if (run?.repository?.full_name !== repository || run.path !== '.github/workflows/lighthouse-sdlc-unattended.yml' || run.event !== 'workflow_dispatch' || run.actor?.login !== owner || run.triggering_actor?.login !== owner || run.display_title !== `${route} #${issue.number} · ${owner}` || run.run_attempt !== 1) return 'uncertain';
|
|
238
|
-
if (run.status !== 'completed' || !['failure', 'timed_out', 'cancelled'].includes(run.conclusion)) return 'not-applicable';
|
|
241
|
+
if (run.status !== 'completed' || !['success', 'failure', 'timed_out', 'cancelled'].includes(run.conclusion)) return 'not-applicable';
|
|
239
242
|
const fresh = await api(`issues/${issue.number}`), latest = await issueComments(api, issue.number);
|
|
240
243
|
const current = latest.filter(c => c.user?.login === 'github-actions[bot]' && c.user.type === 'Bot' && /<!-- lighthouse-(?:unattended:|unattended-recovery:|dispatch:|actions:|stop:)/.test(c.body ?? '')).at(-1);
|
|
241
244
|
if (fresh.state !== 'open' || fresh.user?.login !== owner || current?.id !== last.id) return 'uncertain';
|
|
242
245
|
const reservation = fresh.labels.some(l => l.name === 'agent-working') ? ' The agent-working reservation remains; verify ownership before releasing it.' : '';
|
|
243
|
-
const body = handoff({ status: 'unknown', stage: route, happened: `The unattended execution ended ${run.conclusion}, but its terminal handoff is missing. Publication and integration effects have not been confirmed.`, action: `Workflow maintainer: inspect the execution, artifacts, issue and any candidate PR before resuming.${reservation}`, next: 'Do not retry or release a reservation until the observed effects and current ownership are reconciled.', link: `[Execution](https://github.com/${repository}/actions/runs/${runId})`, details: `<!-- lighthouse-unattended-recovery:${runId}:1 -->` });
|
|
246
|
+
const body = handoff({ status: run.conclusion === 'success' ? 'unknown' : run.conclusion === 'cancelled' ? 'cancelled' : 'failed', stage: route, happened: `The unattended execution ended ${run.conclusion}, but its terminal handoff is missing. Publication and integration effects have not been confirmed.`, action: `Workflow maintainer: inspect the execution, artifacts, issue and any candidate PR before resuming.${reservation}`, next: 'Do not retry or release a reservation until the observed effects and current ownership are reconciled.', link: `[Execution](https://github.com/${repository}/actions/runs/${runId})`, details: `<!-- lighthouse-unattended-recovery:${runId}:1 -->` });
|
|
244
247
|
await api(`issues/${issue.number}/comments`, 'POST', { body });
|
|
245
248
|
return 'restored';
|
|
246
249
|
}
|
|
@@ -286,7 +289,7 @@ export async function prepareRecovery(api, repository, owner, issueNumber, paren
|
|
|
286
289
|
if (preparationRun && await reconcilePreparationFailure(api, repository, owner, issueNumber, comment.id, preparationRun, readArtifact)) { observation.stop = null; Object.assign(publicObservation, { state: 'reconciled', code: 'preparation-failed', nextAction: 'Inspect the confirmed failed execution before requesting new work.' }); }
|
|
287
290
|
const reconciliation = await reconcilePublishedStage(api, repository, owner, issue, comments, observation, readArtifact);
|
|
288
291
|
if (reconciliation === 'restored' || reconciliation === 'present') {
|
|
289
|
-
observation.stop = null;
|
|
292
|
+
if (observation.stop?.observation !== 'cleanup-failure') observation.stop = null;
|
|
290
293
|
if (run?.conclusion === 'failure') Object.assign(publicObservation, { state: 'reconciled', code: 'handoff-restored', nextAction: 'Follow the restored handoff.' });
|
|
291
294
|
}
|
|
292
295
|
if (reconciliation === 'uncertain') Object.assign(publicObservation, { state: 'uncertain', code: 'handoff-unconfirmed', nextAction: 'Inspect the retained result and current issue state before restoring its handoff.' });
|