@mmerterden/multi-agent-pipeline 14.2.1 → 15.0.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.
Files changed (184) hide show
  1. package/CHANGELOG.md +118 -6
  2. package/README.md +20 -9
  3. package/README.tr.md +150 -0
  4. package/docs/FIGMA_PIPELINE.md +3 -3
  5. package/docs/adr/0006-skills-core-external-split.md +1 -1
  6. package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
  7. package/docs/adr/README.md +1 -0
  8. package/docs/architecture.md +24 -9
  9. package/docs/ecosystem.md +237 -0
  10. package/docs/features.md +5 -5
  11. package/index.js +2 -0
  12. package/install/_codex-agents.mjs +11 -2
  13. package/install/_common.mjs +65 -1
  14. package/install/_dev-only-files.mjs +0 -1
  15. package/install/_platform-filter.mjs +73 -7
  16. package/install/_plugin-skills.mjs +33 -8
  17. package/install/claude.mjs +144 -59
  18. package/install/codex.mjs +37 -7
  19. package/install/copilot.mjs +36 -11
  20. package/install/index.mjs +6 -2
  21. package/install/templates/codex-instructions.md +1 -1
  22. package/install/templates/copilot-instructions.md +15 -12
  23. package/package.json +1 -2
  24. package/pipeline/commands/multi-agent/SKILL.md +4 -2
  25. package/pipeline/commands/multi-agent/analysis/SKILL.md +5 -5
  26. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  27. package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
  28. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
  29. package/pipeline/commands/multi-agent/channels/SKILL.md +16 -5
  30. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
  31. package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -4
  32. package/pipeline/commands/multi-agent/dev/SKILL.md +10 -23
  33. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +10 -2
  34. package/pipeline/commands/multi-agent/dev-local/SKILL.md +10 -24
  35. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +10 -3
  36. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  37. package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
  38. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
  39. package/pipeline/commands/multi-agent/jira/SKILL.md +13 -2
  40. package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
  41. package/pipeline/commands/multi-agent/local/SKILL.md +6 -2
  42. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +6 -2
  43. package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
  44. package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
  45. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
  46. package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +13 -9
  47. package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
  48. package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
  49. package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
  50. package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
  51. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
  52. package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
  53. package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
  54. package/pipeline/lib/extract-conventions.sh +44 -15
  55. package/pipeline/lib/fetch-figma-annotations.sh +8 -1
  56. package/pipeline/lib/fetch-fortify.sh +23 -8
  57. package/pipeline/lib/figma-screenshot.sh +11 -1
  58. package/pipeline/lib/issue-fetcher.sh +77 -10
  59. package/pipeline/lib/md2confluence-v3.py +16 -2
  60. package/pipeline/lib/parse-complaints.sh +306 -0
  61. package/pipeline/lib/plan-todos.sh +5 -2
  62. package/pipeline/lib/post-pr-review.sh +8 -6
  63. package/pipeline/lib/shadow-git.sh +50 -9
  64. package/pipeline/lib/submodule-detector.sh +8 -1
  65. package/pipeline/multi-agent-refs/_input-parser.md +1 -1
  66. package/pipeline/multi-agent-refs/channels/confluence.md +3 -0
  67. package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
  68. package/pipeline/multi-agent-refs/channels/jira.md +13 -2
  69. package/pipeline/multi-agent-refs/channels/pr-review-actions.md +1 -1
  70. package/pipeline/multi-agent-refs/channels/pr.md +20 -0
  71. package/pipeline/multi-agent-refs/channels/wiki.md +4 -4
  72. package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
  73. package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
  74. package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
  75. package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
  76. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
  77. package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
  78. package/pipeline/multi-agent-refs/generate-issue.md +3 -3
  79. package/pipeline/multi-agent-refs/issue-jira-triad.md +3 -3
  80. package/pipeline/multi-agent-refs/payload-contracts.md +67 -0
  81. package/pipeline/multi-agent-refs/phases/modes.md +20 -0
  82. package/pipeline/multi-agent-refs/phases/phase-0-init.md +2 -2
  83. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
  84. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
  85. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
  86. package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
  87. package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
  88. package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -40
  89. package/pipeline/multi-agent-refs/phases/phase-7-report.md +5 -3
  90. package/pipeline/multi-agent-refs/phases.md +6 -0
  91. package/pipeline/multi-agent-refs/rules.md +2 -0
  92. package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
  93. package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
  94. package/pipeline/preferences-template.json +13 -5
  95. package/pipeline/rules/figma-pipeline.md +2 -2
  96. package/pipeline/schemas/agent-state.schema.json +1 -1
  97. package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
  98. package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
  99. package/pipeline/schemas/prefs.schema.json +277 -67
  100. package/pipeline/schemas/token-budget.json +2 -2
  101. package/pipeline/scripts/_stack-routing.mjs +79 -0
  102. package/pipeline/scripts/audit-log-rotate.sh +14 -1
  103. package/pipeline/scripts/build-skills-index.mjs +11 -0
  104. package/pipeline/scripts/build-stack-plugins.mjs +35 -60
  105. package/pipeline/scripts/check-derived-drift.mjs +65 -29
  106. package/pipeline/scripts/diff-explain.mjs +41 -3
  107. package/pipeline/scripts/diff-risk-score.mjs +72 -8
  108. package/pipeline/scripts/gc-worktrees.sh +4 -1
  109. package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
  110. package/pipeline/scripts/gen-skills-index.mjs +1 -1
  111. package/pipeline/scripts/learning-curve.mjs +8 -2
  112. package/pipeline/scripts/match-skills.mjs +8 -2
  113. package/pipeline/scripts/migrate-prefs.mjs +28 -20
  114. package/pipeline/scripts/output-quality-check.sh +15 -4
  115. package/pipeline/scripts/phase-tracker.sh +33 -12
  116. package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
  117. package/pipeline/scripts/pre-commit-check.sh +69 -22
  118. package/pipeline/scripts/render-agent-log-cost.sh +8 -3
  119. package/pipeline/scripts/render-cost-summary.sh +42 -22
  120. package/pipeline/scripts/render-work-summary.sh +47 -13
  121. package/pipeline/scripts/review-scope.mjs +1 -1
  122. package/pipeline/scripts/run-aggregator.mjs +43 -14
  123. package/pipeline/scripts/scan-agent-config.sh +1 -1
  124. package/pipeline/scripts/skill-conformance.mjs +165 -30
  125. package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
  126. package/pipeline/scripts/smoke-schema-validation.sh +5 -1
  127. package/pipeline/scripts/test-gap-rules/android.json +25 -0
  128. package/pipeline/scripts/test-gap-rules/ios.json +34 -0
  129. package/pipeline/scripts/test-gap-rules/node.json +29 -0
  130. package/pipeline/scripts/test-gap-rules/python.json +25 -0
  131. package/pipeline/scripts/test-gap-scan.mjs +45 -6
  132. package/pipeline/scripts/uninstall.mjs +196 -14
  133. package/pipeline/scripts/update-issue-progress.sh +12 -16
  134. package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
  135. package/pipeline/scripts/validate-reviewer.mjs +9 -3
  136. package/pipeline/scripts/worktree-finalize.sh +23 -2
  137. package/pipeline/skills/.skill-manifest.json +156 -108
  138. package/pipeline/skills/.skills-index.json +459 -13
  139. package/pipeline/skills/shared/README.md +15 -11
  140. package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
  141. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -0
  142. package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
  143. package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
  144. package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
  145. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +4 -17
  146. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +8 -0
  147. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +5 -18
  148. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +8 -0
  149. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
  150. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
  151. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +4 -0
  152. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -0
  153. package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
  154. package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +10 -6
  155. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -22
  156. package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
  157. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
  158. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
  159. package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
  160. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
  161. package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
  162. package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
  163. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
  164. package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
  165. package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
  166. package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
  167. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
  168. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +187 -0
  169. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +156 -0
  170. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +108 -0
  171. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +176 -0
  172. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +865 -0
  173. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +335 -0
  174. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +344 -0
  175. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +130 -0
  176. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +264 -0
  177. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +298 -0
  178. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +529 -0
  179. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +187 -0
  180. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +171 -0
  181. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +184 -0
  182. package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +26 -0
  183. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +173 -0
  184. package/pipeline/skills/skills-index.md +9 -5
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "stack": "android",
4
+ "description": "Android / Jetpack Compose / Kotlin test gap rules. Sources in *.kt; tests under app/src/test/ or app/src/androidTest/.",
5
+ "sourceExtensions": [".kt", ".java"],
6
+ "testPathSuffixes": ["Test.kt", "Tests.kt", "Test.java"],
7
+ "testPathContains": ["/src/test/", "/src/androidTest/", "/test/", "/tests/"],
8
+ "excludePathGlobs": ["build/", ".gradle/", "generated/", "**/BuildConfig.java", "**/R.java"],
9
+ "publicApiPatterns": [
10
+ { "id": "open_class", "regex": "\\bopen\\s+class\\s+([A-Z][A-Za-z0-9_]*)" },
11
+ { "id": "open_fun", "regex": "\\bopen\\s+fun\\s+([a-z_][A-Za-z0-9_]*)" },
12
+ { "id": "public_fun", "regex": "^\\s*(?:public\\s+)?fun\\s+([a-z_][A-Za-z0-9_]*)" },
13
+ { "id": "public_class", "regex": "^\\s*(?:public\\s+)?class\\s+([A-Z][A-Za-z0-9_]*)" },
14
+ { "id": "jvmstatic", "regex": "@JvmStatic\\b" },
15
+ { "id": "interface", "regex": "^\\s*(?:public\\s+)?interface\\s+([A-Z][A-Za-z0-9_]*)" }
16
+ ],
17
+ "composablePatterns": [
18
+ {
19
+ "id": "composable_fun",
20
+ "regex": "@Composable\\s+(?:public\\s+|internal\\s+)?fun\\s+([A-Z][A-Za-z0-9_]*)"
21
+ }
22
+ ],
23
+ "expectedTestKindsForComposable": ["compose-ui-test"],
24
+ "hint": "Cover @Composable functions with createComposeRule() + onNodeWithText/Tag/ContentDescription assertions; ViewModels with runTest + Turbine."
25
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "stack": "ios",
4
+ "description": "iOS / SwiftUI / UIKit test gap rules. Source files in *.swift; tests in *Tests.swift. Snapshot/ViewInspector tests are also recognised when the source is a SwiftUI View.",
5
+ "sourceExtensions": [".swift"],
6
+ "testPathSuffixes": ["Tests.swift", "Test.swift"],
7
+ "testPathContains": ["/Tests/", "/UnitTests/", "/SnapshotTests/"],
8
+ "excludePathGlobs": [
9
+ "Pods/",
10
+ "Carthage/",
11
+ ".build/",
12
+ "DerivedData/",
13
+ "Generated/",
14
+ "Sourcery/",
15
+ "_Generated.swift",
16
+ "Tests/Fixtures/"
17
+ ],
18
+ "publicApiPatterns": [
19
+ { "id": "public_func", "regex": "\\bpublic\\s+func\\s+([A-Za-z_][A-Za-z0-9_]*)" },
20
+ { "id": "public_class", "regex": "\\bpublic\\s+(?:final\\s+)?class\\s+([A-Z][A-Za-z0-9_]*)" },
21
+ { "id": "public_struct", "regex": "\\bpublic\\s+struct\\s+([A-Z][A-Za-z0-9_]*)" },
22
+ { "id": "public_enum", "regex": "\\bpublic\\s+enum\\s+([A-Z][A-Za-z0-9_]*)" },
23
+ { "id": "public_proto", "regex": "\\bpublic\\s+protocol\\s+([A-Z][A-Za-z0-9_]*)" },
24
+ { "id": "public_init", "regex": "\\bpublic\\s+init\\b" },
25
+ { "id": "objc_export", "regex": "@objc(?:Members)?\\s+(?:public\\s+)?(?:func|class|var)" }
26
+ ],
27
+ "swiftUiViewPatterns": [
28
+ { "id": "view_struct", "regex": "struct\\s+([A-Z][A-Za-z0-9_]*)\\s*:\\s*View\\b" },
29
+ { "id": "config_struct", "regex": "struct\\s+([A-Z][A-Za-z0-9_]*Configuration)\\b" }
30
+ ],
31
+ "expectedTestKindsForView": ["snapshot", "viewinspector", "unit"],
32
+ "snapshotPathHints": ["SnapshotTests", "__Snapshots__"],
33
+ "hint": "Cover with a 3-layer test (structural ViewInspector + snapshot + unit) per swiftui-qa.md."
34
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "stack": "node",
4
+ "description": "Node.js / TypeScript test gap rules. Sources in *.ts/*.tsx/*.js/*.mjs; tests in *.test.* / *.spec.* / __tests__/.",
5
+ "sourceExtensions": [".ts", ".tsx", ".js", ".mjs", ".cjs"],
6
+ "testPathSuffixes": [
7
+ ".test.ts",
8
+ ".test.tsx",
9
+ ".test.js",
10
+ ".test.mjs",
11
+ ".spec.ts",
12
+ ".spec.tsx",
13
+ ".spec.js",
14
+ ".spec.mjs"
15
+ ],
16
+ "testPathContains": ["/__tests__/", "/test/", "/tests/"],
17
+ "excludePathGlobs": ["node_modules/", "dist/", "build/", ".next/", "coverage/", "*.d.ts"],
18
+ "publicApiPatterns": [
19
+ {
20
+ "id": "named_export",
21
+ "regex": "^export\\s+(?:async\\s+)?(?:function|class|const|let|interface|type|enum)\\s+([A-Za-z_$][A-Za-z0-9_$]*)"
22
+ },
23
+ {
24
+ "id": "default_export",
25
+ "regex": "^export\\s+default\\s+(?:async\\s+)?(?:function|class)\\s+([A-Za-z_$][A-Za-z0-9_$]*)"
26
+ }
27
+ ],
28
+ "hint": "Cover named exports with vitest/jest test('<name>', ...). React components: React Testing Library (render + screen + userEvent)."
29
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "stack": "python",
4
+ "description": "Python test gap rules. Sources in *.py; tests in test_*.py / *_test.py / tests/.",
5
+ "sourceExtensions": [".py"],
6
+ "testPathSuffixes": ["_test.py"],
7
+ "testPathPrefixes": ["test_"],
8
+ "testPathContains": ["/tests/", "/test/"],
9
+ "excludePathGlobs": [
10
+ "__pycache__/",
11
+ ".venv/",
12
+ "venv/",
13
+ ".tox/",
14
+ "build/",
15
+ "dist/",
16
+ "*.egg-info/",
17
+ "**/migrations/**"
18
+ ],
19
+ "publicApiPatterns": [
20
+ { "id": "top_def", "regex": "^def\\s+([a-z_][a-z0-9_]*)" },
21
+ { "id": "top_class", "regex": "^class\\s+([A-Z][A-Za-z0-9_]*)" },
22
+ { "id": "async_def", "regex": "^async\\s+def\\s+([a-z_][a-z0-9_]*)" }
23
+ ],
24
+ "hint": "Cover with pytest test_<symbol> style. Async with pytest-asyncio. Avoid name collision with private helpers (prefix _)."
25
+ }
@@ -141,6 +141,45 @@ function isExcluded(path) {
141
141
  return false;
142
142
  }
143
143
 
144
+ // git quotes a `diff --git` path (core.quotePath, on by default) whenever it
145
+ // has a non-ASCII byte or other unusual character - e.g. a Turkish filename
146
+ // renders as `diff --git "a/\303\226deme.swift" "b/\303\226deme.swift"`, not
147
+ // the bare a/...b/... form. The plain regex this replaced never matched that
148
+ // line at all, so every non-ASCII-named file's added lines were silently
149
+ // dropped from the test-gap scan.
150
+ const DIFF_GIT_QUOTED_RE = /^diff --git "a\/((?:[^"\\]|\\.)*)" "b\/((?:[^"\\]|\\.)*)"$/;
151
+ const DIFF_GIT_UNQUOTED_RE = /^diff --git a\/(.+?) b\/(.+)$/;
152
+
153
+ function unquoteGitPath(s) {
154
+ const bytes = [];
155
+ for (let i = 0; i < s.length; i++) {
156
+ if (s[i] === "\\") {
157
+ const octal = s.slice(i + 1, i + 4);
158
+ if (/^[0-7]{3}$/.test(octal)) {
159
+ bytes.push(parseInt(octal, 8));
160
+ i += 3;
161
+ continue;
162
+ }
163
+ const simple = { "\\": 92, '"': 34, t: 9, n: 10, r: 13 };
164
+ const next = s[i + 1];
165
+ if (next in simple) {
166
+ bytes.push(simple[next]);
167
+ i += 1;
168
+ continue;
169
+ }
170
+ }
171
+ bytes.push(s.charCodeAt(i));
172
+ }
173
+ return Buffer.from(bytes).toString("utf-8");
174
+ }
175
+
176
+ function matchDiffGitHeader(line) {
177
+ const q = line.match(DIFF_GIT_QUOTED_RE);
178
+ if (q) return unquoteGitPath(q[2]);
179
+ const u = line.match(DIFF_GIT_UNQUOTED_RE);
180
+ return u ? u[2] : null;
181
+ }
182
+
144
183
  function isTestPath(path) {
145
184
  if (Array.isArray(rules.testPathSuffixes) && rules.testPathSuffixes.some((s) => path.endsWith(s)))
146
185
  return true;
@@ -190,9 +229,9 @@ function parseDiffAddedLines(diffText) {
190
229
  const perFile = new Map();
191
230
  let cur = null;
192
231
  for (const line of diffText.split("\n")) {
193
- const m = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
194
- if (m) {
195
- cur = m[2];
232
+ const p = matchDiffGitHeader(line);
233
+ if (p !== null) {
234
+ cur = p;
196
235
  if (!perFile.has(cur)) perFile.set(cur, { lines: [], isNew: false });
197
236
  continue;
198
237
  }
@@ -300,9 +339,9 @@ function symbolMentionedInTest(diffText, symbol) {
300
339
  const lines = diffText.split("\n");
301
340
  let inTestFile = false;
302
341
  for (const line of lines) {
303
- const fm = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
304
- if (fm) {
305
- inTestFile = isTestPath(fm[2]);
342
+ const fm = matchDiffGitHeader(line);
343
+ if (fm !== null) {
344
+ inTestFile = isTestPath(fm);
306
345
  continue;
307
346
  }
308
347
  if (!inTestFile) continue;
@@ -35,9 +35,20 @@
35
35
  * @module pipeline/scripts/runtime/uninstall
36
36
  */
37
37
 
38
- import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
38
+ import {
39
+ chmodSync,
40
+ existsSync,
41
+ lstatSync,
42
+ readdirSync,
43
+ readFileSync,
44
+ realpathSync,
45
+ renameSync,
46
+ rmSync,
47
+ statSync,
48
+ writeFileSync,
49
+ } from "fs";
39
50
  import { execFileSync } from "child_process";
40
- import { join } from "path";
51
+ import { join, dirname } from "path";
41
52
  import { homedir } from "os";
42
53
  import { pathToFileURL } from "url";
43
54
  import { createInterface } from "readline";
@@ -100,6 +111,43 @@ const adapterTarget = (() => {
100
111
  return raw ? raw.slice("--target=".length) : process.cwd();
101
112
  })();
102
113
 
114
+ /**
115
+ * tmp + rename, preserving a symlinked target and the existing file mode.
116
+ *
117
+ * Duplicated from install/_common.mjs rather than imported: this script runs
118
+ * standalone from ~/.claude/scripts, where the install tree does not exist.
119
+ * A plain writeFileSync could leave the host's settings.json truncated on a
120
+ * crash; a naive rename would replace a dotfiles symlink with a regular file
121
+ * and reset a 0600 file to 0644.
122
+ *
123
+ * @param {string} path
124
+ * @param {string} content
125
+ */
126
+ function atomicWrite(path, content) {
127
+ let target = path;
128
+ try {
129
+ if (lstatSync(path).isSymbolicLink()) target = realpathSync(path);
130
+ } catch {
131
+ // Missing file: nothing to preserve.
132
+ }
133
+ let mode;
134
+ try {
135
+ mode = statSync(target).mode & 0o777;
136
+ } catch {
137
+ mode = undefined;
138
+ }
139
+ const tmp = `${target}.tmp-${process.pid}`;
140
+ writeFileSync(tmp, content);
141
+ if (mode !== undefined) {
142
+ try {
143
+ chmodSync(tmp, mode);
144
+ } catch {
145
+ // Best effort: a failed chmod must not lose the write.
146
+ }
147
+ }
148
+ renameSync(tmp, target);
149
+ }
150
+
103
151
  /**
104
152
  * Report a planned action. Always logged; in --dry-run mode, no side effects
105
153
  * are performed.
@@ -197,10 +245,7 @@ function deregisterCodexMcpServer() {
197
245
  */
198
246
  function deregisterClaudeMcpServer() {
199
247
  const localBin = join(homedir(), ".local", "bin", "claude");
200
- deregisterMcpServer("claude", existsSync(localBin) ? localBin : undefined, [
201
- "--scope",
202
- "user",
203
- ]);
248
+ deregisterMcpServer("claude", existsSync(localBin) ? localBin : undefined, ["--scope", "user"]);
204
249
  }
205
250
 
206
251
  /**
@@ -220,6 +265,104 @@ function deregisterCopilotMcpServer() {
220
265
  * @param {string} parent
221
266
  * @param {(name: string) => boolean} predicate
222
267
  */
268
+ /**
269
+ * Remove the pipeline's command namespace while preserving user-authored
270
+ * local-only alias wrappers (frontmatter `local-only: true`). Those wrappers
271
+ * exist ONLY under the host dir - install/claude.mjs snapshots them across its
272
+ * wipe for the same reason - so removing the namespace wholesale would destroy
273
+ * content a reinstall can never bring back.
274
+ * @param {string} cmdDir - e.g. ~/.claude/commands/multi-agent
275
+ * @returns {number} preserved wrapper dirs
276
+ */
277
+ function rmCommandsPreservingLocalOnly(cmdDir) {
278
+ // An install interrupted between its wipe and its restore leaves wrappers in
279
+ // a hidden stash beside the namespace. Adopt them back before deciding what
280
+ // to preserve, or the "local-only wrappers preserved" promise below strands
281
+ // them in a dot-directory the user never sees.
282
+ const stashDir = join(dirname(cmdDir), ".multi-agent-wrapper-stash");
283
+ if (existsSync(stashDir) && !dryRun) {
284
+ for (const name of readdirSync(stashDir)) {
285
+ const to = join(cmdDir, name);
286
+ if (existsSync(to)) continue;
287
+ try {
288
+ renameSync(join(stashDir, name), to);
289
+ console.log(` recovered stashed wrapper from an interrupted install: ${name}`);
290
+ } catch {
291
+ /* best effort - a failed recovery must not block the uninstall */
292
+ }
293
+ }
294
+ rmIfExists(stashDir);
295
+ }
296
+ if (!existsSync(cmdDir)) return 0;
297
+ const isLocalOnly = (dir) => {
298
+ const skill = join(dir, "SKILL.md");
299
+ try {
300
+ return existsSync(skill) && /^local-only:\s*true\s*$/m.test(readFileSync(skill, "utf-8"));
301
+ } catch {
302
+ return false;
303
+ }
304
+ };
305
+ const preserved = readdirSync(cmdDir, { withFileTypes: true })
306
+ .filter((e) => e.isDirectory() && isLocalOnly(join(cmdDir, e.name)))
307
+ .map((e) => e.name);
308
+ if (preserved.length === 0) {
309
+ rmIfExists(cmdDir);
310
+ return 0;
311
+ }
312
+ for (const entry of readdirSync(cmdDir)) {
313
+ if (preserved.includes(entry)) continue;
314
+ rmIfExists(join(cmdDir, entry));
315
+ }
316
+ console.log(
317
+ ` preserved ${preserved.length} local-only alias wrapper(s): ${preserved.join(", ")}`,
318
+ );
319
+ return preserved.length;
320
+ }
321
+
322
+ /** Written by install/_platform-filter.mjs beside the skills it delivered. */
323
+ const EXTERNAL_SKILLS_MANIFEST = ".external-skills-manifest.json";
324
+
325
+ /**
326
+ * Remove the external skill catalog THIS install delivered, read from the
327
+ * manifest the installer wrote beside it.
328
+ *
329
+ * Deliberately not derived from `.skills-index.json`: that file is the shipped
330
+ * catalog copied verbatim, so it lists platform-filtered skills that were never
331
+ * installed, and it says nothing about which package version's tree is on disk.
332
+ * Either gap would delete a user-authored skill dir that happens to share a
333
+ * catalog name. Same contract as `.plugin-skills-manifest.json`.
334
+ *
335
+ * No manifest (a pre-manifest install) means ours and theirs are
336
+ * indistinguishable, so nothing is removed and the user is told why.
337
+ *
338
+ * @param {string} skillsDir
339
+ * @returns {number} dirs removed
340
+ */
341
+ function rmExternalDeliveredSkills(skillsDir) {
342
+ if (!existsSync(skillsDir)) return 0;
343
+ const manifestPath = join(skillsDir, EXTERNAL_SKILLS_MANIFEST);
344
+ let names = null;
345
+ if (existsSync(manifestPath)) {
346
+ try {
347
+ const parsed = JSON.parse(readFileSync(manifestPath, "utf-8"));
348
+ if (Array.isArray(parsed)) names = new Set(parsed);
349
+ } catch {
350
+ names = null;
351
+ }
352
+ }
353
+ if (!names) {
354
+ console.log(
355
+ ` note: no ${EXTERNAL_SKILLS_MANIFEST} under ${skillsDir} - external skill dirs left in ` +
356
+ `place (cannot tell pipeline-delivered from user-authored; re-run install once, then uninstall)`,
357
+ );
358
+ return 0;
359
+ }
360
+ let n = rmMatchingDirs(skillsDir, (name) => names.has(name));
361
+ n += rmMatchingFiles(skillsDir, (name) => /^NOTICE-.*\.md$/.test(name));
362
+ rmIfExists(manifestPath);
363
+ return n;
364
+ }
365
+
223
366
  function rmMatchingDirs(parent, predicate) {
224
367
  if (!existsSync(parent)) return 0;
225
368
  let count = 0;
@@ -248,6 +391,31 @@ function rmMatchingFiles(parent, predicate) {
248
391
  return count;
249
392
  }
250
393
 
394
+ // Copilot CLI / Codex CLI have no plugin loader, so install/_plugin-skills.mjs
395
+ // copies each stack plugin's authored skills straight into the flat skills/
396
+ // dir (~100 dirs across a typical set of enabled plugins). None of those
397
+ // names are in PIPELINE_CORE_SKILL_DIRS - the allowlist above only covers the
398
+ // pipeline's OWN skills - so they were never removed on uninstall. The
399
+ // installer now writes PLUGIN_SKILLS_MANIFEST listing exactly what it
400
+ // delivered; read it back here rather than guessing plugin skill names.
401
+ const PLUGIN_SKILLS_MANIFEST = ".plugin-skills-manifest.json";
402
+
403
+ function rmPluginDeliveredSkills(skillsDir) {
404
+ const manifestPath = join(skillsDir, PLUGIN_SKILLS_MANIFEST);
405
+ if (!existsSync(manifestPath)) return 0;
406
+ let names;
407
+ try {
408
+ names = JSON.parse(readFileSync(manifestPath, "utf-8"));
409
+ } catch {
410
+ return 0;
411
+ }
412
+ if (!Array.isArray(names)) return 0;
413
+ const nameSet = new Set(names);
414
+ const count = rmMatchingDirs(skillsDir, (name) => nameSet.has(name));
415
+ rmIfExists(manifestPath);
416
+ return count;
417
+ }
418
+
251
419
  // Explicit terminator install/copilot.mjs writes after the pipeline section
252
420
  // (v11.4.1+). Must stay in sync with INSTRUCTIONS_END_MARKER there.
253
421
  const COPILOT_END_MARKER = "<!-- multi-agent-pipeline:copilot-instructions:end -->";
@@ -339,7 +507,7 @@ function stripManagedBlock(filePath) {
339
507
  rmSync(filePath, { force: true });
340
508
  console.log(` removed (was pipeline-only): ${filePath}`);
341
509
  } else {
342
- writeFileSync(filePath, remaining + "\n");
510
+ atomicWrite(filePath, remaining + "\n");
343
511
  console.log(` cleaned pipeline section: ${filePath}`);
344
512
  }
345
513
  return true;
@@ -356,7 +524,7 @@ function stripManagedBlock(filePath) {
356
524
  rmSync(filePath, { force: true });
357
525
  console.log(` removed (was pipeline-only): ${filePath}`);
358
526
  } else {
359
- writeFileSync(filePath, cleaned + "\n");
527
+ atomicWrite(filePath, cleaned + "\n");
360
528
  console.log(` cleaned pipeline block from: ${filePath}`);
361
529
  }
362
530
  return true;
@@ -387,13 +555,21 @@ function cleanClaudeSettings(settingsPath) {
387
555
  if (settings.hooks?.PreToolUse?.length) {
388
556
  const before = settings.hooks.PreToolUse.length;
389
557
  // Match both the current matcher ("Bash") and the dead pre-v11.4.1 one
390
- // ("Bash(git commit:*)"); the pre-commit-check.sh command identifies the
391
- // entry as ours either way.
558
+ // ("Bash(git commit:*)"); the command identifies the entry as ours either
559
+ // way. install/claude.mjs registers TWO PreToolUse gates this way -
560
+ // pre-commit-check.sh AND agent-guard.sh - and only the first was ever
561
+ // stripped here. After ~/.claude/scripts/ is deleted below, the
562
+ // agent-guard.sh entry pointed at a script that no longer existed, so
563
+ // every subsequent Bash call fired a hook error.
392
564
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
393
565
  (h) =>
394
566
  !(
395
567
  (h.matcher === "Bash" || h.matcher === "Bash(git commit:*)") &&
396
- h.hooks?.some((sub) => sub.command?.includes("pre-commit-check.sh"))
568
+ h.hooks?.some(
569
+ (sub) =>
570
+ sub.command?.includes("pre-commit-check.sh") ||
571
+ sub.command?.includes("agent-guard.sh"),
572
+ )
397
573
  ),
398
574
  );
399
575
  if (settings.hooks.PreToolUse.length < before) touched = true;
@@ -405,7 +581,7 @@ function cleanClaudeSettings(settingsPath) {
405
581
  if (dryRun) {
406
582
  report("would update", settingsPath);
407
583
  } else {
408
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
584
+ atomicWrite(settingsPath, JSON.stringify(settings, null, 2) + "\n");
409
585
  console.log(` cleaned pipeline hook + env from: ${settingsPath}`);
410
586
  }
411
587
  }
@@ -465,6 +641,7 @@ export async function main() {
465
641
  );
466
642
  console.log(" - ~/.claude/CLAUDE.md (your customizations)");
467
643
  console.log(" - ~/.claude/rules/ (user-owned; installed write-if-missing, never overwritten)");
644
+ console.log(" - Local-only alias wrappers under commands/multi-agent/ (user-authored)");
468
645
  if (!allData) console.log(" - ~/.claude/multi-agent-preferences.json (your settings)");
469
646
  console.log(" - Your own content in copilot-instructions.md above the pipeline section");
470
647
  console.log("");
@@ -479,7 +656,7 @@ export async function main() {
479
656
  console.log(" [Claude Code] Removing...");
480
657
  deregisterClaudeMcpServer();
481
658
  const CLAUDE = join(HOME, ".claude");
482
- rmIfExists(join(CLAUDE, "commands", "multi-agent"));
659
+ rmCommandsPreservingLocalOnly(join(CLAUDE, "commands", "multi-agent"));
483
660
  rmIfExists(join(CLAUDE, "scripts"));
484
661
  // Pipeline-managed trees the installer lays down alongside scripts/.
485
662
  rmIfExists(join(CLAUDE, "multi-agent-refs"));
@@ -506,6 +683,7 @@ export async function main() {
506
683
  name === "figma-to-component",
507
684
  );
508
685
  n += rmMatchingDirs(skills, (name) => PIPELINE_CORE_SKILL_DIRS.includes(name));
686
+ n += rmExternalDeliveredSkills(skills);
509
687
  if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
510
688
  rmIfExists(join(skills, ".skills-index.json"));
511
689
  rmIfExists(join(skills, "skills-index.md"));
@@ -540,10 +718,13 @@ export async function main() {
540
718
  name === "figma-to-component",
541
719
  );
542
720
  n += rmMatchingDirs(skills, (name) => PIPELINE_CORE_SKILL_DIRS.includes(name));
721
+ n += rmPluginDeliveredSkills(skills);
722
+ n += rmExternalDeliveredSkills(skills);
543
723
  if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
544
724
  rmIfExists(join(skills, ".skills-index.json"));
545
725
  rmIfExists(join(skills, "skills-index.md"));
546
726
  rmIfExists(join(skills, "README.md"));
727
+ rmIfExists(join(COP, "rules"));
547
728
  // Agent definitions are FILES; remove only the pipeline-shipped names so
548
729
  // user-authored agents survive.
549
730
  rmMatchingFiles(join(COP, "agents"), (name) => PIPELINE_AGENT_FILES.includes(name));
@@ -613,10 +794,11 @@ export async function main() {
613
794
  // Codex's own `.system` set plus user skills; `prompts/` and `agents/` hold
614
795
  // user-authored files.
615
796
  const skills = join(CODEX, "skills");
616
- const n = rmMatchingDirs(
797
+ let n = rmMatchingDirs(
617
798
  skills,
618
799
  (name) => name === "multi-agent" || name.startsWith("multi-agent-"),
619
800
  );
801
+ n += rmPluginDeliveredSkills(skills);
620
802
  if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
621
803
  rmIfExists(join(CODEX, "prompts", "multi-agent.md"));
622
804
  removeGeneratedCodexAgents(join(CODEX, "agents"));
@@ -14,8 +14,8 @@
14
14
  # update-issue-progress.sh <task-id>
15
15
  #
16
16
  # Inputs:
17
- # $HOME/.claude/projects/{slug}/state/{taskId}/agent-state.json
18
- # .issue.url, .issue.number, .repos.primary.org, .repos.primary.name
17
+ # $HOME/.claude/logs/multi-agent/{project}/{taskId}/agent-state.json
18
+ # .githubIssue.owner, .githubIssue.repo, .githubIssue.number
19
19
  # .flags.implementation, .flags.testing, .flags.codeConnect, .flags.wiki
20
20
  #
21
21
  # Side effects:
@@ -86,11 +86,12 @@ if [ "$TASK_ID" = "--rewrite" ]; then
86
86
  exit 0
87
87
  fi
88
88
 
89
- # Resolve the project state directory (matches phase-tracker convention).
90
- STATE_GLOB="$HOME/.claude/projects/*"
89
+ # Resolve agent-state.json. multi-repo-pipeline.sh writes it under a
90
+ # per-project directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/)
91
+ # - not the ~/.claude/projects/*/state/ path this used to glob, which no
92
+ # writer in the pipeline ever populates.
91
93
  AGENT_STATE=""
92
- for dir in $STATE_GLOB; do
93
- candidate="$dir/state/$TASK_ID/agent-state.json"
94
+ for candidate in "$HOME"/.claude/logs/multi-agent/*/"$TASK_ID"/agent-state.json; do
94
95
  if [ -f "$candidate" ]; then
95
96
  AGENT_STATE="$candidate"
96
97
  break
@@ -102,18 +103,13 @@ if [ -z "$AGENT_STATE" ]; then
102
103
  exit 2
103
104
  fi
104
105
 
105
- ISSUE_URL=$(jq -r '.issue.url // empty' "$AGENT_STATE")
106
- ISSUE_NUM=$(jq -r '.issue.number // empty' "$AGENT_STATE")
107
- ORG_REPO=$(jq -r '.repos.primary.name // empty' "$AGENT_STATE")
108
- # Derive org/repo from URL if not present in state.
109
- if [ -z "$ORG_REPO" ] || ! [[ "$ORG_REPO" == */* ]]; then
110
- if [[ "$ISSUE_URL" =~ github\.com/([^/]+/[^/]+)/issues/ ]]; then
111
- ORG_REPO="${BASH_REMATCH[1]}"
112
- fi
113
- fi
106
+ # agent-state.schema.json has no `.issue`/`.repos` properties (additionalProperties
107
+ # is false) - the real GitHub-issue coordinates live at `.githubIssue.{owner,repo,number}`.
108
+ ISSUE_NUM=$(jq -r '.githubIssue.number // empty' "$AGENT_STATE")
109
+ ORG_REPO=$(jq -r 'if .githubIssue then (.githubIssue.owner + "/" + .githubIssue.repo) else empty end' "$AGENT_STATE")
114
110
 
115
111
  if [ -z "$ORG_REPO" ] || [ -z "$ISSUE_NUM" ]; then
116
- echo "update-issue-progress: cannot resolve org/repo or issue number from state" >&2
112
+ echo "update-issue-progress: no githubIssue in state for task=$TASK_ID (Jira-originated task, or issue not yet linked)" >&2
117
113
  exit 3
118
114
  fi
119
115