@curdx/flow 2.2.0 → 2.2.3
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 +2 -2
- package/.claude-plugin/plugin.json +19 -2
- package/README.md +15 -8
- package/README.zh.md +5 -3
- package/agent-preamble/preamble.md +33 -0
- package/agents/flow-adversary.md +1 -1
- package/agents/flow-architect.md +2 -1
- package/agents/flow-brownfield-analyst.md +153 -0
- package/agents/flow-debugger.md +6 -11
- package/agents/flow-edge-hunter.md +1 -1
- package/agents/flow-executor.md +30 -8
- package/agents/flow-planner.md +38 -5
- package/agents/flow-product-designer.md +2 -1
- package/agents/flow-qa-engineer.md +9 -5
- package/agents/flow-researcher.md +2 -1
- package/agents/flow-reviewer.md +23 -5
- package/agents/flow-security-auditor.md +5 -3
- package/agents/flow-triage-analyst.md +5 -24
- package/agents/flow-ui-researcher.md +4 -3
- package/agents/flow-ux-designer.md +12 -39
- package/agents/flow-verifier.md +35 -3
- package/cli/README.md +3 -1
- package/cli/doctor-workflow.js +1074 -2
- package/cli/doctor.js +8 -0
- package/cli/help.js +2 -0
- package/cli/lib/doctor-report.js +256 -1
- package/cli/lib/frontmatter.js +44 -0
- package/cli/lib/json-schema.js +57 -0
- package/cli/lib/runtime.js +20 -2
- package/cli/utils.js +6 -1
- package/gates/adversarial-review-gate.md +1 -1
- package/gates/security-gate.md +2 -2
- package/gates/test-quality-gate.md +59 -0
- package/hooks/hooks.json +16 -2
- package/hooks/scripts/common.sh +4 -0
- package/hooks/scripts/session-start.sh +17 -2
- package/hooks/scripts/stop-watcher.sh +69 -18
- package/hooks/scripts/subagent-artifact-guard.sh +159 -0
- package/hooks/scripts/subagent-statusline.sh +105 -0
- package/knowledge/atomic-commits.md +1 -1
- package/knowledge/claude-code-runtime-contracts.md +203 -0
- package/knowledge/epic-decomposition.md +1 -1
- package/knowledge/execution-strategies.md +23 -1
- package/knowledge/planning-reviews.md +2 -2
- package/knowledge/poc-first-workflow.md +8 -8
- package/knowledge/review-feedback-intake.md +57 -0
- package/knowledge/two-stage-review.md +19 -6
- package/knowledge/wave-execution.md +16 -1
- package/output-styles/curdx-evidence-first.md +34 -0
- package/package.json +7 -1
- package/schemas/agent-frontmatter.schema.json +0 -7
- package/schemas/config.schema.json +14 -0
- package/schemas/hooks.schema.json +34 -2
- package/schemas/output-style-frontmatter.schema.json +22 -0
- package/schemas/plugin-manifest.schema.json +387 -17
- package/schemas/plugin-settings.schema.json +29 -0
- package/schemas/skill-frontmatter.schema.json +109 -4
- package/schemas/spec-state.schema.json +29 -4
- package/settings.json +6 -0
- package/skills/brownfield-index/SKILL.md +31 -35
- package/skills/browser-qa/SKILL.md +11 -3
- package/skills/cancel/SKILL.md +82 -0
- package/skills/debug/SKILL.md +6 -2
- package/skills/epic/SKILL.md +5 -3
- package/skills/fast/SKILL.md +1 -0
- package/skills/help/SKILL.md +17 -7
- package/skills/implement/SKILL.md +38 -7
- package/skills/init/SKILL.md +2 -1
- package/skills/review/SKILL.md +4 -1
- package/skills/security-audit/SKILL.md +17 -3
- package/skills/spec/SKILL.md +2 -1
- package/skills/start/SKILL.md +18 -18
- package/skills/status/SKILL.md +85 -0
- package/skills/ui-sketch/SKILL.md +11 -3
- package/skills/verify/SKILL.md +13 -1
- package/templates/config.json.tmpl +4 -1
- package/templates/progress.md.tmpl +19 -0
- package/templates/tasks.md.tmpl +26 -3
package/cli/doctor.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "./utils.js";
|
|
9
9
|
import { buildDoctorReport } from "./lib/doctor-report.js";
|
|
10
10
|
import {
|
|
11
|
+
applyDoctorFixes,
|
|
11
12
|
collectDoctorData,
|
|
12
13
|
createDoctorContext,
|
|
13
14
|
printDoctorSummary,
|
|
@@ -21,6 +22,13 @@ export async function doctor(args = []) {
|
|
|
21
22
|
log.title("🏥 CurdX-Flow Health Check");
|
|
22
23
|
|
|
23
24
|
const doctorData = await collectDoctorData();
|
|
25
|
+
if (context.fix) {
|
|
26
|
+
log.info("Applying safe fixes...");
|
|
27
|
+
const fixes = await applyDoctorFixes(doctorData);
|
|
28
|
+
if (fixes.length === 0) {
|
|
29
|
+
log.info("No automatic fixes available for the current environment");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
24
32
|
const report = buildDoctorReport(doctorData);
|
|
25
33
|
|
|
26
34
|
renderReportLines(report.lines);
|
package/cli/help.js
CHANGED
|
@@ -17,6 +17,8 @@ ${color.bold("COMMANDS")}
|
|
|
17
17
|
when the plugin body is bundled)
|
|
18
18
|
|
|
19
19
|
${color.cyan("doctor")} Check health (claude CLI, plugin, MCPs, recommended)
|
|
20
|
+
--fix Apply safe runtime fixes (bun/uv PATH symlinks)
|
|
21
|
+
--verbose Show raw plugin list details
|
|
20
22
|
|
|
21
23
|
${color.cyan("upgrade")} Update curdx-flow and recommended plugins to latest
|
|
22
24
|
|
package/cli/lib/doctor-report.js
CHANGED
|
@@ -12,6 +12,110 @@ function pluginErrorDetails(plugin) {
|
|
|
12
12
|
return Array.isArray(plugin?.errors) ? plugin.errors : [];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function projectSettingsWarningDetails(warning) {
|
|
16
|
+
if (warning?.scope === "local") {
|
|
17
|
+
if (warning.kind === "invalid-local-setting") {
|
|
18
|
+
return [
|
|
19
|
+
"settings.local.json is the highest-precedence repo-scoped settings surface on this machine",
|
|
20
|
+
"fix the JSON shape or remove the local override if Claude behaves unexpectedly",
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (warning.kind === "required-plugin-disabled") {
|
|
25
|
+
return [
|
|
26
|
+
"settings.local.json overrides project and user plugin preferences on this machine",
|
|
27
|
+
"remove the false entry or enable the required companion plugin locally",
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (warning.kind === "flow-runtime-blocker") {
|
|
32
|
+
return [
|
|
33
|
+
"settings.local.json has higher precedence than .claude/settings.json and can break CurDX-Flow only on this machine",
|
|
34
|
+
"remove the local blocker or add explicit exceptions for curdx-flow workflows",
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
"settings.local.json overrides shared project settings on this machine",
|
|
40
|
+
"fix or remove the local override if Claude behaves differently from the rest of the team",
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!warning?.kind) {
|
|
45
|
+
return [
|
|
46
|
+
"project settings are shared with collaborators",
|
|
47
|
+
"prefer deny rules for .env/secrets and avoid bypassPermissions defaults",
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (warning.kind === "ignored-project-setting" || warning.kind === "managed-only-setting") {
|
|
52
|
+
return [
|
|
53
|
+
"Claude Code will ignore this at project scope or only honor it from managed settings",
|
|
54
|
+
"move it to user settings, settings.local.json, or managed settings as appropriate",
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (warning.kind === "invalid-project-setting") {
|
|
59
|
+
return [
|
|
60
|
+
"Claude Code expects this setting to follow the official settings.json shape",
|
|
61
|
+
"fix the value shape or remove the key from shared project settings",
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (warning.kind === "required-plugin-disabled") {
|
|
66
|
+
return [
|
|
67
|
+
"project enabledPlugins has higher precedence than user plugin preferences",
|
|
68
|
+
"remove the false entry or enable the required companion plugin for this project",
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (
|
|
73
|
+
warning.kind === "shared-script-setting" ||
|
|
74
|
+
warning.kind === "shared-env-setting" ||
|
|
75
|
+
warning.kind === "shared-mcp-auto-approve" ||
|
|
76
|
+
warning.kind === "shared-hook-policy" ||
|
|
77
|
+
warning.kind === "shared-sandbox-policy"
|
|
78
|
+
) {
|
|
79
|
+
return [
|
|
80
|
+
"project settings are shared with collaborators",
|
|
81
|
+
"avoid shared settings that run scripts, inject env vars, narrow hooks, or change sandbox behavior for every collaborator",
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (warning.kind === "skill-shell-disabled") {
|
|
86
|
+
return [
|
|
87
|
+
"Claude Code replaces inline skill shell output with a disabled placeholder when this policy is active",
|
|
88
|
+
"keep it only if your team intentionally bans dynamic shell-backed skill content",
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (warning.kind === "low-effort-project-setting") {
|
|
93
|
+
return [
|
|
94
|
+
"project effortLevel applies to main-thread planning and review turns",
|
|
95
|
+
"prefer high/xhigh for CurDX-Flow planning and verification-heavy workflows",
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (warning.kind === "flow-runtime-blocker") {
|
|
100
|
+
return [
|
|
101
|
+
"CurDX-Flow relies on Claude Code hooks, Agent dispatch, AskUserQuestion, Monitor plus Bash/Read/Edit tooling, and sonnet/opus model aliases",
|
|
102
|
+
"move restrictive policy to a narrower scope or add explicit exceptions for curdx-flow workflows",
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (warning.kind === "deprecated-setting") {
|
|
107
|
+
return [
|
|
108
|
+
"deprecated settings are still accepted for compatibility but should be migrated",
|
|
109
|
+
"prefer the current official replacement before the old key is removed",
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return [
|
|
114
|
+
"project settings are shared with collaborators",
|
|
115
|
+
"prefer deny rules for .env/secrets and avoid bypassPermissions defaults",
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
15
119
|
export function buildDoctorReport({
|
|
16
120
|
claudeVersionValue,
|
|
17
121
|
nodeVersion,
|
|
@@ -20,8 +124,12 @@ export function buildDoctorReport({
|
|
|
20
124
|
mcps = [],
|
|
21
125
|
userMcpConfig,
|
|
22
126
|
runtimeStatus,
|
|
127
|
+
runtimeEnvironment,
|
|
23
128
|
cwd,
|
|
24
129
|
projectState,
|
|
130
|
+
projectMcpConfig,
|
|
131
|
+
projectTeamConfig,
|
|
132
|
+
projectClaudeSettings,
|
|
25
133
|
}) {
|
|
26
134
|
const lines = [];
|
|
27
135
|
const sections = [];
|
|
@@ -191,6 +299,16 @@ export function buildDoctorReport({
|
|
|
191
299
|
for (const [name, status] of Object.entries(runtimeStatus)) {
|
|
192
300
|
if (status.status === "ok") {
|
|
193
301
|
pushSectionLine(runtimeSection, "ok", `${name.padEnd(22)} visible on PATH`);
|
|
302
|
+
} else if (status.status === "linkable") {
|
|
303
|
+
pushSectionLine(
|
|
304
|
+
runtimeSection,
|
|
305
|
+
"warn",
|
|
306
|
+
`${name.padEnd(22)} installed but not on PATH`,
|
|
307
|
+
[
|
|
308
|
+
`detected at ${status.path}`,
|
|
309
|
+
"run: npx @curdx/flow doctor --fix",
|
|
310
|
+
]
|
|
311
|
+
);
|
|
194
312
|
} else if (status.status === "linked") {
|
|
195
313
|
pushSectionLine(runtimeSection, "ok", `${name.padEnd(22)} auto-linked ${status.link} → ${status.path}`);
|
|
196
314
|
} else if (status.status === "missing") {
|
|
@@ -206,12 +324,27 @@ export function buildDoctorReport({
|
|
|
206
324
|
runtimeSection,
|
|
207
325
|
"err",
|
|
208
326
|
`${name.padEnd(22)} installed but not on PATH`,
|
|
209
|
-
[
|
|
327
|
+
[
|
|
328
|
+
`add export PATH="${dir}:$PATH" to your shell rc`,
|
|
329
|
+
"then rerun: npx @curdx/flow doctor",
|
|
330
|
+
]
|
|
210
331
|
);
|
|
211
332
|
}
|
|
212
333
|
}
|
|
213
334
|
}
|
|
214
335
|
|
|
336
|
+
if (runtimeEnvironment?.entries?.length > 0) {
|
|
337
|
+
const runtimeEnvSection = createSection("Runtime environment:");
|
|
338
|
+
for (const entry of runtimeEnvironment.entries) {
|
|
339
|
+
pushSectionLine(
|
|
340
|
+
runtimeEnvSection,
|
|
341
|
+
entry.level || "info",
|
|
342
|
+
entry.text,
|
|
343
|
+
entry.details || []
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
215
348
|
const localProjectSection = createSection("Local project:");
|
|
216
349
|
if (projectState?.exists) {
|
|
217
350
|
pushSectionLine(localProjectSection, "ok", `.flow/ ${cwd}`);
|
|
@@ -224,5 +357,127 @@ export function buildDoctorReport({
|
|
|
224
357
|
pushSectionLine(localProjectSection, "info", ".flow/ not a curdx-flow project (run: curdx-flow init)");
|
|
225
358
|
}
|
|
226
359
|
|
|
360
|
+
const projectMcpSection = createSection("Project MCP config:");
|
|
361
|
+
if (projectMcpConfig?.misplacedExists) {
|
|
362
|
+
pushSectionLine(
|
|
363
|
+
projectMcpSection,
|
|
364
|
+
projectMcpConfig.exists ? "warn" : "err",
|
|
365
|
+
`.claude/.mcp.json ignored by Claude Code`,
|
|
366
|
+
[
|
|
367
|
+
"project MCP config must live at repo root as .mcp.json",
|
|
368
|
+
"move .claude/.mcp.json → .mcp.json, then reopen /mcp or rerun doctor",
|
|
369
|
+
]
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (projectMcpConfig?.exists) {
|
|
374
|
+
if (projectMcpConfig.invalid) {
|
|
375
|
+
pushSectionLine(
|
|
376
|
+
projectMcpSection,
|
|
377
|
+
"err",
|
|
378
|
+
`.mcp.json invalid JSON`,
|
|
379
|
+
[
|
|
380
|
+
projectMcpConfig.parseError,
|
|
381
|
+
"fix the JSON syntax, then run /mcp or npx @curdx/flow doctor again",
|
|
382
|
+
]
|
|
383
|
+
);
|
|
384
|
+
} else if (projectMcpConfig.shapeError) {
|
|
385
|
+
pushSectionLine(
|
|
386
|
+
projectMcpSection,
|
|
387
|
+
"err",
|
|
388
|
+
`.mcp.json unsupported shape`,
|
|
389
|
+
[
|
|
390
|
+
projectMcpConfig.shapeError,
|
|
391
|
+
'expected: { "mcpServers": { "<name>": { ... } } }',
|
|
392
|
+
]
|
|
393
|
+
);
|
|
394
|
+
} else {
|
|
395
|
+
pushSectionLine(
|
|
396
|
+
projectMcpSection,
|
|
397
|
+
"ok",
|
|
398
|
+
`.mcp.json ${projectMcpConfig.serverCount} server(s) declared`
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
for (const warning of projectMcpConfig.relativePathWarnings || []) {
|
|
402
|
+
pushSectionLine(
|
|
403
|
+
projectMcpSection,
|
|
404
|
+
"warn",
|
|
405
|
+
`${warning.serverName.padEnd(22)} relative path in ${warning.field}`,
|
|
406
|
+
[
|
|
407
|
+
`value: ${warning.value}`,
|
|
408
|
+
"Claude Code resolves relative MCP paths against the launch directory, not .mcp.json",
|
|
409
|
+
"use an absolute path or a PATH executable such as npx / uvx",
|
|
410
|
+
"debug: claude --debug mcp",
|
|
411
|
+
]
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} else if (!projectMcpConfig?.misplacedExists) {
|
|
416
|
+
pushSectionLine(projectMcpSection, "info", ".mcp.json not present");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const projectTeamsSection = createSection("Project agent teams:");
|
|
420
|
+
if (projectTeamConfig?.exists) {
|
|
421
|
+
pushSectionLine(
|
|
422
|
+
projectTeamsSection,
|
|
423
|
+
"warn",
|
|
424
|
+
`.claude/teams/teams.json ignored by Claude Code`,
|
|
425
|
+
[
|
|
426
|
+
"official agent-teams docs say project directories do not have a recognized team config surface",
|
|
427
|
+
"remove the file or move team configuration to the supported user-level agent-teams runtime",
|
|
428
|
+
]
|
|
429
|
+
);
|
|
430
|
+
} else {
|
|
431
|
+
pushSectionLine(projectTeamsSection, "info", ".claude/teams/teams.json not present");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const projectSettingsSection = createSection("Project Claude settings:");
|
|
435
|
+
if (projectClaudeSettings?.exists) {
|
|
436
|
+
if (projectClaudeSettings.invalid) {
|
|
437
|
+
pushSectionLine(
|
|
438
|
+
projectSettingsSection,
|
|
439
|
+
"err",
|
|
440
|
+
`.claude/settings.json invalid JSON`,
|
|
441
|
+
[projectClaudeSettings.parseError]
|
|
442
|
+
);
|
|
443
|
+
} else if ((projectClaudeSettings.warnings || []).length > 0) {
|
|
444
|
+
pushSectionLine(projectSettingsSection, "warn", ".claude/settings.json needs review");
|
|
445
|
+
for (const warning of projectClaudeSettings.warnings) {
|
|
446
|
+
pushSectionLine(
|
|
447
|
+
projectSettingsSection,
|
|
448
|
+
"warn",
|
|
449
|
+
warning.message,
|
|
450
|
+
projectSettingsWarningDetails(warning)
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
pushSectionLine(projectSettingsSection, "ok", ".claude/settings.json conservative");
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
pushSectionLine(projectSettingsSection, "info", ".claude/settings.json not present");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (projectClaudeSettings?.localExists) {
|
|
461
|
+
pushSectionLine(projectSettingsSection, "info", ".claude/settings.local.json present (local overrides)");
|
|
462
|
+
if (projectClaudeSettings.localInvalid) {
|
|
463
|
+
pushSectionLine(
|
|
464
|
+
projectSettingsSection,
|
|
465
|
+
"err",
|
|
466
|
+
".claude/settings.local.json invalid JSON",
|
|
467
|
+
[projectClaudeSettings.localParseError]
|
|
468
|
+
);
|
|
469
|
+
} else if ((projectClaudeSettings.localWarnings || []).length > 0) {
|
|
470
|
+
pushSectionLine(projectSettingsSection, "warn", ".claude/settings.local.json affects the local runtime");
|
|
471
|
+
for (const warning of projectClaudeSettings.localWarnings) {
|
|
472
|
+
pushSectionLine(
|
|
473
|
+
projectSettingsSection,
|
|
474
|
+
"warn",
|
|
475
|
+
warning.message,
|
|
476
|
+
projectSettingsWarningDetails(warning)
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
227
482
|
return { lines, sections, errors, warnings };
|
|
228
483
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
|
|
4
|
+
function formatYamlErrors(errors) {
|
|
5
|
+
return errors.map((error) => error.message).join("; ");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function extractFrontmatterBlock(text, sourceLabel = "frontmatter") {
|
|
9
|
+
const match = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
10
|
+
if (!match) {
|
|
11
|
+
throw new Error(`${sourceLabel}: missing YAML frontmatter`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return match[1];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseFrontmatterBlock(block, sourceLabel = "frontmatter") {
|
|
18
|
+
const document = YAML.parseDocument(block, {
|
|
19
|
+
strict: true,
|
|
20
|
+
uniqueKeys: true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (document.errors.length > 0) {
|
|
24
|
+
throw new Error(`${sourceLabel}: invalid YAML (${formatYamlErrors(document.errors)})`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const value = document.toJS();
|
|
28
|
+
if (value == null) return {};
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
30
|
+
throw new Error(`${sourceLabel}: frontmatter must parse to an object`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readFrontmatter(filePath) {
|
|
37
|
+
const text = readFileSync(filePath, "utf-8");
|
|
38
|
+
const block = extractFrontmatterBlock(text, filePath);
|
|
39
|
+
return parseFrontmatterBlock(block, filePath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readFrontmatterFields(filePath) {
|
|
43
|
+
return Object.keys(readFrontmatter(filePath));
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import Ajv from "ajv";
|
|
4
|
+
|
|
5
|
+
const ajv = new Ajv({
|
|
6
|
+
allErrors: true,
|
|
7
|
+
allowUnionTypes: true,
|
|
8
|
+
strict: false,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const validatorCache = new Map();
|
|
12
|
+
|
|
13
|
+
function formatParams(params = {}) {
|
|
14
|
+
if (params.missingProperty) return ` missingProperty=${params.missingProperty}`;
|
|
15
|
+
if (params.additionalProperty) return ` additionalProperty=${params.additionalProperty}`;
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readJsonFile(filePath) {
|
|
20
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatAjvErrors(errors = []) {
|
|
24
|
+
return errors.map((error) => {
|
|
25
|
+
const where = error.instancePath || error.schemaPath || "(root)";
|
|
26
|
+
return `${where}: ${error.message}${formatParams(error.params)}`;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getSchemaValidator(schemaPath) {
|
|
31
|
+
const absPath = resolve(schemaPath);
|
|
32
|
+
if (validatorCache.has(absPath)) return validatorCache.get(absPath);
|
|
33
|
+
|
|
34
|
+
const schema = readJsonFile(absPath);
|
|
35
|
+
const validate = ajv.compile(schema);
|
|
36
|
+
const entry = { absPath, schema, validate };
|
|
37
|
+
validatorCache.set(absPath, entry);
|
|
38
|
+
return entry;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateAgainstSchemaFile(schemaPath, data) {
|
|
42
|
+
const { validate } = getSchemaValidator(schemaPath);
|
|
43
|
+
const valid = Boolean(validate(data));
|
|
44
|
+
return {
|
|
45
|
+
valid,
|
|
46
|
+
errors: valid ? [] : formatAjvErrors(validate.errors),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validateSchemaFile(schemaPath) {
|
|
51
|
+
const schema = readJsonFile(resolve(schemaPath));
|
|
52
|
+
const valid = ajv.validateSchema(schema);
|
|
53
|
+
return {
|
|
54
|
+
valid,
|
|
55
|
+
errors: valid ? [] : formatAjvErrors(ajv.errors),
|
|
56
|
+
};
|
|
57
|
+
}
|
package/cli/lib/runtime.js
CHANGED
|
@@ -52,15 +52,18 @@ function findSymlinkDir() {
|
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
function getRuntimeStatus(cmd, candidates, { repair = false } = {}) {
|
|
56
56
|
if (has(cmd)) return { status: "ok" };
|
|
57
|
-
|
|
58
57
|
const realPath = findRuntime(candidates);
|
|
59
58
|
if (!realPath) return { status: "missing" };
|
|
60
59
|
|
|
61
60
|
const linkDir = findSymlinkDir();
|
|
62
61
|
if (!linkDir) return { status: "path-unwritable", path: realPath };
|
|
63
62
|
|
|
63
|
+
if (!repair) {
|
|
64
|
+
return { status: "linkable", path: realPath, link: join(linkDir, cmd) };
|
|
65
|
+
}
|
|
66
|
+
|
|
64
67
|
const linkPath = join(linkDir, cmd);
|
|
65
68
|
if (existsSync(linkPath)) {
|
|
66
69
|
try {
|
|
@@ -81,6 +84,21 @@ export function ensureRuntimeInPath(cmd, candidates) {
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
export function inspectRuntimeInPath(cmd, candidates) {
|
|
88
|
+
return getRuntimeStatus(cmd, candidates, { repair: false });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function ensureRuntimeInPath(cmd, candidates) {
|
|
92
|
+
return getRuntimeStatus(cmd, candidates, { repair: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function inspectClaudeMemRuntimes() {
|
|
96
|
+
return {
|
|
97
|
+
bun: inspectRuntimeInPath("bun", BUN_CANDIDATES),
|
|
98
|
+
uv: inspectRuntimeInPath("uv", UV_CANDIDATES),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
export function ensureClaudeMemRuntimes() {
|
|
85
103
|
return {
|
|
86
104
|
bun: ensureRuntimeInPath("bun", BUN_CANDIDATES),
|
package/cli/utils.js
CHANGED
|
@@ -32,4 +32,9 @@ export {
|
|
|
32
32
|
parsePluginListJson,
|
|
33
33
|
readUserMcpConfig,
|
|
34
34
|
} from "./lib/claude.js";
|
|
35
|
-
export {
|
|
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
|
|
20
|
+
- Before code merge or human PR/release handoff
|
|
21
21
|
- Enabled by default in Enterprise mode
|
|
22
22
|
|
|
23
23
|
---
|
package/gates/security-gate.md
CHANGED
|
@@ -14,7 +14,7 @@ depends_on: []
|
|
|
14
14
|
## Trigger Timing
|
|
15
15
|
|
|
16
16
|
- When the `security-audit` skill runs
|
|
17
|
-
- Before `/curdx-flow:
|
|
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
|
|
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.
|
package/hooks/hooks.json
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"hooks": [
|
|
6
6
|
{
|
|
7
7
|
"type": "command",
|
|
8
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/session-start.sh"
|
|
8
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/session-start.sh",
|
|
9
|
+
"statusMessage": "Loading CurDX-Flow project context"
|
|
9
10
|
}
|
|
10
11
|
]
|
|
11
12
|
},
|
|
@@ -14,7 +15,8 @@
|
|
|
14
15
|
"hooks": [
|
|
15
16
|
{
|
|
16
17
|
"type": "command",
|
|
17
|
-
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/inject-karpathy.sh"
|
|
18
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/inject-karpathy.sh",
|
|
19
|
+
"statusMessage": "Injecting CurDX-Flow engineering baseline"
|
|
18
20
|
}
|
|
19
21
|
]
|
|
20
22
|
}
|
|
@@ -29,6 +31,18 @@
|
|
|
29
31
|
]
|
|
30
32
|
}
|
|
31
33
|
],
|
|
34
|
+
"SubagentStop": [
|
|
35
|
+
{
|
|
36
|
+
"matcher": "flow-(architect|brownfield-analyst|debugger|edge-hunter|executor|product-designer|planner|qa-engineer|researcher|reviewer|security-auditor|triage-analyst|ui-researcher|ux-designer|verifier|adversary)",
|
|
37
|
+
"hooks": [
|
|
38
|
+
{
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/subagent-artifact-guard.sh",
|
|
41
|
+
"statusMessage": "Checking curdx-flow artifact landing"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
],
|
|
32
46
|
"PreToolUse": [
|
|
33
47
|
{
|
|
34
48
|
"matcher": "AskUserQuestion",
|
package/hooks/scripts/common.sh
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Duties:
|
|
4
4
|
# 1. Daily dependency check — nudge user to `npx @curdx/flow install --all` if recommended plugins missing
|
|
5
5
|
# 2. Load active spec progress into session context
|
|
6
|
+
# 3. Persist stable CurDX-Flow environment hints for this session
|
|
6
7
|
#
|
|
7
8
|
# Design notes:
|
|
8
9
|
# - Idempotent: marker file tracks last check date
|
|
@@ -45,12 +46,15 @@ if [ "$LAST_CHECK" != "$TODAY" ]; then
|
|
|
45
46
|
ADDITIONAL_CONTEXT+="## CurDX-Flow Recommended Plugins Check\n\nThe following recommended plugins were not detected: **${JOINED}**\n\nRun \`npx @curdx/flow install --all\` for interactive one-shot install. Run \`npx @curdx/flow doctor\` for the full health report.\n\n"
|
|
46
47
|
fi
|
|
47
48
|
|
|
48
|
-
echo "$TODAY" > "$MARKER" 2>/dev/null || true
|
|
49
|
+
{ echo "$TODAY" > "$MARKER"; } 2>/dev/null || true
|
|
49
50
|
fi
|
|
50
51
|
|
|
51
52
|
# ---------- 2. Load .flow/ state (if project is a flow project) ----------
|
|
52
53
|
if [ -d ".flow" ]; then
|
|
53
54
|
ADDITIONAL_CONTEXT+="## CurDX-Flow Project Active\n\n"
|
|
55
|
+
ADDITIONAL_CONTEXT+="- Plugin root: \`${CLAUDE_PLUGIN_ROOT:-unknown}\`\n"
|
|
56
|
+
ADDITIONAL_CONTEXT+="- Plugin data: \`${CLAUDE_PLUGIN_DATA:-$DATA_DIR}\`\n"
|
|
57
|
+
ADDITIONAL_CONTEXT+="- Best practice: write long agent artifacts to disk first; keep final assistant summaries short.\n\n"
|
|
54
58
|
|
|
55
59
|
if [ -f ".flow/PROJECT.md" ]; then
|
|
56
60
|
ADDITIONAL_CONTEXT+="### Project Vision\n$(head -80 .flow/PROJECT.md)\n\n"
|
|
@@ -67,7 +71,18 @@ if [ -d ".flow" ]; then
|
|
|
67
71
|
fi
|
|
68
72
|
fi
|
|
69
73
|
|
|
70
|
-
# ---------- 3.
|
|
74
|
+
# ---------- 3. Persist session environment hints ----------
|
|
75
|
+
if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
|
|
76
|
+
{
|
|
77
|
+
printf 'export CURDX_FLOW_PLUGIN_ROOT=%s\n' "$(json_escape "${CLAUDE_PLUGIN_ROOT:-}")"
|
|
78
|
+
printf 'export CURDX_FLOW_PLUGIN_DATA=%s\n' "$(json_escape "${CLAUDE_PLUGIN_DATA:-$DATA_DIR}")"
|
|
79
|
+
if [ -f ".flow/.active-spec" ]; then
|
|
80
|
+
printf 'export CURDX_FLOW_ACTIVE_SPEC=%s\n' "$(json_escape "$(cat .flow/.active-spec 2>/dev/null)")"
|
|
81
|
+
fi
|
|
82
|
+
} >> "$CLAUDE_ENV_FILE" 2>/dev/null || true
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# ---------- 4. Emit hook output ----------
|
|
71
86
|
if [ -n "$ADDITIONAL_CONTEXT" ]; then
|
|
72
87
|
emit_session_start_context "$ADDITIONAL_CONTEXT"
|
|
73
88
|
fi
|