@muggleai/works 5.10.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/dist/{chunk-OCLMXAVJ.js → chunk-MSPMXBO5.js} +418 -326
- package/dist/{chunk-7NC2DJTK.js → chunk-WUMAJOBT.js} +9 -4
- 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/agents/test-prepare-runner.md +3 -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/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 +428 -35
- 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/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-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/preference-gates/watcherLifetime.md +23 -0
- package/dist/plugin/skills/muggle-test/SKILL.md +22 -2
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +3 -0
- package/dist/plugin/skills/muggle-test-prepare/SKILL.md +19 -12
- 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 +13 -2
- package/dist/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
- package/dist/plugin/skills/muggle-test-prepare/steps/readiness-report.md +3 -1
- 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 +4 -2
- 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/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/agents/test-prepare-runner.md +3 -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/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 +428 -35
- 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/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-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/preference-gates/watcherLifetime.md +23 -0
- package/plugin/skills/muggle-test/SKILL.md +22 -2
- package/plugin/skills/muggle-test-feature-local/SKILL.md +3 -0
- package/plugin/skills/muggle-test-prepare/SKILL.md +19 -12
- 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 +13 -2
- package/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
- package/plugin/skills/muggle-test-prepare/steps/readiness-report.md +3 -1
- 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 +4 -2
- 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/dist/src-AWUORQZZ.js +0 -1
|
@@ -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.
|
|
@@ -4,6 +4,86 @@ Gated by [`autoCleanup`](../muggle-preferences/preference-gates/autoCleanup.md).
|
|
|
4
4
|
|
|
5
5
|
On `always`, the steps below run as one pre-authorized sequence (no per-step prompts). Stop on the first failure; do not force.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
**Verify every step, assume none.** Each step states what proves it succeeded. A step whose side effect *usually* happens is not a step that ran — that assumption is how a branch survives a cleanup that reported success. The [report](#report) states verified state, never intent.
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
|
|
11
|
+
Confirm the PR is `MERGED` from provider state. A closed-unmerged PR keeps its branch and worktree: the work never landed, so deleting it destroys it.
|
|
12
|
+
|
|
13
|
+
## 1. Remove the worktree — link-safe
|
|
14
|
+
|
|
15
|
+
Only if a worktree was used.
|
|
16
|
+
|
|
17
|
+
A worktree's dependency dir (`node_modules`, and nested workspace copies) is often a **link** — a symlink or a Windows junction — to a shared tree rather than a real copy. A forced or recursive delete follows the link and wipes that shared target, breaking every other worktree.
|
|
18
|
+
|
|
19
|
+
1. **Never `--force`.** It is the one flag that follows links.
|
|
20
|
+
2. Clear the dependency and build dirs first, nested workspace ones included (`packages/*/node_modules`, `dist`). A plain `git worktree remove` fails with `Directory not empty` while they remain, and the obvious fix for that error is exactly the forbidden flag.
|
|
21
|
+
- A **link** → unlink it first with the host OS's unlink, removing the link only and never its target.
|
|
22
|
+
- A **real directory** → delete it in place.
|
|
23
|
+
Check which it is before deleting; the two are indistinguishable from a listing but not from a `rm -r`.
|
|
24
|
+
3. Then plain `git worktree remove {worktreePath}`.
|
|
25
|
+
4. `git worktree prune` to drop the administrative entry when the directory went away out from under git.
|
|
26
|
+
|
|
27
|
+
**Verify:** the path is gone, it no longer appears in `git worktree list`, **and** the shared dependency tree the links pointed at still exists. That last check is the one that catches a link-follow.
|
|
28
|
+
|
|
29
|
+
## 2. Delete the local branch
|
|
30
|
+
|
|
31
|
+
**Skip entirely when no worktree was used** — the branch is then the user's live checkout, and a checked-out branch must never be deleted.
|
|
32
|
+
|
|
33
|
+
`git branch -d` refuses after a **squash merge**, and always will: squashing mints a new commit carrying the same tree, so the branch tip is never an ancestor of the base. This is not a safety check that failed; it is one that cannot pass.
|
|
34
|
+
|
|
35
|
+
Do not reach for `-D` on faith. Replace the ancestry check with a content check:
|
|
36
|
+
|
|
37
|
+
1. Confirm the merged content is on the base — files the PR added exist at `origin/<base>`, and anything it removed is absent there.
|
|
38
|
+
2. Only then `git branch -D {branch}`.
|
|
39
|
+
|
|
40
|
+
**Verify:** the branch is absent from `git branch --list`.
|
|
41
|
+
|
|
42
|
+
## 3. Delete the remote branch
|
|
43
|
+
|
|
44
|
+
**A provider that auto-deletes the head branch on merge is a setting, not a guarantee.** It can be off for the repo, off for a fork, or simply not fire. Treat auto-delete as something to detect, never as this step having run.
|
|
45
|
+
|
|
46
|
+
1. Query the ref. Already gone → record it deleted and move on.
|
|
47
|
+
2. Still present → delete it explicitly.
|
|
48
|
+
|
|
49
|
+
**Verify:** querying the ref returns not-found.
|
|
50
|
+
|
|
51
|
+
## 4. Clear the session slot
|
|
52
|
+
|
|
53
|
+
The slot is `~/.muggle-ai/muggle-do/sessions/<slug>/` — the home directory, not the project.
|
|
54
|
+
|
|
55
|
+
Clear it only when `prs.json` records a terminal state. A slot for a still-open PR is live state that a watcher and [`reconcile`](../muggle-pr-followup/reconcile.md) both read. The merged PR is the durable record, so the slot's `result.md` is not lost history.
|
|
56
|
+
|
|
57
|
+
**Verify:** the slot directory is gone, and no non-terminal slot was touched.
|
|
58
|
+
|
|
59
|
+
## 5. Clear this run's prepare artifacts
|
|
60
|
+
|
|
61
|
+
The PID tracker and per-service logs written during environment prep — `/tmp/muggle-test-prepare.json` and `/tmp/muggle-prepare-*.log`.
|
|
62
|
+
|
|
63
|
+
**Only this run's.** Artifacts belonging to another session are not yours to delete; it may be mid-prepare with services running. When the tracker is absent no services are tracked, so any remaining logs are orphaned — report them rather than removing them.
|
|
64
|
+
|
|
65
|
+
Cloud results always stay.
|
|
66
|
+
|
|
67
|
+
**Verify:** this run's artifacts are gone; anything left is named in the report as out of scope.
|
|
68
|
+
|
|
69
|
+
## Report
|
|
70
|
+
|
|
71
|
+
Print as the last action, one row per step, filled from the verification checks — never from the fact that a command was issued:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
Cleanup — <slug> (PR #<n>, merged)
|
|
75
|
+
|
|
76
|
+
| Step | Result |
|
|
77
|
+
|:----------------------|:------------------------------------------------|
|
|
78
|
+
| Worktree removed | ✅ .claude/worktrees/<name> (shared deps intact) |
|
|
79
|
+
| Local branch deleted | ✅ users/<user>/<branch> |
|
|
80
|
+
| Remote branch deleted | ✅ (already gone — provider auto-delete) |
|
|
81
|
+
| Session slot cleared | ✅ <slug> |
|
|
82
|
+
| Prepare artifacts | — none for this run |
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Markers: `✅` verified done · `⚠️` done with a caveat worth reading · `❌` not done, with the reason · `—` nothing to do.
|
|
86
|
+
|
|
87
|
+
Every step gets a row. A step that did not run is `❌` plus its reason — never omitted, and never `✅` when its verification did not actually run. A silent gap is worse than a visible one, because it is the version the user ends up believing.
|
|
88
|
+
|
|
89
|
+
State anything deliberately left alone — another session's artifacts, a foreign worktree, an orphaned watcher — on a line below the table, so out-of-scope is declared rather than invisible.
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
# Loop comment signature
|
|
2
2
|
|
|
3
|
-
Every
|
|
3
|
+
Every comment `/muggle-do` posts on either provider — inline thread replies (`gitlab`: discussion notes), top-level reference comments, resolve-reminders — **must** carry the loop marker. It is the only reliable way to tell loop-authored comments from human comments: in single-account workflows the loop posts under the change author's own identity, so the author login cannot distinguish them. Echo-protection and addressed-by-loop classification both depend on this marker.
|
|
4
4
|
|
|
5
|
-
## The
|
|
6
|
-
|
|
7
|
-
Append these two lines as the end of every loop-posted comment body:
|
|
5
|
+
## The marker
|
|
8
6
|
|
|
9
7
|
```
|
|
10
8
|
<!-- muggle-do:bot -->
|
|
11
|
-
🤖 _Posted by `/muggle-do` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_
|
|
12
9
|
```
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
Hidden HTML: both providers render it invisibly and humans never type it. It must stay exactly as written — echo-protection and addressed-by-loop classification read this literal string.
|
|
12
|
+
|
|
13
|
+
Signing a body with `--mode loop` ([`../vcs/post-signature.md`](../vcs/post-signature.md)) emits the marker above the visible Muggle Works line. Never hand-write either one; a body assembled by hand is the one that silently ships without them.
|
|
16
14
|
|
|
17
15
|
## Detection
|
|
18
16
|
|
|
19
17
|
- **Loop-authored** — the comment body contains the literal `<!-- muggle-do:bot -->`.
|
|
20
18
|
- **Human** — the body does not contain the marker.
|
|
21
19
|
|
|
22
|
-
Classify by the marker, never by `author.login`
|
|
20
|
+
Classify by the marker, never by the author login alone (`github`: `author.login`, `gitlab`: `author.username`) — the login is ambiguous under a shared account. A comment that carries the marker is the loop's own and must never re-trigger a cycle; a comment without it is human intent to act on.
|