@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -10
- package/CHANGELOG.md +20 -0
- package/README.zh.md +2 -2
- package/agent-preamble/preamble.md +81 -11
- package/agents/flow-adversary.md +40 -55
- package/agents/flow-architect.md +23 -10
- package/agents/flow-debugger.md +2 -2
- package/agents/flow-edge-hunter.md +20 -6
- package/agents/flow-executor.md +3 -3
- package/agents/flow-planner.md +51 -48
- package/agents/flow-product-designer.md +14 -1
- package/agents/flow-qa-engineer.md +1 -1
- package/agents/flow-researcher.md +17 -2
- package/agents/flow-reviewer.md +5 -1
- package/agents/flow-security-auditor.md +1 -1
- package/agents/flow-triage-analyst.md +1 -1
- package/agents/flow-ui-researcher.md +2 -2
- package/agents/flow-ux-designer.md +1 -1
- package/agents/flow-verifier.md +47 -14
- package/bin/curdx-flow.js +13 -1
- package/cli/doctor.js +28 -13
- package/cli/install.js +62 -36
- package/cli/protocols.js +63 -10
- package/cli/registry.js +73 -0
- package/cli/uninstall.js +9 -11
- package/cli/upgrade.js +6 -10
- package/cli/utils.js +104 -56
- package/commands/fast.md +1 -1
- package/commands/implement.md +4 -4
- package/commands/init.md +14 -3
- package/commands/review.md +14 -5
- package/commands/spec.md +26 -2
- package/commands/start.md +47 -17
- package/commands/verify.md +13 -0
- package/gates/adversarial-review-gate.md +19 -19
- package/gates/devex-gate.md +4 -5
- package/gates/edge-case-gate.md +1 -1
- package/hooks/hooks.json +0 -11
- package/hooks/scripts/quick-mode-guard.sh +12 -9
- package/hooks/scripts/session-start.sh +1 -1
- package/hooks/scripts/stop-watcher.sh +25 -15
- package/knowledge/execution-strategies.md +6 -5
- package/knowledge/spec-driven-development.md +8 -7
- package/knowledge/two-stage-review.md +4 -3
- package/package.json +4 -2
- package/skills/brownfield-index/SKILL.md +62 -0
- package/skills/browser-qa/SKILL.md +50 -0
- package/skills/epic/SKILL.md +68 -0
- package/skills/security-audit/SKILL.md +50 -0
- package/skills/ui-sketch/SKILL.md +49 -0
- package/templates/config.json.tmpl +1 -1
- package/templates/design.md.tmpl +32 -112
- package/templates/requirements.md.tmpl +25 -43
- package/templates/research.md.tmpl +37 -68
- package/templates/tasks.md.tmpl +27 -84
- 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 {
|
|
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
|
-
|
|
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,
|
|
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)
|
|
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)
|
|
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
|
-
|
|
85
|
-
return { action: existing.length === 0 ? "created" : "
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
atomicWrite(path, next);
|
|
123
176
|
return { action: "removed", path };
|
|
124
177
|
}
|
package/cli/registry.js
ADDED
|
@@ -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 =
|
|
21
|
+
const HOME = homedir();
|
|
20
22
|
|
|
21
|
-
//
|
|
22
|
-
const RECOMMENDED =
|
|
23
|
-
|
|
24
|
-
|
|
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("
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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("✔")
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
package/commands/implement.md
CHANGED
|
@@ -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 "
|
|
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 "
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
package/commands/review.md
CHANGED
|
@@ -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
|
|
20
|
-
| `--edge-case` | off | Add edge-case hunting across
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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:
|