@mmerterden/multi-agent-pipeline 12.4.0 → 12.6.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.md +1 -1
  3. package/docs/features.md +20 -0
  4. package/index.js +7 -1
  5. package/install/_dev-only-files.mjs +1 -0
  6. package/package.json +4 -3
  7. package/pipeline/agents/security-auditor.md +1 -1
  8. package/pipeline/commands/archive-guard.md +5 -5
  9. package/pipeline/commands/multi-agent/SKILL.md +2 -0
  10. package/pipeline/commands/multi-agent/design-check/SKILL.md +287 -0
  11. package/pipeline/commands/multi-agent/help/SKILL.md +45 -5
  12. package/pipeline/commands/multi-agent/refactor/SKILL.md +92 -12
  13. package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
  14. package/pipeline/commands/multi-agent/sync/SKILL.md +119 -12
  15. package/pipeline/commands/multi-agent/test/SKILL.md +1 -1
  16. package/pipeline/commands/sim-test.md +5 -5
  17. package/pipeline/lib/credential-store.sh +32 -0
  18. package/pipeline/lib/extract-conventions.sh +1 -0
  19. package/pipeline/lib/repo-cache.sh +1 -0
  20. package/pipeline/lib/shadow-git.sh +8 -0
  21. package/pipeline/multi-agent-refs/cross-cli-contract.md +3 -3
  22. package/pipeline/multi-agent-refs/phases/phase-0-init.md +9 -1
  23. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +2 -2
  24. package/pipeline/multi-agent-refs/phases/phase-4-review.md +3 -2
  25. package/pipeline/multi-agent-refs/phases/phase-5-test.md +14 -4
  26. package/pipeline/multi-agent-refs/phases.md +2 -0
  27. package/pipeline/multi-agent-refs/prompt-assembly.md +31 -0
  28. package/pipeline/preferences-template.json +18 -1
  29. package/pipeline/schemas/agent-state.schema.json +90 -0
  30. package/pipeline/schemas/design-check-config.schema.json +162 -0
  31. package/pipeline/schemas/learnings-ledger.schema.json +4 -0
  32. package/pipeline/schemas/migrations/state-2.0.0-to-2.1.0.mjs +30 -12
  33. package/pipeline/schemas/prefs.schema.json +161 -5
  34. package/pipeline/schemas/token-budget.json +2 -2
  35. package/pipeline/schemas/triage-corpus.schema.json +5 -1
  36. package/pipeline/scripts/README.md +7 -5
  37. package/pipeline/scripts/classify-plan-safety.mjs +8 -3
  38. package/pipeline/scripts/cost-budget-check.mjs +9 -5
  39. package/pipeline/scripts/eval-mine-corpus.mjs +19 -5
  40. package/pipeline/scripts/fixtures/install-layout.tsv +7 -7
  41. package/pipeline/scripts/gc-worktrees.sh +23 -1
  42. package/pipeline/scripts/learning-curve.mjs +167 -0
  43. package/pipeline/scripts/learnings-ledger.mjs +27 -2
  44. package/pipeline/scripts/lint-mcp-refs.mjs +207 -0
  45. package/pipeline/scripts/memory-load.sh +5 -1
  46. package/pipeline/scripts/render-work-summary.sh +4 -1
  47. package/pipeline/scripts/repo-map.mjs +1 -1
  48. package/pipeline/scripts/smoke-command-inventory.sh +81 -0
  49. package/pipeline/scripts/smoke-commands-skills-parity.sh +1 -1
  50. package/pipeline/scripts/smoke-compliance-skills.sh +4 -4
  51. package/pipeline/scripts/smoke-cross-cli-behavior.sh +12 -2
  52. package/pipeline/scripts/smoke-generate-issue.sh +6 -5
  53. package/pipeline/scripts/smoke-learning-curve.sh +61 -0
  54. package/pipeline/scripts/smoke-per-repo-memory.sh +2 -2
  55. package/pipeline/scripts/smoke-review-readiness.sh +3 -2
  56. package/pipeline/scripts/smoke-schema-validation.sh +19 -5
  57. package/pipeline/scripts/smoke-shadow-git.sh +4 -2
  58. package/pipeline/scripts/triage-memory.mjs +66 -10
  59. package/pipeline/scripts/uninstall.mjs +1 -1
  60. package/pipeline/skills/.skill-manifest.json +24 -8
  61. package/pipeline/skills/.skills-index.json +39 -3
  62. package/pipeline/skills/shared/README.md +10 -6
  63. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +10 -8
  64. package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +248 -0
  65. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +22 -0
  66. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +67 -11
  67. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +1 -1
  68. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +51 -9
  69. package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +1 -1
  70. package/pipeline/skills/skills-index.md +8 -4
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-command-inventory.sh - the command inventory is DERIVED, never asserted.
3
+ #
4
+ # Every count that describes "how many multi-agent commands exist" used to be a
5
+ # literal in prose (contract header, sync skill, dispatcher). Adding a command
6
+ # left those literals stale while every gate stayed green, because the gates
7
+ # asserted the literal instead of the tree. This gate computes the count from
8
+ # pipeline/commands/multi-agent/*/ and requires the prose to agree.
9
+ #
10
+ # Verifies:
11
+ # 1. contract header count == number of command directories
12
+ # 2. every command directory appears in the contract inventory block
13
+ # 3. both sync surfaces (command + Copilot skill) carry the same count twice
14
+ # ("N commands are synced" + "instructions + N sub-command skills")
15
+ # 4. every command directory appears in both sync inventory blocks
16
+ # 5. every command directory has its shared/core skill counterpart
17
+ #
18
+ # Exit: 0 all green, 1 any failure.
19
+
20
+ set -euo pipefail
21
+
22
+ HERE="$(cd "$(dirname "$0")" && pwd)"
23
+ ROOT="$(cd "$HERE/../.." && pwd)"
24
+ CMD_DIR="$ROOT/pipeline/commands/multi-agent"
25
+ CONTRACT="$ROOT/pipeline/multi-agent-refs/cross-cli-contract.md"
26
+ SYNC="$ROOT/pipeline/commands/multi-agent/sync/SKILL.md"
27
+ SYNC_SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-sync/SKILL.md"
28
+ SKILLS_DIR="$ROOT/pipeline/skills/shared/core"
29
+
30
+ PASS=0
31
+ FAIL=0
32
+ pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
33
+ fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
34
+
35
+ echo "══ smoke-command-inventory ══"
36
+
37
+ for f in "$CONTRACT" "$SYNC" "$SYNC_SKILL"; do
38
+ [ -f "$f" ] || { echo " ✗ missing file: ${f#$ROOT/}"; exit 1; }
39
+ done
40
+
41
+ COMMANDS=$(find "$CMD_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
42
+ N=$(printf '%s\n' "$COMMANDS" | grep -c . || true)
43
+
44
+ echo "→ 1. derived count from the command tree: $N commands"
45
+ [ "$N" -gt 0 ] && pass "found $N command directories" || fail "no command directories under ${CMD_DIR#$ROOT/}"
46
+
47
+ echo "→ 2. contract header agrees"
48
+ if grep -Fq "## 1. Command Inventory ($N commands)" "$CONTRACT"; then
49
+ pass "contract header = $N"
50
+ else
51
+ fail "contract header does not say ($N commands): $(grep -o '## 1\. Command Inventory ([0-9]* commands)' "$CONTRACT" || echo 'header not found')"
52
+ fi
53
+
54
+ echo "→ 3. sync surfaces agree (2 counts x 2 files)"
55
+ for f in "$SYNC" "$SYNC_SKILL"; do
56
+ rel="${f#$ROOT/}"
57
+ grep -Fq "**$N commands are synced**" "$f" && pass "$rel: synced count = $N" || fail "$rel: 'N commands are synced' != $N"
58
+ grep -Fq "instructions + $N sub-command skills" "$f" && pass "$rel: step-2 count = $N" || fail "$rel: 'instructions + N sub-command skills' != $N"
59
+ done
60
+
61
+ echo "→ 4. every command is listed in the contract + both sync inventories"
62
+ MISSING_LIST=0
63
+ while IFS= read -r cmd; do
64
+ [ -n "$cmd" ] || continue
65
+ for f in "$CONTRACT" "$SYNC" "$SYNC_SKILL"; do
66
+ grep -Eq "(^|[ ,])$cmd([ ,]|$)" "$f" || { fail "${f#$ROOT/}: inventory missing '$cmd'"; MISSING_LIST=1; }
67
+ done
68
+ done <<< "$COMMANDS"
69
+ [ "$MISSING_LIST" -eq 0 ] && pass "all $N commands listed on all 3 surfaces"
70
+
71
+ echo "→ 5. shared/core skill counterpart exists for every command"
72
+ MISSING_SKILL=0
73
+ while IFS= read -r cmd; do
74
+ [ -n "$cmd" ] || continue
75
+ [ -f "$SKILLS_DIR/multi-agent-$cmd/SKILL.md" ] || { fail "no shared/core skill for '$cmd'"; MISSING_SKILL=1; }
76
+ done <<< "$COMMANDS"
77
+ [ "$MISSING_SKILL" -eq 0 ] && pass "all $N commands have a shared/core skill"
78
+
79
+ echo ""
80
+ echo "══ command-inventory smoke: $PASS passed, $FAIL failed ══"
81
+ [ "$FAIL" -eq 0 ] || exit 1
@@ -36,7 +36,7 @@ fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
36
36
  # Underscore-prefixed files (e.g. _account-picker.md) are internal fragments
37
37
  # loaded by other commands via Read; they're not invocable slash commands and
38
38
  # intentionally have no matching skill.
39
- CMDS=$(find "$CMDS_DIR" -mindepth 2 -maxdepth 2 -name "SKILL.md" -exec dirname {} \; | xargs -n1 basename | sort)
39
+ CMDS=$(find "$CMDS_DIR" -mindepth 2 -maxdepth 2 -name "SKILL.md" -exec sh -c 'for f do basename "$(dirname "$f")"; done' sh {} + | sort)
40
40
 
41
41
  # help.md in commands maps to multi-agent-help skill; the main "multi-agent"
42
42
  # skill covers the behavior documented across multiple command files, so we
@@ -5,7 +5,7 @@
5
5
  # that the 4 claimed consumer surfaces actually reference them (not just promise).
6
6
  #
7
7
  # Covers:
8
- # 1. apple-archive-compliance/SKILL.md exists, has frontmatter, 17-rule catalog,
8
+ # 1. apple-archive-compliance/SKILL.md exists, has frontmatter, 18-rule catalog,
9
9
  # EN + TR humanizer blocks, severity table, prerequisite detection.
10
10
  # 2. google-play-compliance/SKILL.md exists, has frontmatter, 21-rule catalog
11
11
  # across 4 categories, EN + TR humanizer blocks, severity table, prereq detection.
@@ -43,8 +43,8 @@ grep -q "^name: apple-archive-compliance$" "$APPLE_SKILL" && pass "apple frontma
43
43
  grep -q "^user-invocable: true$" "$APPLE_SKILL" && pass "apple user-invocable" || fail "apple user-invocable"
44
44
  grep -q "argument-hint:" "$APPLE_SKILL" && pass "apple argument-hint" || fail "apple argument-hint"
45
45
  # Count rule rows in the catalog table (excluding header + separator)
46
- APPLE_RULES=$(awk '/^## 17-Rule Catalog/,/^## Severity mapping/' "$APPLE_SKILL" | grep -cE '^\| *[0-9]+ *\|' || true)
47
- [ "$APPLE_RULES" -eq 17 ] && pass "apple catalog has 17 rule rows" || fail "apple catalog has $APPLE_RULES rule rows (want 17)"
46
+ APPLE_RULES=$(awk '/^## 18-Rule Catalog/,/^## Severity mapping/' "$APPLE_SKILL" | grep -cE '^\| *[0-9]+ *\|' || true)
47
+ [ "$APPLE_RULES" -eq 18 ] && pass "apple catalog has 18 rule rows" || fail "apple catalog has $APPLE_RULES rule rows (want 18)"
48
48
 
49
49
  echo "→ 2. apple skill humanizer blocks EN + TR + severity table"
50
50
  # v8.2 split humanizer language from picker language: gating moved from
@@ -59,7 +59,7 @@ echo "→ 3. apple skill prerequisite detection + archive resolution (v8.4.0+ us
59
59
  grep -q 'ios_app_store_audit' "$APPLE_SKILL" && pass "apple references ios_app_store_audit MCP tool" || fail "apple ios_app_store_audit reference missing"
60
60
  grep -q '.xcarchive' "$APPLE_SKILL" && pass "apple references .xcarchive" || fail "apple .xcarchive missing"
61
61
  grep -q '@mmerterden/dev-toolkit-mcp' "$APPLE_SKILL" && pass "apple references @mmerterden/dev-toolkit-mcp package" || fail "apple dev-toolkit-mcp reference missing"
62
- grep -q 'mcp__dev_toolkit__ios_app_store_audit' "$APPLE_SKILL" && pass "apple shows MCP-call invocation form" || fail "apple MCP-call invocation form missing"
62
+ grep -q 'mcp__dev-toolkit__ios_app_store_audit' "$APPLE_SKILL" && pass "apple shows MCP-call invocation form" || fail "apple MCP-call invocation form missing"
63
63
 
64
64
  echo "→ 4. google-play-compliance SKILL.md frontmatter + catalog shape"
65
65
  grep -q "^name: google-play-compliance$" "$GOOGLE_SKILL" && pass "google frontmatter name" || fail "google frontmatter name"
@@ -61,6 +61,16 @@ fi
61
61
  # ──────────────────────────────────────────────────────────────────────────
62
62
  echo ""
63
63
  echo "→ 2. No deprecated placeholders in Claude source"
64
+ # Corpus guard: the scans below can only be trusted if there is something to
65
+ # scan. Without this, an empty or missing command tree reports every pattern
66
+ # clean instead of reporting that it found nothing to check.
67
+ SKILL_COUNT=$(find "$CLAUDE_CMDS" -name SKILL.md 2>/dev/null | wc -l | tr -d ' ')
68
+ if [ "$SKILL_COUNT" -gt 0 ]; then
69
+ pass "command corpus non-empty ($SKILL_COUNT SKILL.md)"
70
+ else
71
+ fail "no SKILL.md under $CLAUDE_CMDS - placeholder and hardcode scans below are vacuous"
72
+ fi
73
+
64
74
  # Per refs/cross-cli-contract.md Section 2.5
65
75
  DEPRECATED=(
66
76
  "{github-username}"
@@ -72,7 +82,7 @@ DEPRECATED=(
72
82
  )
73
83
  for p in "${DEPRECATED[@]}"; do
74
84
  # grep -l returns filenames; empty = no matches
75
- FOUND=$(grep -l -F -- "$p" $(find "$CLAUDE_CMDS" -name SKILL.md 2>/dev/null) 2>/dev/null || true)
85
+ FOUND=$(find "$CLAUDE_CMDS" -name SKILL.md -exec grep -l -F -- "$p" {} + 2>/dev/null || true)
76
86
  if [ -z "$FOUND" ]; then
77
87
  pass "no '$p' in Claude commands"
78
88
  else
@@ -88,7 +98,7 @@ PERSONAL_HARDCODES=(
88
98
  "${USER}_tkgithub"
89
99
  )
90
100
  for p in "${PERSONAL_HARDCODES[@]}"; do
91
- FOUND=$(grep -l -F -- "$p" $(find "$CLAUDE_CMDS" -name SKILL.md 2>/dev/null) 2>/dev/null || true)
101
+ FOUND=$(find "$CLAUDE_CMDS" -name SKILL.md -exec grep -l -F -- "$p" {} + 2>/dev/null || true)
92
102
  if [ -z "$FOUND" ]; then
93
103
  pass "no '$p' leaked into Claude commands"
94
104
  else
@@ -9,7 +9,7 @@
9
9
  # 5. Convention mining + createmeta + Agile sprint endpoints present.
10
10
  # 6. UTF-8 verbatim POST path (--rawfile + --data-binary) present.
11
11
  # 7. Standard template vocab: Task/Story (Scope/Acceptance/Test Scenarios), Bug (Steps/Expected/Actual/Environment), Swagger.
12
- # 8. Registration: dispatcher rows, inventory lists, count consistency (36).
12
+ # 8. Registration: dispatcher rows, inventory lists, count derived from the command tree.
13
13
  # 9. Forbidden strings absent (em-dash, section sign, the M-word, unicode_escape).
14
14
 
15
15
  set -euo pipefail
@@ -21,6 +21,7 @@ REF="$ROOT/pipeline/multi-agent-refs/generate-issue.md"
21
21
  SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md"
22
22
  DISPATCHER="$ROOT/pipeline/commands/multi-agent/SKILL.md"
23
23
  CONTRACT="$ROOT/pipeline/multi-agent-refs/cross-cli-contract.md"
24
+ CMD_COUNT=$(find "$ROOT/pipeline/commands/multi-agent" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
24
25
  SYNC="$ROOT/pipeline/commands/multi-agent/sync/SKILL.md"
25
26
  HELP="$ROOT/pipeline/commands/multi-agent/help/SKILL.md"
26
27
 
@@ -88,14 +89,14 @@ grep -Fqi "auto-sizing" "$REF" && pass "auto-sizing rule present" || fail "auto-
88
89
  grep -Fqi "swagger" "$REF" && pass "swagger context present" || fail "swagger context missing"
89
90
 
90
91
  echo ""
91
- echo "→ 8. registration + count consistency (36)"
92
+ echo "→ 8. registration + count consistency (derived)"
92
93
  grep -Fq '| `create-jira ' "$DISPATCHER" && pass "dispatcher routing: create-jira" || fail "dispatcher routing missing: generate"
93
94
  grep -Fq "create-jira/SKILL.md" "$DISPATCHER" && pass "dispatcher loading row" || fail "dispatcher loading row missing"
94
- grep -Fq "## 1. Command Inventory (41 commands)" "$CONTRACT" && pass "contract count = 41" || fail "contract count wrong"
95
+ grep -Fq "## 1. Command Inventory ($CMD_COUNT commands)" "$CONTRACT" && pass "contract count = $CMD_COUNT (derived)" || fail "contract count wrong"
95
96
  grep -Eq '(^|[ ,])create-jira([ ,]|$)' "$CONTRACT" && pass "contract list has create-jira" || fail "contract list missing generate"
96
- grep -Fq "**41 commands are synced**" "$SYNC" && pass "sync count = 41" || fail "sync count wrong"
97
+ grep -Fq "**$CMD_COUNT commands are synced**" "$SYNC" && pass "sync count = $CMD_COUNT (derived)" || fail "sync count wrong"
97
98
  SYNC_SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-sync/SKILL.md"
98
- grep -Fq "**41 commands are synced**" "$SYNC_SKILL" && pass "sync SKILL count = 41" || fail "sync SKILL count wrong (Copilot parity)"
99
+ grep -Fq "**$CMD_COUNT commands are synced**" "$SYNC_SKILL" && pass "sync SKILL count = $CMD_COUNT (derived)" || fail "sync SKILL count wrong (Copilot parity)"
99
100
  grep -Fq "create-jira" "$ROOT/pipeline/skills/shared/core/multi-agent-help/SKILL.md" && pass "help SKILL lists create-jira" || fail "help SKILL missing create-jira (Copilot parity)"
100
101
  grep -c "multi-agent:create-jira" "$HELP" | awk '{exit !($1>=2)}' && pass "help.md has EN + TR entries" || fail "help.md entries incomplete"
101
102
  # no stale split-command references anywhere in the synced surfaces
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-learning-curve.sh - contract for learning-curve.mjs (time-bucketed
3
+ # trend over metrics.jsonl).
4
+
5
+ set -uo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ LC="$SCRIPT_DIR/learning-curve.mjs"
9
+
10
+ PASS=0
11
+ FAIL=0
12
+ record_pass() { PASS=$((PASS + 1)); printf ' \342\234\223 %s\n' "$1"; }
13
+ record_fail() { FAIL=$((FAIL + 1)); printf ' \342\234\227 %s\n' "$1"; }
14
+
15
+ printf '\342\206\222 smoke-learning-curve: metrics trend contract\n'
16
+
17
+ tmp=$(mktemp -d "${TMPDIR:-/tmp}/lc.XXXXXX")
18
+ trap 'rm -rf "$tmp"' EXIT
19
+ M="$tmp/metrics.jsonl"
20
+
21
+ # Two early tasks (noisy: 3/2 review cycles, low cache) + two later tasks
22
+ # (clean: 1 cycle, high cache) -> the curve should improve.
23
+ cat > "$M" <<'EOF'
24
+ {"ts":"2026-06-02T10:00:00Z","task_id":"t1","phase":"4","event":"review.completed","details":{"review_cycles":3,"tokens_in":100000,"tokens_out":8000,"tokens_cached":10000}}
25
+ {"ts":"2026-06-02T10:05:00Z","task_id":"t1","phase":"3","event":"rework.started","details":{}}
26
+ {"ts":"2026-06-03T09:00:00Z","task_id":"t2","phase":"4","event":"review.completed","details":{"review_cycles":2,"tokens_in":90000,"tokens_out":7000,"tokens_cached":20000}}
27
+ {"ts":"2026-06-24T09:00:00Z","task_id":"t3","phase":"4","event":"review.completed","details":{"review_cycles":1,"tokens_in":40000,"tokens_out":4000,"tokens_cached":30000}}
28
+ {"ts":"2026-06-25T09:00:00Z","task_id":"t4","phase":"4","event":"review.completed","details":{"review_cycles":1,"tokens_in":38000,"tokens_out":3500,"tokens_cached":32000}}
29
+ EOF
30
+
31
+ # 1. parses
32
+ bash -n "$SCRIPT_DIR/smoke-learning-curve.sh" >/dev/null 2>&1 || true
33
+ if node --check "$LC" 2>/dev/null; then record_pass "learning-curve.mjs parses"; else record_fail "syntax error"; fi
34
+
35
+ # 2. JSON: buckets + improving trend
36
+ out=$(METRICS_FILE="$M" node "$LC" --json 2>/dev/null)
37
+ nbuckets=$(printf '%s' "$out" | python3 -c "import json,sys;print(len(json.load(sys.stdin)['buckets']))")
38
+ [ "$nbuckets" -ge 2 ] && record_pass "produces >=2 time buckets" || record_fail "expected >=2 buckets, got $nbuckets"
39
+
40
+ tokens_delta=$(printf '%s' "$out" | python3 -c "import json,sys;print(json.load(sys.stdin)['trend']['tokensPerTask'])")
41
+ case "$tokens_delta" in
42
+ -*) record_pass "tokens/task trend is negative (improving)" ;;
43
+ *) record_fail "tokens/task trend should drop, got $tokens_delta" ;;
44
+ esac
45
+ cache_delta=$(printf '%s' "$out" | python3 -c "import json,sys;print(json.load(sys.stdin)['trend']['cacheRatio'])")
46
+ case "$cache_delta" in
47
+ -*|0|0.0) record_fail "cache ratio should rise, got $cache_delta" ;;
48
+ *) record_pass "cache ratio trend rises (improving)" ;;
49
+ esac
50
+
51
+ # 3. markdown renders a table + trend
52
+ md=$(METRICS_FILE="$M" node "$LC" 2>/dev/null)
53
+ printf '%s' "$md" | grep -q "Learning curve" && record_pass "markdown has a header" || record_fail "no markdown header"
54
+ printf '%s' "$md" | grep -q "Trend" && record_pass "markdown shows a trend block" || record_fail "no trend block"
55
+
56
+ # 4. missing metrics -> graceful, exit 0
57
+ METRICS_FILE="/nope/none.jsonl" node "$LC" --json >/dev/null 2>&1
58
+ [ "$?" -eq 0 ] && record_pass "missing metrics is a graceful no-op (exit 0)" || record_fail "missing metrics should exit 0"
59
+
60
+ printf '\n\342\225\220\342\225\220 smoke-learning-curve: %d passed, %d failed \342\225\220\342\225\220\n' "$PASS" "$FAIL"
61
+ [ "$FAIL" -eq 0 ]
@@ -35,7 +35,7 @@ repo="$sandbox/repo"
35
35
  mkdir -p "$repo"
36
36
 
37
37
  # --- 1: pref off → silent exit 0 ---
38
- printf '{"global":{"perRepoMemory":false}}' > "$HOME/.claude/preferences.json"
38
+ printf '{"global":{"perRepoMemory":false}}' > "$HOME/.claude/multi-agent-preferences.json"
39
39
  out=$(bash "$LOAD" "$repo" 2>&1 || true)
40
40
  if [ -z "$out" ]; then
41
41
  record_pass "load exits silent when pref off"
@@ -44,7 +44,7 @@ else
44
44
  fi
45
45
 
46
46
  # --- 2: pref on + no memory → silent exit 0 ---
47
- printf '{"global":{"perRepoMemory":true}}' > "$HOME/.claude/preferences.json"
47
+ printf '{"global":{"perRepoMemory":true}}' > "$HOME/.claude/multi-agent-preferences.json"
48
48
  out=$(bash "$LOAD" "$repo" 2>&1 || true)
49
49
  if [ -z "$out" ]; then
50
50
  record_pass "load exits silent when pref on but no memory present"
@@ -21,6 +21,7 @@ SK_J="$ROOT/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md"
21
21
  SK_I="$ROOT/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md"
22
22
  DISPATCHER="$ROOT/pipeline/commands/multi-agent/SKILL.md"
23
23
  CONTRACT="$ROOT/pipeline/multi-agent-refs/cross-cli-contract.md"
24
+ CMD_COUNT=$(find "$ROOT/pipeline/commands/multi-agent" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
24
25
  HELP="$ROOT/pipeline/commands/multi-agent/help/SKILL.md"
25
26
 
26
27
  PASS=0; FAIL=0
@@ -61,12 +62,12 @@ grep -Fqi "confirm" "$REF" && pass "ref has confirm-before-post gate" || fail "r
61
62
  grep -Fqi "rubric" "$REF" && pass "ref has readiness rubric" || fail "ref missing rubric"
62
63
 
63
64
  echo ""
64
- echo "→ 5. registration + count (38)"
65
+ echo "→ 5. registration + count (derived)"
65
66
  grep -Fq '| `review-jira ' "$DISPATCHER" && pass "dispatcher desc: review-jira" || fail "dispatcher desc missing review-jira"
66
67
  grep -Fq '| `review-issue ' "$DISPATCHER" && pass "dispatcher desc: review-issue" || fail "dispatcher desc missing review-issue"
67
68
  grep -Fq "review-jira/SKILL.md" "$DISPATCHER" && pass "dispatcher routing: review-jira" || fail "dispatcher routing missing review-jira"
68
69
  grep -Fq "review-issue/SKILL.md" "$DISPATCHER" && pass "dispatcher routing: review-issue" || fail "dispatcher routing missing review-issue"
69
- grep -Fq "## 1. Command Inventory (41 commands)" "$CONTRACT" && pass "contract count = 41" || fail "contract count wrong"
70
+ grep -Fq "## 1. Command Inventory ($CMD_COUNT commands)" "$CONTRACT" && pass "contract count = $CMD_COUNT (derived)" || fail "contract count wrong"
70
71
  grep -Eq '(^|[ ,])review-jira([ ,]|$)' "$CONTRACT" && pass "contract inventory has review-jira" || fail "contract missing review-jira"
71
72
  grep -Eq '(^|[ ,])review-issue([ ,]|$)' "$CONTRACT" && pass "contract inventory has review-issue" || fail "contract missing review-issue"
72
73
  grep -Fq "multi-agent:review-jira" "$HELP" && pass "help lists review-jira" || fail "help missing review-jira"
@@ -87,17 +87,31 @@ done
87
87
 
88
88
  # ──────────────────────────────────────────────────────────────────────────
89
89
  echo ""
90
- echo "→ 4. prefs.schema.json - keychainMapping has 12 services"
90
+ echo "→ 4. prefs.schema.json - keychainMapping covers every resolved service"
91
+ # A floor, not a pin: new credentials get added over time, and pinning the count
92
+ # made every addition a false failure. What matters is that the keys shipped
93
+ # flows resolve are all declared.
91
94
  KM_COUNT=$(node -e "
92
95
  const s = JSON.parse(require('fs').readFileSync('$PREFS_SCHEMA', 'utf8'));
93
96
  const km = s.properties.global.properties.keychainMapping.properties;
94
97
  console.log(Object.keys(km).length);
95
98
  " 2>/dev/null)
96
- EXPECTED_SERVICES=13 # 12 + legacy "bitbucket" alias
97
- if [ "$KM_COUNT" = "$EXPECTED_SERVICES" ] || [ "$KM_COUNT" = "12" ]; then
98
- pass "keychainMapping has $KM_COUNT services (≥12 expected)"
99
+ if [ "${KM_COUNT:-0}" -ge 12 ] 2>/dev/null; then
100
+ pass "keychainMapping declares $KM_COUNT services (≥12 expected)"
99
101
  else
100
- fail "keychainMapping has $KM_COUNT services, expected ≥12"
102
+ fail "keychainMapping declares ${KM_COUNT:-0} services, expected ≥12"
103
+ fi
104
+
105
+ MISSING_KEYS=$(node -e "
106
+ const s = JSON.parse(require('fs').readFileSync('$PREFS_SCHEMA', 'utf8'));
107
+ const km = Object.keys(s.properties.global.properties.keychainMapping.properties);
108
+ const required = ['jira','bitbucket_token','bitbucket_user','github','confluence','figma','figma_pat','figma_mcp','firebase','npm'];
109
+ console.log(required.filter((k) => !km.includes(k)).join(','));
110
+ " 2>/dev/null)
111
+ if [ -z "$MISSING_KEYS" ]; then
112
+ pass "every credential the pipeline resolves is declared"
113
+ else
114
+ fail "keychainMapping missing declared-and-used key(s): $MISSING_KEYS"
101
115
  fi
102
116
 
103
117
  # ──────────────────────────────────────────────────────────────────────────
@@ -59,7 +59,9 @@ fi
59
59
 
60
60
  # 4-9. Lifecycle on a temp project
61
61
  TMP=$(mktemp -d)
62
- cd "$TMP"
62
+ # A failed cd here would run the relative `rm -rf DerivedData` below in the
63
+ # caller's directory instead of the sandbox.
64
+ cd "$TMP" || { record_fail "cannot cd into sandbox $TMP"; exit 1; }
63
65
  git init --quiet
64
66
  echo "node_modules/" > .gitignore
65
67
  echo "hello" > a.txt
@@ -178,7 +180,7 @@ else
178
180
  record_fail "~/.claude/state is GONE after unsafe prune attempts"
179
181
  fi
180
182
 
181
- cd - >/dev/null
183
+ cd - >/dev/null || true
182
184
  rm -rf "$TMP"
183
185
 
184
186
  # 10-12. Prefs schema
@@ -63,9 +63,27 @@ function die(msg, code = 1) {
63
63
  process.exit(code);
64
64
  }
65
65
 
66
+ // Resolves to the MAIN repository name even when called from inside a pipeline
67
+ // worktree. `--show-toplevel` returns the worktree root, whose basename is the
68
+ // task id, which would shard the per-repo store one directory per task and make
69
+ // prior-art recall always miss. `--git-common-dir` points at the main
70
+ // repository's .git for every linked worktree.
66
71
  function repoSlug() {
67
72
  if (flags["repo-slug"]) return String(flags["repo-slug"]);
68
73
  if (process.env.MULTI_AGENT_REPO_SLUG) return process.env.MULTI_AGENT_REPO_SLUG;
74
+ try {
75
+ const commonDir = execFileSync(
76
+ "git",
77
+ ["rev-parse", "--path-format=absolute", "--git-common-dir"],
78
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
79
+ ).trim();
80
+ if (commonDir) {
81
+ const base = basename(commonDir);
82
+ const root = base === ".git" ? dirname(commonDir) : commonDir.replace(/\.git$/, "");
83
+ const slug = basename(root);
84
+ if (slug) return slug;
85
+ }
86
+ } catch { /* not a git repo, or git too old for --path-format */ }
69
87
  try {
70
88
  const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
71
89
  encoding: "utf8",
@@ -84,6 +102,33 @@ function ensureDir(p) {
84
102
  mkdirSync(dirname(p), { recursive: true });
85
103
  }
86
104
 
105
+ // Freshness gate (P1): stamp each finding with the git blob SHA of its file at
106
+ // ingest time; on query, if the file's current content hash differs, the hit is
107
+ // flagged stale so a reused finding is verified, not trusted blindly.
108
+ function repoTop() {
109
+ try {
110
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
111
+ encoding: "utf8",
112
+ stdio: ["ignore", "pipe", "ignore"],
113
+ }).trim() || null;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ function fileSha(file, top) {
120
+ if (!file || !top) return null;
121
+ try {
122
+ return execFileSync("git", ["hash-object", "--", file], {
123
+ cwd: top,
124
+ encoding: "utf8",
125
+ stdio: ["ignore", "pipe", "ignore"],
126
+ }).trim() || null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
87
132
  const STOP = new Set([
88
133
  "the","a","an","and","or","but","of","in","on","at","to","for","with","by","is","are",
89
134
  "be","been","being","this","that","these","those","it","its","as","not","no","do","does",
@@ -157,6 +202,7 @@ function ingest() {
157
202
  catch (e) { die(`cannot parse triage JSON: ${e.message}`, 1); }
158
203
 
159
204
  const ts = new Date().toISOString();
205
+ const top = repoTop();
160
206
  const corpus = readCorpus(slug);
161
207
  const seen = new Set(corpus.map((r) => `${r.task_id}::${r.classification}::${r.file}::${r.issue}`));
162
208
 
@@ -188,6 +234,7 @@ function ingest() {
188
234
  fix: f.fix || null,
189
235
  reason: f._reason || null,
190
236
  reviewer: f.reviewer || null,
237
+ file_sha: fileSha(file, top),
191
238
  };
192
239
  writeRow(slug, row);
193
240
  seen.add(key);
@@ -212,21 +259,30 @@ function query() {
212
259
  const qTokens = tokenize(issueText);
213
260
  const fileRe = fileGlob ? globToRegExp(fileGlob) : null;
214
261
  const filtered = fileRe ? corpus.filter((r) => fileRe.test(r.file || "")) : corpus.slice();
262
+ const top = repoTop();
215
263
  const ranked = filtered
216
264
  .map((r) => ({ row: r, score: score(qTokens, r) }))
217
265
  .filter((x) => x.score > 0 || qTokens.size === 0)
218
266
  .sort((a, b) => b.score - a.score)
219
267
  .slice(0, topN)
220
- .map(({ row, score }) => ({
221
- taskId: row.task_id,
222
- classification: row.classification,
223
- severity: row.severity,
224
- file: row.file,
225
- line: row.line,
226
- issue: row.issue,
227
- reason: row.reason,
228
- score: Math.round(score * 1000) / 1000,
229
- }));
268
+ .map(({ row, score }) => {
269
+ // Freshness: null when we cannot compare (no stored sha, or file gone /
270
+ // not in a git repo); true means the file changed since the finding was
271
+ // recorded, so verify before reusing.
272
+ const cur = row.file_sha ? fileSha(row.file, top) : null;
273
+ const stale = row.file_sha && cur ? cur !== row.file_sha : null;
274
+ return {
275
+ taskId: row.task_id,
276
+ classification: row.classification,
277
+ severity: row.severity,
278
+ file: row.file,
279
+ line: row.line,
280
+ issue: row.issue,
281
+ reason: row.reason,
282
+ stale,
283
+ score: Math.round(score * 1000) / 1000,
284
+ };
285
+ });
230
286
  process.stdout.write(JSON.stringify({ ok: true, slug, total: filtered.length, hits: ranked }) + "\n");
231
287
  process.exit(0);
232
288
  }
@@ -305,7 +305,7 @@ async function confirm(question) {
305
305
  });
306
306
  }
307
307
 
308
- async function main() {
308
+ export async function main() {
309
309
  console.log("");
310
310
  console.log(" multi-agent-pipeline uninstaller");
311
311
  console.log(" ================================");
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-07-20T07:34:26Z",
4
- "skillCount": 189,
3
+ "generatedAt": "2026-07-25T10:41:38Z",
4
+ "skillCount": 193,
5
5
  "entries": [
6
6
  {
7
7
  "path": "shared/core/apple-archive-compliance/SKILL.md",
8
- "sha256": "38cb4c257f7b2915d1784ec6f355b9a7f14bb32427e09c01872d3c8d6a039a71"
8
+ "sha256": "d3826905606b6ec7d86d3434b03bfdbbd52a424f7108e58ca12a8cb490f1e4ad"
9
9
  },
10
10
  {
11
11
  "path": "shared/core/google-play-compliance/SKILL.md",
@@ -35,6 +35,10 @@
35
35
  "path": "shared/core/multi-agent-create-jira/SKILL.md",
36
36
  "sha256": "40caf0489ad8322cd00d792a244e772d4ddb6ccfd7c3807729d6b17590e7f81c"
37
37
  },
38
+ {
39
+ "path": "shared/core/multi-agent-design-check/SKILL.md",
40
+ "sha256": "a004cf24661789d2a3595c5246dfebe00b76aaee6ece057b458748f502a8cd5c"
41
+ },
38
42
  {
39
43
  "path": "shared/core/multi-agent-dev-autopilot/SKILL.md",
40
44
  "sha256": "82b1cf4a4bbd84fcfef54fcdbcdefa27f2747df6d4fac5c86b42e861013c6d58"
@@ -59,13 +63,17 @@
59
63
  "path": "shared/core/multi-agent-finish/SKILL.md",
60
64
  "sha256": "3e8e430f7aee626be96129a9d0ae6c9ac5ef0dbf80cbe580dde2d7fb5fccfed1"
61
65
  },
66
+ {
67
+ "path": "shared/core/multi-agent-forget/SKILL.md",
68
+ "sha256": "68f6a92a00844ed34c336548888d3e623534c12b5e85afe5762e7a9c8d41aae4"
69
+ },
62
70
  {
63
71
  "path": "shared/core/multi-agent-garbage-collect/SKILL.md",
64
72
  "sha256": "24736f163e645e2bee7d552e555ae6e32cd3fc3d15c844813a75b908a58d52fd"
65
73
  },
66
74
  {
67
75
  "path": "shared/core/multi-agent-help/SKILL.md",
68
- "sha256": "00772bab27906cdbdcae72d115c4e1f12ddd914fa7aa274923618a3a906e6caa"
76
+ "sha256": "330780854ab3b48f788e34f918ecad585bd46ffbb44f14fd38de7b48a4b568aa"
69
77
  },
70
78
  {
71
79
  "path": "shared/core/multi-agent-issue/SKILL.md",
@@ -109,7 +117,7 @@
109
117
  },
110
118
  {
111
119
  "path": "shared/core/multi-agent-refactor/SKILL.md",
112
- "sha256": "f8a9495f6aebacba634672d6c9416046521cbdfa769746c9ff3a4a993482a85a"
120
+ "sha256": "60766ee3a6b974b3872417db5c40d1fc03bfddd99daf4aca11d7a59b57a41b57"
113
121
  },
114
122
  {
115
123
  "path": "shared/core/multi-agent-resume/SKILL.md",
@@ -127,6 +135,14 @@
127
135
  "path": "shared/core/multi-agent-review/SKILL.md",
128
136
  "sha256": "b1fd57d2b80ee0fe2490439d9f2688fcf415857c2aab60549c5456e27693b9a9"
129
137
  },
138
+ {
139
+ "path": "shared/core/multi-agent-routines/SKILL.md",
140
+ "sha256": "91d3bdb15da7addadd1afd0b170057daf3d3fc335da3f6e1feb4feff1af0881e"
141
+ },
142
+ {
143
+ "path": "shared/core/multi-agent-save/SKILL.md",
144
+ "sha256": "f9c6e2ba1dd1b224b80cabe35f4327b5bcc0b1e05868b378547bec616193ef1b"
145
+ },
130
146
  {
131
147
  "path": "shared/core/multi-agent-scan/SKILL.md",
132
148
  "sha256": "266e4e7c9b702bc3205b7d926455c5863e144cddb1e4b883aa81e570a362fd3f"
@@ -149,11 +165,11 @@
149
165
  },
150
166
  {
151
167
  "path": "shared/core/multi-agent-sync/SKILL.md",
152
- "sha256": "de38486056c99a599d93ae5166a2071875331a7d77f1e34f66d8b72657a73c2f"
168
+ "sha256": "3a9f7d8caa47d41d1a4759ce6673bfb94a1ef9f62f7113a4cd3b1e9451464601"
153
169
  },
154
170
  {
155
171
  "path": "shared/core/multi-agent-test/SKILL.md",
156
- "sha256": "f90c34f71ea6cd5cccbfa128597eaaa221a0ca5f62924eb9cfb6b1ed2d07de04"
172
+ "sha256": "2b662ecd9497149c4c2e87cc79e906cc1cab7ea4a7d0efe300b0b018559960c4"
157
173
  },
158
174
  {
159
175
  "path": "shared/core/multi-agent-uninstall/SKILL.md",
@@ -425,7 +441,7 @@
425
441
  },
426
442
  {
427
443
  "path": "shared/external/humanizer/SKILL.md",
428
- "sha256": "7f9d49b90eba79bf7acd8a5d8d5ae091cbc970188b8ecac310cce04b82add42e"
444
+ "sha256": "2fc8fe79a5832b9e1316f515c73f0545023153e85aba4645ba42060a0afdd049"
429
445
  },
430
446
  {
431
447
  "path": "shared/external/ios-accessibility/SKILL.md",