@muggleai/works 5.9.0 → 5.11.0
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/README.md +8 -0
- package/dist/{chunk-LVUJ4E62.js → chunk-MSPMXBO5.js} +638 -468
- package/dist/{chunk-5PYK7DWB.js → chunk-WUMAJOBT.js} +17 -6
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/README.md +19 -13
- package/dist/plugin/agents/test-prepare-runner.md +4 -1
- package/dist/plugin/agents/visual-walkthrough-builder.md +12 -5
- package/dist/plugin/commands/mtest.md +1 -1
- package/dist/plugin/hooks/README.md +15 -3
- package/dist/plugin/hooks/hooks.json +79 -0
- package/dist/plugin/scripts/ensure-electron-app.sh +62 -47
- package/dist/plugin/scripts/guardrail-classify-gate.sh +23 -0
- package/dist/plugin/scripts/guardrail-debug-path-gate.sh +41 -0
- package/dist/plugin/scripts/guardrail-pr-terminal.sh +6 -2
- package/dist/plugin/scripts/guardrail-record-stage-read.sh +21 -0
- package/dist/plugin/scripts/guardrail-record-stage-signals.sh +23 -0
- package/dist/plugin/scripts/guardrail-record-tests.sh +11 -4
- package/dist/plugin/scripts/guardrail-report-format.sh +9 -5
- package/dist/plugin/scripts/guardrail-skill-stages.sh +35 -0
- package/dist/plugin/scripts/guardrail-stage-gate.sh +43 -0
- package/dist/plugin/scripts/guardrail-walkthrough-gate.sh +41 -0
- package/dist/plugin/scripts/guardrails.mjs +474 -80
- package/dist/plugin/scripts/pr-watch-events.sh +116 -0
- package/dist/plugin/scripts/pr-watch-guards.sh +8 -2
- package/dist/plugin/scripts/pr-watch-loop.sh +254 -0
- package/dist/plugin/scripts/reconcile-stale-watchers.sh +66 -18
- package/dist/plugin/scripts/sign-body.sh +88 -0
- package/dist/plugin/skills/CLAUDE.md +16 -0
- package/dist/plugin/skills/_shared/post-merge-cleanup.md +83 -3
- package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +6 -8
- package/dist/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +10 -18
- package/dist/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -0
- package/dist/plugin/skills/_shared/vcs/CLAUDE.md +1 -0
- package/dist/plugin/skills/_shared/vcs/github/pr-edit.md +3 -2
- package/dist/plugin/skills/_shared/vcs/github/reply-line-comment.md +3 -2
- package/dist/plugin/skills/_shared/vcs/github/top-level-comment.md +3 -2
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-create.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-edit.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-note.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +4 -1
- package/dist/plugin/skills/_shared/vcs/post-signature.md +23 -12
- package/dist/plugin/skills/do/open-prs/forward.md +2 -2
- package/dist/plugin/skills/do/per-comment-replies.md +2 -15
- package/dist/plugin/skills/muggle/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-browser-task/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +2 -1
- package/dist/plugin/skills/muggle-pr-followup/SKILL.md +6 -3
- package/dist/plugin/skills/muggle-pr-followup/adopt.md +60 -0
- package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +10 -14
- package/dist/plugin/skills/muggle-pr-followup/auto-track.md +2 -0
- package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +63 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates/help.md +11 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +1 -7
- package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -4
- package/dist/plugin/skills/muggle-pr-followup/reconcile.md +32 -6
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +18 -0
- package/dist/plugin/skills/muggle-preferences/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-preferences/evals/evals.json +45 -0
- package/dist/plugin/skills/muggle-preferences/ops/change-one.md +1 -1
- package/dist/plugin/skills/muggle-preferences/ops/configure.md +4 -5
- package/dist/plugin/skills/muggle-preferences/ops/list.md +1 -1
- package/dist/plugin/skills/muggle-preferences/ops/set.md +3 -4
- package/dist/plugin/skills/muggle-preferences/preference-gates/README.md +3 -3
- package/dist/plugin/skills/muggle-preferences/preference-gates/autoSelectLocalHost.md +2 -2
- package/dist/plugin/skills/muggle-preferences/preference-gates/autoSelectProject.md +1 -1
- package/dist/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +23 -0
- package/dist/plugin/skills/muggle-test/SKILL.md +23 -3
- package/dist/plugin/skills/muggle-test/execute-local.md +2 -0
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +6 -1
- package/dist/plugin/skills/muggle-test-import/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-test-prepare/SKILL.md +28 -15
- package/dist/plugin/skills/muggle-test-prepare/steps/check-running.md +1 -1
- package/dist/plugin/skills/muggle-test-prepare/steps/confirm-recipe.md +56 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/derive-service-graph.md +51 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +111 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
- package/dist/plugin/skills/muggle-test-prepare/steps/readiness-report.md +12 -5
- package/dist/plugin/skills/muggle-test-prepare/steps/record-resolution.md +32 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/replay-or-learn.md +20 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +9 -6
- package/dist/plugin/skills/muggle-test-prepare/steps/smoke-test.md +5 -1
- package/dist/plugin/skills/muggle-test-prepare/steps/start-services.md +3 -1
- package/dist/plugin/skills/muggle-test-regenerate-missing/SKILL.md +1 -1
- package/dist/release-manifest.json +4 -4
- package/dist/src-B5OTAZKM.js +1 -0
- package/package.json +7 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/README.md +19 -13
- package/plugin/agents/test-prepare-runner.md +4 -1
- package/plugin/agents/visual-walkthrough-builder.md +12 -5
- package/plugin/commands/mtest.md +1 -1
- package/plugin/hooks/README.md +15 -3
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/scripts/ensure-electron-app.sh +62 -47
- package/plugin/scripts/guardrail-classify-gate.sh +23 -0
- package/plugin/scripts/guardrail-debug-path-gate.sh +41 -0
- package/plugin/scripts/guardrail-pr-terminal.sh +6 -2
- package/plugin/scripts/guardrail-record-stage-read.sh +21 -0
- package/plugin/scripts/guardrail-record-stage-signals.sh +23 -0
- package/plugin/scripts/guardrail-record-tests.sh +11 -4
- package/plugin/scripts/guardrail-report-format.sh +9 -5
- package/plugin/scripts/guardrail-skill-stages.sh +35 -0
- package/plugin/scripts/guardrail-stage-gate.sh +43 -0
- package/plugin/scripts/guardrail-walkthrough-gate.sh +41 -0
- package/plugin/scripts/guardrails.mjs +474 -80
- package/plugin/scripts/pr-watch-events.sh +116 -0
- package/plugin/scripts/pr-watch-guards.sh +8 -2
- package/plugin/scripts/pr-watch-loop.sh +254 -0
- package/plugin/scripts/reconcile-stale-watchers.sh +66 -18
- package/plugin/scripts/sign-body.sh +88 -0
- package/plugin/skills/CLAUDE.md +16 -0
- package/plugin/skills/_shared/post-merge-cleanup.md +83 -3
- package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +6 -8
- package/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +10 -18
- package/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -0
- package/plugin/skills/_shared/vcs/CLAUDE.md +1 -0
- package/plugin/skills/_shared/vcs/github/pr-edit.md +3 -2
- package/plugin/skills/_shared/vcs/github/reply-line-comment.md +3 -2
- package/plugin/skills/_shared/vcs/github/top-level-comment.md +3 -2
- package/plugin/skills/_shared/vcs/gitlab/mr-create.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-edit.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-note.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +4 -1
- package/plugin/skills/_shared/vcs/post-signature.md +23 -12
- package/plugin/skills/do/open-prs/forward.md +2 -2
- package/plugin/skills/do/per-comment-replies.md +2 -15
- package/plugin/skills/muggle/SKILL.md +1 -1
- package/plugin/skills/muggle-browser-task/SKILL.md +1 -1
- package/plugin/skills/muggle-pr-followup/CLAUDE.md +2 -1
- package/plugin/skills/muggle-pr-followup/SKILL.md +6 -3
- package/plugin/skills/muggle-pr-followup/adopt.md +60 -0
- package/plugin/skills/muggle-pr-followup/arm-watcher.md +10 -14
- package/plugin/skills/muggle-pr-followup/auto-track.md +2 -0
- package/plugin/skills/muggle-pr-followup/evals/evals.json +63 -1
- package/plugin/skills/muggle-pr-followup/output-templates/help.md +11 -1
- package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +1 -7
- package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -4
- package/plugin/skills/muggle-pr-followup/reconcile.md +32 -6
- package/plugin/skills/muggle-pr-followup/state-schemas.md +18 -0
- package/plugin/skills/muggle-preferences/SKILL.md +1 -1
- package/plugin/skills/muggle-preferences/evals/evals.json +45 -0
- package/plugin/skills/muggle-preferences/ops/change-one.md +1 -1
- package/plugin/skills/muggle-preferences/ops/configure.md +4 -5
- package/plugin/skills/muggle-preferences/ops/list.md +1 -1
- package/plugin/skills/muggle-preferences/ops/set.md +3 -4
- package/plugin/skills/muggle-preferences/preference-gates/README.md +3 -3
- package/plugin/skills/muggle-preferences/preference-gates/autoSelectLocalHost.md +2 -2
- package/plugin/skills/muggle-preferences/preference-gates/autoSelectProject.md +1 -1
- package/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +23 -0
- package/plugin/skills/muggle-test/SKILL.md +23 -3
- package/plugin/skills/muggle-test/execute-local.md +2 -0
- package/plugin/skills/muggle-test-feature-local/SKILL.md +6 -1
- package/plugin/skills/muggle-test-import/SKILL.md +1 -1
- package/plugin/skills/muggle-test-prepare/SKILL.md +28 -15
- package/plugin/skills/muggle-test-prepare/steps/check-running.md +1 -1
- package/plugin/skills/muggle-test-prepare/steps/confirm-recipe.md +56 -0
- package/plugin/skills/muggle-test-prepare/steps/derive-service-graph.md +51 -0
- package/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +111 -0
- package/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
- package/plugin/skills/muggle-test-prepare/steps/readiness-report.md +12 -5
- package/plugin/skills/muggle-test-prepare/steps/record-resolution.md +32 -0
- package/plugin/skills/muggle-test-prepare/steps/replay-or-learn.md +20 -0
- package/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +9 -6
- package/plugin/skills/muggle-test-prepare/steps/smoke-test.md +5 -1
- package/plugin/skills/muggle-test-prepare/steps/start-services.md +3 -1
- package/plugin/skills/muggle-test-regenerate-missing/SKILL.md +1 -1
- package/dist/src-FDK2KQSZ.js +0 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Wake conditions for the muggle-pr-followup watch loop — the single definition
|
|
4
|
+
# of what makes a quiet watcher speak. Sourced by pr-watch-loop.sh.
|
|
5
|
+
#
|
|
6
|
+
# These live in a file, rather than in the prose an arming session reads, because
|
|
7
|
+
# a re-derived loop silently loses conditions. Observed across four slots on one
|
|
8
|
+
# machine: two implemented the behind-base wake and two did not, and the two that
|
|
9
|
+
# did not left their PRs permanently unmergeable under a watcher that looked
|
|
10
|
+
# healthy — alive, heartbeating, no fetch errors, and nothing to say. A dropped
|
|
11
|
+
# wake has no failure mode that anyone notices.
|
|
12
|
+
#
|
|
13
|
+
# Every function is pure: arguments in, one event line on stdout, no I/O and no
|
|
14
|
+
# provider calls. That keeps each condition directly testable, which is the
|
|
15
|
+
# property prose never had. Exit 0 means "woke and emitted", 1 means "stay quiet".
|
|
16
|
+
# Each caller owns its own floor and advances it only on a real wake, so one
|
|
17
|
+
# occurrence fires exactly once.
|
|
18
|
+
|
|
19
|
+
# Splits the tab-separated state line into its fields, one per line, preserving
|
|
20
|
+
# empty ones. Not a wake, but every wake below reads its arguments out of this.
|
|
21
|
+
#
|
|
22
|
+
# `IFS=$'\t' read` cannot do it: tab is an IFS *whitespace* character, so bash
|
|
23
|
+
# collapses runs of tabs into a single delimiter, and the two adjacent tabs an
|
|
24
|
+
# empty field produces silently shift every later field left. With no unresolved
|
|
25
|
+
# thread — the common case, and also what a push leaves behind once its thread
|
|
26
|
+
# goes outdated — the thread field is empty, so the pending-check count lands in
|
|
27
|
+
# unresolved_threads and fires a thread wake for a PR with no threads, while the
|
|
28
|
+
# check digest lands in failed_checks and leaves red-CI detection reading a
|
|
29
|
+
# string where it expects a count. awk with an explicit FS does not collapse.
|
|
30
|
+
watch_split_state() {
|
|
31
|
+
printf '%s\n' "$1" | awk -F'\t' '{for (i = 1; i <= NF; i++) print $i}'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# A submitted review newer than the floor. Monotonic ids, so `>` is the whole
|
|
35
|
+
# test. PENDING (unsubmitted) reviews are excluded by the caller's query — they
|
|
36
|
+
# are the reviewer's own drafts and are not feedback until submitted.
|
|
37
|
+
watch_wake_review() {
|
|
38
|
+
local pr="$1" latest="$2" floor="$3"
|
|
39
|
+
[ "${latest:-0}" -gt "${floor:-0}" ] 2>/dev/null || return 1
|
|
40
|
+
echo "EVENT pr=$pr new submitted review id=$latest"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# A thread comment newer than the floor. Same monotonic-id reasoning as reviews.
|
|
44
|
+
watch_wake_comment() {
|
|
45
|
+
local pr="$1" latest="$2" floor="$3"
|
|
46
|
+
[ "${latest:-0}" -gt "${floor:-0}" ] 2>/dev/null || return 1
|
|
47
|
+
echo "EVENT pr=$pr new thread comment id=$latest"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# A thread that is unresolved and not already known to be. `known` is the
|
|
51
|
+
# semicolon-joined THREADS floor; membership is the test, not ordering, because
|
|
52
|
+
# thread ids are opaque strings rather than a monotonic sequence.
|
|
53
|
+
watch_wake_thread() {
|
|
54
|
+
local pr="$1" thread_id="$2" known="$3"
|
|
55
|
+
[ -n "$thread_id" ] || return 1
|
|
56
|
+
case ";${known};" in
|
|
57
|
+
*";${thread_id};"*) return 1 ;;
|
|
58
|
+
esac
|
|
59
|
+
echo "EVENT pr=$pr thread newly unresolved id=$thread_id"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# The head's checks having **settled** red: nothing still pending and at least
|
|
63
|
+
# one failure. Pending-with-a-failure is not a wake — a run in flight may still
|
|
64
|
+
# go green, and the tick would idle on it anyway.
|
|
65
|
+
#
|
|
66
|
+
# The floor is the head SHA rather than a monotonic id because a check rollup is
|
|
67
|
+
# not monotonic: it flips green to red and resets on every push. Keying on the
|
|
68
|
+
# red head fires once per red head and re-arms on the next push.
|
|
69
|
+
watch_wake_ci_red() {
|
|
70
|
+
local pr="$1" pending_count="$2" failed_count="$3" head_sha="$4" floor="$5"
|
|
71
|
+
[ -n "$head_sha" ] || return 1
|
|
72
|
+
[ "$head_sha" != "$floor" ] || return 1
|
|
73
|
+
[ "${pending_count:-0}" -eq 0 ] 2>/dev/null || return 1
|
|
74
|
+
[ "${failed_count:-0}" -gt 0 ] 2>/dev/null || return 1
|
|
75
|
+
echo "EVENT pr=$pr checks settled red head=$head_sha"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# The branch needing a rebase onto its base — conflicting with it, or merely
|
|
79
|
+
# behind it.
|
|
80
|
+
#
|
|
81
|
+
# Behind is the half that regenerated loops kept dropping, and it is invisible
|
|
82
|
+
# from the signals a conflict check uses: a behind-but-clean branch reports
|
|
83
|
+
# mergeable=MERGEABLE, exactly like a current one. `mergeStateStatus` cannot
|
|
84
|
+
# stand in either — it is one enum with precedence, and BLOCKED (review
|
|
85
|
+
# required) masks BEHIND on any PR still awaiting approval, which is most of
|
|
86
|
+
# them. Behind-ness has to be measured, so the caller passes `behind_count` from
|
|
87
|
+
# a compare call.
|
|
88
|
+
#
|
|
89
|
+
# The floor is the rebase key `<head_sha>..<base_tip_sha>`, pairing both sides:
|
|
90
|
+
# staleness is a function of the pair, so a head-only key would wedge
|
|
91
|
+
# permanently the first time the base advanced (nothing can move the head while
|
|
92
|
+
# the branch sits blocked). Pairing re-arms whenever either side moves.
|
|
93
|
+
watch_wake_rebase() {
|
|
94
|
+
local pr="$1" mergeable="$2" behind_count="$3" rebase_key="$4" floor="$5"
|
|
95
|
+
[ -n "$rebase_key" ] || return 1
|
|
96
|
+
[ "$rebase_key" != "$floor" ] || return 1
|
|
97
|
+
if [ "$mergeable" = "CONFLICTING" ]; then
|
|
98
|
+
echo "EVENT pr=$pr branch conflicting with base key=$rebase_key"
|
|
99
|
+
return 0
|
|
100
|
+
fi
|
|
101
|
+
[ -n "$behind_count" ] && [ "$behind_count" -gt 0 ] 2>/dev/null || return 1
|
|
102
|
+
echo "EVENT pr=$pr branch behind base by $behind_count — rebase due key=$rebase_key"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# A blocked watch whose CI signature moved at all. This is a resume probe, not a
|
|
106
|
+
# failure probe: it is dormant unless `blocked_digest` is set, and it wakes on
|
|
107
|
+
# any move away from that value rather than only on red, so a block waiting on a
|
|
108
|
+
# green pass, a rerun, or an external deploy check resumes as promptly as one
|
|
109
|
+
# waiting on a failure.
|
|
110
|
+
watch_wake_blocked_resume() {
|
|
111
|
+
local pr="$1" digest="$2" blocked_digest="$3"
|
|
112
|
+
[ -n "$blocked_digest" ] || return 1
|
|
113
|
+
[ -n "$digest" ] || return 1
|
|
114
|
+
[ "$digest" != "$blocked_digest" ] || return 1
|
|
115
|
+
echo "EVENT pr=$pr ci digest moved while blocked — resume check"
|
|
116
|
+
}
|
|
@@ -18,11 +18,14 @@
|
|
|
18
18
|
# on its own; reconcile re-arms an open PR inside a live
|
|
19
19
|
# session.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# Seconds a watch loop may live. 0 means unbounded — the `never` setting of the
|
|
22
|
+
# watcherLifetime preference, which removes the only time-based reaper for a
|
|
23
|
+
# loop whose session has gone. `watcher_superseded` is then the sole guard.
|
|
24
|
+
MUGGLE_PR_WATCH_MAX_LIFETIME="${MUGGLE_PR_WATCH_MAX_LIFETIME:-604800}"
|
|
22
25
|
MUGGLE_PR_WATCH_POLL_INTERVAL="${MUGGLE_PR_WATCH_POLL_INTERVAL:-60}"
|
|
23
26
|
# Consecutive failed fetches before a loop gives up. A watcher must ride through
|
|
24
27
|
# a GitHub / network outage — an observed drop lasted ~8 minutes — not die and
|
|
25
|
-
# leave the PR unwatched until
|
|
28
|
+
# leave the PR unwatched until its owning session next starts. With the backoff, 60
|
|
26
29
|
# spans hours; only a genuinely persistent unreachable slot (deleted repo,
|
|
27
30
|
# revoked auth) exhausts it.
|
|
28
31
|
MUGGLE_PR_WATCH_MAX_FETCH_FAILURES="${MUGGLE_PR_WATCH_MAX_FETCH_FAILURES:-60}"
|
|
@@ -50,6 +53,9 @@ watcher_superseded() {
|
|
|
50
53
|
|
|
51
54
|
watcher_lifetime_exceeded() {
|
|
52
55
|
local started="$1" now="$2" max="${3:-$MUGGLE_PR_WATCH_MAX_LIFETIME}"
|
|
56
|
+
# 0 is unbounded, not "already expired" — the arithmetic below would other-
|
|
57
|
+
# wise make every loop exit on its first iteration.
|
|
58
|
+
[ "$max" -eq 0 ] 2>/dev/null && return 1
|
|
53
59
|
[ $((now - started)) -ge "$max" ]
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# The muggle-pr-followup watch loop. One process per PR slot: polls provider
|
|
4
|
+
# state, prints one line per new event, and exits when the PR goes terminal.
|
|
5
|
+
#
|
|
6
|
+
# Ships as a file so arming runs it rather than rewrites it. A loop re-derived
|
|
7
|
+
# from prose on every arm drifts — the behind-base wake went missing from half
|
|
8
|
+
# the slots on one machine that way, leaving PRs unmergeable under watchers that
|
|
9
|
+
# looked healthy. The wake conditions live in pr-watch-events.sh; this file is
|
|
10
|
+
# the I/O around them.
|
|
11
|
+
#
|
|
12
|
+
# Usage: pr-watch-loop.sh --slot <dir> --repo <owner/name> --pr <n> --base <branch>
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
|
|
18
|
+
slot=""
|
|
19
|
+
repo=""
|
|
20
|
+
pr_number=""
|
|
21
|
+
base_branch="master"
|
|
22
|
+
|
|
23
|
+
while [ $# -gt 0 ]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--slot) slot="$2"; shift 2 ;;
|
|
26
|
+
--repo) repo="$2"; shift 2 ;;
|
|
27
|
+
--pr) pr_number="$2"; shift 2 ;;
|
|
28
|
+
--base) base_branch="$2"; shift 2 ;;
|
|
29
|
+
*) echo "pr-watch-loop: unknown argument $1" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
if [ -z "$slot" ] || [ -z "$repo" ] || [ -z "$pr_number" ]; then
|
|
34
|
+
echo "pr-watch-loop: --slot, --repo and --pr are required" >&2
|
|
35
|
+
exit 2
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Resolved absolutely at arm time so they still load after the arming session is
|
|
39
|
+
# gone. Guards missing means the plugin moved or upgraded underneath this loop —
|
|
40
|
+
# a newer version's watcher owns the slot now, so step down rather than run on
|
|
41
|
+
# without the supersede check.
|
|
42
|
+
for lib in pr-watch-guards.sh pr-watch-events.sh; do
|
|
43
|
+
[ -f "${script_dir}/${lib}" ] || exit 0
|
|
44
|
+
# shellcheck source=/dev/null
|
|
45
|
+
. "${script_dir}/${lib}"
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
echo "$$" > "${slot}/watch.pid"
|
|
49
|
+
started=$(date +%s)
|
|
50
|
+
fails=0
|
|
51
|
+
|
|
52
|
+
# In-memory floors, above the on-disk watermark. The watermark is advanced by
|
|
53
|
+
# the session after it handles a wave; these stop the loop re-reporting an event
|
|
54
|
+
# in the window before that write lands.
|
|
55
|
+
floor_review=0
|
|
56
|
+
floor_comment=0
|
|
57
|
+
floor_threads=";"
|
|
58
|
+
floor_ci_red=""
|
|
59
|
+
floor_rebase=""
|
|
60
|
+
floor_blocked_digest=""
|
|
61
|
+
|
|
62
|
+
# Pin the token once: a detached loop can lose access to gh's OS keyring
|
|
63
|
+
# mid-run, which surfaces as empty fetches rather than an error.
|
|
64
|
+
pinned_token="$(gh auth token 2>/dev/null)"
|
|
65
|
+
[ -n "$pinned_token" ] && export GH_TOKEN="$pinned_token"
|
|
66
|
+
|
|
67
|
+
fetch_pr_state() {
|
|
68
|
+
gh api graphql -F owner="${repo%%/*}" -F name="${repo##*/}" -F number="$pr_number" -f query='
|
|
69
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
70
|
+
repository(owner: $owner, name: $name) {
|
|
71
|
+
pullRequest(number: $number) {
|
|
72
|
+
state
|
|
73
|
+
headRefOid
|
|
74
|
+
baseRefOid
|
|
75
|
+
mergeable
|
|
76
|
+
commits(last: 1) {
|
|
77
|
+
nodes {
|
|
78
|
+
commit {
|
|
79
|
+
statusCheckRollup {
|
|
80
|
+
contexts(first: 100) {
|
|
81
|
+
nodes {
|
|
82
|
+
__typename
|
|
83
|
+
... on CheckRun { name status conclusion }
|
|
84
|
+
... on StatusContext { context state }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
reviews(last: 20, states: [COMMENTED, APPROVED, CHANGES_REQUESTED, DISMISSED]) { nodes { databaseId } }
|
|
92
|
+
reviewThreads(first: 100) {
|
|
93
|
+
nodes {
|
|
94
|
+
id
|
|
95
|
+
isResolved
|
|
96
|
+
isOutdated
|
|
97
|
+
comments(last: 1) { nodes { databaseId pullRequestReview { state } } }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}' --jq '
|
|
103
|
+
.data.repository.pullRequest as $pr
|
|
104
|
+
| (($pr.commits.nodes[0].commit.statusCheckRollup.contexts.nodes) // []) as $contexts
|
|
105
|
+
| ($contexts | map(
|
|
106
|
+
if .__typename == "CheckRun"
|
|
107
|
+
then {name: .name, verdict: (if .status != "COMPLETED" then "PENDING" else (.conclusion // "NEUTRAL") end)}
|
|
108
|
+
else {name: .context, verdict: (.state // "PENDING")}
|
|
109
|
+
end)) as $checks
|
|
110
|
+
| [
|
|
111
|
+
$pr.state,
|
|
112
|
+
$pr.headRefOid,
|
|
113
|
+
$pr.baseRefOid,
|
|
114
|
+
$pr.mergeable,
|
|
115
|
+
(([$pr.reviews.nodes[].databaseId] | max) // 0),
|
|
116
|
+
(([$pr.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[]
|
|
117
|
+
| select((.pullRequestReview.state // "SUBMITTED") != "PENDING") | .databaseId] | max) // 0),
|
|
118
|
+
([$pr.reviewThreads.nodes[] | select(.isResolved == false) | select(.isOutdated == false)
|
|
119
|
+
| select((.comments.nodes[0].pullRequestReview.state // "SUBMITTED") != "PENDING") | .id] | join(";")),
|
|
120
|
+
($checks | map(select(.verdict == "PENDING")) | length),
|
|
121
|
+
($checks | map(select(.verdict == "FAILURE" or .verdict == "ERROR" or .verdict == "TIMED_OUT" or .verdict == "STARTUP_FAILURE")) | length),
|
|
122
|
+
($checks | sort_by(.name) | map(.name + ":" + .verdict) | join(","))
|
|
123
|
+
] | @tsv' 2>>"${slot}/watch-fetch.log"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# behind_by needs its own call — see watch_wake_rebase for why no field on the
|
|
127
|
+
# PR carries it. Made only when the head/base pair moved, so the steady state
|
|
128
|
+
# stays one request per iteration.
|
|
129
|
+
fetch_behind_count() {
|
|
130
|
+
local head_sha="$1"
|
|
131
|
+
gh api "repos/${repo}/compare/${base_branch}...${head_sha}" --jq '.behind_by' 2>>"${slot}/watch-fetch.log"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
read_watermark_value() {
|
|
135
|
+
local key="$1" line value=""
|
|
136
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
137
|
+
line="${line%$'\r'}"
|
|
138
|
+
case "$line" in
|
|
139
|
+
"${key}="*)
|
|
140
|
+
value="${line#"${key}"=}"
|
|
141
|
+
value="${value%\"}"
|
|
142
|
+
value="${value#\"}"
|
|
143
|
+
;;
|
|
144
|
+
esac
|
|
145
|
+
done < "${slot}/watch-watermark.env"
|
|
146
|
+
printf '%s' "$value"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
while :; do
|
|
150
|
+
watcher_superseded "$slot" "$$" && exit 0
|
|
151
|
+
watcher_lifetime_exceeded "$started" "$(date +%s)" && exit 0
|
|
152
|
+
touch "${slot}/watch-heartbeat" 2>/dev/null
|
|
153
|
+
|
|
154
|
+
# No watermark yet means the arming session has not finished seeding. Wait
|
|
155
|
+
# rather than treat every floor as zero, which would fire the whole backlog.
|
|
156
|
+
if [ ! -f "${slot}/watch-watermark.env" ]; then
|
|
157
|
+
sleep "$MUGGLE_PR_WATCH_POLL_INTERVAL"
|
|
158
|
+
continue
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
watermark_review=$(read_watermark_value REV)
|
|
162
|
+
watermark_comment=$(read_watermark_value COM)
|
|
163
|
+
watermark_threads=$(read_watermark_value THREADS)
|
|
164
|
+
watermark_ci_red=$(read_watermark_value CIRED)
|
|
165
|
+
watermark_rebase=$(read_watermark_value REBASED)
|
|
166
|
+
watermark_blocked_digest=$(read_watermark_value BLOCKED_CIDIGEST)
|
|
167
|
+
|
|
168
|
+
state_line=$(fetch_pr_state)
|
|
169
|
+
# One quick retry before counting a strike: a single flaky call should not
|
|
170
|
+
# advance the failure budget.
|
|
171
|
+
[ -z "$state_line" ] && { sleep 3; state_line=$(fetch_pr_state); }
|
|
172
|
+
|
|
173
|
+
if [ -z "$state_line" ]; then
|
|
174
|
+
fails=$((fails + 1))
|
|
175
|
+
echo "$(date -u +%FT%TZ) fetch empty (${fails}/${MUGGLE_PR_WATCH_MAX_FETCH_FAILURES})" >> "${slot}/watch-fetch.log"
|
|
176
|
+
if [ "$fails" -ge "$MUGGLE_PR_WATCH_MAX_FETCH_FAILURES" ]; then
|
|
177
|
+
echo "WATCH-FAIL pr=$pr_number ${fails} consecutive fetch failures — see watch-fetch.log"
|
|
178
|
+
exit 1
|
|
179
|
+
fi
|
|
180
|
+
sleep "$(watcher_fetch_backoff "$fails")"
|
|
181
|
+
continue
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
mapfile -t state_fields < <(watch_split_state "$state_line")
|
|
185
|
+
pr_state="${state_fields[0]-}"
|
|
186
|
+
head_sha="${state_fields[1]-}"
|
|
187
|
+
base_sha="${state_fields[2]-}"
|
|
188
|
+
mergeable="${state_fields[3]-}"
|
|
189
|
+
latest_review="${state_fields[4]-}"
|
|
190
|
+
latest_comment="${state_fields[5]-}"
|
|
191
|
+
unresolved_threads="${state_fields[6]-}"
|
|
192
|
+
pending_checks="${state_fields[7]-}"
|
|
193
|
+
failed_checks="${state_fields[8]-}"
|
|
194
|
+
ci_digest="${state_fields[9]-}"
|
|
195
|
+
|
|
196
|
+
if [ "$pr_state" = "MERGED" ] || [ "$pr_state" = "CLOSED" ]; then
|
|
197
|
+
echo "TERMINAL pr=$pr_number state=$pr_state"
|
|
198
|
+
exit 0
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
if [ "$pr_state" != "OPEN" ]; then
|
|
202
|
+
fails=$((fails + 1))
|
|
203
|
+
if [ "$fails" -ge "$MUGGLE_PR_WATCH_MAX_FETCH_FAILURES" ]; then
|
|
204
|
+
echo "WATCH-FAIL pr=$pr_number unreadable state after ${fails} tries"
|
|
205
|
+
exit 1
|
|
206
|
+
fi
|
|
207
|
+
sleep "$(watcher_fetch_backoff "$fails")"
|
|
208
|
+
continue
|
|
209
|
+
fi
|
|
210
|
+
fails=0
|
|
211
|
+
|
|
212
|
+
if watch_wake_review "$pr_number" "$latest_review" \
|
|
213
|
+
"$(( watermark_review > floor_review ? watermark_review : floor_review ))"; then
|
|
214
|
+
floor_review="$latest_review"
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
if watch_wake_comment "$pr_number" "$latest_comment" \
|
|
218
|
+
"$(( watermark_comment > floor_comment ? watermark_comment : floor_comment ))"; then
|
|
219
|
+
floor_comment="$latest_comment"
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
if [ -n "$unresolved_threads" ]; then
|
|
223
|
+
IFS=';' read -ra thread_ids <<< "$unresolved_threads"
|
|
224
|
+
for thread_id in "${thread_ids[@]}"; do
|
|
225
|
+
if watch_wake_thread "$pr_number" "$thread_id" "${watermark_threads};${floor_threads}"; then
|
|
226
|
+
floor_threads="${floor_threads}${thread_id};"
|
|
227
|
+
fi
|
|
228
|
+
done
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
if watch_wake_ci_red "$pr_number" "$pending_checks" "$failed_checks" "$head_sha" \
|
|
232
|
+
"${floor_ci_red:-$watermark_ci_red}"; then
|
|
233
|
+
floor_ci_red="$head_sha"
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
rebase_key="${head_sha}..${base_sha}"
|
|
237
|
+
if [ "$rebase_key" != "$watermark_rebase" ] && [ "$rebase_key" != "$floor_rebase" ]; then
|
|
238
|
+
behind_count=$(fetch_behind_count "$head_sha")
|
|
239
|
+
if watch_wake_rebase "$pr_number" "$mergeable" "$behind_count" "$rebase_key" ""; then
|
|
240
|
+
floor_rebase="$rebase_key"
|
|
241
|
+
fi
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# The tick owns clearing the block, and it may not run for a while. Holding
|
|
245
|
+
# the emitted digest stops the loop repeating itself every iteration in the
|
|
246
|
+
# meantime — the repeat nagging the design rules out — while a further move
|
|
247
|
+
# still wakes it.
|
|
248
|
+
if [ "$ci_digest" != "$floor_blocked_digest" ] \
|
|
249
|
+
&& watch_wake_blocked_resume "$pr_number" "$ci_digest" "$watermark_blocked_digest"; then
|
|
250
|
+
floor_blocked_digest="$ci_digest"
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
sleep "$MUGGLE_PR_WATCH_POLL_INTERVAL"
|
|
254
|
+
done
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
|
-
# muggle-pr-followup watchers are session-
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
5
|
+
# muggle-pr-followup watchers are session-bound — they die on session end and on
|
|
6
|
+
# the 7-day /loop expiry, leaving open PRs with no live poller. Re-arming needs
|
|
7
|
+
# Claude tools a shell hook can't call, so this hook nudges rather than acts.
|
|
8
|
+
#
|
|
9
|
+
# The nudge is owner-scoped. Each slot records the session that armed it in
|
|
10
|
+
# owner.json; only slots owned by THIS session are a recovery the agent should
|
|
11
|
+
# run, because re-arming a stranger's PR would hand review work to a session with
|
|
12
|
+
# no context for it. Slots owned by another session (or none) are reported as
|
|
13
|
+
# orphans with the adopt command, never as work to pick up. A pure directory scan
|
|
14
|
+
# — no gh, no writes — so it stays cheap enough for every session start.
|
|
12
15
|
|
|
13
16
|
escape_for_json() {
|
|
14
17
|
local s="$1"
|
|
@@ -20,31 +23,76 @@ escape_for_json() {
|
|
|
20
23
|
printf '%s' "$s"
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
26
|
+
# Extract the first "session_id": "..." from a JSON file or string. Deliberately
|
|
27
|
+
# sed rather than jq: jq is not guaranteed present, and on Windows it appends CRLF
|
|
28
|
+
# that makes an empty field compare non-empty.
|
|
29
|
+
read_session_id() {
|
|
30
|
+
sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1" 2>/dev/null | head -1 | tr -d '\r'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
hook_payload=$(cat 2>/dev/null || true)
|
|
34
|
+
current_session_id=$(printf '%s' "$hook_payload" | read_session_id /dev/stdin)
|
|
35
|
+
|
|
36
|
+
# A slot is an open watcher iff it has prs.json (a tracked PR) and no result.md
|
|
37
|
+
# (not yet finalized). A *.stopped dir is the owner's kill switch — it keeps its
|
|
38
|
+
# prs.json, so the name check is the only thing that distinguishes a slot the
|
|
39
|
+
# owner deliberately killed from one awaiting recovery. Counting those reports
|
|
40
|
+
# stopped watchers back as work.
|
|
25
41
|
sessions_dir="${HOME}/.muggle-ai/muggle-do/sessions"
|
|
26
|
-
|
|
42
|
+
owned_count=0
|
|
43
|
+
orphan_count=0
|
|
44
|
+
first_orphan_slug=""
|
|
45
|
+
|
|
27
46
|
if [ -d "$sessions_dir" ]; then
|
|
28
47
|
for slot in "$sessions_dir"/*/; do
|
|
29
48
|
[ -d "$slot" ] || continue
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
case "$slot" in *.stopped/) continue ;; esac
|
|
50
|
+
[ -f "${slot}prs.json" ] || continue
|
|
51
|
+
[ ! -f "${slot}result.md" ] || continue
|
|
52
|
+
|
|
53
|
+
slot_owner=""
|
|
54
|
+
if [ -f "${slot}owner.json" ]; then
|
|
55
|
+
slot_owner=$(read_session_id "${slot}owner.json")
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Fail closed: an unidentifiable session owns nothing. Absent owner.json
|
|
59
|
+
# (legacy slot) is likewise foreign — never adopted by default.
|
|
60
|
+
if [ -n "$current_session_id" ] && [ "$slot_owner" = "$current_session_id" ]; then
|
|
61
|
+
owned_count=$((owned_count + 1))
|
|
62
|
+
else
|
|
63
|
+
orphan_count=$((orphan_count + 1))
|
|
64
|
+
if [ -z "$first_orphan_slug" ]; then
|
|
65
|
+
first_orphan_slug=$(basename "$slot")
|
|
66
|
+
fi
|
|
32
67
|
fi
|
|
33
68
|
done
|
|
34
69
|
fi
|
|
35
70
|
|
|
36
|
-
# Clean state:
|
|
37
|
-
if [ "$
|
|
71
|
+
# Clean state: nothing of this session's to recover and no orphans → stay silent.
|
|
72
|
+
if [ "$owned_count" -eq 0 ] && [ "$orphan_count" -eq 0 ]; then
|
|
38
73
|
exit 0
|
|
39
74
|
fi
|
|
40
75
|
|
|
41
|
-
if [ "$
|
|
42
|
-
|
|
76
|
+
if [ "$owned_count" -eq 1 ]; then
|
|
77
|
+
owned_word="watcher"
|
|
43
78
|
else
|
|
44
|
-
|
|
79
|
+
owned_word="watchers"
|
|
45
80
|
fi
|
|
46
81
|
|
|
47
|
-
|
|
82
|
+
if [ "$owned_count" -gt 0 ]; then
|
|
83
|
+
context="muggle-pr-followup: ${owned_count} open ${owned_word} owned by this session may have lost its poller. Run \`/muggle:muggle-pr-followup reconcile\` to finalize any whose PR went terminal and re-arm the silently-stopped ones. Reconcile is idempotent and re-arms only watchers this session armed itself."
|
|
84
|
+
else
|
|
85
|
+
context="muggle-pr-followup: no watchers belong to this session — nothing to re-arm."
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
if [ "$orphan_count" -gt 0 ]; then
|
|
89
|
+
if [ "$orphan_count" -eq 1 ]; then
|
|
90
|
+
orphan_word="slot is"
|
|
91
|
+
else
|
|
92
|
+
orphan_word="slots are"
|
|
93
|
+
fi
|
|
94
|
+
context="${context} ${orphan_count} open ${orphan_word} owned by other sessions; reconcile will finalize them if their PR already merged or closed, but will never re-arm them. Adopt one deliberately with \`/muggle:muggle-pr-followup adopt <slug>\` (e.g. ${first_orphan_slug}) only if the user asks — do not offer or adopt on your own."
|
|
95
|
+
fi
|
|
48
96
|
|
|
49
97
|
escaped_context=$(escape_for_json "$context")
|
|
50
98
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Appends the Muggle Works signature to a body about to be posted to a PR/MR.
|
|
4
|
+
#
|
|
5
|
+
# Reads the unsigned body on stdin, writes the signed body on stdout:
|
|
6
|
+
#
|
|
7
|
+
# sign-body.sh --command /muggle-do --mode loop < draft.md
|
|
8
|
+
#
|
|
9
|
+
# --command slash-command of the skill whose post this is (owner of the post,
|
|
10
|
+
# not the producer of the content).
|
|
11
|
+
# --mode loop thread replies and resolve-reminders: prefixes the
|
|
12
|
+
# <!-- muggle-do:bot --> echo-detection marker.
|
|
13
|
+
# editable PR/MR descriptions: prefixes the dedup marker, and cuts
|
|
14
|
+
# any previous signature so refreshes never stack.
|
|
15
|
+
# plain one-shot comments with no marker.
|
|
16
|
+
#
|
|
17
|
+
# Idempotent: an already-signed body is re-signed, not double-signed.
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
readonly REPOSITORY_URL='https://github.com/multiplex-ai/muggle-ai-works'
|
|
22
|
+
readonly SIGNATURE_PREFIX='🤖 _Posted by '
|
|
23
|
+
readonly LOOP_MARKER='<!-- muggle-do:bot -->'
|
|
24
|
+
readonly EDITABLE_MARKER='<!-- muggle-works:signature -->'
|
|
25
|
+
|
|
26
|
+
usage() {
|
|
27
|
+
echo "usage: sign-body.sh --command <slash-command> --mode <loop|editable|plain>" >&2
|
|
28
|
+
exit 2
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
postedCommand=''
|
|
32
|
+
signatureMode=''
|
|
33
|
+
|
|
34
|
+
while [ $# -gt 0 ]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--command) postedCommand="${2-}"; shift 2 ;;
|
|
37
|
+
--mode) signatureMode="${2-}"; shift 2 ;;
|
|
38
|
+
-h|--help) usage ;;
|
|
39
|
+
*) echo "sign-body.sh: unknown argument '$1'" >&2; usage ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
[ -n "$postedCommand" ] || usage
|
|
44
|
+
case "$signatureMode" in
|
|
45
|
+
loop|editable|plain) ;;
|
|
46
|
+
*) usage ;;
|
|
47
|
+
esac
|
|
48
|
+
|
|
49
|
+
# Cuts any signature the body already carries, so re-signing an edited body
|
|
50
|
+
# replaces it instead of stacking a second one underneath.
|
|
51
|
+
stripExistingSignature() {
|
|
52
|
+
awk -v editableMarker="$EDITABLE_MARKER" \
|
|
53
|
+
-v loopMarker="$LOOP_MARKER" \
|
|
54
|
+
-v signaturePrefix="$SIGNATURE_PREFIX" '
|
|
55
|
+
{ lines[NR] = $0 }
|
|
56
|
+
END {
|
|
57
|
+
cut = NR + 1
|
|
58
|
+
for (i = 1; i <= NR; i++) {
|
|
59
|
+
if (lines[i] == editableMarker) { cut = i; break }
|
|
60
|
+
}
|
|
61
|
+
if (cut == NR + 1) {
|
|
62
|
+
for (i = NR; i >= 1; i--) {
|
|
63
|
+
if (lines[i] == "") continue
|
|
64
|
+
if (index(lines[i], signaturePrefix) == 1) {
|
|
65
|
+
cut = i
|
|
66
|
+
if (i > 1 && (lines[i - 1] == loopMarker || lines[i - 1] == editableMarker)) cut = i - 1
|
|
67
|
+
}
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
last = cut - 1
|
|
72
|
+
while (last >= 1 && lines[last] == "") last--
|
|
73
|
+
for (i = 1; i <= last; i++) print lines[i]
|
|
74
|
+
}
|
|
75
|
+
'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
body="$(stripExistingSignature)"
|
|
79
|
+
|
|
80
|
+
case "$signatureMode" in
|
|
81
|
+
loop) marker="$LOOP_MARKER" ;;
|
|
82
|
+
editable) marker="$EDITABLE_MARKER" ;;
|
|
83
|
+
plain) marker='' ;;
|
|
84
|
+
esac
|
|
85
|
+
|
|
86
|
+
printf '%s\n\n' "$body"
|
|
87
|
+
[ -z "$marker" ] || printf '%s\n' "$marker"
|
|
88
|
+
printf '%s`%s` · [Muggle Works](%s)_\n' "$SIGNATURE_PREFIX" "$postedCommand" "$REPOSITORY_URL"
|
|
@@ -20,6 +20,22 @@ When you feel the urge to link "up" to a caller, that is the smell — restructu
|
|
|
20
20
|
|
|
21
21
|
`plugin/skills/skill-deps.config.json` declares support dirs grouped into their owning skill (`do/` → `muggle-do`), shared namespaces exploded to per-file nodes (`_shared`), and `knownReverseDeps` — pre-existing violations grandfathered so CI stays green. That list is debt: fix each link and delete its entry. A new reverse dependency is blocked whether or not it is on the list.
|
|
22
22
|
|
|
23
|
+
## Mandatory stages
|
|
24
|
+
|
|
25
|
+
A `SKILL.md` links out to the files that carry its steps, and nothing forces those links open. Read as a single page, a skill silently loses whatever its stage files required — the step is improvised instead of skipped visibly.
|
|
26
|
+
|
|
27
|
+
Declare the files a skill cannot be run without in its frontmatter, paths relative to the skill directory:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
mandatoryStages:
|
|
31
|
+
- ../_shared/failure-mode-handling.md
|
|
32
|
+
- ../_shared/debug-failed-run.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Invoking the skill names those files as required reading, and a Stop gate holds the turn open until each is opened (`guardrail-skill-stages.sh` / `guardrail-stage-gate.sh`, see [`../hooks/README.md`](../hooks/README.md)). The declaration is explicit rather than inferred from the prose: a gate guessing which links matter blocks on the wrong ones.
|
|
36
|
+
|
|
37
|
+
The bar is "must be **read** before the skill runs", not "must be executed". A failure protocol qualifies: by the time a run fails, a skill that never read it is already improvising. What does not qualify is a file only one branch consults for its content — the gate cannot tell that the branch was not taken, so it would block work that legitimately never needed it. Aliases declare nothing; they invoke the canonical skill, which carries the declaration.
|
|
38
|
+
|
|
23
39
|
## Model tiers
|
|
24
40
|
|
|
25
41
|
Each skill sets a `model:` in its `SKILL.md` frontmatter sized to its cognitive load. `model:` is a native Claude Code field — the override applies while the skill is active and reverts to the session model when it exits. Cheaper, faster models run the mechanical skills; the default (Opus) is reserved for the ones that actually reason. Cost and latency scale with the model, and these skills run often (the watcher fires every minute), so the tier is a real lever, not cosmetics.
|