@mmerterden/multi-agent-pipeline 12.2.0 → 12.4.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 +84 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +68 -32
- package/pipeline/commands/multi-agent/forget/SKILL.md +29 -0
- package/pipeline/commands/multi-agent/help/SKILL.md +18 -0
- package/pipeline/commands/multi-agent/routines/SKILL.md +30 -0
- package/pipeline/commands/multi-agent/save/SKILL.md +46 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
- package/pipeline/lib/fetch-figma-annotations.sh +237 -0
- package/pipeline/multi-agent-refs/analysis-template.md +233 -123
- package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -3
- package/pipeline/schemas/analysis-spec.schema.json +78 -0
- package/pipeline/schemas/figma-project-config.schema.json +49 -0
- package/pipeline/schemas/migrations/prefs-2.3.0-to-2.4.0.mjs +32 -0
- package/pipeline/schemas/prefs.schema.json +58 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +8 -8
- package/pipeline/scripts/migrate-prefs.mjs +8 -1
- package/pipeline/scripts/routine-registry.mjs +226 -0
- package/pipeline/scripts/smoke-fetchers-offline.sh +41 -0
- package/pipeline/scripts/smoke-generate-issue.sh +3 -3
- package/pipeline/scripts/smoke-migrate-state.sh +3 -3
- package/pipeline/scripts/smoke-pref-migration.sh +8 -6
- package/pipeline/scripts/smoke-review-readiness.sh +1 -1
- package/pipeline/scripts/smoke-routines.sh +84 -0
- package/pipeline/scripts/smoke-validate-analysis-doc.sh +161 -0
- package/pipeline/scripts/validate-analysis-doc.mjs +196 -0
- package/pipeline/skills/shared/core/multi-agent-forget/SKILL.md +22 -0
- package/pipeline/skills/shared/core/multi-agent-routines/SKILL.md +20 -0
- package/pipeline/skills/shared/core/multi-agent-save/SKILL.md +27 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -4
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## 1. Command Inventory (
|
|
9
|
+
## 1. Command Inventory (41 commands)
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, dev,
|
|
13
|
-
dev-autopilot, dev-local, dev-local-autopilot, diff-explain, finish, garbage-collect,
|
|
13
|
+
dev-autopilot, dev-local, dev-local-autopilot, diff-explain, finish, forget, garbage-collect,
|
|
14
14
|
help, issue, jira, kill, language, local,
|
|
15
15
|
local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review, review-issue, review-jira,
|
|
16
|
-
scan, search, setup, stack, status, sync, test, uninstall, update
|
|
16
|
+
routines, save, scan, search, setup, stack, status, sync, test, uninstall, update
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Categories:
|
|
@@ -25,6 +25,7 @@ Categories:
|
|
|
25
25
|
- **Tail modes** (run the pipeline tail over already-done local work): `finish`
|
|
26
26
|
- **Ops commands** (one-shot, no worktree): `status`, `log`, `kill`, `purge`, `uninstall`, `resume`, `review`, `review-jira`, `review-issue`, `analysis`, `analysis-resolve`, `build-optimize`, `channels`, `scan`, `search`, `diff-explain`, `garbage-collect`, `prune-logs`
|
|
27
27
|
- **Meta-ops**: `setup`, `sync`, `update`, `help`, `refactor`, `test`, `stack`, `manual-test`, `language`
|
|
28
|
+
- **Routines** (user-defined routine registry; the routines they create are local-only and never synced): `save`, `routines`, `forget`
|
|
28
29
|
|
|
29
30
|
> **Inventory drift is a contract violation.** Adding a slash command under `pipeline/commands/multi-agent/` without updating this list + its counterpart Copilot dir (`pipeline/skills/shared/core/multi-agent-<cmd>/`) is a merge blocker. `smoke-commands-skills-parity.sh` enforces command ↔ skill directory parity; `smoke-cross-cli-behavior.sh` enforces behavior parity. This doc is the authoritative command list - bump the count + table together.
|
|
30
31
|
|
|
@@ -49,6 +49,24 @@
|
|
|
49
49
|
"default": "v3",
|
|
50
50
|
"description": "Template version emitted. v3 is the platform-agnostic + Pass B render template (Locked 22)."
|
|
51
51
|
},
|
|
52
|
+
"options": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"description": "Coverage opt-ins captured at Phase 0 Step 5a. Absent = defaults (no UI tests, basic accessibility).",
|
|
56
|
+
"properties": {
|
|
57
|
+
"uiTests": {
|
|
58
|
+
"type": "boolean",
|
|
59
|
+
"default": false,
|
|
60
|
+
"description": "When true, Section 15.6 renders UI test scenarios (XCUITest / Compose UI test)."
|
|
61
|
+
},
|
|
62
|
+
"a11yDepth": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"enum": ["basic", "full"],
|
|
65
|
+
"default": "basic",
|
|
66
|
+
"description": "basic = Section 16.1 checklist only; full = also render the 16.2 VoiceOver/TalkBack walkthrough."
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
52
70
|
"evidenceDigest": {
|
|
53
71
|
"type": ["string", "null"],
|
|
54
72
|
"pattern": "^sha256:[0-9a-f]{64}$",
|
|
@@ -110,6 +128,8 @@
|
|
|
110
128
|
"url": { "type": "string", "format": "uri" },
|
|
111
129
|
"fileKey": { "type": ["string", "null"] },
|
|
112
130
|
"nodeId": { "type": ["string", "null"] },
|
|
131
|
+
"screenshotUrl": { "type": ["string", "null"], "description": "Local PNG path or signed render URL for this node." },
|
|
132
|
+
"tier": { "type": ["integer", "null"], "enum": [1, 2, 3, null], "description": "Figma access tier used to capture this record (mirrors state.figmaAccess.tier)." },
|
|
113
133
|
"frames": {
|
|
114
134
|
"type": "array",
|
|
115
135
|
"items": {
|
|
@@ -134,6 +154,64 @@
|
|
|
134
154
|
"repoPath": { "type": ["string", "null"] }
|
|
135
155
|
}
|
|
136
156
|
}
|
|
157
|
+
},
|
|
158
|
+
"codeConnectSnippets": {
|
|
159
|
+
"type": "array",
|
|
160
|
+
"description": "Verbatim CodeConnectSnippet blocks captured on Tier 1 (component name + modifier chain).",
|
|
161
|
+
"items": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"additionalProperties": false,
|
|
164
|
+
"properties": {
|
|
165
|
+
"nodeId": { "type": ["string", "null"] },
|
|
166
|
+
"component": { "type": "string" },
|
|
167
|
+
"snippet": { "type": ["string", "null"] }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"tokens": {
|
|
172
|
+
"type": "array",
|
|
173
|
+
"items": {
|
|
174
|
+
"type": "object",
|
|
175
|
+
"additionalProperties": false,
|
|
176
|
+
"properties": {
|
|
177
|
+
"name": { "type": "string" },
|
|
178
|
+
"value": { "type": ["string", "null"] }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"textLayers": {
|
|
183
|
+
"type": "array",
|
|
184
|
+
"description": "Visible text-layer content. Treated as placeholder copy when an annotation exists for the same node (annotation wins).",
|
|
185
|
+
"items": {
|
|
186
|
+
"type": "object",
|
|
187
|
+
"additionalProperties": false,
|
|
188
|
+
"properties": {
|
|
189
|
+
"nodeId": { "type": ["string", "null"] },
|
|
190
|
+
"name": { "type": ["string", "null"] },
|
|
191
|
+
"text": { "type": "string" }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"annotations": {
|
|
196
|
+
"type": "array",
|
|
197
|
+
"description": "Dev Mode annotations captured as authoritative copy ground truth (Phase 1 annotation ingestion). Empty unless annotations.enabled in the project figma-config.",
|
|
198
|
+
"items": {
|
|
199
|
+
"type": "object",
|
|
200
|
+
"additionalProperties": false,
|
|
201
|
+
"required": ["nodeId", "mode"],
|
|
202
|
+
"properties": {
|
|
203
|
+
"nodeId": { "type": "string" },
|
|
204
|
+
"name": { "type": ["string", "null"] },
|
|
205
|
+
"designText": { "type": ["string", "null"], "description": "The visible-layer text at this node (often a placeholder)." },
|
|
206
|
+
"raw": { "type": ["string", "null"], "description": "The raw annotation label/labelMarkdown before parsing." },
|
|
207
|
+
"parsed": {
|
|
208
|
+
"type": "object",
|
|
209
|
+
"description": "Parsed copy keyed by language code (lowercased), e.g. { tr, en }.",
|
|
210
|
+
"additionalProperties": { "type": "string" }
|
|
211
|
+
},
|
|
212
|
+
"mode": { "type": "string", "enum": ["prefixed", "two-line", "single", "empty"] }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
137
215
|
}
|
|
138
216
|
}
|
|
139
217
|
}
|
|
@@ -50,6 +50,55 @@
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
|
+
"annotations": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"description": "Figma Dev Mode annotation ingestion. When enabled, /multi-agent:analysis treats annotations as the authoritative copy for a node (the visible text layer is a placeholder). Absent or disabled = current behavior (visible text layers only).",
|
|
57
|
+
"properties": {
|
|
58
|
+
"enabled": { "type": "boolean", "default": false },
|
|
59
|
+
"langPrefixes": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"items": { "type": "string" },
|
|
62
|
+
"default": ["TR", "EN"],
|
|
63
|
+
"description": "Ordered language-code prefixes the annotation parser recognizes, e.g. [\"TR\",\"EN\"]. A `TR: ...\\nEN: ...` label parses by prefix; a two-line label with no prefix maps line 1 -> langPrefixes[0], line 2 -> langPrefixes[1]; a single line -> langPrefixes[0]."
|
|
64
|
+
},
|
|
65
|
+
"devModeCategory": {
|
|
66
|
+
"type": ["string", "null"],
|
|
67
|
+
"description": "Optional Dev Mode annotation category to filter on (e.g. a copy/UX-writing category). Null = accept all annotations on the node."
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"localization": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"additionalProperties": false,
|
|
74
|
+
"description": "How this project owns localization. Drives Section 10 of /multi-agent:analysis. Default `in-repo` keeps the historical hand-filled per-locale grid; `externally-owned` defers per-locale values to an authoring system (the analysis names key + status + copy source + base value only).",
|
|
75
|
+
"properties": {
|
|
76
|
+
"ownership": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"enum": ["in-repo", "externally-owned"],
|
|
79
|
+
"default": "in-repo"
|
|
80
|
+
},
|
|
81
|
+
"authoringPipeline": {
|
|
82
|
+
"type": ["string", "null"],
|
|
83
|
+
"description": "Human-readable reference to the owning localization system when ownership = externally-owned (e.g. a workflow or resource-bot name). Rendered as the Ownership column value; never a secret."
|
|
84
|
+
},
|
|
85
|
+
"locales": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": { "type": "string" },
|
|
88
|
+
"description": "Supported locale codes for this project (e.g. [\"tr\",\"en\",\"ar\",\"de\",\"es\",\"fr\",\"it\",\"ru\"]). Overrides the built-in default locale set used by Section 10.",
|
|
89
|
+
"minItems": 1
|
|
90
|
+
},
|
|
91
|
+
"baseLanguage": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"default": "en",
|
|
94
|
+
"description": "Source/base language code. A key missing its base-language value is a defect (renders the raw key at runtime)."
|
|
95
|
+
},
|
|
96
|
+
"keyPattern": {
|
|
97
|
+
"type": ["string", "null"],
|
|
98
|
+
"description": "Key naming convention hint, e.g. `Namespace.Case`. Documentation only; not enforced."
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
53
102
|
"github": {
|
|
54
103
|
"type": "object",
|
|
55
104
|
"additionalProperties": false,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prefs-2.3.0-to-2.4.0.mjs - preference schema upgrade 2.3.0 -> 2.4.0
|
|
3
|
+
*
|
|
4
|
+
* Adds the user-defined routine registry:
|
|
5
|
+
* - global.routines (v2.4.0) - array of {name, description, createdAt,
|
|
6
|
+
* source, trigger?} indexing local-only
|
|
7
|
+
* /multi-agent:<name> commands saved via
|
|
8
|
+
* /multi-agent:save. Never synced (personal).
|
|
9
|
+
*
|
|
10
|
+
* Defaults to an empty array. Existing v2.3.0 prefs validate against the
|
|
11
|
+
* v2.4.0 schema without it, but the migrator initializes it so the routine
|
|
12
|
+
* commands can read/append without null-guards.
|
|
13
|
+
*
|
|
14
|
+
* Idempotent: safe on already-2.4.0 input. Non-destructive: preserves any
|
|
15
|
+
* user-saved routines.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} data - parsed JSON content of the preferences file
|
|
18
|
+
* @returns {object} - same data with schemaVersion = "2.4.0"
|
|
19
|
+
*/
|
|
20
|
+
export default function migrate(data) {
|
|
21
|
+
const out = JSON.parse(JSON.stringify(data));
|
|
22
|
+
|
|
23
|
+
out.schemaVersion = "2.4.0";
|
|
24
|
+
|
|
25
|
+
if (!out.global) out.global = {};
|
|
26
|
+
|
|
27
|
+
if (!Array.isArray(out.global.routines)) {
|
|
28
|
+
out.global.routines = [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"2.0.0",
|
|
18
18
|
"2.1.0",
|
|
19
19
|
"2.2.0",
|
|
20
|
-
"2.3.0"
|
|
20
|
+
"2.3.0",
|
|
21
|
+
"2.4.0"
|
|
21
22
|
],
|
|
22
23
|
"description": "v2.0.0: pre-v3.7. v2.1.0: v3.7+ adds identities[].servicePatMap, platformIdentityRouting, recentGroups, recentBranches, serviceStatus, settings, expanded keychainMapping. v2.2.0: v6.0.0 formalizes v5.7 / v5.8 additions (reportChannels, reportContent with technicalAnalysis, wikiScope, autopilotReportTimeoutSeconds) that had been running as 2.1.0 sub-migrations without a proper version bump."
|
|
23
24
|
},
|
|
@@ -273,6 +274,40 @@
|
|
|
273
274
|
"maxItems": 10
|
|
274
275
|
}
|
|
275
276
|
},
|
|
277
|
+
"routines": {
|
|
278
|
+
"type": "array",
|
|
279
|
+
"description": "v2.4.0+. User-defined routines saved via /multi-agent:save. Each entry indexes a local-only /multi-agent:<name> command dir under ~/.claude/commands/multi-agent/. Never synced (personal). Managed by routine-registry.mjs.",
|
|
280
|
+
"maxItems": 100,
|
|
281
|
+
"items": {
|
|
282
|
+
"type": "object",
|
|
283
|
+
"additionalProperties": false,
|
|
284
|
+
"required": ["name", "description"],
|
|
285
|
+
"properties": {
|
|
286
|
+
"name": {
|
|
287
|
+
"type": "string",
|
|
288
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$",
|
|
289
|
+
"maxLength": 40,
|
|
290
|
+
"description": "Routine command name; invocable as /multi-agent:<name>."
|
|
291
|
+
},
|
|
292
|
+
"description": {
|
|
293
|
+
"type": "string",
|
|
294
|
+
"description": "One-line summary shown by /multi-agent:routines and :help."
|
|
295
|
+
},
|
|
296
|
+
"createdAt": {
|
|
297
|
+
"type": "string",
|
|
298
|
+
"format": "date-time"
|
|
299
|
+
},
|
|
300
|
+
"source": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"description": "Where the routine came from, e.g. a CLAUDE.md section name or 'user'."
|
|
303
|
+
},
|
|
304
|
+
"trigger": {
|
|
305
|
+
"type": ["string", "null"],
|
|
306
|
+
"description": "Optional natural-language trigger phrase the routine captures."
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
},
|
|
276
311
|
"recentGroups": {
|
|
277
312
|
"type": "array",
|
|
278
313
|
"description": "v2.1.0+. LRU of multi-repo selection groups. Each group is a set of repo paths chosen together for multi-repo tasks. Max 10.",
|
|
@@ -1258,6 +1293,28 @@
|
|
|
1258
1293
|
"figmaConfigPath": {
|
|
1259
1294
|
"type": "string",
|
|
1260
1295
|
"description": "Path to per-project figma-config.json (for Figma pipeline projects)."
|
|
1296
|
+
},
|
|
1297
|
+
"standardsFile": {
|
|
1298
|
+
"type": "string",
|
|
1299
|
+
"description": "Filename (or absolute/tilde path) of the canonical home-dir Standards reference for this project, used by /multi-agent:analysis Q5 auto-detect (e.g. ~/<project>-Standards.md)."
|
|
1300
|
+
},
|
|
1301
|
+
"editableRelatedRepos": {
|
|
1302
|
+
"type": "array",
|
|
1303
|
+
"items": { "type": "string" },
|
|
1304
|
+
"maxItems": 20,
|
|
1305
|
+
"description": "Extra writable repos to offer in the dev-context picker beyond auto-detected submodules (iOS/Android/Backend)."
|
|
1306
|
+
},
|
|
1307
|
+
"frontendRepos": {
|
|
1308
|
+
"type": "array",
|
|
1309
|
+
"items": { "type": "string" },
|
|
1310
|
+
"maxItems": 20,
|
|
1311
|
+
"description": "Frontend repos for this project, used by /multi-agent:analysis Step 4 (Frontend is rarely in the iOS/Android submodule tree)."
|
|
1312
|
+
},
|
|
1313
|
+
"frontendRoots": {
|
|
1314
|
+
"type": "array",
|
|
1315
|
+
"items": { "type": "string" },
|
|
1316
|
+
"maxItems": 20,
|
|
1317
|
+
"description": "Whitelist source roots for the Frontend repo-evidence scan (overrides the default src/ app/ components/ features/ lib/)."
|
|
1261
1318
|
}
|
|
1262
1319
|
}
|
|
1263
1320
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
.claude/.pipeline-version 1
|
|
2
2
|
.claude/CLAUDE.md 1
|
|
3
3
|
.claude/agents 8
|
|
4
|
-
.claude/commands
|
|
5
|
-
.claude/lib
|
|
4
|
+
.claude/commands 47
|
|
5
|
+
.claude/lib 27
|
|
6
6
|
.claude/multi-agent-preferences.json 1
|
|
7
7
|
.claude/multi-agent-refs 51
|
|
8
8
|
.claude/rules 12
|
|
9
|
-
.claude/schemas
|
|
10
|
-
.claude/scripts
|
|
9
|
+
.claude/schemas 24
|
|
10
|
+
.claude/scripts 197
|
|
11
11
|
.claude/settings.json 1
|
|
12
12
|
.claude/skills 428
|
|
13
13
|
.copilot/.pipeline-version 1
|
|
14
14
|
.copilot/agents 8
|
|
15
15
|
.copilot/copilot-instructions.md 1
|
|
16
|
-
.copilot/lib
|
|
17
|
-
.copilot/schemas
|
|
18
|
-
.copilot/scripts
|
|
19
|
-
.copilot/skills
|
|
16
|
+
.copilot/lib 27
|
|
17
|
+
.copilot/schemas 24
|
|
18
|
+
.copilot/scripts 197
|
|
19
|
+
.copilot/skills 470
|
|
@@ -18,7 +18,7 @@ const DEFAULT_FILE = path.join(os.homedir(), ".claude", "multi-agent-preferences
|
|
|
18
18
|
|
|
19
19
|
// Single source of truth for the migration target version. Referenced by the
|
|
20
20
|
// migrate() bump chain AND every user-facing message, so they cannot drift.
|
|
21
|
-
const TARGET_VERSION = "2.
|
|
21
|
+
const TARGET_VERSION = "2.4.0";
|
|
22
22
|
|
|
23
23
|
const USAGE = "Usage: migrate-prefs.mjs [--dry-run] [--file <path>]";
|
|
24
24
|
|
|
@@ -164,6 +164,9 @@ function migrate(prefs) {
|
|
|
164
164
|
const TARGET = TARGET_VERSION;
|
|
165
165
|
if (out.schemaVersion === TARGET) {
|
|
166
166
|
// already on target - skip version bump, but still run sub-migrations below
|
|
167
|
+
} else if (out.schemaVersion === "2.3.0") {
|
|
168
|
+
out.schemaVersion = TARGET;
|
|
169
|
+
changes.push(`schemaVersion: 2.3.0 → ${TARGET}`);
|
|
167
170
|
} else if (out.schemaVersion === "2.2.0") {
|
|
168
171
|
out.schemaVersion = TARGET;
|
|
169
172
|
changes.push(`schemaVersion: 2.2.0 → ${TARGET}`);
|
|
@@ -241,6 +244,10 @@ function migrate(prefs) {
|
|
|
241
244
|
out.global.accounts = [];
|
|
242
245
|
changes.push("added empty accounts (v7.4.0 per-account host overrides)");
|
|
243
246
|
}
|
|
247
|
+
if (!Array.isArray(out.global.routines)) {
|
|
248
|
+
out.global.routines = [];
|
|
249
|
+
changes.push("added empty routines (v2.4.0 user-defined /multi-agent:save routines)");
|
|
250
|
+
}
|
|
244
251
|
if (!out.global.serviceStatus) {
|
|
245
252
|
out.global.serviceStatus = {};
|
|
246
253
|
changes.push("added empty serviceStatus");
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// routine-registry.mjs - backend for the user-defined routine commands
|
|
3
|
+
// (/multi-agent:save, :routines, :forget).
|
|
4
|
+
//
|
|
5
|
+
// A "routine" is a saved, reusable procedure the user binds to its own
|
|
6
|
+
// /multi-agent:<name> slash command. Each routine is TWO artifacts:
|
|
7
|
+
// 1. a local-only command dir ~/.claude/commands/multi-agent/<name>/SKILL.md
|
|
8
|
+
// (frontmatter `local-only: true` -> the installer preserves it across
|
|
9
|
+
// updates and /multi-agent:sync refuses to leak it into the public repo)
|
|
10
|
+
// 2. a registry entry in ~/.claude/multi-agent-preferences.json
|
|
11
|
+
// at global.routines[] (name, description, createdAt, source)
|
|
12
|
+
//
|
|
13
|
+
// This tool is the mechanical half; the save/routines/forget SKILLs drive the
|
|
14
|
+
// UX (candidate discovery, pickers, confirmation) and call these subcommands.
|
|
15
|
+
//
|
|
16
|
+
// Zero deps. Never writes under a repo `pipeline/` tree; only ever under
|
|
17
|
+
// <home>/.claude/. Refuses to touch a shipped (non-local-only) command dir.
|
|
18
|
+
//
|
|
19
|
+
// Usage:
|
|
20
|
+
// node routine-registry.mjs list [--json] [--home <dir>]
|
|
21
|
+
// node routine-registry.mjs get --name <n> [--home <dir>]
|
|
22
|
+
// node routine-registry.mjs add --name <n> --description <d>
|
|
23
|
+
// [--description-tr <t>] [--argument-hint <h>] --source <s> --body-file <f> [--home <dir>]
|
|
24
|
+
// node routine-registry.mjs remove --name <n> [--home <dir>]
|
|
25
|
+
//
|
|
26
|
+
// Exit: 0 ok, 1 error (validation / collision / not-found), 64 usage.
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
existsSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
writeFileSync,
|
|
32
|
+
mkdirSync,
|
|
33
|
+
rmSync,
|
|
34
|
+
renameSync,
|
|
35
|
+
} from "node:fs";
|
|
36
|
+
import { join, dirname } from "node:path";
|
|
37
|
+
|
|
38
|
+
const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
39
|
+
const MAX_NAME = 40;
|
|
40
|
+
const MAX_ROUTINES = 100;
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const out = { _: [] };
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const a = argv[i];
|
|
46
|
+
if (a.startsWith("--")) {
|
|
47
|
+
const key = a.slice(2);
|
|
48
|
+
const next = argv[i + 1];
|
|
49
|
+
if (next === undefined || next.startsWith("--")) {
|
|
50
|
+
out[key] = true;
|
|
51
|
+
} else {
|
|
52
|
+
out[key] = next;
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
out._.push(a);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fail(msg) {
|
|
63
|
+
console.error(`routine-registry: ${msg}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function homeDir(args) {
|
|
68
|
+
return args.home || process.env.HOME || process.env.USERPROFILE || "";
|
|
69
|
+
}
|
|
70
|
+
function prefsPath(home) {
|
|
71
|
+
return join(home, ".claude", "multi-agent-preferences.json");
|
|
72
|
+
}
|
|
73
|
+
function routineDir(home, name) {
|
|
74
|
+
return join(home, ".claude", "commands", "multi-agent", name);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readPrefs(home) {
|
|
78
|
+
const p = prefsPath(home);
|
|
79
|
+
if (!existsSync(p)) return { global: {} };
|
|
80
|
+
try {
|
|
81
|
+
const prefs = JSON.parse(readFileSync(p, "utf-8"));
|
|
82
|
+
if (!prefs.global) prefs.global = {};
|
|
83
|
+
return prefs;
|
|
84
|
+
} catch {
|
|
85
|
+
fail(`could not parse ${p}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function writePrefs(home, prefs) {
|
|
90
|
+
const p = prefsPath(home);
|
|
91
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
92
|
+
const tmp = `${p}.tmp.${process.pid}`;
|
|
93
|
+
writeFileSync(tmp, `${JSON.stringify(prefs, null, 2)}\n`);
|
|
94
|
+
renameSync(tmp, p);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getRoutines(prefs) {
|
|
98
|
+
return Array.isArray(prefs.global.routines) ? prefs.global.routines : [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A dir is a shipped (non-routine) command when it exists and its SKILL.md is
|
|
102
|
+
// NOT marked local-only. Those must never be overwritten or removed.
|
|
103
|
+
function isLocalOnlyDir(dir) {
|
|
104
|
+
const skill = join(dir, "SKILL.md");
|
|
105
|
+
if (!existsSync(skill)) return false;
|
|
106
|
+
return /^local-only:\s*true\s*$/m.test(readFileSync(skill, "utf-8"));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function cmdList(home, json) {
|
|
110
|
+
const routines = getRoutines(readPrefs(home));
|
|
111
|
+
if (json) {
|
|
112
|
+
process.stdout.write(`${JSON.stringify(routines, null, 2)}\n`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (routines.length === 0) {
|
|
116
|
+
console.log("(no routines saved)");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const r of routines) {
|
|
120
|
+
console.log(`${r.name}\t${r.description || ""}\t${r.createdAt || ""}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cmdGet(home, name) {
|
|
125
|
+
const r = getRoutines(readPrefs(home)).find((x) => x.name === name);
|
|
126
|
+
if (!r) fail(`no routine named "${name}"`);
|
|
127
|
+
process.stdout.write(`${JSON.stringify(r, null, 2)}\n`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildSkill(args) {
|
|
131
|
+
const desc = args.description;
|
|
132
|
+
const descTr = args["description-tr"];
|
|
133
|
+
const hint = args["argument-hint"] || "[optional args]";
|
|
134
|
+
const body = readFileSync(args["body-file"], "utf-8").trimEnd();
|
|
135
|
+
const fm = [
|
|
136
|
+
"---",
|
|
137
|
+
`description: ${JSON.stringify(desc)}`,
|
|
138
|
+
descTr ? `description-tr: ${JSON.stringify(descTr)}` : null,
|
|
139
|
+
`argument-hint: ${JSON.stringify(hint)}`,
|
|
140
|
+
"allowed-tools: Skill, Bash, Read, Edit, Write, AskUserQuestion",
|
|
141
|
+
"local-only: true",
|
|
142
|
+
"---",
|
|
143
|
+
].filter(Boolean);
|
|
144
|
+
return `${fm.join("\n")}\n\n# multi-agent ${args.name} - saved routine\n\n**Input**: $ARGUMENTS\n\n${body}\n`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function cmdAdd(home, args) {
|
|
148
|
+
const name = args.name;
|
|
149
|
+
if (!name || !NAME_RE.test(name) || name.length > MAX_NAME) {
|
|
150
|
+
fail(`invalid --name (need ${NAME_RE} , <= ${MAX_NAME} chars): ${name}`);
|
|
151
|
+
}
|
|
152
|
+
if (!args.description) fail("--description is required");
|
|
153
|
+
if (!args["body-file"] || !existsSync(args["body-file"])) {
|
|
154
|
+
fail("--body-file is required and must exist");
|
|
155
|
+
}
|
|
156
|
+
const dir = routineDir(home, name);
|
|
157
|
+
// Collision: an existing non-local-only dir is a shipped command - never touch.
|
|
158
|
+
if (existsSync(dir) && !isLocalOnlyDir(dir)) {
|
|
159
|
+
fail(`"${name}" collides with a shipped command; pick another name`);
|
|
160
|
+
}
|
|
161
|
+
const prefs = readPrefs(home);
|
|
162
|
+
const routines = getRoutines(prefs);
|
|
163
|
+
if (routines.some((r) => r.name === name)) {
|
|
164
|
+
fail(`a routine named "${name}" already exists; forget it first or pick another name`);
|
|
165
|
+
}
|
|
166
|
+
if (routines.length >= MAX_ROUTINES) fail(`routine cap (${MAX_ROUTINES}) reached`);
|
|
167
|
+
|
|
168
|
+
mkdirSync(dir, { recursive: true });
|
|
169
|
+
const skill = buildSkill(args);
|
|
170
|
+
// Safety: the generated file MUST be local-only so it never syncs to the repo.
|
|
171
|
+
if (!/^local-only:\s*true\s*$/m.test(skill)) fail("internal: generated SKILL missing local-only");
|
|
172
|
+
writeFileSync(join(dir, "SKILL.md"), skill);
|
|
173
|
+
|
|
174
|
+
routines.push({
|
|
175
|
+
name,
|
|
176
|
+
description: args.description,
|
|
177
|
+
createdAt: new Date().toISOString(),
|
|
178
|
+
source: args.source || "user",
|
|
179
|
+
});
|
|
180
|
+
prefs.global.routines = routines;
|
|
181
|
+
writePrefs(home, prefs);
|
|
182
|
+
console.log(`routine-registry: saved "${name}" -> ${join(dir, "SKILL.md")}`);
|
|
183
|
+
console.log("routine-registry: available as /multi-agent:" + name + " after a session reload");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function cmdRemove(home, name) {
|
|
187
|
+
if (!name) fail("--name is required");
|
|
188
|
+
const prefs = readPrefs(home);
|
|
189
|
+
const routines = getRoutines(prefs);
|
|
190
|
+
const idx = routines.findIndex((r) => r.name === name);
|
|
191
|
+
const dir = routineDir(home, name);
|
|
192
|
+
// Guard: only remove a dir that is BOTH registered AND local-only.
|
|
193
|
+
if (existsSync(dir) && !isLocalOnlyDir(dir)) {
|
|
194
|
+
fail(`"${name}" is a shipped command, not a routine; refusing to remove`);
|
|
195
|
+
}
|
|
196
|
+
if (idx < 0 && !existsSync(dir)) fail(`no routine named "${name}"`);
|
|
197
|
+
if (existsSync(dir) && isLocalOnlyDir(dir)) rmSync(dir, { recursive: true, force: true });
|
|
198
|
+
if (idx >= 0) {
|
|
199
|
+
routines.splice(idx, 1);
|
|
200
|
+
prefs.global.routines = routines;
|
|
201
|
+
writePrefs(home, prefs);
|
|
202
|
+
}
|
|
203
|
+
console.log(`routine-registry: removed "${name}" (clears from autocomplete on next session reload)`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function main() {
|
|
207
|
+
const [, , sub, ...rest] = process.argv;
|
|
208
|
+
const args = parseArgs(rest);
|
|
209
|
+
const home = homeDir(args);
|
|
210
|
+
if (!home) fail("cannot resolve home dir");
|
|
211
|
+
switch (sub) {
|
|
212
|
+
case "list":
|
|
213
|
+
return cmdList(home, Boolean(args.json));
|
|
214
|
+
case "get":
|
|
215
|
+
return cmdGet(home, args.name);
|
|
216
|
+
case "add":
|
|
217
|
+
return cmdAdd(home, args);
|
|
218
|
+
case "remove":
|
|
219
|
+
return cmdRemove(home, args.name);
|
|
220
|
+
default:
|
|
221
|
+
console.error("usage: routine-registry.mjs <list|get|add|remove> [flags] (see header)");
|
|
222
|
+
process.exit(64);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
main();
|
|
@@ -354,6 +354,46 @@ assert_in_file "^-i" "$WORK/security-argv.log" "security invoked in -i stdin mod
|
|
|
354
354
|
assert_not_in_file "$SECRET_VALUE" "$WORK/security-argv.log" "secret absent from security argv"
|
|
355
355
|
assert_in_file "$SECRET_VALUE" "$WORK/security-stdin.log" "secret delivered via stdin"
|
|
356
356
|
|
|
357
|
+
# -----------------------------------------------------------------------------
|
|
358
|
+
note "7b. fetch-figma-annotations.sh (Tier-2 annotation ingestion)"
|
|
359
|
+
# -----------------------------------------------------------------------------
|
|
360
|
+
expect_rc 1 "no args exits 1 (usage)" \
|
|
361
|
+
"$SH" "$LIB/fetch-figma-annotations.sh"
|
|
362
|
+
|
|
363
|
+
# Missing token -> blocked JSON + exit 2 (no network).
|
|
364
|
+
expect_rc 2 "missing token exits 2 with blocked JSON" \
|
|
365
|
+
env HOME="$FAKE_HOME" USER=smoke \
|
|
366
|
+
"$SH" "$LIB/fetch-figma-annotations.sh" --file-key ABC --node-id "1:23"
|
|
367
|
+
assert_in_file "missing-token" "$OUT" "blocked JSON names missing-token"
|
|
368
|
+
|
|
369
|
+
# Fake curl returns a /nodes doc carrying a TR:/EN: annotation; with a stub
|
|
370
|
+
# token the parser must reach + parse it (regression guard for the parser).
|
|
371
|
+
BIN_CURL_ANNOT="$WORK/bin-curl-annot"
|
|
372
|
+
mkdir -p "$BIN_CURL_ANNOT"
|
|
373
|
+
cat > "$BIN_CURL_ANNOT/curl" <<'FAKE'
|
|
374
|
+
#!/bin/bash
|
|
375
|
+
out=""; prev=""
|
|
376
|
+
for a in "$@"; do
|
|
377
|
+
[ "$prev" = "-o" ] && out="$a"
|
|
378
|
+
prev="$a"
|
|
379
|
+
done
|
|
380
|
+
if [ -n "$out" ]; then
|
|
381
|
+
cat > "$out" <<'JSON'
|
|
382
|
+
{"nodes":{"1:23":{"document":{"id":"1:23","name":"Title","type":"TEXT","characters":"Giriniz","annotations":[{"label":"TR: Ucusunuzu secin\nEN: Select your flight"}]}}}}
|
|
383
|
+
JSON
|
|
384
|
+
fi
|
|
385
|
+
printf '200'
|
|
386
|
+
exit 0
|
|
387
|
+
FAKE
|
|
388
|
+
chmod +x "$BIN_CURL_ANNOT/curl"
|
|
389
|
+
|
|
390
|
+
expect_rc 0 "stub token + annotation doc parses (exit 0)" \
|
|
391
|
+
env HOME="$FAKE_HOME" USER=smoke FIGMA_PAT=fake-token \
|
|
392
|
+
PATH="$BIN_CURL_ANNOT:$PATH" \
|
|
393
|
+
"$SH" "$LIB/fetch-figma-annotations.sh" --file-key ABC --node-id "1:23" --lang-prefixes TR,EN
|
|
394
|
+
assert_in_file "\"mode\": \"prefixed\"" "$OUT" "annotation parsed as prefixed"
|
|
395
|
+
assert_in_file "Select your flight" "$OUT" "EN copy extracted from annotation"
|
|
396
|
+
|
|
357
397
|
# -----------------------------------------------------------------------------
|
|
358
398
|
note "8. static source checks (bash-3.2 compat + no secrets on argv)"
|
|
359
399
|
# -----------------------------------------------------------------------------
|
|
@@ -363,6 +403,7 @@ OWNED_FILES=(
|
|
|
363
403
|
"$LIB/fetch-swagger.sh"
|
|
364
404
|
"$LIB/fetch-fortify.sh"
|
|
365
405
|
"$LIB/fetch-graylog.sh"
|
|
406
|
+
"$LIB/fetch-figma-annotations.sh"
|
|
366
407
|
"$LIB/figma-mcp-refresh.sh"
|
|
367
408
|
"$LIB/figma-screenshot.sh"
|
|
368
409
|
"$LIB/credential-store.sh"
|
|
@@ -91,11 +91,11 @@ echo ""
|
|
|
91
91
|
echo "→ 8. registration + count consistency (36)"
|
|
92
92
|
grep -Fq '| `create-jira ' "$DISPATCHER" && pass "dispatcher routing: create-jira" || fail "dispatcher routing missing: generate"
|
|
93
93
|
grep -Fq "create-jira/SKILL.md" "$DISPATCHER" && pass "dispatcher loading row" || fail "dispatcher loading row missing"
|
|
94
|
-
grep -Fq "## 1. Command Inventory (
|
|
94
|
+
grep -Fq "## 1. Command Inventory (41 commands)" "$CONTRACT" && pass "contract count = 41" || fail "contract count wrong"
|
|
95
95
|
grep -Eq '(^|[ ,])create-jira([ ,]|$)' "$CONTRACT" && pass "contract list has create-jira" || fail "contract list missing generate"
|
|
96
|
-
grep -Fq "**
|
|
96
|
+
grep -Fq "**41 commands are synced**" "$SYNC" && pass "sync count = 41" || fail "sync count wrong"
|
|
97
97
|
SYNC_SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-sync/SKILL.md"
|
|
98
|
-
grep -Fq "**
|
|
98
|
+
grep -Fq "**41 commands are synced**" "$SYNC_SKILL" && pass "sync SKILL count = 41" || fail "sync SKILL count wrong (Copilot parity)"
|
|
99
99
|
grep -Fq "create-jira" "$ROOT/pipeline/skills/shared/core/multi-agent-help/SKILL.md" && pass "help SKILL lists create-jira" || fail "help SKILL missing create-jira (Copilot parity)"
|
|
100
100
|
grep -c "multi-agent:create-jira" "$HELP" | awk '{exit !($1>=2)}' && pass "help.md has EN + TR entries" || fail "help.md entries incomplete"
|
|
101
101
|
# no stale split-command references anywhere in the synced surfaces
|