@mmerterden/multi-agent-pipeline 20.3.1 → 20.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/docs/ecosystem.md +2 -2
- package/docs/facts.json +1 -1
- package/install/claude.mjs +35 -0
- package/manifest.json +64 -62
- package/package.json +1 -1
- package/pipeline/agents/android-architect.md +1 -0
- package/pipeline/agents/backend-architect.md +1 -0
- package/pipeline/agents/bulk-reader.md +1 -0
- package/pipeline/agents/code-reviewer.md +2 -1
- package/pipeline/agents/dev-critic.md +1 -0
- package/pipeline/agents/explorer.md +1 -0
- package/pipeline/agents/ios-architect.md +1 -0
- package/pipeline/agents/plan-critic.md +1 -0
- package/pipeline/agents/security-auditor.md +1 -0
- package/pipeline/agents/task-clarifier.md +1 -0
- package/pipeline/commands/figma-to-swiftui.md +6 -4
- package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/jira/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/stack/SKILL.md +2 -4
- package/pipeline/commands/multi-agent/sync/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/update/SKILL.md +6 -11
- package/pipeline/lib/redact.mjs +50 -0
- package/pipeline/multi-agent-refs/channels/pr.md +2 -2
- package/pipeline/multi-agent-refs/features/doctor.md +2 -1
- package/pipeline/multi-agent-refs/features/prior-fix-detection.md +1 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +5 -0
- package/pipeline/multi-agent-refs/generate-issue.md +9 -9
- package/pipeline/multi-agent-refs/outside-the-pipeline.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +2 -1
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +4 -4
- package/pipeline/multi-agent-refs/setup/repo-discovery.md +2 -2
- package/pipeline/schemas/autopilot-config.schema.json +5 -0
- package/pipeline/schemas/prefs.schema.json +1 -1
- package/pipeline/scripts/_autopilot-outcomes.mjs +2 -0
- package/pipeline/scripts/audit-log-rotate.sh +3 -0
- package/pipeline/scripts/audit-log.sh +10 -2
- package/pipeline/scripts/autopilot-intake.mjs +17 -15
- package/pipeline/scripts/autopilot-publish.mjs +16 -1
- package/pipeline/scripts/autopilot-runner.mjs +20 -0
- package/pipeline/scripts/capture-resume.sh +51 -37
- package/pipeline/scripts/check-derived-drift.mjs +1 -1
- package/pipeline/scripts/doctor.mjs +21 -0
- package/pipeline/scripts/log-metric.sh +5 -0
- package/pipeline/scripts/plugin-refresh.sh +92 -0
- package/pipeline/scripts/pr-request.mjs +9 -3
- package/pipeline/scripts/update-check.sh +73 -21
- package/pipeline/scripts/usage-report.mjs +4 -4
- package/pipeline/skills/.skill-manifest.json +8 -8
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -4
- package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-jira/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +2 -4
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +1 -1
|
@@ -4,6 +4,7 @@ description: "Phase 1 adversarial plan critic - runs once after the plan valid
|
|
|
4
4
|
model: fable
|
|
5
5
|
preferredModel: fable
|
|
6
6
|
modelRationale: "Finding the failure a planner did not see is judgement over the whole plan, analysis and codebase, so it takes the top rung. Resolved through model-dispatch.sh with a fable default: opus while the fable rung is off, the same resolution review uses. On opus the critic is the planner's own model, which weakens the independence the role exists for; plan-critique-gate.mjs, not the critic, decides the gate either way."
|
|
7
|
+
disallowedTools: Write, Edit, NotebookEdit
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
# Plan Critic Agent - Phase 1, one round
|
|
@@ -4,6 +4,7 @@ description: Security specialist - static application security review across m
|
|
|
4
4
|
model: opus
|
|
5
5
|
preferredModel: opus
|
|
6
6
|
modelRationale: "Security reasoning + compliance catalog cross-reference (OWASP Web/API Top 10, OWASP Mobile Top 10, Apple ITMS, Google Play policy) plus honest confidence calibration on subtle vulnerabilities (auth-flow gaps, injection sinks, SSRF, cert-pinning bypass, sensitive-data leaks). False negatives are expensive and a wrong severity is worse than silence; opus (top available tier) keeps the miss rate and the miscalibration rate low."
|
|
7
|
+
disallowedTools: Write, Edit, NotebookEdit
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
You are a static application security auditor. You read code, configuration and dependency manifests and report vulnerabilities. You do NOT run the application, fire payloads, or reach a live target: this is a defensive, read-only review. Everything you claim is grounded in something you can point to in the source.
|
|
@@ -4,6 +4,7 @@ description: "Phase 0 Step 9 task ambiguity scorer - reads task description, e
|
|
|
4
4
|
model: haiku
|
|
5
5
|
preferredModel: haiku
|
|
6
6
|
modelRationale: "Ambiguity scoring is a low-stakes classification + targeted question generation. Haiku hits this well - short context (issue body + Jira description), structured JSON output, no reasoning chain needed. Sonnet override is available via PHASE_MODEL_OVERRIDE for repos where ambiguity nuance matters (legal, regulated domains)."
|
|
7
|
+
disallowedTools: Write, Edit, NotebookEdit
|
|
7
8
|
---
|
|
8
9
|
|
|
9
10
|
# Task Clarifier Agent - Phase 0 Step 9
|
|
@@ -191,8 +191,8 @@ WIKI_BRANCH=$(git -C "$WIKI_REL_ABS" symbolic-ref refs/remotes/origin/HEAD 2>/de
|
|
|
191
191
|
|
|
192
192
|
2. **Fetch design-source screenshot** (Figma by default; other sources swap the fetch call):
|
|
193
193
|
```bash
|
|
194
|
-
RESPONSE=$(
|
|
195
|
-
|
|
194
|
+
RESPONSE=$(printf 'X-Figma-Token: %s\n' "$DESIGN_TOKEN" | curl -s -H @- \
|
|
195
|
+
"https://api.figma.com/v1/images/{fileKey}?ids={nodeId}&format=png&scale=2")
|
|
196
196
|
IMAGE_URL=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['images']['{nodeId}'])")
|
|
197
197
|
curl -s -o "$WIKI_REL_ABS/{wikiCategory}/{ComponentName}/assets/screenshot.png" "$IMAGE_URL"
|
|
198
198
|
```
|
|
@@ -209,7 +209,9 @@ WIKI_BRANCH=$(git -C "$WIKI_REL_ABS" symbolic-ref refs/remotes/origin/HEAD 2>/de
|
|
|
209
209
|
cd "$WIKI_REL_ABS"
|
|
210
210
|
git add "{wikiCategory}/{ComponentName}/"
|
|
211
211
|
git -c user.name="{identity.name}" -c user.email="{identity.email}" \
|
|
212
|
-
commit -
|
|
212
|
+
commit -F - <<'MA_MSG'
|
|
213
|
+
docs({scope}): add {ComponentName} wiki pages [{jiraId}]
|
|
214
|
+
MA_MSG
|
|
213
215
|
git fetch origin && git rebase "origin/$WIKI_BRANCH"
|
|
214
216
|
git push origin "HEAD:$WIKI_BRANCH"
|
|
215
217
|
```
|
|
@@ -250,7 +252,7 @@ If a GitHub Issue tracks this component, update its body after commit + PR:
|
|
|
250
252
|
- Fill the `### Pull Requests` section with PR URL(s), one row per submodule touched
|
|
251
253
|
- Flip Implementation / Testing / Code Connect flags from Pending to Done using whatever marker the template uses (match in place, don't introduce new markers)
|
|
252
254
|
- Flip the Wiki flag to Done (SubPhase 4F already pushed the wiki pages)
|
|
253
|
-
-
|
|
255
|
+
- Write the updated body to a temp file and apply it with `gh issue edit {issueNo} --body-file <file>` - preserve every other section unchanged
|
|
254
256
|
- NEVER use `Closes:` / `Fixes:` / `Resolves:` keywords - use `Ref:` or `Related:` instead
|
|
255
257
|
|
|
256
258
|
Skip this phase if the task has no GitHub Issue (Jira-only or free-text runs).
|
|
@@ -76,7 +76,7 @@ The same name works on both sides and both mechanisms already exist:
|
|
|
76
76
|
|
|
77
77
|
| Source | Query | Extra condition |
|
|
78
78
|
|---|---|---|
|
|
79
|
-
| GitHub | `gh issue list --repo <r> --label <label> --state open` |
|
|
79
|
+
| 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 |
|
|
80
80
|
| 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 |
|
|
81
81
|
|
|
82
82
|
Two Jira caveats worth saying out loud when Jira is selected: labels are one
|
|
@@ -31,7 +31,7 @@ ACCOUNTS=$(~/.claude/lib/account-resolver.sh --providers jira)
|
|
|
31
31
|
Fetch projects:
|
|
32
32
|
```bash
|
|
33
33
|
TOKEN=$(~/.claude/lib/credential-store.sh get "$ACCOUNT_JIRA_TOKEN_KEY")
|
|
34
|
-
|
|
34
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
35
35
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/project" \
|
|
36
36
|
| python3 -c 'import json,sys; [print(p["key"]+"\t"+p["name"]) for p in json.load(sys.stdin)]'
|
|
37
37
|
```
|
|
@@ -45,7 +45,7 @@ The same list, already merged, sorted and scrubbed of hosts and URLs, is `node "
|
|
|
45
45
|
Combine open issues across selected projects, sort by priority + status:
|
|
46
46
|
```bash
|
|
47
47
|
JQL="assignee=currentUser() AND project in (${JIRA_PROJECT_KEYS}) AND resolution = EMPTY AND status not in (Done,Closed,Cancelled)"
|
|
48
|
-
|
|
48
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
49
49
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/search?jql=$(printf '%s' "$JQL" | python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read()))')&fields=key,summary,status,issuetype,priority,resolution,fixVersions&maxResults=50"
|
|
50
50
|
```
|
|
51
51
|
|
|
@@ -88,7 +88,7 @@ The upstream mapping is **configuration, never hardcoded** (it can reference pri
|
|
|
88
88
|
}
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
**The plugin cache is a mirror, not the authority.** `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/` only holds whatever the last `claude marketplace update` fetched. Reading it as the current upstream version is how this step reported "up to date" while the derivation was four releases behind: the cache sat at 0.2.1 and upstream was at 0.4.1. Resolve in the order below and never let the cache alone produce an "up to date" verdict.
|
|
91
|
+
**The plugin cache is a mirror, not the authority.** `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/` only holds whatever the last `claude plugin marketplace update` fetched. Reading it as the current upstream version is how this step reported "up to date" while the derivation was four releases behind: the cache sat at 0.2.1 and upstream was at 0.4.1. Resolve in the order below and never let the cache alone produce an "up to date" verdict.
|
|
92
92
|
|
|
93
93
|
**Which manifest carries the version** is `upstreamVersionSource`, default `marketplace.json`. That is what a marketplace consumer actually resolves, and some upstreams keep per-plugin `plugin.json` versions deliberately unused - reading those records a version nobody ships. An entry here was recorded at 0.7.0 from `plugin.json` while the upstream marketplace said 0.6.0 and its CHANGELOG stated in as many words that `plugin.json` is not used.
|
|
94
94
|
|
|
@@ -131,7 +131,7 @@ BB_USER=$("$CRED_STORE" get "$USER_KEY")
|
|
|
131
131
|
BB_TOKEN=$("$CRED_STORE" get "$TOKEN_KEY")
|
|
132
132
|
|
|
133
133
|
DIFF_FILE="/tmp/multi-agent-review-${TASK_ID}-diff.patch"
|
|
134
|
-
http=$(curl -sS -m 60
|
|
134
|
+
http=$(curl -sS -m 60 --config <(printf 'user = "%s:%s"\n' "$BB_USER" "$BB_TOKEN") -H "Accept: text/plain" \
|
|
135
135
|
"https://$bb_server_host/rest/api/1.0/projects/$bb_project_key/repos/$bb_repo_slug/pull-requests/$pr_number/diff?contextLines=10" \
|
|
136
136
|
-o "$DIFF_FILE" -w '%{http_code}')
|
|
137
137
|
if [ "$http" != "200" ]; then
|
|
@@ -446,7 +446,7 @@ Save to `prefs.global.hosts.{service}`. For Fortify, also ask `Fortify project v
|
|
|
446
446
|
For Jira, **discover the project keys instead of asking the user to recall them.** The token is already saved and the host is already known, so ask Jira which projects this person actually works in - a corporate instance has thousands of projects, and a typed key is a typo waiting to route branches and new issues at the wrong board:
|
|
447
447
|
|
|
448
448
|
```bash
|
|
449
|
-
|
|
449
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
450
450
|
"https://{JIRA_HOST}/rest/api/2/search?jql=assignee%3DcurrentUser()%20OR%20reporter%3DcurrentUser()%20ORDER%20BY%20updated%20DESC&fields=project&maxResults=100" \
|
|
451
451
|
| jq -r '[.issues[].fields.project | "\(.key)\t\(.name)"] | unique | .[]'
|
|
452
452
|
```
|
|
@@ -804,7 +804,7 @@ Stack skills ship as versioned plugins in the `{owner}/multi-agent-plugins` mark
|
|
|
804
804
|
|
|
805
805
|
1. Ensure the marketplace is known (idempotent):
|
|
806
806
|
```bash
|
|
807
|
-
claude
|
|
807
|
+
bash "$HOME/.claude/scripts/plugin-refresh.sh" {owner} --add-only
|
|
808
808
|
```
|
|
809
809
|
2. Ask the detector, then derive. No marker list here: a second list is a second
|
|
810
810
|
answer, and this one defaulted to `ai-ios-toolkit` when nothing matched - how
|
|
@@ -80,10 +80,8 @@ REPO_SETTINGS=".claude/settings.json"
|
|
|
80
80
|
MP="multi-agent-plugins"
|
|
81
81
|
|
|
82
82
|
# --- ensure the marketplace is known (idempotent) -------------------------
|
|
83
|
-
|
|
84
|
-
claude marketplace add {owner}
|
|
85
|
-
|| echo "note: add the marketplace once with: claude marketplace add {owner}/multi-agent-plugins"
|
|
86
|
-
fi
|
|
83
|
+
bash "$HOME/.claude/scripts/plugin-refresh.sh" {owner} --add-only \
|
|
84
|
+
|| echo "note: add the marketplace once with: claude plugin marketplace add {owner}/$MP"
|
|
87
85
|
|
|
88
86
|
# Enable-mode only: with zero args the write rules below would set every stack
|
|
89
87
|
# toolkit to false (nothing but the always-on pair is in $ON), silently wiping the repo's
|
|
@@ -234,7 +234,7 @@ node "$HOME/multi-agent-pipeline/pipeline/scripts/build-stack-plugins.mjs" --dry
|
|
|
234
234
|
node "$HOME/multi-agent-pipeline/pipeline/scripts/build-stack-plugins.mjs"
|
|
235
235
|
```
|
|
236
236
|
|
|
237
|
-
**Version-based management:** the generator bumps a plugin's `version` (patch) only when its skill set actually changed. Idempotent - a no-op run bumps nothing. Consumers pick up new versions via `/multi-agent:update` (which
|
|
237
|
+
**Version-based management:** the generator bumps a plugin's `version` (patch) only when its skill set actually changed. Idempotent - a no-op run bumps nothing. Consumers pick up new versions via `/multi-agent:update` (which refreshes the catalog with `claude plugin marketplace update multi-agent-plugins`, then runs `claude plugin update` for each installed plugin).
|
|
238
238
|
|
|
239
239
|
**Help + README freshness (mandatory when the generator reported any change):**
|
|
240
240
|
|
|
@@ -329,7 +329,7 @@ for d in tools/*/; do printf '%s' "$PACK" | grep -q "$d" || echo "ABORT: $d miss
|
|
|
329
329
|
```bash
|
|
330
330
|
gh auth switch --user {owner}
|
|
331
331
|
git add -A
|
|
332
|
-
git commit -
|
|
332
|
+
git commit -F "/tmp/release-msg-$NEW.txt" # "{type}(v$NEW): {what changed}", written with the file tool
|
|
333
333
|
git tag "v$NEW" && git push origin main --tags
|
|
334
334
|
```
|
|
335
335
|
|
|
@@ -52,7 +52,9 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
|
|
|
52
52
|
line above is where the floor is reported, so the user sees why a run halted;
|
|
53
53
|
`$REQUIRED` does not survive into the later shell blocks.
|
|
54
54
|
|
|
55
|
-
2. **Stop early when already current
|
|
55
|
+
2. **Stop early when already current.** When the line below prints, skip
|
|
56
|
+
step 3 (no download, no reinstall) and continue at step 4b, so the stack
|
|
57
|
+
plugins are still refreshed:
|
|
56
58
|
```bash
|
|
57
59
|
[ "$CUR" = "$LATEST" ] && echo "✓ Already up to date: v$LATEST"
|
|
58
60
|
```
|
|
@@ -83,18 +85,11 @@ A git clone of the pipeline repo is a maintainer workspace, kept in sync by `/mu
|
|
|
83
85
|
|
|
84
86
|
The installer refreshes every configured CLI target (Claude Code, Copilot CLI, Codex CLI) and writes the new version to `$HOME/.claude/.pipeline-version`.
|
|
85
87
|
|
|
86
|
-
4b. **Update the stack
|
|
88
|
+
4b. **Update the stack plugins** (catalog refresh, then every installed plugin in its own scope):
|
|
87
89
|
```bash
|
|
88
|
-
|
|
89
|
-
# Refresh the marketplace so newly published plugin versions are picked up.
|
|
90
|
-
if claude marketplace list 2>/dev/null | grep -q "multi-agent-plugins"; then
|
|
91
|
-
claude marketplace update multi-agent-plugins 2>/dev/null \
|
|
92
|
-
|| claude marketplace add {owner}/multi-agent-plugins 2>/dev/null
|
|
93
|
-
echo " -> marketplace multi-agent-plugins refreshed"
|
|
94
|
-
else
|
|
95
|
-
echo " -> marketplace not added yet; add with: claude marketplace add {owner}/multi-agent-plugins"
|
|
96
|
-
fi
|
|
90
|
+
bash "$HOME/.claude/scripts/plugin-refresh.sh" {owner}
|
|
97
91
|
```
|
|
92
|
+
A non-zero exit means a step failed and the CLI's own error is already on screen. Report it to the user; do not retry silently.
|
|
98
93
|
|
|
99
94
|
4c. **Prune retired adapter files** (Cursor / Antigravity / Copilot Chat were removed in v10.7.0 - the pipeline targets Claude Code, Copilot CLI and Codex CLI):
|
|
100
95
|
```bash
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* redact.mjs - replace every credential in a text with a visible marker.
|
|
4
|
+
*
|
|
5
|
+
* Uses the certain rules of outbound-gate.mjs, so the log writers, the commit
|
|
6
|
+
* hook and the outbound gate recognise one provider set. A match becomes
|
|
7
|
+
* `[redacted:<rule>]`; the text around it is kept, because a log line is read
|
|
8
|
+
* to debug a failure and the message is what makes it useful.
|
|
9
|
+
*
|
|
10
|
+
* As a CLI it reads stdin and writes the redacted text to stdout. It never
|
|
11
|
+
* fails its caller: on an internal error it writes the input unchanged.
|
|
12
|
+
*
|
|
13
|
+
* @module pipeline/lib/redact
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { RULES } from "./outbound-gate.mjs";
|
|
17
|
+
import { invokedDirectly } from "./invoked-directly.mjs";
|
|
18
|
+
|
|
19
|
+
const CERTAIN = RULES.filter((r) => r.certain);
|
|
20
|
+
|
|
21
|
+
/** @param {string} text */
|
|
22
|
+
export function redact(text) {
|
|
23
|
+
if (typeof text !== "string" || !text) return text;
|
|
24
|
+
let out = text;
|
|
25
|
+
for (const rule of CERTAIN) {
|
|
26
|
+
const re = new RegExp(
|
|
27
|
+
rule.re.source,
|
|
28
|
+
rule.re.flags.includes("g") ? rule.re.flags : `${rule.re.flags}g`,
|
|
29
|
+
);
|
|
30
|
+
out = out.replace(re, () =>
|
|
31
|
+
rule.name === "url-with-credentials" ? `[redacted:${rule.name}]@` : `[redacted:${rule.name}]`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (invokedDirectly(import.meta.url)) {
|
|
38
|
+
let input = "";
|
|
39
|
+
process.stdin.setEncoding("utf8");
|
|
40
|
+
process.stdin.on("data", (d) => (input += d));
|
|
41
|
+
process.stdin.on("end", () => {
|
|
42
|
+
let out;
|
|
43
|
+
try {
|
|
44
|
+
out = redact(input);
|
|
45
|
+
} catch {
|
|
46
|
+
out = input;
|
|
47
|
+
}
|
|
48
|
+
process.stdout.write(out);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -251,7 +251,7 @@ The two adapters run in the same phase over the same source markdown, so the fai
|
|
|
251
251
|
## Reviewer-preserving Bitbucket payload (required)
|
|
252
252
|
|
|
253
253
|
```bash
|
|
254
|
-
PR_JSON=$(curl -s
|
|
254
|
+
PR_JSON=$(curl -s --config <(printf 'user = "%s:%s"\n' "$BB_USER" "$BB_TOKEN") "$PR_URL")
|
|
255
255
|
REVIEWERS=$(jq '[.reviewers[] | {user: {name: .user.name}, approved, status}]' <<< "$PR_JSON")
|
|
256
256
|
VERSION=$(jq -r '.version' <<< "$PR_JSON")
|
|
257
257
|
TITLE=$(jq -r '.title' <<< "$PR_JSON")
|
|
@@ -268,7 +268,7 @@ jq -n --rawfile body /tmp/channels-$TASK_ID-pr.md \
|
|
|
268
268
|
reviewers: $reviewers, fromRef: $fromRef, toRef: $toRef, draft: $draft}' \
|
|
269
269
|
> /tmp/channels-$TASK_ID-pr-payload.json
|
|
270
270
|
|
|
271
|
-
curl -s -X PUT
|
|
271
|
+
curl -s -X PUT --config <(printf 'user = "%s:%s"\n' "$BB_USER" "$BB_TOKEN") -H "Content-Type: application/json" \
|
|
272
272
|
--data-binary @/tmp/channels-$TASK_ID-pr-payload.json "$PR_URL"
|
|
273
273
|
```
|
|
274
274
|
|
|
@@ -167,7 +167,8 @@ reaches Phase 4 and stops there.
|
|
|
167
167
|
### hook-coverage
|
|
168
168
|
|
|
169
169
|
The blocking `PreToolUse` gates from `templates/claude-hooks.json` are present in
|
|
170
|
-
`settings.json
|
|
170
|
+
`settings.json`, and none of them is registered twice on the same matcher
|
|
171
|
+
(Claude Code runs a duplicate on every matching call). WARN. SKIP when the template is not installed - which was the
|
|
171
172
|
permanent state until the installer began copying `templates/`, and is exactly
|
|
172
173
|
the case this severity exists to make visible.
|
|
173
174
|
|
|
@@ -19,7 +19,7 @@ Before any analysis, check if this issue was already fixed by someone else.
|
|
|
19
19
|
3. **Jira comments/status** - fetch latest comments to see if someone reported a fix:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
22
|
+
printf 'Authorization: Bearer %s\n' "$JIRA_TOKEN" | curl -s -H @- \
|
|
23
23
|
"$JIRA_BASE/issue/{jiraId}?fields=status,comment&expand=changelog" \
|
|
24
24
|
| jq '.fields.status.name, .fields.comment.comments[-3:][]?.body'
|
|
25
25
|
```
|
|
@@ -589,6 +589,11 @@ hashes `~/.claude/{scripts,lib}`, the settings files, the profile and
|
|
|
589
589
|
`~/Library/LaunchAgents` before and after publish verification (`hostFingerprint`
|
|
590
590
|
in `autopilot-publish.mjs`) and refuses to push if any changed.
|
|
591
591
|
|
|
592
|
+
It also refuses to launch (`blocked-version-floor`) while the installed version
|
|
593
|
+
is below the published `required` floor (`require-supported-version.sh` exits
|
|
594
|
+
3): the child's Phase 0 would halt on the same check, and the update it needs is
|
|
595
|
+
refused unattended. Like every `blocked-*` outcome it is not an attempt.
|
|
596
|
+
|
|
592
597
|
The profile's `env` reaches the session and the commands it runs. The Claude
|
|
593
598
|
Code docs pass environment to a stdio MCP server through the server entry, so
|
|
594
599
|
pin the two toolkit variables there as well: `claude mcp add --env
|
|
@@ -79,7 +79,7 @@ Fallback chain (same as `$HOME/.claude/multi-agent-refs/issue-jira-triad.md` Cre
|
|
|
79
79
|
2. `prefs.global.defaultJiraKey`
|
|
80
80
|
3. Fetch and ask:
|
|
81
81
|
```bash
|
|
82
|
-
|
|
82
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
83
83
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/project" \
|
|
84
84
|
| python3 -c 'import json,sys; [print(p["key"]+"\t"+p["name"]) for p in json.load(sys.stdin)]'
|
|
85
85
|
```
|
|
@@ -108,7 +108,7 @@ Three GET groups. Run them before drafting anything.
|
|
|
108
108
|
**5a. Sample recent same-type issues:**
|
|
109
109
|
```bash
|
|
110
110
|
JQL="project = ${PROJECT_KEY} AND issuetype = ${ISSUE_TYPE} ORDER BY created DESC"
|
|
111
|
-
|
|
111
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
112
112
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/search?jql=$(printf '%s' "$JQL" | python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read()))')&fields=summary,description,labels,components,priority,fixVersions,issuelinks&maxResults=30"
|
|
113
113
|
```
|
|
114
114
|
Extract from the sample (thresholds are guidance, not hard gates):
|
|
@@ -123,7 +123,7 @@ Extract from the sample (thresholds are guidance, not hard gates):
|
|
|
123
123
|
|
|
124
124
|
**5b. Field discovery (createmeta):**
|
|
125
125
|
```bash
|
|
126
|
-
|
|
126
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
127
127
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/issue/createmeta?projectKeys=${PROJECT_KEY}&issuetypeNames=${ISSUE_TYPE}&expand=projects.issuetypes.fields"
|
|
128
128
|
```
|
|
129
129
|
- 404 or empty `projects[]` (newer Jira DC deprecates the classic form) → fall back to the paged endpoint: `GET /rest/api/2/issue/createmeta/{PROJECT_KEY}/issuetypes` then `GET /rest/api/2/issue/createmeta/{PROJECT_KEY}/issuetypes/{issuetypeId}`.
|
|
@@ -136,9 +136,9 @@ curl -s -H "Authorization: Bearer $TOKEN" \
|
|
|
136
136
|
|
|
137
137
|
**5c. Board + active sprint (Agile REST):**
|
|
138
138
|
```bash
|
|
139
|
-
|
|
139
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
140
140
|
"https://$ACCOUNT_JIRA_HOST/rest/agile/1.0/board?projectKeyOrId=${PROJECT_KEY}"
|
|
141
|
-
|
|
141
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -s -H @- \
|
|
142
142
|
"https://$ACCOUNT_JIRA_HOST/rest/agile/1.0/board/${BOARD_ID}/sprint?state=active"
|
|
143
143
|
```
|
|
144
144
|
- Multiple boards → AskUserQuestion (board name + type). One board → silent. Zero boards or Agile API 404 → the sprint option is silently omitted; the preview notes "backlog only (no board found)".
|
|
@@ -217,7 +217,7 @@ jq -n --rawfile desc /tmp/generate-issue-$$-escaped.txt \
|
|
|
217
217
|
'{fields: {project: {key: $key}, issuetype: {name: $type}, summary: $summary, description: $desc}}' \
|
|
218
218
|
> /tmp/generate-issue-$$-payload.json
|
|
219
219
|
# merge approved optional fields (priority / labels / components / epic / assignee / required customs) into .fields with jq before POST
|
|
220
|
-
curl -sS -X POST -H
|
|
220
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -sS -X POST -H @- -H "Content-Type: application/json" \
|
|
221
221
|
--data-binary @/tmp/generate-issue-$$-payload.json \
|
|
222
222
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/issue"
|
|
223
223
|
```
|
|
@@ -231,20 +231,20 @@ Each step is failure-isolated: on error, warn + continue - the issue already e
|
|
|
231
231
|
1. **Sprint** (when chosen in step 8/9):
|
|
232
232
|
```bash
|
|
233
233
|
jq -n --arg key "$NEW_KEY" '{issues: [$key]}' > /tmp/generate-issue-$$-sprint.json
|
|
234
|
-
curl -sS -X POST -H
|
|
234
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -sS -X POST -H @- -H "Content-Type: application/json" \
|
|
235
235
|
--data-binary @/tmp/generate-issue-$$-sprint.json \
|
|
236
236
|
"https://$ACCOUNT_JIRA_HOST/rest/agile/1.0/sprint/${SPRINT_ID}/issue"
|
|
237
237
|
```
|
|
238
238
|
Post-create move instead of a Sprint custom field in the create payload: field-id-agnostic, works on every Jira Software instance.
|
|
239
239
|
2. **Figma remote link** (when `FIGMA_URL` set):
|
|
240
240
|
```bash
|
|
241
|
-
curl -sS -X POST -H
|
|
241
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -sS -X POST -H @- -H "Content-Type: application/json" \
|
|
242
242
|
-d '{"object":{"url":"'"$FIGMA_URL"'","title":"Figma design"}}' \
|
|
243
243
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/issue/${NEW_KEY}/remotelink"
|
|
244
244
|
```
|
|
245
245
|
3. **Swagger remote link** (when `SWAGGER_URL` set):
|
|
246
246
|
```bash
|
|
247
|
-
curl -sS -X POST -H
|
|
247
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -sS -X POST -H @- -H "Content-Type: application/json" \
|
|
248
248
|
-d '{"object":{"url":"'"$SWAGGER_URL"'","title":"API contract"}}' \
|
|
249
249
|
"https://$ACCOUNT_JIRA_HOST/rest/api/2/issue/${NEW_KEY}/remotelink"
|
|
250
250
|
```
|
|
@@ -24,10 +24,10 @@ mapping would have given you.
|
|
|
24
24
|
|
|
25
25
|
## Keeping the value out of everything
|
|
26
26
|
|
|
27
|
-
`curl`
|
|
27
|
+
The header goes to `curl` on stdin, so the token never reaches argv (where `ps` can read it):
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
|
|
30
|
+
printf 'Authorization: Bearer %s\n' "$TOKEN" | curl -H @- \
|
|
31
31
|
"https://$(jq -r '.global.hosts.jira' "$PREFS")/rest/api/3/issue/PROJ-1"
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -234,7 +234,7 @@ Classify and fetch external data:
|
|
|
234
234
|
[ -z "$JIRA_KEY" ] && JIRA_KEY="${USER}_Jira_Access_Token"
|
|
235
235
|
# unattended: skipped - a run reads no credential itself (features/unattended-security.md)
|
|
236
236
|
JIRA_TOKEN=$(~/.claude/lib/credential-store.sh get "$JIRA_KEY" 2>/dev/null)
|
|
237
|
-
|
|
237
|
+
printf 'Authorization: Bearer %s\n' "$JIRA_TOKEN" | curl -s -H @- \
|
|
238
238
|
"https://{JIRA_HOST}/rest/api/2/issue/{jiraId}?fields=summary,issuetype,status"
|
|
239
239
|
```
|
|
240
240
|
2. Extract: summary, issueType (Bug/Story/Task/Feature)
|
|
@@ -177,8 +177,9 @@ For each task (respecting dependency order):
|
|
|
177
177
|
6. **Intermediate commit** (after each completed task in the plan):
|
|
178
178
|
```bash
|
|
179
179
|
git -C "{worktreePath}" add -A
|
|
180
|
-
git -C "{worktreePath}" commit -
|
|
180
|
+
git -C "{worktreePath}" commit -F "/tmp/wip-message-$TASK_ID.txt"
|
|
181
181
|
```
|
|
182
|
+
Write `wip({scope}): {task summary} [{jiraId}]` to `/tmp/wip-message-$TASK_ID.txt` with the file tool first. The summary comes from the issue, and inside a double-quoted `-m` the shell would run any `$(...)` in it.
|
|
182
183
|
WIP commits are squashed in Phase 4 before PR. This prevents work loss if a later task fails or the session crashes.
|
|
183
184
|
7. Update task status: `completed` (apply the same verify rule - re-read the Status field and retry once on mismatch)
|
|
184
185
|
8. Log: "Phase 2: {task} - Test + Build passed"
|
|
@@ -127,7 +127,7 @@ Branch **deterministically**, no implicit fallback. Read `agent-state.json` and
|
|
|
127
127
|
|
|
128
128
|
The script salvages before removing, keeps the branch, and does **not** check it out - the user's HEAD and uncommitted work stay put. On success it stamps `worktreeRemovedAt`, `artifactsPath` and `worktreePath: null` into the **salvaged** `agent-state.json` itself. Do NOT write those via `write-state.mjs "$STATE_FILE"`: in single-repo mode that path went away with the worktree, so the write fails, the fields land nowhere, and Phase 5 reads a dead path and silently skips the triage ingest. After a removal re-point `STATE_FILE` at `$(jq -r .artifactsPath <<< "$FIN")/agent-state.json`. Exit 3 is a safe skip (uncommitted changes, unpushed or detached HEAD, `--local`, cwd inside the tree, preference off): report and continue. Never `--force`. Contract: [`features/worktree-finalize.md`]($HOME/.claude/multi-agent-refs/features/worktree-finalize.md).
|
|
129
129
|
|
|
130
|
-
10. **Issue body update** (GitHub Issue only): if the issue body has a `### Pull Requests` section and/or a `### Progress` table, fill in the PR URL(s) (one row per submodule - e.g. `- **common:** {url}`, `- **uicomponents:** {url}`) and flip the Implementation / Testing / Code Connect flags from Pending to Done using whatever marker the template uses (match in place, do NOT introduce new markers). Apply with `gh issue edit {issueNo} --body
|
|
130
|
+
10. **Issue body update** (GitHub Issue only): if the issue body has a `### Pull Requests` section and/or a `### Progress` table, fill in the PR URL(s) (one row per submodule - e.g. `- **common:** {url}`, `- **uicomponents:** {url}`) and flip the Implementation / Testing / Code Connect flags from Pending to Done using whatever marker the template uses (match in place, do NOT introduce new markers). Apply it with `gh issue edit {issueNo} --body-file <file>`, preserving every other section unchanged.
|
|
131
131
|
11. **NEVER close or resolve the issue** - neither GitHub Issue nor Jira. Issues require team review (4 approvals) before closing. Only post a comment with commit/PR URLs.
|
|
132
132
|
12. Log: "Phase 4: Commit {sha} - PR #{number}, worktree {removed|kept: <reason>}"
|
|
133
133
|
|
|
@@ -174,13 +174,13 @@ Inputs are read from state and the worktree before writing, not recalled from th
|
|
|
174
174
|
|
|
175
175
|
**Default Reviewers**: use `prefs.projects[{project}].defaultReviewers` when set; otherwise ask once ("Default reviewers for this project? comma-separated, or skip") and persist the answer there. Per-remote resolution rules are in "Default reviewers (required on every PR)" below - this is the prefs half only.
|
|
176
176
|
|
|
177
|
-
**Standard PR (bug fix, feature, refactor)** - invocation shape
|
|
177
|
+
**Standard PR (bug fix, feature, refactor)** - invocation shape. Title (`{type}({scope}): {summary} [{jiraId}]`) and body go to files first, so issue text never sits in a quoted argument:
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
|
-
REVIEWERS=$(jq -r '.projects[
|
|
180
|
+
REVIEWERS=$(jq -r --arg p "$PROJECT" '.projects[$p].defaultReviewers // [] | map("--reviewer " + .) | join(" ")' "$PREFS_FILE")
|
|
181
181
|
# unattended: skipped - pr-request.mjs replaces the PR (features/unattended-security.md)
|
|
182
182
|
gh pr create --base {baseBranch} --head {branch} $REVIEWERS \
|
|
183
|
-
--title "
|
|
183
|
+
--title "$(cat /tmp/pr-title-$TASK_ID.txt)" \
|
|
184
184
|
--body-file /tmp/pr-body-$TASK_ID.md
|
|
185
185
|
```
|
|
186
186
|
|
|
@@ -39,7 +39,7 @@ BB_HOST="$(jq -r '.global.bitbucketHost // empty' "$PREFS_FILE")"
|
|
|
39
39
|
BB_USER="$(~/.claude/lib/credential-store.sh get "$(jq -r '.global.keychainMapping.bitbucket_user' "$PREFS_FILE")" 2>/dev/null)"
|
|
40
40
|
BB_TOK="$(~/.claude/lib/credential-store.sh get "$(jq -r '.global.keychainMapping.bitbucket_token' "$PREFS_FILE")" 2>/dev/null)"
|
|
41
41
|
CUTOFF_MS=$(( ( $(date +%s) - 7776000 ) * 1000 )) # 90 days
|
|
42
|
-
curl -sfS
|
|
42
|
+
curl -sfS --config <(printf 'user = "%s:%s"\n' "$BB_USER" "$BB_TOK") \
|
|
43
43
|
"https://${BB_HOST}/rest/api/1.0/repos?permission=REPO_WRITE&limit=200" \
|
|
44
44
|
| jq -r --argjson cutoff "$CUTOFF_MS" '
|
|
45
45
|
.values[] | select((.updatedDate // 0) >= $cutoff) |
|
|
@@ -59,7 +59,7 @@ CUTOFF_ISO="$(date -u -v-90d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '90 d
|
|
|
59
59
|
gh api --paginate 'user/repos?affiliation=owner,collaborator&sort=pushed&per_page=100' \
|
|
60
60
|
-q ".[] | select(.pushed_at > \"$CUTOFF_ISO\") | {full: .full_name, clone: .clone_url}"
|
|
61
61
|
# Fallback without gh:
|
|
62
|
-
|
|
62
|
+
printf 'Authorization: Bearer %s\n' "$GH_TOK" | curl -sfS -H @- \
|
|
63
63
|
'https://api.github.com/user/repos?affiliation=owner,collaborator&sort=pushed&per_page=100'
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
"default": "agent-queue",
|
|
51
51
|
"description": "Only issues carrying this label are picked up. `gh issue list --label` does the filtering server-side."
|
|
52
52
|
},
|
|
53
|
+
"githubAssignee": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"default": "@me",
|
|
56
|
+
"description": "Only issues assigned to this login are picked up (`gh issue list --assignee`), matching the Jira search's assignee = currentUser(). An issue form can apply the queue label for anyone who opens an issue, and an unattended run acts on the issue's text. \"*\" picks up every labelled issue."
|
|
57
|
+
},
|
|
53
58
|
"jiraJql": {
|
|
54
59
|
"type": "string",
|
|
55
60
|
"description": "Full JQL, so an instance that cannot use labels is a config change rather than a redesign. The label form is sugar for: assignee = currentUser() AND labels = \"<label>\" AND resolution = EMPTY AND status not in (Done, Closed, Cancelled) ORDER BY priority DESC, created ASC. Jira labels are a single global namespace anyone can write to, so on a shared instance prefer a qualified name, and note that the Labels field must be on the edit screen for the issue types in play. An issue that more than one repo's search returns is queued once, for the first repo in config order, so repos sharing a Jira instance should each carry a jiraJql that matches only their own issues."
|
|
@@ -1820,7 +1820,7 @@
|
|
|
1820
1820
|
},
|
|
1821
1821
|
"upstreamLocalClone": {
|
|
1822
1822
|
"type": "string",
|
|
1823
|
-
"description": "Working copy of the upstream repository, checked before the plugin cache. The cache under ~/.claude/plugins/cache is only as fresh as the last `claude marketplace update`, so it is a mirror and never the authority. A private upstream may also be unreachable over the API from the active account, in which case this clone is the only source."
|
|
1823
|
+
"description": "Working copy of the upstream repository, checked before the plugin cache. The cache under ~/.claude/plugins/cache is only as fresh as the last `claude plugin marketplace update`, so it is a mirror and never the authority. A private upstream may also be unreachable over the API from the active account, in which case this clone is the only source."
|
|
1824
1824
|
},
|
|
1825
1825
|
"upstreamRepoUrl": {
|
|
1826
1826
|
"type": "string",
|
|
@@ -41,6 +41,7 @@ export const OUTCOME = Object.freeze({
|
|
|
41
41
|
BLOCKED_NO_PATH: "blocked-no-path",
|
|
42
42
|
BLOCKED_GUARD_MISSING: "blocked-guard-missing",
|
|
43
43
|
BLOCKED_SANDBOX_UNAVAILABLE: "blocked-sandbox-unavailable",
|
|
44
|
+
BLOCKED_VERSION_FLOOR: "blocked-version-floor",
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
/**
|
|
@@ -107,6 +108,7 @@ export const BLOCKED = new Set([
|
|
|
107
108
|
OUTCOME.BLOCKED_NO_PATH,
|
|
108
109
|
OUTCOME.BLOCKED_GUARD_MISSING,
|
|
109
110
|
OUTCOME.BLOCKED_SANDBOX_UNAVAILABLE,
|
|
111
|
+
OUTCOME.BLOCKED_VERSION_FLOOR,
|
|
110
112
|
]);
|
|
111
113
|
|
|
112
114
|
/** Prefix families. `stopped-<status>` is a run that ended mid-way. */
|
|
@@ -89,6 +89,9 @@ if [ -s "$AUDIT_FILE" ]; then
|
|
|
89
89
|
# of the content it archived. gzip -f removes the plain file on success, so
|
|
90
90
|
# only touch whichever of the two actually exists.
|
|
91
91
|
[ -f "$ROTATED.gz" ] && touch "$ROTATED.gz"
|
|
92
|
+
# An archive holds the same events as the live file, so it gets the same mode.
|
|
93
|
+
[ -f "$ROTATED.gz" ] && chmod 600 "$ROTATED.gz" 2>/dev/null
|
|
94
|
+
[ -f "$ROTATED" ] && chmod 600 "$ROTATED" 2>/dev/null
|
|
92
95
|
[ -f "$ROTATED" ] && touch "$ROTATED"
|
|
93
96
|
fi
|
|
94
97
|
|
|
@@ -39,9 +39,10 @@ esac
|
|
|
39
39
|
AUDIT_FILE="${AUDIT_FILE:-${LOGS_ROOT:-$HOME/.claude/logs/multi-agent}/audit.jsonl}"
|
|
40
40
|
mkdir -p "$(dirname "$AUDIT_FILE")" 2>/dev/null || exit 0
|
|
41
41
|
|
|
42
|
-
# Escape a string for embedding inside a JSON double-quoted value.
|
|
42
|
+
# Escape a string for embedding inside a JSON double-quoted value. Newlines,
|
|
43
|
+
# carriage returns and tabs are escaped too: a raw one splits the JSONL line.
|
|
43
44
|
json_escape() {
|
|
44
|
-
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
45
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g; s/\r/\\r/g' | awk 'NR > 1 { printf "\\n" } { printf "%s", $0 }'
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
# SHA-256 hash the repo URL (never log plaintext per section18.2.2)
|
|
@@ -85,6 +86,13 @@ LINE="{\"ts\":\"${TS}\",\"event\":\"$(json_escape "$EVENT")\",\"service\":\"$(js
|
|
|
85
86
|
[ -n "$EXTRAS" ] && LINE="${LINE},${EXTRAS}"
|
|
86
87
|
LINE="${LINE}}"
|
|
87
88
|
|
|
89
|
+
# Caller-supplied values can carry a credential copied out of an error.
|
|
90
|
+
# redact.mjs replaces it with a marker; without node the line is written as is.
|
|
91
|
+
REDACT="$(dirname "${BASH_SOURCE[0]:-$0}")/../lib/redact.mjs"
|
|
92
|
+
[ -f "$REDACT" ] && LINE=$(printf '%s' "$LINE" | node "$REDACT" 2>/dev/null || printf '%s' "$LINE")
|
|
93
|
+
|
|
94
|
+
[ -f "$AUDIT_FILE" ] || (umask 077 && : > "$AUDIT_FILE") 2>/dev/null || true
|
|
95
|
+
chmod 600 "$AUDIT_FILE" 2>/dev/null || true
|
|
88
96
|
echo "$LINE" >> "$AUDIT_FILE" 2>/dev/null || true
|
|
89
97
|
|
|
90
98
|
# Opportunistic rotation. audit-log-rotate.sh documents a launchd/cron install,
|
|
@@ -245,22 +245,24 @@ function sh(cmd, args, opts = {}) {
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
/**
|
|
249
|
+
* The `gh issue list` arguments for one repo. Only issues assigned to the
|
|
250
|
+
* operator are listed by default, as the Jira search does with
|
|
251
|
+
* `assignee = currentUser()`: an issue form can apply the queue label for
|
|
252
|
+
* anyone who opens an issue, and an unattended run acts on the issue's text.
|
|
253
|
+
* `githubAssignee: "*"` lists every labelled issue.
|
|
254
|
+
*/
|
|
255
|
+
export function githubListArgs(repo) {
|
|
249
256
|
const label = repo.githubLabel || "agent-queue";
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
"--json",
|
|
260
|
-
"number,title,labels,url,createdAt",
|
|
261
|
-
"--limit",
|
|
262
|
-
"100",
|
|
263
|
-
]);
|
|
257
|
+
const assignee = repo.githubAssignee || "@me";
|
|
258
|
+
const args = ["issue", "list", "--repo", repo.nameWithOwner, "--label", label];
|
|
259
|
+
if (assignee !== "*") args.push("--assignee", assignee);
|
|
260
|
+
args.push("--state", "open", "--json", "number,title,labels,url,createdAt", "--limit", "100");
|
|
261
|
+
return args;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function fetchGitHub(repo) {
|
|
265
|
+
const out = sh("gh", githubListArgs(repo));
|
|
264
266
|
let rows;
|
|
265
267
|
try {
|
|
266
268
|
rows = JSON.parse(out || "[]");
|