feat(verification): add pre-PR verification workflows and issue-lifecycle support #243
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/pre-pr-verification"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
verify-buildandverify-reviews) adapted from swamp core's pre-PR verification system, with review prompts as single source of truth underverification/review-prompts/verify-buildruns per-directory quality gates (check/lint/fmt/test/lockfile), codegen idempotency checks, and a model version/upgrade gate that catches missing upgrade entries (precedent: issue #554)verify-reviewsruns code-review and adversarial on all source changes, ci-security guarded to.forgejo//.github//scripts/changesverification-conventions.mdagent constraints for the verify-fix-rerun loopverify,verification_passed,verification_failed,post_attestation),verificationResultresource, andverification-clearcheck gatinglink_pr— no PR opens without passing verificationpostAttestation()toSwampClubClientfor posting verification attestations to swamp-club2026.08.28.1with upgrade entryTest plan
deno checkpasses for issue-lifecycledeno lintpasses for issue-lifecycledeno fmt --checkpasses for issue-lifecycledeno install --frozenpasses (lockfile clean)SWAMP_WORKFLOWS_DIR=verification swamp workflow run verify-buildlocally to smoke-test the build workflowSWAMP_WORKFLOWS_DIR=verification swamp workflow run verify-reviewslocally to smoke-test the reviews workflowswamp extension source addaccepts the new version and upgrade path works🤖 Generated with Claude Code
Code Review
Blocking Issues
Missing tests for all new verification methods and check (
issue-lifecycle/extensions/models/issue_lifecycle.ts,issue_lifecycle_test.ts)The PR adds four new methods (
verify,verification_passed,verification_failed,post_attestation) and one new check (verification-clear), plus theverificationResultresource — none of which have unit tests. Every other method inissue_lifecycle_test.tshas corresponding tests (including model-registration smoke tests). The new verification pathway is the most complex addition in this PR and is the gating mechanism before a PR can be opened; it needs test coverage.Missing tests:
verify: state transitions toverifying, lifecycle entry postedverification_passed:verificationResult-mainwritten with correct aggregated counts (stepsCompleted,stepsFailed,stepsSkipped,allPassed), state staysverifyingverification_failed: state transitions back toimplementingpost_attestation: throws when swamp-club unreachable, throws on invalid JSON, posts attestation and lifecycle entryverification-clearcheck: passes whenallPassed=true, rejects when no result exists, rejects whenallPassed=falseverificationResultinmodel.resources,verify/verification_passed/verification_failed/post_attestationinmodel.methods,verification-clearinmodel.checksCI security review guard is inverted (
verification/workflow-verify-reviews.yaml, line 143)The guard evaluates to
true(step runs) when the filtered file count is zero — meaning the CI security review runs on every PR that does not touch.forgejo/,.github/, orscripts/, and is skipped on PRs that do modify those paths. This is the opposite of the intended behavior documented inagent-constraints/verification-conventions.mdand confirmed by the codegen guard pattern inworkflow-verify-build.yamlline 310 (which runs when the flag is present, i.e., guard=true → run).Fix: change
== 0to> 0.Stale
verificationResultsatisfiesverification-cleargate after re-verify (issue-lifecycle/extensions/models/issue_lifecycle.ts, lines 503–547)The
verifymethod does not clear or invalidate the existingverificationResult-main. This creates a gate bypass: after a successful verification round (verify→verification_passed,allPassed=true), if the agent callsverification_failed(returns toimplementing), makes code changes, and then callsverifyagain (re-enteringverifying), the staleverificationResultfrom the previous round still satisfiesverification-clear. The agent can then calllink_prwithout ever callingverification_passedfor the current round.Fix: either (a) have
verifydelete or overwriteverificationResult-mainwithallPassed=falseto invalidate the previous result, or (b) haveverification-clearcompare theverifiedAttimestamp against the state'supdatedAtto reject stale results.Suggestions
Stale test description (
issue-lifecycle/extensions/models/_lib/schemas_test.ts, line 40)The test is named
"TRANSITIONS: link_pr accepts implementing, pr_open, and pr_failed"but the assertion checks["verifying", "pr_open", "pr_failed"]— the phaseimplementingwas replaced byverifyingin this PR. The description should sayverifyinginstead ofimplementing.manifest.yamlData section is incomplete (issue-lifecycle/manifest.yaml, line 80–82)The Data section lists
state,context,classification,plan,feedback,adversarialReview,pullRequestbut omitscodeConformanceReview,verificationResult, andsummary, all of which are registered resources inmodel.resources.manifest.yamlstate machine diagram is stale (issue-lifecycle/manifest.yaml, lines 37–38)The diagram shows:
But the actual behavior (since the 2026.07.30.1 upgrade) transitions both methods to
summarizing, which then transitions todoneviasummarize. The diagram needssummarizingadded betweennotifyanddone.Adversarial Review
Critical / High (if any)
No critical or high severity findings.
Medium
verification/workflow-verify-reviews.yaml:143— ci-security-review guard condition appears inconsistent with codegen guardThe ci-security-review step guard is:
This evaluates to
truewhen no CI-related files changed. Compare with the codegen-verify job guard inworkflow-verify-build.yaml:311:This evaluates to
truewhen codegen is changed. Ifguard=truehas the same meaning in both places (most likely: "condition to run"), then the ci-security guard is inverted — it would run the review when there are NO CI changes and skip it when CI files actually change, which is the exact opposite of the documented intent (agent-constraints/verification-conventions.mdline 61 shows the guard should fire on.forgejo/,.github/,scripts/changes).Breaking scenario: A PR modifies
.forgejo/workflows/*.yamlwith a security-sensitive change. The ci-security-review step is skipped because the guard evaluates tofalse(matching files exist, sosize() != 0). The security review that was specifically designed for this scenario never runs.If the workflow engine uses different guard semantics at the job vs. step level (job=run-when-true, step=skip-when-true), then both guards are correct. But this ambiguity should be verified against the workflow engine's documentation.
Suggested fix: If guards have uniform semantics, change to
size() > 0(orsize() != 0). Add a comment documenting the intended semantics.issue-lifecycle/extensions/models/issue_lifecycle_test.ts— No tests for the 4 new methods or the verification-clear checkThis PR adds 4 new methods (
verify,verification_passed,verification_failed,post_attestation) and a new check (verification-clear), but the test file diff only updates the version assertion and schemas test. Every other method in the model (link_pr,pr_merged,pr_failed,ship,notify,skip_notify,summarize,code_conformance_review,justify_deviations) has dedicated test cases. The new methods have zero test coverage.Breaking scenario: The
verification_passedmethod computesallPassed: failed === 0(line 1799). If this logic were accidentally changed toallPassed: failed > 0, there would be no test to catch the inversion — the verification gate would block all valid PRs and pass all invalid ones.Suggested fix: Add tests for at minimum:
verifytransitions to verifying,verification_passedwrites the verificationResult with correct allPassed/counts,verification_failedtransitions back to implementing,post_attestationthrows on invalid JSON, and theverification-clearcheck rejects when allPassed is false and passes when true. Follow the existing test patterns (usebuildTestContext).issue-lifecycle/extensions/models/issue_lifecycle.ts:505—verification-clearonly gateslink_pr, notcompleteThe
verification-clearcheck hasappliesTo: ["link_pr"], but thecode-conformance-clearcheck (line 435) applies to["link_pr", "complete"]. This inconsistency means callingcompletefrom theimplementingphase bypasses verification entirely while still requiring code conformance review.This may be intentional —
completeis a fallback for non-PR flows where pre-PR verification doesn't apply. But the asymmetry withcode-conformance-clear(which does gatecomplete) suggests this was an oversight rather than a design choice.Breaking scenario: An agent calls
completefromimplementingwithout running verification. The code conformance check passes, but no build/test/review verification was performed. The lifecycle closes without any build validation.Suggested fix: Either add
"complete"to theappliesToarray, or document whycompleteintentionally bypasses verification.Low
verification/workflow-verify-build.yaml:502-503— Fragile grep for model type extractionThis matches the first quoted string on any line containing
type:in any.tsfile. It could match Zod schema fields (type: IssueType), interface properties, or import statements before hitting the actual model type declaration. If a model file has atype:reference in an import or schema before the model definition, the wrong string gets extracted.Suggested fix: Use a more specific pattern like
grep "type:" "$dir/extensions/models/"*.ts | grep -v "import\|z\.\|schema\|Schema"or search for the model export pattern directly.verification/workflow-verify-reviews.yaml:97-107— Temp files not cleaned up on pipeline failureEach review step creates three temp files (
DIFF_FILE,PROMPT_FILE,RESULT_FILE) but only cleans some of them conditionally. Ifset -o pipefailcauses an early exit (e.g.,claude -pfails), remaining temp files leak. This is harmless (they're in/tmp) but untidy.Suggested fix: Add a
trap 'rm -f "$DIFF_FILE" "$PROMPT_FILE" "$RESULT_FILE"' EXITafter creating them.Verdict
PASS — The code is structurally sound. The new verification phase integrates cleanly into the existing state machine, the transition constraints are correct, the upgrade entry matches the version bump, and the schema additions are consistent. The ci-security guard ambiguity (Medium #1) depends on workflow engine semantics that I can't fully verify. The missing tests (Medium #2) are a real gap but don't indicate code incorrectly. No security vulnerabilities, data integrity issues, or correctness bugs were found in the production code paths.
Code Review
Blocking Issues
post_attestationis described as a "hard gate" but no client-side check enforces it (issue_lifecycle.ts:1930–1935).The method description says: "The PR must not open without a stored attestation — this is a hard gate." However, the model's
checksobject has noattestation-clear(or equivalent) entry withappliesTo: ["link_pr"]. The existingverification-clearcheck (line 503) only verifiesallPassed, not thatpost_attestationwas ever called. An agent can legally calllink_prfrom theverifyingphase without ever callingpost_attestation, satisfying every check that runs beforelink_pr.The description and the implementation directly contradict each other. One of these must change:
attestation-clearcheck that reads anattestation-mainresource written bypost_attestation, gated onappliesTo: ["link_pr"].If Option B is correct, the conventions doc and the method description should also be updated to explain where the enforcement actually lives.
Suggestions
Stale method descriptions in
manifest.yaml(lines 73–76): Bothnotifyandskip_notifyare described as transitioning "directly todone," but they actually transition tosummarizing(the state machine diagram in the same manifest is correct). Since the manifest is changed in this PR, this is a good opportunity to fix:notify→ "transition tosummarizing"skip_notify→ "transition directly tosummarizing"Missing success-path test for
post_attestation(issue_lifecycle_test.ts): Only two error-path tests exist (unreachable swamp-club, invalid JSON). There is no test for the happy path — successfulpostAttestationcall, response parsing (id,postedBy,postedAt), or the lifecycle entry that follows. Given the method is described as a critical gate, a localDeno.serve({ port: 0 })stub for/healthzand/api/v1/admin/attestationswould cover the success path without live dependencies.verification_passedname vs. behavior: The method accepts astepsarray that may include"failed"entries and computesallPassed: failed === 0from the data. A caller can invoke it with all-failed steps and it will storeallPassed: false— the name implies the caller has confirmed success, but the method does not enforce that. The description is clear, but the test"verification_passed: allPassed is false when any step failed"highlights the mismatch. Consider whether a runtime guard (throw if every non-skipped step failed) or a name change (record_verification_results) would better express the intent.Adversarial Review
Critical / High
schemas.ts:62-74 — start transition missing verifying phase, breaking the resume invariant
The start transition lists every phase except done and verifying as valid source phases. The manifest documents: "start can resume from any phase except done." Every other non-done phase is included — verifying is the sole omission.
Breaking example: An agent enters the verifying phase (by calling verify), then crashes or loses its session. The user tries to resume with start. The valid-transition check fires, sees phase is verifying, and rejects with: Method start cannot run in phase verifying. Allowed phases: created, triaging, etc. There is no other method that can be called from verifying to escape to a start-resumable phase without completing the full verification loop (verification_failed goes to implementing, but that requires knowing to call it — and the resume flow is specifically designed so start handles recovery from any stuck phase).
The existing schemas_test.ts test at line 50 only spot-checks a few phases and does not assert the complete set, so it misses this gap.
Suggested fix: Add verifying to the start transition array in schemas.ts between implementing and pr_open. And add a corresponding assertion in schemas_test.ts.
Medium
issue_lifecycle.ts:1850-1856 — verification_passed accepts an empty steps array, producing allPassed true with zero verification work
The steps argument uses z.array with no .min(1) constraint. If called with an empty steps array, the aggregation at lines 1887-1890 produces succeeded=0, failed=0, so allPassed is true (failed === 0). This writes a verification result that passes the verification-clear gate (line 552) despite no verification steps having executed.
Breaking example: An agent (buggy or adversarial) calls verification_passed with workflowRunId run-x, commit abc, branch b, and an empty steps array. The verification-clear gate passes. Combined with a posted attestation, link_pr succeeds with zero actual verification.
Suggested fix: Add .min(1) to the steps array in the verification_passed arguments schema, or add a guard in the execute body that throws when args.steps.length is 0.
issue_lifecycle.ts:518-563 — verification-clear check applies to complete but the implementing shortcut path cannot satisfy it
The complete transition accepts implementing, pr_open, releasing (line 92). The verification-clear check has appliesTo link_pr and complete (line 523). If an agent calls complete from implementing (the no-PR-needed shortcut path), the check demands a passing verification result. But the verify method (which enters verifying phase) invalidates prior results, and verification_passed stays in verifying, not implementing. So having allPassed true while in implementing phase is essentially impossible through the normal flow.
This may be intentional (forcing verification even for the shortcut path), but if so the error message should say so explicitly rather than leaving the agent in a confusing dead-end. If unintentional, remove complete from the verification-clear appliesTo list.
Low
workflow-verify-reviews.yaml:94,121,148 — Review scripts use set -o pipefail but not set -e, masking upstream failures
If git diff fails (e.g., corrupt repo, permissions), the diff file is empty. The script continues to invoke claude with an empty diff. Claude likely says there is nothing to review, and the verdict check reports review did not pass rather than the actual root cause. Not a correctness issue (the step still fails), but the error message is misleading.
issue_lifecycle.ts:2046-2049 — post_attestation accepts non-object JSON without validation
JSON.parse could return a primitive (number, string, boolean) if the input is valid JSON but not an object. The cast to Record does not enforce object shape. postAttestation would then send a non-object body, which the API would reject with an unhelpful HTTP error. A typeof check would surface a clearer error.
Verdict
FAIL — The missing verifying phase in the start transition is a HIGH-severity bug that breaks the documented resume-from-any-phase invariant. An agent crash during verification leaves the lifecycle stuck with no standard recovery path.
Code Review
Blocking Issues
None.
Suggestions
verifyingmissing fromTRANSITIONS.start— If an agent process crashes while the lifecycle is in theverifyingphase, it cannot callstartto resume. Recovery requires callingverification_failed(which is allowed fromverifying) to return toimplementing, and then either callingstart(which resets all the way totriaging) or continuing fromimplementing. The missing phase means crash-recovery fromverifyinghas no clean "resume" path — onlyverification_failed→implementing, which discards the in-flight verification. Adding"verifying"toTRANSITIONS.startwould make the recovery story consistent with all other mid-lifecycle phases already listed there.verification-clearoncompletefromimplementingis practically unsatisfiable via normal flows — Theverification-clearcheck applies to bothlink_prandcomplete. To satisfy it, a passingverificationResultmust exist (written byverification_passed). Butverification_passedrequiresverifyingphase (via TRANSITIONS), and to reachimplementingfromverifyingyou must callverification_failed— which signals that verification failed. The only way to satisfyverification-clearforcompletefromimplementingis to: run verification, pass it, then deliberately callverification_failedto return toimplementing(semantically contradictory), and then callcomplete. The effective result is thatcompletefromimplementingis a dead path post-this-PR. If that's intentional (removing the shortcut), the upgrade description could say so explicitly so agents know to use the fullverify → verification_passed → post_attestation → link_prpath instead.workflow-verify-reviews.yamlguard usesdata.latest('repo', 'diff')— The guard onci-security-review(line 141) references the data typedifffrom modelrepo. Two preceding steps indetect-changesboth callmethodName: diffonrepo— one withoutnameOnlyand one withnameOnly: true. The guard depends on.attributes.filesbeing a list of file paths, which comes from thenameOnlyvariant. The behavior ofdata.latestwhen two steps write to the same model/data-type slot is not obvious from the YAML; if the non-nameOnly output is returned instead, the guard expression would fail silently andci-security-reviewwould never trigger. Worth confirming (or renaming the data types in the two steps) that the guard reliably gets the name-only output.verification_passedaccepts failing steps without complaint — The method is namedverification_passedbut it accepts astepsarray that may contain"failed"entries and computesallPassed: failed === 0. If an agent accidentally callsverification_passedwith failed steps,allPassedwill befalseand theverification-cleargate will block, so there is no security consequence — but the method name is misleading. A Zod refinement rejecting the call when any step hasstatus: "failed"would make the contract explicit and catch agent mistakes earlier.Code Review
Blocking Issues (if any)
None.
Suggestions (if any)
Misleading error messages when
completefailsverification-clearorcode-conformance-clear(
issue-lifecycle/extensions/models/issue_lifecycle.ts, checks at lines 518–563 and 446–516)Both
verification-clearandcode-conformance-clearapply to["link_pr", "complete"], but their error messages refer only to "linking a PR":"No verification result exists. Run 'verify' and then 'verification_passed' before linking a PR.""No code conformance review exists. Run 'code_conformance_review' before linking a PR."An agent calling
complete(e.g., fromimplementingorreleasing) will receive an error telling them to do something "before linking a PR," which is confusing when they aren't trying to link a PR. Consider messages like "…before linking a PR or completing the lifecycle."verification_passedmethod name implies unconditional success but records partial failures(
issue-lifecycle/extensions/models/issue_lifecycle.ts, line 1841)The method accepts
stepsthat include"failed"status entries and setsallPassed: failed === 0. Callingverification_passedwith a failed step yieldsallPassed: false, which then blockslink_prviaverification-clear. The test "verification_passed: allPassed is false when any step failed" confirms this behaviour. The nameverification_passedimplies the caller has already confirmed all steps passed, but in practice it's more of a "record verification results" method. This is unlikely to cause a correctness bug in practice (theverification-cleargate catches it), but the semantics may surprise future maintainers.ci-security-reviewguard does not coververification/workflow files(
verification/workflow-verify-reviews.yaml, line 141)The guard fires only when
.forgejo/,.github/, orscripts/files change:The newly added
verification/workflow-verify-build.yamlandverification/workflow-verify-reviews.yamlcontain substantial shell scripts. Changes to those files will not trigger the dedicated CI security review (though they remain covered by code-review and adversarial-review). Consider addingverification/to the guard if you want the stricter CI-focused review to apply to workflow changes.verifymethod writes resources sequentially; minor efficiency loss(
issue-lifecycle/extensions/models/issue_lifecycle.ts, lines 1781–1815)The three
await context.writeResource(...)calls inverifyare sequential. Since they are independent writes, they could be parallelised withPromise.all. This is purely a performance note and not a correctness issue.Adversarial Review
Critical / High
None found.
Medium
post_attestation accesses parsed.subject and parsed.gate with unsafe casting, producing undefined silently if the attestation JSON has a different structure.
File: issue-lifecycle/extensions/models/issue_lifecycle.ts:2062-2063
What is wrong: The post_attestation method casts parsed.subject and parsed.gate to Record and accesses .commit and .allPassed without any validation. If the caller supplies a valid JSON object that lacks subject or gate keys, the commit and gatePassed fields in the stored attestation will be undefined.
Breaking example: Calling post_attestation with attestation containing only a version field writes the attestation resource with commit: undefined and gatePassed: undefined. This does not crash, but the attestation record silently has no commit linkage.
Suggested fix: Log a warning when the commit field is falsy, since a commitless attestation is operationally useless.
verification_passed method name is misleading when called with failed steps.
File: issue-lifecycle/extensions/models/issue_lifecycle.ts:1897-1900
What is wrong: The verification_passed method accepts steps with status failed and correctly sets allPassed: false. However, calling verification_passed with failing steps is semantically confusing.
Breaking example: An agent calls verification_passed with a mix of succeeded and failed steps. The lifecycle entry says Verification passed with a checkmark emoji while the verification actually failed. The gate correctly blocks, but the lifecycle log on swamp-club will be contradictory.
Suggested fix: Only post the passed lifecycle entry when allPassed is true.
codegen-verify guard uses expression that reads raw stdout, which may break if detect-changes output format changes.
File: verification/workflow-verify-build.yaml:310
What is wrong: The guard expression reads from the stdout of the detect-changes step and does a string contains check. This is a string match on raw stdout.
Suggested fix: No immediate fix needed. Low-risk coupling to be aware of when modifying the detect-changes step.
Low
verify method does not validate commit SHA format.
File: issue-lifecycle/extensions/models/issue_lifecycle.ts:1755-1756
The commit argument accepts any string including empty strings.
Upgrade path test swallows all method execution failures.
File: verification/workflow-verify-build.yaml:522-525
By design per the inline comment. The subsequent typeVersion check is the real validation gate.
ci-security-review guard does not cover verification/ directory.
File: verification/workflow-verify-reviews.yaml:141
The guard filters on .forgejo/, .github/, and scripts/ but the verification/ workflow files would not trigger the CI security review.
Verdict
PASS - The code is well-structured. The state machine transitions are correctly updated with the new verifying phase. All new methods (verify, verification_passed, verification_failed, post_attestation) have corresponding tests. Checks are properly gated. The invalidation logic in verify() correctly prevents stale results from satisfying gates. No critical or high-severity issues found.
Code Review
Blocking Issues
None.
Suggestions
post_attestationtests with a real HTTP server may needsanitizeResources: false(issue_lifecycle_test.ts, lines 1616–1710): Two tests (throws on invalid JSON inputandwrites attestation resource on success) start aDeno.serveserver and invokecreateSwampClubClient, which makesfetchrequests. Deno'sfetchmaintains an implicit keepalive connection pool. CLAUDE.md requiressanitizeResources: false(with an explanatory comment) for tests that create HTTP clients with connection pooling. The servers are properly aborted and awaited, so the tests likely pass in practice, but a future Deno version tightening resource tracking could cause spurious failures. Suggested fix: addsanitizeResources: falseto both test options with a comment like// SwampClubClient uses fetch which maintains a connection pool.verification_passedmethod is callable with failed steps (issue_lifecycle.ts, line 1841): The method name implies success, but the implementation accepts steps withstatus: "failed"and writesallPassed: falseto the result resource. The actual gate is enforced by theverification-clearcheck. A brief inline comment on the method description noting that this method records results regardless of outcome (the gate is elsewhere) would prevent future confusion about when this vs.verification_failedshould be called.inputs.commitused unvalidated in shell commands (workflow-verify-build.yamlline 45,workflow-verify-reviews.yamlline 45):${{ inputs.commit }}is interpolated directly intogit worktree add --detach "$CHECKOUT_DIR" "${{ inputs.commit }}". While these workflows are agent-invoked rather than webhook-triggered (limiting practical exposure), validating that the commit value matches^[0-9a-f]{40}$before use would be a safe defensive practice consistent with the security posture of the review prompts.Adversarial Review
Critical / High
No critical or high severity findings.
Medium
verification_passed accepts empty steps array, bypassing verification-clear gate
Same empty-steps issue: all-skipped steps also passes the gate
Low
post_attestation does no structural validation on the attestation JSON
postAttestation response body is type-asserted without validation
Verdict
PASS — The code is well-structured with thorough tests covering the new verification lifecycle phases. The state machine transitions are correctly constrained. The verify method properly invalidates stale results and attestations before starting a new verification cycle. The workflow YAML files correctly isolate the review agent to read-only tools (Read, Glob, Grep), preventing prompt injection from escalating to arbitrary command execution. The shell scripts in the build workflow properly double-quote template expressions, preventing injection through workflow inputs.
The empty-steps bypass (Medium 1 and 2) is a real gap in the verification gate but is mitigated by the attestation being posted to swamp-club (server-side validation) and the fact that the calling agent is expected to cooperate. Consider adding .min(1) or requiring at least one succeeded step as defense-in-depth.