@mmerterden/multi-agent-pipeline 16.30.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 +38 -0
- package/install/_common.mjs +45 -4
- package/package.json +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/features/doctor.md +12 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +2 -2
- 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 +7 -7
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
- package/pipeline/preferences-template.json +2 -1
- package/pipeline/schemas/prefs.schema.json +4 -0
- package/pipeline/scripts/bulk-read.sh +13 -5
- package/pipeline/scripts/doctor.mjs +56 -1
- 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/purge.sh +11 -0
- package/pipeline/scripts/worktree-finalize.sh +9 -0
- package/pipeline/skills/.skill-manifest.json +19 -11
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,44 @@ 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
|
+
|
|
17
55
|
## [16.30.0] - 2026-09-12
|
|
18
56
|
|
|
19
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.
|
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",
|
|
@@ -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
|
|
@@ -195,3 +195,15 @@ read the registration and stopped would have called that healthy.
|
|
|
195
195
|
Free space on the volume holding `$HOME`. WARN under 2 GB: a worktree plus a
|
|
196
196
|
build is the largest thing a run writes, and ENOSPC mid-run corrupts the state
|
|
197
197
|
file it was writing at the time.
|
|
198
|
+
|
|
199
|
+
### worktree-residue
|
|
200
|
+
|
|
201
|
+
Worktrees left under `<repo>/.worktrees/` in the repository the caller is
|
|
202
|
+
standing in. WARN at five or more, or at 2 GB. A finished task removes its own
|
|
203
|
+
worktree at PR time, but a run that stops before Phase 6 never reaches that
|
|
204
|
+
step and nothing else collects it: the finalizer only runs on success, and
|
|
205
|
+
`gc-worktrees` only sweeps entries git has already forgotten. Each survivor is
|
|
206
|
+
a full second checkout, so the total is measured in gigabytes rather than
|
|
207
|
+
megabytes. SKIP outside a git repository - a project name in prefs is a name,
|
|
208
|
+
not a path, and guessing checkout locations to produce a number is how a
|
|
209
|
+
diagnostic starts lying.
|
|
@@ -121,10 +121,10 @@ evidence, and there is now one fewer of them. Triage also runs on `opus`, which
|
|
|
121
121
|
makes it the same model as Reviewer 1; the Step 3 anonymisation requirement
|
|
122
122
|
already covers that case and is not optional here.
|
|
123
123
|
|
|
124
|
-
**Cost accounting.** `prefs.global.costBudget.
|
|
124
|
+
**Cost accounting.** `prefs.global.costBudget.pricingModel` defaults to `fable` to
|
|
125
125
|
keep the estimate an upper bound. With the rung off, that default prices every
|
|
126
126
|
call above what it can cost and trips the budget ceiling early, which then
|
|
127
|
-
triggers a downgrade nobody needed. Set `
|
|
127
|
+
triggers a downgrade nobody needed. Set `pricingModel` to `opus` alongside
|
|
128
128
|
`fableEnabled: false`.
|
|
129
129
|
|
|
130
130
|
No log line is emitted per dispatch for this: it is a configured absence, not a
|
|
@@ -158,6 +158,7 @@ The shell driver auto-delegates to `~/.claude/scripts/keychain.py` on macOS / Li
|
|
|
158
158
|
| `graylog_test` | Graylog (test) | `${USER}_Graylog_Test_Access_Token` | API Token, optional - unset falls back to `graylog` | Same page on the TEST instance; optional |
|
|
159
159
|
| `firebase` | Firebase | `${USER}_Firebase_Access_Json` | Service-account JSON, as issued. One key per Firebase project; extras are named `..._Json_<projectId>` and listed in `global.firebase.accounts[]` | Firebase Console -> Project settings -> Service accounts -> Generate new private key |
|
|
160
160
|
| `jenkins` | Jenkins CI | `${USER}_Jenkins_Access_Token` | API Token | Jenkins -> User -> Configure -> API Token |
|
|
161
|
+
| `vercel` | Vercel deploys | `${USER}_Vercel_Access_Token` | Access Token | Vercel -> Account Settings -> Tokens. Read by `vercel-deploy.sh`; never passed on argv |
|
|
161
162
|
| `appstore_connect_key_id` | App Store Connect | `${USER}_AppStoreConnect_Key_Id` | Identifier, not a secret | App Store Connect -> Users and Access -> Integrations -> App Store Connect API |
|
|
162
163
|
| `appstore_connect_issuer_id` | App Store Connect | `${USER}_AppStoreConnect_Issuer_Id` | Identifier, not a secret | Same page as the key id; one issuer id per team |
|
|
163
164
|
| `appstore_connect_apple_id` | App Store Connect | `${USER}_AppStoreConnect_Apple_Id` | Email address | The Apple ID itself; no generation step |
|
|
@@ -63,7 +63,7 @@ Knowledge files grow over time. Maintenance rules:
|
|
|
63
63
|
and a rebuild is seconds, so age is the wrong question to ask of it.
|
|
64
64
|
- Stale entries are added to the prompt with a "STALE - verify before relying" tag
|
|
65
65
|
- `prune-logs` does not touch knowledge - only deletes logs and state
|
|
66
|
-
- `purge` does not touch knowledge either
|
|
66
|
+
- `purge` does not touch knowledge either. Today the only removal path is `/multi-agent:uninstall --all-data`, which drops every project's knowledge at once; there is no per-project clear yet
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
@@ -447,14 +447,15 @@ fi
|
|
|
447
447
|
|
|
448
448
|
If the path is still registered and healthy after the heal, enter + pull instead of re-adding (existing behavior). Only when `worktree add` still fails after the heal do the rollback / collision flow in the multi-repo block below.
|
|
449
449
|
|
|
450
|
-
**
|
|
450
|
+
**Repo residue guard (required once per repo, idempotent):** keep every path the pipeline writes - `.worktrees/`, `.pipeline/`, `.multi-agent/` and the loose logs - out of the index via the clone-local exclude file (never committed, so no PR noise). One call; the rationale is in the lib header:
|
|
451
451
|
|
|
452
452
|
```bash
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
grep -qxF '.worktrees/' "$ex" 2>/dev/null || printf '.worktrees/\n' >> "$ex"
|
|
453
|
+
. "$HOME/.claude/lib/repo-hygiene.sh"
|
|
454
|
+
ma_hygiene_ensure_exclusions "$PROJECT_ROOT"
|
|
456
455
|
```
|
|
457
456
|
|
|
457
|
+
Rewritten on each call, so an older checkout picks up new entries; lines a human added are never touched.
|
|
458
|
+
|
|
458
459
|
**Traversal-prune contract:** the exclude guard covers only the git INDEX, not
|
|
459
460
|
filesystem scans. Since each worktree is a full checkout, an unpruned tree walk
|
|
460
461
|
double-processes every file and can re-stage gitlinks. So `.worktrees` joins the
|
|
@@ -468,14 +469,13 @@ scan, `shadow-git.sh` excludes, and the shared walkers (`extract-conventions.sh`
|
|
|
468
469
|
When `state.projects[].length > 1`, repeat steps 2-4 **serially per repo** (worktrees are cheap; serial keeps git index sane and surfaces collisions one at a time):
|
|
469
470
|
|
|
470
471
|
```bash
|
|
472
|
+
. "$HOME/.claude/lib/repo-hygiene.sh"
|
|
471
473
|
for proj in "${PROJECTS[@]}"; do
|
|
472
474
|
WT_PATH="$proj/.worktrees/$BRANCH_DIR/"
|
|
473
475
|
git -C "$proj" worktree prune 2>/dev/null || true # heal stale admin state
|
|
474
476
|
git -C "$proj" worktree list --porcelain | grep -qF "$WT_PATH" \
|
|
475
477
|
&& git -C "$proj" worktree unlock "$WT_PATH" 2>/dev/null || true
|
|
476
|
-
|
|
477
|
-
mkdir -p "$(dirname "$ex")"
|
|
478
|
-
grep -qxF '.worktrees/' "$ex" 2>/dev/null || printf '.worktrees/\n' >> "$ex" # residue guard
|
|
478
|
+
ma_hygiene_ensure_exclusions "$proj" # residue guard
|
|
479
479
|
git -C "$proj" worktree add "$WT_PATH" -b "$BRANCH" "origin/$BASE_BRANCH"
|
|
480
480
|
git -C "$WT_PATH" config user.name "${proj_identity_name}"
|
|
481
481
|
git -C "$WT_PATH" config user.email "${proj_identity_email}"
|
|
@@ -120,7 +120,7 @@ Based on Phase 1 `detectedStack`, assign relevant skills:
|
|
|
120
120
|
|
|
121
121
|
#### Output contract
|
|
122
122
|
|
|
123
|
-
Phase 2 produces an object conforming to `$HOME/.claude/schemas/planning-output.schema.json` - `tasks[]` with `id`, `
|
|
123
|
+
Phase 2 produces an object conforming to `$HOME/.claude/schemas/planning-output.schema.json` - `tasks[]` with `id`, `title`, `type`, `files`, plus optional `dependsOn` and `acceptanceCriteria`. Phase 3 reads `tasks[]` in dependency order; the schema's `dependsOn` field drives the ready-task picker.
|
|
124
124
|
|
|
125
125
|
**Required: validator gate (deterministic) - run on the persisted file before the approval gate renders the plan; the validator's exit code decides, not the LLM turn:**
|
|
126
126
|
|
|
@@ -49,7 +49,7 @@ The analysis document is the SOLE design source in Phase 3. Variant choices, pad
|
|
|
49
49
|
|
|
50
50
|
#### Input contract
|
|
51
51
|
|
|
52
|
-
Phase 3 consumes the Phase 2 output object conforming to `$HOME/.claude/schemas/planning-output.schema.json` - the task graph (`tasks[]` with `id`, `
|
|
52
|
+
Phase 3 consumes the Phase 2 output object conforming to `$HOME/.claude/schemas/planning-output.schema.json` - the task graph (`tasks[]` with `id`, `title`, `type`, `files`, and optional `dependsOn` / `acceptanceCriteria`) plus the architecture review notes. Tasks execute in dependency order; the schema's `dependsOn` field drives the ready-task picker. In a Short run (no Phase 2), Opus generates the equivalent task list inline before entering the loop below.
|
|
53
53
|
|
|
54
54
|
**Plan Todo iteration (opt-in)**: gated by `prefs.global.planTodos.enabled` (default: `false`). When enabled and Phase 2 Step 4.5 emitted a `plan.todos[]`, Phase 3 iterates via `$HOME/.claude/lib/plan-todos.sh next/start/complete/fail` instead of walking `tasks[]` directly. When disabled, the loop walks `tasks[]` from `planning-output` - TDD contract is unchanged. Full helper loop + state semantics: `$HOME/.claude/multi-agent-refs/features/plan-todos.md`. A todo with `sourceTag: Reuse` binds the file analysis already found; `Modify` edits in place. Writing a new file over a `Reuse` step is a Locked 11 violation and Phase 4 flags it.
|
|
55
55
|
|
|
@@ -126,7 +126,7 @@ Tier 1 / Tier 2 records print `screenshotUrl` from the captured evidence (Tier 2
|
|
|
126
126
|
git -C "$PROJECT_ROOT" worktree unlock "{worktree-path}" 2>/dev/null || true
|
|
127
127
|
fi
|
|
128
128
|
```
|
|
129
|
-
Phase 0's
|
|
129
|
+
Phase 0's repo residue guard is already in `.git/info/exclude` - no re-add.
|
|
130
130
|
- Recreate worktree from branch: `git -C $PROJECT_ROOT worktree add {worktree-path} {branch}`
|
|
131
131
|
- Re-set git identity: `git -C {worktree-path} config user.name/email` (from state)
|
|
132
132
|
- Go back to Phase 3
|
|
@@ -198,6 +198,10 @@
|
|
|
198
198
|
"type": ["string", "null"],
|
|
199
199
|
"description": "NPM registry token (npm.pkg.github.com or npmjs.com). Used by package publish flow."
|
|
200
200
|
},
|
|
201
|
+
"vercel": {
|
|
202
|
+
"type": ["string", "null"],
|
|
203
|
+
"description": "Keychain item holding the Vercel access token. vercel-deploy.sh resolves it through credential-store.sh so the value never reaches argv, where the CLI would leak it on a retry. Absent = deploys need VERCEL_TOKEN in the environment."
|
|
204
|
+
},
|
|
201
205
|
"appstore_connect_key_id": {
|
|
202
206
|
"type": ["string", "null"],
|
|
203
207
|
"description": "App Store Connect API key ID. Tier 1 of the App Store Connect access chain, used by /multi-agent:store-ready Gate 2 (and its iOS alias /multi-agent:testflight-validation). An identifier rather than a secret; mapped anyway so every credential is read through the same layer. Creating an API key needs an Admin or App Manager role, which is why Tier 2 exists."
|
|
@@ -124,11 +124,19 @@ ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
|
|
124
124
|
REFS_DIR="$ROOT/.multi-agent/refs"
|
|
125
125
|
NODE_ID=""
|
|
126
126
|
if mkdir -p "$REFS_DIR" 2>/dev/null; then
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
# Shared with offload-ref.sh via pipeline/lib/repo-hygiene.sh. `../lib` resolves in
|
|
128
|
+
# both layouts: pipeline/scripts -> pipeline/lib in the repo, ~/.claude/scripts ->
|
|
129
|
+
# ~/.claude/lib installed. Fall back to writing it inline if the lib is absent,
|
|
130
|
+
# so this script stays usable standalone.
|
|
131
|
+
_MA_HYG="$(dirname "${BASH_SOURCE[0]}")/../lib/repo-hygiene.sh"
|
|
132
|
+
if [ -f "$_MA_HYG" ]; then
|
|
133
|
+
. "$_MA_HYG"
|
|
134
|
+
ma_hygiene_local_gitignore "$ROOT"
|
|
135
|
+
else
|
|
136
|
+
GITIGNORE="$ROOT/.multi-agent/.gitignore"
|
|
137
|
+
if [ ! -f "$GITIGNORE" ] || ! grep -q '^\*$' "$GITIGNORE" 2>/dev/null; then
|
|
138
|
+
printf '# Local run artefacts - never commit. Ignores this file too.\n*\n' > "$GITIGNORE"
|
|
139
|
+
fi
|
|
132
140
|
fi
|
|
133
141
|
if command -v shasum >/dev/null 2>&1; then
|
|
134
142
|
DIGEST=$(shasum -a 256 "$FILE" | awk '{print substr($1,1,8)}')
|
|
@@ -84,6 +84,7 @@ const CHECK_IDS = [
|
|
|
84
84
|
"task-tools",
|
|
85
85
|
"mcp-registration",
|
|
86
86
|
"disk-space",
|
|
87
|
+
"worktree-residue",
|
|
87
88
|
];
|
|
88
89
|
|
|
89
90
|
// Only these five may return BLOCK. Enforced below, not merely documented: a
|
|
@@ -641,6 +642,59 @@ function checkDiskSpace() {
|
|
|
641
642
|
ok("disk-space");
|
|
642
643
|
}
|
|
643
644
|
|
|
645
|
+
// A finished task removes its own worktree at PR time, but a run that dies
|
|
646
|
+
// before Phase 6 never reaches that step and nothing else collects it: the
|
|
647
|
+
// finalizer only runs on success and gc-worktrees only sweeps entries git no
|
|
648
|
+
// longer knows about. So worktrees accumulate silently, and each one is a full
|
|
649
|
+
// second checkout. Measured on one real iOS repo: 15 left behind, 11 GB.
|
|
650
|
+
//
|
|
651
|
+
// Only the repo the caller is standing in is examined. A project name in prefs
|
|
652
|
+
// is a name, not a path, and guessing checkout locations to report a number is
|
|
653
|
+
// how a diagnostic starts lying.
|
|
654
|
+
function checkWorktreeResidue() {
|
|
655
|
+
let repo;
|
|
656
|
+
try {
|
|
657
|
+
repo = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
658
|
+
encoding: "utf8",
|
|
659
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
660
|
+
}).trim();
|
|
661
|
+
} catch {
|
|
662
|
+
skip("worktree-residue", "not inside a git repository, so there is no checkout to measure");
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const wt = join(repo, ".worktrees");
|
|
666
|
+
let entries;
|
|
667
|
+
try {
|
|
668
|
+
entries = readdirSync(wt).filter((n) => n !== ".DS_Store" && n !== ".archive");
|
|
669
|
+
} catch {
|
|
670
|
+
ok("worktree-residue");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (entries.length === 0) {
|
|
674
|
+
ok("worktree-residue");
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
let gb = null;
|
|
678
|
+
try {
|
|
679
|
+
const out = execFileSync("du", ["-sk", wt], { encoding: "utf8" });
|
|
680
|
+
const kb = Number(out.trim().split(/\s+/)[0]);
|
|
681
|
+
if (Number.isFinite(kb)) gb = kb / 1024 / 1024;
|
|
682
|
+
} catch {
|
|
683
|
+
/* du is not everywhere; the count alone is still worth reporting */
|
|
684
|
+
}
|
|
685
|
+
const size = gb === null ? "" : `, ${gb.toFixed(1)} GB`;
|
|
686
|
+
if (entries.length >= 5 || (gb !== null && gb >= 2)) {
|
|
687
|
+
report(
|
|
688
|
+
"worktree-residue",
|
|
689
|
+
"WARN",
|
|
690
|
+
`${entries.length} worktree(s) left under ${wt}${size}; runs that stopped before Phase 6 are never collected`,
|
|
691
|
+
"run /multi-agent:garbage-collect, or /multi-agent:kill for a task you know is dead",
|
|
692
|
+
);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
ok("worktree-residue");
|
|
696
|
+
}
|
|
697
|
+
|
|
644
698
|
/* ------------------------------------------------------------------ main -- */
|
|
645
699
|
|
|
646
700
|
function main() {
|
|
@@ -675,7 +729,7 @@ function main() {
|
|
|
675
729
|
} else {
|
|
676
730
|
process.stdout.write(`${line.severity} ${line.id} - ${line.problem} - ${line.step}\n`);
|
|
677
731
|
process.stdout.write(
|
|
678
|
-
|
|
732
|
+
`\n-> indeterminate: the layout did not resolve, so ${CHECK_IDS.length - 1} checks did not run\n`,
|
|
679
733
|
);
|
|
680
734
|
}
|
|
681
735
|
process.exitCode = 4;
|
|
@@ -694,6 +748,7 @@ function main() {
|
|
|
694
748
|
checkTaskTools();
|
|
695
749
|
checkMcpRegistration();
|
|
696
750
|
checkDiskSpace();
|
|
751
|
+
checkWorktreeResidue();
|
|
697
752
|
|
|
698
753
|
const blocked = results.filter((r) => r.severity === "BLOCK");
|
|
699
754
|
const warned = results.filter((r) => r.severity === "WARN");
|
|
@@ -97,8 +97,24 @@ NAME_ARGS=(
|
|
|
97
97
|
-o -name 'context-links-*'
|
|
98
98
|
-o -name 'context-by-type-*'
|
|
99
99
|
-o -name 'analysis-*'
|
|
100
|
+
-o -name 'complaint-analysis-*'
|
|
101
|
+
-o -name 'generate-issue-*'
|
|
102
|
+
-o -name 'cred-inventory-*'
|
|
103
|
+
-o -name 'pr-body-*'
|
|
104
|
+
-o \( -name '*-wiki' -type d \)
|
|
100
105
|
)
|
|
101
106
|
|
|
107
|
+
# `complaint-analysis-*` needs its own entry: find matches the basename against
|
|
108
|
+
# the whole glob, so `analysis-*` never matched it. Same for the other three,
|
|
109
|
+
# which were writing into $ROOT with nothing sweeping them (phase-0-init.md,
|
|
110
|
+
# phase-6-commit.md, generate-issue.md).
|
|
111
|
+
#
|
|
112
|
+
# `*-wiki` is the one loose pattern here, and a suffix glob over $ROOT could
|
|
113
|
+
# name something a person created. It is narrowed twice: `-type d` above, and
|
|
114
|
+
# the `.git` check in the loop below - the pipeline's only `*-wiki` artefact is
|
|
115
|
+
# a shallow clone (analysis/evidence.md), so a directory without a `.git` is
|
|
116
|
+
# somebody else's and is left alone.
|
|
117
|
+
|
|
102
118
|
MTIME_ARGS=()
|
|
103
119
|
if [ "$OLDER_MIN" -gt 0 ]; then
|
|
104
120
|
MTIME_ARGS=(-mmin "+$OLDER_MIN")
|
|
@@ -114,6 +130,20 @@ while IFS= read -r -d '' p; do
|
|
|
114
130
|
skipped_active=$((skipped_active + 1))
|
|
115
131
|
continue
|
|
116
132
|
fi
|
|
133
|
+
# The `.git` requirement belongs to the `*-wiki` pattern alone. Checking the
|
|
134
|
+
# suffix first would re-narrow entries an unambiguous prefix already claimed:
|
|
135
|
+
# `multi-agent-foo-wiki` ends in `-wiki`, and demanding a `.git` there would
|
|
136
|
+
# spare scratch that `multi-agent-*` has always swept unconditionally.
|
|
137
|
+
case "${p##*/}" in
|
|
138
|
+
multi-agent-* | issue-progress-* | channels-* | context-links-* | context-by-type-* | \
|
|
139
|
+
analysis-* | complaint-analysis-* | generate-issue-* | cred-inventory-* | pr-body-*) ;;
|
|
140
|
+
*-wiki)
|
|
141
|
+
# See the NAME_ARGS note: only our shallow clones, never a user's folder.
|
|
142
|
+
if [ ! -e "$p/.git" ]; then
|
|
143
|
+
continue
|
|
144
|
+
fi
|
|
145
|
+
;;
|
|
146
|
+
esac
|
|
117
147
|
matches+=("$p")
|
|
118
148
|
done < <(find "$ROOT" -mindepth 1 -maxdepth 1 \( "${NAME_ARGS[@]}" \) ${MTIME_ARGS[@]+"${MTIME_ARGS[@]}"} -print0 2>/dev/null)
|
|
119
149
|
|
|
@@ -12,8 +12,13 @@
|
|
|
12
12
|
# "Subproject commit" entry by a blanket `git add -A` before the
|
|
13
13
|
# residue guard existed. --yes runs `git rm --cached` (the commit
|
|
14
14
|
# itself stays yours to make).
|
|
15
|
-
# 4. missing residue guard ->
|
|
16
|
-
# .git/info/exclude
|
|
15
|
+
# 4. missing residue guard -> writes the managed block in
|
|
16
|
+
# .git/info/exclude (`.worktrees/`, `.pipeline/`, `.multi-agent/` and
|
|
17
|
+
# the loose run logs) so none of them can reach the index. The block is
|
|
18
|
+
# rewritten on each call, so a repo guarded by the old one-line form is
|
|
19
|
+
# upgraded in place.
|
|
20
|
+
# 5. empty artefact parents -> rmdir only, so a `.worktrees/` or
|
|
21
|
+
# `.pipeline/` left behind by a finished run stops reading as residue.
|
|
17
22
|
#
|
|
18
23
|
# Registered, healthy worktrees are NEVER touched. Finishing a task removes its
|
|
19
24
|
# own worktree in Phase 6 (worktree-finalize, v14.1.0+); killing one is
|
|
@@ -143,22 +148,39 @@ $gitlinks
|
|
|
143
148
|
EOF
|
|
144
149
|
fi
|
|
145
150
|
|
|
146
|
-
# 4. Residue guard: keep
|
|
151
|
+
# 4. Residue guard: keep every pipeline artefact out of the index from now on.
|
|
152
|
+
# `.worktrees/` was the only entry for a long time, which left `.pipeline/` and
|
|
153
|
+
# `.multi-agent/` writable into the branch; the managed block in repo-hygiene.sh
|
|
154
|
+
# covers all of them and is rewritten on each call, so an old checkout upgrades.
|
|
147
155
|
ex="$(git -C "$REPO" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)/info/exclude"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
_MA_HYG="$(dirname "${BASH_SOURCE[0]}")/../lib/repo-hygiene.sh"
|
|
157
|
+
[ -f "$_MA_HYG" ] && . "$_MA_HYG"
|
|
158
|
+
if [ -n "${ex%/info/exclude}" ] && ! grep -qF "${MA_HYGIENE_BEGIN:-# >>> multi-agent pipeline (managed) >>>}" "$ex" 2>/dev/null; then
|
|
159
|
+
if ! command -v ma_hygiene_ensure_exclusions >/dev/null 2>&1; then
|
|
160
|
+
# The guard is the whole point of step 4. Going quiet when the library is
|
|
161
|
+
# missing would leave the repo unguarded and say nothing, which is the one
|
|
162
|
+
# outcome worse than failing.
|
|
163
|
+
echo "gc-worktrees: WARN - $_MA_HYG not found; the residue guard was NOT written" >&2
|
|
164
|
+
elif [ "$DELETE" -eq 1 ]; then
|
|
165
|
+
ma_hygiene_ensure_exclusions "$REPO"
|
|
166
|
+
echo "→ wrote the managed residue block to .git/info/exclude"
|
|
167
|
+
changed=$((changed + 1))
|
|
153
168
|
else
|
|
154
|
-
echo "would
|
|
169
|
+
echo "would write the managed residue block to .git/info/exclude"
|
|
155
170
|
fi
|
|
156
171
|
fi
|
|
157
172
|
|
|
173
|
+
# 5. Artefact parents left empty by a finished run read as leftovers to anyone
|
|
174
|
+
# looking at the checkout. rmdir, never rm -rf: it fails harmlessly if anything
|
|
175
|
+
# is still there.
|
|
176
|
+
if [ "$DELETE" -eq 1 ] && command -v ma_hygiene_prune_empty >/dev/null 2>&1; then
|
|
177
|
+
ma_hygiene_prune_empty "$REPO"
|
|
178
|
+
fi
|
|
179
|
+
|
|
158
180
|
total=$(( ${#orphans[@]} + $(printf '%s' "$gitlinks" | grep -c . || true) ))
|
|
159
181
|
if [ "$DELETE" -eq 1 ]; then
|
|
160
182
|
echo "══ gc-worktrees: applied $changed change(s) in $REPO ══"
|
|
161
|
-
elif [ "$total" -eq 0 ] && [ "$pruned" -eq 0 ] && { [ -z "${ex%/info/exclude}" ] || grep -
|
|
183
|
+
elif [ "$total" -eq 0 ] && [ "$pruned" -eq 0 ] && { [ -z "${ex%/info/exclude}" ] || grep -qF "${MA_HYGIENE_BEGIN:-# >>> multi-agent pipeline (managed) >>>}" "$ex" 2>/dev/null; }; then
|
|
162
184
|
echo "gc-worktrees: no worktree residue in $REPO - nothing to do"
|
|
163
185
|
else
|
|
164
186
|
echo "══ gc-worktrees: dry-run - re-run with --yes to apply ══"
|
|
@@ -158,11 +158,19 @@ mkdir -p "$REFS_DIR" 2>/dev/null || { cat "$BUF"; exit 0; }
|
|
|
158
158
|
|
|
159
159
|
# Never commit an offloaded payload: it is a build log, and the worktree it
|
|
160
160
|
# belongs to is deleted at the end of the task.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
# Shared with bulk-read.sh via pipeline/lib/repo-hygiene.sh. `../lib` resolves in
|
|
162
|
+
# both layouts: pipeline/scripts -> pipeline/lib in the repo, ~/.claude/scripts ->
|
|
163
|
+
# ~/.claude/lib installed. Fall back to writing it inline if the lib is absent,
|
|
164
|
+
# so this script stays usable standalone.
|
|
165
|
+
_MA_HYG="$(dirname "${BASH_SOURCE[0]}")/../lib/repo-hygiene.sh"
|
|
166
|
+
if [ -f "$_MA_HYG" ]; then
|
|
167
|
+
. "$_MA_HYG"
|
|
168
|
+
ma_hygiene_local_gitignore "$ROOT"
|
|
169
|
+
else
|
|
170
|
+
GITIGNORE="$ROOT/.multi-agent/.gitignore"
|
|
171
|
+
if [ ! -f "$GITIGNORE" ] || ! grep -q '^\*$' "$GITIGNORE" 2>/dev/null; then
|
|
172
|
+
printf '# Local run artefacts - never commit. Ignores this file too.\n*\n' > "$GITIGNORE"
|
|
173
|
+
fi
|
|
166
174
|
fi
|
|
167
175
|
|
|
168
176
|
SLUG=$(printf '%s' "$LABEL" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed 's/-\{1,\}/-/g; s/^-//; s/-$//')
|
|
@@ -231,5 +231,16 @@ fi
|
|
|
231
231
|
# Drop the now-empty .worktrees/ shell (only when truly empty).
|
|
232
232
|
[ -d "$WT_ROOT" ] && rmdir "$WT_ROOT" 2>/dev/null
|
|
233
233
|
|
|
234
|
+
# purge is the whole-repo teardown, so it is the one place the managed exclude
|
|
235
|
+
# block should come back out - a per-task finalize must not, because other tasks
|
|
236
|
+
# in the same repo still need the guard. Empty artefact parents go with it.
|
|
237
|
+
# Only the marker-delimited block is removed; lines a human added stay.
|
|
238
|
+
_MA_HYG="$(dirname "${BASH_SOURCE[0]}")/../lib/repo-hygiene.sh"
|
|
239
|
+
if [ -f "$_MA_HYG" ]; then
|
|
240
|
+
. "$_MA_HYG"
|
|
241
|
+
ma_hygiene_prune_empty "$MAIN_WT"
|
|
242
|
+
ma_hygiene_release_exclusions "$MAIN_WT"
|
|
243
|
+
fi
|
|
244
|
+
|
|
234
245
|
echo "══ purge: removed $removed_wt worktree(s) and $removed_br local branch(es) in $MAIN_WT ══"
|
|
235
246
|
exit 0
|
|
@@ -318,6 +318,15 @@ if git -C "$rp_pr" worktree remove "$rp_wt" 2>/dev/null; then
|
|
|
318
318
|
' 2>/dev/null || true
|
|
319
319
|
fi
|
|
320
320
|
|
|
321
|
+
# The worktree is gone, but its parent `.worktrees/` stays behind as an empty
|
|
322
|
+
# directory and reads as leftover to anyone looking at the checkout. rmdir,
|
|
323
|
+
# never rm -rf: it fails harmlessly while any other task still has one.
|
|
324
|
+
_MA_HYG="$(dirname "${BASH_SOURCE[0]}")/../lib/repo-hygiene.sh"
|
|
325
|
+
if [ -f "$_MA_HYG" ]; then
|
|
326
|
+
. "$_MA_HYG"
|
|
327
|
+
ma_hygiene_prune_empty "$rp_pr"
|
|
328
|
+
fi
|
|
329
|
+
|
|
321
330
|
emit
|
|
322
331
|
exit 0
|
|
323
332
|
fi
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-09-
|
|
4
|
-
"skillCount":
|
|
3
|
+
"generatedAt": "2026-09-13T12:09:01Z",
|
|
4
|
+
"skillCount": 209,
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
7
7
|
"path": "shared/core/apple-archive-compliance/SKILL.md",
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"path": "shared/core/google-play-compliance/SKILL.md",
|
|
12
12
|
"sha256": "356ff2b30f414e582bb215c4728696af8e249dfe924e16a550294e48fd9bdcf9"
|
|
13
13
|
},
|
|
14
|
+
{
|
|
15
|
+
"path": "shared/core/multi-agent-analysis-jira/SKILL.md",
|
|
16
|
+
"sha256": "405e3afb02ad00cbcd1eace0ab3659d8d405dc78ec68c46f0836c23a83a43950"
|
|
17
|
+
},
|
|
14
18
|
{
|
|
15
19
|
"path": "shared/core/multi-agent-analysis-resolve/SKILL.md",
|
|
16
20
|
"sha256": "ad59c484a3a251799101aaebfe1cf50ceb10a2e2a389d89c6d56e67ba331ef79"
|
|
@@ -47,6 +51,10 @@
|
|
|
47
51
|
"path": "shared/core/multi-agent-diff-explain/SKILL.md",
|
|
48
52
|
"sha256": "7cb37a224e61d66e350202ecfc5fbc6c33da3fe0718504a093602f5cde5b648f"
|
|
49
53
|
},
|
|
54
|
+
{
|
|
55
|
+
"path": "shared/core/multi-agent-doctor/SKILL.md",
|
|
56
|
+
"sha256": "a9e6b8a764df223b9d6867cc9f2a89d93f989605891c829a81fc3e5ca139a1b2"
|
|
57
|
+
},
|
|
50
58
|
{
|
|
51
59
|
"path": "shared/core/multi-agent-feedback/SKILL.md",
|
|
52
60
|
"sha256": "9ec0c33fdaef52691fac48ac582e665bb8eb87d59009feabf156f71728d977a4"
|
|
@@ -73,7 +81,7 @@
|
|
|
73
81
|
},
|
|
74
82
|
{
|
|
75
83
|
"path": "shared/core/multi-agent-issue/SKILL.md",
|
|
76
|
-
"sha256": "
|
|
84
|
+
"sha256": "aec8737af368b01c7e3133521c711861456c18b07e3829425e5c0485a0181051"
|
|
77
85
|
},
|
|
78
86
|
{
|
|
79
87
|
"path": "shared/core/multi-agent-jira/SKILL.md",
|
|
@@ -101,7 +109,7 @@
|
|
|
101
109
|
},
|
|
102
110
|
{
|
|
103
111
|
"path": "shared/core/multi-agent-manual-test/SKILL.md",
|
|
104
|
-
"sha256": "
|
|
112
|
+
"sha256": "e7f7f8004d286c5b5b596f8659bcf57b2350eb9c67a55470bb2e9b714513a3a4"
|
|
105
113
|
},
|
|
106
114
|
{
|
|
107
115
|
"path": "shared/core/multi-agent-prune-logs/SKILL.md",
|
|
@@ -117,7 +125,7 @@
|
|
|
117
125
|
},
|
|
118
126
|
{
|
|
119
127
|
"path": "shared/core/multi-agent-refactor/SKILL.md",
|
|
120
|
-
"sha256": "
|
|
128
|
+
"sha256": "56b49a8216086961bc407a60b559942252a1aae3005b73811b1d40668758cc84"
|
|
121
129
|
},
|
|
122
130
|
{
|
|
123
131
|
"path": "shared/core/multi-agent-resume-local/SKILL.md",
|
|
@@ -133,7 +141,7 @@
|
|
|
133
141
|
},
|
|
134
142
|
{
|
|
135
143
|
"path": "shared/core/multi-agent-review-issue/SKILL.md",
|
|
136
|
-
"sha256": "
|
|
144
|
+
"sha256": "d9c0a256d37febab7be7d00252710ca065bd4e8a186ef47e00c5898e48f53cf5"
|
|
137
145
|
},
|
|
138
146
|
{
|
|
139
147
|
"path": "shared/core/multi-agent-review-jira/SKILL.md",
|
|
@@ -157,11 +165,11 @@
|
|
|
157
165
|
},
|
|
158
166
|
{
|
|
159
167
|
"path": "shared/core/multi-agent-search/SKILL.md",
|
|
160
|
-
"sha256": "
|
|
168
|
+
"sha256": "32541ceca20c489814f9d948763d5237d4a71473a05683d9da262b8f6dea815b"
|
|
161
169
|
},
|
|
162
170
|
{
|
|
163
171
|
"path": "shared/core/multi-agent-setup/SKILL.md",
|
|
164
|
-
"sha256": "
|
|
172
|
+
"sha256": "40dc72793cd6bd1054d6203275fbc9f4ecdd6d8dc5de8f2437e7f670f4c5c646"
|
|
165
173
|
},
|
|
166
174
|
{
|
|
167
175
|
"path": "shared/core/multi-agent-stack/SKILL.md",
|
|
@@ -169,7 +177,7 @@
|
|
|
169
177
|
},
|
|
170
178
|
{
|
|
171
179
|
"path": "shared/core/multi-agent-status/SKILL.md",
|
|
172
|
-
"sha256": "
|
|
180
|
+
"sha256": "436d1716ce5fc4977dadfce8a85b09877f93c626953739a7ad70eefbbcd25869"
|
|
173
181
|
},
|
|
174
182
|
{
|
|
175
183
|
"path": "shared/core/multi-agent-steer/SKILL.md",
|
|
@@ -181,7 +189,7 @@
|
|
|
181
189
|
},
|
|
182
190
|
{
|
|
183
191
|
"path": "shared/core/multi-agent-sync/SKILL.md",
|
|
184
|
-
"sha256": "
|
|
192
|
+
"sha256": "8f033b904e90926b15f6e5a8f8703b47a17373e5450921514633ea0a9edaf327"
|
|
185
193
|
},
|
|
186
194
|
{
|
|
187
195
|
"path": "shared/core/multi-agent-test-accessibility/SKILL.md",
|
|
@@ -213,7 +221,7 @@
|
|
|
213
221
|
},
|
|
214
222
|
{
|
|
215
223
|
"path": "shared/core/multi-agent-update/SKILL.md",
|
|
216
|
-
"sha256": "
|
|
224
|
+
"sha256": "e93e913473cb5d3a5810b30f26011a0d18754bed983890f4261b87972526d94a"
|
|
217
225
|
},
|
|
218
226
|
{
|
|
219
227
|
"path": "shared/core/multi-agent/SKILL.md",
|
|
@@ -207,7 +207,8 @@ When invoked with the `release` argument:
|
|
|
207
207
|
```
|
|
208
208
|
1. REPO Claude Code -> pipeline repo (genericized)
|
|
209
209
|
2. VERIFY Personal data scan -> 0 results
|
|
210
|
-
3. LINT + TEST ESLint + smoke tests
|
|
210
|
+
3. LINT + TEST ESLint + smoke tests (stale .skill-manifest.json fails here; re-sign
|
|
211
|
+
it with sign-skills.sh whenever a SKILL.md moved)
|
|
211
212
|
4. VERSION package.json version bump (patch/minor/major)
|
|
212
213
|
5. Commit + TAG git commit + git tag v{VERSION}
|
|
213
214
|
6. PUSH git push --tags; publish is local (release.yml off)
|