@curdx/flow 2.0.0-beta.1 → 2.0.0-beta.10

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 (57) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +3 -10
  3. package/CHANGELOG.md +20 -0
  4. package/README.zh.md +2 -2
  5. package/agent-preamble/preamble.md +81 -11
  6. package/agents/flow-adversary.md +40 -55
  7. package/agents/flow-architect.md +23 -10
  8. package/agents/flow-debugger.md +2 -2
  9. package/agents/flow-edge-hunter.md +20 -6
  10. package/agents/flow-executor.md +3 -3
  11. package/agents/flow-planner.md +51 -48
  12. package/agents/flow-product-designer.md +14 -1
  13. package/agents/flow-qa-engineer.md +1 -1
  14. package/agents/flow-researcher.md +17 -2
  15. package/agents/flow-reviewer.md +5 -1
  16. package/agents/flow-security-auditor.md +1 -1
  17. package/agents/flow-triage-analyst.md +1 -1
  18. package/agents/flow-ui-researcher.md +2 -2
  19. package/agents/flow-ux-designer.md +1 -1
  20. package/agents/flow-verifier.md +47 -14
  21. package/bin/curdx-flow.js +13 -1
  22. package/cli/doctor.js +28 -13
  23. package/cli/install.js +62 -36
  24. package/cli/protocols.js +63 -10
  25. package/cli/registry.js +73 -0
  26. package/cli/uninstall.js +9 -11
  27. package/cli/upgrade.js +6 -10
  28. package/cli/utils.js +104 -56
  29. package/commands/fast.md +1 -1
  30. package/commands/implement.md +4 -4
  31. package/commands/init.md +14 -3
  32. package/commands/review.md +14 -5
  33. package/commands/spec.md +26 -2
  34. package/commands/start.md +47 -17
  35. package/commands/verify.md +13 -0
  36. package/gates/adversarial-review-gate.md +19 -19
  37. package/gates/devex-gate.md +4 -5
  38. package/gates/edge-case-gate.md +1 -1
  39. package/hooks/hooks.json +0 -11
  40. package/hooks/scripts/quick-mode-guard.sh +12 -9
  41. package/hooks/scripts/session-start.sh +1 -1
  42. package/hooks/scripts/stop-watcher.sh +25 -15
  43. package/knowledge/execution-strategies.md +6 -5
  44. package/knowledge/spec-driven-development.md +8 -7
  45. package/knowledge/two-stage-review.md +4 -3
  46. package/package.json +4 -2
  47. package/skills/brownfield-index/SKILL.md +62 -0
  48. package/skills/browser-qa/SKILL.md +50 -0
  49. package/skills/epic/SKILL.md +68 -0
  50. package/skills/security-audit/SKILL.md +50 -0
  51. package/skills/ui-sketch/SKILL.md +49 -0
  52. package/templates/config.json.tmpl +1 -1
  53. package/templates/design.md.tmpl +32 -112
  54. package/templates/requirements.md.tmpl +25 -43
  55. package/templates/research.md.tmpl +37 -68
  56. package/templates/tasks.md.tmpl +27 -84
  57. package/hooks/scripts/fail-tracker.sh +0 -31
package/cli/protocols.js CHANGED
@@ -5,10 +5,23 @@
5
5
  * and reversible (uninstall removes it cleanly without touching user content).
6
6
  */
7
7
 
8
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
8
+ import {
9
+ readFileSync,
10
+ writeFileSync,
11
+ existsSync,
12
+ mkdirSync,
13
+ renameSync,
14
+ unlinkSync,
15
+ } from "node:fs";
9
16
  import { join, dirname } from "node:path";
10
-
11
- const HOME = process.env.HOME || "";
17
+ import { homedir } from "node:os";
18
+
19
+ // Use os.homedir() instead of process.env.HOME — HOME can be empty inside
20
+ // non-login shells (CI containers, some spawned child envs), which would
21
+ // resolve GLOBAL_CLAUDE_MD to "/.claude/CLAUDE.md" (filesystem root) and
22
+ // cause mkdir/writeFileSync to fail with EACCES. homedir() falls back to
23
+ // the effective user's passwd entry on POSIX and USERPROFILE on Windows.
24
+ const HOME = homedir();
12
25
  export const GLOBAL_CLAUDE_MD = join(HOME, ".claude", "CLAUDE.md");
13
26
 
14
27
  const SENTINEL_BEGIN =
@@ -53,16 +66,56 @@ function readGlobalMd() {
53
66
 
54
67
  /**
55
68
  * Locate the sentinel block in the content.
56
- * Returns { start, end } indices into content, or null if not found.
69
+ * Returns { start, end } indices into content, `null` if neither sentinel is
70
+ * present, or throws if the block is corrupted (begin without matching end).
71
+ * The throw is intentional — previously the corrupted case silently returned
72
+ * null, so the next run would append a SECOND block, producing drift.
57
73
  */
58
74
  function findBlock(content) {
59
75
  const start = content.indexOf(SENTINEL_BEGIN);
60
- if (start === -1) return null;
76
+ if (start === -1) {
77
+ // Also check for a dangling END without BEGIN — that is also corrupted.
78
+ if (content.indexOf(SENTINEL_END) !== -1) {
79
+ throw new Error(
80
+ `Corrupted protocol block in ${GLOBAL_CLAUDE_MD}: END sentinel found without BEGIN. ` +
81
+ `Manually inspect the file and remove the dangling END line, then re-run.`
82
+ );
83
+ }
84
+ return null;
85
+ }
61
86
  const endIdx = content.indexOf(SENTINEL_END, start);
62
- if (endIdx === -1) return null;
87
+ if (endIdx === -1) {
88
+ throw new Error(
89
+ `Corrupted protocol block in ${GLOBAL_CLAUDE_MD}: BEGIN sentinel found without END. ` +
90
+ `Manually remove the orphan BEGIN line (or restore the END), then re-run.`
91
+ );
92
+ }
63
93
  return { start, end: endIdx + SENTINEL_END.length };
64
94
  }
65
95
 
96
+ /**
97
+ * Write `content` to `path` atomically: write to a sibling temp file first,
98
+ * then rename. This prevents a half-written CLAUDE.md if the process is
99
+ * interrupted mid-write, and avoids races between concurrent install /
100
+ * uninstall invocations.
101
+ */
102
+ function atomicWrite(path, content) {
103
+ const tmp = `${path}.curdx-flow.tmp.${process.pid}`;
104
+ try {
105
+ writeFileSync(tmp, content, "utf-8");
106
+ renameSync(tmp, path);
107
+ } catch (err) {
108
+ // Best-effort cleanup of the temp file; swallow errors here since we
109
+ // are already re-throwing the real failure.
110
+ try {
111
+ if (existsSync(tmp)) unlinkSync(tmp);
112
+ } catch {
113
+ // ignore
114
+ }
115
+ throw err;
116
+ }
117
+ }
118
+
66
119
  /**
67
120
  * Inject (or upgrade) the protocol block in ~/.claude/CLAUDE.md.
68
121
  * @returns {{action:"created"|"upgraded"|"unchanged", path:string}}
@@ -81,8 +134,8 @@ export function injectGlobalProtocols() {
81
134
  ? (existing.endsWith("\n") ? "\n" : "\n\n")
82
135
  : "";
83
136
  const next = existing + sep + FULL_BLOCK + "\n";
84
- writeFileSync(path, next, "utf-8");
85
- return { action: existing.length === 0 ? "created" : "created", path };
137
+ atomicWrite(path, next);
138
+ return { action: existing.length === 0 ? "created" : "appended", path };
86
139
  }
87
140
 
88
141
  // Replace existing block (handle upgrade-in-place)
@@ -92,7 +145,7 @@ export function injectGlobalProtocols() {
92
145
  }
93
146
  const next =
94
147
  existing.slice(0, block.start) + FULL_BLOCK + existing.slice(block.end);
95
- writeFileSync(path, next, "utf-8");
148
+ atomicWrite(path, next);
96
149
  return { action: "upgraded", path };
97
150
  }
98
151
 
@@ -119,6 +172,6 @@ export function removeGlobalProtocols() {
119
172
  }
120
173
 
121
174
  const next = existing.slice(0, start) + existing.slice(end);
122
- writeFileSync(path, next, "utf-8");
175
+ atomicWrite(path, next);
123
176
  return { action: "removed", path };
124
177
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Single source of truth for recommended companion plugins.
3
+ *
4
+ * Background: before this file existed, the list of recommended plugins lived
5
+ * in FOUR independent places (install.js, uninstall.js, upgrade.js,
6
+ * doctor.js). They drifted — chrome-devtools-mcp was added to install.js
7
+ * during the beta.8 MCP decoupling but forgotten in the other three,
8
+ * making it installable but uninstallable. This registry exists so adding
9
+ * or removing a plugin is a one-file change.
10
+ *
11
+ * Every consumer pulls what it needs via property access:
12
+ * - install.js → marketplace + installSpec + hint (+ optional postInstall)
13
+ * - uninstall.js → uninstallSpec
14
+ * - upgrade.js → installSpec (for `claude plugin update`) + marketplaceId
15
+ * - doctor.js → name + installSpec (for manual recovery hints)
16
+ */
17
+
18
+ export const RECOMMENDED_PLUGINS = [
19
+ {
20
+ name: "pua",
21
+ marketplace: "tanweai/pua",
22
+ marketplaceId: "pua",
23
+ installSpec: "pua@pua-skills",
24
+ uninstallSpec: "pua@pua-skills",
25
+ hint: "no-give-up + three red lines",
26
+ },
27
+ {
28
+ name: "claude-mem",
29
+ marketplace: "thedotmack/claude-mem",
30
+ marketplaceId: "thedotmack",
31
+ installSpec: "claude-mem@thedotmack",
32
+ uninstallSpec: "claude-mem@thedotmack",
33
+ hint: "automatic cross-session memory",
34
+ postInstall: "claude-mem-runtimes",
35
+ },
36
+ {
37
+ name: "frontend-design",
38
+ // Already in default marketplace claude-plugins-official, no add needed
39
+ marketplace: null,
40
+ marketplaceId: "claude-plugins-official",
41
+ installSpec: "frontend-design@claude-plugins-official",
42
+ uninstallSpec: "frontend-design@claude-plugins-official",
43
+ hint: "Anthropic official UI skill",
44
+ },
45
+ {
46
+ name: "chrome-devtools-mcp",
47
+ marketplace: "ChromeDevTools/chrome-devtools-mcp",
48
+ marketplaceId: "chrome-devtools-plugins",
49
+ installSpec: "chrome-devtools-mcp@chrome-devtools-plugins",
50
+ uninstallSpec: "chrome-devtools-mcp@chrome-devtools-plugins",
51
+ hint: "Chrome DevTools + Puppeteer (Google official)",
52
+ },
53
+ ];
54
+
55
+ /**
56
+ * Marketplaces to refresh during `upgrade`. Derived from RECOMMENDED_PLUGINS
57
+ * plus the curdx-flow marketplace itself.
58
+ */
59
+ export const MARKETPLACES_TO_REFRESH = [
60
+ "curdx-flow-marketplace",
61
+ ...RECOMMENDED_PLUGINS
62
+ .filter((p) => p.marketplaceId && p.marketplaceId !== "claude-plugins-official")
63
+ .map((p) => p.marketplaceId),
64
+ ];
65
+
66
+ /**
67
+ * Plugin install specs to update during `upgrade` — includes curdx-flow
68
+ * itself plus every recommended plugin.
69
+ */
70
+ export const PLUGINS_TO_UPDATE = [
71
+ "curdx-flow@curdx-flow-marketplace",
72
+ ...RECOMMENDED_PLUGINS.map((p) => p.installSpec),
73
+ ];
package/cli/uninstall.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { existsSync, lstatSync, unlinkSync, rmSync, readlinkSync } from "node:fs";
6
6
  import { join } from "node:path";
7
+ import { homedir } from "node:os";
7
8
 
8
9
  import {
9
10
  color,
@@ -15,18 +16,15 @@ import {
15
16
  listPlugins,
16
17
  } from "./utils.js";
17
18
  import { removeGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
19
+ import { RECOMMENDED_PLUGINS } from "./registry.js";
18
20
 
19
- const HOME = process.env.HOME || "";
21
+ const HOME = homedir();
20
22
 
21
- // Keep aligned with install.js
22
- const RECOMMENDED = [
23
- { name: "pua", uninstallSpec: "pua@pua-skills" },
24
- { name: "claude-mem", uninstallSpec: "claude-mem@thedotmack" },
25
- {
26
- name: "frontend-design",
27
- uninstallSpec: "frontend-design@claude-plugins-official",
28
- },
29
- ];
23
+ // Pull uninstall-relevant subset from the single registry. See registry.js.
24
+ const RECOMMENDED = RECOMMENDED_PLUGINS.map(({ name, uninstallSpec }) => ({
25
+ name,
26
+ uninstallSpec,
27
+ }));
30
28
 
31
29
  // Symlinks created by install.js (only cleaned with --purge)
32
30
  const MANAGED_SYMLINKS = [
@@ -116,7 +114,7 @@ export async function uninstall(args = []) {
116
114
  for (const name of toRemove) {
117
115
  const rec = presentRecs.find((r) => r.name === name);
118
116
  log.blank();
119
- console.log(` ${color.cyan("")} Uninstalling ${color.bold(rec.name)}...`);
117
+ console.log(` ${color.cyan("")} Uninstalling ${color.bold(rec.name)}...`);
120
118
  const r = await run(
121
119
  "claude",
122
120
  ["plugin", "uninstall", rec.uninstallSpec],
package/cli/upgrade.js CHANGED
@@ -3,13 +3,10 @@
3
3
  */
4
4
 
5
5
  import { color, log, run, listPlugins, claudeVersion } from "./utils.js";
6
-
7
- const PLUGINS_TO_UPDATE = [
8
- "curdx-flow@curdx-flow-marketplace",
9
- "pua@pua-skills",
10
- "claude-mem@thedotmack",
11
- "frontend-design@claude-plugins-official",
12
- ];
6
+ import {
7
+ PLUGINS_TO_UPDATE,
8
+ MARKETPLACES_TO_REFRESH,
9
+ } from "./registry.js";
13
10
 
14
11
  export async function upgrade(args = []) {
15
12
  log.title("⬆️ CurDX-Flow upgrade");
@@ -19,10 +16,9 @@ export async function upgrade(args = []) {
19
16
  process.exit(1);
20
17
  }
21
18
 
22
- // Refresh marketplaces first
19
+ // Refresh marketplaces first (derived from cli/registry.js)
23
20
  log.step(1, 2, "Refreshing marketplaces...");
24
- const marketplaces = ["curdx-flow-marketplace", "pua", "thedotmack"];
25
- for (const mp of marketplaces) {
21
+ for (const mp of MARKETPLACES_TO_REFRESH) {
26
22
  const r = await run(
27
23
  "claude",
28
24
  ["plugin", "marketplace", "update", mp],
package/cli/utils.js CHANGED
@@ -108,39 +108,6 @@ export function confirm(message, defaultYes = true) {
108
108
  });
109
109
  }
110
110
 
111
- /**
112
- * Ask user to pick from a list. Returns selected value or null if aborted.
113
- */
114
- export function select(message, choices, defaultIndex = 0) {
115
- return new Promise((resolve) => {
116
- console.log(`${color.cyan("?")} ${message}`);
117
- choices.forEach((ch, i) => {
118
- const marker = i === defaultIndex ? color.green("▸") : " ";
119
- console.log(` ${marker} ${color.bold(String(i + 1))}. ${ch.label}`);
120
- });
121
-
122
- const rl = createInterface({
123
- input: process.stdin,
124
- output: process.stdout,
125
- });
126
- rl.question(
127
- ` ${color.dim(`(default: ${defaultIndex + 1}, q to abort) `)}`,
128
- (ans) => {
129
- rl.close();
130
- const v = ans.trim().toLowerCase();
131
- if (v === "q") return resolve(null);
132
- if (v === "") return resolve(choices[defaultIndex].value);
133
- const n = parseInt(v, 10);
134
- if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
135
- return resolve(choices[n - 1].value);
136
- }
137
- console.log(color.yellow(" (invalid, using default)"));
138
- resolve(choices[defaultIndex].value);
139
- }
140
- );
141
- });
142
- }
143
-
144
111
  /**
145
112
  * Multi-select (checkbox-style via comma-separated input).
146
113
  * Returns array of selected values.
@@ -199,47 +166,124 @@ export function claudeVersion() {
199
166
  return m ? m[1] : res.stdout.trim().split("\n")[0];
200
167
  }
201
168
 
202
- /** List installed plugins via `claude plugin list`. Returns array of { name, version, status }. */
169
+ /**
170
+ * List installed plugins. Prefers the structured `claude plugin list --json`
171
+ * output (stable machine-readable format; confirmed present in claude
172
+ * 2.1.117+). Falls back to parsing the human-readable stream-text output
173
+ * for older CLI versions, but warns that parser is brittle.
174
+ *
175
+ * Returns array of { name, version, status }.
176
+ */
203
177
  export function listPlugins() {
204
- const res = runSync("claude", ["plugin", "list"]);
205
- if (res.code !== 0) return [];
206
- const out = res.stdout;
207
- const plugins = [];
208
- // Parse format like:
178
+ // Preferred: structured JSON output.
179
+ const j = runSync("claude", ["plugin", "list", "--json"]);
180
+ if (j.code === 0 && j.stdout.trim().startsWith("[")) {
181
+ try {
182
+ const arr = JSON.parse(j.stdout);
183
+ return arr.map((p) => ({
184
+ // id has form "name@marketplace" — name is stable for dedup/lookup.
185
+ name: String(p.id || "").split("@")[0],
186
+ version: p.version,
187
+ status: p.enabled === false ? "disabled" : "enabled",
188
+ raw: JSON.stringify(p),
189
+ }));
190
+ } catch {
191
+ // JSON parse failed — fall through to legacy text parser.
192
+ }
193
+ }
194
+
195
+ // Legacy fallback: parse the human-readable format.
209
196
  // ❯ curdx-flow@curdx-flow-marketplace
210
197
  // Version: 1.1.1
211
- // Scope: user
212
198
  // Status: ✔ enabled
213
- const blocks = out.split(/\n\s*❯\s*/).slice(1);
199
+ // Fragile matches unicode markers. Kept only for older claude CLIs.
200
+ const res = runSync("claude", ["plugin", "list"]);
201
+ if (res.code !== 0) return [];
202
+ const plugins = [];
203
+ const blocks = res.stdout.split(/\n\s*❯\s*/).slice(1);
214
204
  for (const block of blocks) {
215
205
  const lines = block.split("\n");
216
206
  const name = lines[0].trim().split("@")[0];
217
207
  const version = (block.match(/Version:\s*(\S+)/) || [])[1];
218
- const status = block.includes("✔") ? "enabled" : block.includes("✘") ? "failed" : "unknown";
208
+ const status = block.includes("✔")
209
+ ? "enabled"
210
+ : block.includes("✘")
211
+ ? "failed"
212
+ : "unknown";
219
213
  plugins.push({ name, version, status, raw: block });
220
214
  }
221
215
  return plugins;
222
216
  }
223
217
 
224
- /** List MCPs via `claude mcp list`. Returns array of { name, status }. */
218
+ /**
219
+ * List MCP servers registered with the `claude` CLI. Returns array of
220
+ * { name, plugin, fullName, status, command }
221
+ * where `plugin` is set when the MCP came from a plugin (real name is
222
+ * `plugin:<plugin>:<mcp>`), `name` is the trailing segment, and `fullName`
223
+ * is the original as reported by claude.
224
+ *
225
+ * Fixture captured from `claude mcp list` (2.1.117):
226
+ * Checking MCP server health…
227
+ *
228
+ * plugin:curdx-flow:context7: npx -y @upstash/context7-mcp@latest - ✓ Connected
229
+ * context7: npx -y @upstash/context7-mcp --api-key ... - ✓ Connected
230
+ * claude.ai Gmail: https://gmailmcp... - ✓ Connected
231
+ *
232
+ * `claude mcp list --json` does not exist on 2.1.117 (verified), so this
233
+ * parser is the primary path. It is fixture-tested in test/utils.test.js
234
+ * so format regressions get caught in CI.
235
+ */
225
236
  export function listMcps() {
226
237
  const res = runSync("claude", ["mcp", "list"]);
227
238
  if (res.code !== 0) return [];
228
- const lines = res.stdout.split("\n");
239
+ return parseMcpList(res.stdout);
240
+ }
241
+
242
+ /** Exported for testing against a fixed input. */
243
+ export function parseMcpList(output) {
229
244
  const mcps = [];
230
- for (const line of lines) {
231
- // Rough parse — adjust if format differs
232
- const m = line.match(/^\s*([a-z0-9-]+)\s*[:\-]/i);
233
- if (m) mcps.push({ name: m[1], status: "registered" });
245
+ for (const raw of output.split("\n")) {
246
+ const line = raw.trimEnd();
247
+ if (!line) continue;
248
+ // skip the health-check header line
249
+ if (line.startsWith("Checking") || line.startsWith("checking")) continue;
250
+ // Expected format: "<fullName>: <command-or-url> - <status>"
251
+ // fullName may itself contain colons when prefixed with "plugin:<p>:<m>".
252
+ // Match from the end to find the status sentinel " - ", then split off
253
+ // the name at the first ": " after the identifier prefix.
254
+ const statusSplit = line.lastIndexOf(" - ");
255
+ if (statusSplit === -1) continue;
256
+ const statusRaw = line.slice(statusSplit + 3).trim();
257
+ const beforeStatus = line.slice(0, statusSplit);
258
+ // Find the first ": " that separates name from command. Note the space
259
+ // after the colon — this disambiguates from the colons inside
260
+ // "plugin:foo:bar".
261
+ const nameSplit = beforeStatus.indexOf(": ");
262
+ if (nameSplit === -1) continue;
263
+ const fullName = beforeStatus.slice(0, nameSplit).trim();
264
+ const command = beforeStatus.slice(nameSplit + 2).trim();
265
+
266
+ let plugin = null;
267
+ let name = fullName;
268
+ if (fullName.startsWith("plugin:")) {
269
+ const parts = fullName.split(":");
270
+ if (parts.length >= 3) {
271
+ plugin = parts[1];
272
+ name = parts.slice(2).join(":");
273
+ }
274
+ }
275
+
276
+ const status = /Connected|✓/.test(statusRaw)
277
+ ? "connected"
278
+ : /Failed|✗/.test(statusRaw)
279
+ ? "failed"
280
+ : "unknown";
281
+
282
+ mcps.push({ name, plugin, fullName, status, command });
234
283
  }
235
284
  return mcps;
236
285
  }
237
286
 
238
- // ---------- Paths ----------
239
- export function pluginCacheDir(pluginName = "curdx-flow", marketplace = "curdx-flow-marketplace") {
240
- return `${process.env.HOME}/.claude/plugins/cache/${marketplace}/${pluginName}`;
241
- }
242
-
243
287
  // ---------- Runtime PATH guards (bun / uv) ----------
244
288
  // claude-mem hard-codes `command: "bun"` in its .mcp.json, but bun installs to
245
289
  // ~/.bun/bin which is not on PATH when Claude Code spawns MCP servers
@@ -247,10 +291,14 @@ export function pluginCacheDir(pluginName = "curdx-flow", marketplace = "curdx-f
247
291
  // detection + self-healing: create a symlink to the user-level bun install
248
292
  // in a PATH-visible directory.
249
293
 
250
- import { mkdirSync, symlinkSync, lstatSync, unlinkSync, readlinkSync } from "node:fs";
251
- // `existsSync` and `join` already imported at the top of this file.
294
+ import { existsSync, mkdirSync, symlinkSync, lstatSync, unlinkSync, readlinkSync } from "node:fs";
295
+ import { homedir } from "node:os";
296
+ // `join` already imported at the top of this file.
252
297
 
253
- const HOME = process.env.HOME || "";
298
+ // os.homedir() is sourced from the OS-level user record and works even
299
+ // when $HOME is empty (non-login shells, some CI containers). See the
300
+ // same rationale in cli/protocols.js.
301
+ const HOME = homedir();
254
302
 
255
303
  /** Candidate bun install locations (priority order) */
256
304
  const BUN_CANDIDATES = [
package/commands/fast.md CHANGED
@@ -123,6 +123,6 @@ Choosing the right scenario matters more than forcing the flow.
123
123
  ## Forbidden
124
124
 
125
125
  - ✗ Committing without running verification
126
- - ✗ Changes touching more than 5 files (means it is no longer fast — run the full flow)
126
+ - ✗ Changes touching many unrelated files or modules (means it is no longer fast — run the full flow)
127
127
  - ✗ Writing library APIs from memory
128
128
  - ✗ Skipping the Step 2 5-question clarification (even when "obvious," explicit statement still has value)
@@ -15,7 +15,7 @@ Execute spec tasks per tasks.md. Select the best execution strategy based on arg
15
15
  ## Step 1: Preflight Checks
16
16
 
17
17
  ```bash
18
- [ ! -d ".flow" ] && { echo " Not a CurDX-Flow project. Run /curdx-flow:init first"; exit 1; }
18
+ [ ! -d ".flow" ] && { echo " Not a CurDX-Flow project. Run /curdx-flow:init first"; exit 1; }
19
19
 
20
20
  ARGS="$ARGUMENTS"
21
21
  SPEC_NAME=""
@@ -35,10 +35,10 @@ for arg in $ARGS; do
35
35
  done
36
36
 
37
37
  [ -z "$SPEC_NAME" ] && SPEC_NAME=$(cat .flow/.active-spec 2>/dev/null)
38
- [ -z "$SPEC_NAME" ] && { echo " No active spec. Run /curdx-flow:start first"; exit 1; }
38
+ [ -z "$SPEC_NAME" ] && { echo " No active spec. Run /curdx-flow:start first"; exit 1; }
39
39
 
40
40
  DIR=".flow/specs/$SPEC_NAME"
41
- [ ! -f "$DIR/tasks.md" ] && { echo " Missing tasks.md. Run /curdx-flow:spec first (or /curdx-flow:spec --phase=tasks to rebuild just the tasks phase)"; exit 1; }
41
+ [ ! -f "$DIR/tasks.md" ] && { echo " Missing tasks.md. Run /curdx-flow:spec first (or /curdx-flow:spec --phase=tasks to rebuild just the tasks phase)"; exit 1; }
42
42
  ```
43
43
 
44
44
  ## Step 2: Parse Task Characteristics from tasks.md
@@ -330,7 +330,7 @@ Prerequisites:
330
330
 
331
331
  ## Step 6: Progress Feedback
332
332
 
333
- Every 5 tasks or every wave, print status:
333
+ At each wave boundary (or periodically during long linear runs), print status:
334
334
 
335
335
  ```
336
336
  ═════ Progress ═════
package/commands/init.md CHANGED
@@ -71,9 +71,20 @@ Append (if not already present):
71
71
 
72
72
  ### Step 5: Health Check
73
73
 
74
- Run `npx @curdx/flow doctor` (or inline its checks) to verify:
75
- - 3 MCPs started (context7 / sequential-thinking / chrome-devtools)
76
- - Recommended plugins status (pua / claude-mem / frontend-design)
74
+ Do NOT shell out to a new terminal for this step — you are already inside
75
+ Claude Code. Verify inline via the information the plugin already has:
76
+
77
+ - Read `~/.claude/plugins/data/curdx-flow/.deps-checked` (optional — the
78
+ SessionStart hook already refreshes this once per day).
79
+ - If the user asks for the full report, suggest they run
80
+ `npx @curdx/flow doctor` in a separate terminal — don't try to spawn
81
+ it from inside the Claude Code session (output won't render cleanly
82
+ and the user has to alt-tab to see it).
83
+
84
+ Items the CLI doctor covers (for user reference):
85
+ - 2 bundled MCPs (context7 / sequential-thinking) — visible in `claude mcp list`
86
+ - 4 recommended plugins (pua / claude-mem / frontend-design / chrome-devtools-mcp)
87
+ - Runtime PATH guards for `bun` / `uv` (relevant only when claude-mem is installed)
77
88
 
78
89
  ### Step 6: Prompt Next Steps
79
90
 
@@ -16,8 +16,8 @@ Distinct from `/curdx-flow:verify`:
16
16
  | Flag | Default | Purpose |
17
17
  |------|---------|---------|
18
18
  | `--stage=<1\|2\|both>` | `both` | Stage 1 = spec compliance only. Stage 2 = code quality only. `both` = sequential. |
19
- | `--adversarial` | off | Add an adversarial review pass (6 dimensions × 2 sequential-thinking rounds). Zero-findings forbidden. |
20
- | `--edge-case` | off | Add edge-case hunting across the 7 categories. Produces a test-gap checklist. |
19
+ | `--adversarial` | off | Add an adversarial review pass across applicable categories (zero findings requires proof-of-checking, not fabrication). |
20
+ | `--edge-case` | off | Add edge-case hunting across applicable categories. Produces a test-gap checklist. |
21
21
 
22
22
  ## Preflight
23
23
 
@@ -65,7 +65,7 @@ Output: Stage-2 section of the report.
65
65
  ## Optional: adversarial review
66
66
 
67
67
  If `--adversarial`:
68
- Dispatch `flow-adversary`. It runs 6 dimensions × 2 rounds of `sequential-thinking`:
68
+ Dispatch `flow-adversary`. It scans the applicable categories (Architecture / Implementation / Testing / Security / Maintainability / UX — skip N/A with reason) using `sequential-thinking` proportional to the residual uncertainty, probing:
69
69
  1. What's missing?
70
70
  2. What's overengineered?
71
71
  3. What would break first in production?
@@ -73,12 +73,12 @@ Dispatch `flow-adversary`. It runs 6 dimensions × 2 rounds of `sequential-think
73
73
  5. What decision locks us out of a future option?
74
74
  6. What would a skeptical reviewer reject?
75
75
 
76
- **Zero findings are forbidden** — if the agent reports "all good", re-dispatch with stronger skepticism. Per `@${CLAUDE_PLUGIN_ROOT}/gates/adversarial-review-gate.md`.
76
+ **Zero findings requires proof-of-checking, not fabrication** — honest "clean" verdicts are fine if the agent lists what it examined. Per `@${CLAUDE_PLUGIN_ROOT}/gates/adversarial-review-gate.md`.
77
77
 
78
78
  ## Optional: edge-case hunting
79
79
 
80
80
  If `--edge-case`:
81
- Dispatch `flow-edge-hunter` across the 7 categories:
81
+ Dispatch `flow-edge-hunter` across the applicable categories (skip N/A with one-line reason):
82
82
  1. Boundary values (0, MAX, empty, one-over-limit)
83
83
  2. Concurrency / race conditions
84
84
  3. Network failure / partial failure
@@ -91,6 +91,15 @@ Output: test-gap checklist with suggested test cases.
91
91
 
92
92
  ## Report
93
93
 
94
+ **Landing check**: sub-agent responses can be truncated. After dispatching review agents, verify the report actually landed on disk:
95
+
96
+ ```bash
97
+ REPORT=".flow/specs/$SPEC_NAME/review-report.md"
98
+ if [ ! -f "$REPORT" ] || [ "$(wc -c < "$REPORT" 2>/dev/null | tr -d ' ')" -lt 300 ]; then
99
+ echo "⚠ Report missing or truncated. Re-dispatching flow-reviewer with a terse 'Write the report now, no narration' prompt."
100
+ fi
101
+ ```
102
+
94
103
  Consolidated output: `.flow/specs/$SPEC_NAME/review-report.md`:
95
104
 
96
105
  ```markdown
package/commands/spec.md CHANGED
@@ -82,7 +82,7 @@ Output: `requirements.md` with user stories (US-NN), acceptance criteria (AC-N.N
82
82
 
83
83
  ### design → `flow-architect`
84
84
  Inputs: `research.md` + `requirements.md`.
85
- Output: `design.md` with architecture decisions (AD-NN), component boundaries, data models, error-path design, mermaid diagrams. Must use `sequential-thinking` MCP (≥8 thoughts).
85
+ Output: `design.md` with architecture decisions (AD-NN), component boundaries, data models, error-path design, mermaid diagrams (when they clarify). Uses `sequential-thinking` MCP proportional to the genuine tradeoff surface.
86
86
 
87
87
  ### tasks → `flow-planner`
88
88
  Inputs: all three prior files + `.flow/PROJECT.md` tech stack.
@@ -94,10 +94,34 @@ After each phase completes successfully, update `.state.json`:
94
94
  {
95
95
  "phase": "<just-completed-phase>",
96
96
  "phase_status": { "<phase>": "completed" },
97
- "updated_at": "<ISO8601 timestamp>"
97
+ "updated": "<ISO8601 timestamp>"
98
98
  }
99
99
  ```
100
100
 
101
+ ### Artifact landing check (mandatory after every phase)
102
+
103
+ Sub-agent responses can be truncated by the model's output-length limit, which means the `Write` tool call for the phase's Markdown artifact may never fire. Do NOT trust the agent's return value alone — always verify the file actually landed.
104
+
105
+ For each phase just dispatched, run:
106
+
107
+ ```bash
108
+ ARTIFACT=".flow/specs/$SPEC_NAME/<phase>.md"
109
+ if [ ! -f "$ARTIFACT" ]; then
110
+ echo "⚠ $ARTIFACT did not land. Re-dispatching <phase> agent with an explicit 'write the file' prompt."
111
+ # Re-dispatch the same agent, but in the prompt, front-load:
112
+ # "Your ONLY job is to call the Write tool with the full <phase>.md content now.
113
+ # Do not explain. Do not narrate. Write the file and stop."
114
+ # This pattern produces an artifact even when prior verbosity caused truncation.
115
+ fi
116
+
117
+ # Minimum-size sanity check — if the file is <500 bytes, the write likely truncated
118
+ if [ -f "$ARTIFACT" ] && [ "$(wc -c < "$ARTIFACT" | tr -d ' ')" -lt 500 ]; then
119
+ echo "⚠ $ARTIFACT looks truncated (<500 bytes). Re-dispatching to complete it."
120
+ fi
121
+ ```
122
+
123
+ Only advance `.state.json.phase` after both the file exists AND passes the size sanity check. If a re-dispatch also fails to produce the artifact, stop and surface the issue to the user instead of silently advancing — that prevents later phases from consuming an empty upstream file.
124
+
101
125
  ## Optional planning review
102
126
 
103
127
  If `--review` (or `--review=<dims>`) is present: