@mmerterden/multi-agent-pipeline 11.4.0 → 11.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.md +1 -0
- package/index.js +9 -2
- package/install/_common.mjs +107 -5
- package/install/_telemetry.mjs +5 -23
- package/install/claude.mjs +70 -10
- package/install/copilot.mjs +75 -6
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- package/pipeline/lib/account-resolver.sh +61 -23
- package/pipeline/lib/channels-multi-repo.sh +7 -2
- package/pipeline/lib/count-lib.sh +27 -0
- package/pipeline/lib/credential-store.sh +23 -6
- package/pipeline/lib/extract-conventions.sh +27 -21
- package/pipeline/lib/fetch-confluence.sh +25 -13
- package/pipeline/lib/fetch-crashlytics.sh +30 -8
- package/pipeline/lib/fetch-fortify.sh +11 -2
- package/pipeline/lib/fetch-swagger.sh +18 -7
- package/pipeline/lib/figma-mcp-refresh.sh +26 -11
- package/pipeline/lib/figma-screenshot.sh +11 -2
- package/pipeline/lib/issue-fetcher.sh +31 -6
- package/pipeline/lib/multi-repo-pipeline.sh +84 -20
- package/pipeline/lib/plan-todos.sh +12 -3
- package/pipeline/lib/post-pr-review.sh +5 -3
- package/pipeline/lib/repo-cache.sh +53 -9
- package/pipeline/lib/review-watch.sh +26 -6
- package/pipeline/lib/shadow-git.sh +45 -4
- package/pipeline/lib/vercel-deploy.sh +10 -1
- package/pipeline/scripts/audit-log-rotate.sh +38 -10
- package/pipeline/scripts/check-md-links.mjs +34 -9
- package/pipeline/scripts/diff-risk-score.mjs +4 -1
- package/pipeline/scripts/evidence-gate.mjs +10 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +4 -4
- package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
- package/pipeline/scripts/keychain-save.sh +13 -9
- package/pipeline/scripts/lint-skills.mjs +40 -2
- package/pipeline/scripts/migrate-prefs.mjs +26 -7
- package/pipeline/scripts/phase-tracker.sh +71 -0
- package/pipeline/scripts/pre-commit-check.sh +39 -0
- package/pipeline/scripts/scan-skills.sh +217 -127
- package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
- package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
- package/pipeline/scripts/smoke-install-layout.sh +11 -3
- package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
- package/pipeline/scripts/smoke-pack-contents.sh +140 -0
- package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
- package/pipeline/scripts/smoke-shadow-git.sh +48 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
- package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
- package/pipeline/scripts/test-gap-scan.mjs +12 -1
- package/pipeline/scripts/triage-memory.mjs +10 -1
- package/pipeline/scripts/uninstall.mjs +105 -36
- package/pipeline/scripts/update-issue-progress.sh +60 -41
- package/pipeline/scripts/write-state.mjs +14 -4
package/install/copilot.mjs
CHANGED
|
@@ -22,7 +22,9 @@ import {
|
|
|
22
22
|
copyFile,
|
|
23
23
|
countFiles,
|
|
24
24
|
ensureDir,
|
|
25
|
+
ensureRealDir,
|
|
25
26
|
isDryRun,
|
|
27
|
+
removePipelineAgentFiles,
|
|
26
28
|
wipeDir,
|
|
27
29
|
writeFile,
|
|
28
30
|
} from "./_common.mjs";
|
|
@@ -61,17 +63,55 @@ export function installCopilot(ctx) {
|
|
|
61
63
|
installSkills({ pipelineSrc, dest: COPILOT_SKILLS, indexOnly, useSymlinks, platformFlag });
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
/** Start of the pipeline-managed span in copilot-instructions.md. */
|
|
67
|
+
export const INSTRUCTIONS_START_MARKER = "# Multi-Agent Development Pipeline";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Explicit end marker written after the pipeline section (v11.4.1+). Updates
|
|
71
|
+
* replace only the start..end span, so anything the user appends AFTER the
|
|
72
|
+
* pipeline section survives. Pre-marker files fall back to
|
|
73
|
+
* `legacyTrailingContent` bounding.
|
|
74
|
+
*/
|
|
75
|
+
export const INSTRUCTIONS_END_MARKER =
|
|
76
|
+
"<!-- multi-agent-pipeline:copilot-instructions:end -->";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Legacy files (written before the end marker existed) have no explicit
|
|
80
|
+
* terminator. Bound the pipeline span at the next top-level "# " heading
|
|
81
|
+
* after the start marker when one exists OUTSIDE fenced code blocks (the
|
|
82
|
+
* pipeline body carries bash comments like "# Bootstrap once ..." inside
|
|
83
|
+
* fences that must not be mistaken for headings); otherwise the span runs
|
|
84
|
+
* to EOF, which matches the pre-marker behavior.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} section - file content from the start marker onward
|
|
87
|
+
* @returns {string} user content trailing the pipeline span ("" if none)
|
|
88
|
+
*/
|
|
89
|
+
export function legacyTrailingContent(section) {
|
|
90
|
+
const lines = section.split("\n");
|
|
91
|
+
let inFence = false;
|
|
92
|
+
for (let i = 1; i < lines.length; i++) {
|
|
93
|
+
if (/^\s*(```|~~~)/.test(lines[i])) {
|
|
94
|
+
inFence = !inFence;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!inFence && /^# /.test(lines[i])) return lines.slice(i).join("\n");
|
|
98
|
+
}
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
|
|
64
102
|
function writeInstructionsFile(path) {
|
|
65
103
|
const pipelineSection = generateCopilotInstructions();
|
|
104
|
+
const managedBlock =
|
|
105
|
+
pipelineSection.trimEnd() + "\n\n" + INSTRUCTIONS_END_MARKER + "\n";
|
|
66
106
|
|
|
67
107
|
if (!existsSync(path)) {
|
|
68
|
-
writeFile(path,
|
|
108
|
+
writeFile(path, managedBlock);
|
|
69
109
|
console.log(" -> Created copilot-instructions.md with pipeline");
|
|
70
110
|
return;
|
|
71
111
|
}
|
|
72
112
|
|
|
73
113
|
let existing = readFileSync(path, "utf-8");
|
|
74
|
-
const marker =
|
|
114
|
+
const marker = INSTRUCTIONS_START_MARKER;
|
|
75
115
|
|
|
76
116
|
// Drift cleanup (v5.6.2): detect pre-v5.0 pipeline sections that predate
|
|
77
117
|
// the stable marker and strip them. These blocks ("## Multi-Agent Task
|
|
@@ -103,12 +143,26 @@ function writeInstructionsFile(path) {
|
|
|
103
143
|
}
|
|
104
144
|
|
|
105
145
|
if (existing.includes(marker)) {
|
|
106
|
-
const
|
|
107
|
-
|
|
146
|
+
const startIdx = existing.indexOf(marker);
|
|
147
|
+
const before = existing.slice(0, startIdx).trimEnd();
|
|
148
|
+
const fromStart = existing.slice(startIdx);
|
|
149
|
+
// Replace only the start..end span. User content appended AFTER the
|
|
150
|
+
// pipeline section (below the end marker, or below the next top-level
|
|
151
|
+
// heading in legacy files) is preserved.
|
|
152
|
+
const endIdx = fromStart.indexOf(INSTRUCTIONS_END_MARKER);
|
|
153
|
+
const trailing =
|
|
154
|
+
endIdx >= 0
|
|
155
|
+
? fromStart.slice(endIdx + INSTRUCTIONS_END_MARKER.length)
|
|
156
|
+
: legacyTrailingContent(fromStart);
|
|
157
|
+
let out = before.length > 0 ? before + "\n\n" : "";
|
|
158
|
+
out += managedBlock;
|
|
159
|
+
const trailingClean = trailing.replace(/^[\r\n]+/, "").trimEnd();
|
|
160
|
+
if (trailingClean.length > 0) out += "\n" + trailingClean + "\n";
|
|
161
|
+
writeFile(path, out);
|
|
108
162
|
const suffix = cleaned ? " (also scrubbed pre-v5.0 drift section)" : "";
|
|
109
163
|
console.log(` -> Updated existing pipeline section in copilot-instructions.md${suffix}`);
|
|
110
164
|
} else {
|
|
111
|
-
writeFile(path, existing.trimEnd() + "\n\n" +
|
|
165
|
+
writeFile(path, existing.trimEnd() + "\n\n" + managedBlock);
|
|
112
166
|
console.log(" -> Appended pipeline section to copilot-instructions.md");
|
|
113
167
|
}
|
|
114
168
|
}
|
|
@@ -117,6 +171,9 @@ function installScripts(pipelineSrc, dest, useSymlinks) {
|
|
|
117
171
|
console.log(" [Copilot CLI] Installing scripts...");
|
|
118
172
|
const scriptsSrc = join(pipelineSrc, "scripts");
|
|
119
173
|
if (!existsSync(scriptsSrc)) return;
|
|
174
|
+
// Replace a --link-era symlink with a real dir before wiping/copying so the
|
|
175
|
+
// install can never land inside the developer's repo checkout.
|
|
176
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
120
177
|
wipeDir(dest);
|
|
121
178
|
copyDir(scriptsSrc, dest, { exclude: DEV_ONLY_SCRIPTS, useSymlinks });
|
|
122
179
|
// Count files actually excluded, not the array length: DEV_ONLY_SCRIPTS mixes
|
|
@@ -133,7 +190,13 @@ function installAgents(pipelineSrc, dest, useSymlinks) {
|
|
|
133
190
|
console.log(" [Copilot CLI] Installing agent definitions...");
|
|
134
191
|
const agentsSrc = join(pipelineSrc, "agents");
|
|
135
192
|
if (!existsSync(agentsSrc)) return;
|
|
136
|
-
|
|
193
|
+
// ~/.copilot/agents also holds user-authored agent files. Never wipe the
|
|
194
|
+
// whole dir; remove only the pipeline-shipped agent files (name set derived
|
|
195
|
+
// from the source tree so it cannot go stale), then copy the fresh set in.
|
|
196
|
+
if (!useSymlinks) {
|
|
197
|
+
ensureRealDir(dest);
|
|
198
|
+
removePipelineAgentFiles(dest, agentsSrc);
|
|
199
|
+
}
|
|
137
200
|
copyDir(agentsSrc, dest, { useSymlinks });
|
|
138
201
|
console.log(` -> ${countFiles(agentsSrc)} files copied to ${dest}`);
|
|
139
202
|
}
|
|
@@ -142,6 +205,7 @@ function installSchemas(pipelineSrc, dest, useSymlinks) {
|
|
|
142
205
|
console.log(" [Copilot CLI] Installing JSON schemas...");
|
|
143
206
|
const schemasSrc = join(pipelineSrc, "schemas");
|
|
144
207
|
if (!existsSync(schemasSrc)) return;
|
|
208
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
145
209
|
wipeDir(dest);
|
|
146
210
|
copyDir(schemasSrc, dest, { useSymlinks });
|
|
147
211
|
console.log(` -> ${countFiles(schemasSrc)} files copied to ${dest}`);
|
|
@@ -151,6 +215,7 @@ function installLib(pipelineSrc, dest, useSymlinks) {
|
|
|
151
215
|
console.log(" [Copilot CLI] Installing shell libraries...");
|
|
152
216
|
const libSrc = join(pipelineSrc, "lib");
|
|
153
217
|
if (!existsSync(libSrc)) return;
|
|
218
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
154
219
|
wipeDir(dest);
|
|
155
220
|
copyDir(libSrc, dest, { useSymlinks });
|
|
156
221
|
console.log(` -> ${countFiles(libSrc)} files copied to ${dest}`);
|
|
@@ -160,6 +225,10 @@ function installSkills(opts) {
|
|
|
160
225
|
const { pipelineSrc, dest, indexOnly, useSymlinks, platformFlag } = opts;
|
|
161
226
|
console.log(" [Copilot CLI] Installing skills...");
|
|
162
227
|
|
|
228
|
+
// Same symlink guard as the other owned trees: never prune or copy through
|
|
229
|
+
// a --link-era symlink into the repo checkout.
|
|
230
|
+
if (!useSymlinks) ensureRealDir(dest);
|
|
231
|
+
|
|
163
232
|
if (indexOnly) {
|
|
164
233
|
try {
|
|
165
234
|
mkdirSync(dest, { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.5.0",
|
|
4
4
|
"description": "8-phase AI development pipeline with full orchestration on Claude Code and Copilot CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"test:unit": "node --test test/*.test.mjs",
|
|
19
19
|
"test:smoke": "node pipeline/scripts/run-smokes.mjs",
|
|
20
20
|
"lint:skills": "node pipeline/scripts/lint-skills.mjs",
|
|
21
|
-
"test:
|
|
21
|
+
"test:quick": "node --test test/*.test.mjs && node pipeline/scripts/lint-skills.mjs",
|
|
22
|
+
"test:coverage": "c8 --check-coverage --reporter=text --reporter=lcov node --test test/*.test.mjs && c8 report",
|
|
22
23
|
"lint": "eslint .",
|
|
23
24
|
"lint:fix": "eslint . --fix",
|
|
24
25
|
"format": "prettier --write \"**/*.{js,mjs,json,md,yml}\" --ignore-path .gitignore",
|
|
@@ -69,17 +70,10 @@
|
|
|
69
70
|
"README.md",
|
|
70
71
|
"CHANGELOG.md",
|
|
71
72
|
"LICENSE",
|
|
72
|
-
"!pipeline/scripts/figma-placeholder-map.json",
|
|
73
|
-
"!pipeline/scripts/sync-figma-source.sh",
|
|
74
|
-
"!pipeline/scripts/import-figma-skills.sh",
|
|
75
|
-
"!pipeline/scripts/smoke-figma-skill-import.sh",
|
|
76
73
|
"!pipeline/scripts/smoke-figma-config-schema.sh",
|
|
77
74
|
"!pipeline/scripts/smoke-personal-data.sh",
|
|
78
75
|
"!pipeline/scripts/smoke-install-leak-gate.sh",
|
|
79
|
-
"!
|
|
80
|
-
"!pipeline/scripts/.last-figma-sync-plan.json",
|
|
81
|
-
"!docs/GENERICITY-REVIEW.md",
|
|
82
|
-
"!docs/STABILITY-FIX-PLAN.md"
|
|
76
|
+
"!docs/internal/**"
|
|
83
77
|
],
|
|
84
78
|
"devDependencies": {
|
|
85
79
|
"@eslint/js": "^10.0.1",
|
|
@@ -20,7 +20,7 @@ Phase 7: Report → Jira / Wiki + log + knowledge/memory
|
|
|
20
20
|
|
|
21
21
|
`--dev local` skips Phase 1 (Analysis), Phase 2 (Planning + Approval Gate), Phase 4 (Review), and Phase 5 (User Test - local/autopilot variants skip the interactive test gate). It differs from `--dev` on two axes: no git worktree is created (development happens directly on the current branch in `$PROJECT_ROOT`), and the interactive User Test phase is skipped.
|
|
22
22
|
|
|
23
|
-
> **Want the quality tail afterwards?** Since this mode skips Review + Test, run [`/multi-agent:finish`](
|
|
23
|
+
> **Want the quality tail afterwards?** Since this mode skips Review + Test, run [`/multi-agent:finish`](../finish/SKILL.md) on the same branch when you're done to add parallel review, a build+test success gate, PR, and a Jira technical-analysis + test-scenario comment - without re-developing.
|
|
24
24
|
|
|
25
25
|
## When to use it
|
|
26
26
|
|
|
@@ -63,10 +63,23 @@ except Exception:
|
|
|
63
63
|
|
|
64
64
|
# 1. Pull every credential entry that looks like one of our token services.
|
|
65
65
|
# Includes token entries AND username entries (BB needs the user keychain key).
|
|
66
|
-
# Uses cross-platform credential-store wrapper (macOS/Windows/Linux backends)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
# Uses cross-platform credential-store wrapper (macOS/Windows/Linux backends),
|
|
67
|
+
# located via the shared resolver (repo checkout or either install tree) -
|
|
68
|
+
# never a hardcoded path. A missing helper degrades to an empty inventory.
|
|
69
|
+
# A pre-set CRED_STORE (tests, custom installs) is honored as-is.
|
|
70
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
71
|
+
# shellcheck disable=SC1090,SC1091
|
|
72
|
+
. "$(cd "$(dirname "$0")" && pwd)/credential-store-resolver.sh" 2>/dev/null \
|
|
73
|
+
|| . "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
74
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
75
|
+
|| true
|
|
76
|
+
fi
|
|
77
|
+
services=""
|
|
78
|
+
if [ -n "${CRED_STORE:-}" ]; then
|
|
79
|
+
services=$("$CRED_STORE" list 2>/dev/null \
|
|
80
|
+
| grep -E '_(Github|Jira|Bitbucket|Confluence)_[A-Za-z]+_(Token|Json|Username)$|_Github_Auth_Token$|_Github_Access_Token$|_Jira_Access_Token$|_Bitbucket_Access_Token$|_Bitbucket_Username$|_Confluence_Access_Token$' \
|
|
81
|
+
|| true)
|
|
82
|
+
fi
|
|
70
83
|
|
|
71
84
|
# 2. Compute unique prefixes (everything before the provider segment).
|
|
72
85
|
prefixes=$(printf '%s\n' "$services" \
|
|
@@ -114,14 +127,6 @@ emit_accounts() {
|
|
|
114
127
|
[ $match -eq 0 ] && continue
|
|
115
128
|
fi
|
|
116
129
|
|
|
117
|
-
# Provider list
|
|
118
|
-
local prov_json=""
|
|
119
|
-
[ -n "$gh_token" ] && prov_json="${prov_json}\"github\","
|
|
120
|
-
[ -n "$jira_token" ] && prov_json="${prov_json}\"jira\","
|
|
121
|
-
[ -n "$bb_token" ] && prov_json="${prov_json}\"bitbucket\","
|
|
122
|
-
[ -n "$conf_token" ] && prov_json="${prov_json}\"confluence\","
|
|
123
|
-
prov_json="${prov_json%,}"
|
|
124
|
-
|
|
125
130
|
# Work-account heuristic: bitbucket+jira together indicates corporate.
|
|
126
131
|
local is_work="false"
|
|
127
132
|
if [ -n "$bb_token" ] && [ -n "$jira_token" ]; then
|
|
@@ -134,18 +139,18 @@ emit_accounts() {
|
|
|
134
139
|
|
|
135
140
|
# Hosts: per-account override (prefs.global.accounts[].jiraHost) wins; falls
|
|
136
141
|
# back to global.hosts.{jira,bitbucket}; otherwise null. No hardcoded values.
|
|
137
|
-
local jira_host="
|
|
142
|
+
local jira_host="" bb_host=""
|
|
138
143
|
local per_jira per_bb
|
|
139
144
|
per_jira=$(account_host_override "$short_id" "jiraHost")
|
|
140
145
|
per_bb=$(account_host_override "$short_id" "bitbucketHost")
|
|
141
146
|
if [ -n "$jira_token" ]; then
|
|
142
|
-
if [ -n "$per_jira" ]; then jira_host="
|
|
143
|
-
elif [ -n "$prefs_jira_host" ]; then jira_host="
|
|
147
|
+
if [ -n "$per_jira" ]; then jira_host="$per_jira"
|
|
148
|
+
elif [ -n "$prefs_jira_host" ]; then jira_host="$prefs_jira_host"
|
|
144
149
|
fi
|
|
145
150
|
fi
|
|
146
151
|
if [ -n "$bb_token" ]; then
|
|
147
|
-
if [ -n "$per_bb" ]; then bb_host="
|
|
148
|
-
elif [ -n "$prefs_bb_host" ]; then bb_host="
|
|
152
|
+
if [ -n "$per_bb" ]; then bb_host="$per_bb"
|
|
153
|
+
elif [ -n "$prefs_bb_host" ]; then bb_host="$prefs_bb_host"
|
|
149
154
|
fi
|
|
150
155
|
fi
|
|
151
156
|
|
|
@@ -185,12 +190,45 @@ emit_accounts() {
|
|
|
185
190
|
[ $first -eq 0 ] && printf ','
|
|
186
191
|
first=0
|
|
187
192
|
|
|
188
|
-
printf
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
# Build the entry with python3 from env vars - printf-interpolated JSON
|
|
194
|
+
# has zero escaping, so a quote or backslash in a keychain service name
|
|
195
|
+
# would corrupt the whole array. Values cross into python via the
|
|
196
|
+
# environment, never via string interpolation.
|
|
197
|
+
ACC_ID="$short_id" ACC_LABEL="$label" ACC_PREFIX="$prefix" \
|
|
198
|
+
ACC_GH_TOKEN="$gh_token" ACC_JIRA_TOKEN="$jira_token" \
|
|
199
|
+
ACC_BB_TOKEN="$bb_token" ACC_CONF_TOKEN="$conf_token" \
|
|
200
|
+
ACC_BB_USER_KEY="$bb_user_key" ACC_GH_USER="$gh_user" \
|
|
201
|
+
ACC_JIRA_HOST="$jira_host" ACC_BB_HOST="$bb_host" ACC_IS_WORK="$is_work" \
|
|
202
|
+
python3 - <<'PY'
|
|
203
|
+
import json, os
|
|
204
|
+
|
|
205
|
+
def env(name):
|
|
206
|
+
return os.environ.get(name, "")
|
|
207
|
+
|
|
208
|
+
providers = []
|
|
209
|
+
if env("ACC_GH_TOKEN"): providers.append("github")
|
|
210
|
+
if env("ACC_JIRA_TOKEN"): providers.append("jira")
|
|
211
|
+
if env("ACC_BB_TOKEN"): providers.append("bitbucket")
|
|
212
|
+
if env("ACC_CONF_TOKEN"): providers.append("confluence")
|
|
213
|
+
|
|
214
|
+
print(json.dumps({
|
|
215
|
+
"id": env("ACC_ID"),
|
|
216
|
+
"label": env("ACC_LABEL"),
|
|
217
|
+
"prefix": env("ACC_PREFIX"),
|
|
218
|
+
"providers": providers,
|
|
219
|
+
"tokens": {
|
|
220
|
+
"github": env("ACC_GH_TOKEN"),
|
|
221
|
+
"jira": env("ACC_JIRA_TOKEN"),
|
|
222
|
+
"bitbucket": env("ACC_BB_TOKEN"),
|
|
223
|
+
"confluence": env("ACC_CONF_TOKEN"),
|
|
224
|
+
},
|
|
225
|
+
"userKeys": {"bitbucket": env("ACC_BB_USER_KEY")},
|
|
226
|
+
"users": {"github": env("ACC_GH_USER")},
|
|
227
|
+
"jiraHost": env("ACC_JIRA_HOST") or None,
|
|
228
|
+
"bitbucketHost": env("ACC_BB_HOST") or None,
|
|
229
|
+
"isWork": env("ACC_IS_WORK") == "true",
|
|
230
|
+
}, ensure_ascii=False), end="")
|
|
231
|
+
PY
|
|
194
232
|
done <<< "$prefixes"
|
|
195
233
|
printf ']'
|
|
196
234
|
}
|
|
@@ -26,6 +26,11 @@ STATE="${2:-}"
|
|
|
26
26
|
}
|
|
27
27
|
[ ! -f "$STATE" ] && { echo "ERR: state file not found: $STATE" >&2; exit 2; }
|
|
28
28
|
|
|
29
|
+
# count_matches: single-line numeric grep -c (the bare `grep -c || echo 0`
|
|
30
|
+
# idiom emits "0" twice on zero matches and breaks the -le comparisons below).
|
|
31
|
+
# shellcheck source=count-lib.sh disable=SC1091
|
|
32
|
+
. "$(cd "$(dirname "$0")" && pwd)/count-lib.sh"
|
|
33
|
+
|
|
29
34
|
# --- Helpers ----------------------------------------------------------------
|
|
30
35
|
# Extract the targets array from agent-state.json. Output: TSV with columns
|
|
31
36
|
# repoName, prUrl, isPrimary (1/0). One target per row.
|
|
@@ -156,7 +161,7 @@ do_render_jira() {
|
|
|
156
161
|
local urls
|
|
157
162
|
urls=$(all_pr_urls | tr ' ' '\n' | grep -v '^$' || true)
|
|
158
163
|
local count
|
|
159
|
-
count=$(printf '%s\n' "$urls" |
|
|
164
|
+
count=$(printf '%s\n' "$urls" | count_matches .)
|
|
160
165
|
|
|
161
166
|
if [ "$count" -le 1 ]; then
|
|
162
167
|
cat "$body_file"
|
|
@@ -179,7 +184,7 @@ do_render_conf() {
|
|
|
179
184
|
local urls
|
|
180
185
|
urls=$(all_pr_urls | tr ' ' '\n' | grep -v '^$' || true)
|
|
181
186
|
local count
|
|
182
|
-
count=$(printf '%s\n' "$urls" |
|
|
187
|
+
count=$(printf '%s\n' "$urls" | count_matches .)
|
|
183
188
|
|
|
184
189
|
if [ "$count" -le 1 ]; then
|
|
185
190
|
cat "$body_file"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# count-lib.sh - shared counting helper for pipeline shell scripts.
|
|
3
|
+
#
|
|
4
|
+
# Why: `grep -c` prints "0" AND exits 1 when nothing matches, so the common
|
|
5
|
+
# idiom `$(grep -c pattern file || echo 0)` emits TWO lines ("0" plus the
|
|
6
|
+
# fallback "0"). That breaks arithmetic (`$((x + 0\n0))` is a syntax error)
|
|
7
|
+
# and numeric comparisons (`[ "0\n0" -le 1 ]` silently evaluates false).
|
|
8
|
+
#
|
|
9
|
+
# count_matches guarantees single-line numeric output in every case:
|
|
10
|
+
# - match count when grep succeeds
|
|
11
|
+
# - "0" when nothing matches or grep itself errors (missing file, ...)
|
|
12
|
+
#
|
|
13
|
+
# Usage (same arguments as grep -c, file or stdin):
|
|
14
|
+
# n=$(count_matches -E 'pattern' "$file")
|
|
15
|
+
# n=$(printf '%s\n' "$blob" | count_matches .)
|
|
16
|
+
#
|
|
17
|
+
# Source from a sibling script:
|
|
18
|
+
# . "$(cd "$(dirname "$0")" && pwd)/count-lib.sh"
|
|
19
|
+
|
|
20
|
+
count_matches() {
|
|
21
|
+
local n
|
|
22
|
+
n=$(grep -c "$@" 2>/dev/null) || true
|
|
23
|
+
case "$n" in
|
|
24
|
+
'' | *[!0-9]*) n=0 ;;
|
|
25
|
+
esac
|
|
26
|
+
printf '%s\n' "$n"
|
|
27
|
+
}
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
# Subcommands:
|
|
9
9
|
# get <key> Read secret value to stdout (empty + exit 1 if missing)
|
|
10
10
|
# set <key> <value> Store secret (overwrites if exists)
|
|
11
|
+
# set <key> - Same, but read the secret from stdin. Preferred:
|
|
12
|
+
# keeps the secret off this script's argv (visible to
|
|
13
|
+
# ps). A literal "-" value cannot be stored this way.
|
|
11
14
|
# delete <key> Remove secret
|
|
12
15
|
# list List all keys (one per line)
|
|
13
16
|
# platform Print detected platform: macos | windows | linux | unknown
|
|
@@ -124,7 +127,11 @@ if (\$c) { \$c.GetNetworkCredential().Password }
|
|
|
124
127
|
|
|
125
128
|
do_set() {
|
|
126
129
|
local key="${1:-}" val="${2:-}"
|
|
127
|
-
[ -z "$key" ] && { echo "usage: $0 set <key> <value
|
|
130
|
+
[ -z "$key" ] && { echo "usage: $0 set <key> <value|->" >&2; exit 3; }
|
|
131
|
+
# "-" means: read the secret from stdin (keeps it off argv).
|
|
132
|
+
if [ "$val" = "-" ]; then
|
|
133
|
+
val=$(cat)
|
|
134
|
+
fi
|
|
128
135
|
if delegate_to_python; then
|
|
129
136
|
# Pass the value via stdin to keep it out of process listings / shell history.
|
|
130
137
|
printf '%s' "$val" | python3 "$KEYCHAIN_PY" set "$key" -
|
|
@@ -132,7 +139,15 @@ do_set() {
|
|
|
132
139
|
fi
|
|
133
140
|
case "$PLATFORM" in
|
|
134
141
|
macos)
|
|
135
|
-
security
|
|
142
|
+
# `security -i` reads the add command from stdin, so the secret never
|
|
143
|
+
# lands on the `security` argv (visible to ps). printf is a bash
|
|
144
|
+
# builtin, so no argv exposure there either. Double quotes/backslashes
|
|
145
|
+
# are escaped for security's interactive tokenizer.
|
|
146
|
+
local key_sec val_sec
|
|
147
|
+
key_sec=${key//\\/\\\\}; key_sec=${key_sec//\"/\\\"}
|
|
148
|
+
val_sec=${val//\\/\\\\}; val_sec=${val_sec//\"/\\\"}
|
|
149
|
+
printf 'add-generic-password -U -s "%s" -a "%s" -w "%s"\n' \
|
|
150
|
+
"$key_sec" "${USER:-claude}" "$val_sec" | security -i >/dev/null 2>&1
|
|
136
151
|
;;
|
|
137
152
|
linux)
|
|
138
153
|
require_cmd secret-tool "Install libsecret-tools (see doctor)."
|
|
@@ -140,11 +155,13 @@ do_set() {
|
|
|
140
155
|
;;
|
|
141
156
|
windows)
|
|
142
157
|
# Escape single quotes for PowerShell single-quoted strings ('' = literal ').
|
|
143
|
-
local key_esc="${key//\'/\'\'}"
|
|
144
|
-
#
|
|
145
|
-
|
|
158
|
+
local key_esc="${key//\'/\'\'}"
|
|
159
|
+
# The secret is piped via stdin ([Console]::In) so it never appears on
|
|
160
|
+
# the PowerShell argv (visible to process listings).
|
|
161
|
+
printf '%s' "$val" | ps_run "
|
|
146
162
|
\$ErrorActionPreference='Stop'
|
|
147
|
-
\$
|
|
163
|
+
\$raw = [Console]::In.ReadToEnd()
|
|
164
|
+
\$pwd = ConvertTo-SecureString \$raw -AsPlainText -Force
|
|
148
165
|
New-StoredCredential -Target '$key_esc' -UserName '${USER:-${USERNAME:-claude}}' -SecurePassword \$pwd -Persist LocalMachine | Out-Null
|
|
149
166
|
" >/dev/null
|
|
150
167
|
;;
|
|
@@ -40,6 +40,12 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
40
40
|
exit 1
|
|
41
41
|
fi
|
|
42
42
|
|
|
43
|
+
# count_matches: single-line numeric grep -c. The bare `grep -c || echo 0`
|
|
44
|
+
# idiom emits "0" twice on zero matches, which turns the $((...)) counter
|
|
45
|
+
# arithmetic below into a silent syntax error that zeroes the bucket.
|
|
46
|
+
# shellcheck source=count-lib.sh disable=SC1091
|
|
47
|
+
. "$(cd "$(dirname "$0")" && pwd)/count-lib.sh"
|
|
48
|
+
|
|
43
49
|
# ---- global timing budget -----------------------------------------------------
|
|
44
50
|
SCRIPT_START_EPOCH=$(date +%s)
|
|
45
51
|
GLOBAL_BUDGET_SEC=180
|
|
@@ -672,16 +678,16 @@ bucket_test_method_naming() {
|
|
|
672
678
|
[ -z "$f" ] && continue
|
|
673
679
|
case "$PLATFORM" in
|
|
674
680
|
ios|android)
|
|
675
|
-
underscore_count=$((underscore_count + $(
|
|
676
|
-
underscore_count=$((underscore_count + $(
|
|
677
|
-
camel_count=$((camel_count + $(
|
|
678
|
-
backtick_count=$((backtick_count + $(
|
|
681
|
+
underscore_count=$((underscore_count + $(count_matches -E 'func +test[A-Za-z0-9]+_[A-Za-z0-9_]+' "$f")))
|
|
682
|
+
underscore_count=$((underscore_count + $(count_matches -E 'fun +[A-Za-z0-9]+_[A-Za-z0-9_]+_[A-Za-z0-9_]+ *\(' "$f")))
|
|
683
|
+
camel_count=$((camel_count + $(count_matches -E 'func +test[A-Z][A-Za-z0-9]+ *\(' "$f")))
|
|
684
|
+
backtick_count=$((backtick_count + $(count_matches -E '`[^`]*`\s*\(' "$f")))
|
|
679
685
|
;;
|
|
680
686
|
backend)
|
|
681
|
-
underscore_count=$((underscore_count + $(
|
|
687
|
+
underscore_count=$((underscore_count + $(count_matches -E '^def +test_[a-z0-9_]+' "$f")))
|
|
682
688
|
;;
|
|
683
689
|
frontend)
|
|
684
|
-
it_count=$((it_count + $(
|
|
690
|
+
it_count=$((it_count + $(count_matches -E "(it|test)\\(['\"\`]" "$f")))
|
|
685
691
|
;;
|
|
686
692
|
esac
|
|
687
693
|
done <<< "$test_files"
|
|
@@ -745,9 +751,9 @@ bucket_accessibility_identifier() {
|
|
|
745
751
|
if [ -z "$sample_ids" ]; then empty_bucket; return; fi
|
|
746
752
|
|
|
747
753
|
local dot_count snake_count camel_count
|
|
748
|
-
dot_count=$(printf '%s\n' "$sample_ids" |
|
|
749
|
-
snake_count=$(printf '%s\n' "$sample_ids" |
|
|
750
|
-
camel_count=$(printf '%s\n' "$sample_ids" |
|
|
754
|
+
dot_count=$(printf '%s\n' "$sample_ids" | count_matches -E '"[a-z]+(\.[a-zA-Z]+)+')
|
|
755
|
+
snake_count=$(printf '%s\n' "$sample_ids" | count_matches -E '"[a-z]+_[a-z_]+')
|
|
756
|
+
camel_count=$(printf '%s\n' "$sample_ids" | count_matches -E '"[a-z]+[A-Z][a-zA-Z]+')
|
|
751
757
|
dot_count=${dot_count:-0}; snake_count=${snake_count:-0}; camel_count=${camel_count:-0}
|
|
752
758
|
|
|
753
759
|
local evidence_json
|
|
@@ -772,9 +778,9 @@ bucket_accessibility_identifier() {
|
|
|
772
778
|
done | head -30 || true)
|
|
773
779
|
if [ -z "$ids" ]; then empty_bucket; return; fi
|
|
774
780
|
local snake_count kebab_count camel_count
|
|
775
|
-
snake_count=$(printf '%s\n' "$ids" |
|
|
776
|
-
kebab_count=$(printf '%s\n' "$ids" |
|
|
777
|
-
camel_count=$(printf '%s\n' "$ids" |
|
|
781
|
+
snake_count=$(printf '%s\n' "$ids" | count_matches -E '"[a-z]+_[a-z_]+')
|
|
782
|
+
kebab_count=$(printf '%s\n' "$ids" | count_matches -E '"[a-z]+-[a-z\-]+')
|
|
783
|
+
camel_count=$(printf '%s\n' "$ids" | count_matches -E '"[a-z]+[A-Z][a-zA-Z]+')
|
|
778
784
|
snake_count=${snake_count:-0}; kebab_count=${kebab_count:-0}; camel_count=${camel_count:-0}
|
|
779
785
|
if [ "$snake_count" -ge "$kebab_count" ] && [ "$snake_count" -ge "$camel_count" ] && [ "$snake_count" -gt 0 ]; then
|
|
780
786
|
emit_bucket "snake_case testTag" "checkin_continue_button" "$(confidence_for "$snake_count")" "[]" '["kebab","camelCase"]'
|
|
@@ -797,8 +803,8 @@ bucket_accessibility_identifier() {
|
|
|
797
803
|
done | head -50 || true)
|
|
798
804
|
if [ -z "$op_ids" ]; then empty_bucket; return; fi
|
|
799
805
|
local camel_count snake_count
|
|
800
|
-
camel_count=$(printf '%s\n' "$op_ids" |
|
|
801
|
-
snake_count=$(printf '%s\n' "$op_ids" |
|
|
806
|
+
camel_count=$(printf '%s\n' "$op_ids" | count_matches -E '[a-z]+[A-Z][A-Za-z]+')
|
|
807
|
+
snake_count=$(printf '%s\n' "$op_ids" | count_matches -E '[a-z]+_[a-z_]+')
|
|
802
808
|
camel_count=${camel_count:-0}; snake_count=${snake_count:-0}
|
|
803
809
|
if [ "$camel_count" -ge "$snake_count" ] && [ "$camel_count" -gt 0 ]; then
|
|
804
810
|
emit_bucket "camelCase operationId" "createFoo" "$(confidence_for "$camel_count")" "[]" '["snake_case"]'
|
|
@@ -817,9 +823,9 @@ bucket_accessibility_identifier() {
|
|
|
817
823
|
done | head -30 || true)
|
|
818
824
|
if [ -z "$ids" ]; then empty_bucket; return; fi
|
|
819
825
|
local kebab_count camel_count snake_count
|
|
820
|
-
kebab_count=$(printf '%s\n' "$ids" |
|
|
821
|
-
camel_count=$(printf '%s\n' "$ids" |
|
|
822
|
-
snake_count=$(printf '%s\n' "$ids" |
|
|
826
|
+
kebab_count=$(printf '%s\n' "$ids" | count_matches -E '"[a-z]+-[a-z\-]+')
|
|
827
|
+
camel_count=$(printf '%s\n' "$ids" | count_matches -E '"[a-z]+[A-Z][a-zA-Z]+')
|
|
828
|
+
snake_count=$(printf '%s\n' "$ids" | count_matches -E '"[a-z]+_[a-z_]+')
|
|
823
829
|
kebab_count=${kebab_count:-0}; camel_count=${camel_count:-0}; snake_count=${snake_count:-0}
|
|
824
830
|
if [ "$kebab_count" -ge "$camel_count" ] && [ "$kebab_count" -ge "$snake_count" ] && [ "$kebab_count" -gt 0 ]; then
|
|
825
831
|
emit_bucket "kebab-case data-testid" 'data-testid="checkin-continue-button"' "$(confidence_for "$kebab_count")" "[]" '["camelCase","snake"]'
|
|
@@ -879,9 +885,9 @@ bucket_localization_key() {
|
|
|
879
885
|
if [ -z "$keys" ]; then empty_bucket; return; fi
|
|
880
886
|
|
|
881
887
|
local dot_count snake_count camel_count
|
|
882
|
-
dot_count=$(printf '%s\n' "$keys" |
|
|
883
|
-
snake_count=$(printf '%s\n' "$keys" |
|
|
884
|
-
camel_count=$(printf '%s\n' "$keys" |
|
|
888
|
+
dot_count=$(printf '%s\n' "$keys" | count_matches -E '^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z][A-Za-z0-9]*)+')
|
|
889
|
+
snake_count=$(printf '%s\n' "$keys" | count_matches -E '^[a-z][a-z0-9]*(_[a-z0-9]+)+')
|
|
890
|
+
camel_count=$(printf '%s\n' "$keys" | count_matches -E '^[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*')
|
|
885
891
|
dot_count=${dot_count:-0}; snake_count=${snake_count:-0}; camel_count=${camel_count:-0}
|
|
886
892
|
|
|
887
893
|
if [ "$dot_count" -ge "$snake_count" ] && [ "$dot_count" -ge "$camel_count" ] && [ "$dot_count" -gt 0 ]; then
|
|
@@ -907,7 +913,7 @@ bucket_di_registration() {
|
|
|
907
913
|
local resolver_count factory_count manual_count swinject_count
|
|
908
914
|
manual_count=0; resolver_count=0; factory_count=0; swinject_count=0
|
|
909
915
|
if [ -n "$configurators" ]; then
|
|
910
|
-
manual_count=$(printf '%s\n' "$configurators" |
|
|
916
|
+
manual_count=$(printf '%s\n' "$configurators" | count_matches 'DependencyConfigurator')
|
|
911
917
|
fi
|
|
912
918
|
files=$(run_find -type f -name '*.swift' 2>/dev/null || true)
|
|
913
919
|
if [ -n "$files" ]; then
|
|
@@ -67,15 +67,17 @@ if [ -z "$URL" ] && [ -z "$PAGE_ID" ]; then
|
|
|
67
67
|
exit 4
|
|
68
68
|
fi
|
|
69
69
|
|
|
70
|
-
# Resolve host + pageId from URL when not given explicitly.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
# Resolve host + pageId from URL when not given explicitly. Inputs are passed
|
|
71
|
+
# as argv: an env prefix on a plain assignment (VAR=x PARSED=$(...)) never
|
|
72
|
+
# reaches the child process, so os.environ would always come back empty.
|
|
73
|
+
PARSED=$(python3 - "$URL" "$HOST_OVERRIDE" "$PAGE_ID" <<'PY'
|
|
74
|
+
import sys, re
|
|
74
75
|
import urllib.parse as up
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
argv = sys.argv[1:] + ["", "", ""]
|
|
78
|
+
url = argv[0]
|
|
79
|
+
host_override = argv[1]
|
|
80
|
+
explicit_id = argv[2]
|
|
79
81
|
|
|
80
82
|
host = host_override
|
|
81
83
|
page_id = explicit_id
|
|
@@ -124,12 +126,22 @@ except Exception:
|
|
|
124
126
|
fi
|
|
125
127
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Confluence_Access_Token"
|
|
126
128
|
|
|
127
|
-
|
|
129
|
+
# Locate the credential helper via the resolver so Copilot-only installs work.
|
|
130
|
+
# shellcheck disable=SC1090,SC1091
|
|
131
|
+
. "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
132
|
+
|| . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
|
|
133
|
+
|| { printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"confluence","expected_key":"'"$TOKEN_KEY"'"}' >&2; exit 2; }
|
|
134
|
+
|
|
135
|
+
TOKEN=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
128
136
|
if [ -z "$TOKEN" ]; then
|
|
129
137
|
printf '%s\n' '{"status":"blocked","reason":"missing-token","service":"confluence","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
130
138
|
exit 2
|
|
131
139
|
fi
|
|
132
140
|
|
|
141
|
+
# Bearer auth via a curl config fed through process substitution so the token
|
|
142
|
+
# never appears in argv (argv is visible to ps).
|
|
143
|
+
confluence_auth_cfg() { printf 'header = "Authorization: Bearer %s"\n' "$TOKEN"; }
|
|
144
|
+
|
|
133
145
|
# Endpoint resolution: Confluence Cloud (<tenant>.atlassian.net/wiki/rest/api)
|
|
134
146
|
# vs Server/Data Center (<host>/rest/api). The same paths exist on both.
|
|
135
147
|
case "$HOST" in
|
|
@@ -141,14 +153,14 @@ esac
|
|
|
141
153
|
if [ -z "$PAGE_ID_RESOLVED" ] && [ -n "$SPACE_KEY" ] && [ -n "$URL" ]; then
|
|
142
154
|
TITLE_RAW=$(python3 -c "
|
|
143
155
|
import urllib.parse as up
|
|
144
|
-
import
|
|
145
|
-
p = up.urlparse(
|
|
156
|
+
import sys, re
|
|
157
|
+
p = up.urlparse(sys.argv[1])
|
|
146
158
|
m = re.match(r'/display/[^/]+/(.+)', p.path)
|
|
147
159
|
print(up.unquote(m.group(1)) if m else '')
|
|
148
|
-
"
|
|
160
|
+
" "$URL")
|
|
149
161
|
if [ -n "$TITLE_RAW" ]; then
|
|
150
162
|
LOOKUP=$(curl -sS --fail --max-time "$TIMEOUT" --connect-timeout 5 \
|
|
151
|
-
-
|
|
163
|
+
-K <(confluence_auth_cfg) -H "Accept: application/json" \
|
|
152
164
|
--data-urlencode "spaceKey=$SPACE_KEY" \
|
|
153
165
|
--data-urlencode "title=$TITLE_RAW" \
|
|
154
166
|
--data-urlencode "expand=" \
|
|
@@ -172,7 +184,7 @@ fi
|
|
|
172
184
|
|
|
173
185
|
# Fetch the page body in storage format.
|
|
174
186
|
PAGE_JSON=$(curl -sS --fail --max-time "$TIMEOUT" --connect-timeout 5 \
|
|
175
|
-
-
|
|
187
|
+
-K <(confluence_auth_cfg) -H "Accept: application/json" \
|
|
176
188
|
"$API_BASE/content/$PAGE_ID_RESOLVED?expand=body.storage,space" 2>/dev/null || true)
|
|
177
189
|
|
|
178
190
|
if [ -z "$PAGE_JSON" ]; then
|