@mmerterden/multi-agent-pipeline 11.3.2 → 11.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 +141 -0
- package/README.md +1 -0
- package/index.js +9 -2
- package/install/_common.mjs +107 -5
- package/install/_telemetry.mjs +5 -23
- package/install/claude.mjs +113 -11
- package/install/copilot.mjs +75 -6
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/review/SKILL.md +36 -4
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +22 -0
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +22 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
- package/pipeline/lib/account-resolver.sh +61 -23
- package/pipeline/lib/channels-multi-repo.sh +7 -2
- package/pipeline/lib/count-lib.sh +27 -0
- package/pipeline/lib/credential-store.sh +23 -6
- package/pipeline/lib/extract-conventions.sh +27 -21
- package/pipeline/lib/fetch-confluence.sh +25 -13
- package/pipeline/lib/fetch-crashlytics.sh +30 -8
- package/pipeline/lib/fetch-fortify.sh +11 -2
- package/pipeline/lib/fetch-swagger.sh +18 -7
- package/pipeline/lib/figma-mcp-refresh.sh +26 -11
- package/pipeline/lib/figma-screenshot.sh +11 -2
- package/pipeline/lib/issue-fetcher.sh +31 -6
- package/pipeline/lib/multi-repo-pipeline.sh +84 -20
- package/pipeline/lib/plan-todos.sh +12 -3
- package/pipeline/lib/post-pr-review.sh +5 -3
- package/pipeline/lib/repo-cache.sh +53 -9
- package/pipeline/lib/review-watch.sh +26 -6
- package/pipeline/lib/shadow-git.sh +45 -4
- package/pipeline/lib/vercel-deploy.sh +10 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
- package/pipeline/multi-agent-refs/readiness-review.md +65 -0
- package/pipeline/scripts/audit-log-rotate.sh +38 -10
- package/pipeline/scripts/check-md-links.mjs +34 -9
- package/pipeline/scripts/diff-risk-score.mjs +4 -1
- package/pipeline/scripts/evidence-gate.mjs +10 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +7 -7
- package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
- package/pipeline/scripts/keychain-save.sh +13 -9
- package/pipeline/scripts/lint-skills.mjs +40 -2
- package/pipeline/scripts/migrate-prefs.mjs +26 -7
- package/pipeline/scripts/phase-tracker.sh +71 -0
- package/pipeline/scripts/pre-commit-check.sh +39 -0
- package/pipeline/scripts/scan-skills.sh +217 -127
- package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
- package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
- package/pipeline/scripts/smoke-generate-issue.sh +3 -3
- package/pipeline/scripts/smoke-install-layout.sh +11 -3
- package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
- package/pipeline/scripts/smoke-pack-contents.sh +140 -0
- package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
- package/pipeline/scripts/smoke-review-readiness.sh +91 -0
- package/pipeline/scripts/smoke-shadow-git.sh +48 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
- package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
- package/pipeline/scripts/smoke-wrapper-preservation.sh +68 -0
- package/pipeline/scripts/test-gap-scan.mjs +12 -1
- package/pipeline/scripts/triage-memory.mjs +10 -1
- package/pipeline/scripts/uninstall.mjs +105 -36
- package/pipeline/scripts/update-issue-progress.sh +60 -41
- package/pipeline/scripts/write-state.mjs +14 -4
- package/pipeline/skills/.skill-manifest.json +13 -5
- package/pipeline/skills/.skills-index.json +20 -2
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +6 -2
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -10
- package/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md +25 -0
- package/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md +25 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/skills-index.md +4 -2
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
.claude/CLAUDE.md 1
|
|
2
2
|
.claude/agents 8
|
|
3
|
-
.claude/commands
|
|
4
|
-
.claude/lib
|
|
3
|
+
.claude/commands 44
|
|
4
|
+
.claude/lib 25
|
|
5
5
|
.claude/multi-agent-preferences.json 1
|
|
6
|
-
.claude/multi-agent-refs
|
|
6
|
+
.claude/multi-agent-refs 51
|
|
7
7
|
.claude/rules 12
|
|
8
8
|
.claude/schemas 23
|
|
9
|
-
.claude/scripts
|
|
9
|
+
.claude/scripts 184
|
|
10
10
|
.claude/settings.json 1
|
|
11
11
|
.claude/skills 428
|
|
12
12
|
.copilot/agents 8
|
|
13
13
|
.copilot/copilot-instructions.md 1
|
|
14
|
-
.copilot/lib
|
|
14
|
+
.copilot/lib 25
|
|
15
15
|
.copilot/schemas 23
|
|
16
|
-
.copilot/scripts
|
|
17
|
-
.copilot/skills
|
|
16
|
+
.copilot/scripts 184
|
|
17
|
+
.copilot/skills 467
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
943
|
|
@@ -17,11 +17,11 @@ fi
|
|
|
17
17
|
echo "Ne tür bir secret kaydedeceksin?"
|
|
18
18
|
echo " 1) Personal Access Token / API Key"
|
|
19
19
|
echo " 2) JSON dosyası (service account vb.)"
|
|
20
|
-
read -
|
|
20
|
+
read -rp "Seçim (1/2): " CHOICE
|
|
21
21
|
|
|
22
22
|
case "$CHOICE" in
|
|
23
23
|
1)
|
|
24
|
-
read -
|
|
24
|
+
read -rsp "Token'ı yapıştır (gizli yazılır): " SECRET
|
|
25
25
|
echo ""
|
|
26
26
|
if [ -z "$SECRET" ]; then
|
|
27
27
|
echo "Hata: Token boş olamaz."
|
|
@@ -29,7 +29,7 @@ case "$CHOICE" in
|
|
|
29
29
|
fi
|
|
30
30
|
;;
|
|
31
31
|
2)
|
|
32
|
-
read -
|
|
32
|
+
read -rp "JSON dosya yolunu gir: " JSON_PATH
|
|
33
33
|
# Kullanıcının girdiği tırnak işaretlerini temizle
|
|
34
34
|
JSON_PATH="${JSON_PATH//\'/}"
|
|
35
35
|
JSON_PATH="${JSON_PATH//\"/}"
|
|
@@ -53,20 +53,24 @@ case "$CHOICE" in
|
|
|
53
53
|
;;
|
|
54
54
|
esac
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
# Locate the credential helper via the resolver so Copilot-only installs work.
|
|
57
|
+
# shellcheck disable=SC1090,SC1091
|
|
58
|
+
. "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
59
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
60
|
+
|| { echo "Hata: credential-store bulunamadi. Kurulum: npx @mmerterden/multi-agent-pipeline install"; exit 1; }
|
|
61
|
+
CRED="$CRED_STORE"
|
|
57
62
|
|
|
58
63
|
"$CRED" delete "$SERVICE_NAME" >/dev/null 2>&1 || true
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
if [ $? -eq 0 ]; then
|
|
64
|
+
# Secret goes through stdin ("set <key> -") so it never appears on argv.
|
|
65
|
+
if printf '%s' "$SECRET" | "$CRED" set "$SERVICE_NAME" -; then
|
|
62
66
|
echo "Kaydedildi: $SERVICE_NAME (platform: $("$CRED" platform))"
|
|
63
67
|
echo ""
|
|
64
68
|
echo "Okumak icin:"
|
|
65
|
-
echo "
|
|
69
|
+
echo " $CRED get \"$SERVICE_NAME\""
|
|
66
70
|
if [ "$CHOICE" = "2" ]; then
|
|
67
71
|
echo ""
|
|
68
72
|
echo "JSON'a geri cevirmek icin:"
|
|
69
|
-
echo "
|
|
73
|
+
echo " $CRED get \"$SERVICE_NAME\" | base64 -d"
|
|
70
74
|
fi
|
|
71
75
|
else
|
|
72
76
|
echo "Hata: Kaydedilemedi."
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Frontmatter linter for every SKILL.md shipped by the pipeline.
|
|
3
3
|
//
|
|
4
|
-
// Until v10.0.0 the
|
|
4
|
+
// Until v10.0.0 the shipped SKILL.md files were counted by smokes but never
|
|
5
5
|
// content-checked, so a skill could ship without the frontmatter that
|
|
6
6
|
// Claude Code / Copilot CLI need to register it. This gate enforces the
|
|
7
7
|
// minimal contract; it is zero-dependency (line-based YAML subset) so it
|
|
8
8
|
// runs inside `npm test` and ci-lite without installs.
|
|
9
9
|
//
|
|
10
|
-
// Contract per SKILL.md:
|
|
10
|
+
// Contract per SKILL.md (errors - fail the gate):
|
|
11
11
|
// - frontmatter block present (--- ... ---) starting at line 1
|
|
12
12
|
// - name: required, lowercase kebab-case (underscores tolerated for
|
|
13
13
|
// legacy skills already published under a snake_case name)
|
|
14
14
|
// - description: required, non-empty, single line, <= 1024 chars
|
|
15
15
|
// - user-invocable: when present, must be true or false
|
|
16
16
|
// - platform: when present, must be ios | android | common | all
|
|
17
|
+
//
|
|
18
|
+
// Spec-conformance signals (warnings - never fail the gate):
|
|
19
|
+
// - name should match the agent-skills spec pattern
|
|
20
|
+
// (lowercase letters/digits/hyphens, <= 64 chars)
|
|
21
|
+
// - description should carry a routing/trigger clause ("use when ...")
|
|
22
|
+
// so the model can decide when to load the skill
|
|
23
|
+
// - a body over ~500 lines with no companion-file reference should be
|
|
24
|
+
// split into references/ files loaded on demand
|
|
17
25
|
|
|
18
26
|
import { readFileSync } from "node:fs";
|
|
19
27
|
import { execSync } from "node:child_process";
|
|
@@ -61,6 +69,16 @@ for (const file of files) {
|
|
|
61
69
|
fail(file, "missing required field: name");
|
|
62
70
|
} else if (!/^[a-z0-9]+([-_][a-z0-9]+)*$/.test(fields.name)) {
|
|
63
71
|
fail(file, `name is not lowercase kebab/snake-case: "${fields.name}"`);
|
|
72
|
+
} else {
|
|
73
|
+
// Spec conformance (warn-only): the agent-skills spec allows lowercase
|
|
74
|
+
// letters, digits, and hyphens, max 64 chars. Snake_case passes the
|
|
75
|
+
// legacy error check above but is out of spec.
|
|
76
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(fields.name)) {
|
|
77
|
+
warn(file, `name is outside the agent-skills spec pattern (hyphens only): "${fields.name}"`);
|
|
78
|
+
}
|
|
79
|
+
if (fields.name.length > 64) {
|
|
80
|
+
warn(file, `name exceeds the spec's 64-char limit (${fields.name.length})`);
|
|
81
|
+
}
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
if (!("description" in fields)) {
|
|
@@ -81,6 +99,26 @@ for (const file of files) {
|
|
|
81
99
|
if (!isBlockScalar && words < 6) {
|
|
82
100
|
warn(file, `description is terse (${words} words) - say what it does AND when to use it`);
|
|
83
101
|
}
|
|
102
|
+
// Routing quality (warn-only): without a trigger clause the model has no
|
|
103
|
+
// signal for WHEN to load the skill. Heuristic phrase match; block
|
|
104
|
+
// scalars are skipped for the same parser reason as above.
|
|
105
|
+
const ROUTING_RE = /\b(use (this|it|when|for|whenever)|when (the|a|an|you|asked|working|building|implementing|writing|reviewing)|triggers?|invoked?( when| for)?|applies when|activated?)\b/i;
|
|
106
|
+
if (!isBlockScalar && !ROUTING_RE.test(desc)) {
|
|
107
|
+
warn(file, "description has no routing/trigger clause (e.g. \"use when ...\")");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Body size (warn-only): the agent-skills guidance is to keep SKILL.md
|
|
112
|
+
// lean and push detail into companion files loaded on demand. A ~500+
|
|
113
|
+
// line body that references no companion file loads whole every time.
|
|
114
|
+
const body = raw.slice(end + 4);
|
|
115
|
+
const bodyLines = body.split("\n").length;
|
|
116
|
+
if (bodyLines > 500) {
|
|
117
|
+
const referencesCompanion =
|
|
118
|
+
/\]\((?!https?:|mailto:|#)[^)]+\)/.test(body) || /`[^`\n]*\/[^`\n]*\.(md|sh|mjs|json|py|txt)`/.test(body);
|
|
119
|
+
if (!referencesCompanion) {
|
|
120
|
+
warn(file, `body is ${bodyLines} lines with no companion-file reference - consider splitting into references/`);
|
|
121
|
+
}
|
|
84
122
|
}
|
|
85
123
|
|
|
86
124
|
if ("user-invocable" in fields && !["true", "false"].includes(fields["user-invocable"])) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// migrate-prefs.mjs - migrates multi-agent-preferences.json
|
|
2
|
+
// migrate-prefs.mjs - migrates multi-agent-preferences.json to the current
|
|
3
|
+
// target schema (see TARGET_VERSION below).
|
|
3
4
|
//
|
|
4
5
|
// Usage:
|
|
5
6
|
// node migrate-prefs.mjs [--dry-run] [--file <path>]
|
|
6
7
|
//
|
|
7
|
-
// - Idempotent: safe to run multiple times.
|
|
8
|
+
// - Idempotent: safe to run multiple times. Target-version files pass through unchanged.
|
|
8
9
|
// - Atomic: writes to a .tmp file and rename()s. No partial writes.
|
|
9
10
|
// - Non-destructive: renames gitIdentities → identities, preserves everything else.
|
|
10
11
|
// - Fills defaults for new v2.1.0 fields.
|
|
@@ -15,15 +16,33 @@ import os from "node:os";
|
|
|
15
16
|
|
|
16
17
|
const DEFAULT_FILE = path.join(os.homedir(), ".claude", "multi-agent-preferences.json");
|
|
17
18
|
|
|
19
|
+
// Single source of truth for the migration target version. Referenced by the
|
|
20
|
+
// migrate() bump chain AND every user-facing message, so they cannot drift.
|
|
21
|
+
const TARGET_VERSION = "2.3.0";
|
|
22
|
+
|
|
23
|
+
const USAGE = "Usage: migrate-prefs.mjs [--dry-run] [--file <path>]";
|
|
24
|
+
|
|
18
25
|
function parseArgs(argv) {
|
|
19
26
|
const args = { file: DEFAULT_FILE, dryRun: false };
|
|
20
27
|
for (let i = 2; i < argv.length; i++) {
|
|
21
28
|
const a = argv[i];
|
|
22
29
|
if (a === "--dry-run") args.dryRun = true;
|
|
23
|
-
else if (a === "--file")
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
else if (a === "--file") {
|
|
31
|
+
const v = argv[++i];
|
|
32
|
+
if (v === undefined) {
|
|
33
|
+
console.error("ERROR: --file requires a path argument");
|
|
34
|
+
console.error(USAGE);
|
|
35
|
+
process.exit(64);
|
|
36
|
+
}
|
|
37
|
+
args.file = v;
|
|
38
|
+
} else if (a === "--help" || a === "-h") {
|
|
39
|
+
console.log(USAGE);
|
|
26
40
|
process.exit(0);
|
|
41
|
+
} else {
|
|
42
|
+
// A typo like --dry-rnu must never fall through to a real write.
|
|
43
|
+
console.error(`ERROR: unknown argument: ${a}`);
|
|
44
|
+
console.error(USAGE);
|
|
45
|
+
process.exit(64);
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
48
|
return args;
|
|
@@ -142,7 +161,7 @@ function migrate(prefs) {
|
|
|
142
161
|
// v2.3.0 is the v7.4.0 target - adds optional `accounts[]` (per-account host
|
|
143
162
|
// overrides) and `recentAccounts[]` (LRU for autopilot account picker).
|
|
144
163
|
// Both default to empty arrays; old prefs continue to validate without them.
|
|
145
|
-
const TARGET =
|
|
164
|
+
const TARGET = TARGET_VERSION;
|
|
146
165
|
if (out.schemaVersion === TARGET) {
|
|
147
166
|
// already on target - skip version bump, but still run sub-migrations below
|
|
148
167
|
} else if (out.schemaVersion === "2.2.0") {
|
|
@@ -371,7 +390,7 @@ function main() {
|
|
|
371
390
|
const { out, changes, alreadyMigrated } = migrate(prefs);
|
|
372
391
|
|
|
373
392
|
if (alreadyMigrated) {
|
|
374
|
-
console.log(`✓ already
|
|
393
|
+
console.log(`✓ already v${TARGET_VERSION} - no changes: ${args.file}`);
|
|
375
394
|
process.exit(0);
|
|
376
395
|
}
|
|
377
396
|
|
|
@@ -289,6 +289,61 @@ save_state() {
|
|
|
289
289
|
mv "${TRACKER_FILE}.tmp" "$TRACKER_FILE"
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
# --- read-modify-write lock ---------------------------------------------------
|
|
293
|
+
# tmp+rename makes each save atomic, but two concurrent invocations (e.g. two
|
|
294
|
+
# parallel Phase 4 reviewers reporting `tokens`) both load the same state and
|
|
295
|
+
# the second save silently drops the first delta. Guard every load->save
|
|
296
|
+
# sequence with a portable mkdir spinlock (macOS bash 3.2: no flock builtin).
|
|
297
|
+
# The wait is bounded and the lock FAILS OPEN with a warning so a stuck lock
|
|
298
|
+
# can never hang the pipeline; a dead owner pid or a lock older than 30s is
|
|
299
|
+
# reclaimed.
|
|
300
|
+
TRACKER_LOCK_DIR=""
|
|
301
|
+
TRACKER_LOCK_HELD=0
|
|
302
|
+
|
|
303
|
+
state_lock_stale() {
|
|
304
|
+
local pid mtime now age
|
|
305
|
+
pid=$(cat "$TRACKER_LOCK_DIR/pid" 2>/dev/null || true)
|
|
306
|
+
if [ -n "$pid" ]; then
|
|
307
|
+
kill -0 "$pid" 2>/dev/null && return 1
|
|
308
|
+
return 0
|
|
309
|
+
fi
|
|
310
|
+
mtime=$(stat -f %m "$TRACKER_LOCK_DIR" 2>/dev/null || stat -c %Y "$TRACKER_LOCK_DIR" 2>/dev/null || echo "")
|
|
311
|
+
[ -z "$mtime" ] && return 1
|
|
312
|
+
now=$(date +%s)
|
|
313
|
+
age=$((now - mtime))
|
|
314
|
+
[ "$age" -gt 30 ]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
acquire_state_lock() {
|
|
318
|
+
TRACKER_LOCK_DIR="${TRACKER_FILE}.lock"
|
|
319
|
+
mkdir -p "$TRACKER_DIR" 2>/dev/null
|
|
320
|
+
local tries=0
|
|
321
|
+
# ~5s bound: 50 tries x 0.1s.
|
|
322
|
+
while ! mkdir "$TRACKER_LOCK_DIR" 2>/dev/null; do
|
|
323
|
+
if state_lock_stale; then
|
|
324
|
+
rm -rf "$TRACKER_LOCK_DIR" 2>/dev/null
|
|
325
|
+
continue
|
|
326
|
+
fi
|
|
327
|
+
tries=$((tries + 1))
|
|
328
|
+
if [ "$tries" -ge 50 ]; then
|
|
329
|
+
echo "phase-tracker: WARN - state lock busy ($TRACKER_LOCK_DIR); proceeding without lock" >&2
|
|
330
|
+
return 0
|
|
331
|
+
fi
|
|
332
|
+
sleep 0.1
|
|
333
|
+
done
|
|
334
|
+
echo "$$" > "$TRACKER_LOCK_DIR/pid" 2>/dev/null
|
|
335
|
+
TRACKER_LOCK_HELD=1
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
release_state_lock() {
|
|
339
|
+
if [ "$TRACKER_LOCK_HELD" = "1" ]; then
|
|
340
|
+
rm -rf "$TRACKER_LOCK_DIR" 2>/dev/null
|
|
341
|
+
TRACKER_LOCK_HELD=0
|
|
342
|
+
fi
|
|
343
|
+
}
|
|
344
|
+
# Never leave a lock behind if a mutating action dies mid-critical-section.
|
|
345
|
+
trap release_state_lock EXIT
|
|
346
|
+
|
|
292
347
|
render() {
|
|
293
348
|
need_jq
|
|
294
349
|
local state
|
|
@@ -509,8 +564,10 @@ case "$ACTION" in
|
|
|
509
564
|
TRACKER_DIR="$(dirname "$TRACKER_FILE")"
|
|
510
565
|
mkdir -p "$TRACKER_DIR" 2>/dev/null
|
|
511
566
|
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
567
|
+
acquire_state_lock
|
|
512
568
|
save_state "$(jq -nc --arg id "$TASK_ID" --arg ts "$NOW" \
|
|
513
569
|
'{task_id:$id,started_at:$ts,phases:[]}')"
|
|
570
|
+
release_state_lock
|
|
514
571
|
# Update pointer so subsequent calls without env can find this tracker.
|
|
515
572
|
# NOTE: the pointer is global; for concurrent runs on the same user account,
|
|
516
573
|
# callers should set MULTI_AGENT_TASK_ID in their shell or prefix every
|
|
@@ -528,6 +585,7 @@ case "$ACTION" in
|
|
|
528
585
|
need_jq
|
|
529
586
|
[ "$#" -ge 2 ] || { echo "add needs <phase_id> <name>" >&2; exit 64; }
|
|
530
587
|
PID="$1"; PNAME="$2"
|
|
588
|
+
acquire_state_lock
|
|
531
589
|
state=$(load_state)
|
|
532
590
|
# Idempotent: a phase id that already exists is a no-op (name, status, and
|
|
533
591
|
# token history are preserved). This is what lets continuation commands
|
|
@@ -538,6 +596,7 @@ case "$ACTION" in
|
|
|
538
596
|
else .phases += [{"id":$id,"name":$name,"status":"pending","subs":[]}]
|
|
539
597
|
end')
|
|
540
598
|
save_state "$new"
|
|
599
|
+
release_state_lock
|
|
541
600
|
render
|
|
542
601
|
;;
|
|
543
602
|
|
|
@@ -550,6 +609,7 @@ case "$ACTION" in
|
|
|
550
609
|
*) echo "bad status: $STATUS" >&2; exit 64 ;;
|
|
551
610
|
esac
|
|
552
611
|
NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
612
|
+
acquire_state_lock
|
|
553
613
|
state=$(load_state)
|
|
554
614
|
# Set status, stamp started_at on first transition to in_progress,
|
|
555
615
|
# stamp completed_at on transition to terminal states (completed/failed/skipped).
|
|
@@ -563,6 +623,7 @@ case "$ACTION" in
|
|
|
563
623
|
)
|
|
564
624
|
')
|
|
565
625
|
save_state "$new"
|
|
626
|
+
release_state_lock
|
|
566
627
|
emit_otel_span "phase.update" "$PID" "$STATUS" "{\"multi_agent.status\": \"$STATUS\"}"
|
|
567
628
|
render
|
|
568
629
|
;;
|
|
@@ -575,6 +636,7 @@ case "$ACTION" in
|
|
|
575
636
|
pending|in_progress|completed|failed|skipped) ;;
|
|
576
637
|
*) echo "bad status: $SSTATUS" >&2; exit 64 ;;
|
|
577
638
|
esac
|
|
639
|
+
acquire_state_lock
|
|
578
640
|
state=$(load_state)
|
|
579
641
|
# If sub with this id exists under this phase, update; else append.
|
|
580
642
|
new=$(echo "$state" | jq \
|
|
@@ -590,6 +652,7 @@ case "$ACTION" in
|
|
|
590
652
|
else . end
|
|
591
653
|
)')
|
|
592
654
|
save_state "$new"
|
|
655
|
+
release_state_lock
|
|
593
656
|
emit_otel_span "phase.sub" "$PID" "$SNAME" "{\"multi_agent.sub_id\": \"$SID\", \"multi_agent.status\": \"$SSTATUS\"}"
|
|
594
657
|
render
|
|
595
658
|
;;
|
|
@@ -602,6 +665,7 @@ case "$ACTION" in
|
|
|
602
665
|
case "$T_IN$T_OUT$T_CACHED" in
|
|
603
666
|
*[!0-9]*) echo "tokens: in/out/cached must be non-negative integers" >&2; exit 64 ;;
|
|
604
667
|
esac
|
|
668
|
+
acquire_state_lock
|
|
605
669
|
state=$(load_state)
|
|
606
670
|
# Additive: add to existing totals so multiple LLM calls accumulate.
|
|
607
671
|
new=$(echo "$state" | jq --arg id "$PID" --argjson ti "$T_IN" --argjson to "$T_OUT" --argjson tc "$T_CACHED" '
|
|
@@ -614,6 +678,7 @@ case "$ACTION" in
|
|
|
614
678
|
)
|
|
615
679
|
')
|
|
616
680
|
save_state "$new"
|
|
681
|
+
release_state_lock
|
|
617
682
|
emit_otel_span "phase.tokens" "$PID" "" "{\"multi_agent.tokens_in_delta\": $T_IN, \"multi_agent.tokens_out_delta\": $T_OUT, \"multi_agent.tokens_cached_delta\": $T_CACHED}"
|
|
618
683
|
# Re-render so live token count is visible on the active phase. Callers
|
|
619
684
|
# that want silent accumulation (e.g. tight loops) can suppress with
|
|
@@ -627,6 +692,7 @@ case "$ACTION" in
|
|
|
627
692
|
need_jq
|
|
628
693
|
[ "$#" -ge 3 ] || { echo "meta needs <phase_id> <key> <value>" >&2; exit 64; }
|
|
629
694
|
PID="$1"; KEY="$2"; VALUE="$3"
|
|
695
|
+
acquire_state_lock
|
|
630
696
|
state=$(load_state)
|
|
631
697
|
new=$(echo "$state" | jq --arg id "$PID" --arg k "$KEY" --arg v "$VALUE" '
|
|
632
698
|
.phases |= map(
|
|
@@ -635,6 +701,7 @@ case "$ACTION" in
|
|
|
635
701
|
else . end
|
|
636
702
|
)')
|
|
637
703
|
save_state "$new"
|
|
704
|
+
release_state_lock
|
|
638
705
|
emit_otel_span "phase.meta" "$PID" "$KEY" "{\"multi_agent.meta_key\": \"$KEY\", \"multi_agent.meta_value\": \"$VALUE\"}"
|
|
639
706
|
if [ "${TRACKER_QUIET:-0}" != "1" ]; then
|
|
640
707
|
render
|
|
@@ -645,12 +712,14 @@ case "$ACTION" in
|
|
|
645
712
|
need_jq
|
|
646
713
|
[ "$#" -ge 2 ] || { echo "model needs <phase_id> <model_name>" >&2; exit 64; }
|
|
647
714
|
PID="$1"; MODEL="$2"
|
|
715
|
+
acquire_state_lock
|
|
648
716
|
state=$(load_state)
|
|
649
717
|
new=$(echo "$state" | jq --arg id "$PID" --arg m "$MODEL" '
|
|
650
718
|
.phases |= map(
|
|
651
719
|
if .id == $id then .model = $m else . end
|
|
652
720
|
)')
|
|
653
721
|
save_state "$new"
|
|
722
|
+
release_state_lock
|
|
654
723
|
emit_otel_span "phase.model" "$PID" "" "{\"multi_agent.model\": \"$MODEL\"}"
|
|
655
724
|
if [ "${TRACKER_QUIET:-0}" != "1" ]; then
|
|
656
725
|
render
|
|
@@ -667,12 +736,14 @@ case "$ACTION" in
|
|
|
667
736
|
if [ "${#TEXT}" -gt 60 ]; then
|
|
668
737
|
TEXT="${TEXT:0:59}…"
|
|
669
738
|
fi
|
|
739
|
+
acquire_state_lock
|
|
670
740
|
state=$(load_state)
|
|
671
741
|
new=$(echo "$state" | jq --arg id "$PID" --arg v "$TEXT" '
|
|
672
742
|
.phases |= map(
|
|
673
743
|
if .id == $id then .meta = ((.meta // {}) + {"Now": $v}) else . end
|
|
674
744
|
)')
|
|
675
745
|
save_state "$new"
|
|
746
|
+
release_state_lock
|
|
676
747
|
emit_otel_span "phase.now" "$PID" "$TEXT" "{}"
|
|
677
748
|
;;
|
|
678
749
|
|
|
@@ -5,6 +5,45 @@
|
|
|
5
5
|
|
|
6
6
|
set -uo pipefail
|
|
7
7
|
|
|
8
|
+
# Hook-mode fast exit. The Claude Code PreToolUse matcher is the tool name
|
|
9
|
+
# ("Bash"), so this script now fires for EVERY Bash call and receives JSON on
|
|
10
|
+
# stdin: {"tool_name":"Bash","tool_input":{"command":"..."}}. Extract the
|
|
11
|
+
# command and exit 0 immediately unless it actually invokes `git commit`.
|
|
12
|
+
# Direct invocations (empty/non-JSON stdin, e.g. smoke tests or manual runs)
|
|
13
|
+
# fall through to the full scan. Extraction failure also falls through - the
|
|
14
|
+
# only cost is an unnecessary scan, never a skipped one on a real commit.
|
|
15
|
+
if [ ! -t 0 ]; then
|
|
16
|
+
HOOK_INPUT="$(cat 2>/dev/null || true)"
|
|
17
|
+
if [ -n "$HOOK_INPUT" ]; then
|
|
18
|
+
HOOK_COMMAND=""
|
|
19
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
20
|
+
HOOK_COMMAND="$(printf '%s' "$HOOK_INPUT" | python3 -c '
|
|
21
|
+
import json, sys
|
|
22
|
+
try:
|
|
23
|
+
print(json.load(sys.stdin).get("tool_input", {}).get("command", ""))
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
' 2>/dev/null || true)"
|
|
27
|
+
elif command -v node >/dev/null 2>&1; then
|
|
28
|
+
HOOK_COMMAND="$(printf '%s' "$HOOK_INPUT" | node -e '
|
|
29
|
+
let raw = "";
|
|
30
|
+
process.stdin.on("data", (c) => (raw += c));
|
|
31
|
+
process.stdin.on("end", () => {
|
|
32
|
+
try {
|
|
33
|
+
process.stdout.write(String(JSON.parse(raw)?.tool_input?.command ?? ""));
|
|
34
|
+
} catch { /* fall through to full scan */ }
|
|
35
|
+
});
|
|
36
|
+
' 2>/dev/null || true)"
|
|
37
|
+
fi
|
|
38
|
+
if [ -n "$HOOK_COMMAND" ]; then
|
|
39
|
+
if ! printf '%s\n' "$HOOK_COMMAND" \
|
|
40
|
+
| grep -qE '(^|[^[:alnum:]_./-])git[[:space:]]+([^|&;]*[[:space:]])?commit([^[:alnum:]_-]|$)'; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
8
47
|
if [ -z "$(git diff --cached --name-only 2>/dev/null)" ]; then
|
|
9
48
|
exit 0
|
|
10
49
|
fi
|