@mmerterden/multi-agent-pipeline 10.12.0 → 11.1.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 +58 -0
- package/install/templates/claude-hooks.json +18 -1
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/generate.md +34 -0
- package/pipeline/commands/multi-agent/help.md +6 -8
- package/pipeline/commands/multi-agent/refs/cross-cli-contract.md +5 -5
- package/pipeline/commands/multi-agent/refs/features/autopilot-circuit-breaker.md +32 -0
- package/pipeline/commands/multi-agent/refs/generate-issue.md +123 -53
- package/pipeline/commands/multi-agent/refs/phases/modes.md +1 -0
- package/pipeline/commands/multi-agent/refs/picker-contract.md +1 -1
- package/pipeline/commands/multi-agent/sync.md +9 -9
- package/pipeline/commands/multi-agent.md +2 -4
- package/pipeline/scripts/agent-guard.py +74 -0
- package/pipeline/scripts/agent-guard.sh +48 -0
- package/pipeline/scripts/build-stack-plugins.mjs +1 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +4 -4
- package/pipeline/scripts/scan-agent-config.sh +107 -0
- package/pipeline/scripts/smoke-agent-guard.sh +74 -0
- package/pipeline/scripts/smoke-autopilot-circuit-breaker.sh +36 -0
- package/pipeline/scripts/smoke-config-hygiene.sh +58 -0
- package/pipeline/scripts/smoke-gate-hooks.sh +18 -0
- package/pipeline/scripts/smoke-generate-issue.sh +44 -39
- package/pipeline/skills/.skills-index.json +32 -14
- package/pipeline/skills/shared/core/multi-agent-generate/SKILL.md +51 -0
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +2 -4
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
- package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +69 -0
- package/pipeline/skills/shared/external/council/SKILL.md +69 -0
- package/pipeline/skills/shared/external/search-first/SKILL.md +76 -0
- package/pipeline/skills/skills-index.md +5 -3
- package/pipeline/commands/multi-agent/generate-bug.md +0 -33
- package/pipeline/commands/multi-agent/generate-task.md +0 -33
- package/pipeline/skills/shared/core/multi-agent-generate-bug/SKILL.md +0 -39
- package/pipeline/skills/shared/core/multi-agent-generate-task/SKILL.md +0 -39
|
@@ -58,7 +58,7 @@ Run every step automatically:
|
|
|
58
58
|
Step 0: FIGMA_SYNC SKIP (deprecated - feedback_figma_source_deprecated)
|
|
59
59
|
Step 1: PLATFORM Detect macOS / Linux / Windows (Git Bash / WSL); export PLATFORM env
|
|
60
60
|
Step 1.5: DETECT Compare timestamps, find stale targets
|
|
61
|
-
Step 2: COPILOT Claude Code -> Copilot CLI (instructions +
|
|
61
|
+
Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 36 sub-command skills)
|
|
62
62
|
Step 3: REPO Claude Code -> pipeline repo (genericized, personal data scrub, bash -n on all sh)
|
|
63
63
|
Step 3c: PLUGINS pipeline shared/external -> multi-agent-plugins marketplace (rebuild knowledge/,
|
|
64
64
|
bump changed plugins' patch version, commit + push the plugins repo)
|
|
@@ -148,7 +148,7 @@ If nothing is stale → report "All targets up to date" and stop.
|
|
|
148
148
|
5. **Files NOT synced** (local-only, may contain personal data):
|
|
149
149
|
- `~/.claude/multi-agent-preferences.json`
|
|
150
150
|
- `~/.claude/CLAUDE.md`, `~/.claude/rules/`, `~/.claude/knowledge/`
|
|
151
|
-
- `~/.claude/scripts/` - EXCEPT `pre-commit-check.sh` and `build-stack-plugins.mjs` (generic, synced)
|
|
151
|
+
- `~/.claude/scripts/` - EXCEPT `pre-commit-check.sh`, `agent-guard.sh`, `agent-guard.py`, and `build-stack-plugins.mjs` (generic, synced)
|
|
152
152
|
- `~/.claude/settings.json`
|
|
153
153
|
|
|
154
154
|
6. **Cross-platform smoke gate** (final step of the REPO sync, before push):
|
|
@@ -169,7 +169,7 @@ If nothing is stale → report "All targets up to date" and stop.
|
|
|
169
169
|
|
|
170
170
|
## Stack-Plugin Sync (Step 3c)
|
|
171
171
|
|
|
172
|
-
Stack skills are distributed as versioned plugins in the `
|
|
172
|
+
Stack skills are distributed as versioned plugins in the `mmerterden/multi-agent-plugins` marketplace. The pipeline's `pipeline/skills/shared/external/` is the **single authoring source**; the marketplace is a derived, versioned publish artifact. This step rebuilds it and publishes only when something changed.
|
|
173
173
|
|
|
174
174
|
**Source of truth:** author/vendor knowledge skills in `pipeline/skills/shared/external/` (the pipeline's own phases also consume them). The plugins' authored lifecycle skills (`index` / `reference` / `workflow` / `tools`) live in the plugins repo and are never touched by sync.
|
|
175
175
|
|
|
@@ -177,7 +177,7 @@ Stack skills are distributed as versioned plugins in the `{owner}/multi-agent-pl
|
|
|
177
177
|
|
|
178
178
|
```bash
|
|
179
179
|
PLUGINS_REPO="$HOME/multi-agent-plugins"
|
|
180
|
-
[ ! -d "$PLUGINS_REPO/.git" ] && gh repo clone
|
|
180
|
+
[ ! -d "$PLUGINS_REPO/.git" ] && gh repo clone mmerterden/multi-agent-plugins "$PLUGINS_REPO"
|
|
181
181
|
cd "$PLUGINS_REPO" && git pull origin main
|
|
182
182
|
|
|
183
183
|
# 1. Preview what would change (routing + version bumps), no writes:
|
|
@@ -197,10 +197,10 @@ node "$HOME/multi-agent-pipeline/pipeline/scripts/build-stack-plugins.mjs"
|
|
|
197
197
|
```bash
|
|
198
198
|
cd "$PLUGINS_REPO"
|
|
199
199
|
if ! git diff --quiet; then
|
|
200
|
-
git config user.name "
|
|
200
|
+
git config user.name "Mert Erden"; git config user.email "mmerterden@gmail.com"
|
|
201
201
|
git add -A
|
|
202
202
|
git commit -m "chore: rebuild stack plugins from pipeline shared/external"
|
|
203
|
-
gh auth switch --user
|
|
203
|
+
gh auth switch --user mmerterden 2>/dev/null || true
|
|
204
204
|
git push origin main
|
|
205
205
|
fi
|
|
206
206
|
```
|
|
@@ -256,7 +256,7 @@ When invoked with the `release` argument:
|
|
|
256
256
|
|
|
257
257
|
```
|
|
258
258
|
1. REPO Claude Code -> pipeline repo (genericized)
|
|
259
|
-
2. VERIFY Personal data scan -> 0 results
|
|
259
|
+
2. VERIFY Personal data scan -> 0 results; config-hygiene scan (scan-agent-config.sh) -> 0 HIGH
|
|
260
260
|
3. LINT + TEST ESLint + smoke tests
|
|
261
261
|
4. VERSION package.json version bump (patch/minor/major)
|
|
262
262
|
5. Commit + TAG git commit + git tag v{VERSION}
|
|
@@ -277,12 +277,12 @@ This runs on the Claude <-> Copilot axis — the two CLIs the pipeline supports
|
|
|
277
277
|
|-------------|-------------|
|
|
278
278
|
| `~/.claude/commands/multi-agent/{cmd}.md` | `~/.copilot/skills/multi-agent-{cmd}/SKILL.md` |
|
|
279
279
|
|
|
280
|
-
**
|
|
280
|
+
**36 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
|
|
281
281
|
|
|
282
282
|
```
|
|
283
283
|
analysis, analysis-resolve, autopilot, build-optimize, channels, delete, dev,
|
|
284
284
|
dev-autopilot, dev-local, dev-local-autopilot, diff-explain, finish, garbage-collect,
|
|
285
|
-
generate
|
|
285
|
+
generate, help, issue, jira, kill, language, local,
|
|
286
286
|
local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review,
|
|
287
287
|
scan, search, setup, stack, status, sync, test, update
|
|
288
288
|
```
|
|
@@ -77,8 +77,7 @@ Lib scripts (`~/.claude/lib/`):
|
|
|
77
77
|
| `analysis ["<feature>"]` | Standalone feature-spec analizi: Figma / Swagger / Confluence / Jira / repo girdileri sabit 7-bölümlük şablona dökülür, humanizer'dan geçer, Local/Confluence/Jira hedef(ler)ine post edilir. Worktree veya commit yok. Dev'e zincirleme yok. |
|
|
78
78
|
| `build-optimize` | iOS-only Xcode build performance wrapper. Vendored `xcode-build-orchestrator`'a dispatch eder; benchmark + compilation / project / SPM analyzer'lar + recommend-first plan `.build-benchmark/optimization-plan.md`. Non-iOS stack'lerde fail-fast. |
|
|
79
79
|
| `channels [PR-url\|#N\|Jira-url\|Jira-id] [--channels pr,jira,confluence,wiki] [--content normal,test,auto-diff,note] [--message "..."]` | Post task report to multi-select channels (PR description, Jira comment, Confluence page, Wiki pages) with multi-select content sources. Humanizer pass per-channel. Bitbucket PR updates use reviewer-preserving PUT. Phase 7 delegates to this command; also invocable post-hoc for fixes made outside the pipeline. No worktree. |
|
|
80
|
-
| `generate
|
|
81
|
-
| `generate-bug ["desc"] [figma-url]` | Create a standards-compliant Jira **Bug**: same convention mining + active-sprint detection; free-text logs/devices map to Steps to Reproduce / Environment; full draft preview + explicit approval before create. No worktree, no commits. |
|
|
80
|
+
| `generate ["desc"] [figma-url] [swagger-url]` | Create a standards-compliant Jira issue: asks the type (**Task** / **Bug** / **Story**), mines the project's recent same-type issues for conventions (summary format, labels, priority, test-scenario style), detects the active sprint, drafts from a standard template with auto-sizing sections (Design Reference / API Contract / Screenshots appear only when their source is given), asks about unknown fields, then full draft preview + explicit approval before create. No worktree, no commits. |
|
|
82
81
|
| `test` or `test [args]` | UI Bug Hunter - screenshot + tap + analyze on booted simulator via MCP (read `$HOME/.claude/commands/sim-test.md`). `/multi-agent:test` also resolves here via the `commands/multi-agent/test.md` delegate. |
|
|
83
82
|
| `manual-test [#id]` | Phase 5 standalone Manual Test - checks out the task branch, prints Xcode / SourceTree hints, waits for user verdict (`ok` / `fix: ...`). |
|
|
84
83
|
| `stack [ios\|android\|backend\|mobile\|all]` | Swap skills for next conversation. No arg = show current stack. |
|
|
@@ -118,8 +117,7 @@ This command uses lazy loading for token efficiency. Read the relevant sub-file
|
|
|
118
117
|
| `local-autopilot` | `$HOME/.claude/commands/multi-agent/local-autopilot.md` |
|
|
119
118
|
| `dev-local` | `$HOME/.claude/commands/multi-agent/dev-local.md` |
|
|
120
119
|
| `dev-local-autopilot` | `$HOME/.claude/commands/multi-agent/dev-local-autopilot.md` |
|
|
121
|
-
| `generate
|
|
122
|
-
| `generate-bug` | `$HOME/.claude/commands/multi-agent/generate-bug.md` (loads `refs/generate-issue.md`) |
|
|
120
|
+
| `generate` | `$HOME/.claude/commands/multi-agent/generate.md` (loads `refs/generate-issue.md`) |
|
|
123
121
|
| `stack` | `$HOME/.claude/commands/multi-agent/stack.md` |
|
|
124
122
|
| `language` | Handled inline - set/show prompt language in preferences |
|
|
125
123
|
| SwiftUI component task (iOS) | `$HOME/.claude/commands/multi-agent/refs/swiftui-guide.md` |
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Decision core for agent-guard.sh (see that file for the hook contract).
|
|
3
|
+
|
|
4
|
+
Reads the PreToolUse payload JSON on stdin and prints exactly one decision:
|
|
5
|
+
BLOCK_ATTRIB | BLOCK_FORCE | OK
|
|
6
|
+
|
|
7
|
+
Never executes the command. shlex.split only tokenizes shell-like text; a
|
|
8
|
+
`$(...)` in the command is a literal token, never a subshell. Any error prints
|
|
9
|
+
OK (fail-open) so a guard bug cannot break a legitimate tool call.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import shlex
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
# --- Rule data (extend here; this is the hookify-style rule surface) ----------
|
|
18
|
+
PROTECTED = {"main", "master", "develop"}
|
|
19
|
+
REMOTES = {"origin", "upstream"}
|
|
20
|
+
ATTRIBUTION = re.compile(
|
|
21
|
+
r"co-authored-by:\s*(claude|anthropic|opus|sonnet|haiku|fable)"
|
|
22
|
+
r"|noreply@anthropic\.com"
|
|
23
|
+
r"|generated with\s+.*claude"
|
|
24
|
+
r"|\U0001F916\s*generated"
|
|
25
|
+
r"|claude code <",
|
|
26
|
+
re.IGNORECASE,
|
|
27
|
+
)
|
|
28
|
+
FORCE = re.compile(r"(^|\s)(--force(\b|$)|--force-with-lease|-\S*f\S*)(\s|$)")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def decide(cmd: str) -> str:
|
|
32
|
+
low = cmd.lower()
|
|
33
|
+
|
|
34
|
+
# Rule 1: AI attribution inside a git commit.
|
|
35
|
+
if re.search(r"git\s+commit", low) and ATTRIBUTION.search(cmd):
|
|
36
|
+
return "BLOCK_ATTRIB"
|
|
37
|
+
|
|
38
|
+
# Rule 2: force-push to a protected branch.
|
|
39
|
+
if re.search(r"git\s+push", low) and FORCE.search(cmd):
|
|
40
|
+
try:
|
|
41
|
+
toks = shlex.split(cmd)
|
|
42
|
+
except Exception:
|
|
43
|
+
toks = cmd.split()
|
|
44
|
+
if any(t in PROTECTED or t.split(":")[-1] in PROTECTED for t in toks):
|
|
45
|
+
return "BLOCK_FORCE"
|
|
46
|
+
# non-flag args after `push`; only a remote (or nothing) means a bare push
|
|
47
|
+
args, seen = [], False
|
|
48
|
+
for t in toks:
|
|
49
|
+
if t == "push":
|
|
50
|
+
seen = True
|
|
51
|
+
continue
|
|
52
|
+
if seen and not t.startswith("-"):
|
|
53
|
+
args.append(t)
|
|
54
|
+
nonremote = [a for a in args if a not in REMOTES]
|
|
55
|
+
if not nonremote and os.environ.get("CUR_BRANCH", "") in PROTECTED:
|
|
56
|
+
return "BLOCK_FORCE"
|
|
57
|
+
|
|
58
|
+
return "OK"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main() -> None:
|
|
62
|
+
try:
|
|
63
|
+
data = json.load(sys.stdin)
|
|
64
|
+
cmd = (data.get("tool_input") or {}).get("command", "")
|
|
65
|
+
if not isinstance(cmd, str) or not cmd:
|
|
66
|
+
print("OK")
|
|
67
|
+
return
|
|
68
|
+
print(decide(cmd))
|
|
69
|
+
except Exception:
|
|
70
|
+
print("OK")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# agent-guard.sh - PreToolUse Bash guard for the multi-agent pipeline.
|
|
3
|
+
#
|
|
4
|
+
# Turns two prompt-level rules into deterministic, OS-enforced gates:
|
|
5
|
+
# 1. No AI/assistant attribution in git commit messages (Co-Authored-By: Claude,
|
|
6
|
+
# "Generated with Claude Code", robot emoji, Anthropic no-reply address).
|
|
7
|
+
# 2. No force-push to a protected branch (main / master / develop).
|
|
8
|
+
#
|
|
9
|
+
# Contract (Claude Code PreToolUse hook):
|
|
10
|
+
# - Reads the tool-call JSON on stdin: {"tool_input":{"command":"..."}, ...}.
|
|
11
|
+
# - Exit 2 -> BLOCK the tool call (reason on stderr, shown to the model).
|
|
12
|
+
# - Exit 0 -> allow.
|
|
13
|
+
#
|
|
14
|
+
# Safety design (fail-OPEN):
|
|
15
|
+
# - The command string is ONLY parsed/pattern-matched, NEVER executed or eval'd
|
|
16
|
+
# (the decision core is agent-guard.py; shlex tokenizes without running).
|
|
17
|
+
# - Any internal error (bad JSON, missing python3/helper, empty input) -> exit 0.
|
|
18
|
+
# A guard bug must never break a legitimate tool call.
|
|
19
|
+
# - No network, no file writes, no secret values printed.
|
|
20
|
+
# - The single exec is a read-only `git rev-parse` to learn the current branch.
|
|
21
|
+
|
|
22
|
+
set -u
|
|
23
|
+
|
|
24
|
+
HERE="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
|
|
25
|
+
HELPER="$HERE/agent-guard.py"
|
|
26
|
+
[ -f "$HELPER" ] || exit 0
|
|
27
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
28
|
+
|
|
29
|
+
PAYLOAD="$(cat 2>/dev/null || true)"
|
|
30
|
+
[ -z "$PAYLOAD" ] && exit 0
|
|
31
|
+
|
|
32
|
+
CUR_BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
33
|
+
|
|
34
|
+
DECISION="$(printf '%s' "$PAYLOAD" | CUR_BRANCH="$CUR_BRANCH" python3 "$HELPER" 2>/dev/null || true)"
|
|
35
|
+
|
|
36
|
+
case "$DECISION" in
|
|
37
|
+
BLOCK_ATTRIB)
|
|
38
|
+
echo "BLOCKED by agent-guard: the commit message carries AI/assistant attribution." >&2
|
|
39
|
+
echo "Remove any 'Co-Authored-By: Claude', 'Generated with Claude Code', robot-emoji, or" >&2
|
|
40
|
+
echo "anthropic no-reply trailer. Commits are authored solely as the user's git identity." >&2
|
|
41
|
+
exit 2 ;;
|
|
42
|
+
BLOCK_FORCE)
|
|
43
|
+
echo "BLOCKED by agent-guard: force-push to a protected branch (main/master/develop) is not allowed." >&2
|
|
44
|
+
echo "Rewriting shared history is a data-loss risk. Push a normal commit, or force-push a feature branch." >&2
|
|
45
|
+
exit 2 ;;
|
|
46
|
+
*)
|
|
47
|
+
exit 0 ;;
|
|
48
|
+
esac
|
|
@@ -36,7 +36,7 @@ const EXTERNAL = join(PIPE_ROOT, 'pipeline/skills/shared/external');
|
|
|
36
36
|
const DRY = args.includes('--dry-run');
|
|
37
37
|
|
|
38
38
|
const COMMON_PLUGIN = 'ai-common-engineering-toolkit';
|
|
39
|
-
const COMMON_SKILLS = ['accessibility-compliance-accessibility-audit', 'firebase', 'humanizer'];
|
|
39
|
+
const COMMON_SKILLS = ['accessibility-compliance-accessibility-audit', 'firebase', 'humanizer', 'council', 'search-first', 'agent-introspection-debugging'];
|
|
40
40
|
|
|
41
41
|
// Apple/Xcode-only skills that match no stack pattern → iOS plugin only.
|
|
42
42
|
const APPLE_ONLY = [
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
.claude/multi-agent-preferences.json 1
|
|
6
6
|
.claude/rules 12
|
|
7
7
|
.claude/schemas 23
|
|
8
|
-
.claude/scripts
|
|
8
|
+
.claude/scripts 180
|
|
9
9
|
.claude/settings.json 1
|
|
10
|
-
.claude/skills
|
|
10
|
+
.claude/skills 563
|
|
11
11
|
.copilot/agents 8
|
|
12
12
|
.copilot/copilot-instructions.md 1
|
|
13
13
|
.copilot/lib 24
|
|
14
14
|
.copilot/schemas 23
|
|
15
|
-
.copilot/scripts
|
|
16
|
-
.copilot/skills
|
|
15
|
+
.copilot/scripts 180
|
|
16
|
+
.copilot/skills 600
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scan-agent-config.sh - config-hygiene gate for the shipped agent surface.
|
|
3
|
+
#
|
|
4
|
+
# Audits the config the pipeline SHIPS (install templates, preferences template,
|
|
5
|
+
# agent definitions, MCP helper scripts) for security problems before release:
|
|
6
|
+
# HIGH - hardcoded secrets; hook commands that eval/execute inspected input;
|
|
7
|
+
# permission bypass flags baked into a shipped template.
|
|
8
|
+
# MEDIUM - unpinned/remote-install supply-chain patterns (curl|bash, npx -y);
|
|
9
|
+
# blanket Bash(*) permission allow.
|
|
10
|
+
#
|
|
11
|
+
# Read-only. Never prints a secret value (reports file + rule only). Self-contained
|
|
12
|
+
# (no external scanner dependency). Exit 1 if any HIGH finding, else 0.
|
|
13
|
+
#
|
|
14
|
+
# Inspired by config-audit tools like ecc-agentshield, but rewritten as a
|
|
15
|
+
# first-party, dependency-free gate over this repo's own shipped surface.
|
|
16
|
+
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
|
|
19
|
+
# ROOT defaults to the repo root; SCAN_ROOT overrides it (used by the smoke test
|
|
20
|
+
# to point at a fixture tree of planted-bad configs).
|
|
21
|
+
ROOT="${SCAN_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
22
|
+
|
|
23
|
+
HIGH=0; MED=0
|
|
24
|
+
high() { HIGH=$((HIGH+1)); echo " [HIGH] $1"; }
|
|
25
|
+
med() { MED=$((MED+1)); echo " [MEDIUM] $1"; }
|
|
26
|
+
|
|
27
|
+
# Shipped config surface (globs expanded safely; missing paths are skipped).
|
|
28
|
+
TARGETS=()
|
|
29
|
+
add() { [ -e "$1" ] && TARGETS+=("$1"); }
|
|
30
|
+
add "$ROOT/install/templates/claude-hooks.json"
|
|
31
|
+
add "$ROOT/install/templates/copilot-instructions.md"
|
|
32
|
+
add "$ROOT/pipeline/preferences-template.json"
|
|
33
|
+
for f in "$ROOT"/pipeline/agents/*.md; do add "$f"; done
|
|
34
|
+
for f in "$ROOT"/pipeline/skills/figma-common/figma-setup/scripts/*.py \
|
|
35
|
+
"$ROOT"/pipeline/skills/figma-ios/figma-to-component/scripts/*.py; do add "$f"; done
|
|
36
|
+
add "$ROOT/pipeline/scripts/agent-guard.sh"
|
|
37
|
+
add "$ROOT/pipeline/scripts/pre-commit-check.sh"
|
|
38
|
+
|
|
39
|
+
rel() { echo "${1#$ROOT/}"; }
|
|
40
|
+
|
|
41
|
+
echo "→ scanning ${#TARGETS[@]} shipped config files"
|
|
42
|
+
|
|
43
|
+
for f in "${TARGETS[@]}"; do
|
|
44
|
+
[ -f "$f" ] || continue
|
|
45
|
+
r="$(rel "$f")"
|
|
46
|
+
|
|
47
|
+
# --- HIGH: hardcoded secrets (high-signal prefixes only; report file, not value)
|
|
48
|
+
if grep -EqI '(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{60,}|xox[baprs]-[A-Za-z0-9-]{12,}|sk_live_[A-Za-z0-9]{20,}|AIza[0-9A-Za-z_-]{35}|npm_[A-Za-z0-9]{36}|glpat-[A-Za-z0-9_-]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----' "$f" 2>/dev/null; then
|
|
49
|
+
high "hardcoded secret / token in $r"
|
|
50
|
+
fi
|
|
51
|
+
if grep -Eq '"type"[[:space:]]*:[[:space:]]*"service_account"' "$f" 2>/dev/null; then
|
|
52
|
+
high "embedded service-account JSON in $r"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# --- HIGH: permission bypass baked into a shipped template
|
|
56
|
+
if grep -Eq 'skipDangerousModePermissionPrompt"?[[:space:]]*:[[:space:]]*true|--dangerously-skip-permissions|"permissions"[[:space:]]*:[[:space:]]*"?(allow-all|bypass)' "$f" 2>/dev/null; then
|
|
57
|
+
high "permission bypass flag in shipped template: $r"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# --- MEDIUM: blanket Bash allow
|
|
61
|
+
if grep -Eq '"Bash\(\*\)"|"Bash\(:\*\)"|Bash\(\*:\*\)' "$f" 2>/dev/null; then
|
|
62
|
+
med "blanket Bash(*) permission in $r"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# --- MEDIUM: remote-install supply-chain patterns
|
|
66
|
+
if grep -Eq 'curl[^|]*\|[[:space:]]*(sudo[[:space:]]+)?(ba)?sh|wget[^|]*\|[[:space:]]*(ba)?sh' "$f" 2>/dev/null; then
|
|
67
|
+
med "pipe-to-shell remote install (curl|bash) in $r"
|
|
68
|
+
fi
|
|
69
|
+
if grep -Eq '(^|[^A-Za-z])npx[[:space:]]+-y[[:space:]]|npx[[:space:]]+--yes[[:space:]]' "$f" 2>/dev/null; then
|
|
70
|
+
med "unpinned 'npx -y' auto-install in $r"
|
|
71
|
+
fi
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
# --- HIGH: hook commands must only invoke vetted scripts, never inline eval -----
|
|
75
|
+
HOOKS="$ROOT/install/templates/claude-hooks.json"
|
|
76
|
+
if [ -f "$HOOKS" ] && command -v python3 >/dev/null 2>&1; then
|
|
77
|
+
bad="$(python3 - "$HOOKS" <<'PY' 2>/dev/null || true
|
|
78
|
+
import json, re, sys
|
|
79
|
+
try:
|
|
80
|
+
t = json.load(open(sys.argv[1]))
|
|
81
|
+
except Exception:
|
|
82
|
+
sys.exit(0)
|
|
83
|
+
bad = []
|
|
84
|
+
for _event, entries in (t.get("hooks") or {}).items():
|
|
85
|
+
for e in entries:
|
|
86
|
+
for h in e.get("hooks", []):
|
|
87
|
+
c = h.get("command", "")
|
|
88
|
+
# allow only: `bash $HOME/.claude/scripts/<name>` (our vetted scripts)
|
|
89
|
+
if not re.fullmatch(r'bash \$HOME/\.claude/scripts/[A-Za-z0-9._-]+', c.strip()):
|
|
90
|
+
bad.append(c)
|
|
91
|
+
if re.search(r'\beval\b|\$\(|`', c):
|
|
92
|
+
bad.append(c)
|
|
93
|
+
print("\n".join(bad))
|
|
94
|
+
PY
|
|
95
|
+
)"
|
|
96
|
+
if [ -n "$bad" ]; then
|
|
97
|
+
high "hooks template has a non-vetted or eval-bearing hook command"
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
echo ""
|
|
102
|
+
echo "══ config-hygiene: $HIGH high, $MED medium ══"
|
|
103
|
+
if [ "$HIGH" -gt 0 ]; then
|
|
104
|
+
echo "FAIL: high-severity config findings must be fixed before release."
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
107
|
+
exit 0
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-agent-guard.sh - behavioral contract for the agent-guard PreToolUse hook.
|
|
3
|
+
#
|
|
4
|
+
# Verifies the guard blocks (exit 2) AI-attribution commits and force-push to a
|
|
5
|
+
# protected branch, allows (exit 0) everything else, fails open on bad input, and
|
|
6
|
+
# NEVER executes the inspected command (injection safety).
|
|
7
|
+
#
|
|
8
|
+
# Runs the guard from the repo copy in a throwaway git repo whose branch we set,
|
|
9
|
+
# so the "bare push on a protected branch" path is exercised deterministically.
|
|
10
|
+
#
|
|
11
|
+
# Exit 0 = all pass, 1 = any failure.
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
16
|
+
GUARD="$ROOT/pipeline/scripts/agent-guard.sh"
|
|
17
|
+
command -v python3 >/dev/null 2>&1 || { echo "error: python3 required" >&2; exit 127; }
|
|
18
|
+
[ -f "$GUARD" ] || { echo "FAIL: agent-guard.sh missing at $GUARD" >&2; exit 1; }
|
|
19
|
+
[ -f "$ROOT/pipeline/scripts/agent-guard.py" ] || { echo "FAIL: agent-guard.py missing" >&2; exit 1; }
|
|
20
|
+
|
|
21
|
+
PASS=0; FAIL=0
|
|
22
|
+
pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
|
|
23
|
+
fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
|
|
24
|
+
|
|
25
|
+
# throwaway repo on a protected branch so `git rev-parse` inside the guard = main
|
|
26
|
+
WORK="$(mktemp -d)"
|
|
27
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
28
|
+
# init on main WITH a commit so `git rev-parse --abbrev-ref HEAD` resolves to main
|
|
29
|
+
# (an unborn branch reports HEAD, which would not exercise the bare-push path)
|
|
30
|
+
( cd "$WORK" \
|
|
31
|
+
&& git init -q \
|
|
32
|
+
&& git symbolic-ref HEAD refs/heads/main \
|
|
33
|
+
&& git -c user.email=t@t -c user.name=t commit -q --allow-empty -m init ) 2>/dev/null
|
|
34
|
+
|
|
35
|
+
J() { python3 -c 'import json,sys; print(json.dumps({"tool_input":{"command":sys.argv[1]}}))' "$1"; }
|
|
36
|
+
run() { # label expected-exit command-string
|
|
37
|
+
local got
|
|
38
|
+
( cd "$WORK" && printf '%s' "$(J "$3")" | bash "$GUARD" >/dev/null 2>&1 )
|
|
39
|
+
got=$?
|
|
40
|
+
if [ "$got" = "$2" ]; then pass "[$got] $1"; else fail "expected $2 got $got: $1"; fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
echo "→ blocks (exit 2)"
|
|
44
|
+
run "commit Co-Authored-By: Claude" 2 'git commit -m "x
|
|
45
|
+
|
|
46
|
+
Co-Authored-By: Claude <noreply@anthropic.com>"'
|
|
47
|
+
run "commit robot-emoji" 2 'git commit -m "x
|
|
48
|
+
|
|
49
|
+
🤖 Generated with Claude Code"'
|
|
50
|
+
run "push --force origin main" 2 'git push --force origin main'
|
|
51
|
+
run "push -f origin develop" 2 'git push -f origin develop'
|
|
52
|
+
run "push --force-with-lease master" 2 'git push --force-with-lease origin master'
|
|
53
|
+
run "bare push -f on main" 2 'git push -f'
|
|
54
|
+
|
|
55
|
+
echo "→ allows (exit 0)"
|
|
56
|
+
run "normal commit" 0 'git commit -m "feat: normal"'
|
|
57
|
+
run "push no force" 0 'git push origin main'
|
|
58
|
+
run "force push feature branch" 0 'git push --force origin feature/DEMO-1'
|
|
59
|
+
run "force push tag" 0 'git push -f origin v1.2.3'
|
|
60
|
+
run "non-git command" 0 'ls -la /tmp'
|
|
61
|
+
run "empty command" 0 ''
|
|
62
|
+
|
|
63
|
+
echo "→ fail-open on bad input"
|
|
64
|
+
( cd "$WORK" && printf 'not-json{' | bash "$GUARD" >/dev/null 2>&1 ); [ $? -eq 0 ] && pass "malformed json -> allow" || fail "malformed json blocked"
|
|
65
|
+
( cd "$WORK" && printf '' | bash "$GUARD" >/dev/null 2>&1 ); [ $? -eq 0 ] && pass "empty stdin -> allow" || fail "empty stdin blocked"
|
|
66
|
+
|
|
67
|
+
echo "→ injection safety (inspected command must NOT execute)"
|
|
68
|
+
SENT="$WORK/sentinel"; : > "$SENT"
|
|
69
|
+
( cd "$WORK" && printf '%s' "$(J "git commit -m \"\$(rm -f '$SENT')\"")" | bash "$GUARD" >/dev/null 2>&1 )
|
|
70
|
+
[ -f "$SENT" ] && pass "sentinel survived (no execution)" || fail "sentinel deleted - command executed"
|
|
71
|
+
|
|
72
|
+
echo ""
|
|
73
|
+
echo "══ agent-guard smoke: $PASS passed, $FAIL failed ══"
|
|
74
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-autopilot-circuit-breaker.sh - wiring contract for the autopilot
|
|
3
|
+
# circuit-breaker feature: the doc exists, enumerates the five trip conditions,
|
|
4
|
+
# and is referenced from the modes "what never skips" list.
|
|
5
|
+
#
|
|
6
|
+
# Exit 0 = all pass, 1 = any failure.
|
|
7
|
+
|
|
8
|
+
set -uo pipefail
|
|
9
|
+
|
|
10
|
+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
11
|
+
DOC="$ROOT/pipeline/commands/multi-agent/refs/features/autopilot-circuit-breaker.md"
|
|
12
|
+
MODES="$ROOT/pipeline/commands/multi-agent/refs/phases/modes.md"
|
|
13
|
+
|
|
14
|
+
PASS=0; FAIL=0
|
|
15
|
+
pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
|
|
16
|
+
fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
|
|
17
|
+
|
|
18
|
+
[ -f "$DOC" ] && pass "feature doc exists" || fail "feature doc missing"
|
|
19
|
+
[ -f "$MODES" ] && pass "modes doc exists" || fail "modes doc missing"
|
|
20
|
+
|
|
21
|
+
echo "→ five trip conditions documented"
|
|
22
|
+
for t in "No-progress stall" "Identical repeated failure" "Rework storm" "Cost drift" "Merge/rebase conflict"; do
|
|
23
|
+
grep -Fq "$t" "$DOC" && pass "trigger: $t" || fail "trigger missing: $t"
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
echo "→ action + safety invariants"
|
|
27
|
+
grep -Fq "resume" "$DOC" && pass "documents resume path" || fail "no resume path"
|
|
28
|
+
grep -Eiq "never auto-resolve|does not paper over|no force-anything" "$DOC" && pass "no-self-heal invariant present" || fail "missing no-self-heal invariant"
|
|
29
|
+
|
|
30
|
+
echo "→ wired into modes 'what never skips'"
|
|
31
|
+
grep -Fq "autopilot-circuit-breaker.md" "$MODES" && pass "modes references the feature doc" || fail "modes does not reference the feature"
|
|
32
|
+
grep -Fiq "Circuit-breaker" "$MODES" && pass "modes lists circuit-breaker under never-skip" || fail "modes missing circuit-breaker bullet"
|
|
33
|
+
|
|
34
|
+
echo ""
|
|
35
|
+
echo "══ autopilot-circuit-breaker smoke: $PASS passed, $FAIL failed ══"
|
|
36
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-config-hygiene.sh - contract for scan-agent-config.sh.
|
|
3
|
+
#
|
|
4
|
+
# 1. The real shipped surface must be clean (exit 0).
|
|
5
|
+
# 2. A fixture tree with planted-bad configs must be caught (exit 1, HIGH findings).
|
|
6
|
+
# Detection matters: a hygiene gate that never fires is worthless.
|
|
7
|
+
#
|
|
8
|
+
# Exit 0 = all pass, 1 = any failure.
|
|
9
|
+
|
|
10
|
+
set -uo pipefail
|
|
11
|
+
|
|
12
|
+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
13
|
+
SCANNER="$ROOT/pipeline/scripts/scan-agent-config.sh"
|
|
14
|
+
[ -f "$SCANNER" ] || { echo "FAIL: scanner missing" >&2; exit 1; }
|
|
15
|
+
|
|
16
|
+
PASS=0; FAIL=0
|
|
17
|
+
pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
|
|
18
|
+
fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
|
|
19
|
+
|
|
20
|
+
echo "→ 1. real shipped surface is clean"
|
|
21
|
+
if bash "$SCANNER" >/dev/null 2>&1; then pass "real surface exits 0"; else fail "real surface flagged (unexpected)"; fi
|
|
22
|
+
|
|
23
|
+
echo "→ 2. planted-bad fixture is caught"
|
|
24
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
25
|
+
mkdir -p "$TMP/install/templates" "$TMP/pipeline/agents" "$TMP/pipeline/scripts"
|
|
26
|
+
|
|
27
|
+
# a fake (non-real) token that matches the high-signal prefix rule
|
|
28
|
+
FAKE_TOKEN="ghp_$(printf 'A%.0s' $(seq 1 36))"
|
|
29
|
+
cat > "$TMP/pipeline/preferences-template.json" <<EOF
|
|
30
|
+
{ "token": "$FAKE_TOKEN", "install": "curl https://x.example/i.sh | bash" }
|
|
31
|
+
EOF
|
|
32
|
+
|
|
33
|
+
# bad hooks template: blanket Bash(*), bypass flag, and an eval-bearing hook cmd
|
|
34
|
+
cat > "$TMP/install/templates/claude-hooks.json" <<'EOF'
|
|
35
|
+
{
|
|
36
|
+
"permissions": { "allow": ["Bash(*)"] },
|
|
37
|
+
"skipDangerousModePermissionPrompt": true,
|
|
38
|
+
"hooks": { "PreToolUse": [ { "matcher": "Bash(git commit:*)",
|
|
39
|
+
"hooks": [ { "type": "command", "command": "bash -c \"eval $(cat x)\"" } ] } ] }
|
|
40
|
+
}
|
|
41
|
+
EOF
|
|
42
|
+
: > "$TMP/pipeline/agents/placeholder.md"
|
|
43
|
+
|
|
44
|
+
OUT="$(SCAN_ROOT="$TMP" bash "$SCANNER" 2>&1 || true)"
|
|
45
|
+
CODE=$?
|
|
46
|
+
# under set -e off, capture exit explicitly
|
|
47
|
+
SCAN_ROOT="$TMP" bash "$SCANNER" >/dev/null 2>&1; CODE=$?
|
|
48
|
+
|
|
49
|
+
[ "$CODE" -eq 1 ] && pass "fixture exits 1 (blocking)" || fail "fixture did not block (exit $CODE)"
|
|
50
|
+
echo "$OUT" | grep -q "\[HIGH\].*secret" && pass "detects hardcoded secret" || fail "missed secret"
|
|
51
|
+
echo "$OUT" | grep -q "\[HIGH\].*bypass" && pass "detects permission bypass" || fail "missed bypass flag"
|
|
52
|
+
echo "$OUT" | grep -q "\[HIGH\].*hook" && pass "detects eval-bearing hook cmd" || fail "missed bad hook command"
|
|
53
|
+
echo "$OUT" | grep -q "\[MEDIUM\].*Bash" && pass "detects blanket Bash(*)" || fail "missed blanket Bash"
|
|
54
|
+
echo "$OUT" | grep -q "\[MEDIUM\].*curl|bash\|pipe-to-shell" && pass "detects curl|bash" || fail "missed curl|bash"
|
|
55
|
+
|
|
56
|
+
echo ""
|
|
57
|
+
echo "══ config-hygiene smoke: $PASS passed, $FAIL failed ══"
|
|
58
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -43,6 +43,24 @@ else
|
|
|
43
43
|
fail "pre-commit-check.sh missing"
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
|
+
# 3b. template also wires agent-guard.sh on git commit AND git push
|
|
47
|
+
node -e '
|
|
48
|
+
const t = JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));
|
|
49
|
+
const pre = (t.hooks && t.hooks.PreToolUse) || [];
|
|
50
|
+
const commit = pre.find(h => /git commit/.test(h.matcher || ""));
|
|
51
|
+
const push = pre.find(h => /git push/.test(h.matcher || ""));
|
|
52
|
+
const cmds = e => (e && e.hooks || []).map(h => h.command || "").join(" ");
|
|
53
|
+
if (!/agent-guard\.sh/.test(cmds(commit))) { console.error("guard not on git commit"); process.exit(1); }
|
|
54
|
+
if (!/agent-guard\.sh/.test(cmds(push))) { console.error("guard not on git push"); process.exit(1); }
|
|
55
|
+
' "$TMPL" 2>/dev/null && pass "template wires agent-guard on git commit + git push" || fail "template does not wire agent-guard on both matchers"
|
|
56
|
+
|
|
57
|
+
# 3c. the guard scripts exist in the source tree
|
|
58
|
+
if [ -f "$ROOT/pipeline/scripts/agent-guard.sh" ] && [ -f "$ROOT/pipeline/scripts/agent-guard.py" ]; then
|
|
59
|
+
pass "agent-guard.sh + agent-guard.py exist to back the hook"
|
|
60
|
+
else
|
|
61
|
+
fail "agent-guard scripts missing"
|
|
62
|
+
fi
|
|
63
|
+
|
|
46
64
|
# 4. picker-contract documents the hook template + the phase-enforced caveat
|
|
47
65
|
PC="$ROOT/pipeline/commands/multi-agent/refs/picker-contract.md"
|
|
48
66
|
if grep -q "claude-hooks.json" "$PC" && grep -q "phase-enforced" "$PC"; then
|