@mmerterden/multi-agent-pipeline 20.3.0 → 20.4.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/CHANGELOG.md +20 -0
- package/docs/ecosystem.md +2 -2
- package/docs/facts.json +1 -1
- package/install/claude.mjs +35 -0
- package/manifest.json +42 -40
- package/package.json +1 -1
- package/pipeline/commands/figma-to-swiftui.md +6 -4
- package/pipeline/commands/multi-agent/jira/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/stack/SKILL.md +2 -4
- package/pipeline/commands/multi-agent/sync/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/update/SKILL.md +3 -10
- package/pipeline/lib/usage-endpoint.mjs +33 -0
- package/pipeline/multi-agent-refs/channels/pr.md +2 -2
- package/pipeline/multi-agent-refs/features/doctor.md +2 -1
- package/pipeline/multi-agent-refs/features/prior-fix-detection.md +1 -1
- package/pipeline/multi-agent-refs/generate-issue.md +9 -9
- package/pipeline/multi-agent-refs/outside-the-pipeline.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +2 -1
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +4 -4
- package/pipeline/multi-agent-refs/setup/repo-discovery.md +2 -2
- package/pipeline/schemas/prefs.schema.json +1 -1
- package/pipeline/scripts/capture-resume.sh +51 -37
- package/pipeline/scripts/check-derived-drift.mjs +1 -1
- package/pipeline/scripts/doctor.mjs +21 -0
- package/pipeline/scripts/plugin-refresh.sh +92 -0
- package/pipeline/scripts/usage-register.mjs +6 -6
- package/pipeline/scripts/usage-report.mjs +6 -5
- package/pipeline/skills/.skill-manifest.json +7 -7
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -4
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-jira/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +2 -4
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +1 -1
|
@@ -251,7 +251,7 @@ The two adapters run in the same phase over the same source markdown, so the fai
|
|
|
251
251
|
## Reviewer-preserving Bitbucket payload (required)
|
|
252
252
|
|
|
253
253
|
```bash
|
|
254
|
-
PR_JSON=$(curl -s
|
|
254
|
+
PR_JSON=$(curl -s --config <(printf 'user = "%s:%s"\n' "$BB_USER" "$BB_TOKEN") "$PR_URL")
|
|
255
255
|
REVIEWERS=$(jq '[.reviewers[] | {user: {name: .user.name}, approved, status}]' <<< "$PR_JSON")
|
|
256
256
|
VERSION=$(jq -r '.version' <<< "$PR_JSON")
|
|
257
257
|
TITLE=$(jq -r '.title' <<< "$PR_JSON")
|
|
@@ -268,7 +268,7 @@ jq -n --rawfile body /tmp/channels-$TASK_ID-pr.md \
|
|
|
268
268
|
reviewers: $reviewers, fromRef: $fromRef, toRef: $toRef, draft: $draft}' \
|
|
269
269
|
> /tmp/channels-$TASK_ID-pr-payload.json
|
|
270
270
|
|
|
271
|
-
curl -s -X PUT
|
|
271
|
+
curl -s -X PUT --config <(printf 'user = "%s:%s"\n' "$BB_USER" "$BB_TOKEN") -H "Content-Type: application/json" \
|
|
272
272
|
--data-binary @/tmp/channels-$TASK_ID-pr-payload.json "$PR_URL"
|
|
273
273
|
```
|
|
274
274
|
|
|
@@ -167,7 +167,8 @@ reaches Phase 4 and stops there.
|
|
|
167
167
|
### hook-coverage
|
|
168
168
|
|
|
169
169
|
The blocking `PreToolUse` gates from `templates/claude-hooks.json` are present in
|
|
170
|
-
`settings.json
|
|
170
|
+
`settings.json`, and none of them is registered twice on the same matcher
|
|
171
|
+
(Claude Code runs a duplicate on every matching call). WARN. SKIP when the template is not installed - which was the
|
|
171
172
|
permanent state until the installer began copying `templates/`, and is exactly
|
|
172
173
|
the case this severity exists to make visible.
|
|
173
174
|
|
|
@@ -19,7 +19,7 @@ Before any analysis, check if this issue was already fixed by someone else.
|
|
|
19
19
|
3. **Jira comments/status** - fetch latest comments to see if someone reported a fix:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
22
|
+
printf 'Authorization: Bearer %s\n' "$JIRA_TOKEN" | curl -s -H @- \
|
|
23
23
|
"$JIRA_BASE/issue/{jiraId}?fields=status,comment&expand=changelog" \
|
|
24
24
|
| jq '.fields.status.name, .fields.comment.comments[-3:][]?.body'
|
|
25
25
|
```
|
|
@@ -79,7 +79,7 @@ Fallback chain (same as `$HOME/.claude/multi-agent-refs/issue-jira-triad.md` Cre
|
|
|
79
79
|
2. `prefs.global.defaultJiraKey`
|
|
80
80
|
3. Fetch and ask:
|
|
81
81
|
```bash
|
|
82
|
-
|
|
82
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
83
83
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/project" \
|
|
84
84
|
| python3 -c 'import json,sys; [print(p["key"]+"\t"+p["name"]) for p in json.load(sys.stdin)]'
|
|
85
85
|
```
|
|
@@ -108,7 +108,7 @@ Three GET groups. Run them before drafting anything.
|
|
|
108
108
|
**5a. Sample recent same-type issues:**
|
|
109
109
|
```bash
|
|
110
110
|
JQL="project = ${PROJECT_KEY} AND issuetype = ${ISSUE_TYPE} ORDER BY created DESC"
|
|
111
|
-
|
|
111
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
112
112
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/search?jql=$(printf '%s' "$JQL" | python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read()))')&fields=summary,description,labels,components,priority,fixVersions,issuelinks&maxResults=30"
|
|
113
113
|
```
|
|
114
114
|
Extract from the sample (thresholds are guidance, not hard gates):
|
|
@@ -123,7 +123,7 @@ Extract from the sample (thresholds are guidance, not hard gates):
|
|
|
123
123
|
|
|
124
124
|
**5b. Field discovery (createmeta):**
|
|
125
125
|
```bash
|
|
126
|
-
|
|
126
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
127
127
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/issue/createmeta?projectKeys=${PROJECT_KEY}&issuetypeNames=${ISSUE_TYPE}&expand=projects.issuetypes.fields"
|
|
128
128
|
```
|
|
129
129
|
- 404 or empty `projects[]` (newer Jira DC deprecates the classic form) → fall back to the paged endpoint: `GET /rest/api/2/issue/createmeta/{PROJECT_KEY}/issuetypes` then `GET /rest/api/2/issue/createmeta/{PROJECT_KEY}/issuetypes/{issuetypeId}`.
|
|
@@ -136,9 +136,9 @@ curl -s -H "Authorization: Bearer $TOKEN" \
|
|
|
136
136
|
|
|
137
137
|
**5c. Board + active sprint (Agile REST):**
|
|
138
138
|
```bash
|
|
139
|
-
|
|
139
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
140
140
|
"https://$ACCOUNT_JIRA_HOST/rest/agile/1.0/board?projectKeyOrId=${PROJECT_KEY}"
|
|
141
|
-
|
|
141
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
142
142
|
"https://$ACCOUNT_JIRA_HOST/rest/agile/1.0/board/${BOARD_ID}/sprint?state=active"
|
|
143
143
|
```
|
|
144
144
|
- Multiple boards → AskUserQuestion (board name + type). One board → silent. Zero boards or Agile API 404 → the sprint option is silently omitted; the preview notes "backlog only (no board found)".
|
|
@@ -217,7 +217,7 @@ jq -n --rawfile desc /tmp/generate-issue-$$-escaped.txt \
|
|
|
217
217
|
'{fields: {project: {key: $key}, issuetype: {name: $type}, summary: $summary, description: $desc}}' \
|
|
218
218
|
> /tmp/generate-issue-$$-payload.json
|
|
219
219
|
# merge approved optional fields (priority / labels / components / epic / assignee / required customs) into .fields with jq before POST
|
|
220
|
-
curl -sS -X POST -H
|
|
220
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -sS -X POST -H @- -H "Content-Type: application/json" \
|
|
221
221
|
--data-binary @/tmp/generate-issue-$$-payload.json \
|
|
222
222
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/issue"
|
|
223
223
|
```
|
|
@@ -231,20 +231,20 @@ Each step is failure-isolated: on error, warn + continue - the issue already e
|
|
|
231
231
|
1. **Sprint** (when chosen in step 8/9):
|
|
232
232
|
```bash
|
|
233
233
|
jq -n --arg key "$NEW_KEY" '{issues: [$key]}' > /tmp/generate-issue-$$-sprint.json
|
|
234
|
-
curl -sS -X POST -H
|
|
234
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -sS -X POST -H @- -H "Content-Type: application/json" \
|
|
235
235
|
--data-binary @/tmp/generate-issue-$$-sprint.json \
|
|
236
236
|
"https://$ACCOUNT_JIRA_HOST/rest/agile/1.0/sprint/${SPRINT_ID}/issue"
|
|
237
237
|
```
|
|
238
238
|
Post-create move instead of a Sprint custom field in the create payload: field-id-agnostic, works on every Jira Software instance.
|
|
239
239
|
2. **Figma remote link** (when `FIGMA_URL` set):
|
|
240
240
|
```bash
|
|
241
|
-
curl -sS -X POST -H
|
|
241
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -sS -X POST -H @- -H "Content-Type: application/json" \
|
|
242
242
|
-d '{"object":{"url":"'"$FIGMA_URL"'","title":"Figma design"}}' \
|
|
243
243
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/issue/${NEW_KEY}/remotelink"
|
|
244
244
|
```
|
|
245
245
|
3. **Swagger remote link** (when `SWAGGER_URL` set):
|
|
246
246
|
```bash
|
|
247
|
-
curl -sS -X POST -H
|
|
247
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -sS -X POST -H @- -H "Content-Type: application/json" \
|
|
248
248
|
-d '{"object":{"url":"'"$SWAGGER_URL"'","title":"API contract"}}' \
|
|
249
249
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/issue/${NEW_KEY}/remotelink"
|
|
250
250
|
```
|
|
@@ -24,10 +24,10 @@ mapping would have given you.
|
|
|
24
24
|
|
|
25
25
|
## Keeping the value out of everything
|
|
26
26
|
|
|
27
|
-
`curl`
|
|
27
|
+
The header goes to `curl` on stdin, so the token never reaches argv (where `ps` can read it):
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
|
|
30
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -H @- \
|
|
31
31
|
"https://$(jq -r '.global.hosts.jira' "$PREFS")/rest/api/3/issue/PROJ-1"
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -234,7 +234,7 @@ Classify and fetch external data:
|
|
|
234
234
|
[ -z "$JIRA_KEY" ] && JIRA_KEY="${USER}_Jira_Access_Token"
|
|
235
235
|
# unattended: skipped - a run reads no credential itself (features/unattended-security.md)
|
|
236
236
|
JIRA_TOKEN=$(~/.claude/lib/credential-store.sh get "$JIRA_KEY" 2>/dev/null)
|
|
237
|
-
|
|
237
|
+
printf 'Authorization: Bearer %s\n' "$JIRA_TOKEN" | curl -s -H @- \
|
|
238
238
|
"https://{JIRA_HOST}/rest/api/2/issue/{jiraId}?fields=summary,issuetype,status"
|
|
239
239
|
```
|
|
240
240
|
2. Extract: summary, issueType (Bug/Story/Task/Feature)
|
|
@@ -177,8 +177,9 @@ For each task (respecting dependency order):
|
|
|
177
177
|
6. **Intermediate commit** (after each completed task in the plan):
|
|
178
178
|
```bash
|
|
179
179
|
git -C "{worktreePath}" add -A
|
|
180
|
-
git -C "{worktreePath}" commit -
|
|
180
|
+
git -C "{worktreePath}" commit -F "/tmp/wip-message-$TASK_ID.txt"
|
|
181
181
|
```
|
|
182
|
+
Write `wip({scope}): {task summary} [{jiraId}]` to `/tmp/wip-message-$TASK_ID.txt` with the file tool first. The summary comes from the issue, and inside a double-quoted `-m` the shell would run any `$(...)` in it.
|
|
182
183
|
WIP commits are squashed in Phase 4 before PR. This prevents work loss if a later task fails or the session crashes.
|
|
183
184
|
7. Update task status: `completed` (apply the same verify rule - re-read the Status field and retry once on mismatch)
|
|
184
185
|
8. Log: "Phase 2: {task} - Test + Build passed"
|
|
@@ -127,7 +127,7 @@ Branch **deterministically**, no implicit fallback. Read `agent-state.json` and
|
|
|
127
127
|
|
|
128
128
|
The script salvages before removing, keeps the branch, and does **not** check it out - the user's HEAD and uncommitted work stay put. On success it stamps `worktreeRemovedAt`, `artifactsPath` and `worktreePath: null` into the **salvaged** `agent-state.json` itself. Do NOT write those via `write-state.mjs "$STATE_FILE"`: in single-repo mode that path went away with the worktree, so the write fails, the fields land nowhere, and Phase 5 reads a dead path and silently skips the triage ingest. After a removal re-point `STATE_FILE` at `$(jq -r .artifactsPath <<< "$FIN")/agent-state.json`. Exit 3 is a safe skip (uncommitted changes, unpushed or detached HEAD, `--local`, cwd inside the tree, preference off): report and continue. Never `--force`. Contract: [`features/worktree-finalize.md`]($HOME/.claude/multi-agent-refs/features/worktree-finalize.md).
|
|
129
129
|
|
|
130
|
-
10. **Issue body update** (GitHub Issue only): if the issue body has a `### Pull Requests` section and/or a `### Progress` table, fill in the PR URL(s) (one row per submodule - e.g. `- **common:** {url}`, `- **uicomponents:** {url}`) and flip the Implementation / Testing / Code Connect flags from Pending to Done using whatever marker the template uses (match in place, do NOT introduce new markers). Apply with `gh issue edit {issueNo} --body
|
|
130
|
+
10. **Issue body update** (GitHub Issue only): if the issue body has a `### Pull Requests` section and/or a `### Progress` table, fill in the PR URL(s) (one row per submodule - e.g. `- **common:** {url}`, `- **uicomponents:** {url}`) and flip the Implementation / Testing / Code Connect flags from Pending to Done using whatever marker the template uses (match in place, do NOT introduce new markers). Apply it with `gh issue edit {issueNo} --body-file <file>`, preserving every other section unchanged.
|
|
131
131
|
11. **NEVER close or resolve the issue** - neither GitHub Issue nor Jira. Issues require team review (4 approvals) before closing. Only post a comment with commit/PR URLs.
|
|
132
132
|
12. Log: "Phase 4: Commit {sha} - PR #{number}, worktree {removed|kept: <reason>}"
|
|
133
133
|
|
|
@@ -174,13 +174,13 @@ Inputs are read from state and the worktree before writing, not recalled from th
|
|
|
174
174
|
|
|
175
175
|
**Default Reviewers**: use `prefs.projects[{project}].defaultReviewers` when set; otherwise ask once ("Default reviewers for this project? comma-separated, or skip") and persist the answer there. Per-remote resolution rules are in "Default reviewers (required on every PR)" below - this is the prefs half only.
|
|
176
176
|
|
|
177
|
-
**Standard PR (bug fix, feature, refactor)** - invocation shape
|
|
177
|
+
**Standard PR (bug fix, feature, refactor)** - invocation shape. Title (`{type}({scope}): {summary} [{jiraId}]`) and body go to files first, so issue text never sits in a quoted argument:
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
|
-
REVIEWERS=$(jq -r '.projects[
|
|
180
|
+
REVIEWERS=$(jq -r --arg p "$PROJECT" '.projects[$p].defaultReviewers // [] | map("--reviewer " + .) | join(" ")' "$PREFS_FILE")
|
|
181
181
|
# unattended: skipped - pr-request.mjs replaces the PR (features/unattended-security.md)
|
|
182
182
|
gh pr create --base {baseBranch} --head {branch} $REVIEWERS \
|
|
183
|
-
--title "
|
|
183
|
+
--title "$(cat /tmp/pr-title-$TASK_ID.txt)" \
|
|
184
184
|
--body-file /tmp/pr-body-$TASK_ID.md
|
|
185
185
|
```
|
|
186
186
|
|
|
@@ -39,7 +39,7 @@ BB_HOST="$(jq -r '.global.bitbucketHost // empty' "$PREFS_FILE")"
|
|
|
39
39
|
BB_USER="$(~/.claude/lib/credential-store.sh get "$(jq -r '.global.keychainMapping.bitbucket_user' "$PREFS_FILE")" 2>/dev/null)"
|
|
40
40
|
BB_TOK="$(~/.claude/lib/credential-store.sh get "$(jq -r '.global.keychainMapping.bitbucket_token' "$PREFS_FILE")" 2>/dev/null)"
|
|
41
41
|
CUTOFF_MS=$(( ( $(date +%s) - 7776000 ) * 1000 )) # 90 days
|
|
42
|
-
curl -sfS
|
|
42
|
+
curl -sfS --config <(printf 'user = "%s:%s"\n' "$BB_USER" "$BB_TOK") \
|
|
43
43
|
"https://${BB_HOST}/rest/api/1.0/repos?permission=REPO_WRITE&limit=200" \
|
|
44
44
|
| jq -r --argjson cutoff "$CUTOFF_MS" '
|
|
45
45
|
.values[] | select((.updatedDate // 0) >= $cutoff) |
|
|
@@ -59,7 +59,7 @@ CUTOFF_ISO="$(date -u -v-90d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '90 d
|
|
|
59
59
|
gh api --paginate 'user/repos?affiliation=owner,collaborator&sort=pushed&per_page=100' \
|
|
60
60
|
-q ".[] | select(.pushed_at > \"$CUTOFF_ISO\") | {full: .full_name, clone: .clone_url}"
|
|
61
61
|
# Fallback without gh:
|
|
62
|
-
|
|
62
|
+
printf 'Authorization: Bearer %s\n' "$GH_TOK" | curl -sfS -H @- \
|
|
63
63
|
'https://api.github.com/user/repos?affiliation=owner,collaborator&sort=pushed&per_page=100'
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -1820,7 +1820,7 @@
|
|
|
1820
1820
|
},
|
|
1821
1821
|
"upstreamLocalClone": {
|
|
1822
1822
|
"type": "string",
|
|
1823
|
-
"description": "Working copy of the upstream repository, checked before the plugin cache. The cache under ~/.claude/plugins/cache is only as fresh as the last `claude marketplace update`, so it is a mirror and never the authority. A private upstream may also be unreachable over the API from the active account, in which case this clone is the only source."
|
|
1823
|
+
"description": "Working copy of the upstream repository, checked before the plugin cache. The cache under ~/.claude/plugins/cache is only as fresh as the last `claude plugin marketplace update`, so it is a mirror and never the authority. A private upstream may also be unreachable over the API from the active account, in which case this clone is the only source."
|
|
1824
1824
|
},
|
|
1825
1825
|
"upstreamRepoUrl": {
|
|
1826
1826
|
"type": "string",
|
|
@@ -33,16 +33,9 @@ LOGS="${LOGS_ROOT:-$HOME/.claude/logs/multi-agent}"
|
|
|
33
33
|
PIPELINE_MEM="$HOME/.claude/memory/multi-agent/_pipeline"
|
|
34
34
|
REVIEW_DAYS=7
|
|
35
35
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# started. 3 had their PR already open and were waiting at Phase 4/7, where the
|
|
40
|
-
# pipeline pauses ON PURPOSE for channel selection. 4 died mid-development.
|
|
41
|
-
#
|
|
42
|
-
# Reporting the newest one of those as "stopped at Phase N" told the truth about
|
|
43
|
-
# one run and hid twenty-one, and it read the same whether the pipeline was
|
|
44
|
-
# waiting for the user or had crashed. A finished job and a dead one should not
|
|
45
|
-
# share a sentence.
|
|
36
|
+
# Unfinished runs are sorted into three buckets because each needs a different
|
|
37
|
+
# action: waiting on the user (an open PR, or a pause for the channel answer),
|
|
38
|
+
# left at a Phase 0 question (nothing built), and stopped mid-development.
|
|
46
39
|
AWAITING_N=0; AWAITING_TASK=""; AWAITING_PHASE=""
|
|
47
40
|
DIED_N=0; DIED_TASK=""; DIED_PHASE=""
|
|
48
41
|
UNSTARTED_N=0; UNSTARTED_TASK=""
|
|
@@ -50,34 +43,35 @@ STALE_TASK=""
|
|
|
50
43
|
STALE_PHASE=""
|
|
51
44
|
|
|
52
45
|
if [ -d "$LOGS" ] && command -v jq >/dev/null 2>&1; then
|
|
53
|
-
#
|
|
54
|
-
# Both layouts are walked: state is written flat as <task-id>/ by
|
|
46
|
+
# Oldest first, so the first hit in each bucket is the one the banner calls
|
|
47
|
+
# the oldest. Both layouts are walked: state is written flat as <task-id>/ by
|
|
55
48
|
# phase-tracker.sh and nested as <project>/<task-id>/ by Phase 0, and a
|
|
56
49
|
# reader that knows only one of them under-reports by the size of the other.
|
|
50
|
+
# artifacts/ holds the copy gc-abandoned takes before it marks a run failed,
|
|
51
|
+
# so it keeps the status the run had while it was alive.
|
|
57
52
|
while IFS= read -r f; do
|
|
58
53
|
[ -f "$f" ] || continue
|
|
59
54
|
STATUS=$(jq -r '.status // ""' "$f" 2>/dev/null)
|
|
60
|
-
# Only runs that
|
|
61
|
-
# unknown
|
|
62
|
-
#
|
|
63
|
-
# this change exists to remove, just pointed the other way. If that hides a
|
|
64
|
-
# real run, the fix is for its writer to record a status.
|
|
55
|
+
# Only runs that claim to be running are bucketed. A file with no status is
|
|
56
|
+
# unknown, and reporting unknown as "stopped mid-development" would be a
|
|
57
|
+
# false claim. The fix for a hidden run is for its writer to record a status.
|
|
65
58
|
# `complete` is the schema spelling and `completed` is in the wild too, so
|
|
66
59
|
# both are excluded; a reader that knows one calls finished runs stale.
|
|
67
60
|
case "$STATUS" in in_progress | paused | awaiting_input) ;; *) continue ;; esac
|
|
68
|
-
|
|
61
|
+
# `phase` is the field name a state written before currentPhase carries.
|
|
62
|
+
PHASE=$(jq -r '.currentPhase // .phase // "?" | tostring' "$f" 2>/dev/null)
|
|
69
63
|
TASK=$(jq -r '.taskId // .jiraId // ""' "$f" 2>/dev/null)
|
|
70
64
|
[ -n "$TASK" ] || continue
|
|
71
|
-
# `.pr` is an object in the schema and a bare URL string in
|
|
72
|
-
#
|
|
73
|
-
# reads as "no PR"
|
|
74
|
-
# and reaped. Read the shape, do not assume it.
|
|
65
|
+
# `.pr` is an object in the schema and a bare URL string in older state
|
|
66
|
+
# files. `jq '.pr.url'` on a string errors, and with stderr suppressed that
|
|
67
|
+
# reads as "no PR", so the shape is read rather than assumed.
|
|
75
68
|
PR=$(jq -r 'if (.pr|type)=="string" then .pr elif (.pr|type)=="object" then (.pr.url // .pr.number // "") else "" end | tostring' "$f" 2>/dev/null)
|
|
76
69
|
|
|
77
70
|
# Waiting for you, not broken: an open PR means the work landed, and
|
|
78
|
-
# Phase 5 pauses for channel selection by design (modes.md).
|
|
71
|
+
# Phase 5 pauses for channel selection by design (modes.md). 6 and 7 are
|
|
72
|
+
# the same pause in a state written before the six-phase numbering.
|
|
79
73
|
if [ "$STATUS" = "awaiting_input" ] || [ -n "$PR" ] ||
|
|
80
|
-
[ "$PHASE" = "6" ] || [ "$PHASE" = "7" ]; then
|
|
74
|
+
[ "$PHASE" = "5" ] || [ "$PHASE" = "6" ] || [ "$PHASE" = "7" ]; then
|
|
81
75
|
AWAITING_N=$((AWAITING_N + 1))
|
|
82
76
|
[ -z "$AWAITING_TASK" ] && { AWAITING_TASK="$TASK"; AWAITING_PHASE="$PHASE"; }
|
|
83
77
|
elif [ "$PHASE" = "0" ]; then
|
|
@@ -90,7 +84,7 @@ if [ -d "$LOGS" ] && command -v jq >/dev/null 2>&1; then
|
|
|
90
84
|
[ -z "$DIED_TASK" ] && { DIED_TASK="$TASK"; DIED_PHASE="$PHASE"; }
|
|
91
85
|
fi
|
|
92
86
|
done <<EOF
|
|
93
|
-
$(find "$LOGS" -name agent-state.json -type f -
|
|
87
|
+
$(find "$LOGS" -maxdepth 4 -name agent-state.json -type f -not -path '*/artifacts/*' -print0 2>/dev/null | xargs -0 ls -tr 2>/dev/null)
|
|
94
88
|
EOF
|
|
95
89
|
# Kept for the JSON consumers that read staleTask/stalePhase.
|
|
96
90
|
if [ -n "$AWAITING_TASK" ]; then
|
|
@@ -130,24 +124,44 @@ if [ "$JSON" -eq 1 ]; then
|
|
|
130
124
|
exit 0
|
|
131
125
|
fi
|
|
132
126
|
|
|
133
|
-
# One line per bucket, each naming its own next action
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
|
|
127
|
+
# One line per bucket, each naming its own next action. Counts are printed
|
|
128
|
+
# even when they are 1, because "1 run" and "13 runs" call for different
|
|
129
|
+
# reactions.
|
|
130
|
+
LANG_CODE=$(ma_output_lang 2>/dev/null || echo en)
|
|
137
131
|
if [ "$AWAITING_N" -gt 0 ]; then
|
|
138
|
-
|
|
139
|
-
|
|
132
|
+
if [ "$LANG_CODE" = "tr" ]; then
|
|
133
|
+
printf 'multi-agent: %s çalışma seni bekliyor, en eskisi %s (Phase %s). Bitirmek için `%s`.\n' \
|
|
134
|
+
"$AWAITING_N" "$AWAITING_TASK" "$AWAITING_PHASE" "$(ma_command resume)"
|
|
135
|
+
else
|
|
136
|
+
printf 'multi-agent: %s run(s) waiting on you, oldest %s at Phase %s - `%s` to finish.\n' \
|
|
137
|
+
"$AWAITING_N" "$AWAITING_TASK" "$AWAITING_PHASE" "$(ma_command resume)"
|
|
138
|
+
fi
|
|
140
139
|
fi
|
|
141
140
|
if [ "$DIED_N" -gt 0 ]; then
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
if [ "$LANG_CODE" = "tr" ]; then
|
|
142
|
+
printf 'multi-agent: %s çalışma geliştirme sırasında durdu, en eskisi %s (Phase %s). Devam için `%s`, silmek için `%s`.\n' \
|
|
143
|
+
"$DIED_N" "$DIED_TASK" "$DIED_PHASE" "$(ma_command resume)" "$(ma_command kill)"
|
|
144
|
+
else
|
|
145
|
+
printf 'multi-agent: %s run(s) stopped mid-development, oldest %s at Phase %s - `%s` or `%s`.\n' \
|
|
146
|
+
"$DIED_N" "$DIED_TASK" "$DIED_PHASE" "$(ma_command resume)" "$(ma_command kill)"
|
|
147
|
+
fi
|
|
144
148
|
fi
|
|
145
149
|
if [ "$UNSTARTED_N" -gt 0 ]; then
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
if [ "$LANG_CODE" = "tr" ]; then
|
|
151
|
+
printf 'multi-agent: %s çalışma Phase 0 sorusunda kaldı, hiçbir şey üretilmedi. Temizlemek için `%s --abandoned`.\n' \
|
|
152
|
+
"$UNSTARTED_N" "$(ma_command garbage-collect)"
|
|
153
|
+
else
|
|
154
|
+
printf 'multi-agent: %s run(s) left at a Phase 0 question, nothing built - `%s --abandoned` to clear.\n' \
|
|
155
|
+
"$UNSTARTED_N" "$(ma_command garbage-collect)"
|
|
156
|
+
fi
|
|
148
157
|
fi
|
|
149
158
|
if [ "$REVIEW_DUE" -eq 1 ]; then
|
|
150
|
-
|
|
151
|
-
|
|
159
|
+
if [ "$LANG_CODE" = "tr" ]; then
|
|
160
|
+
printf 'multi-agent: %s açık pipeline gözlemi var, son inceleme %s+ gün önce. Uygun olduğunda `%s backlog`.\n' \
|
|
161
|
+
"$OPEN_OBS" "$REVIEW_DAYS" "$(ma_command refactor)"
|
|
162
|
+
else
|
|
163
|
+
printf 'multi-agent: %s open pipeline observation(s), last review %s+ days ago - `%s backlog` when convenient.\n' \
|
|
164
|
+
"$OPEN_OBS" "$REVIEW_DAYS" "$(ma_command refactor)"
|
|
165
|
+
fi
|
|
152
166
|
fi
|
|
153
167
|
exit 0
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// This exists because the resolution order is the whole correctness question,
|
|
6
6
|
// and prose in a skill file cannot be tested. The step used to read the
|
|
7
7
|
// installed plugin cache and call the answer authoritative. A cache is only as
|
|
8
|
-
// fresh as the last `claude marketplace update`, so it reported "up to date"
|
|
8
|
+
// fresh as the last `claude plugin marketplace update`, so it reported "up to date"
|
|
9
9
|
// for a derivation that was four releases behind, and the version it had been
|
|
10
10
|
// recorded from was the wrong manifest besides.
|
|
11
11
|
//
|
|
@@ -476,6 +476,27 @@ function checkHookCoverage() {
|
|
|
476
476
|
);
|
|
477
477
|
return;
|
|
478
478
|
}
|
|
479
|
+
const seen = new Set();
|
|
480
|
+
const twice = new Set();
|
|
481
|
+
for (const [event, groups] of Object.entries(have?.hooks || {})) {
|
|
482
|
+
for (const group of Array.isArray(groups) ? groups : []) {
|
|
483
|
+
for (const h of group?.hooks || []) {
|
|
484
|
+
if (typeof h?.command !== "string") continue;
|
|
485
|
+
const key = `${event} ${group.matcher || ""} ${h.command}`;
|
|
486
|
+
if (seen.has(key)) twice.add(`${h.command.split("/").pop()} on ${group.matcher || event}`);
|
|
487
|
+
seen.add(key);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (twice.size) {
|
|
492
|
+
report(
|
|
493
|
+
"hook-coverage",
|
|
494
|
+
"WARN",
|
|
495
|
+
`registered twice, so it runs twice per call: ${[...twice].join(", ")}`,
|
|
496
|
+
`run ${hostCommand("update", HERE)}`,
|
|
497
|
+
);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
479
500
|
ok("hook-coverage");
|
|
480
501
|
}
|
|
481
502
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# plugin-refresh.sh - bring the stack plugins to the latest published version.
|
|
4
|
+
#
|
|
5
|
+
# Two steps, because they are two different things: `claude plugin marketplace
|
|
6
|
+
# update` refreshes the catalog, and each installed plugin then needs its own
|
|
7
|
+
# `claude plugin update` in the scope it was installed in. A project or local
|
|
8
|
+
# install is updated from inside its own project, because that is how the CLI
|
|
9
|
+
# tells two projects' installs apart. The catalog refresh alone leaves every
|
|
10
|
+
# installed plugin where it was.
|
|
11
|
+
#
|
|
12
|
+
# Usage: plugin-refresh.sh <owner> [--add-only]
|
|
13
|
+
# <owner> GitHub owner of the multi-agent-plugins repository
|
|
14
|
+
# --add-only make sure the marketplace is configured, update nothing
|
|
15
|
+
#
|
|
16
|
+
# The CLI's own error output is passed through, never discarded.
|
|
17
|
+
# Exit codes: 0 = done (or no claude CLI on this machine), 1 = a step failed
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
OWNER="${1:-}"
|
|
22
|
+
MODE="${2:-}"
|
|
23
|
+
MP="multi-agent-plugins"
|
|
24
|
+
CLAUDE="${CLAUDE_BIN:-claude}"
|
|
25
|
+
|
|
26
|
+
if [ -z "$OWNER" ]; then
|
|
27
|
+
echo "usage: plugin-refresh.sh <owner> [--add-only]" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
if ! command -v "$CLAUDE" >/dev/null 2>&1; then
|
|
32
|
+
echo " -> claude CLI not found; stack plugins not refreshed"
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
FAILED=0
|
|
37
|
+
|
|
38
|
+
if ! "$CLAUDE" plugin marketplace list 2>/dev/null | grep -q "$MP"; then
|
|
39
|
+
if "$CLAUDE" plugin marketplace add "$OWNER/$MP"; then
|
|
40
|
+
echo " -> marketplace $MP added"
|
|
41
|
+
else
|
|
42
|
+
echo " -> could not add marketplace $OWNER/$MP" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
[ "$MODE" = "--add-only" ] && exit 0
|
|
48
|
+
|
|
49
|
+
if "$CLAUDE" plugin marketplace update "$MP"; then
|
|
50
|
+
echo " -> marketplace $MP refreshed"
|
|
51
|
+
else
|
|
52
|
+
echo " -> marketplace $MP refresh failed" >&2
|
|
53
|
+
FAILED=1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
INSTALLED=$("$CLAUDE" plugin list --json 2>/dev/null | node -e '
|
|
57
|
+
let s = "";
|
|
58
|
+
process.stdin.on("data", (d) => (s += d)).on("end", () => {
|
|
59
|
+
let rows = [];
|
|
60
|
+
try { rows = JSON.parse(s); } catch { process.exit(0); }
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
for (const r of Array.isArray(rows) ? rows : []) {
|
|
63
|
+
if (typeof r.id !== "string" || !r.id.endsWith("@" + process.argv[1])) continue;
|
|
64
|
+
const scope = r.scope || "user";
|
|
65
|
+
const dir = scope === "project" || scope === "local" ? r.projectPath || "" : "";
|
|
66
|
+
if ((scope === "project" || scope === "local") && !dir) continue;
|
|
67
|
+
const key = [r.id, scope, dir].join("\t");
|
|
68
|
+
if (seen.has(key)) continue;
|
|
69
|
+
seen.add(key);
|
|
70
|
+
console.log(key);
|
|
71
|
+
}
|
|
72
|
+
});' "$MP")
|
|
73
|
+
|
|
74
|
+
while IFS=$'\t' read -r ID SCOPE DIR; do
|
|
75
|
+
[ -n "$ID" ] || continue
|
|
76
|
+
WHERE="$SCOPE${DIR:+ $DIR}"
|
|
77
|
+
if [ -n "$DIR" ] && [ ! -d "$DIR" ]; then
|
|
78
|
+
echo " -> $ID ($WHERE) skipped, project directory is gone"
|
|
79
|
+
continue
|
|
80
|
+
fi
|
|
81
|
+
if (cd "${DIR:-$PWD}" && "$CLAUDE" plugin update "$ID" --scope "$SCOPE"); then
|
|
82
|
+
echo " -> $ID ($WHERE) updated"
|
|
83
|
+
else
|
|
84
|
+
echo " -> $ID ($WHERE) update failed" >&2
|
|
85
|
+
FAILED=1
|
|
86
|
+
fi
|
|
87
|
+
done <<EOF
|
|
88
|
+
$INSTALLED
|
|
89
|
+
EOF
|
|
90
|
+
|
|
91
|
+
[ "$FAILED" -eq 0 ] || exit 1
|
|
92
|
+
exit 0
|
|
@@ -49,10 +49,12 @@ import { join } from "node:path";
|
|
|
49
49
|
import { homedir, hostname } from "node:os";
|
|
50
50
|
import { pathToFileURL } from "node:url";
|
|
51
51
|
import { hostCommand } from "../lib/user-facing.mjs";
|
|
52
|
+
import { canonicalEndpoint } from "../lib/usage-endpoint.mjs";
|
|
52
53
|
import { ghTokenFor, resolveReportingLogin } from "./usage-identity.mjs";
|
|
53
54
|
import { lockTimeoutFrom, updateJsonFileSync } from "../lib/json-file-lock.mjs";
|
|
54
55
|
|
|
55
56
|
const DEFAULT_ENDPOINT = "https://mmerterden.com/api/usage/ingest";
|
|
57
|
+
const BUILT_IN_HOSTS = ["mmerterden.com"];
|
|
56
58
|
const TIMEOUT_MS = 10000;
|
|
57
59
|
|
|
58
60
|
function readJson(file) {
|
|
@@ -118,7 +120,7 @@ export function decideRegistration(
|
|
|
118
120
|
|
|
119
121
|
/** The /register URL derived from whatever endpoint the prefs name. */
|
|
120
122
|
export function registerUrl(endpoint) {
|
|
121
|
-
const ep = (endpoint || DEFAULT_ENDPOINT).trim().replace(/\/+$/, "");
|
|
123
|
+
const ep = canonicalEndpoint((endpoint || DEFAULT_ENDPOINT).trim()).replace(/\/+$/, "");
|
|
122
124
|
return ep.endsWith("/ingest") ? `${ep.slice(0, -"/ingest".length)}/register` : `${ep}/register`;
|
|
123
125
|
}
|
|
124
126
|
|
|
@@ -132,7 +134,7 @@ function storedToken(prefs, credStore) {
|
|
|
132
134
|
|
|
133
135
|
/**
|
|
134
136
|
* May the gh token be sent to `url`? The token is a full GitHub credential, so
|
|
135
|
-
* it only goes over https to the
|
|
137
|
+
* it only goes over https to the reporting site's own hosts or a host the prefs
|
|
136
138
|
* name in `usageLog.trustedHosts`; http only to loopback (local development).
|
|
137
139
|
*/
|
|
138
140
|
export function registerUrlAllowed(url, trustedHosts = []) {
|
|
@@ -144,9 +146,7 @@ export function registerUrlAllowed(url, trustedHosts = []) {
|
|
|
144
146
|
}
|
|
145
147
|
if (u.protocol === "http:") return ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname);
|
|
146
148
|
if (u.protocol !== "https:") return false;
|
|
147
|
-
const trusted = new Set(
|
|
148
|
-
[new URL(DEFAULT_ENDPOINT).hostname, ...trustedHosts].map((h) => String(h).toLowerCase()),
|
|
149
|
-
);
|
|
149
|
+
const trusted = new Set([...BUILT_IN_HOSTS, ...trustedHosts].map((h) => String(h).toLowerCase()));
|
|
150
150
|
return trusted.has(u.hostname.toLowerCase());
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -355,7 +355,7 @@ async function main(argv) {
|
|
|
355
355
|
|
|
356
356
|
const optedOut = prefs.global?.usageLog?.optOut === true;
|
|
357
357
|
const entry = `${process.env.USER || user}_Usage_Ingest_Token`;
|
|
358
|
-
const set = spawnSync("bash", [credStore, "set", entry], {
|
|
358
|
+
const set = spawnSync("bash", [credStore, "set", entry, "-"], {
|
|
359
359
|
input: token,
|
|
360
360
|
encoding: "utf8",
|
|
361
361
|
timeout: 10000,
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { readFileSync, existsSync, readdirSync, appendFileSync } from "fs";
|
|
31
|
+
import { canonicalEndpoint } from "../lib/usage-endpoint.mjs";
|
|
31
32
|
import { homedir, platform } from "os";
|
|
32
33
|
import { dirname, join } from "path";
|
|
33
34
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
@@ -339,10 +340,10 @@ function commandOf(state, mode) {
|
|
|
339
340
|
return mode;
|
|
340
341
|
}
|
|
341
342
|
|
|
342
|
-
// A repository name is the user's, not ours
|
|
343
|
-
// the
|
|
344
|
-
//
|
|
345
|
-
//
|
|
343
|
+
// A repository name is the user's, not ours, and the panel never needs it:
|
|
344
|
+
// only the ability to tell one project's runs from another's. A digest keeps
|
|
345
|
+
// the name out of the record. It is not secret: anyone who can guess a name
|
|
346
|
+
// can hash it and compare.
|
|
346
347
|
function projectDigest(name) {
|
|
347
348
|
if (typeof name !== "string" || !name.trim()) return null;
|
|
348
349
|
return `p${createHash("sha256").update(name.trim()).digest("hex").slice(0, 8)}`;
|
|
@@ -721,7 +722,7 @@ async function main() {
|
|
|
721
722
|
const args = parseArgs(process.argv.slice(2));
|
|
722
723
|
if (process.env.MULTI_AGENT_SMOKE && !args.dryRun) return;
|
|
723
724
|
const prefs = resolvePrefs();
|
|
724
|
-
const endpoint = prefs.endpoint || ENDPOINT_DEFAULT;
|
|
725
|
+
const endpoint = canonicalEndpoint(prefs.endpoint || ENDPOINT_DEFAULT);
|
|
725
726
|
|
|
726
727
|
// Gate BEFORE touching the keychain: optOut is a hard block, and reading the
|
|
727
728
|
// secret for a disabled feature is both wasteful and can raise a macOS
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-25T15:14:42Z",
|
|
4
4
|
"skillCount": 218,
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"path": "shared/core/multi-agent-channels/SKILL.md",
|
|
48
|
-
"sha256": "
|
|
48
|
+
"sha256": "03f7c648e1938f3d6d7c48d4c1c01f2bd734cdce873ec5203120b5be3ae761db"
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"path": "shared/core/multi-agent-complaint-analysis/SKILL.md",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
"path": "shared/core/multi-agent-jira/SKILL.md",
|
|
100
|
-
"sha256": "
|
|
100
|
+
"sha256": "d7e23fd1565b6640d33c23cce4d025660212d9d3f3ab80a43369ad80ae8be080"
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"path": "shared/core/multi-agent-kill/SKILL.md",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"path": "shared/core/multi-agent-refactor/SKILL.md",
|
|
136
|
-
"sha256": "
|
|
136
|
+
"sha256": "889c17f800b83d5c5d13d097eff63f28dc8d7869679bfcdb72efecd8b1d66391"
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
"path": "shared/core/multi-agent-research/SKILL.md",
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
},
|
|
206
206
|
{
|
|
207
207
|
"path": "shared/core/multi-agent-stack/SKILL.md",
|
|
208
|
-
"sha256": "
|
|
208
|
+
"sha256": "ac964cd00dd6ed06512530489d867efbc51d7d7614265f9aa022679325d5defe"
|
|
209
209
|
},
|
|
210
210
|
{
|
|
211
211
|
"path": "shared/core/multi-agent-status/SKILL.md",
|
|
@@ -253,11 +253,11 @@
|
|
|
253
253
|
},
|
|
254
254
|
{
|
|
255
255
|
"path": "shared/core/multi-agent-update/SKILL.md",
|
|
256
|
-
"sha256": "
|
|
256
|
+
"sha256": "d96e18b70bab0a601057f75f212dd4fb563db0cb250607c770ce1c7a1df4b1d0"
|
|
257
257
|
},
|
|
258
258
|
{
|
|
259
259
|
"path": "shared/core/multi-agent/SKILL.md",
|
|
260
|
-
"sha256": "
|
|
260
|
+
"sha256": "3bfaa4f0743aa1ab8d7f2bec4edb6b3c759f6c7a890535b2f0e1606f2ce7547d"
|
|
261
261
|
},
|
|
262
262
|
{
|
|
263
263
|
"path": "shared/external/accessibility-compliance-accessibility-audit/SKILL.md",
|