@mmerterden/multi-agent-pipeline 13.4.0 → 13.5.1
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 +67 -0
- package/install/_mcp-register.mjs +125 -0
- package/install/codex.mjs +4 -38
- package/install/copilot.mjs +8 -0
- package/install/index.mjs +8 -3
- package/package.json +1 -1
- package/pipeline/lib/account-resolver.sh +15 -4
- package/pipeline/lib/credential-inventory.sh +25 -2
- package/pipeline/lib/credential-store-resolver.sh +34 -5
- package/pipeline/lib/fetch-confluence.sh +27 -5
- package/pipeline/lib/fetch-crashlytics.sh +27 -5
- package/pipeline/lib/fetch-figma-annotations.sh +18 -4
- package/pipeline/lib/fetch-fortify.sh +27 -5
- package/pipeline/lib/fetch-graylog.sh +27 -5
- package/pipeline/lib/figma-mcp-refresh.sh +35 -11
- package/pipeline/lib/figma-screenshot.sh +18 -7
- package/pipeline/lib/issue-fetcher.sh +19 -4
- package/pipeline/lib/post-pr-review.sh +27 -4
- package/pipeline/lib/repo-cache.sh +19 -4
- package/pipeline/lib/vercel-deploy.sh +15 -4
- package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
- package/pipeline/scripts/audit-log.sh +10 -4
- package/pipeline/scripts/keychain-save.sh +27 -5
- package/pipeline/scripts/phase-tracker.sh +26 -2
- package/pipeline/scripts/uninstall.mjs +24 -5
- package/pipeline/skills/shared/external/ios-coding-standard/references/lint-local.sh +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,73 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [13.5.1] - 2026-07-28
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `ios-coding-standard`'s local lint runner defaulted its scratch root to
|
|
24
|
+
`~/.claude/local/...`. On a Copilot- or Codex-only machine that made the script create
|
|
25
|
+
a stray `.claude` directory belonging to a host that is not installed, just to hold
|
|
26
|
+
lint output. It now picks whichever host tree exists and falls back to `$TMPDIR`.
|
|
27
|
+
Found by the Codex sync verification, which flags any `$HOME/.claude/...` reference
|
|
28
|
+
that reaches the Codex tree.
|
|
29
|
+
|
|
30
|
+
## [13.5.0] - 2026-07-28
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Every fetcher died on a Copilot-only or Codex-only install.** Eleven runtime scripts
|
|
35
|
+
loaded `credential-store-resolver.sh` as `. "$HOME/.claude/lib/..." || . <next> || {
|
|
36
|
+
error }`, all of them under `set -e`. Sourcing a file that does not exist aborts the
|
|
37
|
+
shell outright - `||` included - so on a host without `~/.claude/lib` the chain reached
|
|
38
|
+
neither its later candidates nor its error branch: bare exit 1, no message. Reordering
|
|
39
|
+
does not help, because whichever candidate is absent aborts at that point. All eleven
|
|
40
|
+
now check `[ -f ]` before sourcing and decide on `$CRED_STORE`, and the resolver ends
|
|
41
|
+
with `resolve_credential_store || :` so sourcing it can never trip a caller's `set -e`.
|
|
42
|
+
- The resolver only knew about `~/.claude` and `~/.copilot`. A Codex-only install could
|
|
43
|
+
not locate the credential helper at all, while the file sat in `~/.codex/lib`.
|
|
44
|
+
- **Copilot CLI registered no MCP server.** Codex had registration from the day its
|
|
45
|
+
installer was written; Copilot never did, so it carried all 257 pipeline skills and
|
|
46
|
+
could not call one of the 80 dev-toolkit tools those skills depend on. Both hosts now
|
|
47
|
+
go through the shared `install/_mcp-register.mjs`, which also treats Copilot's
|
|
48
|
+
`already exists` (exit 1, unlike Codex's replace-and-exit-0) as the desired end state
|
|
49
|
+
rather than reporting a working registration as a failed install. Uninstall deregisters
|
|
50
|
+
on both.
|
|
51
|
+
- **`figma-mcp-refresh.sh` called macOS `security` directly**, so silent Figma MCP token
|
|
52
|
+
renewal - the path that keeps every Figma consumer on Tier 1 - never worked on Linux or
|
|
53
|
+
Windows, and surfaced as an unexplained expired-token prompt. It now goes through
|
|
54
|
+
`credential-store.sh` with the `set <key> -` stdin form, which keeps the secret off
|
|
55
|
+
argv on every platform, the property the old `security -i` call was preserving.
|
|
56
|
+
- **`phase-tracker.sh` hashed with a bare `shasum`** - a perl script absent from slim
|
|
57
|
+
Linux images. Where it was missing, OTel trace and span ids came back empty: valid
|
|
58
|
+
JSON, useless telemetry, no error. Same for `post-pr-review.sh`, where an empty
|
|
59
|
+
fingerprint made every finding look new and re-posted the whole review each iteration.
|
|
60
|
+
- `audit-log.sh` fell back to printing `unhashed:<url>` when no hasher was available,
|
|
61
|
+
writing the plaintext remote - credentials included, if the URL carried any - into the
|
|
62
|
+
audit log that hashing exists to protect. It emits a marker instead.
|
|
63
|
+
- `smoke-write-state.sh` counted an honest exit-2 lock timeout as data loss, because it
|
|
64
|
+
read the final state without looking at exit codes. That made it fail once inside a
|
|
65
|
+
fully loaded suite run while passing 12/12 in isolation - a flake that read as a
|
|
66
|
+
data-loss bug and would eventually have been "fixed" by lowering the writer count,
|
|
67
|
+
removing the regression detection the test exists for. It now asserts the real
|
|
68
|
+
invariant: no writer reports success while losing its update.
|
|
69
|
+
- `smoke-token-preflight.sh` asserted the literal `security add-generic-password`, which
|
|
70
|
+
pinned the implementation to macOS. It now asserts the portable route and fails if a
|
|
71
|
+
direct platform keychain call comes back.
|
|
72
|
+
- `multi-agent-refs/rules.md` pointed at `~/.claude/scripts/vercel-deploy.sh`; the
|
|
73
|
+
wrapper installs to `lib/` with the other shell libraries.
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
- **`smoke-shell-portability.sh`** (8 assertions) over every shell file that ships to a
|
|
78
|
+
user tree: single-platform hashing without a fallback, unguarded BSD/GNU-divergent
|
|
79
|
+
constructs, hardcoded absolute home paths, direct platform keychain calls, and the
|
|
80
|
+
`.`-chain-under-`set -e` trap above. It found the `post-pr-review.sh` hasher and the
|
|
81
|
+
`figma-mcp-refresh.sh` keychain call on its first run.
|
|
82
|
+
- `smoke-install-layout.sh --update-fixture` regenerates the layout fixture from the tree
|
|
83
|
+
the gate itself installs, probe plugin included. The documented hand-rolled recipe
|
|
84
|
+
omitted it, so following it baked a three-file shortfall into the fixture.
|
|
85
|
+
|
|
19
86
|
## [13.4.0] - 2026-07-28
|
|
20
87
|
|
|
21
88
|
### Added
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register the companion MCP server with a host CLI.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS SHARED
|
|
5
|
+
*
|
|
6
|
+
* Codex got MCP registration when its installer was written; Copilot never did. The
|
|
7
|
+
* result was a host that carried all 257 pipeline skills and could not call a single
|
|
8
|
+
* one of the 80 dev-toolkit tools those skills depend on - `design-check`, every
|
|
9
|
+
* `ios_*` / `android_*` simulator call, the archive audits. Nothing failed at install
|
|
10
|
+
* time, and the skills only fail when a run actually reaches for a tool.
|
|
11
|
+
*
|
|
12
|
+
* Both CLIs take the same shape (`<cli> mcp add <name> -- <command> [args...]`) and
|
|
13
|
+
* both write their own user-level config, so the difference was purely that one
|
|
14
|
+
* installer called it and the other did not. One implementation, two callers.
|
|
15
|
+
*
|
|
16
|
+
* @module install/_mcp-register
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFileSync } from "child_process";
|
|
20
|
+
import { existsSync } from "fs";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
import { homedir } from "os";
|
|
23
|
+
|
|
24
|
+
import { isDryRun } from "./_common.mjs";
|
|
25
|
+
|
|
26
|
+
export const MCP_SERVER_NAME = "dev-toolkit";
|
|
27
|
+
export const MCP_SERVER_PACKAGE = "@mmerterden/dev-toolkit-mcp";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Where each host's CLI might live.
|
|
31
|
+
*
|
|
32
|
+
* A bare name is resolved through PATH by execFileSync. The explicit paths matter for
|
|
33
|
+
* Copilot: `copilot` is frequently a *shell alias* for `gh copilot`, and an alias is
|
|
34
|
+
* invisible to execFileSync - so a PATH-only lookup reports "not on PATH" on a machine
|
|
35
|
+
* where the user runs `copilot` every day. `gh` installs the real binary under
|
|
36
|
+
* ~/.local/share/gh/copilot/.
|
|
37
|
+
*/
|
|
38
|
+
const CLI_CANDIDATES = Object.freeze({
|
|
39
|
+
codex: ["codex"],
|
|
40
|
+
copilot: ["copilot", join(homedir(), ".local", "share", "gh", "copilot", "copilot")],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* First candidate that exists as a file, or the first bare name to let PATH resolution
|
|
45
|
+
* try. Returning a bare name rather than null keeps the "not on PATH" diagnosis in one
|
|
46
|
+
* place: the execFileSync catch.
|
|
47
|
+
*
|
|
48
|
+
* @param {"codex"|"copilot"} host
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function resolveCli(host) {
|
|
52
|
+
const candidates = CLI_CANDIDATES[host] || [host];
|
|
53
|
+
for (const c of candidates) {
|
|
54
|
+
if (c.includes("/") && existsSync(c)) return c;
|
|
55
|
+
}
|
|
56
|
+
return candidates[0];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register the MCP server. Idempotent from the caller's point of view, though the two
|
|
61
|
+
* CLIs get there differently:
|
|
62
|
+
*
|
|
63
|
+
* - `codex mcp add` replaces an existing entry and exits 0.
|
|
64
|
+
* - `copilot mcp add` refuses, printing `Server "<name>" already exists` and exiting 1.
|
|
65
|
+
*
|
|
66
|
+
* So a non-zero exit is not automatically a failure. An already-registered server is
|
|
67
|
+
* the desired end state, and reporting it as "skipped MCP registration (Command
|
|
68
|
+
* failed)" would send the user to fix something that is already correct - which is
|
|
69
|
+
* exactly what the first version of this did.
|
|
70
|
+
*
|
|
71
|
+
* The existing entry is left alone rather than removed and re-added: a user may have
|
|
72
|
+
* added env vars or switched the command to a local checkout, and an installer has no
|
|
73
|
+
* business discarding that.
|
|
74
|
+
*
|
|
75
|
+
* Never fatal: a missing CLI or a failed write must not fail the install, because the
|
|
76
|
+
* rest of the tree is still correct and useful. It prints the manual command instead.
|
|
77
|
+
*
|
|
78
|
+
* @param {"codex"|"copilot"} host
|
|
79
|
+
* @param {string} label - host label for log lines
|
|
80
|
+
* @returns {{registered: boolean, alreadyPresent?: boolean, reason?: string}}
|
|
81
|
+
*/
|
|
82
|
+
export function registerMcpServer(host, label) {
|
|
83
|
+
const cli = resolveCli(host);
|
|
84
|
+
const manual = `${host} mcp add ${MCP_SERVER_NAME} -- npx -y ${MCP_SERVER_PACKAGE}`;
|
|
85
|
+
|
|
86
|
+
if (isDryRun()) {
|
|
87
|
+
console.log(` [dry-run] would run: ${manual}`);
|
|
88
|
+
return { registered: false, reason: "dry-run" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Opt-out for hermetic installs. Registration shells out to the host CLI, so its
|
|
92
|
+
// result depends on whether that binary is present - which makes the install layout
|
|
93
|
+
// non-deterministic across machines and CI. The layout smoke sets this so its
|
|
94
|
+
// fingerprint stays stable.
|
|
95
|
+
if (process.env.MULTI_AGENT_SKIP_MCP_REGISTER === "1") {
|
|
96
|
+
console.log(` -> ${label}: skipped MCP registration (MULTI_AGENT_SKIP_MCP_REGISTER=1)`);
|
|
97
|
+
return { registered: false, reason: "opted out" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// Bounded: an installer must never hang on a child process. This is a local config
|
|
102
|
+
// write and returns in milliseconds.
|
|
103
|
+
execFileSync(cli, ["mcp", "add", MCP_SERVER_NAME, "--", "npx", "-y", MCP_SERVER_PACKAGE], {
|
|
104
|
+
stdio: "pipe",
|
|
105
|
+
timeout: 20_000,
|
|
106
|
+
});
|
|
107
|
+
console.log(` -> ${label}: registered the ${MCP_SERVER_NAME} MCP server`);
|
|
108
|
+
return { registered: true };
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// The CLI writes its diagnosis to stderr, which execFileSync parks on the error
|
|
111
|
+
// rather than in `message`. Read both, or "already exists" is invisible.
|
|
112
|
+
const out = `${e?.stderr?.toString() ?? ""}\n${e?.stdout?.toString() ?? ""}\n${e?.message ?? ""}`;
|
|
113
|
+
if (/already exists/i.test(out)) {
|
|
114
|
+
console.log(` -> ${label}: ${MCP_SERVER_NAME} MCP server already registered, left as-is`);
|
|
115
|
+
return { registered: true, alreadyPresent: true };
|
|
116
|
+
}
|
|
117
|
+
const why =
|
|
118
|
+
e?.code === "ENOENT"
|
|
119
|
+
? `${host} not on PATH`
|
|
120
|
+
: (out.split("\n").find((l) => l.trim()) || "unknown error").trim();
|
|
121
|
+
console.log(` -> ${label}: skipped MCP registration (${why})`);
|
|
122
|
+
console.log(` run manually: ${manual}`);
|
|
123
|
+
return { registered: false, reason: why };
|
|
124
|
+
}
|
|
125
|
+
}
|
package/install/codex.mjs
CHANGED
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync, readdirSync, rmSync } from "fs";
|
|
21
21
|
import { join } from "path";
|
|
22
|
-
import { execFileSync } from "child_process";
|
|
23
22
|
|
|
24
23
|
import {
|
|
25
24
|
copyDir,
|
|
@@ -32,6 +31,7 @@ import {
|
|
|
32
31
|
writeFile,
|
|
33
32
|
} from "./_common.mjs";
|
|
34
33
|
import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
|
|
34
|
+
import { registerMcpServer } from "./_mcp-register.mjs";
|
|
35
35
|
import { installCodexAgents } from "./_codex-agents.mjs";
|
|
36
36
|
import { existingSkillNames, installAuthoredPluginSkills } from "./_plugin-skills.mjs";
|
|
37
37
|
import { generateCodexInstructions } from "./_codex-instructions.mjs";
|
|
@@ -43,9 +43,8 @@ export const AGENTS_MD_START_MARKER = "# Multi-Agent Development Pipeline";
|
|
|
43
43
|
/** Explicit end marker written after the pipeline section. */
|
|
44
44
|
export const AGENTS_MD_END_MARKER = "<!-- multi-agent-pipeline:codex-instructions:end -->";
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
export
|
|
48
|
-
const MCP_SERVER_PACKAGE = "@mmerterden/dev-toolkit-mcp";
|
|
46
|
+
/** Re-exported for the uninstaller and the Codex install smoke. */
|
|
47
|
+
export { MCP_SERVER_NAME } from "./_mcp-register.mjs";
|
|
49
48
|
|
|
50
49
|
/**
|
|
51
50
|
* `$HOME/.claude/...` path rewrites applied to every file installed into the
|
|
@@ -200,7 +199,7 @@ export function installCodex(ctx) {
|
|
|
200
199
|
installTree("rules", pipelineSrc, CODEX_RULES, useSymlinks);
|
|
201
200
|
writeAgentsMd(join(CODEX_DIR, "AGENTS.md"));
|
|
202
201
|
ensureSharedPreferences(home, pipelineSrc);
|
|
203
|
-
registerMcpServer();
|
|
202
|
+
registerMcpServer("codex", "Codex CLI");
|
|
204
203
|
|
|
205
204
|
console.log("");
|
|
206
205
|
}
|
|
@@ -482,37 +481,4 @@ function ensureSharedPreferences(home, pipelineSrc) {
|
|
|
482
481
|
* it. Hand-merging TOML here would be the riskiest write in this installer for
|
|
483
482
|
* no benefit.
|
|
484
483
|
*/
|
|
485
|
-
function registerMcpServer() {
|
|
486
|
-
if (isDryRun()) {
|
|
487
|
-
console.log(
|
|
488
|
-
` [dry-run] would run: codex mcp add ${MCP_SERVER_NAME} -- npx -y ${MCP_SERVER_PACKAGE}`,
|
|
489
|
-
);
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
// Opt-out for hermetic installs. Registration shells out to `codex`, so its
|
|
493
|
-
// result depends on whether the binary is present - which makes the install
|
|
494
|
-
// layout non-deterministic across machines and CI. The layout smoke sets this
|
|
495
|
-
// so its fingerprint stays stable.
|
|
496
|
-
if (process.env.MULTI_AGENT_SKIP_MCP_REGISTER === "1") {
|
|
497
|
-
console.log(" -> skipped MCP registration (MULTI_AGENT_SKIP_MCP_REGISTER=1)");
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
try {
|
|
501
|
-
// Bounded: an installer must never hang on a child process. `codex mcp add`
|
|
502
|
-
// is a local config write and returns in milliseconds.
|
|
503
|
-
execFileSync("codex", ["mcp", "add", MCP_SERVER_NAME, "--", "npx", "-y", MCP_SERVER_PACKAGE], {
|
|
504
|
-
stdio: "pipe",
|
|
505
|
-
timeout: 20_000,
|
|
506
|
-
});
|
|
507
|
-
console.log(` -> registered the ${MCP_SERVER_NAME} MCP server`);
|
|
508
|
-
} catch (e) {
|
|
509
|
-
const why =
|
|
510
|
-
e?.code === "ENOENT" ? "codex not on PATH" : (e?.message || "unknown error").split("\n")[0];
|
|
511
|
-
console.log(` -> skipped MCP registration (${why})`);
|
|
512
|
-
console.log(
|
|
513
|
-
` run manually: codex mcp add ${MCP_SERVER_NAME} -- npx -y ${MCP_SERVER_PACKAGE}`,
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
484
|
export { generateCodexInstructions };
|
package/install/copilot.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { DEV_ONLY_SCRIPTS, countDevOnlyFiles } from "./_dev-only-files.mjs";
|
|
|
26
26
|
import { generateCopilotInstructions } from "./_copilot-instructions.mjs";
|
|
27
27
|
import { legacyTrailingContent, mergeManagedBlock } from "./_managed-block.mjs";
|
|
28
28
|
import { existingSkillNames, installAuthoredPluginSkills } from "./_plugin-skills.mjs";
|
|
29
|
+
import { registerMcpServer } from "./_mcp-register.mjs";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* @param {{
|
|
@@ -56,6 +57,13 @@ export function installCopilot(ctx) {
|
|
|
56
57
|
installSchemas(pipelineSrc, COPILOT_SCHEMAS, useSymlinks);
|
|
57
58
|
installLib(pipelineSrc, COPILOT_LIB, useSymlinks);
|
|
58
59
|
installSkills({ home, pipelineSrc, dest: COPILOT_SKILLS, indexOnly, useSymlinks, platformFlag });
|
|
60
|
+
|
|
61
|
+
// Copilot never registered the companion MCP server, while Codex did from the day its
|
|
62
|
+
// installer was written. So Copilot carried every pipeline skill and could not call a
|
|
63
|
+
// single one of the tools those skills depend on: design-check, the ios_* / android_*
|
|
64
|
+
// simulator calls, the archive audits. Nothing failed at install time - the skills
|
|
65
|
+
// only fail once a run reaches for a tool that is not there.
|
|
66
|
+
registerMcpServer("copilot", "Copilot CLI");
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
/** Start of the pipeline-managed span in copilot-instructions.md. */
|
package/install/index.mjs
CHANGED
|
@@ -182,11 +182,16 @@ function printSummary(opts) {
|
|
|
182
182
|
console.log("");
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
console.log(" For UI testing,
|
|
185
|
+
console.log(" For UI testing, the mobile MCP server:");
|
|
186
186
|
if (forCodex) {
|
|
187
|
-
console.log(" Codex:
|
|
187
|
+
console.log(" Codex CLI: registered automatically (codex mcp add dev-toolkit)");
|
|
188
|
+
}
|
|
189
|
+
if (forCopilot) {
|
|
190
|
+
console.log(" Copilot CLI: registered automatically (copilot mcp add dev-toolkit)");
|
|
191
|
+
}
|
|
192
|
+
if (forClaude) {
|
|
193
|
+
console.log(" Claude Code: add it yourself - claude mcp add dev-toolkit -- npx -y @mmerterden/dev-toolkit-mcp");
|
|
188
194
|
}
|
|
189
|
-
console.log(" Otherwise add to your MCP config: npx @mmerterden/dev-toolkit-mcp");
|
|
190
195
|
console.log("");
|
|
191
196
|
console.log(" Uninstall everything: npx @mmerterden/multi-agent-pipeline uninstall");
|
|
192
197
|
console.log(" (Personal access tokens in keychain are preserved.)");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.5.1",
|
|
4
4
|
"description": "8-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex 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",
|
|
@@ -69,10 +69,21 @@ except Exception:
|
|
|
69
69
|
# A pre-set CRED_STORE (tests, custom installs) is honored as-is.
|
|
70
70
|
if [ -z "${CRED_STORE:-}" ]; then
|
|
71
71
|
# shellcheck disable=SC1090,SC1091
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
# Existence check before sourcing: `. <missing>` aborts the shell under `set -e`,
|
|
73
|
+
# `||` included, so a `.`-chain reaches neither its later candidates nor its error
|
|
74
|
+
# branch. The loop also covers all three hosts - the chain it replaced knew only
|
|
75
|
+
# .claude and .copilot, so a Codex-only install could not resolve at all.
|
|
76
|
+
for _cred_resolver in \
|
|
77
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
78
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
79
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
80
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
81
|
+
[ -f "$_cred_resolver" ] || continue
|
|
82
|
+
# shellcheck source=/dev/null
|
|
83
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
84
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
85
|
+
done
|
|
86
|
+
unset _cred_resolver
|
|
76
87
|
fi
|
|
77
88
|
services=""
|
|
78
89
|
if [ -n "${CRED_STORE:-}" ]; then
|
|
@@ -111,6 +111,14 @@ probe_http() {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
# probe_basic <url> <user> <pass> -> prints the HTTP status
|
|
114
|
+
# probe_basic_hdr <url> <user> <pass> <header-name> <header-value> -> HTTP status
|
|
115
|
+
probe_basic_hdr() {
|
|
116
|
+
local url="$1" u="$2" pw="$3" hname="$4" hval="$5" out
|
|
117
|
+
out=$(printf 'url = "%s"\nuser = "%s:%s"\nheader = "%s: %s"\nsilent\noutput = "/dev/null"\nwrite-out = "%%{http_code}"\nmax-time = %s\n' \
|
|
118
|
+
"$url" "$u" "$pw" "$hname" "$hval" "$TIMEOUT" | curl --config - 2>/dev/null)
|
|
119
|
+
printf '%s' "${out:-000}"
|
|
120
|
+
}
|
|
121
|
+
|
|
114
122
|
probe_basic() {
|
|
115
123
|
local url="$1" u="$2" p="$3" out
|
|
116
124
|
out=$(printf 'url = "%s"\nuser = "%s:%s"\nsilent\noutput = "/dev/null"\nwrite-out = "%%{http_code}"\nmax-time = %s\n' \
|
|
@@ -125,6 +133,10 @@ probe_basic() {
|
|
|
125
133
|
classify_status() {
|
|
126
134
|
case "$1" in
|
|
127
135
|
2??) echo "reachable" ;;
|
|
136
|
+
# A redirect means the host answered. Corporate services commonly bounce an
|
|
137
|
+
# unauthenticated API call to an SSO login page, which classify as auth-rejected
|
|
138
|
+
# rather than reachable: the request did not get its data.
|
|
139
|
+
30?) echo "auth-rejected" ;;
|
|
128
140
|
401|403) echo "auth-rejected" ;;
|
|
129
141
|
404) echo "reachable" ;; # endpoint answered; the probe path may just not exist
|
|
130
142
|
000|"") echo "unreachable" ;;
|
|
@@ -185,11 +197,22 @@ probe_one() {
|
|
|
185
197
|
fortify)
|
|
186
198
|
host=$(pref global.hosts.fortify)
|
|
187
199
|
[ -n "$host" ] || { echo "no-host-configured"; return; }
|
|
188
|
-
|
|
200
|
+
# Shapes copied from fetch-fortify.sh, which already works against SSC: the API
|
|
201
|
+
# lives under /ssc/, and SSC accepts either a Bearer token or its own
|
|
202
|
+
# `FortifyToken <base64>` scheme. Probing the wrong base path returned a 302 to
|
|
203
|
+
# the login page and read as a broken credential.
|
|
204
|
+
status=$(probe_http "https://${host}/ssc/api/v1/projects?limit=1" "Authorization" "Bearer $tok")
|
|
205
|
+
if [ "$(classify_status "$status")" != "reachable" ]; then
|
|
206
|
+
status=$(probe_http "https://${host}/ssc/api/v1/projects?limit=1" \
|
|
207
|
+
"Authorization" "FortifyToken $(printf '%s' "$tok" | base64 | tr -d '\n')")
|
|
208
|
+
fi ;;
|
|
189
209
|
graylog)
|
|
190
210
|
host=$(pref global.hosts.graylog)
|
|
191
211
|
[ -n "$host" ] || { echo "no-host-configured"; return; }
|
|
192
|
-
|
|
212
|
+
# Graylog PATs authenticate as basic auth with the literal password "token", and
|
|
213
|
+
# the API rejects requests without X-Requested-By. Both per fetch-graylog.sh.
|
|
214
|
+
status=$(probe_basic_hdr "https://${host}/api/system" "$tok" "token" \
|
|
215
|
+
"X-Requested-By" "multi-agent-pipeline") ;;
|
|
193
216
|
jenkins)
|
|
194
217
|
host=$(pref global.hosts.jenkins)
|
|
195
218
|
[ -n "$host" ] || { echo "no-host-configured"; return; }
|
|
@@ -13,15 +13,31 @@
|
|
|
13
13
|
# error instead of `bash: $CRED_STORE: command not found`.
|
|
14
14
|
#
|
|
15
15
|
# Usage from a skill bash block:
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
16
|
+
# for r in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
17
|
+
# "$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
18
|
+
# "$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
19
|
+
# "$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
20
|
+
# [ -f "$r" ] || continue
|
|
21
|
+
# . "$r" 2>/dev/null || true
|
|
22
|
+
# if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
23
|
+
# done
|
|
24
|
+
# [ -n "${CRED_STORE:-}" ] || { echo "credential helper not found - run the installer" >&2; exit 1; }
|
|
19
25
|
# TOKEN=$("$CRED_STORE" get "<KEY>")
|
|
26
|
+
#
|
|
27
|
+
# Two rules, both learned from a silent failure:
|
|
28
|
+
# 1. Check the FILE EXISTS before sourcing it. `. <missing>` aborts the shell under
|
|
29
|
+
# `set -e`, `||` included, so a `.`-chain never reaches its later candidates.
|
|
30
|
+
# 2. Decide on `$CRED_STORE`, never on the source's exit status. Sourcing this file
|
|
31
|
+
# always succeeds by design, so the caller stays alive to report a useful error.
|
|
20
32
|
|
|
21
33
|
resolve_credential_store() {
|
|
34
|
+
# All three supported hosts. Codex was missing here, so a Codex-only install could
|
|
35
|
+
# not resolve the credential helper at all - every fetcher on that host reported
|
|
36
|
+
# "credential helper not found" while the file sat in ~/.codex/lib.
|
|
22
37
|
local cands=(
|
|
23
38
|
"$HOME/.claude/lib/credential-store.sh"
|
|
24
39
|
"$HOME/.copilot/lib/credential-store.sh"
|
|
40
|
+
"$HOME/.codex/lib/credential-store.sh"
|
|
25
41
|
)
|
|
26
42
|
for c in "${cands[@]}"; do
|
|
27
43
|
if [ -x "$c" ]; then
|
|
@@ -36,11 +52,13 @@ credential helper not found.
|
|
|
36
52
|
The multi-agent pipeline expects one of these to exist:
|
|
37
53
|
~/.claude/lib/credential-store.sh (Claude Code installs)
|
|
38
54
|
~/.copilot/lib/credential-store.sh (Copilot CLI installs)
|
|
55
|
+
~/.codex/lib/credential-store.sh (Codex CLI installs)
|
|
39
56
|
|
|
40
57
|
Install with:
|
|
41
58
|
npx @mmerterden/multi-agent-pipeline install --claude # Claude Code
|
|
42
59
|
npx @mmerterden/multi-agent-pipeline install --copilot # Copilot CLI
|
|
43
|
-
npx @mmerterden/multi-agent-pipeline install --
|
|
60
|
+
npx @mmerterden/multi-agent-pipeline install --codex # Codex CLI
|
|
61
|
+
npx @mmerterden/multi-agent-pipeline install --all # all three
|
|
44
62
|
|
|
45
63
|
Or override `CRED_STORE` to point at a custom path before sourcing this resolver.
|
|
46
64
|
MSG
|
|
@@ -53,5 +71,16 @@ MSG
|
|
|
53
71
|
if [ "${BASH_SOURCE[0]:-$0}" = "${0}" ]; then
|
|
54
72
|
resolve_credential_store && echo "$CRED_STORE"
|
|
55
73
|
else
|
|
56
|
-
|
|
74
|
+
# `|| :` matters, and it is not cosmetic.
|
|
75
|
+
#
|
|
76
|
+
# A sourced file runs in the caller's shell, so a bare failing command at this top
|
|
77
|
+
# level trips the caller's `set -e` DURING the source - before the caller's own
|
|
78
|
+
# `. resolver || fallback` can catch anything. Eleven runtime scripts loaded this
|
|
79
|
+
# resolver that way, all of them with `set -e`, so on any host where the first
|
|
80
|
+
# credential-store candidate was absent (a Copilot-only or Codex-only install) they
|
|
81
|
+
# died with a bare exit 1, no message, and their error branches unreachable.
|
|
82
|
+
#
|
|
83
|
+
# Sourcing therefore always succeeds. Callers MUST decide on `$CRED_STORE` being
|
|
84
|
+
# non-empty, never on the source's exit status - see the usage note above.
|
|
85
|
+
resolve_credential_store || :
|
|
57
86
|
fi
|
|
@@ -126,11 +126,33 @@ except Exception:
|
|
|
126
126
|
fi
|
|
127
127
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Confluence_Access_Token"
|
|
128
128
|
|
|
129
|
-
# Locate the
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
# Locate the resolver with an existence check, not a `.`-chain.
|
|
130
|
+
#
|
|
131
|
+
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
|
|
132
|
+
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
|
|
133
|
+
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
|
|
134
|
+
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
|
|
135
|
+
# message. Reordering does not help: whichever candidate is absent aborts at that point.
|
|
136
|
+
# Checking for the file before sourcing it is the only safe form.
|
|
137
|
+
for _cred_resolver in \
|
|
138
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
139
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
|
|
140
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
141
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
142
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
143
|
+
[ -f "$_cred_resolver" ] || continue
|
|
144
|
+
# shellcheck source=/dev/null
|
|
145
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
146
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
147
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
148
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
149
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
150
|
+
done
|
|
151
|
+
unset _cred_resolver
|
|
152
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
153
|
+
printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"confluence","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
154
|
+
exit 2
|
|
155
|
+
fi
|
|
134
156
|
|
|
135
157
|
TOKEN=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
136
158
|
if [ -z "$TOKEN" ]; then
|
|
@@ -124,11 +124,33 @@ except Exception:
|
|
|
124
124
|
fi
|
|
125
125
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Firebase_Access_Json"
|
|
126
126
|
|
|
127
|
-
# Locate the
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
# Locate the resolver with an existence check, not a `.`-chain.
|
|
128
|
+
#
|
|
129
|
+
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
|
|
130
|
+
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
|
|
131
|
+
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
|
|
132
|
+
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
|
|
133
|
+
# message. Reordering does not help: whichever candidate is absent aborts at that point.
|
|
134
|
+
# Checking for the file before sourcing it is the only safe form.
|
|
135
|
+
for _cred_resolver in \
|
|
136
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
137
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
|
|
138
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
139
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
140
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
141
|
+
[ -f "$_cred_resolver" ] || continue
|
|
142
|
+
# shellcheck source=/dev/null
|
|
143
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
144
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
145
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
146
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
147
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
148
|
+
done
|
|
149
|
+
unset _cred_resolver
|
|
150
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
151
|
+
printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"firebase","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
152
|
+
exit 2
|
|
153
|
+
fi
|
|
132
154
|
|
|
133
155
|
SA_JSON_B64=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
134
156
|
if [ -z "$SA_JSON_B64" ]; then
|
|
@@ -38,10 +38,24 @@
|
|
|
38
38
|
set -uo pipefail
|
|
39
39
|
|
|
40
40
|
PREFS="$HOME/.claude/multi-agent-preferences.json"
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
# Locate the resolver with an existence check, not a `.`-chain: sourcing a missing file
|
|
42
|
+
# aborts the shell under `set -e`, `||` included, so a chain skips both its later
|
|
43
|
+
# candidates and its trailing `|| true`. A missing store is tolerated here - CRED_STORE
|
|
44
|
+
# simply stays empty and the caller falls back to an env-supplied token.
|
|
45
|
+
for _cred_resolver in \
|
|
46
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
47
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
48
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
49
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
50
|
+
[ -f "$_cred_resolver" ] || continue
|
|
51
|
+
# shellcheck source=/dev/null
|
|
52
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
53
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
54
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
55
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
56
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
57
|
+
done
|
|
58
|
+
unset _cred_resolver
|
|
45
59
|
CRED_STORE="${CRED_STORE:-}"
|
|
46
60
|
FIGMA_API="https://api.figma.com/v1"
|
|
47
61
|
HTTP_TIMEOUT=60
|
|
@@ -128,11 +128,33 @@ except Exception:
|
|
|
128
128
|
fi
|
|
129
129
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Fortify_Access_Token"
|
|
130
130
|
|
|
131
|
-
# Locate the
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
# Locate the resolver with an existence check, not a `.`-chain.
|
|
132
|
+
#
|
|
133
|
+
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
|
|
134
|
+
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
|
|
135
|
+
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
|
|
136
|
+
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
|
|
137
|
+
# message. Reordering does not help: whichever candidate is absent aborts at that point.
|
|
138
|
+
# Checking for the file before sourcing it is the only safe form.
|
|
139
|
+
for _cred_resolver in \
|
|
140
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
141
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
|
|
142
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
143
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
144
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
145
|
+
[ -f "$_cred_resolver" ] || continue
|
|
146
|
+
# shellcheck source=/dev/null
|
|
147
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
148
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
149
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
150
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
151
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
152
|
+
done
|
|
153
|
+
unset _cred_resolver
|
|
154
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
155
|
+
printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"fortify","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
156
|
+
exit 2
|
|
157
|
+
fi
|
|
136
158
|
|
|
137
159
|
TOKEN=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
138
160
|
if [ -z "$TOKEN" ]; then
|
|
@@ -118,11 +118,33 @@ except Exception:
|
|
|
118
118
|
fi
|
|
119
119
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Graylog_Access_Token"
|
|
120
120
|
|
|
121
|
-
# Locate the
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
# Locate the resolver with an existence check, not a `.`-chain.
|
|
122
|
+
#
|
|
123
|
+
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
|
|
124
|
+
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
|
|
125
|
+
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
|
|
126
|
+
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
|
|
127
|
+
# message. Reordering does not help: whichever candidate is absent aborts at that point.
|
|
128
|
+
# Checking for the file before sourcing it is the only safe form.
|
|
129
|
+
for _cred_resolver in \
|
|
130
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
131
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
|
|
132
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
133
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
134
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
135
|
+
[ -f "$_cred_resolver" ] || continue
|
|
136
|
+
# shellcheck source=/dev/null
|
|
137
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
138
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
139
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
140
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
141
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
142
|
+
done
|
|
143
|
+
unset _cred_resolver
|
|
144
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
145
|
+
printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"graylog","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
146
|
+
exit 2
|
|
147
|
+
fi
|
|
126
148
|
|
|
127
149
|
TOKEN=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
128
150
|
if [ -z "$TOKEN" ]; then
|
|
@@ -20,7 +20,6 @@ set -euo pipefail
|
|
|
20
20
|
|
|
21
21
|
PREFS_FILE="${PREFS_FILE:-$HOME/.claude/multi-agent-preferences.json}"
|
|
22
22
|
TOKEN_ENDPOINT="https://api.figma.com/v1/oauth/token"
|
|
23
|
-
ACCOUNT="${USER:-$(whoami)}"
|
|
24
23
|
|
|
25
24
|
say() { printf '%s\n' "$*"; }
|
|
26
25
|
|
|
@@ -30,7 +29,36 @@ KEY_NAME=$(jq -r '.global.keychainMapping.figma_mcp // empty' "$PREFS_FILE")
|
|
|
30
29
|
[ -n "$KEY_NAME" ] || { say "figma-mcp-refresh: keychainMapping.figma_mcp not set"; exit 2; }
|
|
31
30
|
|
|
32
31
|
REFRESH_KEY="${KEY_NAME}_Refresh"
|
|
33
|
-
|
|
32
|
+
|
|
33
|
+
# Locate the resolver with an existence check, not a `.`-chain.
|
|
34
|
+
#
|
|
35
|
+
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
|
|
36
|
+
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
|
|
37
|
+
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
|
|
38
|
+
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
|
|
39
|
+
# message. Reordering does not help: whichever candidate is absent aborts at that point.
|
|
40
|
+
# Checking for the file before sourcing it is the only safe form.
|
|
41
|
+
for _cred_resolver in \
|
|
42
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
43
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
|
|
44
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
45
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
46
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
47
|
+
[ -f "$_cred_resolver" ] || continue
|
|
48
|
+
# shellcheck source=/dev/null
|
|
49
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
50
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
51
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
52
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
53
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
54
|
+
done
|
|
55
|
+
unset _cred_resolver
|
|
56
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
57
|
+
say "figma-mcp-refresh: credential helper not found - run the pipeline installer"
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
REFRESH_TOKEN=$("$CRED_STORE" get "$REFRESH_KEY" 2>/dev/null || true)
|
|
34
62
|
[ -n "$REFRESH_TOKEN" ] || { say "figma-mcp-refresh: no refresh token at '$REFRESH_KEY'"; exit 2; }
|
|
35
63
|
|
|
36
64
|
SCRIPT_PATH=$(jq -r '.global.tokenScripts.figma_mcp // empty' "$PREFS_FILE")
|
|
@@ -69,17 +97,13 @@ if [ -z "$NEW_ACCESS" ]; then
|
|
|
69
97
|
exit 1
|
|
70
98
|
fi
|
|
71
99
|
|
|
72
|
-
# Keychain write
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
# previous direct call (-T "" -U). Figma tokens are URL-safe, so embedding in
|
|
77
|
-
# the quoted command string is safe.
|
|
100
|
+
# Keychain write through credential-store.sh with the `-` form, which reads the secret
|
|
101
|
+
# from stdin: the value never lands on any argv (visible to ps), which is the property
|
|
102
|
+
# the previous `security -i` call was carefully preserving. The store does the same thing
|
|
103
|
+
# per platform, so that property now holds on Linux and Windows as well.
|
|
78
104
|
save_keychain_secret() {
|
|
79
105
|
local service="$1" secret="$2"
|
|
80
|
-
|
|
81
|
-
printf 'add-generic-password -U -s "%s" -a "%s" -T "" -w "%s"\n' \
|
|
82
|
-
"$service" "$ACCOUNT" "$secret" | security -i >/dev/null
|
|
106
|
+
printf '%s' "$secret" | "$CRED_STORE" set "$service" - >/dev/null
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
# Save new access token first, then the rotated refresh token - order matters so a
|
|
@@ -42,13 +42,24 @@ set -euo pipefail
|
|
|
42
42
|
# --- Globals ----------------------------------------------------------------
|
|
43
43
|
|
|
44
44
|
PREFS="$HOME/.claude/multi-agent-preferences.json"
|
|
45
|
-
# Locate the
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
# Locate the resolver with an existence check, not a `.`-chain: sourcing a missing file
|
|
46
|
+
# aborts the shell under `set -e`, `||` included, so a chain skips both its later
|
|
47
|
+
# candidates and its trailing `|| true`. A missing store is tolerated here - CRED_STORE
|
|
48
|
+
# simply stays empty and the caller falls back to an env-supplied token.
|
|
49
|
+
for _cred_resolver in \
|
|
50
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
51
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
52
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
53
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
54
|
+
[ -f "$_cred_resolver" ] || continue
|
|
55
|
+
# shellcheck source=/dev/null
|
|
56
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
57
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
58
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
59
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
60
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
61
|
+
done
|
|
62
|
+
unset _cred_resolver
|
|
52
63
|
CRED_STORE="${CRED_STORE:-}"
|
|
53
64
|
FIGMA_API="https://api.figma.com/v1"
|
|
54
65
|
PARALLEL_DL=4
|
|
@@ -113,10 +113,25 @@ fetch_jira() {
|
|
|
113
113
|
# A pre-set CRED_STORE (tests, custom installs) is honored as-is.
|
|
114
114
|
if [ -z "${CRED_STORE:-}" ]; then
|
|
115
115
|
# shellcheck disable=SC1090,SC1091
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
# Existence check before sourcing: `. <missing>` aborts the shell under `set -e`,
|
|
117
|
+
# `||` included, so a `.`-chain reaches neither its later candidates nor its error
|
|
118
|
+
# branch. The loop also covers all three hosts - the chain it replaced knew only
|
|
119
|
+
# .claude and .copilot, so a Codex-only install could not resolve at all.
|
|
120
|
+
for _cred_resolver in \
|
|
121
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
122
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
123
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
124
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
125
|
+
[ -f "$_cred_resolver" ] || continue
|
|
126
|
+
# shellcheck source=/dev/null
|
|
127
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
128
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
129
|
+
done
|
|
130
|
+
unset _cred_resolver
|
|
131
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
132
|
+
echo "ERR: credential helper not found" >&2
|
|
133
|
+
return 1
|
|
134
|
+
fi
|
|
120
135
|
fi
|
|
121
136
|
local token
|
|
122
137
|
token=$("$CRED_STORE" get "$JIRA_TOKEN_KEY" 2>/dev/null || true)
|
|
@@ -104,7 +104,14 @@ echo "post-pr-review: decision=$DECISION (blocking=$ACCEPTED_BLOCKING, important
|
|
|
104
104
|
# when the suggested fix wording changes slightly between iterations.
|
|
105
105
|
finding_fingerprint() {
|
|
106
106
|
local path="$1" line="$2" issue="$3"
|
|
107
|
-
|
|
107
|
+
# sha256sum (coreutils) or shasum (perl), whichever this platform has. A bare
|
|
108
|
+
# `shasum` produced an empty fingerprint on slim Linux images, which made every
|
|
109
|
+
# finding look new and re-posted the whole review on each iteration.
|
|
110
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
111
|
+
printf '%s|%s|%s' "$path" "$line" "$issue" | sha256sum | cut -c1-16
|
|
112
|
+
else
|
|
113
|
+
printf '%s|%s|%s' "$path" "$line" "$issue" | shasum -a 256 | cut -c1-16
|
|
114
|
+
fi
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
render_inline_body() {
|
|
@@ -307,9 +314,25 @@ post_bitbucket_server() {
|
|
|
307
314
|
# Credential Manager / Linux libsecret) - sourced lazily so this script keeps
|
|
308
315
|
# working when run from either ~/.claude/lib or ~/.copilot/lib.
|
|
309
316
|
# shellcheck disable=SC1090
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
317
|
+
# Existence check before sourcing: `. <missing>` aborts the shell under `set -e`,
|
|
318
|
+
# `||` included, so a `.`-chain reaches neither its later candidates nor its error
|
|
319
|
+
# branch. The loop also covers all three hosts - the chain it replaced knew only
|
|
320
|
+
# .claude and .copilot, so a Codex-only install could not resolve at all.
|
|
321
|
+
for _cred_resolver in \
|
|
322
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
323
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
324
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
325
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
326
|
+
[ -f "$_cred_resolver" ] || continue
|
|
327
|
+
# shellcheck source=/dev/null
|
|
328
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
329
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
330
|
+
done
|
|
331
|
+
unset _cred_resolver
|
|
332
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
333
|
+
echo "post-pr-review: credential helper not found" >&2
|
|
334
|
+
return 4
|
|
335
|
+
fi
|
|
313
336
|
resolve_credential_store || return 4
|
|
314
337
|
|
|
315
338
|
auth_user=$("$CRED_STORE" get "$user_key" 2>/dev/null || true)
|
|
@@ -142,10 +142,25 @@ print(json.dumps(out))
|
|
|
142
142
|
# A pre-set CRED_STORE (tests, custom installs) is honored as-is.
|
|
143
143
|
if [ -z "${CRED_STORE:-}" ]; then
|
|
144
144
|
# shellcheck disable=SC1090,SC1091
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
# Existence check before sourcing: `. <missing>` aborts the shell under `set -e`,
|
|
146
|
+
# `||` included, so a `.`-chain reaches neither its later candidates nor its error
|
|
147
|
+
# branch. The loop also covers all three hosts - the chain it replaced knew only
|
|
148
|
+
# .claude and .copilot, so a Codex-only install could not resolve at all.
|
|
149
|
+
for _cred_resolver in \
|
|
150
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
151
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
152
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
153
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
154
|
+
[ -f "$_cred_resolver" ] || continue
|
|
155
|
+
# shellcheck source=/dev/null
|
|
156
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
157
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
158
|
+
done
|
|
159
|
+
unset _cred_resolver
|
|
160
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
161
|
+
echo "ERR: credential helper not found" >&2
|
|
162
|
+
exit 3
|
|
163
|
+
fi
|
|
149
164
|
fi
|
|
150
165
|
BB_USER="${BB_USER:-${BB_USER_KEY:+$("$CRED_STORE" get "$BB_USER_KEY" 2>/dev/null || true)}}"
|
|
151
166
|
BB_TOKEN=$("$CRED_STORE" get "$BB_TOKEN_KEY" 2>/dev/null || true)
|
|
@@ -115,10 +115,21 @@ cmd_deploy() {
|
|
|
115
115
|
# The resolver ships alongside this wrapper in lib/; fall back to the
|
|
116
116
|
# per-CLI install locations when running from a different cwd layout.
|
|
117
117
|
# shellcheck source=/dev/null
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
# Existence check before sourcing: `. <missing>` aborts the shell under `set -e`,
|
|
119
|
+
# `||` included, so a `.`-chain reaches neither its later candidates nor its error
|
|
120
|
+
# branch. The loop also covers all three hosts - the chain it replaced knew only
|
|
121
|
+
# .claude and .copilot, so a Codex-only install could not resolve at all.
|
|
122
|
+
for _cred_resolver in \
|
|
123
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
124
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
125
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
126
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
127
|
+
[ -f "$_cred_resolver" ] || continue
|
|
128
|
+
# shellcheck source=/dev/null
|
|
129
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
130
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
131
|
+
done
|
|
132
|
+
unset _cred_resolver
|
|
122
133
|
echo "ERROR: VERCEL_TOKEN not set. Resolve via env or keychain ('${CRED_STORE:-credential-store.sh} get <vercel-key>')." >&2
|
|
123
134
|
echo "Hint: vercel CLI accepts the token via env var; do NOT pass --token= on argv (leaks on retry)." >&2
|
|
124
135
|
exit 1
|
|
@@ -78,7 +78,7 @@ Rules that make this actionable rather than decorative:
|
|
|
78
78
|
Failures remain non-fatal at Phase 1 by default - the agent still runs analysis. What
|
|
79
79
|
changed is that the user learns about it while the choice is still theirs to make.
|
|
80
80
|
|
|
81
|
-
**Graylog specifics.** `fetch-graylog.sh` degrades to empty on any network/VPN failure: it emits a normalized empty object and exits `0`, so an unreachable host reads as `{status:"skipped", reason:"vpn-unreachable"}` and never blocks. Only a genuine auth rejection on a reachable host exits `3` (marked `failed`, still non-fatal here). `state.graylogContext` is prepended to the analysis prompt inside the **Referenced External Sources** section as diagnostic context that is **advisory only** - the agent may use it to orient on a reported error, but code remains ground truth, and there is no Phase 4 review gate for logs.
|
|
81
|
+
**Graylog specifics.** `fetch-graylog.sh` degrades to empty on any network/VPN failure: it emits a normalized empty object and exits `0`, so an unreachable host reads as `{status:"skipped", reason:"vpn-unreachable"}` and never blocks. **Exiting `0` is not permission to stay quiet.** The payload carries `degraded: true` and a `degradeReason`; when it does, log one line naming the host and the reason, and carry it into the Phase 7 report. Logs are advisory, so this never asks a question and never blocks - but "I searched the logs and found nothing" and "I never reached the log server" are different statements, and only one of them is true. Only a genuine auth rejection on a reachable host exits `3` (marked `failed`, still non-fatal here). `state.graylogContext` is prepended to the analysis prompt inside the **Referenced External Sources** section as diagnostic context that is **advisory only** - the agent may use it to orient on a reported error, but code remains ground truth, and there is no Phase 4 review gate for logs.
|
|
82
82
|
|
|
83
83
|
## Prompt injection shape
|
|
84
84
|
|
|
@@ -48,12 +48,18 @@ json_escape() {
|
|
|
48
48
|
hash_url() {
|
|
49
49
|
local url="$1"
|
|
50
50
|
[ -z "$url" ] && { echo ""; return; }
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
# sha256sum first: it is the coreutils tool, present on Linux and in slim containers
|
|
52
|
+
# where `shasum` (a perl script) is not.
|
|
53
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
54
54
|
printf '%s' "$url" | sha256sum | awk '{print $1}'
|
|
55
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
56
|
+
printf '%s' "$url" | shasum -a 256 | awk '{print $1}'
|
|
55
57
|
else
|
|
56
|
-
|
|
58
|
+
# The previous fallback printed `unhashed:<url>`, writing the plaintext remote into
|
|
59
|
+
# the audit log - the exact thing this hashing exists to prevent, and it would have
|
|
60
|
+
# carried any credentials embedded in the remote URL with it. Losing one field beats
|
|
61
|
+
# leaking one.
|
|
62
|
+
echo "unhashable-no-sha256-tool"
|
|
57
63
|
fi
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -53,11 +53,33 @@ case "$CHOICE" in
|
|
|
53
53
|
;;
|
|
54
54
|
esac
|
|
55
55
|
|
|
56
|
-
# Locate the
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
# Locate the resolver with an existence check, not a `.`-chain.
|
|
57
|
+
#
|
|
58
|
+
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
|
|
59
|
+
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
|
|
60
|
+
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
|
|
61
|
+
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
|
|
62
|
+
# message. Reordering does not help: whichever candidate is absent aborts at that point.
|
|
63
|
+
# Checking for the file before sourcing it is the only safe form.
|
|
64
|
+
for _cred_resolver in \
|
|
65
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
66
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
|
|
67
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
68
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
69
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
70
|
+
[ -f "$_cred_resolver" ] || continue
|
|
71
|
+
# shellcheck source=/dev/null
|
|
72
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
73
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
74
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
75
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
76
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
77
|
+
done
|
|
78
|
+
unset _cred_resolver
|
|
79
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
80
|
+
echo "Hata: credential-store bulunamadi. Kurulum: npx @mmerterden/multi-agent-pipeline install"
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
61
83
|
CRED="$CRED_STORE"
|
|
62
84
|
|
|
63
85
|
"$CRED" delete "$SERVICE_NAME" >/dev/null 2>&1 || true
|
|
@@ -193,6 +193,25 @@ MA_DECODE_JS
|
|
|
193
193
|
printf '%s' "$out"
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
# Portable SHA-256 over stdin, printing the same "<hex> -" shape as either tool.
|
|
197
|
+
#
|
|
198
|
+
# GNU coreutils ships sha256sum; macOS ships shasum (a perl script). Neither is
|
|
199
|
+
# universal - Alpine and slim container images typically have sha256sum but no perl, and
|
|
200
|
+
# some hardened macOS setups have neither on a restricted PATH. Trying both keeps the
|
|
201
|
+
# tracker working on all three supported platforms, and the no-hasher branch emits an
|
|
202
|
+
# obviously-synthetic value rather than an empty string, so a broken environment shows
|
|
203
|
+
# up as a visibly fake id instead of a silently blank field.
|
|
204
|
+
sha256_hex() {
|
|
205
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
206
|
+
sha256sum
|
|
207
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
208
|
+
shasum -a 256
|
|
209
|
+
else
|
|
210
|
+
cat >/dev/null
|
|
211
|
+
printf '%s -\n' "0000000000000000000000000000000000000000000000000000000000nohash"
|
|
212
|
+
fi
|
|
213
|
+
}
|
|
214
|
+
|
|
196
215
|
# OTel-compatible span emission - v6.1.0+, opt-in via $MULTI_AGENT_OTEL_SPANS.
|
|
197
216
|
# Appends one JSON line to otel-spans.jsonl. Never fatal on failure.
|
|
198
217
|
#
|
|
@@ -218,9 +237,14 @@ emit_otel_span() {
|
|
|
218
237
|
local spans_file="${TRACKER_DIR}/otel-spans.jsonl"
|
|
219
238
|
|
|
220
239
|
# Deterministic 128-bit trace_id from task_id, 64-bit span_id from event+phase+timestamp.
|
|
240
|
+
#
|
|
241
|
+
# sha256_hex, not `shasum` directly: this runs on every phase on every host, and
|
|
242
|
+
# `shasum` is a perl script that is absent from minimal Linux images and many
|
|
243
|
+
# containers. Without a fallback the ids came back empty and the span was written with
|
|
244
|
+
# blank trace/span - valid JSON, useless telemetry, and no error to notice.
|
|
221
245
|
local trace_id span_id
|
|
222
|
-
trace_id=$(printf "%s" "$task_id" |
|
|
223
|
-
span_id=$(printf "%s:%s:%s" "$event" "$phase_id" "$now_ns" |
|
|
246
|
+
trace_id=$(printf "%s" "$task_id" | sha256_hex | awk '{print substr($1,1,32)}')
|
|
247
|
+
span_id=$(printf "%s:%s:%s" "$event" "$phase_id" "$now_ns" | sha256_hex | awk '{print substr($1,1,16)}')
|
|
224
248
|
|
|
225
249
|
jq -nc \
|
|
226
250
|
--arg trace "$trace_id" --arg span "$span_id" \
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
39
39
|
import { execFileSync } from "child_process";
|
|
40
40
|
import { join } from "path";
|
|
41
|
+
import { homedir } from "os";
|
|
41
42
|
import { pathToFileURL } from "url";
|
|
42
43
|
import { createInterface } from "readline";
|
|
43
44
|
|
|
@@ -157,21 +158,38 @@ function removeGeneratedCodexAgents(agentsDir) {
|
|
|
157
158
|
* uninstall failure. Never edits `config.toml` directly - Codex keeps
|
|
158
159
|
* marketplace and plugin state in the same file.
|
|
159
160
|
*/
|
|
160
|
-
function
|
|
161
|
+
function deregisterMcpServer(host, cliPath) {
|
|
162
|
+
const cli = cliPath || host;
|
|
161
163
|
if (dryRun) {
|
|
162
|
-
report("would run",
|
|
164
|
+
report("would run", `${host} mcp remove dev-toolkit`);
|
|
163
165
|
return;
|
|
164
166
|
}
|
|
165
167
|
try {
|
|
166
|
-
execFileSync(
|
|
167
|
-
console.log(
|
|
168
|
+
execFileSync(cli, ["mcp", "remove", "dev-toolkit"], { stdio: "pipe", timeout: 20_000 });
|
|
169
|
+
console.log(` removed: dev-toolkit MCP registration (${host})`);
|
|
168
170
|
} catch {
|
|
169
171
|
console.log(
|
|
170
|
-
|
|
172
|
+
` note: could not run \`${host} mcp remove dev-toolkit\` (${host} not on PATH, or not registered)`,
|
|
171
173
|
);
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
function deregisterCodexMcpServer() {
|
|
178
|
+
deregisterMcpServer("codex");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Copilot registers the same server, so leaving it behind would keep a stale entry
|
|
183
|
+
* pointing at a package the user just removed.
|
|
184
|
+
*
|
|
185
|
+
* `copilot` is often a shell alias for `gh copilot`, invisible to execFileSync, so the
|
|
186
|
+
* real binary path is tried when the bare name is not resolvable.
|
|
187
|
+
*/
|
|
188
|
+
function deregisterCopilotMcpServer() {
|
|
189
|
+
const ghCopilot = join(homedir(), ".local", "share", "gh", "copilot", "copilot");
|
|
190
|
+
deregisterMcpServer("copilot", existsSync(ghCopilot) ? ghCopilot : undefined);
|
|
191
|
+
}
|
|
192
|
+
|
|
175
193
|
/**
|
|
176
194
|
* Remove every directory under `parent` whose name matches a predicate.
|
|
177
195
|
* @param {string} parent
|
|
@@ -475,6 +493,7 @@ export async function main() {
|
|
|
475
493
|
if (forCopilot && HOME) {
|
|
476
494
|
console.log("");
|
|
477
495
|
console.log(" [Copilot CLI] Removing...");
|
|
496
|
+
deregisterCopilotMcpServer();
|
|
478
497
|
const COP = join(HOME, ".copilot");
|
|
479
498
|
rmIfExists(join(COP, "scripts"));
|
|
480
499
|
// Pipeline-managed trees the installer lays down alongside scripts/.
|
|
@@ -16,7 +16,19 @@
|
|
|
16
16
|
set -euo pipefail
|
|
17
17
|
|
|
18
18
|
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
-
|
|
19
|
+
# Scratch root. `~/.claude` is one host's tree: defaulting there made a Copilot- or
|
|
20
|
+
# Codex-only machine create a stray .claude directory just to hold lint output. Prefer
|
|
21
|
+
# whichever host tree already exists, and fall back to a neutral location.
|
|
22
|
+
if [ -n "${IOS_STANDARD_WORK_ROOT:-}" ]; then
|
|
23
|
+
WORK_ROOT="$IOS_STANDARD_WORK_ROOT"
|
|
24
|
+
else
|
|
25
|
+
WORK_ROOT=""
|
|
26
|
+
for _host_dir in "$HOME/.claude" "$HOME/.copilot" "$HOME/.codex"; do
|
|
27
|
+
[ -d "$_host_dir" ] && { WORK_ROOT="$_host_dir/local/ios-coding-standard"; break; }
|
|
28
|
+
done
|
|
29
|
+
unset _host_dir
|
|
30
|
+
[ -n "$WORK_ROOT" ] || WORK_ROOT="${TMPDIR:-/tmp}/ios-coding-standard"
|
|
31
|
+
fi
|
|
20
32
|
DRAFT_CONFIG="$SKILL_DIR/swiftlint.draft.yml"
|
|
21
33
|
|
|
22
34
|
MODULE_PATH="${1:-}"
|