Conversation
…b directly Mirrors the changes in systeminit/swamp#1139 — the @swamp/issue-lifecycle extension and skill now operate directly on swamp-club lab issues. Issues must already exist in swamp-club; the model fetches them by sequential lab number via GET /api/v1/lab/issues/{number} and PATCHes the type during triage to reflect the classification back. - Delete extensions/models/_lib/issue_tracker.ts and every gh CLI call. - Swap GlobalArgs: drop repo, issueNumber now refers to a swamp-club lab issue. Version bumped to 2026.04.08.1 with an upgrade that strips repo. - Rework SwampClubClient: operate on the lab number directly, add fetchIssue() and updateType(). Drop the /ensure round-trip. - Reconcile classification types to swamp-club's bug/feature/security. Regressions become type=bug with an isRegression flag on the classification record. unclear is gone — use confidence=low + clarifyingQuestions instead. - Remove ci_status, record_pr, fix methods and ciResults/fixDirective resources. The ci_review phase is gone; implementing transitions straight to done. - Update schemas_test.ts state machine tests to the new topology: happy path ends implementing→done, new completion gate test, old fix loop and ci_review tests removed. - Rewrite skill SKILL.md, triage.md, implementation.md, extension README.md, and manifest.yaml description/method list for the swamp-club-first workflow. Depends on swamp-club PATCH-type support in systeminit/swamp-club#374 (merged). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
Truncated AGPL license header in
swamp_club.ts— two lines were accidentally removed from the license footer (diff shows-// You should have received a copy of the GNU Affero General Public License alongand-// with Swamp. If not, see <https://www.gnu.org/licenses/>.). The header in the other files ends with those lines;swamp_club.tsnow does not. -
No tests for new
SwampClubClientmethods —fetchIssue(),updateType(), and the refactoredpatchIssue()are untested. The old code had the GitHub CLI as an external dependency that was hard to unit-test; the new HTTP-based client is straightforward to exercise withDeno.serve({ port: 0 }). Consider adding aswamp_club_test.tscovering at least the happy-path fetch, the 404/error-returns-null case, and the PATCH flow. -
Unvalidated type cast in
fetchIssue()—(issue.type ?? "feature") as IssueTypesilently accepts any string the API returns. If swamp-club ever returns a value outside["bug", "feature", "security"](e.g. a new type added later), the cast succeeds here but fails downstream at Zod schema validation when the context resource is written, producing a less-readable error. Consider usingIssueType.safeParse(issue.type)and falling back to"feature"only on parse failure. -
isRegression: trueis not guarded totype === "bug"— the schema and code both document thatisRegressionimpliestype: bug, but there is no runtime assertion. A caller could passtype=feature, isRegression=truewithout an error. Low-risk given the skill docs are clear, but a one-lineif (args.isRegression && args.type !== "bug")throw would make the invariant explicit.
The refactor is clean and well-scoped. Deleting the 387-line GitHub CLI wrapper and the CI/fix loop simplifies the model substantially. The state machine change (implementing → done direct) is correctly reflected in the schema, transitions map, and tests. Upgrade migration correctly strips the repo attribute.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
issue_lifecycle.ts:425-435—isRegressioninvariant not enforced at the schema or method level.
The docs and skill references stateisRegression: true"impliestype=bug". But nothing prevents callingtriagewithtype=security, isRegression=true. The classification record and the swamp-club lifecycle entry would both contain a contradictory{type: "security", isRegression: true}. This is a data integrity issue — downstream consumers trusting the invariant would draw wrong conclusions.
Breaking example:swamp model method run issue-42 triage --input type=security --input isRegression=true --input confidence=high --input reasoning="..."succeeds and writes contradictory data.
Suggested fix: Add a Zod.refine()on the triage arguments or an early guard inexecute:if (args.isRegression && args.type !== "bug") { throw new Error("isRegression=true requires type=bug"); }
-
swamp_club.ts:111—issue.typecast bypasses runtime validation.
(issue.type ?? "feature") as IssueTypetrusts the API response. If swamp-club returns an unexpected type value (e.g.,"task","incident", or a future type added server-side), it flows into thecontextresource unchecked. Whether this corrupts data depends on whether the framework validateswriteResourcepayloads against the Zod schema at runtime.
Breaking example: swamp-club adds a"task"issue type;fetchIssuereturns{type: "task"}, which is cast toIssueTypeand written to the context resource. If the framework doesn't validate, thecontext.typefield silently contains an invalid enum value.
Suggested fix: UseIssueType.catch("feature").parse(issue.type)instead ofas IssueType.
Low
-
swamp_club.ts:151-157,188-195— Response bodies not consumed on success.
BothpostLifecycleEntryandpatchIssuereadres.text()on non-OK responses for logging, but on success the response body is never consumed or cancelled. In Deno, unconsumed bodies can keep HTTP connections open until GC. With the 15s timeout this is bounded, but under rapid iteration (many lifecycle calls in sequence) it could temporarily exhaust the connection pool.
Suggested fix: Addawait res.body?.cancel();after theif (!res.ok)block in each method, or structure asif (!res.ok) { ... } else { await res.body?.cancel(); }. -
issue_lifecycle.ts:108-112— Upgrade stripsrepobut leavesissueNumbersemantically reinterpreted.
The upgrade function deletesrepobut keepsissueNumberas-is. For existing instances,issueNumberreferred to a GitHub issue number (e.g.,850). Post-upgrade, it refers to a swamp-club lab issue number. Runningstarton an upgraded instance will attempt to fetch swamp-club issue#850, which almost certainly doesn't exist, and throw. This is the correct behavior (fail-fast), but the error message at line 374 doesn't hint that this might be an upgraded instance with a staleissueNumber. Consider adding a note in the upgradedescriptionor the error message. -
issue_lifecycle.ts:1008-1010—approvereads plan after writing state.
Theapprovemethod writes the state to"approved"(line 1001) before reading the plan (line 1008). IfreadResourcefails (throws), the state is already transitioned toapprovedbut the lifecycle entry was never posted to swamp-club. Theplan-existscheck should prevent plan-less approval, so this is defense-in-depth ordering — but swapping the read before the write would be marginally safer.
Verdict
PASS — This is a clean, well-structured refactor that simplifies the model by removing ~1,000 lines of GitHub integration code. The state machine changes are consistent, the upgrade path is correct, tests are updated, and the new swamp-club-first flow is coherent. The medium findings are minor data integrity gaps that don't block merge.
The _swampClub module-level singleton was keyed on nothing — once created for issue #N, every subsequent getSwampClub() call returned the same client regardless of the new issueNumber in globalArgs. Since user models are loaded via dynamic import() in the same process, the module stays cached across method calls, so running start for issue #10 and then issue #20 in the same session silently sent #20's lifecycle entries, type updates, and status transitions to #10. Drop the cache entirely and call createSwampClubClient directly at each use. The reachability check is a single 5s-timeout HTTP GET and runs once per method invocation — negligible next to the lifecycle POST already happening on the same code path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Adversarial Review
Critical / High
No critical or high severity findings.
Medium
-
issue_lifecycle.ts:925—resolve_findingssilently skips resolutions with emptyresolutionNote.
The guardif (note)is falsy for empty strings. If a caller passes{ findingId: "ADV-1", resolutionNote: "" }, theresolutionMap.get(f.id)returns"", theif (note)branch is skipped, and the finding is not resolved — but the log on line 943 still reports it as resolved (since it countsargs.resolutions.length, not actual matches).Breaking input:
resolutions: [{ findingId: "ADV-1", resolutionNote: "" }]→ finding stays unresolved, log says "Resolved 1 finding(s)", user thinks it worked.Suggested fix: Change
if (note)toif (note !== undefined)(the Map returnsundefinedfor missing keys, not"") so that empty-string resolutions are still applied. -
issue_lifecycle.ts:943—resolve_findingsreports phantom resolutions for unknown finding IDs.
const resolved = args.resolutions.lengthcounts all inputs, including those whosefindingIddoesn't match any existing finding. A typo in a finding ID silently drops the resolution.Breaking input:
resolutions: [{ findingId: "ADV-TYPO", resolutionNote: "fixed" }]→ nothing resolved, log says "Resolved 1 finding(s)".Suggested fix: Count the findings that actually flipped
resolvedto true, or throw/warn when afindingIdhas no match.
Low
-
swamp_club.ts:92-106—fetchIssueperforms no schema validation on the API response.
The response is cast viaas { issue?: ... }afterres.json(). If the swamp-club API returns an unexpected shape (e.g.,{ data: { issue: ... } }or a flat object), the code silently returnsnull— which is handled, but indistinguishable from a 404, making debugging harder. Not a correctness bug since the null path is handled, but a diagnostic gap. -
issue_lifecycle.ts:61—readStatecasts deserialized JSON without Zod validation.
JSON.parse(...) as StateDatatrusts the stored data shape. If a prior version wrote a different schema (e.g., before the upgrade removedrepo), thephasefield could be missing or unexpected, causing thevalid-transitioncheck to silently pass or fail in confusing ways. The upgrade mechanism should prevent this in practice, but the cast provides no safety net. -
swamp_club.ts:110—typedefaults to"feature"when API returns no type field.
(issue.type ?? "feature") as IssueTypemeans an issue withtype: nullortype: undefinedfrom the API silently becomes a "feature". If the intent is that type is always present on swamp-club issues, this default masks a broken API response.
Verdict
PASS — The code is a straightforward simplification (net -1067 lines) with a clean state machine, good transition guards, and consistent best-effort patterns for the swamp-club API. The empty-string resolution note bug (Medium #1) is the only finding that produces incorrect behavior in a realistic scenario, but it requires a somewhat unusual input. No blockers.
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
Unsafe type cast in
fetchIssue()—(issue.type ?? "feature") as IssueTypesilently accepts any string the server returns. Consider usingIssueType.safeParse(issue.type)and falling back on a default or throwing, so a server returning an unexpected type doesn't silently corrupt the context resource. -
SwampClubClient.logonly bindswarning— the constructor assignsthis.log = logger?.warning.bind(logger) ?? (() => {}). This means all client-level log calls go through the warning channel; there's no way to emit info-level messages from within the class. Not a bug, but worth noting if finer-grained log levels are desired later.
Overall this is a clean, well-structured refactor. The simplification is significant (−1067 net lines), the state machine tests are solid, no any types in hand-written code, credentials are kept private via #apiKey, and error paths don't expose secrets. The upgrade path correctly strips the deprecated repo arg. Approved.
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
Unsafe type cast in
fetchIssue()—(issue.type ?? "feature") as IssueTypesilently accepts any string the server returns. Consider usingIssueType.safeParse(issue.type)and falling back on a default or throwing, so a server returning an unexpected type doesn't silently corrupt the context resource. -
SwampClubClient.logonly bindswarning— the constructor assignsthis.log = logger?.warning.bind(logger) ?? (() => {}). This means all client-level log calls go through the warning channel; there's no way to emit info-level messages from within the class. Not a bug, but worth noting if finer-grained log levels are desired later.
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
Unsafe type cast in fetchIssue() - the expression (issue.type ?? 'feature') as IssueType silently accepts any string the server returns. Consider using IssueType.safeParse(issue.type) and falling back on a default or throwing, so a server returning an unexpected type does not silently corrupt the context resource.
-
SwampClubClient.log only binds warning - the constructor assigns this.log = logger?.warning.bind(logger) ?? (() => {}). All client-level log calls go through the warning channel with no way to emit info-level messages from within the class. Not a bug, but worth noting if finer-grained log levels are desired later.
Overall this is a clean, well-structured refactor. The simplification is significant (-1067 net lines), the state machine tests are solid, no any types in hand-written code, credentials are kept private via #apiKey, and error paths do not expose secrets. The upgrade path correctly strips the deprecated repo arg.
The swamp-club refactor accidentally dropped the final two lines of the
required copyright header ("You should have received a copy..." and
"with Swamp. If not, see..."). Per CLAUDE.md, every .ts file must carry
the full AGPLv3 header from FILE-LICENSE-TEMPLATE.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Adversarial Review
Critical / High
No critical or high severity issues found.
Medium
-
swamp_club.ts:114— UnsafeIssueTypecast from API response(issue.type ?? "feature") as IssueTypecasts whatever string the API returns to theIssueTypeunion without validation. If swamp-club returnstype: "enhancement"(or any value not inbug | feature | security), it passes through as-is. Downstream,triagewould callsc.updateType(args.type)with the user-supplied classification, but the context resource would already contain the invalid type fromfetchIssue. Whether this causes a runtime failure depends on whether the framework validateswriteResourcedata against the Zod schema — if it doesn't, the invalid value silently persists.Breaking input: swamp-club API returns
{ "issue": { "number": 42, "type": "enhancement", ... } }.Suggested fix: Validate with the Zod enum and fall back:
type: IssueType.catch("feature").parse(issue.type),
-
issue_lifecycle.ts:943—resolve_findingsmisreports resolution count for non-existent finding IDsLine 943 reports
args.resolutions.lengthas the "resolved" count, butresolutionMap.get(f.id)only matches findings whose ID exists incurrent.findings. If a caller passesfindingId: "ADV-99"and no such finding exists, the log and lifecycle entry claim it was resolved, but nothing was actually changed. Theremainingcount (line 944) is correct, but theresolvedcount is inflated.Breaking input:
{ resolutions: [{ findingId: "ADV-NONEXISTENT", resolutionNote: "fixed" }] }— logs "Resolved 1 finding(s)" but zero findings are actually resolved.Suggested fix: Count actual matches:
const resolved = updatedFindings.filter( (f) => f.resolved && resolutionMap.has(f.id), ).length;
-
issue_lifecycle.ts:1002-1010—approvewrites state before reading planThe
approvemethod writesphase: "approved"(line 1003) before reading the plan (line 1010). IfreadResourcethrows (framework error, corrupt data), the state is already transitioned toapprovedwith no rollback. Theplan-existspre-flight check mitigates this for the null case, but a JSON parse failure in the framework would leave the model inapprovedphase without a valid plan reference.Suggested fix: Read the plan first, then write the state transition.
Low
-
swamp_club.ts:65— Logger useswarninglevel for all swamp-club operationsthis.log = logger?.warning.bind(logger) ?? (() => {});binds all logging to thewarninglevel. Successful lifecycle posts produce no log output, but all error/failure messages are warnings. This is fine functionally, but meansthis.logon line 65 is slightly misleading — it's an error logger, not a general logger. -
issue_lifecycle.ts:61—readStateparses JSON withascast, no Zod validationJSON.parse(new TextDecoder().decode(content)) as StateDatatrusts the stored data is valid. If a prior bug or manual edit corrupts the state file, the invalid data passes through silently. Thephasefield would be an arbitrary string that never matchesTRANSITIONSentries, causing confusing error messages rather than a clear "corrupted state" error. -
issue_lifecycle.ts:609—reviewmethod accessible from any phase includingcreatedreviewhas no entry inTRANSITIONS, so thevalid-transitioncheck returnspass: true(line 157). Callingreviewbeforestartthrows"No plan exists yet"— a correct but slightly confusing error. Not a bug, just an indirect error message.
Verdict
PASS — The code is a clean, well-structured refactoring that removes a large GitHub-coupled subsystem in favor of a simpler HTTP client. The state machine tests are thorough. The medium findings are genuine but non-blocking edge cases: the type cast (#1) is the most worth addressing before merge since it could silently persist invalid data.
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
** health-check overhead**: every method that calls (triage, plan, iterate, adversarial_review, resolve_findings, approve, implement, complete) makes an extra HTTP GET to
/api/healthbefore the actual operation. For operations where swamp-club posting is best-effort, the health check cost may not be worth it — if swamp-club is unreachable, the fetch/PATCH will just silently fail anyway. Consider skipping the health check for methods where the client is optional. -
All swamp_club logs use
warninglevel:this.log = logger?.warning.bind(logger)means every log in the client is a warning, including informational messages. If the host logger distinguishes info vs warning, it may cause noise. Minor, but worth noting. -
No tests for
swamp_club.ts: the new HTTP client (fetchIssue, updateType, patchIssue, postLifecycleEntry, loadAuthFile, createSwampClubClient) has no test coverage. CLAUDE.md only mandates tests for vault/ and datastore/ extensions, so this isn't blocking — but a local Deno.serve mock would give useful regression protection for the new API contract.
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
Health-check overhead in createSwampClubClient: every method that calls createSwampClubClient (triage, plan, iterate, adversarial_review, resolve_findings, approve, implement, complete) makes an extra HTTP GET to /api/health before the actual operation. For methods where swamp-club posting is best-effort, the health check cost may not be worth it — if swamp-club is unreachable, the fetch/PATCH will fail silently anyway. Consider skipping the health check for optional-client callers.
-
All swamp_club logs use warning level:
this.log = logger?.warning.bind(logger)means every log in the client is a warning, including informational failure messages. If the host logger distinguishes info vs warning, this may cause noise. -
No tests for swamp_club.ts: the new HTTP client (fetchIssue, updateType, patchIssue, postLifecycleEntry, loadAuthFile, createSwampClubClient) has no test coverage. CLAUDE.md only mandates tests for vault/ and datastore/ extensions, so this is not blocking — but a local Deno.serve mock would give useful regression protection for the new API contract.
Backport of systeminit/swamp#1143. Restores the PR-linkage capability dropped in #57 (drop GitHub integration) without re-introducing any git-host coupling. The refactor correctly removed the `gh` CLI dependency, but it also removed the concept of PR linkage — an audit-trail concept that isn't coupled to GitHub. This brings back the minimal piece. ## New domain concepts - `pr_open` phase — the PR is linked and we're awaiting CI/review/merge. No new swamp-club status; maps to `in_progress` like `implementing`. - `pullRequest-main` resource — single-instance value object holding the opaque PR URL and a `linkedAt` timestamp. Git-host agnostic. - `link_pr` method — idempotent (valid from both `implementing` and `pr_open`); each call overwrites the resource so re-pushes, URL corrections, or replacement PRs don't need a new phase. ## Backwards compatibility `complete` now accepts both `implementing` and `pr_open` as source phases. Existing records that didn't go through `link_pr` can still reach `done` via the legacy path. New work should flow `implementing → link_pr → pr_open → complete → done`. ## Version bump manifest.yaml: `2026.04.08.1 → 2026.04.08.2`, with the state machine diagram and methods list updated to reflect the new phase and method. Model upgrade entry added; no global-args migration needed. ## Skill doc hardening While updating `implementation.md` for the new `link_pr` step, also scoped all hardcoded tmp filename examples in `planning.md` and `adversarial-review.md`: - `/tmp/plan.yaml` → `/tmp/plan-issue-<N>.yaml` - `/tmp/findings.yaml` → `/tmp/findings-issue-<N>.yaml` - `/tmp/resolutions.yaml` → `/tmp/resolutions-issue-<N>.yaml` Generic filenames collide with stale content from previous lifecycle sessions and can silently leak unrelated data into the current run. ## Test coverage Existing `schemas_test.ts` updated to reflect the new state machine: - Happy-path walk now routes `implementing → link_pr → pr_open → complete`. - New `legacy path` test ensures `complete` still accepts `implementing`. - `completion gate` assertion expanded to `["implementing", "pr_open"]`. New tests added: - `Phase: pr_open sits between implementing and done` - `TRANSITIONS: link_pr is idempotent from implementing and pr_open` - `TRANSITIONS: link_pr is rejected from earlier lifecycle phases` - `PullRequestSchema` positive, empty-rejection, required-field tests New `issue_lifecycle_test.ts` with 7 method-level tests covering the `link_pr` happy path, state transition to `pr_open`, idempotency, zod schema rejection, and model registration smoke tests. Full verification gate (in the issue-lifecycle package): - `deno task check` — clean - `deno task lint` — clean - `deno task fmt:check` — clean - `deno task test` — **23 passed, 0 failed** (16 schema + 7 method) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backport of systeminit/swamp#1143. Restores the PR-linkage capability dropped in #57 (drop GitHub integration) without re-introducing any git-host coupling. The refactor correctly removed the `gh` CLI dependency, but it also removed the concept of PR linkage — an audit-trail concept that isn't coupled to GitHub. This brings back the minimal piece. ## New domain concepts - `pr_open` phase — the PR is linked and we're awaiting CI/review/merge. No new swamp-club status; maps to `in_progress` like `implementing`. - `pullRequest-main` resource — single-instance value object holding the opaque PR URL and a `linkedAt` timestamp. Git-host agnostic. - `link_pr` method — idempotent (valid from both `implementing` and `pr_open`); each call overwrites the resource so re-pushes, URL corrections, or replacement PRs don't need a new phase. ## Backwards compatibility `complete` now accepts both `implementing` and `pr_open` as source phases. Existing records that didn't go through `link_pr` can still reach `done` via the legacy path. New work should flow `implementing → link_pr → pr_open → complete → done`. ## Version bump manifest.yaml: `2026.04.08.1 → 2026.04.08.2`, with the state machine diagram and methods list updated to reflect the new phase and method. Model upgrade entry added; no global-args migration needed. ## Skill doc hardening While updating `implementation.md` for the new `link_pr` step, also scoped all hardcoded tmp filename examples in `planning.md` and `adversarial-review.md`: - `/tmp/plan.yaml` → `/tmp/plan-issue-<N>.yaml` - `/tmp/findings.yaml` → `/tmp/findings-issue-<N>.yaml` - `/tmp/resolutions.yaml` → `/tmp/resolutions-issue-<N>.yaml` Generic filenames collide with stale content from previous lifecycle sessions and can silently leak unrelated data into the current run. ## Test coverage Existing `schemas_test.ts` updated to reflect the new state machine: - Happy-path walk now routes `implementing → link_pr → pr_open → complete`. - New `legacy path` test ensures `complete` still accepts `implementing`. - `completion gate` assertion expanded to `["implementing", "pr_open"]`. New tests added: - `Phase: pr_open sits between implementing and done` - `TRANSITIONS: link_pr is idempotent from implementing and pr_open` - `TRANSITIONS: link_pr is rejected from earlier lifecycle phases` - `PullRequestSchema` positive, empty-rejection, required-field tests New `issue_lifecycle_test.ts` with 7 method-level tests covering the `link_pr` happy path, state transition to `pr_open`, idempotency, zod schema rejection, and model registration smoke tests. Full verification gate (in the issue-lifecycle package): - `deno task check` — clean - `deno task lint` — clean - `deno task fmt:check` — clean - `deno task test` — **23 passed, 0 failed** (16 schema + 7 method) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Mirrors systeminit/swamp#1139. The
@swamp/issue-lifecycleextension and skill now operate directly on swamp-club lab issues. Issues must already exist in swamp-club — the model fetches them by sequential lab number viaGET /api/v1/lab/issues/{number}and PATCHes thetypefield during triage so the classification is reflected back in swamp-club.extensions/models/_lib/issue_tracker.ts(387 lines ofghCLI wrapper) and every tracker/GitHub call inissue_lifecycle.ts.repois dropped;issueNumbernow refers to a swamp-club lab issue number. Version bumped to2026.04.08.1with an upgrade entry that stripsrepofrom old instances.fetchIssue()(GET) andupdateType()(PATCH) methods. The GitHub-coupled/ensureround-trip is gone.bug | feature | security. Regressions are modelled astype: bugwith a newisRegression: booleanflag on the classification record.unclearis gone — useconfidence: low+clarifyingQuestionsinstead.ci_status,record_pr,fix(all GitHub PR/CI coupled). Resources removed:ciResults,fixDirective. Phase removed:ci_review. State machine is nowimplementing → donedirectly.schemas_test.tsupdated to reflect the new state machine: happy path endsimplementing→done, newcompletion gatetest, oldfix loopandci_reviewtests removed.SKILL.md,triage.md,implementation.md) andREADME.md+manifest.yamldescription/method list updated for the swamp-club-first workflow.Depends on systeminit/swamp-club#374 (merged), which adds PATCH-type support to swamp-club.
New state machine
Diff stats
10 files changed, 402 insertions(+), 1469 deletions(-) — net −1067 lines.
Test plan
deno task check— cleandeno task lint— cleandeno task fmt— cleandeno task test— 9 passed, 0 failed (state machine invariants)swamp model create @swamp/issue-lifecycle issue-<N> --global-arg issueNumber=<N>, thenstart→triageand verify the swamp-club issue type flips and the lifecycle entries appear.🤖 Generated with Claude Code