@muggleai/works 5.4.2 → 5.5.1

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.
Files changed (84) hide show
  1. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  2. package/dist/plugin/.cursor-plugin/plugin.json +1 -1
  3. package/dist/plugin/hooks/README.md +6 -0
  4. package/dist/plugin/hooks/hooks.json +20 -7
  5. package/dist/plugin/scripts/guardrail-build-router.sh +17 -3
  6. package/dist/plugin/scripts/guardrail-e2e-gate.sh +33 -3
  7. package/dist/plugin/scripts/guardrail-pr-opened.sh +15 -4
  8. package/dist/plugin/scripts/guardrail-record-tests.sh +19 -6
  9. package/dist/plugin/scripts/guardrail-report-format.sh +17 -3
  10. package/dist/plugin/scripts/reconcile-stale-watchers.sh +59 -0
  11. package/dist/plugin/skills/CLAUDE.md +6 -0
  12. package/dist/plugin/skills/_shared/dev-loop/run.md +1 -1
  13. package/dist/plugin/skills/_shared/post-merge-cleanup.md +2 -3
  14. package/dist/plugin/skills/_shared/rebase-before-e2e.md +1 -1
  15. package/dist/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -1
  16. package/dist/plugin/skills/_shared/resolve-rebase-conflicts.md +9 -33
  17. package/dist/plugin/skills/_shared/session-state-writes.md +2 -2
  18. package/dist/plugin/skills/_shared/telemetry-events/muggle-do-cycle.md +1 -1
  19. package/dist/plugin/skills/_shared/telemetry-events/pr-followup-tick.md +2 -0
  20. package/dist/plugin/skills/_shared/use-worktrees.md +0 -1
  21. package/dist/plugin/skills/_shared/vcs/github/verify-working-tree.md +1 -1
  22. package/dist/plugin/skills/_shared/verify-or-rollback-gate.md +39 -0
  23. package/dist/plugin/skills/do/address-reviews.md +2 -11
  24. package/dist/plugin/skills/do/fix-ci.md +3 -2
  25. package/dist/plugin/skills/do/resolve-conflicts.md +7 -13
  26. package/dist/plugin/skills/do/respawn-watcher.md +27 -0
  27. package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +5 -3
  28. package/dist/plugin/skills/muggle-pr-followup/SKILL.md +7 -1
  29. package/dist/plugin/skills/muggle-pr-followup/auto-track.md +1 -1
  30. package/dist/plugin/skills/muggle-pr-followup/blocked-tick.md +38 -0
  31. package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
  32. package/dist/plugin/skills/muggle-pr-followup/cancel-cron.md +6 -5
  33. package/dist/plugin/skills/muggle-pr-followup/contract.md +29 -7
  34. package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +89 -0
  35. package/dist/plugin/skills/muggle-pr-followup/output-templates/blocked-reminder.md +30 -0
  36. package/dist/plugin/skills/muggle-pr-followup/output-templates/watcher-log.md +11 -1
  37. package/dist/plugin/skills/muggle-pr-followup/output-templates.md +2 -1
  38. package/dist/plugin/skills/muggle-pr-followup/reconcile.md +38 -5
  39. package/dist/plugin/skills/muggle-pr-followup/record-cron-id.md +12 -0
  40. package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +38 -4
  41. package/dist/plugin/skills/skill-deps.config.json +8 -0
  42. package/dist/release-manifest.json +4 -4
  43. package/package.json +7 -6
  44. package/plugin/.claude-plugin/plugin.json +1 -1
  45. package/plugin/.cursor-plugin/plugin.json +1 -1
  46. package/plugin/hooks/README.md +6 -0
  47. package/plugin/hooks/hooks.json +20 -7
  48. package/plugin/scripts/guardrail-build-router.sh +17 -3
  49. package/plugin/scripts/guardrail-e2e-gate.sh +33 -3
  50. package/plugin/scripts/guardrail-pr-opened.sh +15 -4
  51. package/plugin/scripts/guardrail-record-tests.sh +19 -6
  52. package/plugin/scripts/guardrail-report-format.sh +17 -3
  53. package/plugin/scripts/reconcile-stale-watchers.sh +59 -0
  54. package/plugin/skills/CLAUDE.md +6 -0
  55. package/plugin/skills/_shared/dev-loop/run.md +1 -1
  56. package/plugin/skills/_shared/post-merge-cleanup.md +2 -3
  57. package/plugin/skills/_shared/rebase-before-e2e.md +1 -1
  58. package/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -1
  59. package/plugin/skills/_shared/resolve-rebase-conflicts.md +9 -33
  60. package/plugin/skills/_shared/session-state-writes.md +2 -2
  61. package/plugin/skills/_shared/telemetry-events/muggle-do-cycle.md +1 -1
  62. package/plugin/skills/_shared/telemetry-events/pr-followup-tick.md +2 -0
  63. package/plugin/skills/_shared/use-worktrees.md +0 -1
  64. package/plugin/skills/_shared/vcs/github/verify-working-tree.md +1 -1
  65. package/plugin/skills/_shared/verify-or-rollback-gate.md +39 -0
  66. package/plugin/skills/do/address-reviews.md +2 -11
  67. package/plugin/skills/do/fix-ci.md +3 -2
  68. package/plugin/skills/do/resolve-conflicts.md +7 -13
  69. package/plugin/skills/do/respawn-watcher.md +27 -0
  70. package/plugin/skills/muggle-pr-followup/CLAUDE.md +5 -3
  71. package/plugin/skills/muggle-pr-followup/SKILL.md +7 -1
  72. package/plugin/skills/muggle-pr-followup/auto-track.md +1 -1
  73. package/plugin/skills/muggle-pr-followup/blocked-tick.md +38 -0
  74. package/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
  75. package/plugin/skills/muggle-pr-followup/cancel-cron.md +6 -5
  76. package/plugin/skills/muggle-pr-followup/contract.md +29 -7
  77. package/plugin/skills/muggle-pr-followup/evals/evals.json +89 -0
  78. package/plugin/skills/muggle-pr-followup/output-templates/blocked-reminder.md +30 -0
  79. package/plugin/skills/muggle-pr-followup/output-templates/watcher-log.md +11 -1
  80. package/plugin/skills/muggle-pr-followup/output-templates.md +2 -1
  81. package/plugin/skills/muggle-pr-followup/reconcile.md +38 -5
  82. package/plugin/skills/muggle-pr-followup/record-cron-id.md +12 -0
  83. package/plugin/skills/muggle-pr-followup/state-schemas.md +38 -4
  84. package/plugin/skills/skill-deps.config.json +8 -0
@@ -1,9 +1,19 @@
1
1
  # Reconcile Procedure
2
2
 
3
- The procedure for the **reconcile mode** of `muggle-pr-followup` — a sweep that finalizes session slots whose PR went terminal while polling was lapsed. Routing is in [`SKILL.md`](SKILL.md#routing).
3
+ The procedure for the **reconcile mode** of `muggle-pr-followup` — a sweep that finalizes session slots whose PR went terminal while polling was lapsed, and re-arms open slots whose watcher stopped silently (a dropped respawn). Routing is in [`SKILL.md`](SKILL.md#routing).
4
4
 
5
5
  Termination is otherwise tick-driven ([`contract.md`](contract.md) Step 2): a slot finalizes only when a tick fires and observes `MERGED` / `CLOSED`. If the tick stream stops first — the recurring `/loop` cron auto-expires after 7 days, the session ends, or the machine is off when the PR merges — no tick catches the transition, and the slot is left un-finalized: no `result.md`, no post-merge cleanup, and a surviving cron would keep polling a dead PR. Reconcile is the catch-up.
6
6
 
7
+ ## Triggers
8
+
9
+ Three ways in, all running the same procedure:
10
+
11
+ - **Manual** — `/muggle:muggle-pr-followup reconcile` (or `sweep`).
12
+ - **Auto-track** — the top of a no-arg invocation ([`auto-track.md`](auto-track.md)).
13
+ - **Session start** — the `reconcile-stale-watchers.sh` hook ([`../../hooks/README.md`](../../hooks/README.md)) runs this sweep, catching a watcher that died with its session (end, or 7-day `/loop` cron expiry) before its PR's merge was observed.
14
+
15
+ Recover-don't-seed holds on every trigger: a session-start run still never seeds a first watcher (see Invariants).
16
+
7
17
  ## Input
8
18
 
9
19
  `$ARGUMENTS` is `reconcile` (or `sweep`), optionally followed by a `<slug>` to scope the sweep to one slot.
@@ -20,14 +30,37 @@ For each candidate, fetch the PR per [`../_shared/vcs/github/pr-metadata.md`](..
20
30
 
21
31
  ### Step 3 — Finalize the terminal ones
22
32
 
23
- For each candidate whose live `state` is `MERGED` or `CLOSED`, run [`finalize.md`](finalize.md). `finalize.md` dispatches nothing, so a backfilled merge gets no post-merge cleanup — its branch is typically long gone, and the `autoCleanup` gate governs if the user runs cleanup later. Slots still `open` are left untouched — reconcile finalizes, it does not re-arm a watcher (re-arming an open PR is [`auto-track.md`](auto-track.md)'s job).
33
+ For each candidate whose live `state` is `MERGED` or `CLOSED`, run [`finalize.md`](finalize.md) — which unschedules the cron recorded-id-first per [`cancel-cron.md`](cancel-cron.md), killing it even when `CronList` has gone blind. `finalize.md` dispatches nothing, so a backfilled merge gets no post-merge cleanup — its branch is typically long gone, and the `autoCleanup` gate governs if the user runs cleanup later. Slots still `open` are carried to Step 3.6, which re-arms any whose watcher went silent.
34
+
35
+ ### Step 3.5 — Sweep orphaned crons
36
+
37
+ Step 3 kills the cron of every slot it finalized this run. This step catches the crons **finalize can't reach through a slot** — a watcher cron whose session slot was deleted out from under it, or one already-finalized (`result.md` present, skipped in Step 1) whose cron outlived the finalize. Both keep polling a dead or absent PR until the 7-day `/loop` expiry.
38
+
39
+ Call `CronList`. For every job whose command ends with `/muggle:muggle-pr-followup <slug> <n>`:
40
+
41
+ - **No session slot for `<slug>`** (`~/.muggle-ai/muggle-do/sessions/<slug>/` is gone) → `CronDelete` it. The slot it belonged to was removed; the cron is a pure orphan.
42
+ - **Slot present and terminal** (`result.md` exists, or `prs.json[0].state` is `merged`/`closed`) → `CronDelete` it. A straggler the finalize missed.
43
+ - **Slot present and open** → leave it. A live watcher.
44
+
45
+ This reaches only crons `CronList` still enumerates. A cron that both survived a compaction (invisible to `CronList`) **and** lost its slot (recorded id gone with it) is beyond either mechanism — see the residual note below.
46
+
47
+ ### Step 3.6 — Re-arm a silently-stopped open watcher
48
+
49
+ The recovery net for a **dropped respawn**: a `/muggle-do` cycle cancels the watcher's cron when it dispatches ([`contract.md`](contract.md) Steps 4 / 5 / 5b) and is responsible for respawning it when the cycle ends, but a cycle that crashes or errors out before it respawns can leave an open slot with no cron and no next tick — the poller stops silently. This step re-arms it.
50
+
51
+ For each candidate still `open` after Step 3, check when its watcher last ticked — the newest ISO-8601 line in `followup.log` (or `cron.json.recorded_at` if the log is empty). If that is **older than 15 minutes** (comfortably beyond the `1m` cadence, so a live cron would have logged many times inside the window), the poller is gone → re-arm:
52
+
53
+ - `CronCreate` a recurring cron (call the **tool**, never a shell) with `cron: "* * * * *"` and prompt `/muggle:muggle-pr-followup <slug> <n>`, then record its id and `interval: "1m"` to `cron.json` (whole-file rewrite per [`state-schemas.md`](state-schemas.md#cronjson)). Append a `re-armed (silent watcher)` line to the slot's `followup.log`.
54
+
55
+ A fresh log line (within the window) means the cron is alive — even one `CronList` has gone blind to — so this step leaves it untouched; re-arming can never double an already-live poller. This recovers only a slot that was **already being watched**; a PR that never had a watcher is seeded by [`auto-track.md`](auto-track.md) / bootstrap, not here.
24
56
 
25
57
  ### Step 4 — Report
26
58
 
27
- One line: slots scanned, finalized (with final state each), and left open. Silent only when zero slots exist.
59
+ One line: slots scanned, finalized (with final state each), left open, re-armed (silently-stopped watchers recovered), and orphan crons swept. Silent only when zero slots exist and nothing was swept.
28
60
 
29
61
  ## Invariants
30
62
 
31
- - **Idempotent.** A slot with `result.md` is never re-finalized; once everything terminal is swept, re-running is a no-op.
32
- - **Finalize-only.** Reconcile never seeds, re-arms, or dispatches a watcher. Open slots pass through untouched.
63
+ - **Idempotent.** A slot with `result.md` is never re-finalized; a still-ticking open slot is never re-armed; once everything terminal is swept and every open watcher is live, re-running is a no-op.
64
+ - **Recover, don't seed.** Reconcile finalizes terminal slots, sweeps orphan crons, and re-arms an open slot whose watcher went silent (a dropped respawn — Step 3.6). It never arms a PR that was never watched — seeding a first watcher is [`auto-track.md`](auto-track.md)'s / bootstrap's job.
33
65
  - **Per-slot isolation.** One slot's `gh` failure never blocks finalizing the others.
66
+ - **Residual orphan.** A cron that survived a compaction (blind to `CronList`) whose slot was also deleted has no on-disk id left to `CronDelete` and no `CronList` entry to match — only a session restart clears it. Recording the id durably while the slot lives ([`record-cron-id.md`](record-cron-id.md)) shrinks this window to slots removed before their first tick.
@@ -0,0 +1,12 @@
1
+ # Record the watcher's cron id
2
+
3
+ The self-record every tick runs so this slot's cron stays deletable after `CronList` goes blind to it. `CronList` stops enumerating a `/loop` cron once its session is continued or compacted, but the cron keeps firing; a cron id captured to `cron.json` **while the cron was still visible** remains a valid `CronDelete` target for teardown ([`cancel-cron.md`](cancel-cron.md), [`finalize.md`](finalize.md), [`reconcile.md`](reconcile.md)). Recording early — every tick, starting with the first — is what makes the id durable.
4
+
5
+ > **`CronList` is a Claude Code tool call, not a shell command.** Invoke it through the tool system. Never wrap it in Bash.
6
+
7
+ 1. Call the `CronList` tool.
8
+ 2. Find the job whose command ends with `/muggle:muggle-pr-followup <slug> <n>` — the exact two-arg match for this slot's PR.
9
+ 3. If found and its id differs from `cron.json.cron_id`: rewrite `cron.json` (whole-file Write per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md)) with the observed `cron_id`, the current `interval`, and a fresh `recorded_at`.
10
+ 4. If `CronList` returns nothing (already blind) and `cron.json` holds a non-null `cron_id`: **leave it** — the previously-recorded id is the only handle left, so never overwrite it with `null`.
11
+
12
+ Skip in the stale-fire path ([`contract.md`](contract.md) Step 0): a stale slot is being torn down, not re-recorded.
@@ -30,6 +30,23 @@ A list of one entry. (Historical: the file is an array for forward-compat with t
30
30
  - `state` is the **observed** state from the last `gh pr view`. The watcher refreshes it each tick.
31
31
  - Terminal states (`merged`, `closed`) are sticky — once set, the watcher writes `result.md` and exits without rescheduling.
32
32
 
33
+ ## `cron.json`
34
+
35
+ A durable, on-disk handle to this slot's watcher cron. Its whole reason to exist: `CronList` goes **blind to crons that outlive a session continue / compaction** (the watcher's `/loop` cron survives, but the tool can no longer enumerate it), so a teardown that can only find crons through `CronList` can never delete the orphan — it re-fires until the 7-day `/loop` expiry. A cron id recorded to disk **while the cron was still visible** stays a valid `CronDelete` target afterward. See [`record-cron-id.md`](record-cron-id.md) (who writes it) and [`cancel-cron.md`](cancel-cron.md) (who deletes by it).
36
+
37
+ ```json
38
+ {
39
+ "cron_id": "<scheduler-id-or-null>",
40
+ "command": "/muggle:muggle-pr-followup <slug> <n>",
41
+ "interval": "1m",
42
+ "recorded_at": "<ISO-8601>"
43
+ }
44
+ ```
45
+
46
+ - `cron_id`: the scheduler id of the live `/loop` cron for this slot. Bootstrap seeds `null` (it dispatches `/loop` as its last action and cannot yet see the id); the first tick self-records the real id per [`record-cron-id.md`](record-cron-id.md). `null` again for the one tick after `/muggle-do` respawns the watcher (a dispatch cancels the old cron and the respawn arms a new one whose id is unknown until the next tick observes it).
47
+ - `command`: the exact two-arg dispatch, the same string [`cancel-cron.md`](cancel-cron.md) matches on as its `CronList` fallback.
48
+ - `interval`: the poll cadence — always `1m`. The watcher polls at `1m` whether or not the PR is blocked; a blocked PR reminds at `1m`, it does not back off (see [`blocked-tick.md`](blocked-tick.md) and [`contract.md`](contract.md) Steps 2.5 / 7). Recorded for teardown/forensics.
49
+
33
50
  ## `last_seen.json`
34
51
 
35
52
  Keyed by `"<owner>/<repo>#<n>"`. One key per PR in the slot.
@@ -45,8 +62,17 @@ Keyed by `"<owner>/<repo>#<n>"`. One key per PR in the slot.
45
62
  "pushed_shas": ["<sha>", ...],
46
63
  "ci_fix_attempts": { "<sha>": <int> },
47
64
  "ci_escalated_shas": ["<sha>", ...],
48
- "conflict_resolve_attempts": { "<sha>": <int> },
49
- "conflict_escalated_shas": ["<sha>", ...]
65
+ "conflict_resolve_attempts": { "<head-sha>..<base-tip-sha>": <int> },
66
+ "conflict_escalated_keys": ["<head-sha>..<base-tip-sha>", ...],
67
+ "blocked": {
68
+ "reason": "conflict_escalated" | "ci_escalated" | "reviews_escalated",
69
+ "since": "<ISO-8601>",
70
+ "fingerprint": {
71
+ "head_sha": "<sha>",
72
+ "latest_review_id": <int>,
73
+ "ci_digest": "<string>"
74
+ }
75
+ }
50
76
  }
51
77
  }
52
78
  ```
@@ -59,8 +85,16 @@ Keyed by `"<owner>/<repo>#<n>"`. One key per PR in the slot.
59
85
  - `pushed_shas`: every SHA `/muggle-do` has pushed for this PR. Append-only. Used by the resolve-reminder stage to recognize threads addressed by the loop.
60
86
  - `ci_fix_attempts`: per-SHA count of fix-ci cycles `/muggle-do` has run. The watcher stops dispatching fix-ci for a SHA once its count reaches 3. Keyed by head SHA.
61
87
  - `ci_escalated_shas`: head SHAs whose CI the fix-ci stage gave up on (attempts exhausted or only out-of-scope checks). The watcher excludes these from CI dispatch so a hopeless SHA is never re-fixed.
62
- - `conflict_resolve_attempts`: per-SHA count of rebase cycles `/muggle-do` has run for this SHA (behind-only or conflicting — both rebase onto the base). The watcher stops dispatching a rebase for a SHA once its count reaches 2. Keyed by head SHA. A clean behind-only rebase produces a new SHA, so the cap only bites a SHA that keeps failing to rebase-and-verify.
63
- - `conflict_escalated_shas`: head SHAs whose rebase `/muggle-do` gave up on (attempts exhausted, or a conflict under `autoResolveConflicts=never`). The watcher excludes these from rebase dispatch so a hopeless SHA is never re-attempted.
88
+ - `conflict_resolve_attempts`: count of rebase cycles `/muggle-do` has run (behind-only or conflicting — both rebase onto the base). The watcher stops dispatching once a key's count reaches 2. Keyed by `rebase_key` — `"<head_sha>..<base_tip_sha>"`, the head paired with the base branch tip it was measured against.
89
+ - `conflict_escalated_keys`: `rebase_key`s whose rebase `/muggle-do` gave up on (attempts exhausted, or a conflict under `autoResolveConflicts=never`). The watcher excludes these from rebase dispatch so a hopeless pairing is never re-attempted.
90
+
91
+ Both are keyed on the pair, not the head alone, because whether a branch conflicts depends on both sides. Under a head-only key, a base that moves produces a genuinely new conflict against an unchanged head — and the stale entry suppresses it permanently, because nothing can change the head while the branch sits blocked. Pairing re-arms the budget whenever either side moves. Legacy entries written before this change are bare SHAs with no `..`; readers ignore them, which un-wedges any slot they had blocked.
92
+
93
+ Unlike these, `ci_fix_attempts` / `ci_escalated_shas` stay keyed on the head SHA alone — a CI result is a function of the head only, so base movement must not re-arm them.
94
+ - `blocked`: present only while the watcher is **awaiting the owner** on a PR that cannot progress without a human ([`contract.md`](contract.md) Step 7). Absent ⇒ the watcher is in its normal dispatch flow. When present, the watcher **keeps the normal `1m` cadence** and each tick is a reminder-or-resume check ([`contract.md`](contract.md) Step 2.5): it re-emits a one-line reminder to the owner, recomputes the `fingerprint`, and clears the block the moment any component moves. Its value is the reason-specific reminder plus fingerprint auto-resume.
95
+ - `reason`: which durable block is being awaited — `conflict_escalated` (`rebase_key` ∈ `conflict_escalated_keys`), `ci_escalated` (`head_sha` ∈ `ci_escalated_shas`), or `reviews_escalated` (a review sits in `escalated_review_ids` awaiting the user, actionable set empty). Selects the reminder wording; the resume decision is fingerprint-driven, not reason-driven.
96
+ - `since`: when the block was first flagged — lets the reminder state how long the owner has been the blocker.
97
+ - `fingerprint`: the external state the block is waiting on. `head_sha` moves on a new push (which also clears the per-SHA escalation sets, keyed by SHA); `latest_review_id` is `max(id)` over submitted reviews and moves when a reviewer submits anything new; `ci_digest` is a stable digest of the head SHA's CI rollup (bucket + each check's name/conclusion, sorted) and moves when a check flips, a rerun lands, or an external check such as a staging deploy posts. Any change clears the block and resumes evaluation.
64
98
 
65
99
  ## `state.md`
66
100
 
@@ -0,0 +1,8 @@
1
+ {
2
+ "supportDirs": { "do": "muggle-do" },
3
+ "sharedNamespaces": ["_shared"],
4
+ "knownReverseDeps": {
5
+ "comment": "Reverse dependencies grandfathered so CI stays green — each is a genuine violation of plugin/skills/CLAUDE.md (a shared module or lower-level skill linking UP into a caller). Currently empty: the tree is one-way. If an entry is ever added here, it is debt to fix and delete, not a pattern to copy. The guard blocks any NEW cycle regardless of this list.",
6
+ "edges": []
7
+ }
8
+ }