@mmerterden/multi-agent-pipeline 16.29.0 → 16.31.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 +82 -0
- package/docs/features.md +14 -0
- package/install/_common.mjs +45 -4
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/design-check/SKILL.md +6 -5
- package/pipeline/commands/multi-agent/help/SKILL.md +13 -12
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -4
- package/pipeline/lib/credential-inventory.sh +1 -0
- package/pipeline/lib/repo-hygiene.sh +164 -0
- package/pipeline/lib/vercel-deploy.sh +41 -22
- package/pipeline/multi-agent-refs/channels/pr.md +37 -1
- package/pipeline/multi-agent-refs/features/doctor.md +12 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +2 -2
- package/pipeline/multi-agent-refs/features/visual-evidence.md +103 -20
- package/pipeline/multi-agent-refs/keychain.md +1 -0
- package/pipeline/multi-agent-refs/knowledge.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +36 -7
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +14 -2
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +12 -2
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +23 -0
- package/pipeline/preferences-template.json +2 -1
- package/pipeline/schemas/agent-state.schema.json +79 -1
- package/pipeline/schemas/prefs.schema.json +24 -1
- package/pipeline/schemas/token-budget.json +10 -10
- package/pipeline/scripts/bulk-read.sh +13 -5
- package/pipeline/scripts/capture-evidence.sh +170 -5
- package/pipeline/scripts/doctor.mjs +56 -1
- package/pipeline/scripts/evidence-gate.mjs +31 -2
- package/pipeline/scripts/gc-tmp.sh +30 -0
- package/pipeline/scripts/gc-worktrees.sh +32 -10
- package/pipeline/scripts/offload-ref.sh +13 -5
- package/pipeline/scripts/probe-evidence-capability.sh +250 -0
- package/pipeline/scripts/purge.sh +11 -0
- package/pipeline/scripts/run-ui-tests.sh +380 -0
- package/pipeline/scripts/worktree-finalize.sh +9 -0
- package/pipeline/skills/.skill-manifest.json +19 -11
- package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +10 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,88 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## [16.31.0] - 2026-09-13
|
|
18
|
+
|
|
19
|
+
A pipeline that leaves worktrees, artefacts and exclude lines behind is not generic tooling, it is a guest that rearranges the furniture. This release makes a run leave the repo exactly as it found it, and fixes two credentials-and-integrity gaps found while proving it.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`pipeline/lib/repo-hygiene.sh`** - one owner for everything the pipeline writes into somebody else's repo. `ensure_exclusions` writes a marked block into `.git/info/exclude` covering `.worktrees/`, `.pipeline/`, `.multi-agent/`, `triage-output.json`, `.review-diff.txt`, `.build.log` and `.test.log`; `release_exclusions` removes exactly that block and nothing around it; `prune_empty` removes directories it can `rmdir` and never touches `memory/`. The single-line residue guard that lived inline in Phase 0 moved here, so the write and the removal are now written next to each other rather than one existing and the other not.
|
|
24
|
+
|
|
25
|
+
The exclude path resolves through `--git-common-dir`, not `--git-dir`: inside a worktree the latter points at `.git/worktrees/<name>/`, where an exclude file is read by that worktree alone and disappears with it.
|
|
26
|
+
|
|
27
|
+
**`safe_root` refuses `/`, `$HOME`, an empty argument and anything that does not resolve.** Every sibling GC script already had that guard; this one wrote into `$HOME/.multi-agent/` without it, which is a real path on a real machine.
|
|
28
|
+
|
|
29
|
+
- **`doctor`: a `worktree-residue` check** (the 15th). Reports per-repo `.worktrees` count and total size and points at `/multi-agent:garbage-collect` above the threshold. Written because the leak was invisible: one reference repo had accumulated 15 worktrees and 11.4 GB, and nothing reported it.
|
|
30
|
+
|
|
31
|
+
- **`smoke-repo-residue.sh`** (26 assertions, 10 sections) - simulates a full run's artefacts in a temporary repo and proves `git status --porcelain` is empty afterwards. There was no gate of this shape before, which is why the leak survived.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **`vercel-deploy.sh` never read the credential store it advertised.** The header has promised "resolved from prefs keychainMapping if unset" since the wrapper was written; the block under that promise sourced the resolver only to name it in an error message. A machine with the token onboarded was still told to set an environment variable. Resolution now lives in one function that both `doctor` and `deploy` call, so the diagnostic can no longer report a token missing that a deploy would have found.
|
|
36
|
+
|
|
37
|
+
The key was also **impossible to map**: `keychainMapping` is `additionalProperties: false` and carried no `vercel` slot, so the remediation the error printed could not be followed. The slot now exists in the schema, the preferences template, `credential-inventory.sh`'s capability table and the `keychain.md` key table - the four places `smoke-credential-key-alignment.sh` requires a runtime key to appear, and that gate caught three of them being missing.
|
|
38
|
+
|
|
39
|
+
- **`.skill-manifest.json` had been stale since 09-09.** Four `SKILL.md` files changed under it and two more were never in it at all, so every install printed drift warnings nobody could act on, and a signed manifest that is allowed to rot is decorative. `smoke-gate-wiring.sh` exempted `sign-skills.sh` as "release-time signing" for a release step that did not exist; the release flow now names it, and the manifest smoke verifies the **shipped tree** rather than only its fixtures.
|
|
40
|
+
|
|
41
|
+
- **An exclude file with no trailing newline was corrupted.** The block marker was appended directly onto the user's last line, which both mangled that line and made the block unremovable, since the removal matches on the marker being at the start of a line.
|
|
42
|
+
|
|
43
|
+
- **`gc-tmp.sh` swept four temp prefixes it never knew about** (`complaint-analysis-*`, `generate-issue-*`, `cred-inventory-*`, `pr-body-*`) and `*-wiki` clones. The `.git` requirement that guards wiki clones applies to those alone - applied to the prefix list it would have narrowed an unconditional sweep.
|
|
44
|
+
|
|
45
|
+
- **`gc-worktrees.sh` failed silently when the hygiene lib was absent**, doing nothing where it was meant to write the guard. It now says so loudly.
|
|
46
|
+
|
|
47
|
+
- **Two legacy data roots** (`~/.claude/multi-agent-tasks/`, `~/.claude/.multi-agent-state/`, both with zero code references) are recognised by `pruneAbandonedTrees()` through an entry marker, so one content-bearing entry without the marker keeps the whole tree rather than the installer deleting somebody's data.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- `worktree-finalize.sh` prunes through the resolved real path rather than `$PROJECT_ROOT`, which differs whenever the repo is reached through a symlink.
|
|
52
|
+
- `purge` is the only path that releases the managed exclude block - a garbage collection run is not a reason to stop ignoring the pipeline's own artefacts.
|
|
53
|
+
- `smoke-offload-ref.sh` and `smoke-gc-worktrees.sh` now assert through `git check-ignore` on the real leak paths instead of matching our own printed strings.
|
|
54
|
+
|
|
55
|
+
## [16.30.0] - 2026-09-12
|
|
56
|
+
|
|
57
|
+
The flow video existed as a contract with no recorder, no UI test ever ran, and the PR carried filenames where a reviewer needed a picture. This release makes the evidence real and asks the user what it is worth paying for.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **`capture-evidence.sh video start|stop`** - the recorder the contract has described since 16.24.0 and nothing implemented. iOS records through `simctl io recordVideo --codec h264` (hevc does not play in the Jira attachment preview or several browsers) and stops on SIGINT, which is the only signal that makes simctl close the container; a TERM leaves an mp4 every player refuses. Android uses `screenrecord` with its own 180s ceiling, which the script clamps to and reports, because one preference honoured on one platform and silently halved on the other is worse than a stated limit. Shell rather than MCP, like the still capture beside it: a host with no toolkit MCP registered still produces evidence.
|
|
62
|
+
|
|
63
|
+
**Both recorders encode on change**, so a flow over a screen that never moved is a valid two-frame file a fraction of a second long. That file is kept - it is not broken - but `stop` says on stderr that nothing moved, and the caller records it as a gap rather than presenting a still as a flow. Nothing asserts the duration against wall clock anywhere, because that check fails a correct capture of a static screen.
|
|
64
|
+
|
|
65
|
+
- **`run-ui-tests.sh`** - finds the repo's own UI test target, picks the tests covering the changed files, and runs them. Exit 3 (target present, nothing matches) and exit 4 (no target) are reasons to fall to the next video tier; only exit 1 is a red test, and the log is the evidence, since a runner that died before reaching the tests also exits non-zero.
|
|
66
|
+
|
|
67
|
+
**The signal is `XCUIApplication`, not a folder named `*UITests`.** In the reference iOS app 477 files sit under such a path and exactly 2 drive the UI; the other 475 are snapshot tests that render a view and compare pixels without launching the app. Recording video around one of those produces a still frame and files it as a flow. And a real app has many candidates - 17 UI test directories there, 8 instrumentation modules in the Android app - so detection reports the whole set and lets the changed files choose. Taking the first off a `find` is a guess wearing a measurement's clothes.
|
|
68
|
+
|
|
69
|
+
Detection reads the filesystem and never calls `xcodebuild -list`, which resolves the SPM graph first and took 72 seconds on the reference workspace. It also found a target the `xcodebuild` path had missed entirely. The scheme, which only a run needs, is resolved in `run` behind a timeout.
|
|
70
|
+
|
|
71
|
+
- **`probe-evidence-capability.sh`** - measures before the user is asked. Offering "unit + UI test with a screen recording" and discovering afterwards that there is no target or no booted device spends the user's answer on something that was never going to happen. Every absent value carries its reason, because `no booted simulator, but one is available to boot` and `no iOS simulator available on this machine` close the same menu row and ask for completely different things. A value it could not measure is `null`, never `false`: with no `adb` on the PATH, "no device attached" is a negative nobody looked for and reads exactly like one somebody checked.
|
|
72
|
+
|
|
73
|
+
- **Test depth is a question** (Phase 0 Step 7.7): unit only, unit plus the UI test with a recording, or unit plus an MCP-driven flow. The options are built from the probe; a closed one keeps its row and states why, and when every option but the first is closed nothing is asked at all and `testDepthSource` records `forced` - a one-option question has no answer in it. Asked at intake rather than in Phase 5 because Phase 5 is absent from four of the eight modes, and a question asked where it cannot be reached is a question nobody answers. `prefs.global.testDepth.default` serves both autopilot entries.
|
|
74
|
+
|
|
75
|
+
- **The tier is re-checked at capture time.** A simulator booted at intake can be gone by the time the build goes green, so Phase 3 re-measures the device row alone and records the transition it came down from.
|
|
76
|
+
|
|
77
|
+
- **`state.visualEvidence.host`, and a PR that shows the picture.** Resolved in Phase 6: Jira takes both stills and video as attachments; with no Jira the stills go to an orphan `evidence/<task-id>` branch and the PR body embeds them. There is no GitHub API that attaches a file to an issue or a PR - the web uploader needs a browser session - so a branch is the only mechanism a script has that neither touches the PR diff nor publishes a release. **A private repo gets a blob permalink rather than an embed**: GitHub renders markdown images through a proxy that has no credentials for a private repo, and a broken image reads as missing evidence. Video stays Jira-only, and on a GitHub run none is recorded at all rather than paying UI-test minutes for a download nobody opens.
|
|
78
|
+
|
|
79
|
+
- **The probe's output contract**, settled by its own review. `--json-out <path>` writes the JSON the state records while stdout stays `KEY=VALUE`, so one run serves both the shell that builds the menu and the state that keeps the measurement; two runs meant two repo scans and two chances to disagree. Every value is **shell-quoted**, because Phase 0 evals that output and the reasons are prose: unquoted, `EVIDENCE_DEVICE_REASON=no booted simulator, but one is available to boot` makes `eval` run `booted` as a command and keep only the first word. That bug is invisible on any machine where the reasons happen to come back empty, which is the machine it was first tested on. `--only device` skips the detection scan for the Phase 3 re-check, and reports tier 1 as **unknown** rather than closed there - a tier that was never probed is not a tier that was ruled out, which is the same rule the probe states about every other field.
|
|
80
|
+
|
|
81
|
+
- **`smoke-ui-test-runner.sh`** (24 assertions) and **`smoke-evidence-probe.sh`** (33). The first caught a real bug while being written: `run` keyed its "no target" exit off the single chosen target, which is deliberately empty while several candidates exist, so "this repo has nothing to record" and "nothing covers what changed" collapsed into one code.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- **`smoke-no-mcp-in-dev-phases.sh` failed the pipeline's own prescribed calls.** It rejected every `mcpCalls[]` entry at phase >= 2 regardless of tool, while the rule it enforces (Locked 30, `rules/figma-pipeline.md`, `phase-3-dev.md` item 7) names `mcp__claude_ai_Figma__*` and nothing else, because what it protects is a single source of design truth, not MCP as a transport. `phase-3-dev.md` builds with `ios_xcodebuild` and `phase-5-test.md` audits with the accessibility tools, both at phase >= 2 and neither reading a design; the first person to record telemetry honestly is the one the gate would have failed. Now filtered on the tool name, with three assertions covering both directions.
|
|
86
|
+
|
|
87
|
+
- **The design-check MCP probe had never run.** `design-check/SKILL.md` read `mcpServers[...].args[0]` as a script path and `cd`'d to its parent. The registration is the npx form, so `args[0]` is `-y`, the `cd` failed every time, and a gate that exists to catch a stale tool list was inert. It now launches the server exactly as it is registered, which is what `doctor.mjs` already did.
|
|
88
|
+
|
|
89
|
+
- **`evidence-gate.mjs` never read the `screenshot` field** it has carried in the manual-test document shape since the gate shipped, so a criterion with `"screenshot": null` passed as a verified manual test on a UI change - the one case the picture was added for. New `--require-screenshot` flag, set by Phase 5 when visual evidence is required: a passing criterion must name a file that is actually on disk, since a path pointing nowhere is not evidence.
|
|
90
|
+
|
|
91
|
+
- **The derived `multi-agent-manual-test` skill had no evidence gate at all**, while its command sibling had one. Two copies of a command that disagree about whether a claim needs proof is the drift `skill-siblings.mjs` exists to surface.
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- `features/visual-evidence.md` section 4 rewritten around probe -> question -> run, plus a new host section carrying the two GitHub constraints that shape the PR rendering.
|
|
96
|
+
- Four phase docs gained a step, so `token-budget.json` moves with them: phase 0 (probe + depth question), 3 and 5 (recording), 6 (host resolution). The docs were compressed first and the new numbers are the measured residual, not headroom - these files are read into context on every run, so the budget is the place that cost is recorded rather than absorbed.
|
|
97
|
+
- `smoke-visual-evidence.sh` now records two seconds on a booted device and verifies the h264 file instead of grepping this repo's own markdown for the words "Tier 1" and `agent_run_steps`. A gate that asserts what we print proves nothing. Where no device is present the skip is printed as its own assertion, because `run-smokes.mjs` counts a silent suite as zero assertions and treats it as inert. It also holds the host to its two constraints: the public shape must embed an image, and the private shape must **not** - an embedded raw URL from a private repo renders broken for every reader, which looks like missing evidence rather than a link nobody clicked.
|
|
98
|
+
|
|
17
99
|
## [16.29.0] - 2026-09-11
|
|
18
100
|
|
|
19
101
|
### Added
|
package/docs/features.md
CHANGED
|
@@ -271,6 +271,20 @@ Signals + weights: `security_path` ×3, `migration` ×4, `public_api` ×2, `no_t
|
|
|
271
271
|
|
|
272
272
|
`pipeline/scripts/test-gap-scan.mjs` runs at Phase 5 Step 0. Walks the diff for newly added public symbols and reports those with no paired test. Stack-specific rules ship for iOS, Android, Python, Node.js. iOS Views and Android `@Composable` symbols default to `important`; other public API additions to `suggestion`. Optional gating via `prefs.testGap.blockingThreshold` - when set, the report becomes a Phase 4 rework finding once `important + blocking` count exceeds the threshold.
|
|
273
273
|
|
|
274
|
+
### Visual Evidence (UI changes)
|
|
275
|
+
|
|
276
|
+
A UI change carries its own picture. `state.visualEvidence.required` is decided mechanically from `taskType` plus the changed-file list, never from a reading of the task.
|
|
277
|
+
|
|
278
|
+
**Stills.** The "before" is the reporter's own ticket attachment, harvested in Phase 0; the pipeline never rebuilds the old state to photograph it. The "after" is captured in Phase 3 right after the build goes green, not Phase 5, which autopilot and both local modes drop. `capture-evidence.sh` cleans the status bar and downscales to 1242px so two captures of one screen differ by the change and not by the clock.
|
|
279
|
+
|
|
280
|
+
**The flow video rides on a test run.** `probe-evidence-capability.sh` measures the UI test target, the tests matching this change, the device, the recorder and the MCP registration; Phase 0 Step 7.7 then asks the depth with the options built from that measurement, and a closed option keeps its row and states why. Tier 1 runs the repo's own UI test and records around it, tier 2 drives the flow through `agent_run_steps`, tier 3 records nothing and says so. The tier is re-checked before the recording starts, because a simulator booted at intake can be gone by Phase 3.
|
|
281
|
+
|
|
282
|
+
UI test detection keys on `XCUIApplication` rather than on a folder named `*UITests`: in a real app the overwhelming majority of files under such a path are snapshot tests, which never launch the app and would produce a still frame filed as a flow.
|
|
283
|
+
|
|
284
|
+
**Where it lands.** Jira takes both stills and video as attachments. With no Jira the stills go to an orphan `evidence/<task-id>` branch and the PR body embeds them, or links them with a blob permalink when the repo is private (GitHub's image proxy has no credentials for a private repo, and a broken image reads as missing evidence). Phase 6 blocks when a required artefact is neither published nor explained; the gate is against silence, not against an honest "the ticket carries no image".
|
|
285
|
+
|
|
286
|
+
Toggle via `prefs.global.visualEvidence.enabled` (default ON), `visualEvidence.githubHost`, `visualEvidence.maxAttachmentMb`, `visualEvidence.maxVideoSeconds`, `prefs.global.testDepth.default`.
|
|
287
|
+
|
|
274
288
|
### Triage Memory
|
|
275
289
|
|
|
276
290
|
Per-repo append-only JSONL corpus at `~/.claude/memory/multi-agent/<repo-slug>/triage-corpus.jsonl`. Phase 7 ingests every triage output (idempotent), Phase 1 enriches the analysis with similar past tasks, Phase 4 triage attaches prior-art hits to each raw finding with an explicit bias hedge. Token-overlap recall, zero deps, Node-18-compatible. `/multi-agent:search "<text>" --semantic` routes the query to the corpus instead of agent-log grep. Toggle via `prefs.global.priorArtEnrichment.enabled` (default ON).
|
package/install/_common.mjs
CHANGED
|
@@ -268,13 +268,30 @@ export function pruneLegacyMultiAgentSkills(skillsDir) {
|
|
|
268
268
|
*
|
|
269
269
|
* `root: "home"` entries sit beside `~/.claude`, not inside it.
|
|
270
270
|
*
|
|
271
|
-
* @type {ReadonlyArray<{dir: string, root?: "claude"|"home", reason: string}>}
|
|
271
|
+
* @type {ReadonlyArray<{dir: string, root?: "claude"|"home", reason: string, knownEntries?: string[], entryMarker?: string}>}
|
|
272
272
|
*/
|
|
273
273
|
const ABANDONED_TREES = Object.freeze([
|
|
274
274
|
{
|
|
275
275
|
dir: "eval",
|
|
276
276
|
reason: "eval corpora; the harnesses that read them are maintainer-only and no longer ship",
|
|
277
277
|
},
|
|
278
|
+
{
|
|
279
|
+
// Two roots the pipeline wrote task state into before the log root settled
|
|
280
|
+
// on ~/.claude/logs/multi-agent/. Nothing in the tree references either any
|
|
281
|
+
// more - no script, no doc, no schema - so they are orphaned copies of
|
|
282
|
+
// state nothing can read. `entryMarker` is the guard: every entry must be a
|
|
283
|
+
// directory holding an agent-state.json, which is the shape only this
|
|
284
|
+
// pipeline wrote. A folder someone else put there fails that test and the
|
|
285
|
+
// tree is kept.
|
|
286
|
+
dir: "multi-agent-tasks",
|
|
287
|
+
entryMarker: "agent-state.json",
|
|
288
|
+
reason: "pre-log-root task state; the current root is ~/.claude/logs/multi-agent/",
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
dir: ".multi-agent-state",
|
|
292
|
+
entryMarker: "agent-state.json",
|
|
293
|
+
reason: "pre-log-root task state; the current root is ~/.claude/logs/multi-agent/",
|
|
294
|
+
},
|
|
278
295
|
{
|
|
279
296
|
dir: ".multi-agent",
|
|
280
297
|
root: "home",
|
|
@@ -290,9 +307,33 @@ const ABANDONED_TREES = Object.freeze([
|
|
|
290
307
|
/**
|
|
291
308
|
* @param {string} target
|
|
292
309
|
* @param {ReadonlyArray<string>|undefined} knownEntries
|
|
310
|
+
* @param {string|undefined} entryMarker file every entry must contain, for trees keyed by task id
|
|
293
311
|
* @returns {boolean} true when every top-level entry is one the abandoned tree is known to contain
|
|
294
312
|
*/
|
|
295
|
-
function isKnownLegacyTree(target, knownEntries) {
|
|
313
|
+
function isKnownLegacyTree(target, knownEntries, entryMarker) {
|
|
314
|
+
if (entryMarker) {
|
|
315
|
+
// Shape check rather than a name list, for trees whose entries are task ids
|
|
316
|
+
// and therefore unknowable in advance. An entry qualifies when it carries
|
|
317
|
+
// the marker, or when it is an empty directory - an abandoned run that was
|
|
318
|
+
// torn down leaves one of those, and nothing that is empty can belong to
|
|
319
|
+
// anyone else. A directory with content but no marker is somebody's, and
|
|
320
|
+
// one such entry keeps the whole tree.
|
|
321
|
+
try {
|
|
322
|
+
return readdirSync(target)
|
|
323
|
+
.filter((name) => name !== ".DS_Store")
|
|
324
|
+
.every((name) => {
|
|
325
|
+
const entry = join(target, name);
|
|
326
|
+
if (existsSync(join(entry, entryMarker))) return true;
|
|
327
|
+
try {
|
|
328
|
+
return readdirSync(entry).filter((n) => n !== ".DS_Store").length === 0;
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
296
337
|
if (!knownEntries) return true;
|
|
297
338
|
try {
|
|
298
339
|
return readdirSync(target).every((name) => knownEntries.includes(name));
|
|
@@ -329,11 +370,11 @@ export const COMMAND_RENAMES = Object.freeze({
|
|
|
329
370
|
*/
|
|
330
371
|
export function pruneAbandonedTrees(claudeDir, home = dirname(claudeDir)) {
|
|
331
372
|
let removed = 0;
|
|
332
|
-
for (const { dir, root, reason, knownEntries } of ABANDONED_TREES) {
|
|
373
|
+
for (const { dir, root, reason, knownEntries, entryMarker } of ABANDONED_TREES) {
|
|
333
374
|
const base = root === "home" ? home : claudeDir;
|
|
334
375
|
const target = join(base, dir);
|
|
335
376
|
if (!existsSync(target)) continue;
|
|
336
|
-
if (!isKnownLegacyTree(target, knownEntries)) {
|
|
377
|
+
if (!isKnownLegacyTree(target, knownEntries, entryMarker)) {
|
|
337
378
|
console.log(
|
|
338
379
|
` kept ${target}: holds entries no abandoned install wrote, so it is not ours to remove`,
|
|
339
380
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.31.0",
|
|
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",
|
|
@@ -79,23 +79,24 @@ Every Phase 0 / Phase 2 decision uses a native `AskUserQuestion` picker (numbere
|
|
|
79
79
|
|
|
80
80
|
a. **Session** - `mcp__multi-agent-toolkit__design_scenario_inventory` must be present in the tools available to you, and `mcp__multi-agent-toolkit__design_visual_compare` must accept `live_region`. Absent → the session is bound to a stale process.
|
|
81
81
|
|
|
82
|
-
b. **Disk** - probe
|
|
82
|
+
b. **Disk** - probe what a fresh connection WOULD serve. Launch it as registered; under npx `args[0]` is `-y`, not a path.
|
|
83
83
|
```bash
|
|
84
|
-
|
|
84
|
+
MA_MCP_CMD=$(python3 -c "import json,os,shlex;e=json.load(open(os.path.expanduser('~/.claude.json')))['mcpServers']['multi-agent-toolkit'];print(shlex.join([e['command']]+e.get('args',[])))")
|
|
85
85
|
printf '%s\n%s\n' \
|
|
86
86
|
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
|
|
87
87
|
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
|
|
88
|
-
|
|
|
88
|
+
| eval "$MA_MCP_CMD" 2>/dev/null | python3 -c "
|
|
89
89
|
import sys,json
|
|
90
90
|
for l in sys.stdin:
|
|
91
91
|
try: m=json.loads(l)
|
|
92
92
|
except: continue
|
|
93
|
-
if m.get('id')==1: print('version',
|
|
93
|
+
if m.get('id')==1: print('version',m['result']['serverInfo']['version'])
|
|
94
94
|
if m.get('id')==2:
|
|
95
95
|
n=[t['name'] for t in m['result']['tools']]
|
|
96
|
-
print('tools',
|
|
96
|
+
print('tools',len(n),'inventory','design_scenario_inventory' in n)"
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
|
|
99
100
|
Branch on the two results:
|
|
100
101
|
- **disk OK, session missing the tool** → **HALT** and tell the user to reconnect: `/mcp` → `multi-agent-toolkit` → Reconnect. Note the tool-count tell (a stale build advertises one fewer `design_*` tool). If a reconnect does not take, stale server processes may be lingering - `pgrep -f multi-agent-toolkit-mcp/index.js` with `ps -o lstart=` shows their start times, and any that predate the code's mtime cannot serve the current tools.
|
|
101
102
|
- **disk itself stale** (older version, or `inventory False`) → **HALT**: the checkout needs updating before a reconnect can help.
|
|
@@ -243,24 +243,25 @@ Key Features:
|
|
|
243
243
|
Multi-Platform iOS, Android, Backend, Web - auto-detected
|
|
244
244
|
Build Queue xcodebuild/Gradle serialized with lock file - parallel safe
|
|
245
245
|
Stack Aware Auto-detects iOS/Android/Python/Node/Go/Docker, uses the enabled stack plugin's skills
|
|
246
|
-
Det. Gates Build + lint + test + secret scan
|
|
247
|
-
Multi-Repo Per-repo worktrees
|
|
248
|
-
Identity Routing Git identity
|
|
249
|
-
Issue Safety Never auto-closes issues (4 approvals
|
|
250
|
-
Store Compliance /multi-agent:test "store-ready"
|
|
251
|
-
|
|
246
|
+
Det. Gates Build + lint + test + secret scan before AI review
|
|
247
|
+
Multi-Repo Per-repo worktrees + identity, integration build before commit
|
|
248
|
+
Identity Routing Git identity picked from the repo origin URL (corporate vs personal)
|
|
249
|
+
Issue Safety Never auto-closes issues (4 approvals, GitHub + Jira)
|
|
250
|
+
Store Compliance /multi-agent:test "store-ready" - 18-rule iOS audit (ITMS, Privacy Manifest,
|
|
251
|
+
signing, debug leaks, IPv6, SDK list) + 21-rule Android audit
|
|
252
252
|
Bilingual EN + TR - outputLanguage toggles assistant explanations; promptLanguage is locked en
|
|
253
253
|
|
|
254
254
|
Quality & Telemetry (advisory, on by default - flip prefs.global.* to disable):
|
|
255
255
|
|
|
256
256
|
Diff Risk Score Phase 4 Step 1.75 ranks files before reviewer dispatch (security paths,
|
|
257
|
-
|
|
257
|
+
migrations, no-test-change, complexity) - heuristic, sub-second
|
|
258
258
|
Test Gap Report Phase 5 Step 0 surfaces public symbols added in this branch with no paired test
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
259
|
+
Visual Evidence before/after stills + flow video on UI changes; Step 7.7 asks the depth
|
|
260
|
+
Cost Breakdown Phase 7 appends per-phase tokens + estimated USD to agent-log.md
|
|
261
|
+
Triage Memory Phase 7 ingests accepted/deferred/rejected findings into a repo corpus
|
|
262
|
+
Prior-Art Lookup Phase 1 + Phase 4 query the corpus for similar findings, inject as context
|
|
263
|
+
Per-Persona Dispatch reads `preferredModel` from the persona file; override per call via
|
|
264
|
+
PHASE_MODEL_OVERRIDE; ladder fable -> opus -> sonnet -> haiku
|
|
264
265
|
|
|
265
266
|
------------------------------------------------------------
|
|
266
267
|
|
|
@@ -51,5 +51,5 @@ Lets you switch to the task branch for manual testing in Xcode before the PR is
|
|
|
51
51
|
```json
|
|
52
52
|
{"criteria":[{"spec":"<quote>","source":"analysis 15.2 | plan task 3 | user","observed":"<what was seen>","verdict":"pass|fail|not-tested","reason":"<required when not-tested>","screenshot":"<path or null>"}],"verdict":"passed|failed"}
|
|
53
53
|
```
|
|
54
|
-
then run `node $HOME/.claude/scripts/evidence-gate.mjs --claim manual --status passed --evidence "$WORKTREE/.pipeline/manual-test.json"
|
|
54
|
+
then run `node $HOME/.claude/scripts/evidence-gate.mjs --claim manual --status passed --evidence "$WORKTREE/.pipeline/manual-test.json"`, adding `--require-screenshot` when `state.visualEvidence.required` is true (a passing criterion then has to name a screenshot that is actually on disk). Exit 1 means the "ok" is not accepted: name the criterion that is missing evidence and wait for the next reply. Exit 0 → `phase-tracker.sh update 5 completed` + `phase-tracker.sh meta 5 Result "local test passed (user)"`, recreate the worktree, continue to Phase 6. Full contract: `$HOME/.claude/multi-agent-refs/phases/phase-5-test.md` step 5.
|
|
55
55
|
- **Fix needed** → `phase-tracker.sh now 5 "applying fix: <summary>"`, recreate the worktree, apply the fix
|
|
@@ -120,9 +120,8 @@ Step 0 gate rules and why: `features/doctor.md`.
|
|
|
120
120
|
- `~/.claude/CLAUDE.md`, `~/.claude/rules/`, `~/.claude/knowledge/`
|
|
121
121
|
- `~/.claude/scripts/` - EXCEPT `pre-commit-check.sh`, `agent-guard.sh`, `agent-guard.py`, and `build-stack-plugins.mjs` (generic, synced)
|
|
122
122
|
- `~/.claude/settings.json`
|
|
123
|
-
- **Any `~/.claude/commands/multi-agent/*/SKILL.md` whose frontmatter has `local-only: true`** - these are user/repo-specific alias wrappers that delegate to a private marketplace plugin's skills exposed as `multi-agent:<name>`. Syncing them would leak the private plugin/skill names into the public pipeline.
|
|
123
|
+
- **Any `~/.claude/commands/multi-agent/*/SKILL.md` whose frontmatter has `local-only: true`** - these are user/repo-specific alias wrappers that delegate to a private marketplace plugin's skills exposed as `multi-agent:<name>`. Syncing them would leak the private plugin/skill names into the public pipeline. Skip them at copy time; the backstop below proves none reached `pipeline/commands/`.
|
|
124
124
|
```bash
|
|
125
|
-
# backstop: no local-only wrapper may exist in the synced target
|
|
126
125
|
grep -rl "^local-only: true" ~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
127
126
|
&& { echo "ABORT: local-only wrapper leaked into pipeline/commands"; exit 1; } || true
|
|
128
127
|
# backstop: corporate refs only (generic ai-ios-toolkit is fine)
|
|
@@ -140,7 +139,6 @@ Step 0 gate rules and why: `features/doctor.md`.
|
|
|
140
139
|
# normalize: restore English descriptions, strip description-en sidecars
|
|
141
140
|
node ~/multi-agent-pipeline/pipeline/scripts/localize-commands.mjs restore \
|
|
142
141
|
--dir ~/multi-agent-pipeline/pipeline/commands/multi-agent
|
|
143
|
-
# backstop: no sidecar may survive into the repo
|
|
144
142
|
grep -rl "^description-en:" ~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
145
143
|
&& { echo "ABORT: description-en sidecar leaked into pipeline/commands"; exit 1; } || true
|
|
146
144
|
# backstop: repo description lines must be English (Turkish-diacritics proxy)
|
|
@@ -467,7 +465,8 @@ When invoked with the `release` argument:
|
|
|
467
465
|
```
|
|
468
466
|
1. REPO Claude Code -> pipeline repo (genericized)
|
|
469
467
|
2. VERIFY Personal data scan -> 0 results; config-hygiene scan (scan-agent-config.sh) -> 0 HIGH
|
|
470
|
-
3. LINT + TEST ESLint + smoke tests
|
|
468
|
+
3. LINT + TEST ESLint + smoke tests (stale .skill-manifest.json fails here; re-sign
|
|
469
|
+
it with sign-skills.sh whenever a SKILL.md moved)
|
|
471
470
|
4. VERSION package.json version bump (patch/minor/major)
|
|
472
471
|
5. Commit + TAG git commit + git tag v{VERSION}
|
|
473
472
|
6. PUSH git push --tags; publish is local (release.yml off)
|
|
@@ -358,6 +358,7 @@ capability_of() {
|
|
|
358
358
|
figma|figma_mcp) echo "fetch design context, screenshots and Code Connect mappings" ;;
|
|
359
359
|
jenkins) echo "read build results" ;;
|
|
360
360
|
npm) echo "publish to the npm registry" ;;
|
|
361
|
+
vercel) echo "deploy the website to Vercel and read its deployment status" ;;
|
|
361
362
|
appstore_connect_key_id|appstore_connect_issuer_id)
|
|
362
363
|
echo "validate an archive against App Store rules before submission (the .p8 itself is a file at ~/.appstoreconnect/private_keys/, never a mapping)" ;;
|
|
363
364
|
appstore_connect_apple_id|appstore_connect_password_item)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# repo-hygiene.sh
|
|
4
|
+
# Keeps pipeline artefacts out of the user's repository. Sourceable library.
|
|
5
|
+
#
|
|
6
|
+
# The pipeline writes into a checkout in three ways: the worktree parent
|
|
7
|
+
# (.worktrees/), the per-run artefact dir (.pipeline/), and the offload tree
|
|
8
|
+
# (.multi-agent/). Only the first was ever excluded, and only by a one-line
|
|
9
|
+
# guard duplicated across two phase docs and gc-worktrees.sh. The other two
|
|
10
|
+
# were never excluded anywhere, which is invisible in worktree mode (the
|
|
11
|
+
# worktree is deleted) and permanent in --local mode (nothing deletes it).
|
|
12
|
+
#
|
|
13
|
+
# Functions:
|
|
14
|
+
# ma_hygiene_ensure_exclusions <repo-root> Write the managed block into
|
|
15
|
+
# .git/info/exclude. Idempotent;
|
|
16
|
+
# rewrites the block on upgrade so
|
|
17
|
+
# new entries reach old checkouts.
|
|
18
|
+
# ma_hygiene_release_exclusions <repo-root> Remove the managed block, and
|
|
19
|
+
# nothing else. Lines a human put
|
|
20
|
+
# there are never touched.
|
|
21
|
+
# ma_hygiene_local_gitignore <repo-root> Write .multi-agent/.gitignore.
|
|
22
|
+
# ma_hygiene_prune_empty <repo-root> rmdir the artefact parents when
|
|
23
|
+
# their last child is gone.
|
|
24
|
+
#
|
|
25
|
+
# Every function is best-effort: a read-only or non-git directory is not an
|
|
26
|
+
# error, because refusing to run a pipeline over a repo we cannot tidy is worse
|
|
27
|
+
# than leaving it untidy. Callers do not check the return value.
|
|
28
|
+
#
|
|
29
|
+
# bash 3.2 compatible (macOS ships 3.2 and has no flock, no mapfile).
|
|
30
|
+
|
|
31
|
+
MA_HYGIENE_BEGIN='# >>> multi-agent pipeline (managed) >>>'
|
|
32
|
+
MA_HYGIENE_END='# <<< multi-agent pipeline (managed) <<<'
|
|
33
|
+
|
|
34
|
+
# Paths the pipeline writes inside a checkout. `.worktrees/` predates this file
|
|
35
|
+
# and may already be present as a bare line; git treats a duplicated pattern as
|
|
36
|
+
# one, so the managed block restates it rather than trying to adopt it.
|
|
37
|
+
ma_hygiene_patterns() {
|
|
38
|
+
cat <<'PATTERNS'
|
|
39
|
+
.worktrees/
|
|
40
|
+
.pipeline/
|
|
41
|
+
.multi-agent/
|
|
42
|
+
triage-output.json
|
|
43
|
+
.review-diff.txt
|
|
44
|
+
.build.log
|
|
45
|
+
.test.log
|
|
46
|
+
PATTERNS
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Same contract every gc-*.sh in this tree carries: refuse to operate on `/`,
|
|
50
|
+
# on $HOME, or on a path that does not resolve. $HOME matters specifically -
|
|
51
|
+
# `$HOME/.multi-agent/` is a real directory the installer knows about, so a
|
|
52
|
+
# caller that passed the wrong root would have had `prune_empty` delete a file
|
|
53
|
+
# out of it. Callers never check the return value; refusing is silent and safe.
|
|
54
|
+
#
|
|
55
|
+
# One consequence worth knowing: offload-ref.sh and bulk-read.sh fall back to
|
|
56
|
+
# $PWD when they are not inside a repo, so offloading from $HOME itself now
|
|
57
|
+
# skips writing the ignore file. The payload is still written; only the ignore
|
|
58
|
+
# is skipped, and $HOME/.multi-agent/ is a tree the installer prunes anyway.
|
|
59
|
+
ma_hygiene_safe_root() {
|
|
60
|
+
local root="${1:-}" resolved
|
|
61
|
+
[ -n "$root" ] || return 1
|
|
62
|
+
resolved=$(cd "$root" 2>/dev/null && pwd -P) || return 1
|
|
63
|
+
[ -n "$resolved" ] || return 1
|
|
64
|
+
[ "$resolved" = "/" ] && return 1
|
|
65
|
+
[ "$resolved" = "$(cd "$HOME" 2>/dev/null && pwd -P)" ] && return 1
|
|
66
|
+
printf '%s\n' "$resolved"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Resolve the exclude file through --git-common-dir, not --git-dir: inside a
|
|
70
|
+
# worktree the latter points at .git/worktrees/<name>, whose info/exclude is
|
|
71
|
+
# per-worktree and dies with it.
|
|
72
|
+
ma_hygiene_exclude_path() {
|
|
73
|
+
local root="$1" common
|
|
74
|
+
common=$(git -C "$root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 1
|
|
75
|
+
[ -n "$common" ] || return 1
|
|
76
|
+
printf '%s/info/exclude\n' "$common"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
ma_hygiene_ensure_exclusions() {
|
|
80
|
+
local root ex tmp
|
|
81
|
+
root=$(ma_hygiene_safe_root "${1:-}") || return 0
|
|
82
|
+
ex=$(ma_hygiene_exclude_path "$root") || return 0
|
|
83
|
+
mkdir -p "$(dirname "$ex")" 2>/dev/null || return 0
|
|
84
|
+
[ -f "$ex" ] || : > "$ex" 2>/dev/null || return 0
|
|
85
|
+
|
|
86
|
+
tmp="${ex}.ma-tmp.$$"
|
|
87
|
+
# Drop any previous managed block, keep everything else verbatim, then append
|
|
88
|
+
# the current one. sed is the portable way to delete an inclusive range.
|
|
89
|
+
if grep -qF "$MA_HYGIENE_BEGIN" "$ex" 2>/dev/null; then
|
|
90
|
+
sed "/^$(printf '%s' "$MA_HYGIENE_BEGIN" | sed 's/[][\.*^$\/]/\\&/g')$/,/^$(printf '%s' "$MA_HYGIENE_END" | sed 's/[][\.*^$\/]/\\&/g')$/d" "$ex" > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
|
|
91
|
+
else
|
|
92
|
+
cat "$ex" > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# A file whose last line has no newline would take the BEGIN marker onto the
|
|
96
|
+
# end of it. That corrupts the user's line AND leaves the marker unanchored,
|
|
97
|
+
# so `release_exclusions` can never match it again and the block is stuck in
|
|
98
|
+
# their file for good. Close the line first.
|
|
99
|
+
if [ -s "$tmp" ] && [ "$(tail -c 1 "$tmp" | wc -l | tr -d ' ')" = "0" ]; then
|
|
100
|
+
printf '\n' >> "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
printf '%s\n' "$MA_HYGIENE_BEGIN"
|
|
105
|
+
ma_hygiene_patterns
|
|
106
|
+
printf '%s\n' "$MA_HYGIENE_END"
|
|
107
|
+
} >> "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
|
|
108
|
+
|
|
109
|
+
mv "$tmp" "$ex" 2>/dev/null || rm -f "$tmp"
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
ma_hygiene_release_exclusions() {
|
|
114
|
+
local root ex tmp
|
|
115
|
+
root=$(ma_hygiene_safe_root "${1:-}") || return 0
|
|
116
|
+
ex=$(ma_hygiene_exclude_path "$root") || return 0
|
|
117
|
+
[ -f "$ex" ] || return 0
|
|
118
|
+
grep -qF "$MA_HYGIENE_BEGIN" "$ex" 2>/dev/null || return 0
|
|
119
|
+
|
|
120
|
+
tmp="${ex}.ma-tmp.$$"
|
|
121
|
+
sed "/^$(printf '%s' "$MA_HYGIENE_BEGIN" | sed 's/[][\.*^$\/]/\\&/g')$/,/^$(printf '%s' "$MA_HYGIENE_END" | sed 's/[][\.*^$\/]/\\&/g')$/d" "$ex" > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
|
|
122
|
+
mv "$tmp" "$ex" 2>/dev/null || rm -f "$tmp"
|
|
123
|
+
return 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# `*` and not a list of children: an ignore file that names its siblings does
|
|
127
|
+
# not name itself, so .multi-agent/.gitignore stayed permanently untracked -
|
|
128
|
+
# the one piece of residue the guard existed to prevent. Git reads an ignore
|
|
129
|
+
# file whether or not it is itself ignored, so self-exclusion is safe.
|
|
130
|
+
ma_hygiene_local_gitignore() {
|
|
131
|
+
local root gi
|
|
132
|
+
root=$(ma_hygiene_safe_root "${1:-}") || return 0
|
|
133
|
+
gi="$root/.multi-agent/.gitignore"
|
|
134
|
+
mkdir -p "$root/.multi-agent" 2>/dev/null || return 0
|
|
135
|
+
if [ ! -f "$gi" ] || ! grep -q '^\*$' "$gi" 2>/dev/null; then
|
|
136
|
+
printf '# Local run artefacts - never commit. Ignores this file too.\n*\n' > "$gi" 2>/dev/null || return 0
|
|
137
|
+
fi
|
|
138
|
+
return 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# rmdir, never rm -rf: it fails harmlessly when anything is left, which is the
|
|
142
|
+
# behaviour we want. An empty .worktrees/ and .pipeline/evidence/ survive a
|
|
143
|
+
# clean run today and read as leftovers to anyone looking at the checkout.
|
|
144
|
+
ma_hygiene_prune_empty() {
|
|
145
|
+
local root d
|
|
146
|
+
root=$(ma_hygiene_safe_root "${1:-}") || return 0
|
|
147
|
+
for d in "$root/.pipeline/evidence" "$root/.pipeline" "$root/.worktrees/.archive" "$root/.worktrees" "$root/.multi-agent/refs"; do
|
|
148
|
+
[ -d "$d" ] && rmdir "$d" 2>/dev/null
|
|
149
|
+
done
|
|
150
|
+
|
|
151
|
+
# .multi-agent survives the loop above because it still holds the .gitignore
|
|
152
|
+
# this library wrote. That file is ours, so a directory holding nothing else
|
|
153
|
+
# is residue, not user data - but `memory/` there IS user data and is never
|
|
154
|
+
# touched (see commands/multi-agent/uninstall/SKILL.md).
|
|
155
|
+
if [ -d "$root/.multi-agent" ]; then
|
|
156
|
+
local leftover
|
|
157
|
+
leftover=$(ls -A "$root/.multi-agent" 2>/dev/null)
|
|
158
|
+
if [ "$leftover" = ".gitignore" ]; then
|
|
159
|
+
rm -f "$root/.multi-agent/.gitignore" 2>/dev/null
|
|
160
|
+
rmdir "$root/.multi-agent" 2>/dev/null
|
|
161
|
+
fi
|
|
162
|
+
fi
|
|
163
|
+
return 0
|
|
164
|
+
}
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
# pipeline/lib/vercel-deploy.sh doctor # env + CLI check
|
|
24
24
|
#
|
|
25
25
|
# Environment:
|
|
26
|
-
# VERCEL_TOKEN - required for deploy
|
|
26
|
+
# VERCEL_TOKEN - required for deploy; when unset it is resolved from
|
|
27
|
+
# prefs keychainMapping.vercel via credential-store.sh
|
|
27
28
|
# VERCEL_ORG_ID - optional, scopes deploys to a team
|
|
28
29
|
# VERCEL_PROJECT_ID - optional, links to a specific project
|
|
29
30
|
# VERCEL_DEPLOY_QUIET - when set, suppresses progress lines (raw CLI output stays redacted)
|
|
@@ -63,8 +64,44 @@ cmd_redact() {
|
|
|
63
64
|
redact_filter
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
# Resolve VERCEL_TOKEN from the credential store when the environment has none.
|
|
68
|
+
# The header has promised "resolved from prefs keychainMapping if unset" since
|
|
69
|
+
# this wrapper was written, but nothing ever read the store: the block below
|
|
70
|
+
# sourced the resolver only to name it in an error message. Both `doctor` and
|
|
71
|
+
# `deploy` call this now, so the diagnostic can no longer report "unset" for a
|
|
72
|
+
# token a deploy would have found.
|
|
73
|
+
#
|
|
74
|
+
# credential-store.sh takes the LOGICAL key and maps it through
|
|
75
|
+
# prefs.global.keychainMapping itself, so `get vercel` is the whole mechanism.
|
|
76
|
+
# Command substitution, never argv: the CLI echoes its own arguments on retry.
|
|
77
|
+
ma_vercel_resolve_token() {
|
|
78
|
+
[ -z "${VERCEL_TOKEN:-}" ] || return 0
|
|
79
|
+
local _r
|
|
80
|
+
for _r in \
|
|
81
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
82
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
83
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
84
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
85
|
+
[ -f "$_r" ] || continue
|
|
86
|
+
# shellcheck source=/dev/null
|
|
87
|
+
. "$_r" 2>/dev/null || true
|
|
88
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
89
|
+
done
|
|
90
|
+
[ -n "${CRED_STORE:-}" ] || return 0
|
|
91
|
+
local _tok
|
|
92
|
+
_tok=$("$CRED_STORE" get vercel 2>/dev/null || true)
|
|
93
|
+
# Only export a real value. Exporting an empty VERCEL_TOKEN would hand every
|
|
94
|
+
# child process a defined-but-blank variable where it previously saw none,
|
|
95
|
+
# and "set to nothing" reads differently from "unset" to the Vercel CLI.
|
|
96
|
+
[ -n "$_tok" ] || return 0
|
|
97
|
+
VERCEL_TOKEN="$_tok"
|
|
98
|
+
export VERCEL_TOKEN
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
66
102
|
cmd_doctor() {
|
|
67
103
|
local rc=0
|
|
104
|
+
ma_vercel_resolve_token
|
|
68
105
|
if command -v vercel >/dev/null 2>&1; then
|
|
69
106
|
echo "vercel: $(vercel --version 2>&1 | redact_filter | head -1)"
|
|
70
107
|
else
|
|
@@ -109,28 +146,10 @@ cmd_deploy() {
|
|
|
109
146
|
exit 1
|
|
110
147
|
fi
|
|
111
148
|
|
|
149
|
+
ma_vercel_resolve_token
|
|
112
150
|
if [ -z "${VERCEL_TOKEN:-}" ]; then
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# The resolver ships alongside this wrapper in lib/; fall back to the
|
|
116
|
-
# per-CLI install locations when running from a different cwd layout.
|
|
117
|
-
# shellcheck source=/dev/null
|
|
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
|
|
133
|
-
echo "ERROR: VERCEL_TOKEN not set. Resolve via env or keychain ('${CRED_STORE:-credential-store.sh} get <vercel-key>')." >&2
|
|
151
|
+
echo "ERROR: VERCEL_TOKEN not set and keychainMapping.vercel resolved nothing." >&2
|
|
152
|
+
echo "Map it once: point prefs.global.keychainMapping.vercel at the keychain item holding the token." >&2
|
|
134
153
|
echo "Hint: vercel CLI accepts the token via env var; do NOT pass --token= on argv (leaks on retry)." >&2
|
|
135
154
|
exit 1
|
|
136
155
|
fi
|