@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.
Files changed (78) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +19 -2
  3. package/README.md +15 -8
  4. package/README.zh.md +5 -3
  5. package/agent-preamble/preamble.md +33 -0
  6. package/agents/flow-adversary.md +1 -1
  7. package/agents/flow-architect.md +2 -1
  8. package/agents/flow-brownfield-analyst.md +153 -0
  9. package/agents/flow-debugger.md +6 -11
  10. package/agents/flow-edge-hunter.md +1 -1
  11. package/agents/flow-executor.md +30 -8
  12. package/agents/flow-planner.md +38 -5
  13. package/agents/flow-product-designer.md +2 -1
  14. package/agents/flow-qa-engineer.md +9 -5
  15. package/agents/flow-researcher.md +2 -1
  16. package/agents/flow-reviewer.md +23 -5
  17. package/agents/flow-security-auditor.md +5 -3
  18. package/agents/flow-triage-analyst.md +5 -24
  19. package/agents/flow-ui-researcher.md +4 -3
  20. package/agents/flow-ux-designer.md +12 -39
  21. package/agents/flow-verifier.md +35 -3
  22. package/cli/README.md +3 -1
  23. package/cli/doctor-workflow.js +1074 -2
  24. package/cli/doctor.js +8 -0
  25. package/cli/help.js +2 -0
  26. package/cli/lib/doctor-report.js +256 -1
  27. package/cli/lib/frontmatter.js +44 -0
  28. package/cli/lib/json-schema.js +57 -0
  29. package/cli/lib/runtime.js +20 -2
  30. package/cli/utils.js +6 -1
  31. package/gates/adversarial-review-gate.md +1 -1
  32. package/gates/security-gate.md +2 -2
  33. package/gates/test-quality-gate.md +59 -0
  34. package/hooks/hooks.json +16 -2
  35. package/hooks/scripts/common.sh +4 -0
  36. package/hooks/scripts/session-start.sh +17 -2
  37. package/hooks/scripts/stop-watcher.sh +69 -18
  38. package/hooks/scripts/subagent-artifact-guard.sh +159 -0
  39. package/hooks/scripts/subagent-statusline.sh +105 -0
  40. package/knowledge/atomic-commits.md +1 -1
  41. package/knowledge/claude-code-runtime-contracts.md +203 -0
  42. package/knowledge/epic-decomposition.md +1 -1
  43. package/knowledge/execution-strategies.md +23 -1
  44. package/knowledge/planning-reviews.md +2 -2
  45. package/knowledge/poc-first-workflow.md +8 -8
  46. package/knowledge/review-feedback-intake.md +57 -0
  47. package/knowledge/two-stage-review.md +19 -6
  48. package/knowledge/wave-execution.md +16 -1
  49. package/output-styles/curdx-evidence-first.md +34 -0
  50. package/package.json +7 -1
  51. package/schemas/agent-frontmatter.schema.json +0 -7
  52. package/schemas/config.schema.json +14 -0
  53. package/schemas/hooks.schema.json +34 -2
  54. package/schemas/output-style-frontmatter.schema.json +22 -0
  55. package/schemas/plugin-manifest.schema.json +387 -17
  56. package/schemas/plugin-settings.schema.json +29 -0
  57. package/schemas/skill-frontmatter.schema.json +109 -4
  58. package/schemas/spec-state.schema.json +29 -4
  59. package/settings.json +6 -0
  60. package/skills/brownfield-index/SKILL.md +31 -35
  61. package/skills/browser-qa/SKILL.md +11 -3
  62. package/skills/cancel/SKILL.md +82 -0
  63. package/skills/debug/SKILL.md +6 -2
  64. package/skills/epic/SKILL.md +5 -3
  65. package/skills/fast/SKILL.md +1 -0
  66. package/skills/help/SKILL.md +17 -7
  67. package/skills/implement/SKILL.md +38 -7
  68. package/skills/init/SKILL.md +2 -1
  69. package/skills/review/SKILL.md +4 -1
  70. package/skills/security-audit/SKILL.md +17 -3
  71. package/skills/spec/SKILL.md +2 -1
  72. package/skills/start/SKILL.md +18 -18
  73. package/skills/status/SKILL.md +85 -0
  74. package/skills/ui-sketch/SKILL.md +11 -3
  75. package/skills/verify/SKILL.md +13 -1
  76. package/templates/config.json.tmpl +4 -1
  77. package/templates/progress.md.tmpl +19 -0
  78. 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
 
@@ -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
- [`add export PATH="${dir}:$PATH" to your shell rc`]
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
+ }
@@ -52,15 +52,18 @@ function findSymlinkDir() {
52
52
  return null;
53
53
  }
54
54
 
55
- export function ensureRuntimeInPath(cmd, candidates) {
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 { ensureClaudeMemRuntimes, ensureRuntimeInPath } from "./lib/runtime.js";
35
+ export {
36
+ ensureClaudeMemRuntimes,
37
+ ensureRuntimeInPath,
38
+ inspectClaudeMemRuntimes,
39
+ inspectRuntimeInPath,
40
+ } from "./lib/runtime.js";
@@ -17,7 +17,7 @@ depends_on: []
17
17
 
18
18
  - /curdx-flow:review command
19
19
  - Before Phase transitions (requirements → design, design → tasks)
20
- - Before code merge (/curdx-flow:ship)
20
+ - Before code merge or human PR/release handoff
21
21
  - Enabled by default in Enterprise mode
22
22
 
23
23
  ---
@@ -14,7 +14,7 @@ depends_on: []
14
14
  ## Trigger Timing
15
15
 
16
16
  - When the `security-audit` skill runs
17
- - Before `/curdx-flow:ship` (auto-triggered, Phase 6+)
17
+ - Before human PR/release handoff, after `/curdx-flow:verify` and `/curdx-flow:review`
18
18
  - When committing specs involving auth / payments / PII
19
19
 
20
20
  ---
@@ -154,7 +154,7 @@ pnpm audit
154
154
 
155
155
  ### Blocking Items
156
156
 
157
- - If SR-01 ~ SR-05 are found → block immediately, prohibit `/curdx-flow:ship`
157
+ - If SR-01 ~ SR-05 are found → block immediately; do not hand off for PR/release
158
158
  - Must fix or explicitly exempt (record in STATE.md as tech debt + commitment to fix before release)
159
159
 
160
160
  ### Warning Items
@@ -0,0 +1,59 @@
1
+ ---
2
+ gate: test-quality-gate
3
+ category: standard-mode
4
+ severity: blocking
5
+ depends_on: []
6
+ ---
7
+
8
+ # Test Quality Gate
9
+
10
+ A green test suite is not enough. Tests must exercise real behavior and fail for the right reason.
11
+
12
+ ## Blocking Findings
13
+
14
+ Flag as blocking when a test is the only evidence for an FR/AC and any of these hold:
15
+
16
+ 1. **Mock-only behavior**
17
+ - Assertions only check mock calls (`toHaveBeenCalled`, `calledWith`, spy counts).
18
+ - The real module/function under test is never invoked.
19
+ - The test would still pass if the production implementation were empty.
20
+
21
+ 2. **Mock setup dominates evidence**
22
+ - Mock/stub/spy setup lines are more than 3x real behavioral assertions.
23
+ - The test mostly restates fixture wiring instead of asserting output, state, persistence, or user-visible behavior.
24
+
25
+ 3. **Skipped or inert tests**
26
+ - `it.skip`, `describe.skip`, `test.skip`, `xit`, `pending`, or equivalent on covered behavior.
27
+ - Test has no assertions and no meaningful side-effect check.
28
+
29
+ 4. **Implementation-biased regression**
30
+ - Test was added after implementation without evidence of RED failure when the task claims TDD.
31
+ - Test asserts internal private structure instead of externally observable behavior.
32
+
33
+ 5. **Missing cleanup for stateful mocks**
34
+ - Stateful mocks/spies are used across tests without `afterEach` cleanup (`restoreAllMocks`, `clearAllMocks`, sandbox restore, etc.).
35
+ - Shared mock state can leak between tests.
36
+
37
+ ## Acceptable Mock Usage
38
+
39
+ Mocks are acceptable when they isolate a boundary and the assertion still verifies real behavior:
40
+
41
+ - Network/payment/email provider mocked, but service logic and error handling are real.
42
+ - Clock/randomness mocked to make deterministic assertions.
43
+ - Database mocked only when a separate integration test covers persistence behavior.
44
+
45
+ ## Evidence Checklist
46
+
47
+ For each FR/AC test evidence, record:
48
+
49
+ - Test file and test name.
50
+ - What real code path is invoked.
51
+ - What behavioral assertion proves the requirement.
52
+ - Whether the test was observed RED before GREEN when TDD is claimed.
53
+ - Whether mocks are boundary-only or behavior-replacing.
54
+
55
+ ## Verdicts
56
+
57
+ - `PASS`: Tests exercise real behavior with meaningful assertions.
58
+ - `WARN`: Mock-heavy but supported by separate integration/e2e coverage.
59
+ - `FAIL`: Mock-only/skipped/no-assertion test is used as primary evidence.
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",
@@ -33,3 +33,7 @@ emit_stop_block() {
33
33
  local reason="${1:-}"
34
34
  printf '{"decision":"block","reason":%s}\n' "$(json_escape "$reason")"
35
35
  }
36
+
37
+ emit_subagentstop_block() {
38
+ emit_stop_block "${1:-}"
39
+ }
@@ -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. Emit hook output ----------
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