@mmerterden/multi-agent-pipeline 20.4.0 → 20.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 (41) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +10 -0
  3. package/docs/facts.json +5 -4
  4. package/install/_dev-only-files.mjs +1 -0
  5. package/manifest.json +44 -41
  6. package/package.json +4 -3
  7. package/pipeline/agents/android-architect.md +1 -0
  8. package/pipeline/agents/backend-architect.md +1 -0
  9. package/pipeline/agents/bulk-reader.md +1 -0
  10. package/pipeline/agents/code-reviewer.md +2 -1
  11. package/pipeline/agents/dev-critic.md +1 -0
  12. package/pipeline/agents/explorer.md +1 -0
  13. package/pipeline/agents/ios-architect.md +1 -0
  14. package/pipeline/agents/plan-critic.md +1 -0
  15. package/pipeline/agents/security-auditor.md +1 -0
  16. package/pipeline/agents/task-clarifier.md +1 -0
  17. package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +1 -1
  18. package/pipeline/commands/multi-agent/update/SKILL.md +3 -1
  19. package/pipeline/lib/confusables.json +100 -0
  20. package/pipeline/lib/normalize-text.mjs +48 -0
  21. package/pipeline/lib/outbound-gate.mjs +10 -1
  22. package/pipeline/lib/redact.mjs +53 -0
  23. package/pipeline/lib/vercel-deploy.sh +5 -7
  24. package/pipeline/multi-agent-refs/features/unattended-security.md +5 -0
  25. package/pipeline/schemas/autopilot-config.schema.json +5 -0
  26. package/pipeline/scripts/_autopilot-outcomes.mjs +2 -0
  27. package/pipeline/scripts/agent-guard.py +30 -1
  28. package/pipeline/scripts/audit-log-rotate.sh +3 -0
  29. package/pipeline/scripts/audit-log.sh +10 -2
  30. package/pipeline/scripts/autopilot-intake.mjs +17 -15
  31. package/pipeline/scripts/autopilot-publish.mjs +16 -1
  32. package/pipeline/scripts/autopilot-runner.mjs +20 -0
  33. package/pipeline/scripts/log-metric.sh +5 -0
  34. package/pipeline/scripts/pr-request.mjs +9 -3
  35. package/pipeline/scripts/pre-commit-check.sh +59 -16
  36. package/pipeline/scripts/pre-push-check.sh +3 -3
  37. package/pipeline/scripts/run-ui-tests.sh +5 -5
  38. package/pipeline/scripts/update-check.sh +73 -21
  39. package/pipeline/scripts/website-deploy-commit.sh +3 -2
  40. package/pipeline/skills/.skill-manifest.json +2 -2
  41. package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +1 -1
@@ -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 24)
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=24
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:-24}"; shift; [ $# -gt 0 ] && shift ;;
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 read from the pipeline repo clone.
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 candidate in "$HOME/multi-agent-pipeline" "$HOME/dev/multi-agent-pipeline" "$HOME/projects/multi-agent-pipeline"; do
62
- if [ -f "$candidate/package.json" ]; then
63
- LOCAL_VERSION=$(node -p "require('$candidate/package.json').version" 2>/dev/null || true)
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 marker in "$HOME/.claude/.pipeline-version" "$HOME/.copilot/.pipeline-version" \
71
- "$HOME/.codex/.pipeline-version"; do
72
- if [ -f "$marker" ]; then
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
- # Highest of two dotted versions; ties resolve to the second argument.
81
- highest_of() {
82
- printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | tail -1
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
- printf '%s|%s|%s\n' "$now" "$latest" "$required" > "$CACHE_FILE" 2>/dev/null || true
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" ] && [ "$(highest_of "$required" "$latest")" != "$latest" ]; then
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" ] && [ "$LOCAL_VERSION" != "$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 [ "$(highest_of "$LOCAL_VERSION" "$latest")" = "$latest" ]; then
192
+ if version_lt "$LOCAL_VERSION" "$latest"; then
141
193
  printf '%s|%s\n' "$LOCAL_VERSION" "$latest"
142
194
  fi
143
195
  exit 0
@@ -7,8 +7,9 @@
7
7
  # refuses to build a commit whose author is not a contributor on the project. The
8
8
  # push still succeeds, the deployment is created and sits at UNKNOWN with a 0ms
9
9
  # build (API `readyState: BLOCKED`), and the live site keeps serving the previous
10
- # version. v16.4.0 and v16.5.0 were both pushed that way and neither was built,
11
- # while the sync reported the website as synced both times.
10
+ # version. A successful push is therefore not evidence of a deploy, so the script
11
+ # pins the author before committing and reads the deployment state back after
12
+ # pushing instead of reporting the website as synced on the push alone.
12
13
  #
13
14
  # Usage:
14
15
  # website-deploy-commit.sh <identity-name> <identity-email> <version> [repo-dir]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-25T15:14:42Z",
3
+ "generatedAt": "2026-09-26T13:39:28Z",
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": "ca671ffe46809e3b0c825a6b5c19e8ea00e529874531eb480a0987428366d2f4"
32
+ "sha256": "d4f23f6f7bb493a4efa2cfa252fab50b22b6506aeaf31985db4498b5f010c8c8"
33
33
  },
34
34
  {
35
35
  "path": "shared/core/multi-agent-autopilot-status/SKILL.md",
@@ -74,7 +74,7 @@ The same name works on both sides and both mechanisms already exist:
74
74
 
75
75
  | Source | Query | Extra condition |
76
76
  |---|---|---|
77
- | GitHub | `gh issue list --repo <r> --label <label> --state open` | none - the label is the whole gate |
77
+ | GitHub | `gh issue list --repo <r> --label <label> --assignee @me --state open` | **assigned to you** (`githubAssignee`, `"*"` for any), so a label an issue form applies is not enough |
78
78
  | Jira | `assignee = currentUser() AND labels = "<label>" AND resolution = EMPTY AND status not in (Done, Closed, Cancelled)` | **assigned to you**, so a label somebody else adds is not enough |
79
79
 
80
80
  Two Jira caveats worth saying out loud when Jira is selected: labels are one