@mmerterden/multi-agent-pipeline 20.3.1 → 20.5.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 +29 -0
- package/docs/ecosystem.md +2 -2
- package/docs/facts.json +1 -1
- package/install/claude.mjs +35 -0
- package/manifest.json +64 -62
- package/package.json +1 -1
- package/pipeline/agents/android-architect.md +1 -0
- package/pipeline/agents/backend-architect.md +1 -0
- package/pipeline/agents/bulk-reader.md +1 -0
- package/pipeline/agents/code-reviewer.md +2 -1
- package/pipeline/agents/dev-critic.md +1 -0
- package/pipeline/agents/explorer.md +1 -0
- package/pipeline/agents/ios-architect.md +1 -0
- package/pipeline/agents/plan-critic.md +1 -0
- package/pipeline/agents/security-auditor.md +1 -0
- package/pipeline/agents/task-clarifier.md +1 -0
- package/pipeline/commands/figma-to-swiftui.md +6 -4
- package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +1 -1
- 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 +6 -11
- package/pipeline/lib/redact.mjs +50 -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/features/unattended-security.md +5 -0
- 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/autopilot-config.schema.json +5 -0
- package/pipeline/schemas/prefs.schema.json +1 -1
- package/pipeline/scripts/_autopilot-outcomes.mjs +2 -0
- package/pipeline/scripts/audit-log-rotate.sh +3 -0
- package/pipeline/scripts/audit-log.sh +10 -2
- package/pipeline/scripts/autopilot-intake.mjs +17 -15
- package/pipeline/scripts/autopilot-publish.mjs +16 -1
- package/pipeline/scripts/autopilot-runner.mjs +20 -0
- 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/log-metric.sh +5 -0
- package/pipeline/scripts/plugin-refresh.sh +92 -0
- package/pipeline/scripts/pr-request.mjs +9 -3
- package/pipeline/scripts/update-check.sh +73 -21
- package/pipeline/scripts/usage-report.mjs +4 -4
- package/pipeline/skills/.skill-manifest.json +8 -8
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -4
- package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +1 -1
- 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
|
@@ -112,7 +112,7 @@ import { OUTCOME } from "./_autopilot-outcomes.mjs";
|
|
|
112
112
|
import { commandFor, getAdapter, loadAdapters, resolveStack } from "./_stack-adapter.mjs";
|
|
113
113
|
import { checkCommit, park, updateState } from "./gate-ledger.mjs";
|
|
114
114
|
import { LOCKFILES } from "./package-manager.mjs";
|
|
115
|
-
import { PROTECTED_BRANCHES, validatePrRequest } from "./pr-request.mjs";
|
|
115
|
+
import { PROTECTED_BRANCHES, hasAutoClose, validatePrRequest } from "./pr-request.mjs";
|
|
116
116
|
import { summarize } from "./test-summary.mjs";
|
|
117
117
|
|
|
118
118
|
const HERE = import.meta.dirname;
|
|
@@ -576,6 +576,20 @@ function checkRemoteBranch(stage, url, branch, headSha, prior) {
|
|
|
576
576
|
}
|
|
577
577
|
|
|
578
578
|
/** Gate ledger: the commit hook's requirement, re-checked for every commit in the range. */
|
|
579
|
+
// A closing keyword in a commit message closes the issue when the PR merges,
|
|
580
|
+
// whatever the PR text says, and issues are closed by people, not by a run.
|
|
581
|
+
function checkCommitMessages(stage, commits) {
|
|
582
|
+
for (const sha of commits) {
|
|
583
|
+
const message = stage.out(["log", "-1", "--format=%B", sha]) || "";
|
|
584
|
+
if (hasAutoClose(message)) {
|
|
585
|
+
throw new Refusal(
|
|
586
|
+
"auto-close",
|
|
587
|
+
`commit ${sha.slice(0, 12)} carries an auto-close keyword; use Ref: #N`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
579
593
|
function checkLedger(state, commits, wt, env) {
|
|
580
594
|
for (const c of commits) {
|
|
581
595
|
const parent = gitOut(wt, ["rev-parse", "--verify", `${c}^1`], env);
|
|
@@ -1319,6 +1333,7 @@ export async function publish({ sessionId, statePath, item, config, env = proces
|
|
|
1319
1333
|
const mergeBase = stage.out(["merge-base", baseSha, headSha]);
|
|
1320
1334
|
if (!mergeBase) throw new Refusal("branch", `${req.branch} shares no history with ${req.base}`);
|
|
1321
1335
|
|
|
1336
|
+
checkCommitMessages(stage, commits);
|
|
1322
1337
|
checkLedger(state, commits, wt, localEnv);
|
|
1323
1338
|
checkTestLog(state, wt, statePath);
|
|
1324
1339
|
checkSecrets(wt, `${mergeBase}..${headSha}`, env);
|
|
@@ -2086,6 +2086,26 @@ async function main() {
|
|
|
2086
2086
|
return 0;
|
|
2087
2087
|
}
|
|
2088
2088
|
|
|
2089
|
+
// ---- 3d. VERSION --------------------------------------------------------
|
|
2090
|
+
// Phase 0 of the child halts below the published floor, and the update it
|
|
2091
|
+
// would need is refused unattended. Checked here so a floor costs no session.
|
|
2092
|
+
const floorGate = join(SCRIPTS, "require-supported-version.sh");
|
|
2093
|
+
if (existsSync(floorGate)) {
|
|
2094
|
+
const floor = spawnSync("bash", [floorGate], { encoding: "utf-8", timeout: 30_000 });
|
|
2095
|
+
if (floor.status === 3) {
|
|
2096
|
+
const [, local, , required] = (floor.stdout || "").trim().split("|");
|
|
2097
|
+
const reason = `installed v${local || "?"} is below the required v${required || "?"}; run the update`;
|
|
2098
|
+
log(`not launching: ${reason}`);
|
|
2099
|
+
if (!DRY) {
|
|
2100
|
+
record({ source: next.source, id: next.id, outcome: OUTCOME.BLOCKED_VERSION_FLOOR });
|
|
2101
|
+
writeState("queue.json", { ...queue, blockedReason: reason });
|
|
2102
|
+
refreshStatus();
|
|
2103
|
+
}
|
|
2104
|
+
tick({ action: "version-floor" });
|
|
2105
|
+
return 0;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2089
2109
|
// ---- 4. TAKE -----------------------------------------------------------
|
|
2090
2110
|
const sessionId = randomUUID();
|
|
2091
2111
|
const taskId = `ap-${Date.now()}`;
|
|
@@ -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
|
|
|
@@ -81,6 +81,11 @@ PHASE_SCHEMA="$(node -e 'const c=require("'"$(cd "$(dirname "$0")" && pwd)"'/../
|
|
|
81
81
|
TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
82
82
|
LINE="{\"ts\":\"${TS}\",\"task_id\":\"${TASK_ID}\",\"phase\":\"${PHASE}\",\"event\":\"${EVENT}\",\"details\":${DETAILS},\"phaseSchema\":${PHASE_SCHEMA}}"
|
|
83
83
|
|
|
84
|
+
# Caller-supplied values can carry a credential copied out of an error.
|
|
85
|
+
# redact.mjs replaces it with a marker; without node the line is written as is.
|
|
86
|
+
REDACT="$(cd "$(dirname "$0")" && pwd)/../lib/redact.mjs"
|
|
87
|
+
[ -f "$REDACT" ] && LINE=$(printf '%s' "$LINE" | node "$REDACT" 2>/dev/null || printf '%s' "$LINE")
|
|
88
|
+
|
|
84
89
|
# Atomic append (one line, well under PIPE_BUF).
|
|
85
90
|
echo "$LINE" >> "$METRICS_FILE" 2>/dev/null || {
|
|
86
91
|
echo "warn: log-metric: cannot write to $METRICS_FILE - telemetry skipped" >&2
|
|
@@ -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
|
|
@@ -46,7 +46,14 @@ export const FIELDS = ["branch", "base", "title", "body", "draft", "repo"];
|
|
|
46
46
|
export const PROTECTED_BRANCHES = ["main", "master", "develop"];
|
|
47
47
|
const REF = /^[A-Za-z0-9._/-]+$/;
|
|
48
48
|
const REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
49
|
-
|
|
49
|
+
// GitHub closes an issue from `#N`, `owner/repo#N` and the issue's full URL.
|
|
50
|
+
const AUTO_CLOSE =
|
|
51
|
+
/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s+(?:(?:[\w.-]+\/[\w.-]+)?#\d+|https?:\/\/[^\s/]+\/[\w.-]+\/[\w.-]+\/issues\/\d+)/i;
|
|
52
|
+
|
|
53
|
+
/** True when the text would close an issue once the PR or commit merges. */
|
|
54
|
+
export function hasAutoClose(text) {
|
|
55
|
+
return typeof text === "string" && AUTO_CLOSE.test(text);
|
|
56
|
+
}
|
|
50
57
|
|
|
51
58
|
/**
|
|
52
59
|
* The request object `write` produces. draft is always true.
|
|
@@ -92,8 +99,7 @@ export function validatePrRequest(req) {
|
|
|
92
99
|
if (typeof req.branch === "string" && req.branch === req.base)
|
|
93
100
|
errors.push("branch and base are the same");
|
|
94
101
|
for (const k of ["title", "body"]) {
|
|
95
|
-
if (
|
|
96
|
-
errors.push(`${k} carries an auto-close keyword; use Ref: #N`);
|
|
102
|
+
if (hasAutoClose(req[k])) errors.push(`${k} carries an auto-close keyword; use Ref: #N`);
|
|
97
103
|
}
|
|
98
104
|
const secrets = [];
|
|
99
105
|
for (const k of ["title", "body"]) {
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
# Usage:
|
|
27
27
|
# bash pipeline/scripts/update-check.sh # auto-detect local version
|
|
28
28
|
# bash pipeline/scripts/update-check.sh --local 10.8.0 # explicit local version
|
|
29
|
-
# bash pipeline/scripts/update-check.sh --ttl-hours 24 # cache window (default
|
|
29
|
+
# bash pipeline/scripts/update-check.sh --ttl-hours 24 # cache window (default: prefs
|
|
30
|
+
# # updateCheck.ttlHours, else 24)
|
|
30
31
|
# bash pipeline/scripts/update-check.sh --force # ignore cache
|
|
31
32
|
# bash pipeline/scripts/update-check.sh --print-required # emit the floor only
|
|
32
33
|
#
|
|
@@ -41,7 +42,8 @@ set -euo pipefail
|
|
|
41
42
|
PKG="@mmerterden/multi-agent-pipeline"
|
|
42
43
|
REGISTRY_URL="https://registry.npmjs.org/${PKG/\//%2F}"
|
|
43
44
|
CACHE_FILE="${UPDATE_CHECK_CACHE:-$HOME/.claude/logs/multi-agent/.update-check}"
|
|
44
|
-
TTL_HOURS=
|
|
45
|
+
TTL_HOURS=""
|
|
46
|
+
FAIL_TTL_SECONDS=3600
|
|
45
47
|
LOCAL_VERSION=""
|
|
46
48
|
FORCE=0
|
|
47
49
|
PRINT_REQUIRED=0
|
|
@@ -49,37 +51,65 @@ PRINT_REQUIRED=0
|
|
|
49
51
|
while [ $# -gt 0 ]; do
|
|
50
52
|
case "$1" in
|
|
51
53
|
--local) LOCAL_VERSION="${2:-}"; shift; [ $# -gt 0 ] && shift ;;
|
|
52
|
-
--ttl-hours) TTL_HOURS="${2:-
|
|
54
|
+
--ttl-hours) TTL_HOURS="${2:-}"; shift; [ $# -gt 0 ] && shift ;;
|
|
53
55
|
--force) FORCE=1; shift ;;
|
|
54
56
|
--print-required) PRINT_REQUIRED=1; shift ;;
|
|
55
57
|
*) shift ;;
|
|
56
58
|
esac
|
|
57
59
|
done
|
|
58
60
|
|
|
59
|
-
# Local version: explicit arg, else
|
|
61
|
+
# Local version: explicit arg, else the marker the installer stamps. The
|
|
62
|
+
# marker is what is installed and what /multi-agent:update rewrites, so it is
|
|
63
|
+
# read before any repo clone; a clone is only a fallback for a machine that
|
|
64
|
+
# runs the pipeline from source without installing it.
|
|
60
65
|
if [ -z "$LOCAL_VERSION" ]; then
|
|
61
|
-
for
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
for marker in "$HOME/.claude/.pipeline-version" "$HOME/.copilot/.pipeline-version" \
|
|
67
|
+
"$HOME/.codex/.pipeline-version"; do
|
|
68
|
+
if [ -f "$marker" ]; then
|
|
69
|
+
LOCAL_VERSION=$(head -1 "$marker" 2>/dev/null | tr -d '[:space:]')
|
|
64
70
|
[ -n "$LOCAL_VERSION" ] && break
|
|
65
71
|
fi
|
|
66
72
|
done
|
|
67
73
|
fi
|
|
68
|
-
# npx-only installs have no repo clone; the installer stamps the version here.
|
|
69
74
|
if [ -z "$LOCAL_VERSION" ]; then
|
|
70
|
-
for
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
LOCAL_VERSION=$(head -1 "$marker" 2>/dev/null | tr -d '[:space:]')
|
|
75
|
+
for candidate in "$HOME/multi-agent-pipeline" "$HOME/dev/multi-agent-pipeline" "$HOME/projects/multi-agent-pipeline"; do
|
|
76
|
+
if [ -f "$candidate/package.json" ]; then
|
|
77
|
+
LOCAL_VERSION=$(node -p "require('$candidate/package.json').version" 2>/dev/null || true)
|
|
74
78
|
[ -n "$LOCAL_VERSION" ] && break
|
|
75
79
|
fi
|
|
76
80
|
done
|
|
77
81
|
fi
|
|
78
82
|
[ -z "$LOCAL_VERSION" ] && exit 0 # cannot determine local version -> silent no-op
|
|
79
83
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
# Cache lifetime: --ttl-hours, else prefs global.updateCheck.ttlHours, else 24.
|
|
85
|
+
# Anything that is not a whole number falls back to 24.
|
|
86
|
+
if [ -z "$TTL_HOURS" ]; then
|
|
87
|
+
PREFS_FILE="${MULTI_AGENT_PREFS:-$HOME/.claude/multi-agent-preferences.json}"
|
|
88
|
+
if [ -f "$PREFS_FILE" ] && command -v jq >/dev/null 2>&1; then
|
|
89
|
+
TTL_HOURS=$(jq -r '.global.updateCheck.ttlHours // empty' "$PREFS_FILE" 2>/dev/null || true)
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
case "$TTL_HOURS" in (""|*[!0-9]*) TTL_HOURS=24 ;; esac
|
|
93
|
+
|
|
94
|
+
# 0 when $1 sorts strictly below $2. Semver order: numeric major.minor.patch,
|
|
95
|
+
# and a prerelease (1.2.3-beta) below its release (1.2.3). A leading v is ignored.
|
|
96
|
+
version_lt() {
|
|
97
|
+
local a="${1#v}" b="${2#v}" ac ap bc bp i x y
|
|
98
|
+
ac="${a%%-*}"; bc="${b%%-*}"
|
|
99
|
+
ap=""; bp=""
|
|
100
|
+
[ "$ac" != "$a" ] && ap="${a#*-}"
|
|
101
|
+
[ "$bc" != "$b" ] && bp="${b#*-}"
|
|
102
|
+
for i in 1 2 3; do
|
|
103
|
+
x=$(printf '%s' "$ac" | cut -d. -f"$i"); y=$(printf '%s' "$bc" | cut -d. -f"$i")
|
|
104
|
+
case "$x" in (""|*[!0-9]*) x=0 ;; esac
|
|
105
|
+
case "$y" in (""|*[!0-9]*) y=0 ;; esac
|
|
106
|
+
[ "$x" -lt "$y" ] && return 0
|
|
107
|
+
[ "$x" -gt "$y" ] && return 1
|
|
108
|
+
done
|
|
109
|
+
[ -n "$ap" ] && [ -z "$bp" ] && return 0
|
|
110
|
+
[ -z "$ap" ] && [ -n "$bp" ] && return 1
|
|
111
|
+
[ -n "$ap" ] && [ "$(printf '%s\n%s\n' "$ap" "$bp" | sort | head -1)" = "$ap" ] && [ "$ap" != "$bp" ] && return 0
|
|
112
|
+
return 1
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
now=$(date +%s)
|
|
@@ -92,6 +122,8 @@ if [ "$FORCE" -eq 0 ] && [ -f "$CACHE_FILE" ]; then
|
|
|
92
122
|
cached_latest=$(cut -d'|' -f2 "$CACHE_FILE" 2>/dev/null || echo "")
|
|
93
123
|
cached_required=$(cut -d'|' -f3 "$CACHE_FILE" 2>/dev/null || echo "")
|
|
94
124
|
case "$cached_epoch" in (*[!0-9]*|"") cached_epoch=0 ;; esac
|
|
125
|
+
# A stamp in the future is a clock or copy mistake, not a fresh answer.
|
|
126
|
+
[ "$cached_epoch" -gt "$now" ] && cached_epoch=0
|
|
95
127
|
if [ $((now - cached_epoch)) -lt $((TTL_HOURS * 3600)) ] && [ -n "$cached_latest" ]; then
|
|
96
128
|
latest="$cached_latest"
|
|
97
129
|
required="$cached_required"
|
|
@@ -100,6 +132,16 @@ fi
|
|
|
100
132
|
|
|
101
133
|
# Stale or missing cache -> one bounded registry call (silent on any failure).
|
|
102
134
|
# The abbreviated packument carries dist-tags at a quarter of the full document.
|
|
135
|
+
# A failed read is remembered for FAIL_TTL_SECONDS in "<cache>.fail", so an
|
|
136
|
+
# offline machine does not wait out the timeout on every call.
|
|
137
|
+
if [ -z "$latest" ] && [ "$FORCE" -eq 0 ] && [ -f "$CACHE_FILE.fail" ]; then
|
|
138
|
+
failed_at=$(head -1 "$CACHE_FILE.fail" 2>/dev/null | tr -d '[:space:]')
|
|
139
|
+
case "$failed_at" in (*[!0-9]*|"") failed_at=0 ;; esac
|
|
140
|
+
if [ "$failed_at" -le "$now" ] && [ $((now - failed_at)) -lt "$FAIL_TTL_SECONDS" ]; then
|
|
141
|
+
exit 0
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
103
145
|
if [ -z "$latest" ]; then
|
|
104
146
|
tags=$(curl -sm 3 -H 'Accept: application/vnd.npm.install-v1+json' "$REGISTRY_URL" 2>/dev/null \
|
|
105
147
|
| { if command -v jq >/dev/null 2>&1; then
|
|
@@ -110,14 +152,25 @@ if [ -z "$latest" ]; then
|
|
|
110
152
|
latest="${tags%%|*}"
|
|
111
153
|
required="${tags#*|}"
|
|
112
154
|
[ "$required" = "$tags" ] && required=""
|
|
113
|
-
[ -z "$latest" ] && exit 0
|
|
114
155
|
mkdir -p "$(dirname "$CACHE_FILE")" 2>/dev/null || exit 0
|
|
115
|
-
|
|
156
|
+
if [ -z "$latest" ]; then
|
|
157
|
+
printf '%s\n' "$now" > "$CACHE_FILE.fail" 2>/dev/null || true
|
|
158
|
+
exit 0
|
|
159
|
+
fi
|
|
160
|
+
rm -f "$CACHE_FILE.fail" 2>/dev/null || true
|
|
161
|
+
# Written through a temp file and a rename, so a concurrent reader never
|
|
162
|
+
# sees a half-written line.
|
|
163
|
+
tmp_cache=$(mktemp "$CACHE_FILE.XXXXXX" 2>/dev/null) || exit 0
|
|
164
|
+
if printf '%s|%s|%s\n' "$now" "$latest" "$required" > "$tmp_cache" 2>/dev/null; then
|
|
165
|
+
mv -f "$tmp_cache" "$CACHE_FILE" 2>/dev/null || rm -f "$tmp_cache"
|
|
166
|
+
else
|
|
167
|
+
rm -f "$tmp_cache"
|
|
168
|
+
fi
|
|
116
169
|
fi
|
|
117
170
|
|
|
118
171
|
# A floor above latest is a publisher mistake (the `required` tag was moved to a
|
|
119
172
|
# version that `latest` no longer covers). Clamp rather than brick every user.
|
|
120
|
-
if [ -n "$required" ] &&
|
|
173
|
+
if [ -n "$required" ] && version_lt "$latest" "$required"; then
|
|
121
174
|
required="$latest"
|
|
122
175
|
fi
|
|
123
176
|
|
|
@@ -127,8 +180,7 @@ if [ "$PRINT_REQUIRED" -eq 1 ]; then
|
|
|
127
180
|
fi
|
|
128
181
|
|
|
129
182
|
# Forced update: local sorts strictly BELOW the required floor.
|
|
130
|
-
if [ -n "$required" ] &&
|
|
131
|
-
&& [ "$(highest_of "$LOCAL_VERSION" "$required")" = "$required" ]; then
|
|
183
|
+
if [ -n "$required" ] && version_lt "$LOCAL_VERSION" "$required"; then
|
|
132
184
|
printf '%s|%s|force\n' "$LOCAL_VERSION" "$latest"
|
|
133
185
|
exit 0
|
|
134
186
|
fi
|
|
@@ -137,7 +189,7 @@ fi
|
|
|
137
189
|
|
|
138
190
|
# Update available only when latest sorts strictly ABOVE local (a dev machine
|
|
139
191
|
# running ahead of the registry must not see an "update" prompt).
|
|
140
|
-
if
|
|
192
|
+
if version_lt "$LOCAL_VERSION" "$latest"; then
|
|
141
193
|
printf '%s|%s\n' "$LOCAL_VERSION" "$latest"
|
|
142
194
|
fi
|
|
143
195
|
exit 0
|
|
@@ -340,10 +340,10 @@ function commandOf(state, mode) {
|
|
|
340
340
|
return mode;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
// A repository name is the user's, not ours
|
|
344
|
-
// the
|
|
345
|
-
//
|
|
346
|
-
//
|
|
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.
|
|
347
347
|
function projectDigest(name) {
|
|
348
348
|
if (typeof name !== "string" || !name.trim()) return null;
|
|
349
349
|
return `p${createHash("sha256").update(name.trim()).digest("hex").slice(0, 8)}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-25T16:25:54Z",
|
|
4
4
|
"skillCount": 218,
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "shared/core/multi-agent-autopilot-on/SKILL.md",
|
|
32
|
-
"sha256": "
|
|
32
|
+
"sha256": "d4f23f6f7bb493a4efa2cfa252fab50b22b6506aeaf31985db4498b5f010c8c8"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"path": "shared/core/multi-agent-autopilot-status/SKILL.md",
|
|
@@ -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",
|