@curdx/flow 2.2.0 → 2.2.4

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 (83) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +19 -2
  3. package/README.md +15 -8
  4. package/README.zh.md +5 -3
  5. package/agent-preamble/preamble.md +33 -0
  6. package/agents/flow-adversary.md +1 -1
  7. package/agents/flow-architect.md +2 -1
  8. package/agents/flow-brownfield-analyst.md +153 -0
  9. package/agents/flow-debugger.md +6 -11
  10. package/agents/flow-edge-hunter.md +1 -1
  11. package/agents/flow-executor.md +30 -8
  12. package/agents/flow-planner.md +38 -5
  13. package/agents/flow-product-designer.md +2 -1
  14. package/agents/flow-qa-engineer.md +9 -5
  15. package/agents/flow-researcher.md +2 -1
  16. package/agents/flow-reviewer.md +23 -5
  17. package/agents/flow-security-auditor.md +5 -3
  18. package/agents/flow-triage-analyst.md +5 -24
  19. package/agents/flow-ui-researcher.md +4 -3
  20. package/agents/flow-ux-designer.md +12 -39
  21. package/agents/flow-verifier.md +35 -3
  22. package/cli/README.md +3 -1
  23. package/cli/doctor-workflow.js +165 -2
  24. package/cli/doctor.js +8 -0
  25. package/cli/help.js +2 -0
  26. package/cli/lib/doctor-claude-settings.js +736 -0
  27. package/cli/lib/doctor-report.js +256 -1
  28. package/cli/lib/doctor-runtime-environment.js +196 -0
  29. package/cli/lib/frontmatter.js +44 -0
  30. package/cli/lib/json-schema.js +57 -0
  31. package/cli/lib/runtime.js +20 -2
  32. package/cli/lib/semver.js +14 -0
  33. package/cli/uninstall-actions.js +323 -0
  34. package/cli/uninstall.js +9 -253
  35. package/cli/utils.js +6 -1
  36. package/gates/adversarial-review-gate.md +1 -1
  37. package/gates/security-gate.md +2 -2
  38. package/gates/test-quality-gate.md +59 -0
  39. package/hooks/hooks.json +16 -2
  40. package/hooks/scripts/common.sh +4 -0
  41. package/hooks/scripts/session-start.sh +17 -2
  42. package/hooks/scripts/stop-watcher.sh +69 -18
  43. package/hooks/scripts/subagent-artifact-guard.sh +159 -0
  44. package/hooks/scripts/subagent-statusline.sh +105 -0
  45. package/knowledge/atomic-commits.md +1 -1
  46. package/knowledge/claude-code-runtime-contracts.md +203 -0
  47. package/knowledge/epic-decomposition.md +1 -1
  48. package/knowledge/execution-strategies.md +23 -1
  49. package/knowledge/planning-reviews.md +2 -2
  50. package/knowledge/poc-first-workflow.md +8 -8
  51. package/knowledge/review-feedback-intake.md +57 -0
  52. package/knowledge/two-stage-review.md +19 -6
  53. package/knowledge/wave-execution.md +16 -1
  54. package/output-styles/curdx-evidence-first.md +34 -0
  55. package/package.json +7 -1
  56. package/schemas/agent-frontmatter.schema.json +0 -7
  57. package/schemas/config.schema.json +14 -0
  58. package/schemas/hooks.schema.json +34 -2
  59. package/schemas/output-style-frontmatter.schema.json +22 -0
  60. package/schemas/plugin-manifest.schema.json +387 -17
  61. package/schemas/plugin-settings.schema.json +29 -0
  62. package/schemas/skill-frontmatter.schema.json +109 -4
  63. package/schemas/spec-state.schema.json +29 -4
  64. package/settings.json +6 -0
  65. package/skills/brownfield-index/SKILL.md +31 -35
  66. package/skills/browser-qa/SKILL.md +11 -3
  67. package/skills/cancel/SKILL.md +82 -0
  68. package/skills/debug/SKILL.md +6 -2
  69. package/skills/epic/SKILL.md +5 -3
  70. package/skills/fast/SKILL.md +1 -0
  71. package/skills/help/SKILL.md +17 -7
  72. package/skills/implement/SKILL.md +38 -7
  73. package/skills/init/SKILL.md +2 -1
  74. package/skills/review/SKILL.md +4 -1
  75. package/skills/security-audit/SKILL.md +17 -3
  76. package/skills/spec/SKILL.md +2 -1
  77. package/skills/start/SKILL.md +18 -18
  78. package/skills/status/SKILL.md +85 -0
  79. package/skills/ui-sketch/SKILL.md +11 -3
  80. package/skills/verify/SKILL.md +13 -1
  81. package/templates/config.json.tmpl +4 -1
  82. package/templates/progress.md.tmpl +19 -0
  83. package/templates/tasks.md.tmpl +26 -3
@@ -0,0 +1,323 @@
1
+ import { existsSync, lstatSync, unlinkSync, rmSync, readlinkSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ import { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
6
+ import {
7
+ removeMcp,
8
+ removePluginMarketplace,
9
+ uninstallPlugin,
10
+ } from "./lib/claude-ops.js";
11
+ import {
12
+ confirm,
13
+ color,
14
+ listPlugins,
15
+ log,
16
+ resultLastLine,
17
+ resultOutput,
18
+ } from "./utils.js";
19
+ import {
20
+ UNINSTALL_STEP_COUNT,
21
+ getInstalledTargets,
22
+ getManagedMarketplaceIds,
23
+ selectRecommendedPluginsToRemove,
24
+ shouldKeepBundledMcps,
25
+ shouldKeepRequiredPlugins,
26
+ } from "./uninstall-workflow.js";
27
+
28
+ const HOME = homedir();
29
+
30
+ const RECOMMENDED = RECOMMENDED_PLUGINS.map(toUninstallTarget);
31
+ const REQUIRED = REQUIRED_PLUGINS.map(toUninstallTarget);
32
+
33
+ // Symlinks created by install.js (only cleaned with --purge)
34
+ const MANAGED_SYMLINKS = [
35
+ join(HOME, ".local", "bin", "bun"),
36
+ join(HOME, ".local", "bin", "uv"),
37
+ ];
38
+
39
+ export async function uninstallCurdxFlowPlugin(
40
+ {
41
+ listPluginsImpl = listPlugins,
42
+ uninstallPluginImpl = uninstallPlugin,
43
+ logImpl = log,
44
+ resultOutputImpl = resultOutput,
45
+ } = {}
46
+ ) {
47
+ logImpl.blank();
48
+ logImpl.step(1, UNINSTALL_STEP_COUNT, "Uninstalling curdx-flow plugin...");
49
+ const curdx = listPluginsImpl().find((plugin) => plugin.name === "curdx-flow");
50
+ if (!curdx) {
51
+ logImpl.info("curdx-flow not installed, skipping");
52
+ return;
53
+ }
54
+
55
+ const result = await uninstallPluginImpl({
56
+ scope: "user",
57
+ uninstallSpec: "curdx-flow@curdx-flow-marketplace",
58
+ });
59
+ if (result.code === 0) {
60
+ logImpl.ok("curdx-flow uninstalled");
61
+ return;
62
+ }
63
+
64
+ logImpl.err(`Uninstall failed: ${resultOutputImpl(result)}`);
65
+ }
66
+
67
+ export async function maybeUninstallRecommendedPlugins(
68
+ { yes, keepRecommended },
69
+ {
70
+ getInstalledTargetsImpl = getInstalledTargets,
71
+ selectRecommendedPluginsToRemoveImpl = selectRecommendedPluginsToRemove,
72
+ uninstallNamedPluginImpl = uninstallNamedPlugin,
73
+ logImpl = log,
74
+ } = {}
75
+ ) {
76
+ logImpl.blank();
77
+ logImpl.step(2, UNINSTALL_STEP_COUNT, "Recommended plugins");
78
+ if (keepRecommended) {
79
+ logImpl.info("Keeping recommended plugins (--keep-recommended)");
80
+ return;
81
+ }
82
+
83
+ const present = getInstalledTargetsImpl(RECOMMENDED);
84
+ if (present.length === 0) {
85
+ logImpl.info("No installed recommended plugins");
86
+ return;
87
+ }
88
+
89
+ const selected = await selectRecommendedPluginsToRemoveImpl({ yes, present });
90
+ for (const name of selected) {
91
+ const entry = present.find((plugin) => plugin.name === name);
92
+ if (!entry) continue;
93
+ await uninstallNamedPluginImpl(entry);
94
+ }
95
+ }
96
+
97
+ export async function uninstallNamedPlugin(
98
+ entry,
99
+ {
100
+ uninstallPluginImpl = uninstallPlugin,
101
+ resultLastLineImpl = resultLastLine,
102
+ } = {}
103
+ ) {
104
+ log.blank();
105
+ console.log(` ${color.cyan("▸")} Uninstalling ${color.bold(entry.name)}...`);
106
+ const result = await uninstallPluginImpl(entry);
107
+ if (result.code === 0) {
108
+ console.log(` ${color.green("✓")} ${entry.name} uninstalled`);
109
+ return;
110
+ }
111
+
112
+ console.log(
113
+ ` ${color.red("✗")} ${entry.name} uninstall failed: ${resultLastLineImpl(result)}`
114
+ );
115
+ }
116
+
117
+ export async function maybeRemoveBundledMcps(
118
+ { yes, keepRecommended },
119
+ {
120
+ shouldKeepBundledMcpsImpl = shouldKeepBundledMcps,
121
+ confirmImpl = confirm,
122
+ removeMcpImpl = removeMcp,
123
+ logImpl = log,
124
+ } = {}
125
+ ) {
126
+ logImpl.blank();
127
+ logImpl.info("Required MCP servers (context7, sequential-thinking)");
128
+ if (shouldKeepBundledMcpsImpl({ yes, keepRecommended })) {
129
+ logImpl.info(
130
+ color.dim("--yes or --keep-recommended: keeping user-level MCPs (remove manually with `claude mcp remove <name>`)")
131
+ );
132
+ return;
133
+ }
134
+
135
+ const removeMcps = await confirmImpl(
136
+ `Remove user-level MCPs registered by install (${BUNDLED_MCPS.map((mcp) => mcp.name).join(", ")})? ${color.dim("(keeps them if other tools depend on them)")}`,
137
+ false
138
+ );
139
+ if (!removeMcps) {
140
+ logImpl.info("Keeping user-level MCPs");
141
+ return;
142
+ }
143
+
144
+ for (const mcp of BUNDLED_MCPS) {
145
+ const result = await removeMcpImpl({ name: mcp.name });
146
+ if (result.code === 0) {
147
+ logImpl.ok(` ${mcp.name.padEnd(22)} removed`);
148
+ } else {
149
+ logImpl.info(` ${mcp.name.padEnd(22)} ${color.dim("not present or already removed")}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ export async function maybeUninstallRequiredPlugins(
155
+ { yes },
156
+ {
157
+ shouldKeepRequiredPluginsImpl = shouldKeepRequiredPlugins,
158
+ confirmImpl = confirm,
159
+ uninstallPluginImpl = uninstallPlugin,
160
+ logImpl = log,
161
+ } = {}
162
+ ) {
163
+ logImpl.blank();
164
+ logImpl.info("Required companion plugins");
165
+ if (shouldKeepRequiredPluginsImpl({ yes })) {
166
+ logImpl.info(
167
+ color.dim("--yes mode: keeping required companion plugins (use --purge to remove them)")
168
+ );
169
+ return;
170
+ }
171
+
172
+ const removeRequired = await confirmImpl(
173
+ `Remove required companion plugins (${REQUIRED.map((plugin) => plugin.name).join(", ")})? ${color.dim("(keeps shared tools available if other workflows depend on them)")}`,
174
+ false
175
+ );
176
+ if (!removeRequired) {
177
+ logImpl.info("Keeping required companion plugins");
178
+ return;
179
+ }
180
+
181
+ for (const plugin of REQUIRED) {
182
+ const result = await uninstallPluginImpl(plugin);
183
+ if (result.code === 0) {
184
+ logImpl.ok(` ${plugin.name.padEnd(22)} uninstalled`);
185
+ } else {
186
+ logImpl.info(` ${plugin.name.padEnd(22)} ${color.dim("not present or already removed")}`);
187
+ }
188
+ }
189
+ }
190
+
191
+ export async function maybePurgeRuntimeArtifacts(
192
+ { purge },
193
+ {
194
+ purgeManagedMarketplacesImpl = purgeManagedMarketplaces,
195
+ removeManagedSymlinksImpl = removeManagedSymlinks,
196
+ logImpl = log,
197
+ } = {}
198
+ ) {
199
+ logImpl.blank();
200
+ logImpl.step(3, UNINSTALL_STEP_COUNT, "Runtime symlinks and marketplaces");
201
+ if (!purge) {
202
+ logImpl.info(
203
+ color.dim("Keeping ~/.local/bin/bun, ~/.local/bin/uv (use --purge to remove)")
204
+ );
205
+ logImpl.info(
206
+ color.dim("Reason: these bun/uv binaries may be used by other tools — confirm before deleting")
207
+ );
208
+ return;
209
+ }
210
+
211
+ await purgeManagedMarketplacesImpl();
212
+ removeManagedSymlinksImpl();
213
+ }
214
+
215
+ export async function maybeRemoveProjectState(
216
+ { yes },
217
+ {
218
+ cwd = process.cwd(),
219
+ confirmImpl = confirm,
220
+ existsSyncImpl = existsSync,
221
+ rmSyncImpl = rmSync,
222
+ logImpl = log,
223
+ } = {}
224
+ ) {
225
+ logImpl.blank();
226
+ logImpl.step(4, UNINSTALL_STEP_COUNT, "Project state directory");
227
+ const flowDir = join(cwd, ".flow");
228
+ if (!existsSyncImpl(flowDir)) {
229
+ logImpl.info(".flow/ does not exist, skipping");
230
+ return;
231
+ }
232
+
233
+ if (yes) {
234
+ logImpl.info(
235
+ color.dim("--yes mode: keeping .flow/ (contains specs & decisions — confirm by hand before deleting)")
236
+ );
237
+ return;
238
+ }
239
+
240
+ const ok = await confirmImpl(
241
+ `${color.red("DANGER:")} delete the ${color.bold(".flow/")} directory of the current project? ${color.dim("(includes all specs / decisions, not recoverable)")}`,
242
+ false
243
+ );
244
+ if (!ok) {
245
+ logImpl.info("Keeping .flow/");
246
+ return;
247
+ }
248
+
249
+ try {
250
+ rmSyncImpl(flowDir, { recursive: true, force: true });
251
+ logImpl.ok(`Removed ${flowDir}`);
252
+ } catch (err) {
253
+ logImpl.err(`Removal failed: ${err.message}`);
254
+ }
255
+ }
256
+
257
+ async function purgeManagedMarketplaces(
258
+ {
259
+ getManagedMarketplaceIdsImpl = getManagedMarketplaceIds,
260
+ removePluginMarketplaceImpl = removePluginMarketplace,
261
+ logImpl = log,
262
+ } = {}
263
+ ) {
264
+ const marketplaceIds = getManagedMarketplaceIdsImpl(RECOMMENDED.concat(REQUIRED));
265
+
266
+ for (const marketplaceId of marketplaceIds) {
267
+ const result = await removePluginMarketplaceImpl(marketplaceId);
268
+ if (result.code === 0) {
269
+ logImpl.ok(`Removed marketplace ${marketplaceId}`);
270
+ } else if (!result.stderr.includes("not found")) {
271
+ logImpl.warn(`Failed to remove marketplace ${marketplaceId}: ${resultLastLine(result)}`);
272
+ }
273
+ }
274
+ }
275
+
276
+ function removeManagedSymlinks(
277
+ {
278
+ existsSyncImpl = existsSync,
279
+ isBrokenSymlinkImpl = isBrokenSymlink,
280
+ lstatSyncImpl = lstatSync,
281
+ readlinkSyncImpl = readlinkSync,
282
+ unlinkSyncImpl = unlinkSync,
283
+ logImpl = log,
284
+ } = {}
285
+ ) {
286
+ for (const link of MANAGED_SYMLINKS) {
287
+ if (!existsSyncImpl(link) && !isBrokenSymlinkImpl(link)) {
288
+ continue;
289
+ }
290
+ try {
291
+ const stat = lstatSyncImpl(link);
292
+ if (!stat.isSymbolicLink()) {
293
+ logImpl.warn(
294
+ `${link} is not a symlink (likely a real file placed by the user), skipping`
295
+ );
296
+ continue;
297
+ }
298
+ const target = readlinkSyncImpl(link);
299
+ unlinkSyncImpl(link);
300
+ logImpl.ok(`Removed symlink ${link} ${color.dim(`(was → ${target})`)}`);
301
+ } catch (err) {
302
+ logImpl.warn(`Failed to remove ${link}: ${err.message}`);
303
+ }
304
+ }
305
+ }
306
+
307
+ function toUninstallTarget(entry) {
308
+ return {
309
+ name: entry.name,
310
+ uninstallSpec: entry.uninstallSpec,
311
+ uninstallArgs: entry.uninstallArgs || [],
312
+ marketplaceId: entry.marketplaceId,
313
+ scope: entry.scope,
314
+ };
315
+ }
316
+
317
+ function isBrokenSymlink(pathname) {
318
+ try {
319
+ return lstatSync(pathname).isSymbolicLink();
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
package/cli/uninstall.js CHANGED
@@ -2,48 +2,22 @@
2
2
  * uninstall command — remove curdx-flow plugin (and optionally recommended plugins / artifacts).
3
3
  */
4
4
 
5
- import { existsSync, lstatSync, unlinkSync, rmSync, readlinkSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { homedir } from "node:os";
8
-
9
- import {
10
- color,
11
- log,
12
- resultLastLine,
13
- resultOutput,
14
- confirm,
15
- listPlugins,
16
- } from "./utils.js";
17
- import { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
18
- import {
19
- removeMcp,
20
- removePluginMarketplace,
21
- uninstallPlugin,
22
- } from "./lib/claude-ops.js";
5
+ import { log } from "./utils.js";
23
6
  import {
24
7
  createUninstallContext,
25
8
  ensureClaudeCliAvailableForUninstall,
26
- getInstalledTargets,
27
- getManagedMarketplaceIds,
28
9
  printUninstallSummary,
29
10
  removeProtocolsStep,
30
- selectRecommendedPluginsToRemove,
31
- shouldKeepBundledMcps,
32
- shouldKeepRequiredPlugins,
33
- UNINSTALL_STEP_COUNT,
34
11
  confirmUninstallStep,
35
12
  } from "./uninstall-workflow.js";
36
-
37
- const HOME = homedir();
38
-
39
- const RECOMMENDED = RECOMMENDED_PLUGINS.map(toUninstallTarget);
40
- const REQUIRED = REQUIRED_PLUGINS.map(toUninstallTarget);
41
-
42
- // Symlinks created by install.js (only cleaned with --purge)
43
- const MANAGED_SYMLINKS = [
44
- join(HOME, ".local", "bin", "bun"),
45
- join(HOME, ".local", "bin", "uv"),
46
- ];
13
+ import {
14
+ maybePurgeRuntimeArtifacts,
15
+ maybeRemoveBundledMcps,
16
+ maybeRemoveProjectState,
17
+ maybeUninstallRecommendedPlugins,
18
+ maybeUninstallRequiredPlugins,
19
+ uninstallCurdxFlowPlugin,
20
+ } from "./uninstall-actions.js";
47
21
 
48
22
  export async function uninstall(args = []) {
49
23
  const context = createUninstallContext(args);
@@ -66,221 +40,3 @@ export async function uninstall(args = []) {
66
40
 
67
41
  printUninstallSummary(context);
68
42
  }
69
-
70
- async function uninstallCurdxFlowPlugin() {
71
- log.blank();
72
- log.step(1, UNINSTALL_STEP_COUNT, "Uninstalling curdx-flow plugin...");
73
- const curdx = listPlugins().find((plugin) => plugin.name === "curdx-flow");
74
- if (!curdx) {
75
- log.info("curdx-flow not installed, skipping");
76
- return;
77
- }
78
-
79
- const result = await uninstallPlugin({
80
- scope: "user",
81
- uninstallSpec: "curdx-flow@curdx-flow-marketplace",
82
- });
83
- if (result.code === 0) {
84
- log.ok("curdx-flow uninstalled");
85
- return;
86
- }
87
-
88
- log.err(`Uninstall failed: ${resultOutput(result)}`);
89
- }
90
-
91
- async function maybeUninstallRecommendedPlugins({ yes, keepRecommended }) {
92
- log.blank();
93
- log.step(2, UNINSTALL_STEP_COUNT, "Recommended plugins");
94
- if (keepRecommended) {
95
- log.info("Keeping recommended plugins (--keep-recommended)");
96
- return;
97
- }
98
-
99
- const present = getInstalledTargets(RECOMMENDED);
100
- if (present.length === 0) {
101
- log.info("No installed recommended plugins");
102
- return;
103
- }
104
-
105
- const selected = await selectRecommendedPluginsToRemove({ yes, present });
106
- for (const name of selected) {
107
- const entry = present.find((plugin) => plugin.name === name);
108
- if (!entry) continue;
109
- await uninstallNamedPlugin(entry);
110
- }
111
- }
112
-
113
- async function uninstallNamedPlugin(entry) {
114
- log.blank();
115
- console.log(` ${color.cyan("▸")} Uninstalling ${color.bold(entry.name)}...`);
116
- const result = await uninstallPlugin(entry);
117
- if (result.code === 0) {
118
- console.log(` ${color.green("✓")} ${entry.name} uninstalled`);
119
- return;
120
- }
121
-
122
- console.log(
123
- ` ${color.red("✗")} ${entry.name} uninstall failed: ${resultLastLine(result)}`
124
- );
125
- }
126
-
127
- async function maybeRemoveBundledMcps({ yes, keepRecommended }) {
128
- log.blank();
129
- log.info("Required MCP servers (context7, sequential-thinking)");
130
- if (shouldKeepBundledMcps({ yes, keepRecommended })) {
131
- log.info(
132
- color.dim("--yes or --keep-recommended: keeping user-level MCPs (remove manually with `claude mcp remove <name>`)")
133
- );
134
- return;
135
- }
136
-
137
- const removeMcps = await confirm(
138
- `Remove user-level MCPs registered by install (${BUNDLED_MCPS.map((mcp) => mcp.name).join(", ")})? ${color.dim("(keeps them if other tools depend on them)")}`,
139
- false
140
- );
141
- if (!removeMcps) {
142
- log.info("Keeping user-level MCPs");
143
- return;
144
- }
145
-
146
- for (const mcp of BUNDLED_MCPS) {
147
- const result = await removeMcp({ name: mcp.name });
148
- if (result.code === 0) {
149
- log.ok(` ${mcp.name.padEnd(22)} removed`);
150
- } else {
151
- log.info(` ${mcp.name.padEnd(22)} ${color.dim("not present or already removed")}`);
152
- }
153
- }
154
- }
155
-
156
- async function maybeUninstallRequiredPlugins({ yes }) {
157
- log.blank();
158
- log.info("Required companion plugins");
159
- if (shouldKeepRequiredPlugins({ yes })) {
160
- log.info(
161
- color.dim("--yes mode: keeping required companion plugins (use --purge to remove them)")
162
- );
163
- return;
164
- }
165
-
166
- const removeRequired = await confirm(
167
- `Remove required companion plugins (${REQUIRED.map((plugin) => plugin.name).join(", ")})? ${color.dim("(keeps shared tools available if other workflows depend on them)")}`,
168
- false
169
- );
170
- if (!removeRequired) {
171
- log.info("Keeping required companion plugins");
172
- return;
173
- }
174
-
175
- for (const plugin of REQUIRED) {
176
- const result = await uninstallPlugin(plugin);
177
- if (result.code === 0) {
178
- log.ok(` ${plugin.name.padEnd(22)} uninstalled`);
179
- } else {
180
- log.info(` ${plugin.name.padEnd(22)} ${color.dim("not present or already removed")}`);
181
- }
182
- }
183
- }
184
-
185
- async function maybePurgeRuntimeArtifacts({ purge }) {
186
- log.blank();
187
- log.step(3, UNINSTALL_STEP_COUNT, "Runtime symlinks and marketplaces");
188
- if (!purge) {
189
- log.info(
190
- color.dim("Keeping ~/.local/bin/bun, ~/.local/bin/uv (use --purge to remove)")
191
- );
192
- log.info(
193
- color.dim("Reason: these bun/uv binaries may be used by other tools — confirm before deleting")
194
- );
195
- return;
196
- }
197
-
198
- await purgeManagedMarketplaces();
199
- removeManagedSymlinks();
200
- }
201
-
202
- async function purgeManagedMarketplaces() {
203
- const marketplaceIds = getManagedMarketplaceIds(RECOMMENDED.concat(REQUIRED));
204
-
205
- for (const marketplaceId of marketplaceIds) {
206
- const result = await removePluginMarketplace(marketplaceId);
207
- if (result.code === 0) {
208
- log.ok(`Removed marketplace ${marketplaceId}`);
209
- } else if (!result.stderr.includes("not found")) {
210
- log.warn(`Failed to remove marketplace ${marketplaceId}: ${resultLastLine(result)}`);
211
- }
212
- }
213
- }
214
-
215
- function removeManagedSymlinks() {
216
- for (const link of MANAGED_SYMLINKS) {
217
- if (!existsSync(link) && !isBrokenSymlink(link)) {
218
- continue;
219
- }
220
- try {
221
- const stat = lstatSync(link);
222
- if (!stat.isSymbolicLink()) {
223
- log.warn(
224
- `${link} is not a symlink (likely a real file placed by the user), skipping`
225
- );
226
- continue;
227
- }
228
- const target = readlinkSync(link);
229
- unlinkSync(link);
230
- log.ok(`Removed symlink ${link} ${color.dim(`(was → ${target})`)}`);
231
- } catch (err) {
232
- log.warn(`Failed to remove ${link}: ${err.message}`);
233
- }
234
- }
235
- }
236
-
237
- async function maybeRemoveProjectState({ yes }) {
238
- log.blank();
239
- log.step(4, UNINSTALL_STEP_COUNT, "Project state directory");
240
- const flowDir = join(process.cwd(), ".flow");
241
- if (!existsSync(flowDir)) {
242
- log.info(".flow/ does not exist, skipping");
243
- return;
244
- }
245
-
246
- if (yes) {
247
- log.info(
248
- color.dim("--yes mode: keeping .flow/ (contains specs & decisions — confirm by hand before deleting)")
249
- );
250
- return;
251
- }
252
-
253
- const ok = await confirm(
254
- `${color.red("DANGER:")} delete the ${color.bold(".flow/")} directory of the current project? ${color.dim("(includes all specs / decisions, not recoverable)")}`,
255
- false
256
- );
257
- if (!ok) {
258
- log.info("Keeping .flow/");
259
- return;
260
- }
261
-
262
- try {
263
- rmSync(flowDir, { recursive: true, force: true });
264
- log.ok(`Removed ${flowDir}`);
265
- } catch (err) {
266
- log.err(`Removal failed: ${err.message}`);
267
- }
268
- }
269
-
270
- function toUninstallTarget(entry) {
271
- return {
272
- name: entry.name,
273
- uninstallSpec: entry.uninstallSpec,
274
- uninstallArgs: entry.uninstallArgs || [],
275
- marketplaceId: entry.marketplaceId,
276
- scope: entry.scope,
277
- };
278
- }
279
-
280
- function isBrokenSymlink(pathname) {
281
- try {
282
- return lstatSync(pathname).isSymbolicLink();
283
- } catch {
284
- return false;
285
- }
286
- }
package/cli/utils.js CHANGED
@@ -32,4 +32,9 @@ export {
32
32
  parsePluginListJson,
33
33
  readUserMcpConfig,
34
34
  } from "./lib/claude.js";
35
- export { ensureClaudeMemRuntimes, ensureRuntimeInPath } from "./lib/runtime.js";
35
+ export {
36
+ ensureClaudeMemRuntimes,
37
+ ensureRuntimeInPath,
38
+ inspectClaudeMemRuntimes,
39
+ inspectRuntimeInPath,
40
+ } from "./lib/runtime.js";
@@ -17,7 +17,7 @@ depends_on: []
17
17
 
18
18
  - /curdx-flow:review command
19
19
  - Before Phase transitions (requirements → design, design → tasks)
20
- - Before code merge (/curdx-flow:ship)
20
+ - Before code merge or human PR/release handoff
21
21
  - Enabled by default in Enterprise mode
22
22
 
23
23
  ---
@@ -14,7 +14,7 @@ depends_on: []
14
14
  ## Trigger Timing
15
15
 
16
16
  - When the `security-audit` skill runs
17
- - Before `/curdx-flow:ship` (auto-triggered, Phase 6+)
17
+ - Before human PR/release handoff, after `/curdx-flow:verify` and `/curdx-flow:review`
18
18
  - When committing specs involving auth / payments / PII
19
19
 
20
20
  ---
@@ -154,7 +154,7 @@ pnpm audit
154
154
 
155
155
  ### Blocking Items
156
156
 
157
- - If SR-01 ~ SR-05 are found → block immediately, prohibit `/curdx-flow:ship`
157
+ - If SR-01 ~ SR-05 are found → block immediately; do not hand off for PR/release
158
158
  - Must fix or explicitly exempt (record in STATE.md as tech debt + commitment to fix before release)
159
159
 
160
160
  ### Warning Items
@@ -0,0 +1,59 @@
1
+ ---
2
+ gate: test-quality-gate
3
+ category: standard-mode
4
+ severity: blocking
5
+ depends_on: []
6
+ ---
7
+
8
+ # Test Quality Gate
9
+
10
+ A green test suite is not enough. Tests must exercise real behavior and fail for the right reason.
11
+
12
+ ## Blocking Findings
13
+
14
+ Flag as blocking when a test is the only evidence for an FR/AC and any of these hold:
15
+
16
+ 1. **Mock-only behavior**
17
+ - Assertions only check mock calls (`toHaveBeenCalled`, `calledWith`, spy counts).
18
+ - The real module/function under test is never invoked.
19
+ - The test would still pass if the production implementation were empty.
20
+
21
+ 2. **Mock setup dominates evidence**
22
+ - Mock/stub/spy setup lines are more than 3x real behavioral assertions.
23
+ - The test mostly restates fixture wiring instead of asserting output, state, persistence, or user-visible behavior.
24
+
25
+ 3. **Skipped or inert tests**
26
+ - `it.skip`, `describe.skip`, `test.skip`, `xit`, `pending`, or equivalent on covered behavior.
27
+ - Test has no assertions and no meaningful side-effect check.
28
+
29
+ 4. **Implementation-biased regression**
30
+ - Test was added after implementation without evidence of RED failure when the task claims TDD.
31
+ - Test asserts internal private structure instead of externally observable behavior.
32
+
33
+ 5. **Missing cleanup for stateful mocks**
34
+ - Stateful mocks/spies are used across tests without `afterEach` cleanup (`restoreAllMocks`, `clearAllMocks`, sandbox restore, etc.).
35
+ - Shared mock state can leak between tests.
36
+
37
+ ## Acceptable Mock Usage
38
+
39
+ Mocks are acceptable when they isolate a boundary and the assertion still verifies real behavior:
40
+
41
+ - Network/payment/email provider mocked, but service logic and error handling are real.
42
+ - Clock/randomness mocked to make deterministic assertions.
43
+ - Database mocked only when a separate integration test covers persistence behavior.
44
+
45
+ ## Evidence Checklist
46
+
47
+ For each FR/AC test evidence, record:
48
+
49
+ - Test file and test name.
50
+ - What real code path is invoked.
51
+ - What behavioral assertion proves the requirement.
52
+ - Whether the test was observed RED before GREEN when TDD is claimed.
53
+ - Whether mocks are boundary-only or behavior-replacing.
54
+
55
+ ## Verdicts
56
+
57
+ - `PASS`: Tests exercise real behavior with meaningful assertions.
58
+ - `WARN`: Mock-heavy but supported by separate integration/e2e coverage.
59
+ - `FAIL`: Mock-only/skipped/no-assertion test is used as primary evidence.