@curdx/flow 2.1.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 +25 -2
- package/.claude-plugin/plugin.json +27 -1
- package/CHANGELOG.md +32 -0
- package/README.md +18 -8
- package/README.zh.md +8 -3
- package/agent-preamble/preamble.md +35 -2
- 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 +25 -20
- 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 +6 -5
- package/agents/flow-ux-designer.md +12 -39
- package/agents/flow-verifier.md +38 -6
- package/bin/curdx-flow +5 -0
- package/cli/README.md +13 -10
- package/cli/doctor-workflow.js +1074 -2
- package/cli/doctor.js +8 -0
- package/cli/help.js +2 -0
- package/cli/install-companions.js +4 -1
- package/cli/install-required-plugins.js +18 -5
- package/cli/install-self-update.js +2 -91
- package/cli/install.js +12 -1
- package/cli/lib/claude.js +42 -11
- package/cli/lib/doctor-report.js +303 -9
- package/cli/lib/frontmatter.js +44 -0
- package/cli/lib/json-schema.js +57 -0
- package/cli/lib/runtime.js +20 -2
- package/cli/lib/semver.js +95 -0
- package/cli/utils.js +7 -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/quick-mode-guard.sh +6 -7
- 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 +28 -6
- package/knowledge/planning-reviews.md +4 -4
- 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 +33 -18
- package/output-styles/curdx-evidence-first.md +34 -0
- package/package.json +9 -2
- package/schemas/agent-frontmatter.schema.json +59 -0
- package/schemas/config.schema.json +37 -3
- package/schemas/gate-frontmatter.schema.json +30 -0
- package/schemas/hooks.schema.json +115 -0
- package/schemas/output-style-frontmatter.schema.json +22 -0
- package/schemas/plugin-manifest.schema.json +436 -0
- package/schemas/plugin-settings.schema.json +29 -0
- package/schemas/skill-frontmatter.schema.json +177 -0
- package/schemas/spec-state.schema.json +35 -5
- package/settings.json +6 -0
- package/skills/brownfield-index/SKILL.md +33 -36
- package/skills/browser-qa/SKILL.md +16 -7
- package/skills/cancel/SKILL.md +82 -0
- package/skills/debug/SKILL.md +7 -2
- package/skills/epic/SKILL.md +7 -4
- package/skills/fast/SKILL.md +3 -1
- package/skills/help/SKILL.md +18 -7
- package/skills/implement/SKILL.md +44 -12
- package/skills/implement/references/wave-execution.md +9 -9
- package/skills/init/SKILL.md +3 -1
- package/skills/review/SKILL.md +6 -2
- package/skills/security-audit/SKILL.md +19 -4
- package/skills/spec/SKILL.md +6 -4
- package/skills/start/SKILL.md +20 -19
- package/skills/status/SKILL.md +85 -0
- package/skills/ui-sketch/SKILL.md +13 -4
- package/skills/verify/SKILL.md +15 -2
- package/templates/CONTEXT.md.tmpl +1 -1
- package/templates/PROJECT.md.tmpl +1 -1
- package/templates/config.json.tmpl +9 -6
- package/templates/progress.md.tmpl +21 -2
- package/templates/tasks.md.tmpl +26 -3
|
@@ -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),
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
function normalizeVersionToken(token) {
|
|
2
|
+
return /^\d+$/.test(token) ? Number(token) : token;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function parseVersion(version) {
|
|
6
|
+
const normalized = String(version || "").trim().replace(/^v/i, "");
|
|
7
|
+
const [coreRaw = "0", prereleaseRaw] = normalized.split("-", 2);
|
|
8
|
+
const core = coreRaw.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
9
|
+
const prerelease = prereleaseRaw
|
|
10
|
+
? prereleaseRaw.split(/[.-]/).filter(Boolean).map(normalizeVersionToken)
|
|
11
|
+
: [];
|
|
12
|
+
|
|
13
|
+
return { core, prerelease };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function compareIdentifier(left, right) {
|
|
17
|
+
if (left === right) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const leftIsNumber = typeof left === "number";
|
|
22
|
+
const rightIsNumber = typeof right === "number";
|
|
23
|
+
|
|
24
|
+
if (leftIsNumber && rightIsNumber) {
|
|
25
|
+
return left > right ? 1 : -1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (leftIsNumber) {
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (rightIsNumber) {
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return left > right ? 1 : -1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function compareVersions(leftVersion, rightVersion) {
|
|
40
|
+
const left = parseVersion(leftVersion);
|
|
41
|
+
const right = parseVersion(rightVersion);
|
|
42
|
+
const coreLength = Math.max(left.core.length, right.core.length);
|
|
43
|
+
|
|
44
|
+
for (let index = 0; index < coreLength; index += 1) {
|
|
45
|
+
const leftPart = left.core[index] ?? 0;
|
|
46
|
+
const rightPart = right.core[index] ?? 0;
|
|
47
|
+
if (leftPart !== rightPart) {
|
|
48
|
+
return leftPart > rightPart ? 1 : -1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const leftHasPrerelease = left.prerelease.length > 0;
|
|
53
|
+
const rightHasPrerelease = right.prerelease.length > 0;
|
|
54
|
+
|
|
55
|
+
if (!leftHasPrerelease && !rightHasPrerelease) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!leftHasPrerelease) {
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!rightHasPrerelease) {
|
|
64
|
+
return -1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const prereleaseLength = Math.max(left.prerelease.length, right.prerelease.length);
|
|
68
|
+
for (let index = 0; index < prereleaseLength; index += 1) {
|
|
69
|
+
const leftPart = left.prerelease[index];
|
|
70
|
+
const rightPart = right.prerelease[index];
|
|
71
|
+
|
|
72
|
+
if (leftPart === undefined) {
|
|
73
|
+
return -1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (rightPart === undefined) {
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const comparison = compareIdentifier(leftPart, rightPart);
|
|
81
|
+
if (comparison !== 0) {
|
|
82
|
+
return comparison;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isVersionNewer(latestVersion, currentVersion) {
|
|
90
|
+
return compareVersions(latestVersion, currentVersion) > 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isVersionAtLeast(version, minimumVersion) {
|
|
94
|
+
return compareVersions(version, minimumVersion) >= 0;
|
|
95
|
+
}
|
package/cli/utils.js
CHANGED
|
@@ -29,6 +29,12 @@ export {
|
|
|
29
29
|
listPluginMarketplaces,
|
|
30
30
|
listPlugins,
|
|
31
31
|
parseMcpList,
|
|
32
|
+
parsePluginListJson,
|
|
32
33
|
readUserMcpConfig,
|
|
33
34
|
} from "./lib/claude.js";
|
|
34
|
-
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# CurDX-Flow PreToolUse Hook for AskUserQuestion
|
|
3
|
-
# Blocks AskUserQuestion when the active spec has quickMode=true
|
|
4
|
-
# This prevents
|
|
3
|
+
# Blocks AskUserQuestion when the active spec has quickMode=true.
|
|
4
|
+
# This prevents quick execution loops from stalling waiting for user input.
|
|
5
5
|
#
|
|
6
6
|
# The hook reads Claude's PreToolUse input JSON from stdin. We only act when
|
|
7
7
|
# the tool being invoked is AskUserQuestion.
|
|
@@ -34,7 +34,7 @@ if [ "$TOOL_NAME" != "AskUserQuestion" ]; then
|
|
|
34
34
|
exit 0
|
|
35
35
|
fi
|
|
36
36
|
|
|
37
|
-
# Check if we're in a flow project with quick mode enabled
|
|
37
|
+
# Check if we're in a flow project with quick mode enabled.
|
|
38
38
|
[ ! -d ".flow" ] && exit 0
|
|
39
39
|
|
|
40
40
|
ACTIVE=$(cat .flow/.active-spec 2>/dev/null)
|
|
@@ -43,7 +43,7 @@ ACTIVE=$(cat .flow/.active-spec 2>/dev/null)
|
|
|
43
43
|
STATE_FILE=".flow/specs/$ACTIVE/.state.json"
|
|
44
44
|
[ ! -f "$STATE_FILE" ] && exit 0
|
|
45
45
|
|
|
46
|
-
# Read quickMode
|
|
46
|
+
# Read quickMode. Pass STATE_FILE via env (NOT shell interpolation
|
|
47
47
|
# into the python source) so an active-spec name containing quotes/$ cannot
|
|
48
48
|
# inject python code.
|
|
49
49
|
export STATE_FILE
|
|
@@ -52,15 +52,14 @@ import json, os
|
|
|
52
52
|
try:
|
|
53
53
|
s = json.load(open(os.environ["STATE_FILE"]))
|
|
54
54
|
qm = s.get("quickMode", False)
|
|
55
|
-
|
|
56
|
-
print("true" if (qm or mode == "autonomous") else "false")
|
|
55
|
+
print("true" if qm else "false")
|
|
57
56
|
except Exception:
|
|
58
57
|
print("false")
|
|
59
58
|
' 2>/dev/null)
|
|
60
59
|
|
|
61
60
|
if [ "$QUICK_MODE" = "true" ]; then
|
|
62
61
|
# Block and inject guidance
|
|
63
|
-
MSG="[CurDX-Flow quick-mode-guard] Active spec '$ACTIVE' is in quick mode
|
|
62
|
+
MSG="[CurDX-Flow quick-mode-guard] Active spec '$ACTIVE' is in quick mode — AskUserQuestion is forbidden. Decide based on user preferences in .flow/CONTEXT.md plus the most reasonable assumption, and record your assumption in .progress.md."
|
|
64
63
|
emit_pretooluse_deny "$MSG"
|
|
65
64
|
exit 0
|
|
66
65
|
fi
|
|
@@ -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
|
|
@@ -57,7 +57,7 @@ fi
|
|
|
57
57
|
# the stop-hook strategy never activated.
|
|
58
58
|
export STATE_FILE
|
|
59
59
|
|
|
60
|
-
read STRATEGY PHASE TASK_INDEX TOTAL_TASKS FAILED ROUNDS <<EOF
|
|
60
|
+
read STRATEGY PHASE TASK_INDEX TOTAL_TASKS FAILED ROUNDS RECOVERY_MODE MAX_FIX_TASKS <<EOF
|
|
61
61
|
$(python3 <<'PY'
|
|
62
62
|
import json, os, sys
|
|
63
63
|
p = os.environ.get("STATE_FILE")
|
|
@@ -72,7 +72,9 @@ ti = ex.get("task_index", 0)
|
|
|
72
72
|
tt = ex.get("total_tasks", 0)
|
|
73
73
|
failed = ex.get("failed_attempts", 0)
|
|
74
74
|
rounds = ex.get("global_iteration", 0)
|
|
75
|
-
|
|
75
|
+
recovery_mode = ex.get("recovery_mode", "manual")
|
|
76
|
+
max_fix_tasks = ex.get("max_fix_tasks_per_original", 2)
|
|
77
|
+
print(strategy, phase, ti, tt, failed, rounds, recovery_mode, max_fix_tasks)
|
|
76
78
|
PY
|
|
77
79
|
)
|
|
78
80
|
EOF
|
|
@@ -81,7 +83,7 @@ EOF
|
|
|
81
83
|
[ "$STRATEGY" != "stop-hook" ] && allow_stop
|
|
82
84
|
[ "$PHASE" != "execute" ] && allow_stop
|
|
83
85
|
|
|
84
|
-
# ---------- 5. Check
|
|
86
|
+
# ---------- 5. Check hook input + completion signal in transcript ----------
|
|
85
87
|
# Claude Code passes transcript path via stdin as JSON: {"transcript_path": "/path/..."}
|
|
86
88
|
# We read stdin to detect ALL_TASKS_COMPLETE or TASK_FAILED
|
|
87
89
|
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
@@ -89,6 +91,19 @@ TRANSCRIPT_PATH=$(echo "$INPUT" | python3 -c 'import json,sys;
|
|
|
89
91
|
try: print(json.load(sys.stdin).get("transcript_path",""))
|
|
90
92
|
except: print("")' 2>/dev/null)
|
|
91
93
|
|
|
94
|
+
STOP_HOOK_ACTIVE=$(echo "$INPUT" | python3 -c 'import json,sys;
|
|
95
|
+
try: print("true" if json.load(sys.stdin).get("stop_hook_active", False) else "false")
|
|
96
|
+
except: print("false")' 2>/dev/null)
|
|
97
|
+
|
|
98
|
+
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
|
99
|
+
# Claude Code sets stop_hook_active during a stop-hook continuation.
|
|
100
|
+
# Treat it as context only: the final decision still comes from transcript
|
|
101
|
+
# signals, state-file progress, and tasks.md parity. Unconditionally allowing
|
|
102
|
+
# stop here can terminate an in-flight stop-hook loop after the first
|
|
103
|
+
# continuation, leaving remaining tasks stranded.
|
|
104
|
+
echo "[CurDX-Flow stop-hook] stop_hook_active=true; evaluating transcript/state before deciding" >&2
|
|
105
|
+
fi
|
|
106
|
+
|
|
92
107
|
TRANSCRIPT_TAIL=""
|
|
93
108
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
94
109
|
# Read last 50KB only (efficiency)
|
|
@@ -100,22 +115,54 @@ fi
|
|
|
100
115
|
# python source text. Previously a spec name containing single quotes or
|
|
101
116
|
# $-signs could break the script or inject arbitrary code.
|
|
102
117
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
# Helper: count unchecked tasks in tasks.md. If tasks.md is absent, return 0
|
|
119
|
+
# to avoid blocking recovery for partially-initialized specs.
|
|
120
|
+
unchecked_task_count() {
|
|
121
|
+
local tasks_file="$SPEC_DIR/tasks.md"
|
|
122
|
+
[ ! -f "$tasks_file" ] && { echo 0; return; }
|
|
123
|
+
grep -Ec '^- \[ \] \*\*[0-9]+(\.[0-9]+|\.VF|\.X|\.X\+1)*\*\*' "$tasks_file" 2>/dev/null || echo 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
last_task_signal() {
|
|
127
|
+
local msg="${1:-}"
|
|
128
|
+
printf '%s' "$msg" \
|
|
129
|
+
| grep -Eo 'ALL_TASKS_COMPLETE|TASK_(COMPLETE|FAILED):[[:space:]]*[0-9]+(\.([0-9]+|VF|X(\+[0-9]+)?))*' \
|
|
130
|
+
| tail -1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
failed_task_id() {
|
|
134
|
+
local msg="${1:-}"
|
|
135
|
+
printf '%s' "$msg" | sed -nE 's/.*TASK_FAILED:[[:space:]]*([0-9]+(\.([0-9]+|VF|X(\+[0-9]+)?))*).*/\1/p' | tail -1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
mark_execute_complete() {
|
|
106
139
|
python3 <<'PY' 2>/dev/null
|
|
107
140
|
import json, os
|
|
108
141
|
p = os.environ["STATE_FILE"]
|
|
109
142
|
s = json.load(open(p))
|
|
110
143
|
s.setdefault("phase_status", {})["execute"] = "completed"
|
|
111
|
-
s["phase"] = "verify"
|
|
144
|
+
s["phase"] = "verify"
|
|
112
145
|
json.dump(s, open(p, "w"), indent=2, ensure_ascii=False)
|
|
113
146
|
PY
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Check for explicit completion signals
|
|
150
|
+
LAST_TASK_SIGNAL="$(last_task_signal "$TRANSCRIPT_TAIL")"
|
|
151
|
+
|
|
152
|
+
if [ "$LAST_TASK_SIGNAL" = "ALL_TASKS_COMPLETE" ]; then
|
|
153
|
+
UNCHECKED="$(unchecked_task_count)"
|
|
154
|
+
if [ "${UNCHECKED:-0}" -gt 0 ]; then
|
|
155
|
+
block_continue "[CurDX-Flow stop-hook] ALL_TASKS_COMPLETE was emitted, but tasks.md still has ${UNCHECKED} unchecked task(s). Read .flow/specs/${ACTIVE}/tasks.md, complete only the remaining unchecked tasks, update tasks.md, then emit ALL_TASKS_COMPLETE again."
|
|
156
|
+
fi
|
|
157
|
+
mark_execute_complete
|
|
114
158
|
allow_stop
|
|
115
159
|
fi
|
|
116
160
|
|
|
117
|
-
# Check for fail signal (accumulate; actual stop decision below)
|
|
118
|
-
if
|
|
161
|
+
# Check for the latest fail signal (accumulate; actual stop decision below)
|
|
162
|
+
if printf '%s' "$LAST_TASK_SIGNAL" | grep -q "^TASK_FAILED"; then
|
|
163
|
+
FAILED_TASK="$(failed_task_id "$LAST_TASK_SIGNAL")"
|
|
164
|
+
[ -z "$FAILED_TASK" ] && FAILED_TASK="the current task"
|
|
165
|
+
|
|
119
166
|
# Increment failed_attempts
|
|
120
167
|
python3 <<'PY' 2>/dev/null
|
|
121
168
|
import json, os
|
|
@@ -127,6 +174,14 @@ json.dump(s, open(p, "w"), indent=2, ensure_ascii=False)
|
|
|
127
174
|
PY
|
|
128
175
|
# Re-read — again via os.environ, no shell interpolation into python.
|
|
129
176
|
FAILED=$(python3 -c 'import json, os; print(json.load(open(os.environ["STATE_FILE"]))["execute_state"]["failed_attempts"])' 2>/dev/null || echo 0)
|
|
177
|
+
|
|
178
|
+
if [ "${FAILED:-0}" -lt 3 ]; then
|
|
179
|
+
if [ "${RECOVERY_MODE:-manual}" = "fix-task" ]; then
|
|
180
|
+
block_continue "[CurDX-Flow stop-hook] TASK_FAILED observed for ${FAILED_TASK}. Do not skip it. Recovery mode is fix-task: insert one targeted [FIX ${FAILED_TASK}] task immediately after the failed task in tasks.md (max ${MAX_FIX_TASKS:-2} fix task(s) per original), update .state.json execute_state.fix_task_map, then execute the fix task before retrying ${FAILED_TASK}. The fix task must include Do, Files, Done when, Verify, and Commit fields."
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
block_continue "[CurDX-Flow stop-hook] TASK_FAILED observed for ${FAILED_TASK}. Do not advance past the failed task. Re-read tasks.md, perform root-cause analysis, retry the first unchecked task, and emit TASK_COMPLETE only after its Verify command passes. failed_attempts=${FAILED}/3."
|
|
184
|
+
fi
|
|
130
185
|
fi
|
|
131
186
|
|
|
132
187
|
# ---------- 6. Safety brakes ----------
|
|
@@ -142,15 +197,11 @@ fi
|
|
|
142
197
|
|
|
143
198
|
# Check if all tasks done
|
|
144
199
|
if [ "$TASK_INDEX" -ge "$TOTAL_TASKS" ] && [ "$TOTAL_TASKS" -gt 0 ]; then
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
s.setdefault("phase_status", {})["execute"] = "completed"
|
|
151
|
-
s["phase"] = "verify"
|
|
152
|
-
json.dump(s, open(p, "w"), indent=2, ensure_ascii=False)
|
|
153
|
-
PY
|
|
200
|
+
UNCHECKED="$(unchecked_task_count)"
|
|
201
|
+
if [ "${UNCHECKED:-0}" -gt 0 ]; then
|
|
202
|
+
block_continue "[CurDX-Flow stop-hook] State says execute is complete (${TASK_INDEX}/${TOTAL_TASKS}), but tasks.md still has ${UNCHECKED} unchecked task(s). Continue with the first unchecked task; do not add new tasks."
|
|
203
|
+
fi
|
|
204
|
+
mark_execute_complete
|
|
154
205
|
allow_stop
|
|
155
206
|
fi
|
|
156
207
|
|